- 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>
2803 lines
80 KiB
Markdown
2803 lines
80 KiB
Markdown
# zcatpdf - Plan de Implementacion Completo
|
|
|
|
> **Creado**: 2025-12-08
|
|
> **Version actual**: 0.5
|
|
> **Objetivo**: Implementar TODAS las funcionalidades faltantes
|
|
|
|
---
|
|
|
|
## Estado Actual (v0.5)
|
|
|
|
### Funcionalidades Completadas
|
|
- [x] PDF 1.4 basico (estructura, objetos, xref, trailer)
|
|
- [x] Paginas (A4, Letter, Legal, A3, A5, custom)
|
|
- [x] Texto (drawText, cell, multiCell, word-wrap)
|
|
- [x] 14 fuentes Type1 estandar con metricas
|
|
- [x] Colores RGB, CMYK, Grayscale
|
|
- [x] Lineas, rectangulos, circulos/elipses
|
|
- [x] Imagenes JPEG (DCT passthrough)
|
|
- [x] Tablas con helper (header, rows, footer, styling)
|
|
- [x] Paginacion (numeros de pagina, headers, footers)
|
|
- [x] Links clickeables (URL externos, internos entre paginas)
|
|
- [x] ~70 tests, 7 ejemplos funcionando
|
|
|
|
### Archivos del Proyecto
|
|
```
|
|
src/
|
|
├── root.zig # Exports publicos
|
|
├── pdf.zig # Facade principal
|
|
├── page.zig # Pagina (800+ lineas)
|
|
├── content_stream.zig # Operadores PDF
|
|
├── table.zig # Helper tablas
|
|
├── pagination.zig # Paginacion/headers/footers
|
|
├── links.zig # Tipos de links
|
|
├── fonts/
|
|
│ ├── mod.zig
|
|
│ └── type1.zig # 14 fuentes Type1 + metricas
|
|
├── graphics/
|
|
│ ├── mod.zig
|
|
│ └── color.zig # Color RGB/CMYK/Gray
|
|
├── images/
|
|
│ ├── mod.zig
|
|
│ ├── image_info.zig
|
|
│ ├── jpeg.zig # Parser JPEG
|
|
│ └── png.zig # Solo metadata (NotImplemented)
|
|
├── objects/
|
|
│ ├── mod.zig
|
|
│ └── base.zig # PageSize, Unit, etc.
|
|
└── output/
|
|
├── mod.zig
|
|
└── producer.zig # Serializacion PDF
|
|
```
|
|
|
|
---
|
|
|
|
## FASE 6: PNG Completo + Compresion zlib
|
|
|
|
**Prioridad**: ALTA
|
|
**Dependencias**: Ninguna
|
|
**Archivos a modificar**: `src/images/png.zig`, `src/output/producer.zig`
|
|
|
|
### 6.1 Implementar zlib/deflate en Zig puro
|
|
|
|
```zig
|
|
// src/compression/deflate.zig
|
|
|
|
pub const Deflate = struct {
|
|
// Implementar DEFLATE (RFC 1951) sin dependencias externas
|
|
// Zig std tiene std.compress.zlib que podemos usar!
|
|
|
|
pub fn compress(allocator: Allocator, data: []const u8) ![]u8 {
|
|
var compressed = std.ArrayList(u8).init(allocator);
|
|
var compressor = try std.compress.zlib.compressor(compressed.writer());
|
|
try compressor.writer().writeAll(data);
|
|
try compressor.finish();
|
|
return compressed.toOwnedSlice();
|
|
}
|
|
|
|
pub fn decompress(allocator: Allocator, data: []const u8) ![]u8 {
|
|
var decompressed = std.ArrayList(u8).init(allocator);
|
|
var stream = std.io.fixedBufferStream(data);
|
|
var decompressor = std.compress.zlib.decompressor(stream.reader());
|
|
// ... read all
|
|
}
|
|
};
|
|
```
|
|
|
|
**NOTA**: Zig 0.15 incluye `std.compress.zlib` - usarlo directamente!
|
|
|
|
### 6.2 Completar PNG parser
|
|
|
|
```zig
|
|
// src/images/png.zig - COMPLETAR
|
|
|
|
pub fn parse(allocator: Allocator, data: []const u8) !ImageInfo {
|
|
const meta = try parseMetadata(data);
|
|
|
|
// 1. Encontrar todos los chunks IDAT
|
|
var idat_data = ArrayList(u8).init(allocator);
|
|
defer idat_data.deinit();
|
|
|
|
var pos: usize = 8;
|
|
while (pos < data.len) {
|
|
const chunk = readChunkHeader(data, pos) orelse break;
|
|
if (std.mem.eql(u8, &chunk.chunk_type, "IDAT")) {
|
|
try idat_data.appendSlice(data[pos + 8 .. pos + 8 + chunk.length]);
|
|
}
|
|
pos += 8 + chunk.length + 4; // header + data + CRC
|
|
}
|
|
|
|
// 2. Descomprimir zlib
|
|
const raw = try zlib.decompress(allocator, idat_data.items);
|
|
defer allocator.free(raw);
|
|
|
|
// 3. Aplicar filtros PNG inversos (unfilter)
|
|
const unfiltered = try pngUnfilter(allocator, raw, meta);
|
|
defer allocator.free(unfiltered);
|
|
|
|
// 4. Separar RGB y Alpha si es RGBA
|
|
if (meta.color_type == .rgba) {
|
|
// Extraer alpha como soft mask
|
|
const rgb = try extractRgb(allocator, unfiltered, meta);
|
|
const alpha = try extractAlpha(allocator, unfiltered, meta);
|
|
|
|
// Comprimir ambos con FlateDecode para PDF
|
|
const rgb_compressed = try zlib.compress(allocator, rgb);
|
|
const alpha_compressed = try zlib.compress(allocator, alpha);
|
|
|
|
return ImageInfo{
|
|
.width = meta.width,
|
|
.height = meta.height,
|
|
.color_space = .device_rgb,
|
|
.bits_per_component = 8,
|
|
.filter = .flate_decode,
|
|
.data = rgb_compressed,
|
|
.soft_mask = alpha_compressed, // NUEVO campo
|
|
};
|
|
}
|
|
|
|
// Para RGB/Grayscale sin alpha
|
|
const compressed = try zlib.compress(allocator, unfiltered);
|
|
return ImageInfo{...};
|
|
}
|
|
|
|
fn pngUnfilter(allocator: Allocator, data: []const u8, meta: PngMetadata) ![]u8 {
|
|
// Implementar filtros PNG: None(0), Sub(1), Up(2), Average(3), Paeth(4)
|
|
const bytes_per_pixel = meta.channels * (meta.bit_depth / 8);
|
|
const row_bytes = meta.width * bytes_per_pixel;
|
|
|
|
var output = try allocator.alloc(u8, meta.height * row_bytes);
|
|
var prev_row: ?[]u8 = null;
|
|
|
|
for (0..meta.height) |y| {
|
|
const filter_type = data[y * (row_bytes + 1)];
|
|
const row_data = data[y * (row_bytes + 1) + 1 ..][0..row_bytes];
|
|
const out_row = output[y * row_bytes ..][0..row_bytes];
|
|
|
|
switch (filter_type) {
|
|
0 => @memcpy(out_row, row_data), // None
|
|
1 => unfilterSub(out_row, row_data, bytes_per_pixel),
|
|
2 => unfilterUp(out_row, row_data, prev_row),
|
|
3 => unfilterAverage(out_row, row_data, prev_row, bytes_per_pixel),
|
|
4 => unfilterPaeth(out_row, row_data, prev_row, bytes_per_pixel),
|
|
else => return error.InvalidFilter,
|
|
}
|
|
prev_row = out_row;
|
|
}
|
|
return output;
|
|
}
|
|
```
|
|
|
|
### 6.3 Soft Mask para transparencia
|
|
|
|
```zig
|
|
// src/images/image_info.zig - AGREGAR
|
|
|
|
pub const ImageInfo = struct {
|
|
// ... campos existentes ...
|
|
|
|
/// Soft mask for alpha channel (optional)
|
|
soft_mask: ?[]const u8 = null,
|
|
soft_mask_owns_data: bool = false,
|
|
|
|
pub fn deinit(self: *ImageInfo, allocator: Allocator) void {
|
|
if (self.owns_data) allocator.free(self.data);
|
|
if (self.soft_mask_owns_data) {
|
|
if (self.soft_mask) |sm| allocator.free(sm);
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### 6.4 Actualizar OutputProducer para soft masks
|
|
|
|
```zig
|
|
// src/output/producer.zig - MODIFICAR
|
|
|
|
// En generateWithImages, despues de escribir XObjects:
|
|
if (img.soft_mask) |mask_data| {
|
|
// Escribir soft mask como objeto separado
|
|
const mask_id = first_image_id + images.len + @intCast(u32, mask_index);
|
|
try self.beginObject(mask_id);
|
|
try writer.writeAll("<< /Type /XObject\n");
|
|
try writer.writeAll("/Subtype /Image\n");
|
|
try writer.print("/Width {d}\n", .{img.width});
|
|
try writer.print("/Height {d}\n", .{img.height});
|
|
try writer.writeAll("/ColorSpace /DeviceGray\n");
|
|
try writer.writeAll("/BitsPerComponent 8\n");
|
|
try writer.writeAll("/Filter /FlateDecode\n");
|
|
try writer.print("/Length {d}\n", .{mask_data.len});
|
|
try writer.writeAll(">>\nstream\n");
|
|
try writer.writeAll(mask_data);
|
|
try writer.writeAll("\nendstream\n");
|
|
try self.endObject();
|
|
}
|
|
|
|
// En el XObject de la imagen principal:
|
|
if (img.soft_mask != null) {
|
|
try writer.print("/SMask {d} 0 R\n", .{soft_mask_obj_id});
|
|
}
|
|
```
|
|
|
|
### 6.5 Comprimir content streams
|
|
|
|
```zig
|
|
// src/output/producer.zig - MODIFICAR
|
|
|
|
// Opcion para comprimir content streams
|
|
pub const OutputOptions = struct {
|
|
compress_streams: bool = true,
|
|
};
|
|
|
|
// En generateWithImages:
|
|
const content_data = if (options.compress_streams)
|
|
try zlib.compress(allocator, page.content)
|
|
else
|
|
page.content;
|
|
|
|
try writer.writeAll("<< /Length ");
|
|
try writer.print("{d}", .{content_data.len});
|
|
if (options.compress_streams) {
|
|
try writer.writeAll(" /Filter /FlateDecode");
|
|
}
|
|
try writer.writeAll(" >>\nstream\n");
|
|
```
|
|
|
|
### 6.6 Tests para PNG
|
|
|
|
```zig
|
|
// src/images/png.zig - AGREGAR tests
|
|
|
|
test "parse RGB PNG" {
|
|
// Crear PNG de prueba o usar archivo de test
|
|
const allocator = std.testing.allocator;
|
|
const png_data = @embedFile("test_assets/test_rgb.png");
|
|
const info = try parse(allocator, png_data);
|
|
defer info.deinit(allocator);
|
|
|
|
try std.testing.expectEqual(info.color_space, .device_rgb);
|
|
try std.testing.expect(info.soft_mask == null);
|
|
}
|
|
|
|
test "parse RGBA PNG with alpha" {
|
|
const allocator = std.testing.allocator;
|
|
const png_data = @embedFile("test_assets/test_rgba.png");
|
|
const info = try parse(allocator, png_data);
|
|
defer info.deinit(allocator);
|
|
|
|
try std.testing.expect(info.soft_mask != null);
|
|
}
|
|
```
|
|
|
|
### Ejemplo: examples/png_demo.zig
|
|
|
|
```zig
|
|
const std = @import("std");
|
|
const pdf = @import("zcatpdf");
|
|
|
|
pub fn main() !void {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
|
|
var doc = pdf.Pdf.init(allocator, .{});
|
|
defer doc.deinit();
|
|
|
|
var page = try doc.addPage(.{});
|
|
|
|
// PNG con transparencia sobre fondo de color
|
|
page.setFillColor(pdf.Color.rgb(255, 200, 200));
|
|
try page.fillRect(100, 500, 200, 200);
|
|
|
|
// La imagen PNG con alpha se blendea correctamente
|
|
const img_idx = try doc.addPngImageFromFile("logo.png");
|
|
try page.drawImage(img_idx, 150, 550, 100, 100);
|
|
|
|
try doc.save("png_demo.pdf");
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## FASE 7: Fuentes TrueType (TTF)
|
|
|
|
**Prioridad**: ALTA
|
|
**Dependencias**: Compresion zlib (Fase 6)
|
|
**Archivos nuevos**: `src/fonts/ttf.zig`, `src/fonts/subset.zig`
|
|
|
|
### 7.1 Estructura TTF Parser
|
|
|
|
```zig
|
|
// src/fonts/ttf.zig
|
|
|
|
pub const TtfFont = struct {
|
|
allocator: Allocator,
|
|
data: []const u8,
|
|
owns_data: bool,
|
|
|
|
// Tablas TTF parseadas
|
|
head: HeadTable,
|
|
hhea: HheaTable,
|
|
maxp: MaxpTable,
|
|
hmtx: []HmtxEntry,
|
|
cmap: CmapTable,
|
|
glyf: ?GlyfTable, // Para TrueType outlines
|
|
loca: ?LocaTable,
|
|
post: PostTable,
|
|
name: NameTable,
|
|
os2: ?Os2Table,
|
|
|
|
// Metricas calculadas
|
|
units_per_em: u16,
|
|
ascender: i16,
|
|
descender: i16,
|
|
line_gap: i16,
|
|
|
|
const Self = @This();
|
|
|
|
pub fn initFromFile(allocator: Allocator, path: []const u8) !Self {
|
|
const file = try std.fs.cwd().openFile(path, .{});
|
|
defer file.close();
|
|
const data = try file.readToEndAlloc(allocator, 10 * 1024 * 1024);
|
|
return try initFromData(allocator, data, true);
|
|
}
|
|
|
|
pub fn initFromData(allocator: Allocator, data: []const u8, owns: bool) !Self {
|
|
var self = Self{
|
|
.allocator = allocator,
|
|
.data = data,
|
|
.owns_data = owns,
|
|
// ...
|
|
};
|
|
try self.parseTables();
|
|
return self;
|
|
}
|
|
|
|
fn parseTables(self: *Self) !void {
|
|
// Leer offset table
|
|
const sfnt_version = readU32BE(self.data, 0);
|
|
if (sfnt_version != 0x00010000 and sfnt_version != 0x4F54544F) {
|
|
return error.InvalidTtfSignature;
|
|
}
|
|
|
|
const num_tables = readU16BE(self.data, 4);
|
|
|
|
// Leer table directory
|
|
var pos: usize = 12;
|
|
for (0..num_tables) |_| {
|
|
const tag = self.data[pos..][0..4].*;
|
|
const offset = readU32BE(self.data, pos + 8);
|
|
const length = readU32BE(self.data, pos + 12);
|
|
|
|
if (std.mem.eql(u8, &tag, "head")) {
|
|
self.head = try parseHead(self.data[offset..][0..length]);
|
|
} else if (std.mem.eql(u8, &tag, "hhea")) {
|
|
self.hhea = try parseHhea(self.data[offset..][0..length]);
|
|
}
|
|
// ... etc para cada tabla
|
|
|
|
pos += 16;
|
|
}
|
|
}
|
|
|
|
/// Obtiene el ancho de un glyph (en unidades de diseno)
|
|
pub fn getGlyphWidth(self: *const Self, glyph_id: u16) u16 {
|
|
if (glyph_id < self.hmtx.len) {
|
|
return self.hmtx[glyph_id].advance_width;
|
|
}
|
|
return self.hmtx[self.hmtx.len - 1].advance_width;
|
|
}
|
|
|
|
/// Convierte codepoint Unicode a glyph ID
|
|
pub fn charToGlyph(self: *const Self, codepoint: u21) u16 {
|
|
return self.cmap.lookup(codepoint) orelse 0;
|
|
}
|
|
|
|
/// Calcula ancho de string en puntos
|
|
pub fn stringWidth(self: *const Self, text: []const u8, size: f32) f32 {
|
|
var width: f32 = 0;
|
|
var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
|
|
while (iter.nextCodepoint()) |cp| {
|
|
const glyph = self.charToGlyph(cp);
|
|
const glyph_width = self.getGlyphWidth(glyph);
|
|
width += @as(f32, @floatFromInt(glyph_width)) * size / @as(f32, @floatFromInt(self.units_per_em));
|
|
}
|
|
return width;
|
|
}
|
|
};
|
|
```
|
|
|
|
### 7.2 CMap parsing (Unicode mapping)
|
|
|
|
```zig
|
|
// src/fonts/ttf.zig - CMap
|
|
|
|
pub const CmapTable = struct {
|
|
format: u16,
|
|
data: []const u8,
|
|
|
|
/// Lookup codepoint -> glyph ID
|
|
pub fn lookup(self: *const CmapTable, codepoint: u21) ?u16 {
|
|
switch (self.format) {
|
|
4 => return self.lookupFormat4(codepoint),
|
|
12 => return self.lookupFormat12(codepoint),
|
|
else => return null,
|
|
}
|
|
}
|
|
|
|
fn lookupFormat4(self: *const CmapTable, codepoint: u21) ?u16 {
|
|
if (codepoint > 0xFFFF) return null;
|
|
const cp16: u16 = @intCast(codepoint);
|
|
|
|
const seg_count = readU16BE(self.data, 6) / 2;
|
|
const end_codes = self.data[14..];
|
|
const start_codes = self.data[14 + seg_count * 2 + 2..];
|
|
const id_deltas = self.data[14 + seg_count * 4 + 2..];
|
|
const id_range_offsets = self.data[14 + seg_count * 6 + 2..];
|
|
|
|
for (0..seg_count) |i| {
|
|
const end = readU16BE(end_codes, i * 2);
|
|
if (cp16 <= end) {
|
|
const start = readU16BE(start_codes, i * 2);
|
|
if (cp16 >= start) {
|
|
const range_offset = readU16BE(id_range_offsets, i * 2);
|
|
if (range_offset == 0) {
|
|
const delta = readI16BE(id_deltas, i * 2);
|
|
return @bitCast(@as(i16, @intCast(cp16)) +% delta);
|
|
} else {
|
|
const offset = range_offset / 2 + (cp16 - start);
|
|
const glyph_addr = 14 + seg_count * 6 + 2 + i * 2 + offset * 2;
|
|
return readU16BE(self.data, glyph_addr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
```
|
|
|
|
### 7.3 Subsetting (solo incluir glyphs usados)
|
|
|
|
```zig
|
|
// src/fonts/subset.zig
|
|
|
|
pub const FontSubset = struct {
|
|
original: *const TtfFont,
|
|
used_glyphs: std.AutoHashMap(u16, void),
|
|
glyph_remap: std.AutoHashMap(u16, u16), // old_id -> new_id
|
|
|
|
pub fn init(allocator: Allocator, font: *const TtfFont) FontSubset {
|
|
return .{
|
|
.original = font,
|
|
.used_glyphs = std.AutoHashMap(u16, void).init(allocator),
|
|
.glyph_remap = std.AutoHashMap(u16, u16).init(allocator),
|
|
};
|
|
}
|
|
|
|
pub fn addText(self: *FontSubset, text: []const u8) !void {
|
|
var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
|
|
while (iter.nextCodepoint()) |cp| {
|
|
const glyph = self.original.charToGlyph(cp);
|
|
try self.used_glyphs.put(glyph, {});
|
|
// Tambien agregar glyphs de composites si aplica
|
|
}
|
|
}
|
|
|
|
/// Genera TTF subset con solo los glyphs usados
|
|
pub fn generateSubset(self: *FontSubset, allocator: Allocator) ![]u8 {
|
|
// 1. Asignar nuevos IDs a glyphs (0=.notdef siempre primero)
|
|
var new_id: u16 = 0;
|
|
try self.glyph_remap.put(0, 0);
|
|
new_id += 1;
|
|
|
|
var iter = self.used_glyphs.keyIterator();
|
|
while (iter.next()) |glyph| {
|
|
if (glyph.* != 0) {
|
|
try self.glyph_remap.put(glyph.*, new_id);
|
|
new_id += 1;
|
|
}
|
|
}
|
|
|
|
// 2. Construir nuevas tablas
|
|
const new_glyf = try self.buildGlyfTable(allocator);
|
|
const new_loca = try self.buildLocaTable(allocator);
|
|
const new_hmtx = try self.buildHmtxTable(allocator);
|
|
const new_cmap = try self.buildCmapTable(allocator);
|
|
|
|
// 3. Ensamblar TTF
|
|
return try self.assembleTtf(allocator, new_glyf, new_loca, new_hmtx, new_cmap);
|
|
}
|
|
};
|
|
```
|
|
|
|
### 7.4 Embeber TTF en PDF
|
|
|
|
```zig
|
|
// src/fonts/embedded.zig
|
|
|
|
pub const EmbeddedFont = struct {
|
|
name: []const u8, // Nombre unico en PDF (ej: "F1")
|
|
ttf: *const TtfFont,
|
|
subset: ?*FontSubset,
|
|
|
|
/// Genera el objeto Font para PDF
|
|
pub fn generatePdfObjects(self: *EmbeddedFont, producer: *OutputProducer) !struct {
|
|
font_obj_id: u32,
|
|
descriptor_obj_id: u32,
|
|
file_obj_id: u32,
|
|
tounicode_obj_id: u32,
|
|
} {
|
|
// Font dictionary
|
|
// << /Type /Font
|
|
// /Subtype /TrueType (o /Type0 para CID)
|
|
// /BaseFont /FontName+Subset
|
|
// /FontDescriptor 5 0 R
|
|
// /ToUnicode 6 0 R
|
|
// /Encoding /WinAnsiEncoding (o CMap para CID)
|
|
// >>
|
|
|
|
// FontDescriptor
|
|
// << /Type /FontDescriptor
|
|
// /FontName /FontName+Subset
|
|
// /Flags 32
|
|
// /FontBBox [...]
|
|
// /ItalicAngle 0
|
|
// /Ascent 800
|
|
// /Descent -200
|
|
// /CapHeight 700
|
|
// /StemV 80
|
|
// /FontFile2 7 0 R (TTF data)
|
|
// >>
|
|
|
|
// FontFile2 (compressed TTF)
|
|
// << /Length ... /Length1 ... /Filter /FlateDecode >>
|
|
// stream
|
|
// [compressed TTF subset data]
|
|
// endstream
|
|
}
|
|
|
|
/// Genera ToUnicode CMap para busqueda de texto
|
|
pub fn generateToUnicode(self: *EmbeddedFont, allocator: Allocator) ![]u8 {
|
|
var buf = ArrayList(u8).init(allocator);
|
|
const w = buf.writer();
|
|
|
|
try w.writeAll("/CIDInit /ProcSet findresource begin\n");
|
|
try w.writeAll("12 dict begin\n");
|
|
try w.writeAll("begincmap\n");
|
|
try w.writeAll("/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n");
|
|
try w.writeAll("/CMapName /Adobe-Identity-UCS def\n");
|
|
try w.writeAll("/CMapType 2 def\n");
|
|
try w.writeAll("1 begincodespacerange\n");
|
|
try w.writeAll("<0000> <FFFF>\n");
|
|
try w.writeAll("endcodespacerange\n");
|
|
|
|
// Mapear glyph IDs a Unicode
|
|
// ...
|
|
|
|
try w.writeAll("endcmap\n");
|
|
try w.writeAll("CMapName currentdict /CMap defineresource pop\n");
|
|
try w.writeAll("end end\n");
|
|
|
|
return buf.toOwnedSlice();
|
|
}
|
|
};
|
|
```
|
|
|
|
### 7.5 API para usuario
|
|
|
|
```zig
|
|
// src/pdf.zig - AGREGAR
|
|
|
|
pub const Pdf = struct {
|
|
// ... campos existentes ...
|
|
embedded_fonts: std.ArrayListUnmanaged(EmbeddedFont),
|
|
|
|
/// Carga una fuente TTF desde archivo
|
|
pub fn loadFont(self: *Self, path: []const u8) !FontHandle {
|
|
const ttf = try TtfFont.initFromFile(self.allocator, path);
|
|
try self.embedded_fonts.append(self.allocator, .{
|
|
.ttf = ttf,
|
|
.subset = null,
|
|
});
|
|
return FontHandle{ .index = self.embedded_fonts.items.len - 1 };
|
|
}
|
|
|
|
/// Carga una fuente TTF desde memoria
|
|
pub fn loadFontFromMemory(self: *Self, data: []const u8) !FontHandle {
|
|
const ttf = try TtfFont.initFromData(self.allocator, data, false);
|
|
// ...
|
|
}
|
|
};
|
|
|
|
pub const FontHandle = struct {
|
|
index: usize,
|
|
};
|
|
|
|
// src/page.zig - AGREGAR
|
|
|
|
pub const Page = struct {
|
|
/// Usa fuente TTF embebida
|
|
pub fn setTtfFont(self: *Self, handle: FontHandle, size: f32) !void {
|
|
self.current_ttf_font = handle;
|
|
self.current_font_size = size;
|
|
// Agregar a fonts_used
|
|
}
|
|
};
|
|
```
|
|
|
|
### Ejemplo: examples/ttf_demo.zig
|
|
|
|
```zig
|
|
const std = @import("std");
|
|
const pdf = @import("zcatpdf");
|
|
|
|
pub fn main() !void {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
const allocator = gpa.allocator();
|
|
|
|
var doc = pdf.Pdf.init(allocator, .{});
|
|
defer doc.deinit();
|
|
|
|
// Cargar fuentes TTF
|
|
const roboto = try doc.loadFont("fonts/Roboto-Regular.ttf");
|
|
const roboto_bold = try doc.loadFont("fonts/Roboto-Bold.ttf");
|
|
|
|
var page = try doc.addPage(.{});
|
|
|
|
// Usar fuentes TTF
|
|
try page.setTtfFont(roboto_bold, 24);
|
|
try page.drawText(50, 750, "Hola Mundo con TTF!");
|
|
|
|
try page.setTtfFont(roboto, 12);
|
|
try page.drawText(50, 720, "Texto con caracteres especiales: cafe, nino, corazon");
|
|
|
|
// El subsetting se hace automaticamente al guardar
|
|
try doc.save("ttf_demo.pdf");
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## FASE 8: Bookmarks / Outline
|
|
|
|
**Prioridad**: ALTA
|
|
**Dependencias**: Ninguna
|
|
**Archivos nuevos**: `src/outline.zig`
|
|
|
|
### 8.1 Estructura de Outline
|
|
|
|
```zig
|
|
// src/outline.zig
|
|
|
|
pub const OutlineItem = struct {
|
|
title: []const u8,
|
|
page_index: usize, // Pagina destino (0-based)
|
|
y_position: ?f32 = null, // Posicion Y en la pagina (opcional)
|
|
children: std.ArrayListUnmanaged(OutlineItem),
|
|
|
|
// Campos internos para serialization
|
|
obj_id: u32 = 0,
|
|
parent_obj_id: u32 = 0,
|
|
|
|
pub fn init(allocator: Allocator, title: []const u8, page: usize) OutlineItem {
|
|
return .{
|
|
.title = title,
|
|
.page_index = page,
|
|
.children = .{},
|
|
};
|
|
}
|
|
|
|
pub fn addChild(self: *OutlineItem, allocator: Allocator, title: []const u8, page: usize) !*OutlineItem {
|
|
const child = OutlineItem.init(allocator, title, page);
|
|
try self.children.append(allocator, child);
|
|
return &self.children.items[self.children.items.len - 1];
|
|
}
|
|
|
|
pub fn deinit(self: *OutlineItem, allocator: Allocator) void {
|
|
for (self.children.items) |*child| {
|
|
child.deinit(allocator);
|
|
}
|
|
self.children.deinit(allocator);
|
|
}
|
|
};
|
|
|
|
pub const Outline = struct {
|
|
allocator: Allocator,
|
|
items: std.ArrayListUnmanaged(OutlineItem),
|
|
|
|
pub fn init(allocator: Allocator) Outline {
|
|
return .{
|
|
.allocator = allocator,
|
|
.items = .{},
|
|
};
|
|
}
|
|
|
|
pub fn addItem(self: *Outline, title: []const u8, page: usize) !*OutlineItem {
|
|
const item = OutlineItem.init(self.allocator, title, page);
|
|
try self.items.append(self.allocator, item);
|
|
return &self.items.items[self.items.items.len - 1];
|
|
}
|
|
|
|
/// Cuenta total de items (incluyendo hijos)
|
|
pub fn totalCount(self: *const Outline) usize {
|
|
var count: usize = 0;
|
|
for (self.items.items) |*item| {
|
|
count += 1 + countChildren(item);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
fn countChildren(item: *const OutlineItem) usize {
|
|
var count: usize = 0;
|
|
for (item.children.items) |*child| {
|
|
count += 1 + countChildren(child);
|
|
}
|
|
return count;
|
|
}
|
|
};
|
|
```
|
|
|
|
### 8.2 Serializar Outline en PDF
|
|
|
|
```zig
|
|
// src/output/producer.zig - MODIFICAR
|
|
|
|
pub fn generateWithImages(..., outline: ?*const Outline) ![]u8 {
|
|
// ...
|
|
|
|
// Calcular IDs de objetos
|
|
// ... existentes ...
|
|
const first_outline_id: u32 = first_page_id + pages.len * 2;
|
|
const outline_count = if (outline) |o| o.totalCount() else 0;
|
|
|
|
// En Catalog, agregar referencia a Outlines
|
|
if (outline != null) {
|
|
try writer.print("/Outlines {d} 0 R\n", .{first_outline_id});
|
|
try writer.writeAll("/PageMode /UseOutlines\n"); // Abrir con bookmarks visibles
|
|
}
|
|
|
|
// Escribir objetos de outline
|
|
if (outline) |o| {
|
|
try self.writeOutlines(o, first_outline_id, first_page_id);
|
|
}
|
|
}
|
|
|
|
fn writeOutlines(self: *Self, outline: *const Outline, first_id: u32, first_page_id: u32) !void {
|
|
const writer = self.buffer.writer(self.allocator);
|
|
|
|
// Objeto raiz de Outlines
|
|
try self.beginObject(first_id);
|
|
try writer.writeAll("<< /Type /Outlines\n");
|
|
|
|
if (outline.items.items.len > 0) {
|
|
try writer.print("/First {d} 0 R\n", .{first_id + 1});
|
|
try writer.print("/Last {d} 0 R\n", .{first_id + countUntilLast(outline)});
|
|
try writer.print("/Count {d}\n", .{outline.totalCount()});
|
|
} else {
|
|
try writer.writeAll("/Count 0\n");
|
|
}
|
|
try writer.writeAll(">>\n");
|
|
try self.endObject();
|
|
|
|
// Escribir items recursivamente
|
|
var current_id = first_id + 1;
|
|
for (outline.items.items, 0..) |*item, i| {
|
|
try self.writeOutlineItem(item, ¤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 <<resources>> >>
|
|
|
|
for (self.fields.items, 0..) |field, i| {
|
|
const field_id = first_field_id + @intCast(u32, i);
|
|
try producer.beginObject(field_id);
|
|
try self.writeFieldDict(producer.writer(), &field, page_id);
|
|
try producer.endObject();
|
|
}
|
|
}
|
|
|
|
fn writeFieldDict(writer: anytype, field: *const FormField, page_id: u32) !void {
|
|
try writer.writeAll("<< /Type /Annot /Subtype /Widget\n");
|
|
try writer.print("/Rect [{d:.2} {d:.2} {d:.2} {d:.2}]\n", .{
|
|
field.rect.x, field.rect.y,
|
|
field.rect.x + field.rect.width,
|
|
field.rect.y + field.rect.height,
|
|
});
|
|
try writer.print("/P {d} 0 R\n", .{page_id});
|
|
try writer.writeAll("/T ");
|
|
try writeString(writer, field.name);
|
|
try writer.writeByte('\n');
|
|
|
|
switch (field.field_type) {
|
|
.text => {
|
|
try writer.writeAll("/FT /Tx\n");
|
|
if (field.options.multiline) {
|
|
try writer.writeAll("/Ff 4096\n"); // Multiline flag
|
|
}
|
|
},
|
|
.checkbox => {
|
|
try writer.writeAll("/FT /Btn\n");
|
|
// ...
|
|
},
|
|
// ... otros tipos
|
|
}
|
|
|
|
try writer.writeAll(">>");
|
|
}
|
|
};
|
|
```
|
|
|
|
### 15.3 API para usuario
|
|
|
|
```zig
|
|
// src/page.zig - AGREGAR
|
|
|
|
pub const Page = struct {
|
|
form_fields: std.ArrayListUnmanaged(FormField),
|
|
|
|
/// Agrega campo de texto
|
|
pub fn addTextField(self: *Self, name: []const u8, x: f32, y: f32, w: f32, h: f32) !*FormField {
|
|
const field = FormField{
|
|
.field_type = .text,
|
|
.name = name,
|
|
.rect = .{ .x = x, .y = y, .width = w, .height = h },
|
|
};
|
|
try self.form_fields.append(self.allocator, field);
|
|
return &self.form_fields.items[self.form_fields.items.len - 1];
|
|
}
|
|
|
|
/// Agrega checkbox
|
|
pub fn addCheckbox(self: *Self, name: []const u8, x: f32, y: f32, size: f32) !*FormField {
|
|
// ...
|
|
}
|
|
|
|
/// Agrega combobox (dropdown)
|
|
pub fn addCombobox(self: *Self, name: []const u8, x: f32, y: f32, w: f32, h: f32,
|
|
options: []const []const u8) !*FormField {
|
|
// ...
|
|
}
|
|
};
|
|
```
|
|
|
|
### Ejemplo: examples/forms_demo.zig
|
|
|
|
```zig
|
|
const std = @import("std");
|
|
const pdf = @import("zcatpdf");
|
|
|
|
pub fn main() !void {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
const allocator = gpa.allocator();
|
|
|
|
var doc = pdf.Pdf.init(allocator, .{});
|
|
defer doc.deinit();
|
|
|
|
var page = try doc.addPage(.{});
|
|
|
|
try page.setFont(.helvetica_bold, 18);
|
|
try page.drawText(50, 800, "Formulario de Registro");
|
|
|
|
try page.setFont(.helvetica, 12);
|
|
|
|
// Nombre
|
|
try page.drawText(50, 750, "Nombre:");
|
|
_ = try page.addTextField("nombre", 120, 745, 200, 20);
|
|
|
|
// Email
|
|
try page.drawText(50, 710, "Email:");
|
|
_ = try page.addTextField("email", 120, 705, 200, 20);
|
|
|
|
// Acepto terminos
|
|
try page.drawText(50, 670, "Acepto los terminos:");
|
|
_ = try page.addCheckbox("acepto", 180, 668, 15);
|
|
|
|
// Pais (dropdown)
|
|
try page.drawText(50, 630, "Pais:");
|
|
_ = try page.addCombobox("pais", 120, 625, 150, 20, &.{
|
|
"Espana", "Mexico", "Argentina", "Colombia", "Chile",
|
|
});
|
|
|
|
try doc.save("forms_demo.pdf");
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## FASE 16: SVG Import (Basico)
|
|
|
|
**Prioridad**: BAJA
|
|
**Dependencias**: Curvas Bezier (Fase 9)
|
|
**Archivos nuevos**: `src/svg/`
|
|
|
|
### 16.1 Parser SVG simplificado
|
|
|
|
```zig
|
|
// src/svg/parser.zig
|
|
|
|
pub const SvgParser = struct {
|
|
allocator: Allocator,
|
|
|
|
pub fn parseFile(self: *SvgParser, path: []const u8) !SvgDocument {
|
|
const file = try std.fs.cwd().openFile(path, .{});
|
|
defer file.close();
|
|
const content = try file.readToEndAlloc(self.allocator, 1024 * 1024);
|
|
defer self.allocator.free(content);
|
|
return try self.parse(content);
|
|
}
|
|
|
|
pub fn parse(self: *SvgParser, svg_content: []const u8) !SvgDocument {
|
|
// Parser XML simplificado
|
|
var doc = SvgDocument.init(self.allocator);
|
|
|
|
// Extraer viewBox
|
|
if (findAttribute(svg_content, "viewBox")) |vb| {
|
|
doc.viewbox = try parseViewBox(vb);
|
|
}
|
|
|
|
// Parsear elementos
|
|
try self.parseElements(&doc, svg_content);
|
|
|
|
return doc;
|
|
}
|
|
|
|
fn parseElements(self: *SvgParser, doc: *SvgDocument, content: []const u8) !void {
|
|
// Buscar elementos soportados
|
|
var pos: usize = 0;
|
|
while (findNextElement(content, pos)) |elem| {
|
|
const tag = elem.tag;
|
|
if (std.mem.eql(u8, tag, "path")) {
|
|
try doc.elements.append(self.allocator, try self.parsePath(elem));
|
|
} else if (std.mem.eql(u8, tag, "rect")) {
|
|
try doc.elements.append(self.allocator, try self.parseRect(elem));
|
|
} else if (std.mem.eql(u8, tag, "circle")) {
|
|
try doc.elements.append(self.allocator, try self.parseCircle(elem));
|
|
} else if (std.mem.eql(u8, tag, "line")) {
|
|
try doc.elements.append(self.allocator, try self.parseLine(elem));
|
|
} else if (std.mem.eql(u8, tag, "text")) {
|
|
try doc.elements.append(self.allocator, try self.parseText(elem));
|
|
}
|
|
pos = elem.end;
|
|
}
|
|
}
|
|
|
|
fn parsePath(self: *SvgParser, elem: Element) !SvgElement {
|
|
// Parsear atributo "d" (path data)
|
|
const d = findAttribute(elem.content, "d") orelse return error.MissingPathData;
|
|
return SvgElement{
|
|
.element_type = .path,
|
|
.path_data = try parsePathData(self.allocator, d),
|
|
.style = try parseStyle(elem.content),
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const SvgDocument = struct {
|
|
viewbox: ?ViewBox = null,
|
|
elements: std.ArrayListUnmanaged(SvgElement),
|
|
};
|
|
|
|
pub const SvgElement = struct {
|
|
element_type: ElementType,
|
|
path_data: ?[]PathCommand = null,
|
|
rect_data: ?RectData = null,
|
|
// ... otros datos
|
|
style: Style,
|
|
|
|
pub const ElementType = enum { path, rect, circle, ellipse, line, polyline, polygon, text };
|
|
};
|
|
|
|
pub const PathCommand = struct {
|
|
command: u8, // M, L, C, Q, Z, etc
|
|
args: [6]f32,
|
|
arg_count: u8,
|
|
};
|
|
```
|
|
|
|
### 16.2 Renderizar SVG a PDF
|
|
|
|
```zig
|
|
// src/svg/renderer.zig
|
|
|
|
pub const SvgRenderer = struct {
|
|
pub fn render(page: *Page, doc: *const SvgDocument, x: f32, y: f32, scale: f32) !void {
|
|
try page.stream.saveState();
|
|
try page.stream.translate(x, y);
|
|
try page.stream.scale(scale, -scale); // SVG Y axis is inverted
|
|
|
|
for (doc.elements.items) |elem| {
|
|
try renderElement(page, &elem);
|
|
}
|
|
|
|
try page.stream.restoreState();
|
|
}
|
|
|
|
fn renderElement(page: *Page, elem: *const SvgElement) !void {
|
|
// Aplicar estilo
|
|
if (elem.style.fill) |fill| {
|
|
page.setFillColor(svgColorToPdf(fill));
|
|
}
|
|
if (elem.style.stroke) |stroke| {
|
|
page.setStrokeColor(svgColorToPdf(stroke));
|
|
}
|
|
if (elem.style.stroke_width) |sw| {
|
|
try page.setLineWidth(sw);
|
|
}
|
|
|
|
switch (elem.element_type) {
|
|
.path => try renderPath(page, elem.path_data.?),
|
|
.rect => try renderRect(page, elem.rect_data.?),
|
|
.circle => try renderCircle(page, elem.circle_data.?),
|
|
// ...
|
|
}
|
|
}
|
|
|
|
fn renderPath(page: *Page, commands: []const PathCommand) !void {
|
|
for (commands) |cmd| {
|
|
switch (cmd.command) {
|
|
'M' => try page.stream.moveTo(cmd.args[0], cmd.args[1]),
|
|
'L' => try page.stream.lineTo(cmd.args[0], cmd.args[1]),
|
|
'C' => try page.stream.curveTo(
|
|
cmd.args[0], cmd.args[1],
|
|
cmd.args[2], cmd.args[3],
|
|
cmd.args[4], cmd.args[5],
|
|
),
|
|
'Q' => {
|
|
// Convertir cuadratica a cubica
|
|
// ...
|
|
},
|
|
'Z' => try page.stream.closePath(),
|
|
// ... mas comandos
|
|
}
|
|
}
|
|
try page.stream.stroke();
|
|
}
|
|
};
|
|
```
|
|
|
|
### 16.3 API
|
|
|
|
```zig
|
|
// src/page.zig - AGREGAR
|
|
|
|
pub const Page = struct {
|
|
/// Dibuja SVG desde archivo
|
|
pub fn drawSvgFile(self: *Self, path: []const u8, x: f32, y: f32, opts: SvgOptions) !void {
|
|
var parser = svg.SvgParser{ .allocator = self.allocator };
|
|
const doc = try parser.parseFile(path);
|
|
defer doc.deinit(self.allocator);
|
|
|
|
const scale = opts.width / (doc.viewbox.?.width);
|
|
try svg.SvgRenderer.render(self, &doc, x, y, scale);
|
|
}
|
|
|
|
/// Dibuja SVG desde string
|
|
pub fn drawSvg(self: *Self, svg_content: []const u8, x: f32, y: f32, opts: SvgOptions) !void {
|
|
// ...
|
|
}
|
|
|
|
pub const SvgOptions = struct {
|
|
width: f32 = 100,
|
|
height: ?f32 = null, // Auto-calculate maintaining aspect ratio
|
|
};
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## FASE 17: Templates
|
|
|
|
**Prioridad**: BAJA
|
|
**Dependencias**: Ninguna
|
|
**Archivos nuevos**: `src/template.zig`
|
|
|
|
### 17.1 Sistema de templates
|
|
|
|
```zig
|
|
// src/template.zig
|
|
|
|
pub const Template = struct {
|
|
allocator: Allocator,
|
|
content_stream: []const u8,
|
|
width: f32,
|
|
height: f32,
|
|
resources: Resources,
|
|
|
|
pub const Resources = struct {
|
|
fonts: []const Font,
|
|
images: []const usize,
|
|
};
|
|
|
|
/// Crea template desde pagina existente
|
|
pub fn fromPage(allocator: Allocator, page: *const Page) !Template {
|
|
return .{
|
|
.allocator = allocator,
|
|
.content_stream = try allocator.dupe(u8, page.getContent()),
|
|
.width = page.width,
|
|
.height = page.height,
|
|
.resources = .{
|
|
.fonts = try collectFonts(allocator, page),
|
|
.images = &.{},
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Aplica template a nueva pagina
|
|
pub fn applyTo(self: *const Template, page: *Page) !void {
|
|
// Copiar content stream
|
|
try page.stream.appendSlice(self.content_stream);
|
|
|
|
// Copiar recursos
|
|
for (self.resources.fonts) |font| {
|
|
try page.fonts_used.put(font, {});
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### 17.2 API
|
|
|
|
```zig
|
|
// src/pdf.zig - AGREGAR
|
|
|
|
pub const Pdf = struct {
|
|
templates: std.StringHashMap(Template),
|
|
|
|
/// Guarda pagina como template
|
|
pub fn saveAsTemplate(self: *Self, page: *const Page, name: []const u8) !void {
|
|
const template = try Template.fromPage(self.allocator, page);
|
|
try self.templates.put(name, template);
|
|
}
|
|
|
|
/// Agrega pagina usando template
|
|
pub fn addPageFromTemplate(self: *Self, template_name: []const u8) !*Page {
|
|
const template = self.templates.get(template_name) orelse return error.TemplateNotFound;
|
|
var page = try self.addPage(.{});
|
|
try template.applyTo(page);
|
|
return page;
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## FASE 18: Markdown Styling
|
|
|
|
**Prioridad**: BAJA
|
|
**Dependencias**: Ninguna
|
|
**Archivos nuevos**: `src/markdown.zig`
|
|
|
|
### 18.1 Parser Markdown basico
|
|
|
|
```zig
|
|
// src/markdown.zig
|
|
|
|
pub const MarkdownRenderer = struct {
|
|
allocator: Allocator,
|
|
|
|
pub fn render(self: *MarkdownRenderer, page: *Page, x: f32, y: f32, width: f32,
|
|
markdown: []const u8) !f32 {
|
|
var current_y = y;
|
|
var lines = std.mem.tokenize(u8, markdown, "\n");
|
|
|
|
while (lines.next()) |line| {
|
|
current_y = try self.renderLine(page, x, current_y, width, line);
|
|
}
|
|
|
|
return current_y;
|
|
}
|
|
|
|
fn renderLine(self: *MarkdownRenderer, page: *Page, x: f32, y: f32,
|
|
width: f32, line: []const u8) !f32 {
|
|
// Headers
|
|
if (std.mem.startsWith(u8, line, "# ")) {
|
|
try page.setFont(.helvetica_bold, 24);
|
|
try page.drawText(x, y, line[2..]);
|
|
return y - 30;
|
|
} else if (std.mem.startsWith(u8, line, "## ")) {
|
|
try page.setFont(.helvetica_bold, 18);
|
|
try page.drawText(x, y, line[3..]);
|
|
return y - 24;
|
|
} else if (std.mem.startsWith(u8, line, "### ")) {
|
|
try page.setFont(.helvetica_bold, 14);
|
|
try page.drawText(x, y, line[4..]);
|
|
return y - 20;
|
|
}
|
|
|
|
// Bullet list
|
|
if (std.mem.startsWith(u8, line, "- ") or std.mem.startsWith(u8, line, "* ")) {
|
|
try page.setFont(.helvetica, 12);
|
|
try page.drawText(x, y, "•");
|
|
try page.drawText(x + 15, y, line[2..]);
|
|
return y - 16;
|
|
}
|
|
|
|
// Bold/italic inline (simplificado)
|
|
try page.setFont(.helvetica, 12);
|
|
try self.renderInline(page, x, y, width, line);
|
|
return y - 16;
|
|
}
|
|
|
|
fn renderInline(self: *MarkdownRenderer, page: *Page, x: f32, y: f32,
|
|
width: f32, text: []const u8) !void {
|
|
var current_x = x;
|
|
var i: usize = 0;
|
|
var start: usize = 0;
|
|
|
|
while (i < text.len) {
|
|
// **bold**
|
|
if (i + 1 < text.len and text[i] == '*' and text[i+1] == '*') {
|
|
// Render text before
|
|
if (i > start) {
|
|
try page.drawText(current_x, y, text[start..i]);
|
|
current_x += page.getStringWidth(text[start..i]);
|
|
}
|
|
|
|
// Find closing **
|
|
const end = std.mem.indexOf(u8, text[i+2..], "**") orelse break;
|
|
const bold_text = text[i+2..i+2+end];
|
|
|
|
try page.setFont(.helvetica_bold, page.current_font_size);
|
|
try page.drawText(current_x, y, bold_text);
|
|
current_x += page.getStringWidth(bold_text);
|
|
try page.setFont(.helvetica, page.current_font_size);
|
|
|
|
i += 4 + end;
|
|
start = i;
|
|
continue;
|
|
}
|
|
|
|
// *italic*
|
|
if (text[i] == '*' and (i == 0 or text[i-1] != '*')) {
|
|
// Similar...
|
|
}
|
|
|
|
i += 1;
|
|
}
|
|
|
|
// Render remaining text
|
|
if (start < text.len) {
|
|
try page.drawText(current_x, y, text[start..]);
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### 18.2 API
|
|
|
|
```zig
|
|
// src/page.zig - AGREGAR
|
|
|
|
pub const Page = struct {
|
|
/// Renderiza texto Markdown
|
|
pub fn writeMarkdown(self: *Self, x: f32, y: f32, width: f32, markdown: []const u8) !f32 {
|
|
var renderer = MarkdownRenderer{ .allocator = self.allocator };
|
|
return try renderer.render(self, x, y, width, markdown);
|
|
}
|
|
};
|
|
```
|
|
|
|
### Ejemplo: examples/markdown_demo.zig
|
|
|
|
```zig
|
|
const std = @import("std");
|
|
const pdf = @import("zcatpdf");
|
|
|
|
pub fn main() !void {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
const allocator = gpa.allocator();
|
|
|
|
var doc = pdf.Pdf.init(allocator, .{});
|
|
defer doc.deinit();
|
|
|
|
var page = try doc.addPage(.{});
|
|
|
|
const markdown =
|
|
\\# Titulo Principal
|
|
\\
|
|
\\Este es un parrafo con **texto en negrita** y *texto en italica*.
|
|
\\
|
|
\\## Subtitulo
|
|
\\
|
|
\\Lista de items:
|
|
\\- Primer item
|
|
\\- Segundo item
|
|
\\- Tercer item
|
|
\\
|
|
\\### Seccion menor
|
|
\\
|
|
\\Mas texto aqui.
|
|
;
|
|
|
|
_ = try page.writeMarkdown(50, 780, 500, markdown);
|
|
|
|
try doc.save("markdown_demo.pdf");
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## FASE 19: Mejoras de Calidad
|
|
|
|
**Prioridad**: CONTINUA
|
|
**Dependencias**: Todas las fases anteriores
|
|
|
|
### 19.1 Builder Pattern / Fluent API
|
|
|
|
```zig
|
|
// src/page.zig - MEJORAR
|
|
|
|
pub const Page = struct {
|
|
/// Metodos fluent que retornan *Self
|
|
pub fn font(self: *Self, f: Font, size: f32) *Self {
|
|
self.setFont(f, size) catch {};
|
|
return self;
|
|
}
|
|
|
|
pub fn color(self: *Self, c: Color) *Self {
|
|
self.setFillColor(c);
|
|
return self;
|
|
}
|
|
|
|
pub fn at(self: *Self, x: f32, y: f32) *Self {
|
|
self.setXY(x, y);
|
|
return self;
|
|
}
|
|
|
|
pub fn text(self: *Self, t: []const u8) *Self {
|
|
self.drawText(self.x, self.y, t) catch {};
|
|
return self;
|
|
}
|
|
};
|
|
|
|
// Uso:
|
|
page.font(.helvetica_bold, 24)
|
|
.color(Color.blue)
|
|
.at(50, 750)
|
|
.text("Hello!")
|
|
.font(.helvetica, 12)
|
|
.at(50, 720)
|
|
.text("World");
|
|
```
|
|
|
|
### 19.2 Auto Page Break
|
|
|
|
```zig
|
|
// src/page.zig - AGREGAR
|
|
|
|
pub const Page = struct {
|
|
auto_page_break: bool = false,
|
|
page_break_margin: f32 = 20,
|
|
on_page_break: ?*const fn(*Pdf, *Page) anyerror!*Page = null,
|
|
|
|
pub fn enableAutoPageBreak(self: *Self, margin: f32, callback: anytype) void {
|
|
self.auto_page_break = true;
|
|
self.page_break_margin = margin;
|
|
self.on_page_break = callback;
|
|
}
|
|
|
|
fn checkPageBreak(self: *Self, height: f32) !void {
|
|
if (!self.auto_page_break) return;
|
|
|
|
if (self.y - height < self.page_break_margin) {
|
|
// Crear nueva pagina
|
|
if (self.on_page_break) |callback| {
|
|
_ = try callback(self.document, self);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### 19.3 Documentacion completa
|
|
|
|
- [ ] Documentar todos los modulos publicos con doc comments
|
|
- [ ] Crear README.md con ejemplos
|
|
- [ ] Generar documentacion con `zig build docs`
|
|
- [ ] Ejemplos para cada funcionalidad
|
|
|
|
### 19.4 Test coverage
|
|
|
|
- [ ] Tests unitarios para cada modulo
|
|
- [ ] Tests de integracion
|
|
- [ ] Tests de regresion con PDFs de referencia
|
|
- [ ] Benchmarks de rendimiento
|
|
|
|
---
|
|
|
|
## Resumen de Fases
|
|
|
|
| Fase | Nombre | Prioridad | Complejidad | Dependencias |
|
|
|------|--------|-----------|-------------|--------------|
|
|
| 6 | PNG + zlib | ALTA | Media | - |
|
|
| 7 | TTF Fonts | ALTA | Alta | Fase 6 |
|
|
| 8 | Bookmarks | ALTA | Baja | - |
|
|
| 9 | Curvas Bezier | MEDIA | Baja | - |
|
|
| 10 | Rotacion | MEDIA | Baja | - |
|
|
| 11 | Transparencia | MEDIA | Media | - |
|
|
| 12 | Gradientes | MEDIA | Media | Fase 11 |
|
|
| 13 | Barcodes | MEDIA | Media | - |
|
|
| 14 | Encriptacion | BAJA | Alta | - |
|
|
| 15 | Forms | BAJA | Alta | - |
|
|
| 16 | SVG | BAJA | Alta | Fase 9 |
|
|
| 17 | Templates | BAJA | Baja | - |
|
|
| 18 | Markdown | BAJA | Baja | - |
|
|
| 19 | Calidad | CONTINUA | - | Todas |
|
|
|
|
---
|
|
|
|
## Orden Recomendado de Implementacion
|
|
|
|
1. **Fase 6**: PNG + zlib (usa std.compress.zlib de Zig)
|
|
2. **Fase 8**: Bookmarks (relativamente simple, muy util)
|
|
3. **Fase 9**: Curvas Bezier (base para otras funcionalidades)
|
|
4. **Fase 10**: Rotacion (complementa graficos)
|
|
5. **Fase 11**: Transparencia (muy solicitado)
|
|
6. **Fase 7**: TTF Fonts (complejo pero esencial)
|
|
7. **Fase 13**: Barcodes (util para facturas)
|
|
8. **Fase 12**: Gradientes (nice to have)
|
|
9. **Fase 17**: Templates (util para documentos repetitivos)
|
|
10. **Fase 18**: Markdown (nice to have)
|
|
11. **Fase 15**: Forms (complejo, uso especifico)
|
|
12. **Fase 16**: SVG (muy complejo)
|
|
13. **Fase 14**: Encriptacion (complejo, uso especifico)
|
|
|
|
---
|
|
|
|
## Notas Tecnicas
|
|
|
|
### Zig 0.15.2 APIs
|
|
- Usar `std.ArrayListUnmanaged` (no `std.ArrayList`)
|
|
- Usar `std.compress.zlib` para compresion
|
|
- Usar `@intFromFloat`, `@floatFromInt` (no `@intCast`, etc. para conversiones float/int)
|
|
|
|
### PDF 1.4 Limitaciones
|
|
- Sin transparencia nativa (requiere PDF 1.4+)
|
|
- Sin Unicode directo en Type1 (solo WinAnsi)
|
|
- Sin compresion de objetos
|
|
|
|
### Referencias
|
|
- [PDF Reference 1.4](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.4.pdf)
|
|
- [fpdf2 source](https://github.com/py-pdf/fpdf2)
|
|
- [TrueType Reference](https://developer.apple.com/fonts/TrueType-Reference-Manual/)
|
|
- [QR Code spec](https://www.qrcode.com/en/about/standards.html)
|
|
|
|
---
|
|
|
|
*Plan creado: 2025-12-08*
|
|
*Ultima actualizacion: 2025-12-08*
|