feat: issue/0020 — generacion de PDFs en Python y Go
Añade 3 tipos Python (PDFDoc, PDFPage, PDFStyle) y 10 funciones Python para construir PDFs con fpdf2 (builder fluent), fusionar PDFs con pypdf y convertir HTML/Markdown a PDF via weasyprint (stub si no disponible). Añade pdf_simple_report en Go como stub hasta que go-pdf/fpdf se integre. - python/types/infra/: pdf_doc, pdf_page, pdf_style - python/functions/infra/: pdf_create, pdf_add_page, pdf_add_text, pdf_add_table, pdf_add_image, pdf_add_header_footer, pdf_from_html, pdf_from_markdown, pdf_merge, pdf_save - functions/infra/pdf_simple_report.go: stub Go con ReportSection/ReportTable - 17 tests Python pasando (pytest) - fpdf2 y pypdf añadidos via uv al venv Python Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""pdf_add_table — añade una tabla con headers y filas al documento PDF."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
_types_dir = os.path.join(os.path.dirname(__file__), "..", "..", "..", "python", "types", "infra")
|
||||
sys.path.insert(0, _types_dir)
|
||||
from pdf_doc import PDFDoc
|
||||
from pdf_style import PDFStyle
|
||||
|
||||
|
||||
def _parse_hex_color(hex_color: str) -> tuple[int, int, int]:
|
||||
h = hex_color.lstrip("#")
|
||||
if len(h) == 3:
|
||||
h = "".join(c * 2 for c in h)
|
||||
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
||||
|
||||
|
||||
def pdf_add_table(
|
||||
doc: PDFDoc,
|
||||
headers: list[str],
|
||||
rows: list[list[str]],
|
||||
col_widths: list[float] | None = None,
|
||||
header_style: PDFStyle | None = None,
|
||||
row_style: PDFStyle | None = None,
|
||||
border: int = 1,
|
||||
row_height: float = 8.0,
|
||||
alternate_bg: bool = True,
|
||||
) -> PDFDoc:
|
||||
"""Añade una tabla con headers y filas de datos al documento PDF.
|
||||
|
||||
Las columnas se distribuyen uniformemente si no se especifican anchos.
|
||||
Soporta colores alternados en filas, bordes configurables y estilos
|
||||
diferenciados para headers y filas de datos.
|
||||
|
||||
Args:
|
||||
doc: PDFDoc con pagina activa.
|
||||
headers: lista de strings con los nombres de columna.
|
||||
rows: lista de listas de strings con los datos de cada fila.
|
||||
col_widths: lista de anchos de columna en mm. None = distribucion uniforme.
|
||||
header_style: PDFStyle para la fila de headers. None usa bold 10pt.
|
||||
row_style: PDFStyle para filas de datos. None usa 10pt normal.
|
||||
border: grosor/tipo de borde (0=sin borde, 1=borde completo).
|
||||
row_height: altura de cada celda en mm. Por defecto 8.
|
||||
alternate_bg: si True, alterna fondo gris claro en filas pares.
|
||||
|
||||
Returns:
|
||||
PDFDoc con la tabla añadida.
|
||||
"""
|
||||
fpdf = doc.fpdf
|
||||
|
||||
if not headers:
|
||||
return doc
|
||||
|
||||
# Anchos de columna
|
||||
available_width = fpdf.epw
|
||||
n_cols = len(headers)
|
||||
if col_widths is None:
|
||||
col_widths = [available_width / n_cols] * n_cols
|
||||
else:
|
||||
# Rellenar si faltan columnas
|
||||
while len(col_widths) < n_cols:
|
||||
col_widths.append(available_width / n_cols)
|
||||
|
||||
# Estilos
|
||||
if header_style is None:
|
||||
header_style = PDFStyle(
|
||||
font_family="Helvetica-Bold",
|
||||
font_size=10.0,
|
||||
color="#000000",
|
||||
alignment="left",
|
||||
)
|
||||
if row_style is None:
|
||||
row_style = PDFStyle(font_size=10.0, color="#000000", alignment="left")
|
||||
|
||||
def set_font_from_style(s: PDFStyle):
|
||||
family = s.font_family
|
||||
bold = "-Bold" in family or "-BoldOblique" in family
|
||||
italic = "-Oblique" in family or "-BoldOblique" in family
|
||||
base = family.split("-")[0]
|
||||
st = ("B" if bold else "") + ("I" if italic else "")
|
||||
fpdf.set_font(base, style=st, size=s.font_size)
|
||||
r, g, b = _parse_hex_color(s.color)
|
||||
fpdf.set_text_color(r, g, b)
|
||||
|
||||
def align_char(a: str) -> str:
|
||||
return {"left": "L", "center": "C", "right": "R", "justify": "J"}.get(a, "L")
|
||||
|
||||
# Header row
|
||||
set_font_from_style(header_style)
|
||||
fpdf.set_fill_color(230, 230, 230)
|
||||
fpdf.set_draw_color(180, 180, 180)
|
||||
|
||||
for i, (header, width) in enumerate(zip(headers, col_widths)):
|
||||
fpdf.cell(
|
||||
width,
|
||||
row_height,
|
||||
str(header),
|
||||
border=border,
|
||||
align=align_char(header_style.alignment),
|
||||
fill=True,
|
||||
)
|
||||
fpdf.ln()
|
||||
|
||||
# Data rows
|
||||
set_font_from_style(row_style)
|
||||
fpdf.set_text_color(*_parse_hex_color(row_style.color))
|
||||
|
||||
for row_idx, row in enumerate(rows):
|
||||
fill = alternate_bg and (row_idx % 2 == 1)
|
||||
if fill:
|
||||
fpdf.set_fill_color(245, 245, 245)
|
||||
else:
|
||||
fpdf.set_fill_color(255, 255, 255)
|
||||
|
||||
for col_idx, (cell, width) in enumerate(zip(row, col_widths)):
|
||||
fpdf.cell(
|
||||
width,
|
||||
row_height,
|
||||
str(cell) if cell is not None else "",
|
||||
border=border,
|
||||
align=align_char(row_style.alignment),
|
||||
fill=True,
|
||||
)
|
||||
fpdf.ln()
|
||||
|
||||
fpdf.ln(2)
|
||||
return doc
|
||||
Reference in New Issue
Block a user