merge: issue/0020-pdf-generation — PDF generation Python+Go
# Conflicts: # registry.db
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
# 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
|
||||
|
||||
```python
|
||||
@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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```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
|
||||
Reference in New Issue
Block a user