Files
navegator/dev/issues/009-pdf-generation.md
T
Developer c165f2f788 docs: issues técnicas para nuevas funcionalidades
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/
2026-03-25 00:49:06 +01:00

11 KiB

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

// 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

// 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

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)
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

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

opts := browser.DefaultPDFOptions()
opts.Format = browser.PDFFormatA4
opts.PrintBackground = true

b.SavePDF(ctx, "report.pdf", opts)

Implementación interna

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

{
  "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

<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

@media print {
    .no-print {
        display: none !important;
    }

    .page-break {
        page-break-after: always;
    }

    body {
        font-size: 12pt;
    }
}

Inyectar CSS antes de generar PDF

// 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

// 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

// 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

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