From 18385941041049d5ecfead7b9e83d5c4217f634e Mon Sep 17 00:00:00 2001 From: reugenio Date: Mon, 8 Dec 2025 20:53:33 +0100 Subject: [PATCH] feat: v0.5 - Clickable link annotations (Feature Complete) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 Implementation: - Link annotations in PDF (clickable in viewers) - Page.addUrlLink() - add URL annotation - Page.addInternalLink() - add internal page link - Page.urlLink() / writeUrlLink() - visual + annotation combined - Page.getLinks() - retrieve page links - OutputProducer generates /Annots arrays for pages - Link annotation objects with /Type /Annot /Subtype /Link New example: - links_demo.zig - demonstrates URL and internal links - 2 pages with cross-page navigation - External URLs (example.com, GitHub, mailto) - Internal links between pages Final state: - 7 examples working (hello, invoice, text_demo, image_demo, table_demo, pagination_demo, links_demo) - ~70 tests passing - Complete feature set for document generation zpdf is now feature-complete for typical document generation needs: text, tables, images, pagination, and clickable links. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 434 +++++++++++++++++----------------------- build.zig | 19 ++ examples/links_demo.zig | 177 ++++++++++++++++ src/output/producer.zig | 77 ++++++- src/page.zig | 59 ++++++ src/pdf.zig | 1 + 6 files changed, 512 insertions(+), 255 deletions(-) create mode 100644 examples/links_demo.zig diff --git a/CLAUDE.md b/CLAUDE.md index 05d121c..0f57a30 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ > **Ultima actualizacion**: 2025-12-08 > **Lenguaje**: Zig 0.15.2 -> **Estado**: v0.4 - Imagenes + Utilidades (Table, Pagination, Headers/Footers, Links) +> **Estado**: v0.5 - Clickable Links + Complete Feature Set > **Fuente principal**: fpdf2 (Python) - https://github.com/py-pdf/fpdf2 ## Descripcion del Proyecto @@ -13,274 +13,184 @@ - Zero dependencias (100% Zig puro) - API simple y directa inspirada en fpdf2 - Enfocado en generacion de facturas/documentos comerciales -- Soporte para texto, tablas, imagenes y formas basicas -- Calidad open source (doc comments, codigo claro) +- Soporte completo: texto, tablas, imagenes, links, paginacion -**Objetivo**: Ser el pilar para generar PDFs en Zig con codigo 100% propio, replicando funcionalidad de librerias maduras como fpdf2/gofpdf. +**Caracteristicas principales**: +- Sistema de texto completo (cell, multiCell, alineacion, word wrap) +- 14 fuentes Type1 standard (Helvetica, Times, Courier, etc.) +- Imagenes JPEG embebidas (passthrough, sin re-encoding) +- Table helper para tablas formateadas +- Paginacion automatica (numeros de pagina, headers, footers) +- Links clickeables (URLs externas + links internos entre paginas) +- Colores RGB, CMYK, Grayscale --- -## Estado Actual del Proyecto +## Estado Actual - v0.5 -### Implementacion v0.4 +### Funcionalidades Implementadas -| Componente | Estado | Archivo | -|------------|--------|---------| -| **Pdf (API Principal)** | | | -| Pdf init/deinit | OK | `src/pdf.zig` | -| setTitle, setAuthor, setSubject | OK | `src/pdf.zig` | -| addPage (with options) | OK | `src/pdf.zig` | -| addJpegImage / addJpegImageFromFile | OK | `src/pdf.zig` | -| output() / render() | OK | `src/pdf.zig` | -| save() | OK | `src/pdf.zig` | -| **Page** | | | -| Page init/deinit | OK | `src/page.zig` | -| setFont / getFont / getFontSize | OK | `src/page.zig` | -| setFillColor / setStrokeColor / setTextColor | OK | `src/page.zig` | -| setXY / setX / setY / getX / getY | OK | `src/page.zig` | -| setMargins / setCellMargin | OK | `src/page.zig` | -| drawText / writeText | OK | `src/page.zig` | -| **drawLink / writeLink** | OK | `src/page.zig` | -| cell() / cellAdvanced() | OK | `src/page.zig` | -| multiCell() | OK | `src/page.zig` | -| ln() | OK | `src/page.zig` | -| drawLine / drawRect / fillRect | OK | `src/page.zig` | -| **image() / imageFit()** | OK | `src/page.zig` | -| **Table Helper** | | | -| Table.init() | OK | `src/table.zig` | -| Table.header() | OK | `src/table.zig` | -| Table.row() / rowStyled() | OK | `src/table.zig` | -| Table.footer() | OK | `src/table.zig` | -| Table.separator() / space() | OK | `src/table.zig` | -| setColumnAlign() / setColumnAligns() | OK | `src/table.zig` | -| **Pagination** | | | -| Pagination.addPageNumbers() | OK | `src/pagination.zig` | -| Pagination.addFooter() | OK | `src/pagination.zig` | -| addHeader() | OK | `src/pagination.zig` | -| addFooterWithLine() | OK | `src/pagination.zig` | -| **Images** | | | -| JPEG embedding (DCT passthrough) | OK | `src/images/jpeg.zig` | -| PNG metadata parsing | OK | `src/images/png.zig` | -| ImageInfo struct | OK | `src/images/image_info.zig` | -| **Types** | | | -| PageSize enum (A4, Letter, A3, A5, Legal) | OK | `src/objects/base.zig` | -| Font enum (14 Type1 fonts) | OK | `src/fonts/type1.zig` | -| Color struct (RGB, CMYK, Grayscale) | OK | `src/graphics/color.zig` | -| Align enum / Border struct | OK | `src/page.zig` | -| TableOptions struct | OK | `src/table.zig` | -| PageNumberOptions / HeaderOptions / FooterOptions | OK | `src/pagination.zig` | +| Categoria | Funcionalidad | Estado | +|-----------|---------------|--------| +| **Documento** | | | +| | Crear PDF 1.4 | OK | +| | Metadatos (titulo, autor, etc.) | OK | +| | Multiples paginas | OK | +| | Tamanos estandar (A4, Letter, A3, A5, Legal) | OK | +| **Texto** | | | +| | drawText() - texto en posicion | OK | +| | cell() - celda con bordes/relleno | OK | +| | multiCell() - word wrap automatico | OK | +| | Alineacion (left, center, right) | OK | +| | 14 fuentes Type1 standard | OK | +| **Graficos** | | | +| | Lineas | OK | +| | Rectangulos (stroke, fill) | OK | +| | Colores RGB/CMYK/Gray | OK | +| **Imagenes** | | | +| | JPEG embedding | OK | +| | image() / imageFit() | OK | +| | Aspect ratio preservation | OK | +| **Tablas** | | | +| | Table helper | OK | +| | header(), row(), footer() | OK | +| | Alineacion por columna | OK | +| | Estilos personalizados | OK | +| **Paginacion** | | | +| | Numeros de pagina | OK | +| | Headers automaticos | OK | +| | Footers automaticos | OK | +| | skip_pages option | OK | +| **Links** | | | +| | URL links clickeables | OK | +| | Internal page links | OK | +| | Visual styling (azul+subrayado) | OK | +| | Link annotations | OK | -### Tests +### Tests y Ejemplos -| Categoria | Tests | Estado | -|-----------|-------|--------| -| root.zig (integration) | 8 | OK | -| page.zig (Page operations) | 18 | OK | -| content_stream.zig | 6 | OK | -| graphics/color.zig | 5 | OK | -| fonts/type1.zig | 5 | OK | -| objects/base.zig | 5 | OK | -| output/producer.zig | 5 | OK | -| images/jpeg.zig | 4 | OK | -| images/png.zig | 3 | OK | -| table.zig | 3 | OK | -| pagination.zig | 2 | OK | -| links.zig | 2 | OK | -| **Total** | **~70** | OK | - -### Ejemplos - -| Ejemplo | Descripcion | Estado | -|---------|-------------|--------| -| hello.zig | PDF minimo con texto y formas | OK | -| invoice.zig | Factura completa realista | OK | -| text_demo.zig | Demo sistema de texto | OK | -| image_demo.zig | Demo imagenes JPEG | OK | -| **table_demo.zig** | Demo Table helper (3 estilos de tabla) | OK | -| **pagination_demo.zig** | Demo paginacion (5 paginas, headers, footers) | OK | +- **~70 tests** unitarios pasando +- **7 ejemplos** funcionales: + - `hello.zig` - PDF minimo + - `invoice.zig` - Factura completa + - `text_demo.zig` - Sistema de texto + - `image_demo.zig` - Imagenes JPEG + - `table_demo.zig` - Table helper + - `pagination_demo.zig` - Paginacion multi-pagina + - `links_demo.zig` - Links clickeables --- -## Arquitectura Modular +## Arquitectura ``` zpdf/ -├── CLAUDE.md # Este archivo - estado del proyecto +├── CLAUDE.md # Documentacion del proyecto ├── build.zig # Sistema de build ├── src/ -│ ├── root.zig # Exports publicos + Document legacy +│ ├── root.zig # Exports publicos │ ├── pdf.zig # Pdf facade (API principal) -│ ├── page.zig # Page + sistema de texto -│ ├── content_stream.zig # Content stream (operadores PDF) +│ ├── page.zig # Page + texto + links +│ ├── content_stream.zig # Operadores PDF │ ├── table.zig # Table helper -│ ├── pagination.zig # Numeracion de paginas, headers, footers -│ ├── links.zig # Links/URLs (estructura) +│ ├── pagination.zig # Paginacion, headers, footers +│ ├── links.zig # Link types │ ├── fonts/ -│ │ ├── mod.zig # Exports de fonts │ │ └── type1.zig # 14 fuentes Type1 + metricas │ ├── graphics/ -│ │ ├── mod.zig # Exports de graphics │ │ └── color.zig # Color (RGB, CMYK, Gray) │ ├── images/ -│ │ ├── mod.zig # Exports + detectFormat() -│ │ ├── image_info.zig # ImageInfo struct -│ │ ├── jpeg.zig # JPEG parser (DCT passthrough) -│ │ └── png.zig # PNG metadata parser +│ │ ├── jpeg.zig # JPEG parser +│ │ ├── png.zig # PNG metadata +│ │ └── image_info.zig # ImageInfo struct │ ├── objects/ -│ │ ├── mod.zig # Exports de objects -│ │ └── base.zig # PageSize, Orientation, Unit +│ │ └── base.zig # PageSize, Orientation │ └── output/ -│ ├── mod.zig # Exports de output -│ └── producer.zig # OutputProducer (serializa PDF + imagenes) +│ └── producer.zig # Serializa PDF + images + links └── examples/ - ├── hello.zig # Ejemplo basico - ├── invoice.zig # Factura ejemplo - ├── text_demo.zig # Demo sistema de texto - ├── image_demo.zig # Demo imagenes - ├── table_demo.zig # Demo tablas - └── pagination_demo.zig # Demo paginacion + ├── hello.zig + ├── invoice.zig + ├── text_demo.zig + ├── image_demo.zig + ├── table_demo.zig + ├── pagination_demo.zig + └── links_demo.zig ``` --- -## Roadmap +## API Quick Reference -### Fase 1 - Core + Refactoring (COMPLETADO) -- [x] Estructura documento PDF 1.4 -- [x] Paginas (A4, Letter, A3, A5, Legal, custom) -- [x] Texto basico (14 fuentes Type1 built-in) -- [x] Lineas y rectangulos -- [x] Colores RGB, CMYK, Grayscale -- [x] Serializacion correcta -- [x] Refactoring modular - -### Fase 2 - Sistema de Texto (COMPLETADO) -- [x] cell() - celda con bordes, relleno, alineacion -- [x] multiCell() - texto con word wrap automatico -- [x] ln() - salto de linea -- [x] Alineacion (left, center, right) -- [x] Bordes configurables - -### Fase 3 - Imagenes (COMPLETADO) -- [x] JPEG embebido (DCT passthrough, sin re-encoding) -- [x] PNG metadata parsing (embedding pendiente por zlib API) -- [x] image() - dibujar imagen en posicion -- [x] imageFit() - escalar manteniendo aspect ratio - -### Fase 4 - Utilidades (COMPLETADO) -- [x] Table helper (header, row, footer, estilos, alineacion por columna) -- [x] Numeracion de paginas (Pagination.addPageNumbers) -- [x] Headers automaticos (addHeader con linea separadora) -- [x] Footers automaticos (addFooter, addFooterWithLine) -- [x] Links visuales (drawLink, writeLink - azul + subrayado) - -### Fase 5 - Avanzado (FUTURO) -- [ ] Link annotations (clickeables en PDF) -- [ ] PNG embedding completo -- [ ] Fuentes TTF embebidas -- [ ] Compresion de streams (zlib) -- [ ] Bookmarks/outline - ---- - -## API Actual - -### Crear Documento y Paginas +### Documento Basico ```zig const zpdf = @import("zpdf"); -var doc = zpdf.Pdf.init(allocator, .{ - .page_size = .a4, - .orientation = .portrait, -}); +var doc = zpdf.Pdf.init(allocator, .{}); defer doc.deinit(); doc.setTitle("Mi Documento"); -doc.setAuthor("zpdf"); - var page = try doc.addPage(.{}); -try doc.save("documento.pdf"); + +try page.setFont(.helvetica_bold, 24); +try page.drawText(50, 750, "Hello, PDF!"); + +try doc.save("output.pdf"); ``` -### Sistema de Texto +### Celdas y Tablas ```zig -// cell() - celda con bordes y alineacion +// cell() simple try page.cell(100, 20, "Hello", zpdf.Border.all, .center, true); -// multiCell() - word wrap automatico +// multiCell con word wrap try page.multiCell(400, null, texto_largo, zpdf.Border.none, .left, false); -// ln() - salto de linea -page.ln(20); -``` - -### Table Helper - -```zig +// Table helper const widths = [_]f32{ 200, 100, 100 }; var table = zpdf.Table.init(page, .{ - .x = 50, - .y = 700, .col_widths = &widths, - .header_bg = zpdf.Color.rgb(41, 98, 255), }); - -table.setColumnAlign(0, .left); -table.setColumnAlign(1, .center); -table.setColumnAlign(2, .right); - -try table.header(&.{ "Producto", "Cantidad", "Precio" }); -try table.row(&.{ "Widget A", "10", "$50.00" }); -try table.row(&.{ "Widget B", "5", "$25.00" }); -try table.footer(&.{ "Total", "", "$75.00" }); +try table.header(&.{ "Producto", "Qty", "Precio" }); +try table.row(&.{ "Item A", "10", "$50" }); +try table.footer(&.{ "Total", "", "$50" }); ``` -### Paginacion y Headers/Footers +### Imagenes ```zig -// Agregar numeros de pagina a todas las paginas +const img_idx = try doc.addJpegImageFromFile("foto.jpg"); +const info = doc.getImage(img_idx).?; +try page.image(img_idx, info, 50, 500, 200, 150); +``` + +### Links Clickeables + +```zig +// URL link con visual styling +_ = try page.urlLink(100, 700, "Click aqui", "https://example.com"); + +// O desde posicion actual +_ = try page.writeUrlLink("Visitar web", "https://example.com"); + +// Link interno (saltar a pagina) +try page.addInternalLink(2, 100, 700, 150, 20); // page 2 +``` + +### Paginacion + +```zig +// Numeros de pagina try zpdf.Pagination.addPageNumbers(&doc, .{ .format = "Page {PAGE} of {PAGES}", .position = .bottom_center, }); -// Header con linea separadora -try zpdf.addHeader(&doc, "Documento Confidencial", .{ - .alignment = .center, +// Header con linea +try zpdf.addHeader(&doc, "Documento", .{ .draw_line = true, }); - -// Footer con linea -try zpdf.addFooterWithLine(&doc, "Copyright 2025", .{ - .alignment = .left, - .draw_line = true, -}); -``` - -### Imagenes JPEG - -```zig -// Cargar imagen desde archivo -const img_idx = try doc.addJpegImageFromFile("foto.jpg"); -const info = doc.getImage(img_idx).?; - -// Dibujar en la pagina -try page.image(img_idx, info, 50, 500, 200, 150); - -// O escalar automaticamente manteniendo aspect ratio -try page.imageFit(img_idx, info, 50, 500, 400, 300); -``` - -### Links Visuales - -```zig -// Texto con estilo de link (azul + subrayado) -_ = try page.drawLink(100, 700, "Visita nuestra web"); - -// O desde la posicion actual -_ = try page.writeLink("Click aqui"); ``` --- @@ -291,82 +201,100 @@ _ = try page.writeLink("Click aqui"); # Zig path ZIG=/mnt/cello2/arno/re/recode/zig/zig-0.15.2/zig-x86_64-linux-0.15.2/zig -# Compilar todo +# Compilar $ZIG build # Tests $ZIG build test -# Ejecutar ejemplos +# Ejemplos ./zig-out/bin/hello ./zig-out/bin/invoice ./zig-out/bin/text_demo ./zig-out/bin/image_demo ./zig-out/bin/table_demo ./zig-out/bin/pagination_demo +./zig-out/bin/links_demo ``` --- -## Historial de Desarrollo +## Roadmap Completado -### 2025-12-08 - v0.4 (Imagenes + Utilidades) -- **Fase 3 - Imagenes**: - - JPEG embedding con DCT passthrough (sin re-encoding) - - PNG metadata parsing (embedding pendiente) - - image() y imageFit() en Page - - XObject generation en OutputProducer -- **Fase 4 - Utilidades**: - - Table helper completo (header, row, footer, estilos) - - Alineacion por columna en tablas - - Pagination.addPageNumbers() con formato {PAGE}/{PAGES} - - addHeader() y addFooterWithLine() con lineas separadoras - - drawLink() y writeLink() para texto estilo link -- 6 ejemplos funcionales -- ~70 tests pasando +### Fase 1 - Core (COMPLETADO) +- [x] Estructura PDF 1.4 +- [x] Paginas multiples +- [x] Texto basico +- [x] Graficos (lineas, rectangulos) +- [x] Colores + +### Fase 2 - Texto Avanzado (COMPLETADO) +- [x] cell() / cellAdvanced() +- [x] multiCell() con word wrap +- [x] Alineacion +- [x] Bordes + +### Fase 3 - Imagenes (COMPLETADO) +- [x] JPEG embedding +- [x] image() / imageFit() +- [x] PNG metadata (embedding pendiente) + +### Fase 4 - Utilidades (COMPLETADO) +- [x] Table helper +- [x] Paginacion +- [x] Headers/Footers +- [x] Links visuales + +### Fase 5 - Links Clickeables (COMPLETADO) +- [x] URL annotations +- [x] Internal page links +- [x] Link annotations en PDF + +### Futuro (Opcional) +- [ ] PNG embedding completo +- [ ] Compresion de streams +- [ ] Fuentes TTF +- [ ] Bookmarks + +--- + +## Historial + +### 2025-12-08 - v0.5 (Links Clickeables) +- Link annotations clickeables en PDF +- addUrlLink() / addInternalLink() en Page +- urlLink() / writeUrlLink() combinan visual + annotation +- OutputProducer genera /Annots en pages +- links_demo.zig ejemplo con 2 paginas +- 7 ejemplos, ~70 tests + +### 2025-12-08 - v0.4 (Utilidades) +- Table helper +- Pagination (numeros de pagina) +- Headers/Footers automaticos +- Links visuales ### 2025-12-08 - v0.3 (Imagenes) -- Modulo images/ (jpeg.zig, png.zig, image_info.zig) -- addJpegImage() y addJpegImageFromFile() en Pdf -- image_demo.zig ejemplo -- 66 tests pasando +- JPEG embedding +- image() / imageFit() -### 2025-12-08 - v0.2 (Sistema de Texto) -- Refactoring modular completo -- Sistema de texto: cell, multiCell, ln, alineacion, bordes -- Nueva API Pdf -- 52 tests +### 2025-12-08 - v0.2 (Texto) +- cell() / multiCell() +- Word wrap, alineacion ### 2025-12-08 - v0.1 (Core) -- Estructura inicial, Document, Page, Font, Color -- Graficos basicos, serializacion PDF 1.4 +- Estructura inicial --- -## Equipo y Metodologia +## Equipo -### Normas de Trabajo - -**IMPORTANTE**: Todas las normas de trabajo estan en: ``` /mnt/cello2/arno/re/recode/TEAM_STANDARDS/ -``` - -### Control de Versiones - -```bash git remote: git@git.reugenio.com:reugenio/zpdf.git ``` --- -## Referencias - -- **fpdf2 (Python)**: https://github.com/py-pdf/fpdf2 -- **PDF 1.4 Spec**: https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.4.pdf -- **pdf-nano (Zig)**: https://github.com/GregorBudweiser/pdf-nano - ---- - **zpdf - Generador PDF para Zig** -*v0.4 - 2025-12-08* +*v0.5 - Feature Complete - 2025-12-08* diff --git a/build.zig b/build.zig index b72b2e0..751f968 100644 --- a/build.zig +++ b/build.zig @@ -137,4 +137,23 @@ pub fn build(b: *std.Build) void { run_pagination_demo.step.dependOn(b.getInstallStep()); const pagination_demo_step = b.step("pagination_demo", "Run pagination demo example"); pagination_demo_step.dependOn(&run_pagination_demo.step); + + // Example: links_demo + const links_demo_exe = b.addExecutable(.{ + .name = "links_demo", + .root_module = b.createModule(.{ + .root_source_file = b.path("examples/links_demo.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "zpdf", .module = zpdf_mod }, + }, + }), + }); + b.installArtifact(links_demo_exe); + + const run_links_demo = b.addRunArtifact(links_demo_exe); + run_links_demo.step.dependOn(b.getInstallStep()); + const links_demo_step = b.step("links_demo", "Run links demo example"); + links_demo_step.dependOn(&run_links_demo.step); } diff --git a/examples/links_demo.zig b/examples/links_demo.zig new file mode 100644 index 0000000..179484e --- /dev/null +++ b/examples/links_demo.zig @@ -0,0 +1,177 @@ +//! Links Demo - Demonstrates clickable URL links in PDFs +//! +//! Shows how to create: +//! - Clickable URL links (external websites) +//! - Internal page links (jump to page) +//! - Visual link styling (blue underlined text) + +const std = @import("std"); +const pdf = @import("zpdf"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + std.debug.print("zpdf - Links Demo\n", .{}); + + var doc = pdf.Pdf.init(allocator, .{}); + defer doc.deinit(); + + doc.setTitle("Links Demo"); + doc.setAuthor("zpdf"); + + // ========================================================================= + // Page 1: External URL Links + // ========================================================================= + var page1 = try doc.addPage(.{}); + page1.setMargins(50, 50, 50); + + // Title + try page1.setFont(.helvetica_bold, 24); + page1.setFillColor(pdf.Color.rgb(41, 98, 255)); + page1.setXY(50, 780); + try page1.cell(0, 30, "Clickable Links Demo", pdf.Border.none, .center, false); + page1.ln(50); + + // Description + try page1.setFont(.helvetica, 12); + page1.setFillColor(pdf.Color.black); + const desc = + \\This PDF demonstrates clickable links. When you view this in a PDF reader, + \\you can click on the blue underlined text to open URLs in your browser. + ; + try page1.multiCell(500, null, desc, pdf.Border.none, .left, false); + page1.ln(30); + + // Section: External Links + try page1.setFont(.helvetica_bold, 16); + page1.setFillColor(pdf.Color.rgb(51, 51, 51)); + try page1.cell(0, 25, "External URL Links", pdf.Border.none, .left, false); + page1.ln(30); + + try page1.setFont(.helvetica, 12); + page1.setFillColor(pdf.Color.black); + + // Link 1: Example website + try page1.cell(0, 18, "Visit the example website: ", pdf.Border.none, .left, false); + _ = try page1.writeUrlLink("https://example.com", "https://example.com"); + page1.ln(25); + + // Link 2: GitHub + try page1.cell(0, 18, "Check out Zig on GitHub: ", pdf.Border.none, .left, false); + _ = try page1.writeUrlLink("Zig Language", "https://github.com/ziglang/zig"); + page1.ln(25); + + // Link 3: Email + try page1.cell(0, 18, "Send us an email: ", pdf.Border.none, .left, false); + _ = try page1.writeUrlLink("contact@example.com", "mailto:contact@example.com"); + page1.ln(40); + + // Section: Internal Links + try page1.setFont(.helvetica_bold, 16); + page1.setFillColor(pdf.Color.rgb(51, 51, 51)); + try page1.cell(0, 25, "Internal Page Links", pdf.Border.none, .left, false); + page1.ln(30); + + try page1.setFont(.helvetica, 12); + page1.setFillColor(pdf.Color.black); + try page1.cell(0, 18, "Click to jump to: ", pdf.Border.none, .left, false); + + // Internal link to page 2 + const link_text = "Page 2 - More Information"; + const link_width = try page1.drawLink(page1.getX(), page1.getY(), link_text); + try page1.addInternalLink(1, page1.getX(), page1.getY() - 2, link_width, 16); + page1.ln(40); + + // Information box + page1.setFillColor(pdf.Color.rgb(240, 248, 255)); + try page1.fillRect(50, page1.getY() - 80, 495, 80); + + try page1.setFont(.helvetica, 11); + page1.setFillColor(pdf.Color.rgb(51, 51, 51)); + page1.setXY(60, page1.getY() - 15); + const info_text = + \\Note: Link clickability depends on your PDF viewer. Most modern viewers + \\(Adobe Reader, Preview, Chrome, Firefox) support clickable links. + \\The blue underlined styling is visual, while the annotation makes it clickable. + ; + try page1.multiCell(475, null, info_text, pdf.Border.none, .left, false); + + // ========================================================================= + // Page 2: More Information + // ========================================================================= + var page2 = try doc.addPage(.{}); + page2.setMargins(50, 50, 50); + + // Title + try page2.setFont(.helvetica_bold, 24); + page2.setFillColor(pdf.Color.rgb(41, 98, 255)); + page2.setXY(50, 780); + try page2.cell(0, 30, "Page 2 - More Information", pdf.Border.none, .center, false); + page2.ln(50); + + try page2.setFont(.helvetica, 12); + page2.setFillColor(pdf.Color.black); + const page2_text = + \\You navigated here from a clickable internal link! + \\ + \\Internal links allow you to create table of contents, cross-references, + \\and navigation within your PDF documents. + \\ + \\The link annotation stores the target page number and the PDF viewer + \\handles the navigation when clicked. + ; + try page2.multiCell(500, null, page2_text, pdf.Border.none, .left, false); + page2.ln(30); + + // Link back to page 1 + try page2.cell(0, 18, "Go back to: ", pdf.Border.none, .left, false); + const back_text = "Page 1 - Links Demo"; + const back_width = try page2.drawLink(page2.getX(), page2.getY(), back_text); + try page2.addInternalLink(0, page2.getX(), page2.getY() - 2, back_width, 16); + page2.ln(50); + + // Technical details + try page2.setFont(.helvetica_bold, 14); + page2.setFillColor(pdf.Color.rgb(51, 51, 51)); + try page2.cell(0, 20, "How Links Work in PDF", pdf.Border.none, .left, false); + page2.ln(25); + + try page2.setFont(.helvetica, 11); + page2.setFillColor(pdf.Color.black); + const tech_text = + \\PDF links are implemented using Annotation objects (/Type /Annot /Subtype /Link). + \\ + \\Each link annotation contains: + \\- /Rect: The clickable area coordinates [x1, y1, x2, y2] + \\- /A: Action dictionary for URL links (/S /URI /URI "url") + \\- /Dest: Destination for internal links (page reference) + \\- /Border: Border style (we use [0 0 0] for invisible borders) + \\ + \\The visual styling (blue text + underline) is separate from the annotation. + \\zpdf's urlLink() and writeUrlLink() methods combine both automatically. + ; + try page2.multiCell(500, null, tech_text, pdf.Border.none, .left, false); + + // Add page numbers + try pdf.Pagination.addPageNumbers(&doc, .{ + .format = "Page {PAGE} of {PAGES}", + .position = .bottom_center, + }); + + // Footer + try pdf.Pagination.addFooter(&doc, "Generated with zpdf - Links Demo", .{ + .alignment = .center, + .font_size = 8, + .color = pdf.Color.light_gray, + }); + + // Save + const filename = "links_demo.pdf"; + try doc.save(filename); + + std.debug.print("Created: {s}\n", .{filename}); + std.debug.print("Open in a PDF viewer and click the links!\n", .{}); + std.debug.print("Done!\n", .{}); +} diff --git a/src/output/producer.zig b/src/output/producer.zig index c0724b9..fd69c47 100644 --- a/src/output/producer.zig +++ b/src/output/producer.zig @@ -10,6 +10,7 @@ const std = @import("std"); const base = @import("../objects/base.zig"); const Font = @import("../fonts/type1.zig").Font; const ImageInfo = @import("../images/image_info.zig").ImageInfo; +const Link = @import("../links.zig").Link; /// Image reference for serialization pub const ImageData = struct { @@ -24,6 +25,7 @@ pub const PageData = struct { content: []const u8, fonts_used: []const Font, images_used: []const ImageData = &[_]ImageData{}, + links: []const Link = &[_]Link{}, }; /// Generates a complete PDF document. @@ -84,19 +86,27 @@ pub const OutputProducer = struct { } const fonts = fonts_list.items; + // Count total links across all pages + var total_links: usize = 0; + for (pages) |page| { + total_links += page.links.len; + } + // Calculate object IDs: // 1 = Catalog // 2 = Pages (root) // 3 = Info (optional) // 4..4+num_fonts-1 = Font objects // 4+num_fonts..4+num_fonts+num_images-1 = Image XObjects - // 4+num_fonts+num_images.. = Page + Content objects + // next = Link annotation objects + // next = Page + Content objects const catalog_id: u32 = 1; const pages_root_id: u32 = 2; const info_id: u32 = 3; const first_font_id: u32 = 4; const first_image_id: u32 = first_font_id + @as(u32, @intCast(fonts.len)); - const first_page_id: u32 = first_image_id + @as(u32, @intCast(images.len)); + const first_link_id: u32 = first_image_id + @as(u32, @intCast(images.len)); + const first_page_id: u32 = first_link_id + @as(u32, @intCast(total_links)); // Object 1: Catalog try self.beginObject(catalog_id); @@ -194,7 +204,57 @@ pub const OutputProducer = struct { try self.endObject(); } + // Link annotation objects + var current_link_id = first_link_id; + for (pages) |page| { + for (page.links) |link| { + try self.beginObject(current_link_id); + try writer.writeAll("<< /Type /Annot\n"); + try writer.writeAll("/Subtype /Link\n"); + + // Rectangle: [x1, y1, x2, y2] + try writer.print("/Rect [{d:.2} {d:.2} {d:.2} {d:.2}]\n", .{ + link.rect.x, + link.rect.y, + link.rect.x + link.rect.width, + link.rect.y + link.rect.height, + }); + + // Border (none for cleaner look) + try writer.writeAll("/Border [0 0 0]\n"); + + switch (link.link_type) { + .url => { + // External URL action + try writer.writeAll("/A << /Type /Action /S /URI /URI ("); + // Escape special characters in URL + for (link.target.url) |c| { + switch (c) { + '(', ')' => { + try writer.writeByte('\\'); + try writer.writeByte(c); + }, + '\\' => try writer.writeAll("\\\\"), + else => try writer.writeByte(c), + } + } + try writer.writeAll(") >>\n"); + }, + .internal => { + // Internal page link - reference page object ID + const target_page_id = first_page_id + @as(u32, @intCast(link.target.internal * 2)); + try writer.print("/Dest [{d} 0 R /XYZ null null null]\n", .{target_page_id}); + }, + } + + try writer.writeAll(">>\n"); + try self.endObject(); + current_link_id += 1; + } + } + // Page and Content objects + var link_offset: u32 = 0; for (pages, 0..) |page, i| { const page_obj_id = first_page_id + @as(u32, @intCast(i * 2)); const content_obj_id = page_obj_id + 1; @@ -206,6 +266,16 @@ pub const OutputProducer = struct { try writer.print("/MediaBox [0 0 {d:.2} {d:.2}]\n", .{ page.width, page.height }); try writer.print("/Contents {d} 0 R\n", .{content_obj_id}); + // Link annotations + if (page.links.len > 0) { + try writer.writeAll("/Annots ["); + for (0..page.links.len) |link_idx| { + const annot_id = first_link_id + link_offset + @as(u32, @intCast(link_idx)); + try writer.print("{d} 0 R ", .{annot_id}); + } + try writer.writeAll("]\n"); + } + // Resources try writer.writeAll("/Resources <<\n"); try writer.writeAll(" /Font <<\n"); @@ -229,6 +299,9 @@ pub const OutputProducer = struct { try writer.writeAll(">>\n"); try self.endObject(); + // Track link offset for next page + link_offset += @as(u32, @intCast(page.links.len)); + // Content stream try self.beginObject(content_obj_id); try writer.print("<< /Length {d} >>\n", .{page.content.len}); diff --git a/src/page.zig b/src/page.zig index dfe1feb..14ae4de 100644 --- a/src/page.zig +++ b/src/page.zig @@ -12,6 +12,7 @@ const Color = @import("graphics/color.zig").Color; const Font = @import("fonts/type1.zig").Font; const PageSize = @import("objects/base.zig").PageSize; const ImageInfo = @import("images/image_info.zig").ImageInfo; +const Link = @import("links.zig").Link; /// Text alignment options pub const Align = enum { @@ -66,6 +67,9 @@ pub const Page = struct { /// Images used on this page (for resource dictionary) images_used: std.ArrayListUnmanaged(ImageRef), + /// Links on this page (for annotations) + links: std.ArrayListUnmanaged(Link), + const Self = @This(); /// Graphics state for the page @@ -114,6 +118,7 @@ pub const Page = struct { .state = .{}, .fonts_used = std.AutoHashMap(Font, void).init(allocator), .images_used = .{}, + .links = .{}, }; } @@ -122,6 +127,7 @@ pub const Page = struct { self.content.deinit(); self.fonts_used.deinit(); self.images_used.deinit(self.allocator); + self.links.deinit(self.allocator); } // ========================================================================= @@ -701,6 +707,59 @@ pub const Page = struct { self.state.x += width; return width; } + + /// Adds a clickable URL link annotation to the page. + /// The link will be clickable in PDF viewers. + /// + /// Parameters: + /// - url: The target URL (e.g., "https://example.com") + /// - x, y: Position of the link area (bottom-left corner) + /// - width, height: Size of the clickable area + pub fn addUrlLink(self: *Self, url: []const u8, x: f32, y: f32, width: f32, height: f32) !void { + try self.links.append(self.allocator, .{ + .link_type = .url, + .target = .{ .url = url }, + .rect = .{ .x = x, .y = y, .width = width, .height = height }, + }); + } + + /// Adds a clickable internal link (jump to page) annotation. + /// + /// Parameters: + /// - page_num: Target page number (0-based) + /// - x, y: Position of the link area + /// - width, height: Size of the clickable area + pub fn addInternalLink(self: *Self, page_num: usize, x: f32, y: f32, width: f32, height: f32) !void { + try self.links.append(self.allocator, .{ + .link_type = .internal, + .target = .{ .internal = page_num }, + .rect = .{ .x = x, .y = y, .width = width, .height = height }, + }); + } + + /// Draws text as a clickable URL link (visual + annotation). + /// Combines drawLink visual styling with an actual clickable annotation. + /// + /// Returns the width of the link text. + pub fn urlLink(self: *Self, x: f32, y: f32, text: []const u8, url: []const u8) !f32 { + const width = try self.drawLink(x, y, text); + const height = self.state.font_size; + try self.addUrlLink(url, x, y - 2, width, height + 4); + return width; + } + + /// Draws text as a clickable URL link at the current position. + /// Advances the X position after drawing. + pub fn writeUrlLink(self: *Self, text: []const u8, url: []const u8) !f32 { + const width = try self.urlLink(self.state.x, self.state.y, text, url); + self.state.x += width; + return width; + } + + /// Returns the list of links on this page. + pub fn getLinks(self: *const Self) []const Link { + return self.links.items; + } }; // ============================================================================= diff --git a/src/pdf.zig b/src/pdf.zig index 4c8c784..ad69134 100644 --- a/src/pdf.zig +++ b/src/pdf.zig @@ -242,6 +242,7 @@ pub const Pdf = struct { .height = page.height, .content = page.getContent(), .fonts_used = fonts_slice, + .links = page.getLinks(), }); }