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

14 KiB

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

# 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

# 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

# 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

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

  • Estructura básica PDF (header, catalog, pages, xref, trailer)
  • Fuentes Type1 (Helvetica, Times, Courier)
  • Texto: text(), cell()
  • Gráficos: line(), rect()
  • 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

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

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