# 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 ```zig 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 ```zig // 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 ```zig // 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 ```zig // 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 ```zig // 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 ```zig // 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 ```zig 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 ```zig 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 ```zig 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