Files
fn_registry/dev/issues/completed/0020-pdf-generation.md
T
2026-04-13 02:03:28 +02:00

14 KiB

0020 — PDF Generation

Metadata

Campo Valor
ID 0020
Estado pendiente
Prioridad media
Tipo feature

Dependencias

Ninguna.


Objetivo

Crear funciones para generar y construir PDFs programaticamente: reportes, facturas, exports de datos, conversiones desde HTML/Markdown. Complementa las funciones de lectura de PDF ya existentes (pdf_to_markdown_py_core, extract_pdf_text_py_core, extract_pdf_bookmarks_py_core) con la capacidad inversa de crear documentos.

Contexto

  • El registry tiene 3 funciones para leer PDFs y 2 pipelines relacionados (export_analysis_pdfs_bash_pipelines, notebook_to_pdf_bash_infra), pero cero funciones para generar PDFs desde datos o contenido.
  • Casos de uso frecuentes que hoy requieren codigo ad-hoc: generar reportes de ejecuciones, exportar tablas de datos como PDF, crear facturas, convertir documentacion markdown a PDF distribuible.
  • Python tiene las mejores librerias de generacion de PDF (reportlab, fpdf2, weasyprint) sin necesidad de binarios externos como wkhtmltopdf.
  • Go puede cubrir casos simples de reportes sin dependencias pesadas, usando go-pdf/fpdf.
  • El dominio natural es infra porque generar archivos es I/O, igual que las funciones de export/deploy existentes.

Arquitectura

Python (dominio infra) — generacion completa

python/functions/infra/
├── pdf_create.py               — NEW: crear documento PDF nuevo
├── pdf_create.md               — NEW
├── pdf_add_page.py             — NEW: añadir pagina con tamaño/orientacion
├── pdf_add_page.md             — NEW
├── pdf_add_text.py             — NEW: añadir bloque de texto con estilo
├── pdf_add_text.md             — NEW
├── pdf_add_table.py            — NEW: añadir tabla con datos, headers, anchos
├── pdf_add_table.md            — NEW
├── pdf_add_image.py            — NEW: añadir imagen desde archivo o bytes
├── pdf_add_image.md            — NEW
├── pdf_add_header_footer.py    — NEW: header/footer recurrente con numeros de pagina
├── pdf_add_header_footer.md    — NEW
├── pdf_from_html.py            — NEW: HTML string → PDF
├── pdf_from_html.md            — NEW
├── pdf_from_markdown.py        — NEW: Markdown → HTML → PDF
├── pdf_from_markdown.md        — NEW
├── pdf_merge.py                — NEW: fusionar multiples PDFs en uno
├── pdf_merge.md                — NEW
├── pdf_save.py                 — NEW: guardar PDF a archivo o retornar bytes
├── pdf_save.md                 — NEW

python/types/infra/
├── pdf_doc.py                  — NEW: tipo PDFDoc
├── pdf_doc.md                  — NEW
├── pdf_page.py                 — NEW: tipo PDFPage
├── pdf_page.md                 — NEW
├── pdf_style.py                — NEW: tipo PDFStyle
├── pdf_style.md                — NEW

Go (dominio infra) — reportes simples

functions/infra/
├── pdf_simple_report.go        — NEW: generar PDF simple desde titulo + secciones + tablas
├── pdf_simple_report.md        — NEW

Patron pure core / impure shell

Todas las funciones de generacion son impuras (escriben estado en un objeto documento mutable y/o hacen I/O a disco). No hay funciones puras en esta issue porque la generacion de PDFs es inherentemente stateful.

Los tipos (PDFDoc, PDFPage, PDFStyle) son product types que definen la estructura de datos, no tienen I/O.

Diseño

Tipos

@dataclass
class PDFStyle:
    font_family: str = "Helvetica"
    font_size: float = 12.0
    color: str = "#000000"          # hex color
    alignment: str = "left"         # left, center, right, justify
    line_height: float = 1.2
    margin_top: float = 0.0
    margin_bottom: float = 2.0

