# 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 ├── 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 <> endobj 2 0 obj <> endobj 3 0 obj <>>>>> endobj 4 0 obj <> stream BT /F1 12 Tf 100 700 Td (Hola mundo) Tj ET endstream endobj 5 0 obj <> endobj xref 0 6 0000000000 65535 f 0000000015 00000 n 0000000060 00000 n 0000000147 00000 n 0000000247 00000 n 0000000340 00000 n trailer <> 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/)