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

541 lines
14 KiB
Markdown

# 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