El fichero, entero
Esto es exactamente lo que descargas: sin resúmenes ni recortes.
Exportar un informe a PDF
Un PDF que sale de un script se envía a gente que no va a poder pedir correcciones. Se genera entero desde el dato, no a mano, y dos ejecuciones con los mismos datos producen el mismo fichero.
Elegir la herramienta
| Caso | Herramienta |
|---|---|
| Informe con tablas y texto fluido | ReportLab Platypus |
| Maquetación pixel a pixel (tarjeta, diploma, CV) | ReportLab canvas |
| Ya existe una versión HTML fiel | Playwright → page.pdf() |
No mezcles: elegir canvas para un informe de 12 páginas significa calcular saltos de página a mano, y ese cálculo siempre acaba mal.
Informe con Platypus
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib import colors
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak,
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
doc = SimpleDocTemplate(
str(salida), pagesize=A4,
leftMargin=20 * mm, rightMargin=20 * mm,
topMargin=18 * mm, bottomMargin=18 * mm,
title="Estado del proyecto", author="…", # metadatos: se ven en el visor
)
estilos = getSampleStyleSheet()
cuerpo = ParagraphStyle('cuerpo', parent=estilos['BodyText'],
fontName='Inter', fontSize=9.5, leading=13.5)
historia = [
Paragraph("Estado del proyecto", estilos['Title']),
Spacer(1, 6 * mm),
Paragraph(resumen, cuerpo),
]
doc.build(historia, onFirstPage=portada, onLaterPages=pie)
El texto va siempre en Paragraph, nunca como cadena suelta: solo el
párrafo sabe partir líneas. Y Paragraph interpreta un subconjunto de HTML
(<b>, <i>, <font>), así que cualquier dato que venga de fuera hay que
escaparlo: un & sin escapar rompe la generación entera.
Tablas que sobreviven al salto de página
tabla = Table(filas, colWidths=[70*mm, 30*mm, 40*mm, 30*mm], repeatRows=1)
tabla.setStyle(TableStyle([
('FONTNAME', (0, 0), (-1, 0), 'Inter-Bold'),
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f1efe8')),
('ALIGN', (1, 1), (-1, -1), 'RIGHT'), # números a la derecha
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LINEBELOW', (0, 0), (-1, 0), 0.5, colors.HexColor('#e0dfd8')),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#faf9f6')]),
]))
repeatRows=1repite la cabecera en cada página. Sin ella, la página 3 es una tabla de números sin significado.- Anchos de columna explícitos. El automático reparte mal en cuanto una celda es larga.
- Las celdas con texto largo también van en
Paragraph, o se salen de la celda sin avisar. - Números alineados a la derecha y con el mismo número de decimales. Una columna de importes desalineada es el detalle que hace dudar del resto.
Tipografías y colores
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
pdfmetrics.registerFont(TTFont('Inter', ruta / 'Inter-Regular.ttf'))
pdfmetrics.registerFont(TTFont('Inter-Bold', ruta / 'Inter-Bold.ttf'))
Registra las TTF del proyecto: las Type1 por defecto (Helvetica) no llevan
acentos completos según la codificación y el PDF sale con huecos. Y usa la
misma paleta que el resto de la marca en colors.HexColor, para que el PDF no
parezca de otra empresa.
Encabezado, pie y numeración
def pie(canvas, doc):
canvas.saveState()
canvas.setFont('Inter', 7.5)
canvas.setFillColor(colors.HexColor('#4a4943'))
canvas.drawString(20 * mm, 12 * mm, f"{titulo} · {fecha:%d/%m/%Y}")
canvas.drawRightString(A4[0] - 20 * mm, 12 * mm, f"{doc.page}")
canvas.restoreState()
saveState/restoreState siempre: sin ellos, la fuente del pie se filtra a la
página siguiente. Para “página X de Y” hace falta una pasada doble
(doc.multiBuild con un marcador), porque el total no se conoce hasta el final.
Checklist antes de enviar
- Fecha de generación y origen del dato visibles en el documento.
- Acentos,
ñy€correctos —abre el PDF, no confíes en que no falló—. - Ninguna tabla cortada a mitad de fila ni texto fuera de márgenes.
- Metadatos (
title,author) puestos: es lo que se ve en la pestaña del visor y al adjuntarlo. - Salida a un directorio de build, nunca sobreescribiendo la fuente.
- Script idempotente: reejecutarlo no cambia nada más que el fichero.
- Sin datos personales o confidenciales que no correspondan al destinatario. Un PDF se reenvía solo.