Files

14 KiB

id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags
id title status type domain scope priority depends blocks related created updated tags
0020 PDF Generation completado feature
multi-app media
2026-05-17 2026-05-17

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