zcatpdf/docs/ARQUITECTURA_FPDF2.md
reugenio 2996289953 feat: v0.2 - Complete text system (cell, multiCell, alignment)
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>
2025-12-08 19:46:30 +01:00

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/)