zcatpdf/IMPLEMENTATION_PLAN.md
reugenio f09922076f refactor: Rename zpdf to zcatpdf for consistency with zcat* family
- Renamed all references from zpdf to zcatpdf
- Module import: @import("zcatpdf")
- Consistent with zcatui, zcatgui naming convention
- All lowercase per Zig standards

Note: Directory rename (zpdf -> zcatpdf) and Forgejo repo rename
should be done manually after this commit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 02:10:57 +01:00

2803 lines
80 KiB
Markdown

# zcatpdf - Plan de Implementacion Completo
> **Creado**: 2025-12-08
> **Version actual**: 0.5
> **Objetivo**: Implementar TODAS las funcionalidades faltantes
---
## Estado Actual (v0.5)
### Funcionalidades Completadas
- [x] PDF 1.4 basico (estructura, objetos, xref, trailer)
- [x] Paginas (A4, Letter, Legal, A3, A5, custom)
- [x] Texto (drawText, cell, multiCell, word-wrap)
- [x] 14 fuentes Type1 estandar con metricas
- [x] Colores RGB, CMYK, Grayscale
- [x] Lineas, rectangulos, circulos/elipses
- [x] Imagenes JPEG (DCT passthrough)
- [x] Tablas con helper (header, rows, footer, styling)
- [x] Paginacion (numeros de pagina, headers, footers)
- [x] Links clickeables (URL externos, internos entre paginas)
- [x] ~70 tests, 7 ejemplos funcionando
### Archivos del Proyecto
```
src/
├── root.zig # Exports publicos
├── pdf.zig # Facade principal
├── page.zig # Pagina (800+ lineas)
├── content_stream.zig # Operadores PDF
├── table.zig # Helper tablas
├── pagination.zig # Paginacion/headers/footers
├── links.zig # Tipos de links
├── fonts/
│ ├── mod.zig
│ └── type1.zig # 14 fuentes Type1 + metricas
├── graphics/
│ ├── mod.zig
│ └── color.zig # Color RGB/CMYK/Gray
├── images/
│ ├── mod.zig
│ ├── image_info.zig
│ ├── jpeg.zig # Parser JPEG
│ └── png.zig # Solo metadata (NotImplemented)
├── objects/
│ ├── mod.zig
│ └── base.zig # PageSize, Unit, etc.
└── output/
├── mod.zig
└── producer.zig # Serializacion PDF
```
---
## FASE 6: PNG Completo + Compresion zlib
**Prioridad**: ALTA
**Dependencias**: Ninguna
**Archivos a modificar**: `src/images/png.zig`, `src/output/producer.zig`
### 6.1 Implementar zlib/deflate en Zig puro
```zig
// src/compression/deflate.zig
pub const Deflate = struct {
// Implementar DEFLATE (RFC 1951) sin dependencias externas
// Zig std tiene std.compress.zlib que podemos usar!
pub fn compress(allocator: Allocator, data: []const u8) ![]u8 {
var compressed = std.ArrayList(u8).init(allocator);
var compressor = try std.compress.zlib.compressor(compressed.writer());
try compressor.writer().writeAll(data);
try compressor.finish();
return compressed.toOwnedSlice();
}
pub fn decompress(allocator: Allocator, data: []const u8) ![]u8 {
var decompressed = std.ArrayList(u8).init(allocator);
var stream = std.io.fixedBufferStream(data);
var decompressor = std.compress.zlib.decompressor(stream.reader());
// ... read all
}
};
```
**NOTA**: Zig 0.15 incluye `std.compress.zlib` - usarlo directamente!
### 6.2 Completar PNG parser
```zig
// src/images/png.zig - COMPLETAR
pub fn parse(allocator: Allocator, data: []const u8) !ImageInfo {
const meta = try parseMetadata(data);
// 1. Encontrar todos los chunks IDAT
var idat_data = ArrayList(u8).init(allocator);
defer idat_data.deinit();
var pos: usize = 8;
while (pos < data.len) {
const chunk = readChunkHeader(data, pos) orelse break;
if (std.mem.eql(u8, &chunk.chunk_type, "IDAT")) {
try idat_data.appendSlice(data[pos + 8 .. pos + 8 + chunk.length]);
}
pos += 8 + chunk.length + 4; // header + data + CRC
}
// 2. Descomprimir zlib
const raw = try zlib.decompress(allocator, idat_data.items);
defer allocator.free(raw);
// 3. Aplicar filtros PNG inversos (unfilter)
const unfiltered = try pngUnfilter(allocator, raw, meta);
defer allocator.free(unfiltered);
// 4. Separar RGB y Alpha si es RGBA
if (meta.color_type == .rgba) {
// Extraer alpha como soft mask
const rgb = try extractRgb(allocator, unfiltered, meta);
const alpha = try extractAlpha(allocator, unfiltered, meta);
// Comprimir ambos con FlateDecode para PDF
const rgb_compressed = try zlib.compress(allocator, rgb);
const alpha_compressed = try zlib.compress(allocator, alpha);
return ImageInfo{
.width = meta.width,
.height = meta.height,
.color_space = .device_rgb,
.bits_per_component = 8,
.filter = .flate_decode,
.data = rgb_compressed,
.soft_mask = alpha_compressed, // NUEVO campo
};
}
// Para RGB/Grayscale sin alpha
const compressed = try zlib.compress(allocator, unfiltered);
return ImageInfo{...};
}
fn pngUnfilter(allocator: Allocator, data: []const u8, meta: PngMetadata) ![]u8 {
// Implementar filtros PNG: None(0), Sub(1), Up(2), Average(3), Paeth(4)
const bytes_per_pixel = meta.channels * (meta.bit_depth / 8);
const row_bytes = meta.width * bytes_per_pixel;
var output = try allocator.alloc(u8, meta.height * row_bytes);
var prev_row: ?[]u8 = null;
for (0..meta.height) |y| {
const filter_type = data[y * (row_bytes + 1)];
const row_data = data[y * (row_bytes + 1) + 1 ..][0..row_bytes];
const out_row = output[y * row_bytes ..][0..row_bytes];
switch (filter_type) {
0 => @memcpy(out_row, row_data), // None
1 => unfilterSub(out_row, row_data, bytes_per_pixel),
2 => unfilterUp(out_row, row_data, prev_row),
3 => unfilterAverage(out_row, row_data, prev_row, bytes_per_pixel),
4 => unfilterPaeth(out_row, row_data, prev_row, bytes_per_pixel),
else => return error.InvalidFilter,
}
prev_row = out_row;
}
return output;
}
```
### 6.3 Soft Mask para transparencia
```zig
// src/images/image_info.zig - AGREGAR
pub const ImageInfo = struct {
// ... campos existentes ...
/// Soft mask for alpha channel (optional)
soft_mask: ?[]const u8 = null,
soft_mask_owns_data: bool = false,
pub fn deinit(self: *ImageInfo, allocator: Allocator) void {
if (self.owns_data) allocator.free(self.data);
if (self.soft_mask_owns_data) {
if (self.soft_mask) |sm| allocator.free(sm);
}
}
};
```
### 6.4 Actualizar OutputProducer para soft masks
```zig
// src/output/producer.zig - MODIFICAR
// En generateWithImages, despues de escribir XObjects:
if (img.soft_mask) |mask_data| {
// Escribir soft mask como objeto separado
const mask_id = first_image_id + images.len + @intCast(u32, mask_index);
try self.beginObject(mask_id);
try writer.writeAll("<< /Type /XObject\n");
try writer.writeAll("/Subtype /Image\n");
try writer.print("/Width {d}\n", .{img.width});
try writer.print("/Height {d}\n", .{img.height});
try writer.writeAll("/ColorSpace /DeviceGray\n");
try writer.writeAll("/BitsPerComponent 8\n");
try writer.writeAll("/Filter /FlateDecode\n");
try writer.print("/Length {d}\n", .{mask_data.len});
try writer.writeAll(">>\nstream\n");
try writer.writeAll(mask_data);
try writer.writeAll("\nendstream\n");
try self.endObject();
}
// En el XObject de la imagen principal:
if (img.soft_mask != null) {
try writer.print("/SMask {d} 0 R\n", .{soft_mask_obj_id});
}
```
### 6.5 Comprimir content streams
```zig
// src/output/producer.zig - MODIFICAR
// Opcion para comprimir content streams
pub const OutputOptions = struct {
compress_streams: bool = true,
};
// En generateWithImages:
const content_data = if (options.compress_streams)
try zlib.compress(allocator, page.content)
else
page.content;
try writer.writeAll("<< /Length ");
try writer.print("{d}", .{content_data.len});
if (options.compress_streams) {
try writer.writeAll(" /Filter /FlateDecode");
}
try writer.writeAll(" >>\nstream\n");
```
### 6.6 Tests para PNG
```zig
// src/images/png.zig - AGREGAR tests
test "parse RGB PNG" {
// Crear PNG de prueba o usar archivo de test
const allocator = std.testing.allocator;
const png_data = @embedFile("test_assets/test_rgb.png");
const info = try parse(allocator, png_data);
defer info.deinit(allocator);
try std.testing.expectEqual(info.color_space, .device_rgb);
try std.testing.expect(info.soft_mask == null);
}
test "parse RGBA PNG with alpha" {
const allocator = std.testing.allocator;
const png_data = @embedFile("test_assets/test_rgba.png");
const info = try parse(allocator, png_data);
defer info.deinit(allocator);
try std.testing.expect(info.soft_mask != null);
}
```
### Ejemplo: examples/png_demo.zig
```zig
const std = @import("std");
const pdf = @import("zcatpdf");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var doc = pdf.Pdf.init(allocator, .{});
defer doc.deinit();
var page = try doc.addPage(.{});
// PNG con transparencia sobre fondo de color
page.setFillColor(pdf.Color.rgb(255, 200, 200));
try page.fillRect(100, 500, 200, 200);
// La imagen PNG con alpha se blendea correctamente
const img_idx = try doc.addPngImageFromFile("logo.png");
try page.drawImage(img_idx, 150, 550, 100, 100);
try doc.save("png_demo.pdf");
}
```
---
## FASE 7: Fuentes TrueType (TTF)
**Prioridad**: ALTA
**Dependencias**: Compresion zlib (Fase 6)
**Archivos nuevos**: `src/fonts/ttf.zig`, `src/fonts/subset.zig`
### 7.1 Estructura TTF Parser
```zig
// src/fonts/ttf.zig
pub const TtfFont = struct {
allocator: Allocator,
data: []const u8,
owns_data: bool,
// Tablas TTF parseadas
head: HeadTable,
hhea: HheaTable,
maxp: MaxpTable,
hmtx: []HmtxEntry,
cmap: CmapTable,
glyf: ?GlyfTable, // Para TrueType outlines
loca: ?LocaTable,
post: PostTable,
name: NameTable,
os2: ?Os2Table,
// Metricas calculadas
units_per_em: u16,
ascender: i16,
descender: i16,
line_gap: i16,
const Self = @This();
pub fn initFromFile(allocator: Allocator, path: []const u8) !Self {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
const data = try file.readToEndAlloc(allocator, 10 * 1024 * 1024);
return try initFromData(allocator, data, true);
}
pub fn initFromData(allocator: Allocator, data: []const u8, owns: bool) !Self {
var self = Self{
.allocator = allocator,
.data = data,
.owns_data = owns,
// ...
};
try self.parseTables();
return self;
}
fn parseTables(self: *Self) !void {
// Leer offset table
const sfnt_version = readU32BE(self.data, 0);
if (sfnt_version != 0x00010000 and sfnt_version != 0x4F54544F) {
return error.InvalidTtfSignature;
}
const num_tables = readU16BE(self.data, 4);
// Leer table directory
var pos: usize = 12;
for (0..num_tables) |_| {
const tag = self.data[pos..][0..4].*;
const offset = readU32BE(self.data, pos + 8);
const length = readU32BE(self.data, pos + 12);
if (std.mem.eql(u8, &tag, "head")) {
self.head = try parseHead(self.data[offset..][0..length]);
} else if (std.mem.eql(u8, &tag, "hhea")) {
self.hhea = try parseHhea(self.data[offset..][0..length]);
}
// ... etc para cada tabla
pos += 16;
}
}
/// Obtiene el ancho de un glyph (en unidades de diseno)
pub fn getGlyphWidth(self: *const Self, glyph_id: u16) u16 {
if (glyph_id < self.hmtx.len) {
return self.hmtx[glyph_id].advance_width;
}
return self.hmtx[self.hmtx.len - 1].advance_width;
}
/// Convierte codepoint Unicode a glyph ID
pub fn charToGlyph(self: *const Self, codepoint: u21) u16 {
return self.cmap.lookup(codepoint) orelse 0;
}
/// Calcula ancho de string en puntos
pub fn stringWidth(self: *const Self, text: []const u8, size: f32) f32 {
var width: f32 = 0;
var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
while (iter.nextCodepoint()) |cp| {
const glyph = self.charToGlyph(cp);
const glyph_width = self.getGlyphWidth(glyph);
width += @as(f32, @floatFromInt(glyph_width)) * size / @as(f32, @floatFromInt(self.units_per_em));
}
return width;
}
};
```
### 7.2 CMap parsing (Unicode mapping)
```zig
// src/fonts/ttf.zig - CMap
pub const CmapTable = struct {
format: u16,
data: []const u8,
/// Lookup codepoint -> glyph ID
pub fn lookup(self: *const CmapTable, codepoint: u21) ?u16 {
switch (self.format) {
4 => return self.lookupFormat4(codepoint),
12 => return self.lookupFormat12(codepoint),
else => return null,
}
}
fn lookupFormat4(self: *const CmapTable, codepoint: u21) ?u16 {
if (codepoint > 0xFFFF) return null;
const cp16: u16 = @intCast(codepoint);
const seg_count = readU16BE(self.data, 6) / 2;
const end_codes = self.data[14..];
const start_codes = self.data[14 + seg_count * 2 + 2..];
const id_deltas = self.data[14 + seg_count * 4 + 2..];
const id_range_offsets = self.data[14 + seg_count * 6 + 2..];
for (0..seg_count) |i| {
const end = readU16BE(end_codes, i * 2);
if (cp16 <= end) {
const start = readU16BE(start_codes, i * 2);
if (cp16 >= start) {
const range_offset = readU16BE(id_range_offsets, i * 2);
if (range_offset == 0) {
const delta = readI16BE(id_deltas, i * 2);
return @bitCast(@as(i16, @intCast(cp16)) +% delta);
} else {
const offset = range_offset / 2 + (cp16 - start);
const glyph_addr = 14 + seg_count * 6 + 2 + i * 2 + offset * 2;
return readU16BE(self.data, glyph_addr);
}
}
}
}
return null;
}
};
```
### 7.3 Subsetting (solo incluir glyphs usados)
```zig
// src/fonts/subset.zig
pub const FontSubset = struct {
original: *const TtfFont,
used_glyphs: std.AutoHashMap(u16, void),
glyph_remap: std.AutoHashMap(u16, u16), // old_id -> new_id
pub fn init(allocator: Allocator, font: *const TtfFont) FontSubset {
return .{
.original = font,
.used_glyphs = std.AutoHashMap(u16, void).init(allocator),
.glyph_remap = std.AutoHashMap(u16, u16).init(allocator),
};
}
pub fn addText(self: *FontSubset, text: []const u8) !void {
var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
while (iter.nextCodepoint()) |cp| {
const glyph = self.original.charToGlyph(cp);
try self.used_glyphs.put(glyph, {});
// Tambien agregar glyphs de composites si aplica
}
}
/// Genera TTF subset con solo los glyphs usados
pub fn generateSubset(self: *FontSubset, allocator: Allocator) ![]u8 {
// 1. Asignar nuevos IDs a glyphs (0=.notdef siempre primero)
var new_id: u16 = 0;
try self.glyph_remap.put(0, 0);
new_id += 1;
var iter = self.used_glyphs.keyIterator();
while (iter.next()) |glyph| {
if (glyph.* != 0) {
try self.glyph_remap.put(glyph.*, new_id);
new_id += 1;
}
}
// 2. Construir nuevas tablas
const new_glyf = try self.buildGlyfTable(allocator);
const new_loca = try self.buildLocaTable(allocator);
const new_hmtx = try self.buildHmtxTable(allocator);
const new_cmap = try self.buildCmapTable(allocator);
// 3. Ensamblar TTF
return try self.assembleTtf(allocator, new_glyf, new_loca, new_hmtx, new_cmap);
}
};
```
### 7.4 Embeber TTF en PDF
```zig
// src/fonts/embedded.zig
pub const EmbeddedFont = struct {
name: []const u8, // Nombre unico en PDF (ej: "F1")
ttf: *const TtfFont,
subset: ?*FontSubset,
/// Genera el objeto Font para PDF
pub fn generatePdfObjects(self: *EmbeddedFont, producer: *OutputProducer) !struct {
font_obj_id: u32,
descriptor_obj_id: u32,
file_obj_id: u32,
tounicode_obj_id: u32,
} {
// Font dictionary
// << /Type /Font
// /Subtype /TrueType (o /Type0 para CID)
// /BaseFont /FontName+Subset
// /FontDescriptor 5 0 R
// /ToUnicode 6 0 R
// /Encoding /WinAnsiEncoding (o CMap para CID)
// >>
// FontDescriptor
// << /Type /FontDescriptor
// /FontName /FontName+Subset
// /Flags 32
// /FontBBox [...]
// /ItalicAngle 0
// /Ascent 800
// /Descent -200
// /CapHeight 700
// /StemV 80
// /FontFile2 7 0 R (TTF data)
// >>
// FontFile2 (compressed TTF)
// << /Length ... /Length1 ... /Filter /FlateDecode >>
// stream
// [compressed TTF subset data]
// endstream
}
/// Genera ToUnicode CMap para busqueda de texto
pub fn generateToUnicode(self: *EmbeddedFont, allocator: Allocator) ![]u8 {
var buf = ArrayList(u8).init(allocator);
const w = buf.writer();
try w.writeAll("/CIDInit /ProcSet findresource begin\n");
try w.writeAll("12 dict begin\n");
try w.writeAll("begincmap\n");
try w.writeAll("/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n");
try w.writeAll("/CMapName /Adobe-Identity-UCS def\n");
try w.writeAll("/CMapType 2 def\n");
try w.writeAll("1 begincodespacerange\n");
try w.writeAll("<0000> <FFFF>\n");
try w.writeAll("endcodespacerange\n");
// Mapear glyph IDs a Unicode
// ...
try w.writeAll("endcmap\n");
try w.writeAll("CMapName currentdict /CMap defineresource pop\n");
try w.writeAll("end end\n");
return buf.toOwnedSlice();
}
};
```
### 7.5 API para usuario
```zig
// src/pdf.zig - AGREGAR
pub const Pdf = struct {
// ... campos existentes ...
embedded_fonts: std.ArrayListUnmanaged(EmbeddedFont),
/// Carga una fuente TTF desde archivo
pub fn loadFont(self: *Self, path: []const u8) !FontHandle {
const ttf = try TtfFont.initFromFile(self.allocator, path);
try self.embedded_fonts.append(self.allocator, .{
.ttf = ttf,
.subset = null,
});
return FontHandle{ .index = self.embedded_fonts.items.len - 1 };
}
/// Carga una fuente TTF desde memoria
pub fn loadFontFromMemory(self: *Self, data: []const u8) !FontHandle {
const ttf = try TtfFont.initFromData(self.allocator, data, false);
// ...
}
};
pub const FontHandle = struct {
index: usize,
};
// src/page.zig - AGREGAR
pub const Page = struct {
/// Usa fuente TTF embebida
pub fn setTtfFont(self: *Self, handle: FontHandle, size: f32) !void {
self.current_ttf_font = handle;
self.current_font_size = size;
// Agregar a fonts_used
}
};
```
### Ejemplo: examples/ttf_demo.zig
```zig
const std = @import("std");
const pdf = @import("zcatpdf");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var doc = pdf.Pdf.init(allocator, .{});
defer doc.deinit();
// Cargar fuentes TTF
const roboto = try doc.loadFont("fonts/Roboto-Regular.ttf");
const roboto_bold = try doc.loadFont("fonts/Roboto-Bold.ttf");
var page = try doc.addPage(.{});
// Usar fuentes TTF
try page.setTtfFont(roboto_bold, 24);
try page.drawText(50, 750, "Hola Mundo con TTF!");
try page.setTtfFont(roboto, 12);
try page.drawText(50, 720, "Texto con caracteres especiales: cafe, nino, corazon");
// El subsetting se hace automaticamente al guardar
try doc.save("ttf_demo.pdf");
}
```
---
## FASE 8: Bookmarks / Outline
**Prioridad**: ALTA
**Dependencias**: Ninguna
**Archivos nuevos**: `src/outline.zig`
### 8.1 Estructura de Outline
```zig
// src/outline.zig
pub const OutlineItem = struct {
title: []const u8,
page_index: usize, // Pagina destino (0-based)
y_position: ?f32 = null, // Posicion Y en la pagina (opcional)
children: std.ArrayListUnmanaged(OutlineItem),
// Campos internos para serialization
obj_id: u32 = 0,
parent_obj_id: u32 = 0,
pub fn init(allocator: Allocator, title: []const u8, page: usize) OutlineItem {
return .{
.title = title,
.page_index = page,
.children = .{},
};
}
pub fn addChild(self: *OutlineItem, allocator: Allocator, title: []const u8, page: usize) !*OutlineItem {
const child = OutlineItem.init(allocator, title, page);
try self.children.append(allocator, child);
return &self.children.items[self.children.items.len - 1];
}
pub fn deinit(self: *OutlineItem, allocator: Allocator) void {
for (self.children.items) |*child| {
child.deinit(allocator);
}
self.children.deinit(allocator);
}
};
pub const Outline = struct {
allocator: Allocator,
items: std.ArrayListUnmanaged(OutlineItem),
pub fn init(allocator: Allocator) Outline {
return .{
.allocator = allocator,
.items = .{},
};
}
pub fn addItem(self: *Outline, title: []const u8, page: usize) !*OutlineItem {
const item = OutlineItem.init(self.allocator, title, page);
try self.items.append(self.allocator, item);
return &self.items.items[self.items.items.len - 1];
}
/// Cuenta total de items (incluyendo hijos)
pub fn totalCount(self: *const Outline) usize {
var count: usize = 0;
for (self.items.items) |*item| {
count += 1 + countChildren(item);
}
return count;
}
fn countChildren(item: *const OutlineItem) usize {
var count: usize = 0;
for (item.children.items) |*child| {
count += 1 + countChildren(child);
}
return count;
}
};
```
### 8.2 Serializar Outline en PDF
```zig
// src/output/producer.zig - MODIFICAR
pub fn generateWithImages(..., outline: ?*const Outline) ![]u8 {
// ...
// Calcular IDs de objetos
// ... existentes ...
const first_outline_id: u32 = first_page_id + pages.len * 2;
const outline_count = if (outline) |o| o.totalCount() else 0;
// En Catalog, agregar referencia a Outlines
if (outline != null) {
try writer.print("/Outlines {d} 0 R\n", .{first_outline_id});
try writer.writeAll("/PageMode /UseOutlines\n"); // Abrir con bookmarks visibles
}
// Escribir objetos de outline
if (outline) |o| {
try self.writeOutlines(o, first_outline_id, first_page_id);
}
}
fn writeOutlines(self: *Self, outline: *const Outline, first_id: u32, first_page_id: u32) !void {
const writer = self.buffer.writer(self.allocator);
// Objeto raiz de Outlines
try self.beginObject(first_id);
try writer.writeAll("<< /Type /Outlines\n");
if (outline.items.items.len > 0) {
try writer.print("/First {d} 0 R\n", .{first_id + 1});
try writer.print("/Last {d} 0 R\n", .{first_id + countUntilLast(outline)});
try writer.print("/Count {d}\n", .{outline.totalCount()});
} else {
try writer.writeAll("/Count 0\n");
}
try writer.writeAll(">>\n");
try self.endObject();
// Escribir items recursivamente
var current_id = first_id + 1;
for (outline.items.items, 0..) |*item, i| {
try self.writeOutlineItem(item, &current_id, first_id, first_page_id,
if (i > 0) &outline.items.items[i - 1] else null,
if (i < outline.items.items.len - 1) &outline.items.items[i + 1] else null);
}
}
fn writeOutlineItem(self: *Self, item: *const OutlineItem, current_id: *u32, parent_id: u32,
first_page_id: u32, prev: ?*const OutlineItem, next: ?*const OutlineItem) !void {
const writer = self.buffer.writer(self.allocator);
const my_id = current_id.*;
current_id.* += 1;
try self.beginObject(my_id);
try writer.writeAll("<< /Title ");
try writeString(writer, item.title);
try writer.writeByte('\n');
try writer.print("/Parent {d} 0 R\n", .{parent_id});
if (prev) |p| {
try writer.print("/Prev {d} 0 R\n", .{p.obj_id});
}
if (next) |n| {
try writer.print("/Next {d} 0 R\n", .{n.obj_id});
}
// Destino
const page_obj_id = first_page_id + item.page_index * 2;
if (item.y_position) |y| {
try writer.print("/Dest [{d} 0 R /XYZ 0 {d:.2} 0]\n", .{page_obj_id, y});
} else {
try writer.print("/Dest [{d} 0 R /Fit]\n", .{page_obj_id});
}
// Hijos
if (item.children.items.len > 0) {
try writer.print("/First {d} 0 R\n", .{current_id.*});
// ... similar recursion
}
try writer.writeAll(">>\n");
try self.endObject();
// Procesar hijos
for (item.children.items) |*child| {
try self.writeOutlineItem(child, current_id, my_id, first_page_id, ...);
}
}
```
### 8.3 API para usuario
```zig
// src/pdf.zig - AGREGAR
pub const Pdf = struct {
outline: ?Outline = null,
/// Crea o retorna el outline del documento
pub fn getOutline(self: *Self) *Outline {
if (self.outline == null) {
self.outline = Outline.init(self.allocator);
}
return &self.outline.?;
}
/// Agrega un bookmark de nivel superior
pub fn addBookmark(self: *Self, title: []const u8, page_index: usize) !*OutlineItem {
return try self.getOutline().addItem(title, page_index);
}
};
```
### Ejemplo: examples/bookmarks_demo.zig
```zig
const std = @import("std");
const pdf = @import("zcatpdf");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var doc = pdf.Pdf.init(allocator, .{});
defer doc.deinit();
// Pagina 1: Introduccion
var page1 = try doc.addPage(.{});
try page1.setFont(.helvetica_bold, 24);
try page1.drawText(50, 750, "Introduccion");
// Pagina 2: Capitulo 1
var page2 = try doc.addPage(.{});
try page2.setFont(.helvetica_bold, 24);
try page2.drawText(50, 750, "Capitulo 1: Conceptos Basicos");
try page2.setFont(.helvetica_bold, 18);
try page2.drawText(50, 700, "1.1 Fundamentos");
try page2.drawText(50, 600, "1.2 Aplicaciones");
// Pagina 3: Capitulo 2
var page3 = try doc.addPage(.{});
try page3.setFont(.helvetica_bold, 24);
try page3.drawText(50, 750, "Capitulo 2: Avanzado");
// Crear bookmarks jerarquicos
_ = try doc.addBookmark("Introduccion", 0);
var cap1 = try doc.addBookmark("Capitulo 1: Conceptos Basicos", 1);
_ = try cap1.addChild(allocator, "1.1 Fundamentos", 1); // .y_position = 700
_ = try cap1.addChild(allocator, "1.2 Aplicaciones", 1); // .y_position = 600
_ = try doc.addBookmark("Capitulo 2: Avanzado", 2);
try doc.save("bookmarks_demo.pdf");
}
```
---
## FASE 9: Curvas Bezier y Graficos Avanzados
**Prioridad**: MEDIA
**Dependencias**: Ninguna
**Archivos a modificar**: `src/content_stream.zig`, `src/page.zig`
### 9.1 Operadores PDF para curvas
```zig
// src/content_stream.zig - AGREGAR
pub const ContentStream = struct {
// ... existente ...
/// Cubic Bezier curve (PDF 'c' operator)
/// Dibuja curva desde punto actual a (x3,y3) con puntos de control (x1,y1) y (x2,y2)
pub fn curveTo(self: *Self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) !void {
try self.fmt("{d:.2} {d:.2} {d:.2} {d:.2} {d:.2} {d:.2} c\n",
.{x1, y1, x2, y2, x3, y3});
}
/// Cubic Bezier con primer punto de control = punto actual (PDF 'v' operator)
pub fn curveToV(self: *Self, x2: f32, y2: f32, x3: f32, y3: f32) !void {
try self.fmt("{d:.2} {d:.2} {d:.2} {d:.2} v\n", .{x2, y2, x3, y3});
}
/// Cubic Bezier con segundo punto de control = punto final (PDF 'y' operator)
pub fn curveToY(self: *Self, x1: f32, y1: f32, x3: f32, y3: f32) !void {
try self.fmt("{d:.2} {d:.2} {d:.2} {d:.2} y\n", .{x1, y1, x3, y3});
}
/// Cerrar subpath y stroke
pub fn closeStroke(self: *Self) !void {
try self.appendLiteral("s\n");
}
/// Cerrar, fill y stroke
pub fn closeFillStroke(self: *Self) !void {
try self.appendLiteral("b\n");
}
/// Fill con regla even-odd
pub fn fillEvenOdd(self: *Self) !void {
try self.appendLiteral("f*\n");
}
/// Clip path
pub fn clip(self: *Self) !void {
try self.appendLiteral("W n\n");
}
/// Clip con even-odd
pub fn clipEvenOdd(self: *Self) !void {
try self.appendLiteral("W* n\n");
}
};
```
### 9.2 API de alto nivel para curvas
```zig
// src/page.zig - AGREGAR
pub const Page = struct {
/// Dibuja curva Bezier cubica
pub fn drawBezier(self: *Self, x1: f32, y1: f32, x2: f32, y2: f32,
x3: f32, y3: f32, x4: f32, y4: f32) !void {
try self.stream.moveTo(x1, y1);
try self.stream.curveTo(x2, y2, x3, y3, x4, y4);
try self.stream.stroke();
}
/// Dibuja curva Bezier cuadratica (convertida a cubica)
pub fn drawQuadBezier(self: *Self, x1: f32, y1: f32, cx: f32, cy: f32, x2: f32, y2: f32) !void {
// Convertir cuadratica a cubica
// CP1 = P0 + 2/3 * (CP - P0)
// CP2 = P2 + 2/3 * (CP - P2)
const cp1x = x1 + 2.0/3.0 * (cx - x1);
const cp1y = y1 + 2.0/3.0 * (cy - y1);
const cp2x = x2 + 2.0/3.0 * (cx - x2);
const cp2y = y2 + 2.0/3.0 * (cy - y2);
try self.stream.moveTo(x1, y1);
try self.stream.curveTo(cp1x, cp1y, cp2x, cp2y, x2, y2);
try self.stream.stroke();
}
/// Dibuja arco de circunferencia
pub fn drawArc(self: *Self, cx: f32, cy: f32, radius: f32,
start_angle: f32, end_angle: f32) !void {
// Aproximar arco con curvas Bezier (maximo 90 grados por segmento)
const segments = @ceil(@abs(end_angle - start_angle) / (std.math.pi / 2.0));
const angle_per_seg = (end_angle - start_angle) / segments;
var angle = start_angle;
const start_x = cx + radius * @cos(angle);
const start_y = cy + radius * @sin(angle);
try self.stream.moveTo(start_x, start_y);
for (0..@intFromFloat(segments)) |_| {
const next_angle = angle + angle_per_seg;
try self.appendArcSegment(cx, cy, radius, angle, next_angle);
angle = next_angle;
}
try self.stream.stroke();
}
fn appendArcSegment(self: *Self, cx: f32, cy: f32, r: f32, a1: f32, a2: f32) !void {
// Aproximacion de Bezier para arco
// https://pomax.github.io/bezierinfo/#circles_cubic
const da = a2 - a1;
const k = 4.0 / 3.0 * @tan(da / 4.0);
const x1 = cx + r * @cos(a1);
const y1 = cy + r * @sin(a1);
const x4 = cx + r * @cos(a2);
const y4 = cy + r * @sin(a2);
const x2 = x1 - k * r * @sin(a1);
const y2 = y1 + k * r * @cos(a1);
const x3 = x4 + k * r * @sin(a2);
const y3 = y4 - k * r * @cos(a2);
try self.stream.curveTo(x2, y2, x3, y3, x4, y4);
}
/// Dibuja poligono cerrado
pub fn drawPolygon(self: *Self, points: []const [2]f32) !void {
if (points.len < 3) return;
try self.stream.moveTo(points[0][0], points[0][1]);
for (points[1..]) |p| {
try self.stream.lineTo(p[0], p[1]);
}
try self.stream.closePath();
try self.stream.stroke();
}
/// Rellena poligono
pub fn fillPolygon(self: *Self, points: []const [2]f32) !void {
if (points.len < 3) return;
try self.stream.moveTo(points[0][0], points[0][1]);
for (points[1..]) |p| {
try self.stream.lineTo(p[0], p[1]);
}
try self.stream.closePath();
try self.stream.fill();
}
/// Dibuja rectangulo con esquinas redondeadas
pub fn drawRoundedRect(self: *Self, x: f32, y: f32, w: f32, h: f32, r: f32) !void {
// Constante para aproximar curva circular con Bezier
const k: f32 = 0.5522847498; // 4/3 * (sqrt(2) - 1)
const kr = k * r;
try self.stream.moveTo(x + r, y);
try self.stream.lineTo(x + w - r, y);
try self.stream.curveTo(x + w - r + kr, y, x + w, y + r - kr, x + w, y + r);
try self.stream.lineTo(x + w, y + h - r);
try self.stream.curveTo(x + w, y + h - r + kr, x + w - r + kr, y + h, x + w - r, y + h);
try self.stream.lineTo(x + r, y + h);
try self.stream.curveTo(x + r - kr, y + h, x, y + h - r + kr, x, y + h - r);
try self.stream.lineTo(x, y + r);
try self.stream.curveTo(x, y + r - kr, x + r - kr, y, x + r, y);
try self.stream.closePath();
try self.stream.stroke();
}
pub fn fillRoundedRect(self: *Self, x: f32, y: f32, w: f32, h: f32, r: f32) !void {
// Similar pero con fill
// ...
try self.stream.fill();
}
};
```
### Ejemplo: examples/curves_demo.zig
```zig
const std = @import("std");
const pdf = @import("zcatpdf");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var doc = pdf.Pdf.init(allocator, .{});
defer doc.deinit();
var page = try doc.addPage(.{});
try page.setFont(.helvetica_bold, 16);
try page.drawText(50, 800, "Curvas Bezier Demo");
// Curva Bezier cubica
page.setStrokeColor(pdf.Color.blue);
try page.setLineWidth(2);
try page.drawBezier(100, 700, 150, 750, 250, 650, 300, 700);
// Arco
page.setStrokeColor(pdf.Color.red);
try page.drawArc(400, 700, 50, 0, std.math.pi);
// Rectangulo redondeado
page.setStrokeColor(pdf.Color.rgb(0, 128, 0));
try page.drawRoundedRect(100, 500, 200, 100, 15);
// Poligono (estrella)
page.setStrokeColor(pdf.Color.rgb(255, 165, 0));
const star_points = [_][2]f32{
.{400, 600}, .{420, 550}, .{470, 550},
.{430, 520}, .{450, 470}, .{400, 500},
.{350, 470}, .{370, 520}, .{330, 550},
.{380, 550},
};
try page.fillPolygon(&star_points);
try doc.save("curves_demo.pdf");
}
```
---
## FASE 10: Rotacion y Transformaciones
**Prioridad**: MEDIA
**Dependencias**: Ninguna
**Archivos a modificar**: `src/content_stream.zig`, `src/page.zig`
### 10.1 Operadores de transformacion
```zig
// src/content_stream.zig - AGREGAR
pub const ContentStream = struct {
/// Matriz de transformacion (PDF 'cm' operator)
/// | a b 0 |
/// | c d 0 |
/// | e f 1 |
pub fn transform(self: *Self, a: f32, b: f32, c: f32, d: f32, e: f32, f_: f32) !void {
try self.fmt("{d:.4} {d:.4} {d:.4} {d:.4} {d:.2} {d:.2} cm\n",
.{a, b, c, d, e, f_});
}
/// Traslacion
pub fn translate(self: *Self, tx: f32, ty: f32) !void {
try self.transform(1, 0, 0, 1, tx, ty);
}
/// Escala
pub fn scale(self: *Self, sx: f32, sy: f32) !void {
try self.transform(sx, 0, 0, sy, 0, 0);
}
/// Rotacion (angulo en radianes)
pub fn rotate(self: *Self, angle: f32) !void {
const c = @cos(angle);
const s = @sin(angle);
try self.transform(c, s, -s, c, 0, 0);
}
/// Rotacion alrededor de un punto
pub fn rotateAround(self: *Self, angle: f32, cx: f32, cy: f32) !void {
try self.translate(cx, cy);
try self.rotate(angle);
try self.translate(-cx, -cy);
}
/// Skew/Shear
pub fn skew(self: *Self, angle_x: f32, angle_y: f32) !void {
try self.transform(1, @tan(angle_y), @tan(angle_x), 1, 0, 0);
}
};
```
### 10.2 API de alto nivel
```zig
// src/page.zig - AGREGAR
pub const Page = struct {
/// Dibuja texto rotado
pub fn drawTextRotated(self: *Self, x: f32, y: f32, text: []const u8, angle_deg: f32) !void {
const angle_rad = angle_deg * std.math.pi / 180.0;
try self.stream.saveState();
try self.stream.translate(x, y);
try self.stream.rotate(angle_rad);
// Dibujar texto en origen (0,0)
try self.stream.beginText();
try self.stream.setFont(self.current_font.?.pdfName(), self.current_font_size);
try self.stream.setTextPosition(0, 0);
try self.stream.showText(text);
try self.stream.endText();
try self.stream.restoreState();
}
/// Dibuja imagen rotada
pub fn drawImageRotated(self: *Self, img_index: usize, x: f32, y: f32,
w: f32, h: f32, angle_deg: f32) !void {
const angle_rad = angle_deg * std.math.pi / 180.0;
try self.stream.saveState();
try self.stream.translate(x + w/2, y + h/2); // Centro de la imagen
try self.stream.rotate(angle_rad);
try self.stream.translate(-w/2, -h/2);
try self.stream.scale(w, h);
var buf: [16]u8 = undefined;
const img_name = std.fmt.bufPrint(&buf, "/I{d}", .{img_index}) catch unreachable;
try self.stream.drawXObject(img_name);
try self.stream.restoreState();
}
/// Ejecuta operaciones dentro de un contexto transformado
pub fn withTransform(self: *Self, transform_fn: fn(*Self) anyerror!void,
a: f32, b: f32, c: f32, d: f32, e: f32, f_: f32) !void {
try self.stream.saveState();
try self.stream.transform(a, b, c, d, e, f_);
try transform_fn(self);
try self.stream.restoreState();
}
};
```
### Ejemplo: examples/rotation_demo.zig
```zig
const std = @import("std");
const pdf = @import("zcatpdf");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var doc = pdf.Pdf.init(allocator, .{});
defer doc.deinit();
var page = try doc.addPage(.{});
try page.setFont(.helvetica_bold, 14);
// Texto a diferentes angulos
try page.drawTextRotated(300, 400, "0 grados", 0);
try page.drawTextRotated(300, 400, "45 grados", 45);
try page.drawTextRotated(300, 400, "90 grados", 90);
try page.drawTextRotated(300, 400, "180 grados", 180);
try page.drawTextRotated(300, 400, "270 grados", 270);
// Marca de agua diagonal
try page.setFont(.helvetica_bold, 60);
page.setFillColor(pdf.Color.rgba(200, 200, 200, 128)); // Gris semi-transparente
try page.drawTextRotated(150, 300, "BORRADOR", 45);
try doc.save("rotation_demo.pdf");
}
```
---
## FASE 11: Transparencia y Alpha
**Prioridad**: MEDIA
**Dependencias**: Ninguna
**Archivos a modificar**: `src/content_stream.zig`, `src/page.zig`, `src/output/producer.zig`
### 11.1 ExtGState para transparencia
```zig
// src/graphics/ext_gstate.zig - NUEVO
pub const ExtGState = struct {
name: []const u8, // Ej: "GS1"
fill_alpha: ?f32, // CA: fill opacity (0-1)
stroke_alpha: ?f32, // ca: stroke opacity
blend_mode: ?BlendMode,
pub const BlendMode = enum {
normal,
multiply,
screen,
overlay,
darken,
lighten,
color_dodge,
color_burn,
hard_light,
soft_light,
difference,
exclusion,
pub fn pdfName(self: BlendMode) []const u8 {
return switch (self) {
.normal => "Normal",
.multiply => "Multiply",
.screen => "Screen",
.overlay => "Overlay",
// ... etc
};
}
};
pub fn toPdfDict(self: *const ExtGState, writer: anytype) !void {
try writer.writeAll("<< /Type /ExtGState\n");
if (self.fill_alpha) |a| {
try writer.print("/CA {d:.3}\n", .{a});
}
if (self.stroke_alpha) |a| {
try writer.print("/ca {d:.3}\n", .{a});
}
if (self.blend_mode) |bm| {
try writer.print("/BM /{s}\n", .{bm.pdfName()});
}
try writer.writeAll(">>");
}
};
```
### 11.2 Gestionar estados graficos en Page
```zig
// src/page.zig - AGREGAR
pub const Page = struct {
ext_gstates: std.ArrayListUnmanaged(ExtGState),
current_alpha: f32 = 1.0,
/// Establece opacidad para fill (0.0 - 1.0)
pub fn setFillAlpha(self: *Self, alpha: f32) !void {
const gs_name = try self.getOrCreateGState(.{ .fill_alpha = alpha });
try self.stream.fmt("/{s} gs\n", .{gs_name});
self.current_alpha = alpha;
}
/// Establece opacidad para stroke (0.0 - 1.0)
pub fn setStrokeAlpha(self: *Self, alpha: f32) !void {
const gs_name = try self.getOrCreateGState(.{ .stroke_alpha = alpha });
try self.stream.fmt("/{s} gs\n", .{gs_name});
}
/// Establece ambas opacidades
pub fn setAlpha(self: *Self, alpha: f32) !void {
const gs_name = try self.getOrCreateGState(.{
.fill_alpha = alpha,
.stroke_alpha = alpha
});
try self.stream.fmt("/{s} gs\n", .{gs_name});
self.current_alpha = alpha;
}
/// Establece modo de mezcla
pub fn setBlendMode(self: *Self, mode: ExtGState.BlendMode) !void {
const gs_name = try self.getOrCreateGState(.{ .blend_mode = mode });
try self.stream.fmt("/{s} gs\n", .{gs_name});
}
fn getOrCreateGState(self: *Self, params: struct {
fill_alpha: ?f32 = null,
stroke_alpha: ?f32 = null,
blend_mode: ?ExtGState.BlendMode = null,
}) ![]const u8 {
// Buscar estado existente con mismos parametros
for (self.ext_gstates.items) |gs| {
if (gs.fill_alpha == params.fill_alpha and
gs.stroke_alpha == params.stroke_alpha and
gs.blend_mode == params.blend_mode) {
return gs.name;
}
}
// Crear nuevo
var buf: [8]u8 = undefined;
const name = try std.fmt.bufPrint(&buf, "GS{d}", .{self.ext_gstates.items.len});
const name_copy = try self.allocator.dupe(u8, name);
try self.ext_gstates.append(self.allocator, .{
.name = name_copy,
.fill_alpha = params.fill_alpha,
.stroke_alpha = params.stroke_alpha,
.blend_mode = params.blend_mode,
});
return name_copy;
}
};
```
### 11.3 Actualizar OutputProducer
```zig
// src/output/producer.zig - MODIFICAR
// En Resources de cada pagina:
if (page.ext_gstates.len > 0) {
try writer.writeAll(" /ExtGState <<\n");
for (page.ext_gstates) |gs| {
try writer.print(" /{s} {d} 0 R\n", .{gs.name, gs_obj_id});
}
try writer.writeAll(" >>\n");
}
// Escribir objetos ExtGState
for (page.ext_gstates, 0..) |gs, i| {
const gs_id = first_gs_id + i;
try self.beginObject(gs_id);
try gs.toPdfDict(writer);
try writer.writeByte('\n');
try self.endObject();
}
```
### Ejemplo: examples/transparency_demo.zig
```zig
const std = @import("std");
const pdf = @import("zcatpdf");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var doc = pdf.Pdf.init(allocator, .{});
defer doc.deinit();
var page = try doc.addPage(.{});
// Rectangulos superpuestos con transparencia
page.setFillColor(pdf.Color.red);
try page.setFillAlpha(0.5);
try page.fillRect(100, 600, 150, 150);
page.setFillColor(pdf.Color.blue);
try page.setFillAlpha(0.5);
try page.fillRect(175, 550, 150, 150);
page.setFillColor(pdf.Color.rgb(0, 200, 0));
try page.setFillAlpha(0.5);
try page.fillRect(150, 500, 150, 150);
// Restablecer opacidad
try page.setFillAlpha(1.0);
// Texto con diferentes opacidades
try page.setFont(.helvetica_bold, 24);
page.setFillColor(pdf.Color.black);
try page.drawText(100, 400, "100% opaco");
try page.setFillAlpha(0.5);
try page.drawText(100, 360, "50% opaco");
try page.setFillAlpha(0.25);
try page.drawText(100, 320, "25% opaco");
try doc.save("transparency_demo.pdf");
}
```
---
## FASE 12: Gradientes
**Prioridad**: MEDIA
**Dependencias**: Transparencia (Fase 11)
**Archivos nuevos**: `src/graphics/gradient.zig`
### 12.1 Tipos de gradientes
```zig
// src/graphics/gradient.zig
pub const Gradient = struct {
gradient_type: GradientType,
color_stops: []const ColorStop,
// Para linear
start_point: ?[2]f32 = null,
end_point: ?[2]f32 = null,
// Para radial
center: ?[2]f32 = null,
radius: ?f32 = null,
pub const GradientType = enum {
linear, // Shading type 2
radial, // Shading type 3
};
pub const ColorStop = struct {
position: f32, // 0.0 - 1.0
color: Color,
};
};
pub const GradientBuilder = struct {
allocator: Allocator,
stops: std.ArrayListUnmanaged(Gradient.ColorStop),
pub fn init(allocator: Allocator) GradientBuilder {
return .{
.allocator = allocator,
.stops = .{},
};
}
pub fn addStop(self: *GradientBuilder, position: f32, color: Color) !void {
try self.stops.append(self.allocator, .{
.position = position,
.color = color,
});
}
pub fn buildLinear(self: *GradientBuilder, x1: f32, y1: f32, x2: f32, y2: f32) Gradient {
return .{
.gradient_type = .linear,
.color_stops = self.stops.items,
.start_point = .{x1, y1},
.end_point = .{x2, y2},
};
}
pub fn buildRadial(self: *GradientBuilder, cx: f32, cy: f32, r: f32) Gradient {
return .{
.gradient_type = .radial,
.color_stops = self.stops.items,
.center = .{cx, cy},
.radius = r,
};
}
};
```
### 12.2 Serializar gradiente en PDF
```zig
// src/graphics/gradient.zig - continuar
pub fn toPdfShading(self: *const Gradient, writer: anytype, shading_id: u32) !void {
// PDF Shading dictionary
try writer.writeAll("<< /ShadingType ");
switch (self.gradient_type) {
.linear => {
try writer.writeAll("2\n"); // Axial shading
try writer.print("/Coords [{d:.2} {d:.2} {d:.2} {d:.2}]\n", .{
self.start_point.?[0], self.start_point.?[1],
self.end_point.?[0], self.end_point.?[1],
});
},
.radial => {
try writer.writeAll("3\n"); // Radial shading
try writer.print("/Coords [{d:.2} {d:.2} 0 {d:.2} {d:.2} {d:.2}]\n", .{
self.center.?[0], self.center.?[1],
self.center.?[0], self.center.?[1], self.radius.?,
});
},
}
try writer.writeAll("/ColorSpace /DeviceRGB\n");
try writer.writeAll("/Function << /FunctionType 3\n");
// Encode color stops as stitching function
try self.writeColorFunction(writer);
try writer.writeAll(">>\n");
try writer.writeAll(">>");
}
fn writeColorFunction(self: *const Gradient, writer: anytype) !void {
// Type 3 (stitching) function que interpola entre color stops
try writer.writeAll("/Domain [0 1]\n");
try writer.writeAll("/Functions [\n");
for (self.color_stops[0..self.color_stops.len-1], 0..) |stop, i| {
const next = self.color_stops[i + 1];
// Type 2 (exponential) function para cada segmento
try writer.writeAll(" << /FunctionType 2 /Domain [0 1] /N 1\n");
const c1 = stop.color.toRgbFloats();
const c2 = next.color.toRgbFloats();
try writer.print(" /C0 [{d:.3} {d:.3} {d:.3}]\n", .{c1.r, c1.g, c1.b});
try writer.print(" /C1 [{d:.3} {d:.3} {d:.3}] >>\n", .{c2.r, c2.g, c2.b});
}
try writer.writeAll("]\n");
// Bounds
try writer.writeAll("/Bounds [");
for (self.color_stops[1..self.color_stops.len-1]) |stop| {
try writer.print("{d:.3} ", .{stop.position});
}
try writer.writeAll("]\n");
// Encode
try writer.writeAll("/Encode [");
for (0..self.color_stops.len-1) |_| {
try writer.writeAll("0 1 ");
}
try writer.writeAll("]\n");
}
```
### 12.3 API de alto nivel
```zig
// src/page.zig - AGREGAR
pub const Page = struct {
shadings: std.ArrayListUnmanaged(Gradient),
/// Rellena rectangulo con gradiente linear
pub fn fillRectGradient(self: *Self, x: f32, y: f32, w: f32, h: f32, gradient: Gradient) !void {
const shading_name = try self.addShading(gradient);
try self.stream.saveState();
// Clip al rectangulo
try self.stream.rect(x, y, w, h);
try self.stream.clip();
// Aplicar shading
try self.stream.fmt("/{s} sh\n", .{shading_name});
try self.stream.restoreState();
}
/// Crea gradiente linear simple (2 colores)
pub fn linearGradient(start_color: Color, end_color: Color,
x1: f32, y1: f32, x2: f32, y2: f32) Gradient {
return .{
.gradient_type = .linear,
.color_stops = &[_]Gradient.ColorStop{
.{ .position = 0, .color = start_color },
.{ .position = 1, .color = end_color },
},
.start_point = .{x1, y1},
.end_point = .{x2, y2},
};
}
};
```
### Ejemplo: examples/gradient_demo.zig
```zig
const std = @import("std");
const pdf = @import("zcatpdf");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var doc = pdf.Pdf.init(allocator, .{});
defer doc.deinit();
var page = try doc.addPage(.{});
// Gradiente linear horizontal
const grad1 = page.linearGradient(
pdf.Color.red, pdf.Color.blue,
100, 700, 300, 700
);
try page.fillRectGradient(100, 650, 200, 100, grad1);
// Gradiente linear vertical
const grad2 = page.linearGradient(
pdf.Color.rgb(255, 255, 0), pdf.Color.rgb(0, 128, 0),
350, 750, 350, 650
);
try page.fillRectGradient(350, 650, 200, 100, grad2);
// Gradiente con multiples stops
var builder = pdf.GradientBuilder.init(allocator);
try builder.addStop(0.0, pdf.Color.red);
try builder.addStop(0.33, pdf.Color.rgb(255, 165, 0));
try builder.addStop(0.66, pdf.Color.rgb(255, 255, 0));
try builder.addStop(1.0, pdf.Color.rgb(0, 128, 0));
const rainbow = builder.buildLinear(100, 500, 500, 500);
try page.fillRectGradient(100, 450, 400, 80, rainbow);
try doc.save("gradient_demo.pdf");
}
```
---
## FASE 13: Codigos de Barras
**Prioridad**: MEDIA
**Dependencias**: Ninguna
**Archivos nuevos**: `src/barcode/`, con Code128, Code39, EAN, QR
### 13.1 Code128
```zig
// src/barcode/code128.zig
pub const Code128 = struct {
const START_A: u8 = 103;
const START_B: u8 = 104;
const START_C: u8 = 105;
const STOP: u8 = 106;
// Patrones de barras (1=barra, 0=espacio)
const PATTERNS = [107][]const u8{
"11011001100", // 0: space (B) / 00 (C)
"11001101100", // 1
// ... 107 patrones
};
/// Codifica string a Code128B
pub fn encode(text: []const u8) ![]u8 {
var result = std.ArrayList(u8).init(allocator);
// Start code B
try result.appendSlice(PATTERNS[START_B]);
var checksum: u32 = START_B;
for (text, 1..) |char, pos| {
const value = char - 32; // Code B offset
try result.appendSlice(PATTERNS[value]);
checksum += value * @intCast(u32, pos);
}
// Checksum
checksum %= 103;
try result.appendSlice(PATTERNS[@intCast(usize, checksum)]);
// Stop
try result.appendSlice(PATTERNS[STOP]);
return result.toOwnedSlice();
}
/// Dibuja codigo de barras en pagina
pub fn draw(page: *Page, x: f32, y: f32, text: []const u8, opts: Options) !void {
const pattern = try encode(text);
defer allocator.free(pattern);
var current_x = x;
for (pattern) |bit| {
if (bit == '1') {
page.setFillColor(Color.black);
try page.fillRect(current_x, y, opts.bar_width, opts.height);
}
current_x += opts.bar_width;
}
// Texto debajo (opcional)
if (opts.show_text) {
try page.setFont(.courier, opts.text_size);
const text_width = page.getStringWidth(text);
try page.drawText(x + (current_x - x - text_width) / 2, y - opts.text_size - 2, text);
}
}
pub const Options = struct {
bar_width: f32 = 1.0,
height: f32 = 50,
show_text: bool = true,
text_size: f32 = 10,
};
};
```
### 13.2 QR Code
```zig
// src/barcode/qr.zig
pub const QrCode = struct {
// QR Code requiere algoritmo mas complejo:
// - Reed-Solomon error correction
// - Masking patterns
// - Format/version info
/// Genera matriz QR para texto
pub fn generate(allocator: Allocator, text: []const u8, version: u8, ecc: EccLevel) !QrMatrix {
// 1. Codificar datos (modo byte)
const data_bits = try encodeData(allocator, text);
// 2. Anadir error correction (Reed-Solomon)
const codewords = try addErrorCorrection(allocator, data_bits, version, ecc);
// 3. Colocar en matriz
const size = 21 + (version - 1) * 4;
var matrix = try QrMatrix.init(allocator, size);
// Patrones fijos (finder, timing, etc)
placeFinderPatterns(&matrix);
placeTimingPatterns(&matrix);
// Datos
placeDataBits(&matrix, codewords);
// Aplicar mascara
const best_mask = selectBestMask(&matrix);
applyMask(&matrix, best_mask);
// Format info
placeFormatInfo(&matrix, ecc, best_mask);
return matrix;
}
/// Dibuja QR en pagina PDF
pub fn draw(page: *Page, x: f32, y: f32, matrix: *const QrMatrix, module_size: f32) !void {
page.setFillColor(Color.black);
for (0..matrix.size) |row| {
for (0..matrix.size) |col| {
if (matrix.get(row, col)) {
const px = x + @intToFloat(f32, col) * module_size;
const py = y - @intToFloat(f32, row) * module_size;
try page.fillRect(px, py - module_size, module_size, module_size);
}
}
}
}
pub const EccLevel = enum { L, M, Q, H };
pub const QrMatrix = struct {
data: []bool,
size: usize,
pub fn get(self: *const QrMatrix, row: usize, col: usize) bool {
return self.data[row * self.size + col];
}
pub fn set(self: *QrMatrix, row: usize, col: usize, value: bool) void {
self.data[row * self.size + col] = value;
}
};
};
```
### 13.3 API unificada
```zig
// src/barcode/mod.zig
pub const code128 = @import("code128.zig");
pub const code39 = @import("code39.zig");
pub const ean = @import("ean.zig");
pub const qr = @import("qr.zig");
// src/page.zig - AGREGAR
pub const Page = struct {
/// Dibuja codigo de barras Code128
pub fn drawBarcode128(self: *Self, x: f32, y: f32, text: []const u8, opts: anytype) !void {
try barcode.code128.draw(self, x, y, text, opts);
}
/// Dibuja codigo QR
pub fn drawQrCode(self: *Self, x: f32, y: f32, text: []const u8, opts: anytype) !void {
const matrix = try barcode.qr.generate(self.allocator, text,
opts.version orelse 0, opts.ecc orelse .M);
defer matrix.deinit(self.allocator);
try barcode.qr.draw(self, x, y, &matrix, opts.module_size orelse 3);
}
};
```
### Ejemplo: examples/barcode_demo.zig
```zig
const std = @import("std");
const pdf = @import("zcatpdf");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var doc = pdf.Pdf.init(allocator, .{});
defer doc.deinit();
var page = try doc.addPage(.{});
try page.setFont(.helvetica_bold, 16);
try page.drawText(50, 800, "Barcode Demo");
// Code128
try page.setFont(.helvetica, 12);
try page.drawText(50, 750, "Code 128:");
try page.drawBarcode128(50, 680, "ABC-12345", .{
.height = 50,
.bar_width = 1.5,
});
// QR Code
try page.drawText(50, 600, "QR Code:");
try page.drawQrCode(50, 450, "https://example.com", .{
.module_size = 4,
.ecc = .M,
});
// QR con mas datos
try page.drawText(300, 600, "QR Code (mas datos):");
try page.drawQrCode(300, 450, "Hola mundo! Este es un ejemplo de QR Code generado con zcatpdf", .{
.module_size = 3,
.ecc = .H,
});
try doc.save("barcode_demo.pdf");
}
```
---
## FASE 14: Encriptacion PDF
**Prioridad**: BAJA
**Dependencias**: Ninguna (pero complejo)
**Archivos nuevos**: `src/security/`
### 14.1 Estructura basica
```zig
// src/security/encryption.zig
pub const Encryption = struct {
method: Method,
user_password: []const u8,
owner_password: []const u8,
permissions: Permissions,
// Claves derivadas
encryption_key: [16]u8,
user_key: [32]u8,
owner_key: [32]u8,
pub const Method = enum {
rc4_40, // PDF 1.1
rc4_128, // PDF 1.4
aes_128, // PDF 1.5
aes_256, // PDF 1.7 ext 3
};
pub const Permissions = packed struct(u32) {
reserved1: u2 = 0,
print: bool = true,
modify: bool = true,
copy: bool = true,
annotations: bool = true,
fill_forms: bool = true,
extract: bool = true,
assemble: bool = true,
print_high_quality: bool = true,
reserved2: u22 = 0xFFFFF,
};
pub fn init(user_pass: []const u8, owner_pass: []const u8, perms: Permissions) Encryption {
var enc = Encryption{
.method = .aes_128,
.user_password = user_pass,
.owner_password = owner_pass,
.permissions = perms,
.encryption_key = undefined,
.user_key = undefined,
.owner_key = undefined,
};
enc.deriveKeys();
return enc;
}
fn deriveKeys(self: *Encryption) void {
// Algoritmo segun PDF spec
// 1. Pad passwords (32 bytes con padding estandar)
// 2. MD5 hash con owner password, file ID, permissions
// 3. Para AES-128: usar primera parte como key
// ...
}
/// Encripta un objeto PDF
pub fn encryptObject(self: *const Encryption, obj_num: u32, gen_num: u32, data: []u8) void {
// 1. Derivar key especifica del objeto
// 2. Para RC4: XOR con keystream
// 3. Para AES: usar CBC mode
}
};
```
### 14.2 Integrar en OutputProducer
```zig
// src/output/producer.zig - MODIFICAR
pub const OutputProducer = struct {
encryption: ?*const Encryption = null,
pub fn generateEncrypted(self: *Self, pages: []const PageData, meta: DocumentMetadata,
encryption: *const Encryption) ![]u8 {
self.encryption = encryption;
// ... resto igual pero encriptando streams
}
// En writeContentStream:
if (self.encryption) |enc| {
enc.encryptObject(obj_id, 0, content_data);
}
// Agregar Encrypt dictionary en trailer
// /Encrypt << /Filter /Standard /V 4 /R 4 /Length 128 /CF <<...>> /O (...) /U (...) /P -3904 >>
};
```
### Ejemplo: examples/encrypted_demo.zig
```zig
const std = @import("std");
const pdf = @import("zcatpdf");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var doc = pdf.Pdf.init(allocator, .{});
defer doc.deinit();
// Configurar encriptacion
doc.setEncryption(.{
.user_password = "", // Sin password para abrir
.owner_password = "secret123", // Password para editar
.permissions = .{
.print = true,
.copy = false, // No permitir copiar texto
.modify = false, // No permitir modificar
},
});
var page = try doc.addPage(.{});
try page.setFont(.helvetica_bold, 24);
try page.drawText(50, 750, "Documento Protegido");
try page.setFont(.helvetica, 12);
try page.drawText(50, 700, "Este documento no permite copiar ni modificar.");
try doc.save("encrypted_demo.pdf");
}
```
---
## FASE 15: Formularios PDF (AcroForms)
**Prioridad**: BAJA
**Dependencias**: Ninguna
**Archivos nuevos**: `src/forms/`
### 15.1 Campos de formulario
```zig
// src/forms/field.zig
pub const FormField = struct {
field_type: FieldType,
name: []const u8,
rect: Rect,
value: ?[]const u8 = null,
default_value: ?[]const u8 = null,
options: FieldOptions = .{},
pub const FieldType = enum {
text,
checkbox,
radio,
combobox,
listbox,
button,
signature,
};
pub const FieldOptions = struct {
required: bool = false,
read_only: bool = false,
no_export: bool = false,
multiline: bool = false,
password: bool = false,
max_length: ?u32 = null,
font_size: f32 = 12,
alignment: Align = .left,
};
pub const Rect = struct { x: f32, y: f32, width: f32, height: f32 };
};
pub const TextField = struct {
base: FormField,
pub fn init(name: []const u8, x: f32, y: f32, w: f32, h: f32) TextField {
return .{
.base = .{
.field_type = .text,
.name = name,
.rect = .{ .x = x, .y = y, .width = w, .height = h },
},
};
}
};
pub const CheckboxField = struct {
base: FormField,
checked: bool = false,
pub fn init(name: []const u8, x: f32, y: f32, size: f32) CheckboxField {
return .{
.base = .{
.field_type = .checkbox,
.name = name,
.rect = .{ .x = x, .y = y, .width = size, .height = size },
},
};
}
};
```
### 15.2 Serializar formularios
```zig
// src/forms/acroform.zig
pub const AcroForm = struct {
fields: std.ArrayListUnmanaged(FormField),
pub fn toPdfObjects(self: *const AcroForm, producer: *OutputProducer, first_field_id: u32) !void {
// AcroForm dictionary
// << /Fields [...] /NeedAppearances true /DR <<resources>> >>
for (self.fields.items, 0..) |field, i| {
const field_id = first_field_id + @intCast(u32, i);
try producer.beginObject(field_id);
try self.writeFieldDict(producer.writer(), &field, page_id);
try producer.endObject();
}
}
fn writeFieldDict(writer: anytype, field: *const FormField, page_id: u32) !void {
try writer.writeAll("<< /Type /Annot /Subtype /Widget\n");
try writer.print("/Rect [{d:.2} {d:.2} {d:.2} {d:.2}]\n", .{
field.rect.x, field.rect.y,
field.rect.x + field.rect.width,
field.rect.y + field.rect.height,
});
try writer.print("/P {d} 0 R\n", .{page_id});
try writer.writeAll("/T ");
try writeString(writer, field.name);
try writer.writeByte('\n');
switch (field.field_type) {
.text => {
try writer.writeAll("/FT /Tx\n");
if (field.options.multiline) {
try writer.writeAll("/Ff 4096\n"); // Multiline flag
}
},
.checkbox => {
try writer.writeAll("/FT /Btn\n");
// ...
},
// ... otros tipos
}
try writer.writeAll(">>");
}
};
```
### 15.3 API para usuario
```zig
// src/page.zig - AGREGAR
pub const Page = struct {
form_fields: std.ArrayListUnmanaged(FormField),
/// Agrega campo de texto
pub fn addTextField(self: *Self, name: []const u8, x: f32, y: f32, w: f32, h: f32) !*FormField {
const field = FormField{
.field_type = .text,
.name = name,
.rect = .{ .x = x, .y = y, .width = w, .height = h },
};
try self.form_fields.append(self.allocator, field);
return &self.form_fields.items[self.form_fields.items.len - 1];
}
/// Agrega checkbox
pub fn addCheckbox(self: *Self, name: []const u8, x: f32, y: f32, size: f32) !*FormField {
// ...
}
/// Agrega combobox (dropdown)
pub fn addCombobox(self: *Self, name: []const u8, x: f32, y: f32, w: f32, h: f32,
options: []const []const u8) !*FormField {
// ...
}
};
```
### Ejemplo: examples/forms_demo.zig
```zig
const std = @import("std");
const pdf = @import("zcatpdf");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var doc = pdf.Pdf.init(allocator, .{});
defer doc.deinit();
var page = try doc.addPage(.{});
try page.setFont(.helvetica_bold, 18);
try page.drawText(50, 800, "Formulario de Registro");
try page.setFont(.helvetica, 12);
// Nombre
try page.drawText(50, 750, "Nombre:");
_ = try page.addTextField("nombre", 120, 745, 200, 20);
// Email
try page.drawText(50, 710, "Email:");
_ = try page.addTextField("email", 120, 705, 200, 20);
// Acepto terminos
try page.drawText(50, 670, "Acepto los terminos:");
_ = try page.addCheckbox("acepto", 180, 668, 15);
// Pais (dropdown)
try page.drawText(50, 630, "Pais:");
_ = try page.addCombobox("pais", 120, 625, 150, 20, &.{
"Espana", "Mexico", "Argentina", "Colombia", "Chile",
});
try doc.save("forms_demo.pdf");
}
```
---
## FASE 16: SVG Import (Basico)
**Prioridad**: BAJA
**Dependencias**: Curvas Bezier (Fase 9)
**Archivos nuevos**: `src/svg/`
### 16.1 Parser SVG simplificado
```zig
// src/svg/parser.zig
pub const SvgParser = struct {
allocator: Allocator,
pub fn parseFile(self: *SvgParser, path: []const u8) !SvgDocument {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
const content = try file.readToEndAlloc(self.allocator, 1024 * 1024);
defer self.allocator.free(content);
return try self.parse(content);
}
pub fn parse(self: *SvgParser, svg_content: []const u8) !SvgDocument {
// Parser XML simplificado
var doc = SvgDocument.init(self.allocator);
// Extraer viewBox
if (findAttribute(svg_content, "viewBox")) |vb| {
doc.viewbox = try parseViewBox(vb);
}
// Parsear elementos
try self.parseElements(&doc, svg_content);
return doc;
}
fn parseElements(self: *SvgParser, doc: *SvgDocument, content: []const u8) !void {
// Buscar elementos soportados
var pos: usize = 0;
while (findNextElement(content, pos)) |elem| {
const tag = elem.tag;
if (std.mem.eql(u8, tag, "path")) {
try doc.elements.append(self.allocator, try self.parsePath(elem));
} else if (std.mem.eql(u8, tag, "rect")) {
try doc.elements.append(self.allocator, try self.parseRect(elem));
} else if (std.mem.eql(u8, tag, "circle")) {
try doc.elements.append(self.allocator, try self.parseCircle(elem));
} else if (std.mem.eql(u8, tag, "line")) {
try doc.elements.append(self.allocator, try self.parseLine(elem));
} else if (std.mem.eql(u8, tag, "text")) {
try doc.elements.append(self.allocator, try self.parseText(elem));
}
pos = elem.end;
}
}
fn parsePath(self: *SvgParser, elem: Element) !SvgElement {
// Parsear atributo "d" (path data)
const d = findAttribute(elem.content, "d") orelse return error.MissingPathData;
return SvgElement{
.element_type = .path,
.path_data = try parsePathData(self.allocator, d),
.style = try parseStyle(elem.content),
};
}
};
pub const SvgDocument = struct {
viewbox: ?ViewBox = null,
elements: std.ArrayListUnmanaged(SvgElement),
};
pub const SvgElement = struct {
element_type: ElementType,
path_data: ?[]PathCommand = null,
rect_data: ?RectData = null,
// ... otros datos
style: Style,
pub const ElementType = enum { path, rect, circle, ellipse, line, polyline, polygon, text };
};
pub const PathCommand = struct {
command: u8, // M, L, C, Q, Z, etc
args: [6]f32,
arg_count: u8,
};
```
### 16.2 Renderizar SVG a PDF
```zig
// src/svg/renderer.zig
pub const SvgRenderer = struct {
pub fn render(page: *Page, doc: *const SvgDocument, x: f32, y: f32, scale: f32) !void {
try page.stream.saveState();
try page.stream.translate(x, y);
try page.stream.scale(scale, -scale); // SVG Y axis is inverted
for (doc.elements.items) |elem| {
try renderElement(page, &elem);
}
try page.stream.restoreState();
}
fn renderElement(page: *Page, elem: *const SvgElement) !void {
// Aplicar estilo
if (elem.style.fill) |fill| {
page.setFillColor(svgColorToPdf(fill));
}
if (elem.style.stroke) |stroke| {
page.setStrokeColor(svgColorToPdf(stroke));
}
if (elem.style.stroke_width) |sw| {
try page.setLineWidth(sw);
}
switch (elem.element_type) {
.path => try renderPath(page, elem.path_data.?),
.rect => try renderRect(page, elem.rect_data.?),
.circle => try renderCircle(page, elem.circle_data.?),
// ...
}
}
fn renderPath(page: *Page, commands: []const PathCommand) !void {
for (commands) |cmd| {
switch (cmd.command) {
'M' => try page.stream.moveTo(cmd.args[0], cmd.args[1]),
'L' => try page.stream.lineTo(cmd.args[0], cmd.args[1]),
'C' => try page.stream.curveTo(
cmd.args[0], cmd.args[1],
cmd.args[2], cmd.args[3],
cmd.args[4], cmd.args[5],
),
'Q' => {
// Convertir cuadratica a cubica
// ...
},
'Z' => try page.stream.closePath(),
// ... mas comandos
}
}
try page.stream.stroke();
}
};
```
### 16.3 API
```zig
// src/page.zig - AGREGAR
pub const Page = struct {
/// Dibuja SVG desde archivo
pub fn drawSvgFile(self: *Self, path: []const u8, x: f32, y: f32, opts: SvgOptions) !void {
var parser = svg.SvgParser{ .allocator = self.allocator };
const doc = try parser.parseFile(path);
defer doc.deinit(self.allocator);
const scale = opts.width / (doc.viewbox.?.width);
try svg.SvgRenderer.render(self, &doc, x, y, scale);
}
/// Dibuja SVG desde string
pub fn drawSvg(self: *Self, svg_content: []const u8, x: f32, y: f32, opts: SvgOptions) !void {
// ...
}
pub const SvgOptions = struct {
width: f32 = 100,
height: ?f32 = null, // Auto-calculate maintaining aspect ratio
};
};
```
---
## FASE 17: Templates
**Prioridad**: BAJA
**Dependencias**: Ninguna
**Archivos nuevos**: `src/template.zig`
### 17.1 Sistema de templates
```zig
// src/template.zig
pub const Template = struct {
allocator: Allocator,
content_stream: []const u8,
width: f32,
height: f32,
resources: Resources,
pub const Resources = struct {
fonts: []const Font,
images: []const usize,
};
/// Crea template desde pagina existente
pub fn fromPage(allocator: Allocator, page: *const Page) !Template {
return .{
.allocator = allocator,
.content_stream = try allocator.dupe(u8, page.getContent()),
.width = page.width,
.height = page.height,
.resources = .{
.fonts = try collectFonts(allocator, page),
.images = &.{},
},
};
}
/// Aplica template a nueva pagina
pub fn applyTo(self: *const Template, page: *Page) !void {
// Copiar content stream
try page.stream.appendSlice(self.content_stream);
// Copiar recursos
for (self.resources.fonts) |font| {
try page.fonts_used.put(font, {});
}
}
};
```
### 17.2 API
```zig
// src/pdf.zig - AGREGAR
pub const Pdf = struct {
templates: std.StringHashMap(Template),
/// Guarda pagina como template
pub fn saveAsTemplate(self: *Self, page: *const Page, name: []const u8) !void {
const template = try Template.fromPage(self.allocator, page);
try self.templates.put(name, template);
}
/// Agrega pagina usando template
pub fn addPageFromTemplate(self: *Self, template_name: []const u8) !*Page {
const template = self.templates.get(template_name) orelse return error.TemplateNotFound;
var page = try self.addPage(.{});
try template.applyTo(page);
return page;
}
};
```
---
## FASE 18: Markdown Styling
**Prioridad**: BAJA
**Dependencias**: Ninguna
**Archivos nuevos**: `src/markdown.zig`
### 18.1 Parser Markdown basico
```zig
// src/markdown.zig
pub const MarkdownRenderer = struct {
allocator: Allocator,
pub fn render(self: *MarkdownRenderer, page: *Page, x: f32, y: f32, width: f32,
markdown: []const u8) !f32 {
var current_y = y;
var lines = std.mem.tokenize(u8, markdown, "\n");
while (lines.next()) |line| {
current_y = try self.renderLine(page, x, current_y, width, line);
}
return current_y;
}
fn renderLine(self: *MarkdownRenderer, page: *Page, x: f32, y: f32,
width: f32, line: []const u8) !f32 {
// Headers
if (std.mem.startsWith(u8, line, "# ")) {
try page.setFont(.helvetica_bold, 24);
try page.drawText(x, y, line[2..]);
return y - 30;
} else if (std.mem.startsWith(u8, line, "## ")) {
try page.setFont(.helvetica_bold, 18);
try page.drawText(x, y, line[3..]);
return y - 24;
} else if (std.mem.startsWith(u8, line, "### ")) {
try page.setFont(.helvetica_bold, 14);
try page.drawText(x, y, line[4..]);
return y - 20;
}
// Bullet list
if (std.mem.startsWith(u8, line, "- ") or std.mem.startsWith(u8, line, "* ")) {
try page.setFont(.helvetica, 12);
try page.drawText(x, y, "•");
try page.drawText(x + 15, y, line[2..]);
return y - 16;
}
// Bold/italic inline (simplificado)
try page.setFont(.helvetica, 12);
try self.renderInline(page, x, y, width, line);
return y - 16;
}
fn renderInline(self: *MarkdownRenderer, page: *Page, x: f32, y: f32,
width: f32, text: []const u8) !void {
var current_x = x;
var i: usize = 0;
var start: usize = 0;
while (i < text.len) {
// **bold**
if (i + 1 < text.len and text[i] == '*' and text[i+1] == '*') {
// Render text before
if (i > start) {
try page.drawText(current_x, y, text[start..i]);
current_x += page.getStringWidth(text[start..i]);
}
// Find closing **
const end = std.mem.indexOf(u8, text[i+2..], "**") orelse break;
const bold_text = text[i+2..i+2+end];
try page.setFont(.helvetica_bold, page.current_font_size);
try page.drawText(current_x, y, bold_text);
current_x += page.getStringWidth(bold_text);
try page.setFont(.helvetica, page.current_font_size);
i += 4 + end;
start = i;
continue;
}
// *italic*
if (text[i] == '*' and (i == 0 or text[i-1] != '*')) {
// Similar...
}
i += 1;
}
// Render remaining text
if (start < text.len) {
try page.drawText(current_x, y, text[start..]);
}
}
};
```
### 18.2 API
```zig
// src/page.zig - AGREGAR
pub const Page = struct {
/// Renderiza texto Markdown
pub fn writeMarkdown(self: *Self, x: f32, y: f32, width: f32, markdown: []const u8) !f32 {
var renderer = MarkdownRenderer{ .allocator = self.allocator };
return try renderer.render(self, x, y, width, markdown);
}
};
```
### Ejemplo: examples/markdown_demo.zig
```zig
const std = @import("std");
const pdf = @import("zcatpdf");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var doc = pdf.Pdf.init(allocator, .{});
defer doc.deinit();
var page = try doc.addPage(.{});
const markdown =
\\# Titulo Principal
\\
\\Este es un parrafo con **texto en negrita** y *texto en italica*.
\\
\\## Subtitulo
\\
\\Lista de items:
\\- Primer item
\\- Segundo item
\\- Tercer item
\\
\\### Seccion menor
\\
\\Mas texto aqui.
;
_ = try page.writeMarkdown(50, 780, 500, markdown);
try doc.save("markdown_demo.pdf");
}
```
---
## FASE 19: Mejoras de Calidad
**Prioridad**: CONTINUA
**Dependencias**: Todas las fases anteriores
### 19.1 Builder Pattern / Fluent API
```zig
// src/page.zig - MEJORAR
pub const Page = struct {
/// Metodos fluent que retornan *Self
pub fn font(self: *Self, f: Font, size: f32) *Self {
self.setFont(f, size) catch {};
return self;
}
pub fn color(self: *Self, c: Color) *Self {
self.setFillColor(c);
return self;
}
pub fn at(self: *Self, x: f32, y: f32) *Self {
self.setXY(x, y);
return self;
}
pub fn text(self: *Self, t: []const u8) *Self {
self.drawText(self.x, self.y, t) catch {};
return self;
}
};
// Uso:
page.font(.helvetica_bold, 24)
.color(Color.blue)
.at(50, 750)
.text("Hello!")
.font(.helvetica, 12)
.at(50, 720)
.text("World");
```
### 19.2 Auto Page Break
```zig
// src/page.zig - AGREGAR
pub const Page = struct {
auto_page_break: bool = false,
page_break_margin: f32 = 20,
on_page_break: ?*const fn(*Pdf, *Page) anyerror!*Page = null,
pub fn enableAutoPageBreak(self: *Self, margin: f32, callback: anytype) void {
self.auto_page_break = true;
self.page_break_margin = margin;
self.on_page_break = callback;
}
fn checkPageBreak(self: *Self, height: f32) !void {
if (!self.auto_page_break) return;
if (self.y - height < self.page_break_margin) {
// Crear nueva pagina
if (self.on_page_break) |callback| {
_ = try callback(self.document, self);
}
}
}
};
```
### 19.3 Documentacion completa
- [ ] Documentar todos los modulos publicos con doc comments
- [ ] Crear README.md con ejemplos
- [ ] Generar documentacion con `zig build docs`
- [ ] Ejemplos para cada funcionalidad
### 19.4 Test coverage
- [ ] Tests unitarios para cada modulo
- [ ] Tests de integracion
- [ ] Tests de regresion con PDFs de referencia
- [ ] Benchmarks de rendimiento
---
## Resumen de Fases
| Fase | Nombre | Prioridad | Complejidad | Dependencias |
|------|--------|-----------|-------------|--------------|
| 6 | PNG + zlib | ALTA | Media | - |
| 7 | TTF Fonts | ALTA | Alta | Fase 6 |
| 8 | Bookmarks | ALTA | Baja | - |
| 9 | Curvas Bezier | MEDIA | Baja | - |
| 10 | Rotacion | MEDIA | Baja | - |
| 11 | Transparencia | MEDIA | Media | - |
| 12 | Gradientes | MEDIA | Media | Fase 11 |
| 13 | Barcodes | MEDIA | Media | - |
| 14 | Encriptacion | BAJA | Alta | - |
| 15 | Forms | BAJA | Alta | - |
| 16 | SVG | BAJA | Alta | Fase 9 |
| 17 | Templates | BAJA | Baja | - |
| 18 | Markdown | BAJA | Baja | - |
| 19 | Calidad | CONTINUA | - | Todas |
---
## Orden Recomendado de Implementacion
1. **Fase 6**: PNG + zlib (usa std.compress.zlib de Zig)
2. **Fase 8**: Bookmarks (relativamente simple, muy util)
3. **Fase 9**: Curvas Bezier (base para otras funcionalidades)
4. **Fase 10**: Rotacion (complementa graficos)
5. **Fase 11**: Transparencia (muy solicitado)
6. **Fase 7**: TTF Fonts (complejo pero esencial)
7. **Fase 13**: Barcodes (util para facturas)
8. **Fase 12**: Gradientes (nice to have)
9. **Fase 17**: Templates (util para documentos repetitivos)
10. **Fase 18**: Markdown (nice to have)
11. **Fase 15**: Forms (complejo, uso especifico)
12. **Fase 16**: SVG (muy complejo)
13. **Fase 14**: Encriptacion (complejo, uso especifico)
---
## Notas Tecnicas
### Zig 0.15.2 APIs
- Usar `std.ArrayListUnmanaged` (no `std.ArrayList`)
- Usar `std.compress.zlib` para compresion
- Usar `@intFromFloat`, `@floatFromInt` (no `@intCast`, etc. para conversiones float/int)
### PDF 1.4 Limitaciones
- Sin transparencia nativa (requiere PDF 1.4+)
- Sin Unicode directo en Type1 (solo WinAnsi)
- Sin compresion de objetos
### Referencias
- [PDF Reference 1.4](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.4.pdf)
- [fpdf2 source](https://github.com/py-pdf/fpdf2)
- [TrueType Reference](https://developer.apple.com/fonts/TrueType-Reference-Manual/)
- [QR Code spec](https://www.qrcode.com/en/about/standards.html)
---
*Plan creado: 2025-12-08*
*Ultima actualizacion: 2025-12-08*