zcatpdf/docs/ARQUITECTURA_ZPDF.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

Arquitectura zpdf - Diseño Basado en fpdf2

Fecha: 2025-12-08 Basado en: fpdf2 v2.8.5 (Python) Adaptado para: Zig 0.15.2


Filosofía de Diseño

  1. Traducción fiel de fpdf2 - Misma arquitectura, adaptada a idiomas de Zig
  2. Zero dependencias - Todo en Zig puro (excepto zlib para compresión)
  3. API ergonómica - Aprovechamos las capacidades de Zig
  4. Comptime donde sea posible - Métricas de fuentes, validaciones
  5. Sin allocaciones ocultas - El usuario controla la memoria

Comparativa: Estado Actual vs Objetivo

Estado Actual (root.zig)

Document
├── pages: ArrayList(Page)
└── render() -> []u8

Page
├── content: ArrayList(u8)    # Content stream raw
├── current_font, current_font_size
├── stroke_color, fill_color
└── drawText(), drawLine(), drawRect(), fillRect()

Problemas actuales:

  • Todo en un solo archivo (523 líneas)
  • No hay sistema de objetos PDF tipado
  • xref table tiene bugs (orden incorrecto)
  • No hay separación clara de responsabilidades
  • No soporta múltiples fuentes en un documento

Objetivo (basado en fpdf2)

Pdf (facade principal)
├── pages: ArrayList(PdfPage)
├── fonts: FontRegistry
├── images: ImageCache
├── state: GraphicsState
├── output_producer: OutputProducer
└── métodos: addPage(), setFont(), cell(), line(), rect(), image()

PdfPage
├── index: usize
├── dimensions: PageDimensions
├── content_stream: ContentStream
├── resources: ResourceSet
└── annotations: ArrayList(Annotation)

ContentStream
├── buffer: ArrayList(u8)
└── métodos: moveTo(), lineTo(), text(), rect(), etc.

OutputProducer
├── pdf_objects: ArrayList(PdfObject)
├── offsets: HashMap(u32, usize)
└── bufferize() -> []u8

Estructura de Archivos Propuesta

zpdf/
├── src/
│   ├── root.zig              # Re-exports públicos
│   │
│   ├── pdf.zig               # Facade principal (Pdf struct)
│   ├── page.zig              # PdfPage
│   ├── content_stream.zig    # ContentStream (operadores PDF)
│   │
│   ├── objects/
│   │   ├── mod.zig           # Re-exports
│   │   ├── base.zig          # PdfObject trait/interface
│   │   ├── catalog.zig       # /Catalog
│   │   ├── pages.zig         # /Pages (raíz)
│   │   ├── page.zig          # /Page object
│   │   ├── font.zig          # /Font objects
│   │   ├── stream.zig        # Generic streams
│   │   └── xobject.zig       # Images
│   │
│   ├── fonts/
│   │   ├── mod.zig
│   │   ├── type1.zig         # Fuentes Type1 estándar
│   │   ├── metrics.zig       # Métricas (comptime)
│   │   └── registry.zig      # FontRegistry
│   │
│   ├── graphics/
│   │   ├── mod.zig
│   │   ├── color.zig         # RGB, CMYK, Gray
│   │   ├── state.zig         # GraphicsState
│   │   └── path.zig          # Paths vectoriales
│   │
│   ├── text/
│   │   ├── mod.zig
│   │   ├── cell.zig          # cell(), multi_cell()
│   │   ├── layout.zig        # Word wrap, alineación
│   │   └── fragment.zig      # Text fragments
│   │
│   ├── image/
│   │   ├── mod.zig
│   │   ├── jpeg.zig          # Parser JPEG
│   │   └── png.zig           # Parser PNG
│   │
│   ├── output/
│   │   ├── mod.zig
│   │   ├── producer.zig      # OutputProducer
│   │   ├── xref.zig          # Cross-reference table
│   │   └── writer.zig        # Buffer writer helpers
│   │
│   └── util/
│       ├── mod.zig
│       └── encoding.zig      # PDF string encoding
│
├── examples/
│   ├── hello.zig
│   ├── invoice.zig
│   └── table.zig
│
└── tests/
    ├── pdf_test.zig
    ├── content_stream_test.zig
    └── ...

API Objetivo Detallada

Creación de Documento

const std = @import("std");
const zpdf = @import("zpdf");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Crear documento con opciones
    var pdf = zpdf.Pdf.init(allocator, .{
        .orientation = .portrait,   // o .landscape
        .unit = .mm,                // .pt, .cm, .in
        .format = .a4,              // o dimensiones custom
    });
    defer pdf.deinit();

    // Metadata
    pdf.setTitle("Factura #001");
    pdf.setAuthor("ACME Corp");
    pdf.setCreator("zpdf");

    // Nueva página (usa formato por defecto)
    try pdf.addPage(.{});

    // O con opciones específicas
    try pdf.addPage(.{
        .orientation = .landscape,
        .format = .letter,
    });
}

Texto

// Fuente
try pdf.setFont(.helvetica_bold, 24);

// Color de texto
pdf.setTextColor(zpdf.Color.rgb(0, 0, 128));