@dataclass
class PDFPage:
    width: float = 210.0            # mm (A4 default)
    height: float = 297.0           # mm (A4 default)
    orientation: str = "portrait"   # portrait, landscape

@dataclass
class PDFDoc:
    pages: list                     # lista de paginas con contenido
    title: str = ""
    author: str = ""
    subject: str = ""
    default_font: str = "Helvetica"
    margin_left: float = 20.0       # mm
    margin_right: float = 20.0
    margin_top: float = 20.0
    margin_bottom: float = 20.0

Funciones Python

Funcion ID Purity Firma (simplificada)
pdf_create pdf_create_py_infra impure (title, author, page_size, margins) -> PDFDoc
pdf_add_page pdf_add_page_py_infra impure (doc, orientation, width, height) -> PDFDoc
pdf_add_text pdf_add_text_py_infra impure (doc, text, style, x, y) -> PDFDoc
pdf_add_table pdf_add_table_py_infra impure (doc, headers, rows, col_widths, style) -> PDFDoc
pdf_add_image pdf_add_image_py_infra impure (doc, image_path_or_bytes, x, y, width, height) -> PDFDoc
pdf_add_header_footer pdf_add_header_footer_py_infra impure (doc, header_text, footer_text, page_numbers) -> PDFDoc
pdf_from_html pdf_from_html_py_infra impure (html_string, output_path, page_size) -> str
pdf_from_markdown pdf_from_markdown_py_infra impure (markdown_string, output_path, css) -> str
pdf_merge pdf_merge_py_infra impure (pdf_paths, output_path) -> str
pdf_save pdf_save_py_infra impure (doc, output_path) -> str | bytes

Funcion Go

Funcion ID Purity Firma (simplificada)
pdf_simple_report pdf_simple_report_go_infra impure (title string, sections []Section, outputPath string) (string, error)

La funcion Go recibe un titulo, una lista de secciones (cada una con titulo, texto y tabla opcional) y genera un PDF completo en un solo paso. No expone el builder granular de Python — es una funcion de alto nivel para reportes rapidos sin dependencia de Python.

Tareas

Fase 1: Tipos y builder basico

  • 1.1 Crear tipos PDFDoc, PDFPage, PDFStyle en python/types/infra/ con .py y .md
  • 1.2 pdf_create — inicializa documento fpdf2, retorna wrapper PDFDoc
  • 1.3 pdf_add_page — añade pagina con orientacion y tamaño configurables
  • 1.4 pdf_add_text — escribe texto con fuente, tamaño, color, posicion
  • 1.5 pdf_save — serializa documento a archivo o retorna bytes
  • 1.6 Tests basicos: crear documento, añadir pagina, escribir texto, guardar, verificar que el PDF resultante es valido

Fase 2: Contenido rico

  • 2.1 pdf_add_table — tabla con headers, filas, anchos de columna, bordes, colores alternados
  • 2.2 pdf_add_image — imagen desde path o bytes, redimensionado automatico si se especifica solo ancho o alto
  • 2.3 pdf_add_header_footer — header/footer recurrente, numero de pagina con formato configurable (Pagina {n} de {total})
  • 2.4 pdf_merge — fusionar N archivos PDF en uno usando PyPDF2 (ya es dependencia del registry)
  • 2.5 Tests: tabla con datos reales, imagen embebida, merge de 3 PDFs

Fase 3: Conversiones

  • 3.1 pdf_from_html — HTML a PDF usando weasyprint. Soporta CSS inline y externo.
  • 3.2 pdf_from_markdown — Markdown a HTML (via markdown o mistune) y luego a PDF con weasyprint. CSS por defecto incluido.
  • 3.3 Tests: HTML con tabla y estilos a PDF, markdown con headers y listas a PDF
  • 3.4 Documentar instalacion de dependencias de weasyprint (librerias de sistema: pango, cairo, gdk-pixbuf)

