Phase 1 - Refactoring: - Modular architecture: fonts/, graphics/, objects/, output/ - Fixed Zig 0.15 API changes (ArrayListUnmanaged) - Fixed memory issues in render() Phase 2 - Text System: - cell() with borders, fill, alignment - cellAdvanced() with position control - multiCell() with automatic word wrap - ln() for line breaks - getStringWidth() for text width calculation - Page margins (setMargins, setCellMargin) - Align enum (left, center, right) - Border packed struct New features: - New Pdf API (cleaner than legacy Document) - Document metadata (setTitle, setAuthor, setSubject) - Color: RGB, CMYK, Grayscale support - 52 unit tests passing - New example: text_demo.zig 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
509 lines
14 KiB
Markdown
509 lines
14 KiB
Markdown
# Análisis de Arquitectura: fpdf2 (Python)
|
|
|
|
**Fecha:** 2025-12-08
|
|
**Versión analizada:** fpdf2 v2.8.5
|
|
**Repositorio:** https://github.com/py-pdf/fpdf2
|
|
|
|
---
|
|
|
|
## Resumen Ejecutivo
|
|
|
|
fpdf2 es una librería Python para generación de PDFs que ha evolucionado durante 20+ años desde el FPDF original de PHP. Su arquitectura se basa en:
|
|
|
|
1. **Una clase principal `FPDF`** que actúa como facade/builder
|
|
2. **Objetos PDF tipados** (`syntax.py`) que representan la estructura del documento
|
|
3. **Un OutputProducer** que serializa todo a bytes
|
|
4. **Sistemas modulares** para texto, gráficos, imágenes, etc.
|
|
|
|
---
|
|
|
|
## Estructura de Archivos (por importancia)
|
|
|
|
| Archivo | Líneas | Descripción |
|
|
|---------|--------|-------------|
|
|
| `fpdf.py` | 6094 | Clase principal FPDF - facade/builder |
|
|
| `drawing.py` | 5271 | Sistema de dibujo vectorial |
|
|
| `fonts.py` | 3365 | Gestión de fuentes (Type1 + TTF) |
|
|
| `output.py` | 2011 | Serialización final del PDF |
|
|
| `enums.py` | 1778 | Enumeraciones (Align, XPos, YPos, etc.) |
|
|
| `svg.py` | 1806 | Parser/renderer SVG |
|
|
| `pattern.py` | 1487 | Patrones de relleno |
|
|
| `html.py` | 1283 | Parser HTML a PDF |
|
|
| `table.py` | 925 | Sistema de tablas |
|
|
| `line_break.py` | 817 | Word wrap y saltos de línea |
|
|
| `image_parsing.py` | 715 | Parseo de imágenes (JPEG, PNG) |
|
|
| `text_region.py` | 737 | Regiones de texto (columnas) |
|
|
| `syntax.py` | 405 | Tipos básicos PDF |
|
|
| `graphics_state.py` | 393 | Estado gráfico (colores, líneas) |
|
|
|
|
---
|
|
|
|
## Arquitectura de Clases
|
|
|
|
### 1. Clase Principal: FPDF (`fpdf.py`)
|
|
|
|
```
|
|
FPDF(GraphicsStateMixin, TextRegionMixin)
|
|
│
|
|
├── Estado del documento
|
|
│ ├── page: int # Página actual (1-indexed)
|
|
│ ├── pages: Dict[int, PDFPage] # Todas las páginas
|
|
│ ├── fonts: FontRegistry # Fuentes registradas
|
|
│ ├── links: dict # Enlaces internos
|
|
│ └── image_cache: ImageCache # Caché de imágenes
|
|
│
|
|
├── Estado gráfico (de GraphicsStateMixin)
|
|
│ ├── draw_color: DeviceRGB # Color de trazo
|
|
│ ├── fill_color: DeviceRGB # Color de relleno
|
|
│ ├── text_color: DeviceRGB # Color de texto
|
|
│ ├── line_width: float # Grosor de línea
|
|
│ └── dash_pattern: dict # Patrón de línea discontinua
|
|
│
|
|
├── Estado de texto
|
|
│ ├── font_family: str # Familia de fuente actual
|
|
│ ├── font_style: str # Estilo (B, I, BI)
|
|
│ ├── font_size_pt: float # Tamaño en puntos
|
|
│ ├── current_font: CoreFont|TTFFont
|
|
│ ├── underline: bool
|
|
│ └── strikethrough: bool
|
|
│
|
|
├── Posicionamiento
|
|
│ ├── x, y: float # Posición actual
|
|
│ ├── l_margin, t_margin, r_margin, b_margin
|
|
│ ├── w, h: float # Dimensiones página (user units)
|
|
│ ├── w_pt, h_pt: float # Dimensiones página (points)
|
|
│ └── k: float # Factor de escala (unit -> points)
|
|
│
|
|
└── Métodos principales
|
|
├── add_page() # Nueva página
|
|
├── set_font(family, style, size)
|
|
├── cell(w, h, text, ...) # Celda con texto
|
|
├── multi_cell(w, h, text, ...) # Celda multilínea
|
|
├── text(x, y, text) # Texto en posición absoluta
|
|
├── line(x1, y1, x2, y2) # Línea
|
|
├── rect(x, y, w, h, style) # Rectángulo
|
|
├── image(file, x, y, w, h) # Imagen
|
|
└── output(name) # Generar PDF final
|
|
```
|
|
|
|
### 2. Sistema de Tipos PDF (`syntax.py`)
|
|
|
|
```
|
|
PDFObject (base)
|
|
├── id: int # ID del objeto (asignado al serializar)
|
|
├── ref: str # "N 0 R" para referencias
|
|
├── serialize() -> str # Convierte a texto PDF
|
|
└── _build_obj_dict() -> dict # Construye diccionario de propiedades
|
|
|
|
PDFContentStream(PDFObject)
|
|
├── _contents: bytes # Contenido del stream
|
|
├── compress: bool # Si aplicar FlateDecode
|
|
├── filter: Name # /FlateDecode o None
|
|
└── length: int # Longitud del contenido
|
|
|
|
Otros tipos:
|
|
├── Name(str) # /NombrePDF
|
|
├── PDFString(str) # (texto) o <hex>
|
|
├── PDFArray(list) # [elem1 elem2]
|
|
├── PDFDate # D:YYYYMMDDHHmmSS
|
|
└── Raw(str) # Texto sin transformar
|
|
```
|
|
|
|
### 3. Sistema de Output (`output.py`)
|
|
|
|
```
|
|
OutputProducer
|
|
├── fpdf: FPDF # Referencia al documento
|
|
├── pdf_objs: list # Lista de todos los objetos PDF
|
|
├── obj_id: int # Contador de IDs
|
|
├── offsets: dict # Offset de cada objeto para xref
|
|
└── buffer: bytearray # Buffer de salida final
|
|
|
|
Flujo de bufferize():
|
|
1. Insertar PDFHeader
|
|
2. Crear páginas root (PDFPagesRoot)
|
|
3. Crear catálogo (PDFCatalog)
|
|
4. Añadir páginas (PDFPage + content streams)
|
|
5. Añadir anotaciones
|
|
6. Insertar recursos (fuentes, imágenes, etc.)
|
|
7. Añadir estructura de árbol
|
|
8. Añadir outline/bookmarks
|
|
9. Añadir metadata XMP
|
|
10. Añadir info dictionary
|
|
11. Añadir tabla xref y trailer
|
|
12. Serializar todo a buffer
|
|
```
|
|
|
|
### 4. Objetos de Página
|
|
|
|
```
|
|
PDFPage(PDFObject)
|
|
├── type = Name("Page")
|
|
├── parent: PDFPagesRoot # Referencia al padre
|
|
├── media_box: str # Dimensiones [0 0 W H]
|
|
├── contents: PDFContentStream # Stream de contenido
|
|
├── resources: PDFResources # Recursos usados
|
|
├── annots: list # Anotaciones
|
|
└── ...
|
|
|
|
PDFPagesRoot(PDFObject)
|
|
├── type = Name("Pages")
|
|
├── count: int # Número de páginas
|
|
├── kids: PDFArray[PDFPage] # Array de páginas
|
|
└── media_box: str # Dimensiones por defecto
|
|
```
|
|
|
|
---
|
|
|
|
## Flujo de Generación de Contenido
|
|
|
|
### Escribir texto con cell()
|
|
|
|
```python
|
|
# 1. Usuario llama
|
|
pdf.cell(100, 10, "Hola mundo")
|
|
|
|
# 2. FPDF.cell() hace:
|
|
# a. Normaliza el texto
|
|
# b. Preload de estilos de fuente (bold, italic, etc.)
|
|
# c. Crea TextLine con fragmentos
|
|
# d. Llama a _render_styled_text_line()
|
|
|
|
# 3. _render_styled_text_line():
|
|
# a. Calcula ancho y posición
|
|
# b. Genera comandos PDF para el content stream
|
|
# c. Llama a _out() para añadir al stream
|
|
|
|
# 4. _out() añade al buffer de la página actual:
|
|
# "BT 100.00 700.00 Td (Hola mundo) Tj ET"
|
|
```
|
|
|
|
### Comandos PDF generados (Content Stream)
|
|
|
|
```
|
|
% Texto
|
|
BT % Begin Text
|
|
/F1 12 Tf % Font 1, 12pt
|
|
100.00 700.00 Td % Move to position
|
|
(Hola mundo) Tj % Show text
|
|
ET % End Text
|
|
|
|
% Línea
|
|
100.00 700.00 m % Move to
|
|
200.00 700.00 l % Line to
|
|
S % Stroke
|
|
|
|
% Rectángulo
|
|
100.00 700.00 50.00 -20.00 re % Rectangle
|
|
S % Stroke (o f para fill, B para ambos)
|
|
|
|
% Colores
|
|
0 0 0 RG % Set stroke color RGB
|
|
0.5 0.5 0.5 rg % Set fill color RGB
|
|
|
|
% Estado gráfico
|
|
q % Save state
|
|
... operaciones ...
|
|
Q % Restore state
|
|
```
|
|
|
|
---
|
|
|
|
## Sistema de Unidades
|
|
|
|
```python
|
|
# Factor de escala k (de unit a points)
|
|
# 1 point = 1/72 inch
|
|
|
|
unit_to_k = {
|
|
"pt": 1,
|
|
"mm": 72 / 25.4, # ~2.834645669
|
|
"cm": 72 / 2.54, # ~28.34645669
|
|
"in": 72,
|
|
}
|
|
|
|
# Conversión:
|
|
# points = user_units * k
|
|
# user_units = points / k
|
|
|
|
# Coordenadas Y se invierten:
|
|
# pdf_y = (page_height - user_y) * k
|
|
```
|
|
|
|
---
|
|
|
|
## Sistema de Fuentes
|
|
|
|
### Fuentes Type1 (Core)
|
|
|
|
14 fuentes estándar que no necesitan embeber:
|
|
```
|
|
Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique
|
|
Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic
|
|
Courier, Courier-Bold, Courier-Oblique, Courier-BoldOblique
|
|
Symbol, ZapfDingbats
|
|
```
|
|
|
|
Métricas hardcodeadas en `fpdf/font/` como archivos `.pkl`.
|
|
|
|
### Fuentes TTF
|
|
|
|
1. Se parsea el archivo TTF con `fontTools`
|
|
2. Se hace subset (solo glifos usados)
|
|
3. Se embebe el subset en el PDF
|
|
4. Se crea tabla CMap para mapeo unicode
|
|
|
|
---
|
|
|
|
## Sistema de Colores
|
|
|
|
```python
|
|
# DeviceRGB (0-1 floats)
|
|
class DeviceRGB:
|
|
r: float # 0.0 - 1.0
|
|
g: float
|
|
b: float
|
|
|
|
# También soporta DeviceCMYK y DeviceGray
|
|
|
|
# Serialización a PDF:
|
|
# Stroke: "R G B RG" (ej: "1 0 0 RG" = rojo)
|
|
# Fill: "R G B rg" (ej: "0 1 0 rg" = verde)
|
|
```
|
|
|
|
---
|
|
|
|
## Sistema de Imágenes
|
|
|
|
```python
|
|
# Formatos soportados:
|
|
# - JPEG: Se embebe directamente (DCTDecode)
|
|
# - PNG: Se descomprime y recomprime (FlateDecode)
|
|
# Alpha channel se convierte a SMask
|
|
# - GIF: Se convierte a PNG
|
|
# - TIFF: Se convierte a PNG
|
|
# - SVG: Se renderiza a paths
|
|
|
|
# Cada imagen es un PDFXObject:
|
|
PDFXObject(PDFContentStream)
|
|
├── type = Name("XObject")
|
|
├── subtype = Name("Image")
|
|
├── width, height: int
|
|
├── color_space: str # /DeviceRGB, /DeviceGray, /DeviceCMYK
|
|
├── bits_per_component: int # 8
|
|
├── filter: Name # /DCTDecode, /FlateDecode
|
|
├── decode_parms: dict # Parámetros de decodificación
|
|
└── s_mask: PDFXObject # Máscara de transparencia (opcional)
|
|
```
|
|
|
|
---
|
|
|
|
## Content Streams - Operadores PDF Principales
|
|
|
|
### Operadores de Gráficos
|
|
|
|
| Operador | Descripción | Ejemplo |
|
|
|----------|-------------|---------|
|
|
| `m` | moveto | `100 200 m` |
|
|
| `l` | lineto | `200 200 l` |
|
|
| `c` | curveto (bezier) | `x1 y1 x2 y2 x3 y3 c` |
|
|
| `re` | rectangle | `x y w h re` |
|
|
| `h` | closepath | `h` |
|
|
| `S` | stroke | `S` |
|
|
| `f` | fill | `f` |
|
|
| `B` | fill + stroke | `B` |
|
|
| `n` | no-op (end path) | `n` |
|
|
| `q` | save state | `q` |
|
|
| `Q` | restore state | `Q` |
|
|
| `w` | line width | `0.5 w` |
|
|
| `J` | line cap | `0 J` (0=butt, 1=round, 2=square) |
|
|
| `j` | line join | `0 j` (0=miter, 1=round, 2=bevel) |
|
|
| `d` | dash pattern | `[3 2] 0 d` |
|
|
| `cm` | transform matrix | `1 0 0 1 tx ty cm` |
|
|
|
|
### Operadores de Color
|
|
|
|
| Operador | Descripción | Ejemplo |
|
|
|----------|-------------|---------|
|
|
| `RG` | stroke RGB | `1 0 0 RG` |
|
|
| `rg` | fill RGB | `0 1 0 rg` |
|
|
| `K` | stroke CMYK | `0 0 0 1 K` |
|
|
| `k` | fill CMYK | `0 0 0 1 k` |
|
|
| `G` | stroke gray | `0.5 G` |
|
|
| `g` | fill gray | `0.5 g` |
|
|
|
|
### Operadores de Texto
|
|
|
|
| Operador | Descripción | Ejemplo |
|
|
|----------|-------------|---------|
|
|
| `BT` | begin text | `BT` |
|
|
| `ET` | end text | `ET` |
|
|
| `Tf` | set font | `/F1 12 Tf` |
|
|
| `Td` | move text position | `100 200 Td` |
|
|
| `Tj` | show text | `(Hello) Tj` |
|
|
| `TJ` | show text with kerning | `[(H) -20 (ello)] TJ` |
|
|
| `Tc` | character spacing | `0.5 Tc` |
|
|
| `Tw` | word spacing | `2 Tw` |
|
|
| `Tz` | horizontal scaling | `100 Tz` |
|
|
| `TL` | leading | `14 TL` |
|
|
| `T*` | next line | `T*` |
|
|
| `Tr` | render mode | `0 Tr` (0=fill, 1=stroke, 2=both) |
|
|
|
|
### Operadores de Imagen
|
|
|
|
| Operador | Descripción | Ejemplo |
|
|
|----------|-------------|---------|
|
|
| `Do` | paint XObject | `/I1 Do` |
|
|
|
|
---
|
|
|
|
## Estructura de un PDF Mínimo
|
|
|
|
```
|
|
%PDF-1.4
|
|
%éëñ¿
|
|
|
|
1 0 obj
|
|
<</Type /Catalog /Pages 2 0 R>>
|
|
endobj
|
|
|
|
2 0 obj
|
|
<</Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 595.28 841.89]>>
|
|
endobj
|
|
|
|
3 0 obj
|
|
<</Type /Page /Parent 2 0 R /Contents 4 0 R /Resources <</Font <</F1 5 0 R>>>>>>
|
|
endobj
|
|
|
|
4 0 obj
|
|
<</Length 44>>
|
|
stream
|
|
BT /F1 12 Tf 100 700 Td (Hola mundo) Tj ET
|
|
endstream
|
|
endobj
|
|
|
|
5 0 obj
|
|
<</Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding>>
|
|
endobj
|
|
|
|
xref
|
|
0 6
|
|
0000000000 65535 f
|
|
0000000015 00000 n
|
|
0000000060 00000 n
|
|
0000000147 00000 n
|
|
0000000247 00000 n
|
|
0000000340 00000 n
|
|
|
|
trailer
|
|
<</Size 6 /Root 1 0 R>>
|
|
|
|
startxref
|
|
448
|
|
%%EOF
|
|
```
|
|
|
|
---
|
|
|
|
## Core Mínimo para Facturas
|
|
|
|
Para generar facturas necesitamos:
|
|
|
|
### Imprescindible (Fase 1)
|
|
- [x] Estructura básica PDF (header, catalog, pages, xref, trailer)
|
|
- [x] Fuentes Type1 (Helvetica, Times, Courier)
|
|
- [x] Texto: `text()`, `cell()`
|
|
- [x] Gráficos: `line()`, `rect()`
|
|
- [x] Colores RGB
|
|
|
|
### Importante (Fase 2)
|
|
- [ ] `multi_cell()` con word wrap
|
|
- [ ] Alineación texto (left, center, right, justify)
|
|
- [ ] Imágenes JPEG/PNG (para logos)
|
|
- [ ] SetMargins, SetAutoPageBreak
|
|
|
|
### Deseable (Fase 3)
|
|
- [ ] Tablas
|
|
- [ ] Headers/footers
|
|
- [ ] Links
|
|
- [ ] Numeración de páginas
|
|
|
|
---
|
|
|
|
## Notas para Implementación en Zig
|
|
|
|
### Equivalencias de Tipos
|
|
|
|
| Python | Zig |
|
|
|--------|-----|
|
|
| `class FPDF` | `pub const Pdf = struct` |
|
|
| `str` | `[]const u8` |
|
|
| `float` | `f32` |
|
|
| `int` | `i32` |
|
|
| `list` | `std.ArrayList(T)` o `[]const T` |
|
|
| `dict` | `std.StringHashMap(V)` o `struct` |
|
|
| `bytes` | `[]u8` |
|
|
| `Optional[T]` | `?T` |
|
|
| `BytesIO` | `std.ArrayList(u8)` |
|
|
| Exception | `error` set |
|
|
|
|
### Patrón de Content Stream
|
|
|
|
```zig
|
|
const ContentStream = struct {
|
|
buffer: std.ArrayList(u8),
|
|
allocator: std.mem.Allocator,
|
|
|
|
pub fn init(allocator: std.mem.Allocator) ContentStream {
|
|
return .{
|
|
.buffer = std.ArrayList(u8).init(allocator),
|
|
.allocator = allocator,
|
|
};
|
|
}
|
|
|
|
pub fn write(self: *ContentStream, comptime fmt: []const u8, args: anytype) !void {
|
|
try std.fmt.format(self.buffer.writer(), fmt, args);
|
|
}
|
|
|
|
pub fn moveTo(self: *ContentStream, x: f32, y: f32) !void {
|
|
try self.write("{d:.2} {d:.2} m\n", .{x, y});
|
|
}
|
|
|
|
pub fn lineTo(self: *ContentStream, x: f32, y: f32) !void {
|
|
try self.write("{d:.2} {d:.2} l\n", .{x, y});
|
|
}
|
|
|
|
pub fn stroke(self: *ContentStream) !void {
|
|
try self.write("S\n", .{});
|
|
}
|
|
};
|
|
```
|
|
|
|
### Factor de escala
|
|
|
|
```zig
|
|
pub const Unit = enum {
|
|
pt,
|
|
mm,
|
|
cm,
|
|
in,
|
|
|
|
pub fn toK(self: Unit) f32 {
|
|
return switch (self) {
|
|
.pt => 1.0,
|
|
.mm => 72.0 / 25.4,
|
|
.cm => 72.0 / 2.54,
|
|
.in => 72.0,
|
|
};
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Referencias
|
|
|
|
- [PDF Reference 1.4](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.4.pdf)
|
|
- [PDF 1.7 ISO 32000](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf)
|
|
- [fpdf2 Source Code](https://github.com/py-pdf/fpdf2)
|
|
- [fpdf2 Documentation](https://py-pdf.github.io/fpdf2/)
|