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

80 KiB

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

  • PDF 1.4 basico (estructura, objetos, xref, trailer)
  • Paginas (A4, Letter, Legal, A3, A5, custom)
  • Texto (drawText, cell, multiCell, word-wrap)
  • 14 fuentes Type1 estandar con metricas
  • Colores RGB, CMYK, Grayscale
  • Lineas, rectangulos, circulos/elipses
  • Imagenes JPEG (DCT passthrough)
  • Tablas con helper (header, rows, footer, styling)
  • Paginacion (numeros de pagina, headers, footers)
  • Links clickeables (URL externos, internos entre paginas)
  • ~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

// 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

// 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

// 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

// 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

// 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

// 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

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

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

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

// 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

// 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

// 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

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

// 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

// 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

// 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

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

// 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

// 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

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

// 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

// 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

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

// 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

// 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

// 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

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

// 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

// 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

// 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

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

// 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

// 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

// 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

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

// 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

// 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

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

// 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

// 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

// 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

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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

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

// 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

// 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


Plan creado: 2025-12-08 Ultima actualizacion: 2025-12-08