Fase 4: Go y cleanup

  • 4.1 pdf_simple_report en Go con go-pdf/fpdf — titulo, secciones con texto, tablas opcionales
  • 4.2 Tests Go con verificacion de output valido
  • 4.3 fn index y verificar que todas las funciones y tipos aparecen en registry.db
  • 4.4 Verificar go vet -tags fts5 y tests Python

Ejemplo de uso

Factura

from infra import pdf_create, pdf_add_page, pdf_add_text, pdf_add_table
from infra import pdf_add_image, pdf_save
from infra import PDFStyle

doc = pdf_create(title="Factura #2024-0042", author="Mi Empresa S.L.")
doc = pdf_add_page(doc)

# Logo
doc = pdf_add_image(doc, "assets/logo.png", x=15, y=10, width=40)

# Datos empresa
header = PDFStyle(font_size=10, color="#666666")
doc = pdf_add_text(doc, "Mi Empresa S.L.\nCIF: B12345678\nCalle Principal 42", style=header, x=120, y=10)

# Titulo
titulo = PDFStyle(font_size=22, font_family="Helvetica-Bold", color="#333333")
doc = pdf_add_text(doc, "FACTURA #2024-0042", style=titulo, x=15, y=55)

# Tabla de items
headers = ["Concepto", "Cantidad", "Precio", "Total"]
rows = [
    ["Consultoria tecnica", "40 h", "75.00 EUR", "3,000.00 EUR"],
    ["Desarrollo API", "1", "2,500.00 EUR", "2,500.00 EUR"],
    ["Hosting mensual", "3 meses", "50.00 EUR", "150.00 EUR"],
]
doc = pdf_add_table(doc, headers, rows, col_widths=[80, 30, 40, 40])

# Total
total = PDFStyle(font_size=16, font_family="Helvetica-Bold", alignment="right")
doc = pdf_add_text(doc, "TOTAL: 5,650.00 EUR", style=total)

pdf_save(doc, "factura_2024_0042.pdf")

Reporte de datos

from infra import pdf_create, pdf_add_page, pdf_add_text, pdf_add_table
from infra import pdf_add_header_footer, pdf_save
from infra import PDFStyle

# Generar reporte desde resultados de query
doc = pdf_create(title="Reporte Mensual - Abril 2026")
doc = pdf_add_header_footer(doc,
    header_text="Reporte Mensual",
    footer_text="Pagina {n} de {total}",
    page_numbers=True
)
doc = pdf_add_page(doc)

titulo = PDFStyle(font_size=18, font_family="Helvetica-Bold")
doc = pdf_add_text(doc, "Resumen de Ejecuciones — Abril 2026", style=titulo)

doc = pdf_add_text(doc, f"Total ejecuciones: {len(executions)}")
doc = pdf_add_text(doc, f"Tasa de exito: {success_rate:.1f}%")
doc = pdf_add_text(doc, f"Duracion promedio: {avg_duration:.0f} ms")

headers = ["Pipeline", "Ejecuciones", "Exitos", "Fallos", "Duracion avg"]
doc = pdf_add_table(doc, headers, execution_summary_rows)

pdf_save(doc, "reporte_abril_2026.pdf")

Export de Markdown a PDF

from infra import pdf_from_markdown

# Convertir documentacion a PDF distribuible
with open("docs/spec_operaciones.md") as f:
    md_content = f.read()

output = pdf_from_markdown(md_content, "spec_operaciones.pdf",
    css="body { font-family: sans-serif; } code { background: #f5f5f5; }"
)
print(f"PDF generado: {output}")

Reporte simple en Go