// Posición actual
pdf.setXY(50, 50);

// Texto simple en posición actual
try pdf.text("Hola mundo");

// Celda: rectángulo con texto
try pdf.cell(.{
    .w = 100,           // Ancho (0 = hasta margen derecho)
    .h = 10,            // Alto (null = altura de fuente)
    .text = "Celda",
    .border = .all,     // o .{ .left = true, .bottom = true }
    .align = .center,   // .left, .right, .justify
    .fill = true,
    .new_x = .right,    // Posición X después
    .new_y = .top,      // Posición Y después
});

// Celda multilínea con word wrap
try pdf.multiCell(.{
    .w = 100,
    .h = 10,
    .text = "Este es un texto largo que se dividirá en varias líneas automáticamente.",
    .border = .none,
    .align = .justify,
});

// Salto de línea
pdf.ln(10);  // Baja 10 unidades

Gráficos

// Colores
pdf.setDrawColor(zpdf.Color.black);
pdf.setFillColor(zpdf.Color.rgb(200, 200, 200));

// Línea
pdf.setLineWidth(0.5);
try pdf.line(50, 100, 150, 100);

// Rectángulo
try pdf.rect(50, 110, 100, 50, .stroke);      // Solo borde
try pdf.rect(50, 110, 100, 50, .fill);        // Solo relleno
try pdf.rect(50, 110, 100, 50, .stroke_fill); // Ambos

// Rectángulo redondeado
try pdf.roundedRect(50, 170, 100, 50, 5, .stroke_fill);

// Círculo / Elipse
try pdf.circle(100, 250, 25, .fill);
try pdf.ellipse(100, 300, 40, 20, .stroke);

// Polígono
try pdf.polygon(&.{
    .{ 100, 350 },
    .{ 150, 400 },
    .{ 50, 400 },
}, .fill);

Imágenes

// Desde archivo
try pdf.image("logo.png", .{
    .x = 10,
    .y = 10,
    .w = 50,        // Ancho (null = automático)
    .h = null,      // Alto (null = mantener ratio)
});

// Desde bytes en memoria
try pdf.imageFromBytes(png_bytes, .{
    .x = 10,
    .y = 70,
    .w = 100,
});

Tablas

// Helper de alto nivel para tablas
var table = pdf.table(.{
    .x = 50,
    .y = 500,
    .width = 500,
    .columns = &.{
        .{ .header = "Descripción", .width = 200, .align = .left },
        .{ .header = "Cantidad", .width = 100, .align = .center },
        .{ .header = "Precio", .width = 100, .align = .right },
        .{ .header = "Total", .width = 100, .align = .right },
    },
    .header_style = .{
        .font = .helvetica_bold,
        .size = 10,
        .fill_color = zpdf.Color.light_gray,
    },
});

try table.addRow(&.{ "Producto A", "2", "10.00 €", "20.00 €" });
try table.addRow(&.{ "Producto B", "1", "25.00 €", "25.00 €" });
try table.addRow(&.{ "Producto C", "5", "5.00 €", "25.00 €" });

try table.render();

Output

// Guardar a archivo
try pdf.save("documento.pdf");

// O obtener bytes
const bytes = try pdf.output();
defer allocator.free(bytes);

Implementación de Tipos Clave

Color

pub const Color = union(enum) {
    rgb: struct { r: u8, g: u8, b: u8 },
    cmyk: struct { c: u8, m: u8, y: u8, k: u8 },
    gray: u8,

    pub fn rgb(r: u8, g: u8, b: u8) Color {
        return .{ .rgb = .{ .r = r, .g = g, .b = b } };
    }

    pub fn toStrokeCmd(self: Color) []const u8 {
        // Returns "R G B RG" or "C M Y K K" etc.
    }

    pub fn toFillCmd(self: Color) []const u8 {
        // Returns "r g b rg" or "c m y k k" etc.
    }

    // Colores predefinidos
    pub const black = rgb(0, 0, 0);
    pub const white = rgb(255, 255, 255);
    pub const red = rgb(255, 0, 0);
    pub const green = rgb(0, 255, 0);
    pub const blue = rgb(0, 0, 255);
};

ContentStream

