c165f2f788
Agrega 19 issues técnicas documentando funcionalidades implementadas y pendientes. Issues completadas (movidas a dev/issues/completed/): - 001-conversor-web-markdown.md - 002-accessibility-tree.md - 003-gestion-cookies-perfil.md - 004-gestion-extensiones-chrome.md - 005-eliminar-timeouts-innecesarios.md Issues implementadas: - 006-manejo-tabs-ventanas.md - 016-manejo-iframes.md - 017-actions-api.md - 018-file-uploads.md - 019-expected-conditions-mejoradas.md Issues pendientes (media prioridad): - 007-alert-prompt-confirm-handling.md - 008-screenshot-elementos-especificos.md - 009-pdf-generation.md - 010-device-emulation-completo.md - 011-downloads-handling.md Issues pendientes (baja prioridad / avanzado): - 012-browser-contexts-multi-sesion.md - 013-video-recording.md - 014-network-mocking-avanzado.md - 015-geolocation-permissions.md Incluye también dev/NUEVAS_FUNCIONALIDADES.md con resumen completo. Directorio: dev/
441 lines
11 KiB
Markdown
441 lines
11 KiB
Markdown
# Issue #009: PDF Generation
|
|
|
|
**Tipo**: Enhancement
|
|
**Prioridad**: Media
|
|
**Estado**: Pendiente
|
|
|
|
## Descripción
|
|
|
|
Implementar generación de PDFs de páginas web, similar a "Imprimir a PDF" del navegador.
|
|
|
|
## Funcionalidad deseada
|
|
|
|
### Operaciones básicas
|
|
- Generar PDF de página completa
|
|
- Generar PDF de página actual (viewport)
|
|
- Control de formato de página (A4, Letter, etc.)
|
|
- Orientación (portrait/landscape)
|
|
- Márgenes personalizables
|
|
- Headers y footers personalizados
|
|
- Background graphics (imágenes de fondo)
|
|
- Scale/zoom del contenido
|
|
|
|
### Operaciones avanzadas
|
|
- Rangos de páginas específicos
|
|
- Números de página
|
|
- Fecha/hora en header/footer
|
|
- CSS para medios de impresión
|
|
- Protección de PDF (opcional)
|
|
|
|
## Implementación técnica
|
|
|
|
### Archivo sugerido
|
|
`pkg/browser/pdf.go`
|
|
|
|
### CDP Method
|
|
**Page.printToPDF** - Genera PDF de la página
|
|
|
|
### API propuesta
|
|
|
|
```go
|
|
// PDFFormat formato de papel
|
|
type PDFFormat string
|
|
|
|
const (
|
|
PDFFormatA4 PDFFormat = "A4"
|
|
PDFFormatLetter PDFFormat = "Letter"
|
|
PDFFormatLegal PDFFormat = "Legal"
|
|
PDFFormatA3 PDFFormat = "A3"
|
|
PDFFormatTabloid PDFFormat = "Tabloid"
|
|
)
|
|
|
|
// PDFOrientation orientación de página
|
|
type PDFOrientation string
|
|
|
|
const (
|
|
PDFOrientationPortrait PDFOrientation = "portrait"
|
|
PDFOrientationLandscape PDFOrientation = "landscape"
|
|
)
|
|
|
|
// PDFMargins márgenes del PDF
|
|
type PDFMargins struct {
|
|
Top float64 // En pulgadas
|
|
Right float64
|
|
Bottom float64
|
|
Left float64
|
|
}
|
|
|
|
// PDFOptions opciones para generación de PDF
|
|
type PDFOptions struct {
|
|
// Formato de papel
|
|
Format PDFFormat // Default: A4
|
|
|
|
// Orientación
|
|
Orientation PDFOrientation // Default: portrait
|
|
|
|
// Dimensiones personalizadas (en pulgadas)
|
|
// Si se especifica, ignora Format
|
|
Width float64
|
|
Height float64
|
|
|
|
// Márgenes (en pulgadas)
|
|
Margins PDFMargins // Default: 1cm todos
|
|
|
|
// Scale del contenido (0.1 - 2.0)
|
|
Scale float64 // Default: 1.0
|
|
|
|
// Incluir colores y gráficos de fondo
|
|
PrintBackground bool // Default: false
|
|
|
|
// Rango de páginas (ej: "1-5, 8, 11-13")
|
|
PageRanges string
|
|
|
|
// Header template (HTML)
|
|
HeaderTemplate string
|
|
|
|
// Footer template (HTML)
|
|
FooterTemplate string
|
|
|
|
// Mostrar header y footer
|
|
DisplayHeaderFooter bool
|
|
|
|
// Preferir CSS para @media print
|
|
PreferCSSPageSize bool
|
|
|
|
// Generar PDFs etiquetados (accesibilidad)
|
|
GenerateTaggedPDF bool
|
|
}
|
|
|
|
// DefaultPDFOptions retorna opciones por defecto
|
|
func DefaultPDFOptions() *PDFOptions
|
|
|
|
// GeneratePDF genera un PDF de la página actual
|
|
func (b *Browser) GeneratePDF(ctx context.Context, opts *PDFOptions) ([]byte, error)
|
|
|
|
// SavePDF genera y guarda PDF a archivo
|
|
func (b *Browser) SavePDF(ctx context.Context, filepath string, opts *PDFOptions) error
|
|
|
|
// PrintToPDF genera PDF (alias de GeneratePDF)
|
|
func (b *Browser) PrintToPDF(ctx context.Context, opts *PDFOptions) ([]byte, error)
|
|
```
|
|
|
|
## Casos de uso
|
|
|
|
### Caso 1: PDF simple
|
|
```go
|
|
// PDF con opciones por defecto (A4, portrait)
|
|
pdf, _ := b.GeneratePDF(ctx, nil)
|
|
os.WriteFile("page.pdf", pdf, 0644)
|
|
```
|
|
|
|
### Caso 2: PDF con configuración personalizada
|
|
```go
|
|
opts := &browser.PDFOptions{
|
|
Format: browser.PDFFormatLetter,
|
|
Orientation: browser.PDFOrientationLandscape,
|
|
PrintBackground: true, // Incluir colores de fondo
|
|
Scale: 0.8, // 80% del tamaño
|
|
Margins: browser.PDFMargins{
|
|
Top: 0.5,
|
|
Right: 0.5,
|
|
Bottom: 0.5,
|
|
Left: 0.5,
|
|
},
|
|
}
|
|
|
|
pdf, _ := b.GeneratePDF(ctx, opts)
|
|
```
|
|
|
|
### Caso 3: PDF con header y footer
|
|
```go
|
|
opts := &browser.PDFOptions{
|
|
DisplayHeaderFooter: true,
|
|
HeaderTemplate: `
|
|
<div style="font-size: 10px; text-align: center; width: 100%;">
|
|
<span class="title"></span>
|
|
</div>
|
|
`,
|
|
FooterTemplate: `
|
|
<div style="font-size: 10px; text-align: center; width: 100%;">
|
|
Página <span class="pageNumber"></span> de <span class="totalPages"></span>
|
|
</div>
|
|
`,
|
|
}
|
|
|
|
pdf, _ := b.GeneratePDF(ctx, opts)
|
|
```
|
|
|
|
### Caso 4: PDF de rango específico
|
|
```go
|
|
opts := &browser.PDFOptions{
|
|
PageRanges: "1-3, 5", // Solo páginas 1, 2, 3 y 5
|
|
}
|
|
|
|
pdf, _ := b.GeneratePDF(ctx, opts)
|
|
```
|
|
|
|
### Caso 5: Guardar directamente a archivo
|
|
```go
|
|
opts := browser.DefaultPDFOptions()
|
|
opts.Format = browser.PDFFormatA4
|
|
opts.PrintBackground = true
|
|
|
|
b.SavePDF(ctx, "report.pdf", opts)
|
|
```
|
|
|
|
## Implementación interna
|
|
|
|
```go
|
|
func (b *Browser) GeneratePDF(ctx context.Context, opts *PDFOptions) ([]byte, error) {
|
|
if opts == nil {
|
|
opts = DefaultPDFOptions()
|
|
}
|
|
|
|
// Construir parámetros CDP
|
|
params := map[string]interface{}{
|
|
"printBackground": opts.PrintBackground,
|
|
"displayHeaderFooter": opts.DisplayHeaderFooter,
|
|
"preferCSSPageSize": opts.PreferCSSPageSize,
|
|
"generateTaggedPDF": opts.GenerateTaggedPDF,
|
|
}
|
|
|
|
// Formato o dimensiones custom
|
|
if opts.Width > 0 && opts.Height > 0 {
|
|
params["paperWidth"] = opts.Width
|
|
params["paperHeight"] = opts.Height
|
|
} else {
|
|
// Usar formato predefinido
|
|
params["format"] = string(opts.Format)
|
|
}
|
|
|
|
// Orientación
|
|
if opts.Orientation != "" {
|
|
params["landscape"] = opts.Orientation == PDFOrientationLandscape
|
|
}
|
|
|
|
// Márgenes
|
|
params["marginTop"] = opts.Margins.Top
|
|
params["marginRight"] = opts.Margins.Right
|
|
params["marginBottom"] = opts.Margins.Bottom
|
|
params["marginLeft"] = opts.Margins.Left
|
|
|
|
// Scale
|
|
if opts.Scale > 0 {
|
|
params["scale"] = opts.Scale
|
|
}
|
|
|
|
// Page ranges
|
|
if opts.PageRanges != "" {
|
|
params["pageRanges"] = opts.PageRanges
|
|
}
|
|
|
|
// Templates
|
|
if opts.HeaderTemplate != "" {
|
|
params["headerTemplate"] = opts.HeaderTemplate
|
|
}
|
|
if opts.FooterTemplate != "" {
|
|
params["footerTemplate"] = opts.FooterTemplate
|
|
}
|
|
|
|
// Ejecutar comando
|
|
var result struct {
|
|
Data string `json:"data"` // Base64
|
|
Stream string `json:"stream"` // Stream handle (para PDFs grandes)
|
|
}
|
|
|
|
if err := b.cdpClient.Execute(ctx, "Page.printToPDF", params, &result); err != nil {
|
|
return nil, fmt.Errorf("failed to generate PDF: %w", err)
|
|
}
|
|
|
|
// Decodificar base64
|
|
data, err := base64.StdEncoding.DecodeString(result.Data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode PDF: %w", err)
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func DefaultPDFOptions() *PDFOptions {
|
|
return &PDFOptions{
|
|
Format: PDFFormatA4,
|
|
Orientation: PDFOrientationPortrait,
|
|
Scale: 1.0,
|
|
Margins: PDFMargins{
|
|
Top: 0.4, // ~1cm
|
|
Right: 0.4,
|
|
Bottom: 0.4,
|
|
Left: 0.4,
|
|
},
|
|
PrintBackground: false,
|
|
}
|
|
}
|
|
```
|
|
|
|
## Comandos CDP
|
|
|
|
```json
|
|
{
|
|
"method": "Page.printToPDF",
|
|
"params": {
|
|
"landscape": false,
|
|
"displayHeaderFooter": true,
|
|
"printBackground": true,
|
|
"scale": 1,
|
|
"paperWidth": 8.5,
|
|
"paperHeight": 11,
|
|
"marginTop": 0.4,
|
|
"marginBottom": 0.4,
|
|
"marginLeft": 0.4,
|
|
"marginRight": 0.4,
|
|
"pageRanges": "1-5",
|
|
"headerTemplate": "<div>Header</div>",
|
|
"footerTemplate": "<div>Footer</div>",
|
|
"preferCSSPageSize": false,
|
|
"generateTaggedPDF": false
|
|
}
|
|
}
|
|
|
|
// Response:
|
|
{
|
|
"data": "base64_encoded_pdf_data..."
|
|
}
|
|
```
|
|
|
|
## Variables en templates
|
|
|
|
### Header/Footer templates soportan:
|
|
- `<span class="date"></span>` - Fecha actual
|
|
- `<span class="title"></span>` - Título de la página
|
|
- `<span class="url"></span>` - URL de la página
|
|
- `<span class="pageNumber"></span>` - Número de página actual
|
|
- `<span class="totalPages"></span>` - Total de páginas
|
|
|
|
### Ejemplo de template completo
|
|
```html
|
|
<div style="font-size: 10px; width: 100%; padding: 0 1cm;">
|
|
<div style="float: left;">
|
|
<span class="title"></span>
|
|
</div>
|
|
<div style="float: right;">
|
|
<span class="date"></span>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
## CSS para impresión
|
|
|
|
### Aplicar estilos específicos para PDF
|
|
```css
|
|
@media print {
|
|
.no-print {
|
|
display: none !important;
|
|
}
|
|
|
|
.page-break {
|
|
page-break-after: always;
|
|
}
|
|
|
|
body {
|
|
font-size: 12pt;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Inyectar CSS antes de generar PDF
|
|
```go
|
|
// Inyectar estilos de impresión
|
|
b.Evaluate(ctx, `
|
|
const style = document.createElement('style');
|
|
style.textContent = '@media print { .sidebar { display: none; } }';
|
|
document.head.appendChild(style);
|
|
`)
|
|
|
|
// Generar PDF
|
|
pdf, _ := b.GeneratePDF(ctx, opts)
|
|
```
|
|
|
|
## Casos de uso avanzados
|
|
|
|
### Generar reporte con múltiples páginas
|
|
```go
|
|
// Navegar a página de reporte
|
|
b.Navigate(ctx, "https://example.com/report", nil)
|
|
|
|
// Esperar a que cargue completamente
|
|
b.WaitForSelector(ctx, ".report-ready", nil)
|
|
|
|
// Generar PDF
|
|
opts := &browser.PDFOptions{
|
|
Format: browser.PDFFormatA4,
|
|
PrintBackground: true,
|
|
DisplayHeaderFooter: true,
|
|
HeaderTemplate: `<div style="font-size: 10px; text-align: right; width: 100%; padding-right: 1cm;">
|
|
Reporte generado: <span class="date"></span>
|
|
</div>`,
|
|
FooterTemplate: `<div style="font-size: 10px; text-align: center; width: 100%;">
|
|
<span class="pageNumber"></span> / <span class="totalPages"></span>
|
|
</div>`,
|
|
}
|
|
|
|
b.SavePDF(ctx, "reporte.pdf", opts)
|
|
```
|
|
|
|
### PDF con contenido dinámico
|
|
```go
|
|
// Generar contenido dinámico
|
|
b.Evaluate(ctx, `
|
|
document.body.innerHTML = '<h1>Reporte Dinámico</h1>';
|
|
for (let i = 1; i <= 10; i++) {
|
|
document.body.innerHTML += '<p>Elemento ' + i + '</p>';
|
|
}
|
|
`)
|
|
|
|
// Generar PDF
|
|
pdf, _ := b.GeneratePDF(ctx, nil)
|
|
```
|
|
|
|
### Batch PDF generation
|
|
```go
|
|
urls := []string{
|
|
"https://example.com/page1",
|
|
"https://example.com/page2",
|
|
"https://example.com/page3",
|
|
}
|
|
|
|
for i, url := range urls {
|
|
b.Navigate(ctx, url, nil)
|
|
b.WaitForNavigation(ctx, nil)
|
|
|
|
filename := fmt.Sprintf("page_%d.pdf", i+1)
|
|
b.SavePDF(ctx, filename, nil)
|
|
}
|
|
```
|
|
|
|
## Consideraciones
|
|
|
|
### Tamaño del PDF
|
|
- PDFs grandes pueden exceder límite de respuesta CDP
|
|
- Usar streaming para PDFs > 10MB (no implementado en v1)
|
|
|
|
### Performance
|
|
- Generación de PDF es **bloqueante**
|
|
- Puede tomar varios segundos para páginas grandes
|
|
- Considerar timeout apropiado
|
|
|
|
### Calidad
|
|
- Images embebidas mantienen su resolución
|
|
- Fonts pueden no incluirse (usar web fonts)
|
|
- JavaScript no se ejecuta durante generación
|
|
|
|
### Headless mode
|
|
- PDF generation funciona mejor en headless
|
|
- Algunas páginas pueden requerir modo visible
|
|
|
|
## Referencias
|
|
|
|
- CDP Page.printToPDF: https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF
|
|
- Chrome printing: https://developer.chrome.com/docs/chromium/print-previews
|
|
- Playwright PDF: https://playwright.dev/docs/api/class-page#page-pdf
|
|
- Puppeteer PDF: https://pptr.dev/api/puppeteer.page.pdf
|