# 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> \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, ¤t_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 <> >> 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*