# 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: `
`, FooterTemplate: `
Página de
`, } 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": "
Header
", "footerTemplate": "
Footer
", "preferCSSPageSize": false, "generateTaggedPDF": false } } // Response: { "data": "base64_encoded_pdf_data..." } ``` ## Variables en templates ### Header/Footer templates soportan: - `` - Fecha actual - `` - Título de la página - `` - URL de la página - `` - Número de página actual - `` - Total de páginas ### Ejemplo de template completo ```html
``` ## 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: `
Reporte generado:
`, FooterTemplate: `
/
`, } b.SavePDF(ctx, "reporte.pdf", opts) ``` ### PDF con contenido dinámico ```go // Generar contenido dinámico b.Evaluate(ctx, ` document.body.innerHTML = '

Reporte Dinámico

'; for (let i = 1; i <= 10; i++) { document.body.innerHTML += '

Elemento ' + i + '

'; } `) // 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