sections := []infra.ReportSection{
    {
        Title: "Estado del Registry",
        Text:  "El registry contiene 142 funciones, 38 tipos y 12 proposals pendientes.",
    },
    {
        Title: "Funciones por Dominio",
        Table: &infra.ReportTable{
            Headers: []string{"Dominio", "Funciones", "Tipos"},
            Rows: [][]string{
                {"core", "45", "12"},
                {"infra", "38", "8"},
                {"finance", "22", "6"},
                {"datascience", "18", "5"},
            },
        },
    },
}

path, err := infra.PdfSimpleReport("Registry Status Report", sections, "registry_report.pdf")
if err != nil {
    log.Fatal(err)
}
fmt.Println("Reporte generado:", path)

Decisiones de diseño

  • fpdf2 para Python (builder granular): Libreria ligera, pure Python, sin dependencias binarias. Cubre el 90% de los casos (texto, tablas, imagenes). No requiere instalacion de sistema.
  • weasyprint para HTML/Markdown → PDF: Cuando se necesita layout CSS completo (columnas, grids, estilos complejos). Requiere librerias de sistema (pango, cairo) pero produce PDFs de alta calidad tipografica.
  • PyPDF2 para merge: Ya es dependencia del registry (usada por extract_pdf_text_py_core). No añade dependencias nuevas.
  • go-pdf/fpdf para Go: Zero-dependency, API simple, suficiente para reportes tabulares sin necesidad de layout CSS.
  • Dominio infra (no core): Generar archivos es I/O, todas las funciones son impuras. Consistente con notebook_to_pdf_bash_infra y las funciones de export existentes.
  • API fluent (doc = fn(doc, ...)): Cada funcion recibe el documento y lo retorna modificado. Permite encadenar operaciones y mantiene la composabilidad funcional del registry. Internamente muta el objeto fpdf2 — la interfaz inmutable es una convencion, no una garantia.
  • pdf_simple_report en Go como funcion de alto nivel: No replica el builder granular de Python. Es una funcion unica que genera un reporte completo desde datos estructurados. Para PDFs complejos, usar Python.
  • Separar builder (pdf_create/add_*/save) de conversiones (pdf_from_html/markdown/merge): Dos patrones de uso distintos que no comparten estado. El builder construye desde cero; las conversiones transforman contenido existente.

Riesgos

  • Dependencias de sistema para weasyprint: pango, cairo, gdk-pixbuf deben estar instalados. En la maquina de desarrollo esto es un apt install pero complica portabilidad. Mitigado documentando la instalacion en la fase 3 y haciendo que pdf_from_html/pdf_from_markdown fallen con mensaje claro si weasyprint no esta disponible. Las funciones del builder (fpdf2) no tienen este problema.
  • Tamaño de PDFs generados: Imagenes grandes embebidas sin compresion pueden generar PDFs pesados. Mitigado con compresion JPEG automatica en pdf_add_image y documentando limites recomendados.
  • Diferencias tipograficas entre SO: Las fuentes disponibles varian entre Linux/Mac/Windows. Mitigado usando Helvetica (built-in en fpdf2) como fuente por defecto, que no requiere archivos .ttf externos.
  • go-pdf/fpdf es una dependencia Go nueva: Añade un import al modulo. Mitigado porque es una libreria estable, sin dependencias transitivas, y solo la usa una funcion.

Criterios de aceptacion

  • pdf_create + pdf_add_page + pdf_add_text + pdf_save genera un PDF valido que se puede abrir
  • pdf_add_table renderiza tabla con headers, bordes y datos alineados
  • pdf_add_image embebe PNG y JPEG correctamente
  • pdf_from_markdown convierte markdown con headers, listas y codigo a PDF legible
  • pdf_merge combina 3+ PDFs sin corrupcion
  • pdf_simple_report en Go genera PDF con titulo, texto y tabla
  • Todos los IDs siguen formato {name}_py_infra / {name}_go_infra
  • fn index registra las 10 funciones Python, 1 funcion Go y 3 tipos sin errores
  • Tests pasan en Python y Go