pub 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 deinit(self: *ContentStream) void {
        self.buffer.deinit();
    }

    // Gráficos de bajo nivel
    pub fn moveTo(self: *ContentStream, x: f32, y: f32) !void {
        try self.buffer.writer().print("{d:.2} {d:.2} m\n", .{ x, y });
    }

    pub fn lineTo(self: *ContentStream, x: f32, y: f32) !void {
        try self.buffer.writer().print("{d:.2} {d:.2} l\n", .{ x, y });
    }

    pub fn rect(self: *ContentStream, x: f32, y: f32, w: f32, h: f32) !void {
        try self.buffer.writer().print("{d:.2} {d:.2} {d:.2} {d:.2} re\n", .{ x, y, w, h });
    }

    pub fn stroke(self: *ContentStream) !void {
        try self.buffer.appendSlice("S\n");
    }

    pub fn fill(self: *ContentStream) !void {
        try self.buffer.appendSlice("f\n");
    }

    pub fn strokeAndFill(self: *ContentStream) !void {
        try self.buffer.appendSlice("B\n");
    }

    pub fn closePath(self: *ContentStream) !void {
        try self.buffer.appendSlice("h\n");
    }

    pub fn saveState(self: *ContentStream) !void {
        try self.buffer.appendSlice("q\n");
    }

    pub fn restoreState(self: *ContentStream) !void {
        try self.buffer.appendSlice("Q\n");
    }

    // Texto
    pub fn beginText(self: *ContentStream) !void {
        try self.buffer.appendSlice("BT\n");
    }

    pub fn endText(self: *ContentStream) !void {
        try self.buffer.appendSlice("ET\n");
    }

    pub fn setFont(self: *ContentStream, font_name: []const u8, size: f32) !void {
        try self.buffer.writer().print("/{s} {d:.2} Tf\n", .{ font_name, size });
    }

    pub fn textPosition(self: *ContentStream, x: f32, y: f32) !void {
        try self.buffer.writer().print("{d:.2} {d:.2} Td\n", .{ x, y });
    }

    pub fn showText(self: *ContentStream, text: []const u8) !void {
        try self.buffer.append('(');
        for (text) |c| {
            switch (c) {
                '(', ')', '\\' => {
                    try self.buffer.append('\\');
                    try self.buffer.append(c);
                },
                else => try self.buffer.append(c),
            }
        }
        try self.buffer.appendSlice(") Tj\n");
    }

    // Colores
    pub fn setStrokeColor(self: *ContentStream, color: Color) !void {
        switch (color) {
            .rgb => |c| {
                const r = @as(f32, @floatFromInt(c.r)) / 255.0;
                const g = @as(f32, @floatFromInt(c.g)) / 255.0;
                const b = @as(f32, @floatFromInt(c.b)) / 255.0;
                try self.buffer.writer().print("{d:.3} {d:.3} {d:.3} RG\n", .{ r, g, b });
            },
            .gray => |g| {
                const v = @as(f32, @floatFromInt(g)) / 255.0;
                try self.buffer.writer().print("{d:.3} G\n", .{v});
            },
            .cmyk => |c| {
                // TODO: CMYK support
                _ = c;
            },
        }
    }

    pub fn setFillColor(self: *ContentStream, color: Color) !void {
        switch (color) {
            .rgb => |c| {
                const r = @as(f32, @floatFromInt(c.r)) / 255.0;
                const g = @as(f32, @floatFromInt(c.g)) / 255.0;
                const b = @as(f32, @floatFromInt(c.b)) / 255.0;
                try self.buffer.writer().print("{d:.3} {d:.3} {d:.3} rg\n", .{ r, g, b });
            },
            .gray => |g| {
                const v = @as(f32, @floatFromInt(g)) / 255.0;
                try self.buffer.writer().print("{d:.3} g\n", .{v});
            },
            .cmyk => |c| {
                // TODO: CMYK support
                _ = c;
            },
        }
    }

    pub fn setLineWidth(self: *ContentStream, width: f32) !void {
        try self.buffer.writer().print("{d:.2} w\n", .{width});
    }
};

PdfObject Interface

pub const PdfObject = struct {
    id: ?u32 = null,
    vtable: *const VTable,

    const VTable = struct {
        serialize: *const fn (self: *const PdfObject, writer: anytype) anyerror!void,
    };

    pub fn ref(self: PdfObject) ![]const u8 {
        if (self.id) |id| {
            var buf: [32]u8 = undefined;
            const len = std.fmt.bufPrint(&buf, "{d} 0 R", .{id}) catch unreachable;
            return buf[0..len];
        }
        return error.ObjectNotAssignedId;
    }

    pub fn serialize(self: *const PdfObject, writer: anytype) !void {
        try self.vtable.serialize(self, writer);
    }
};

Fases de Implementación

Fase 1: Refactorización Core (PRÓXIMA)

  1. Separar root.zig en múltiples archivos
  2. Implementar ContentStream como struct separado
  3. Implementar PdfObject base
  4. Arreglar xref table
  5. Tests exhaustivos

Fase 2: Sistema de Texto Completo

  1. cell() con bordes y alineación
  2. multi_cell() con word wrap
  3. Métricas de fuentes Type1 (comptime)
  4. Cálculo de ancho de texto

Fase 3: Gráficos Completos

  1. Círculos, elipses, arcos
  2. Curvas Bezier
  3. Dash patterns
  4. Transformaciones (rotate, scale)

Fase 4: Imágenes

  1. Parser JPEG (headers para embeber directo)
  2. Parser PNG (con soporte alpha)
  3. Caché de imágenes

Fase 5: Features Avanzados

  1. Sistema de tablas
  2. Links internos y externos
  3. Bookmarks/outline
  4. Headers/footers
  5. Compresión de streams (zlib)

Referencias

  • ARQUITECTURA_FPDF2.md - Análisis detallado de fpdf2
  • PLAN_MAESTRO_ZPDF.md - Plan general del proyecto
  • /reference/fpdf2/ - Código fuente de fpdf2 clonado