feat: v0.5 - Clickable link annotations (Feature Complete)

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 <noreply@anthropic.com>
This commit is contained in:
reugenio 2025-12-08 20:53:33 +01:00
parent 236cf7f328
commit 1838594104
6 changed files with 512 additions and 255 deletions

434
CLAUDE.md
View file

@ -2,7 +2,7 @@
> **Ultima actualizacion**: 2025-12-08 > **Ultima actualizacion**: 2025-12-08
> **Lenguaje**: Zig 0.15.2 > **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 > **Fuente principal**: fpdf2 (Python) - https://github.com/py-pdf/fpdf2
## Descripcion del Proyecto ## Descripcion del Proyecto
@ -13,274 +13,184 @@
- Zero dependencias (100% Zig puro) - Zero dependencias (100% Zig puro)
- API simple y directa inspirada en fpdf2 - API simple y directa inspirada en fpdf2
- Enfocado en generacion de facturas/documentos comerciales - Enfocado en generacion de facturas/documentos comerciales
- Soporte para texto, tablas, imagenes y formas basicas - Soporte completo: texto, tablas, imagenes, links, paginacion
- Calidad open source (doc comments, codigo claro)
**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 | | Categoria | Funcionalidad | Estado |
|------------|--------|---------| |-----------|---------------|--------|
| **Pdf (API Principal)** | | | | **Documento** | | |
| Pdf init/deinit | OK | `src/pdf.zig` | | | Crear PDF 1.4 | OK |
| setTitle, setAuthor, setSubject | OK | `src/pdf.zig` | | | Metadatos (titulo, autor, etc.) | OK |
| addPage (with options) | OK | `src/pdf.zig` | | | Multiples paginas | OK |
| addJpegImage / addJpegImageFromFile | OK | `src/pdf.zig` | | | Tamanos estandar (A4, Letter, A3, A5, Legal) | OK |
| output() / render() | OK | `src/pdf.zig` | | **Texto** | | |
| save() | OK | `src/pdf.zig` | | | drawText() - texto en posicion | OK |
| **Page** | | | | | cell() - celda con bordes/relleno | OK |
| Page init/deinit | OK | `src/page.zig` | | | multiCell() - word wrap automatico | OK |
| setFont / getFont / getFontSize | OK | `src/page.zig` | | | Alineacion (left, center, right) | OK |
| setFillColor / setStrokeColor / setTextColor | OK | `src/page.zig` | | | 14 fuentes Type1 standard | OK |
| setXY / setX / setY / getX / getY | OK | `src/page.zig` | | **Graficos** | | |
| setMargins / setCellMargin | OK | `src/page.zig` | | | Lineas | OK |
| drawText / writeText | OK | `src/page.zig` | | | Rectangulos (stroke, fill) | OK |
| **drawLink / writeLink** | OK | `src/page.zig` | | | Colores RGB/CMYK/Gray | OK |
| cell() / cellAdvanced() | OK | `src/page.zig` | | **Imagenes** | | |
| multiCell() | OK | `src/page.zig` | | | JPEG embedding | OK |
| ln() | OK | `src/page.zig` | | | image() / imageFit() | OK |
| drawLine / drawRect / fillRect | OK | `src/page.zig` | | | Aspect ratio preservation | OK |
| **image() / imageFit()** | OK | `src/page.zig` | | **Tablas** | | |
| **Table Helper** | | | | | Table helper | OK |
| Table.init() | OK | `src/table.zig` | | | header(), row(), footer() | OK |
| Table.header() | OK | `src/table.zig` | | | Alineacion por columna | OK |
| Table.row() / rowStyled() | OK | `src/table.zig` | | | Estilos personalizados | OK |
| Table.footer() | OK | `src/table.zig` | | **Paginacion** | | |
| Table.separator() / space() | OK | `src/table.zig` | | | Numeros de pagina | OK |
| setColumnAlign() / setColumnAligns() | OK | `src/table.zig` | | | Headers automaticos | OK |
| **Pagination** | | | | | Footers automaticos | OK |
| Pagination.addPageNumbers() | OK | `src/pagination.zig` | | | skip_pages option | OK |
| Pagination.addFooter() | OK | `src/pagination.zig` | | **Links** | | |
| addHeader() | OK | `src/pagination.zig` | | | URL links clickeables | OK |
| addFooterWithLine() | OK | `src/pagination.zig` | | | Internal page links | OK |
| **Images** | | | | | Visual styling (azul+subrayado) | OK |
| JPEG embedding (DCT passthrough) | OK | `src/images/jpeg.zig` | | | Link annotations | OK |
| 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` |
### Tests ### Tests y Ejemplos
| Categoria | Tests | Estado | - **~70 tests** unitarios pasando
|-----------|-------|--------| - **7 ejemplos** funcionales:
| root.zig (integration) | 8 | OK | - `hello.zig` - PDF minimo
| page.zig (Page operations) | 18 | OK | - `invoice.zig` - Factura completa
| content_stream.zig | 6 | OK | - `text_demo.zig` - Sistema de texto
| graphics/color.zig | 5 | OK | - `image_demo.zig` - Imagenes JPEG
| fonts/type1.zig | 5 | OK | - `table_demo.zig` - Table helper
| objects/base.zig | 5 | OK | - `pagination_demo.zig` - Paginacion multi-pagina
| output/producer.zig | 5 | OK | - `links_demo.zig` - Links clickeables
| 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 |
--- ---
## Arquitectura Modular ## Arquitectura
``` ```
zpdf/ zpdf/
├── CLAUDE.md # Este archivo - estado del proyecto ├── CLAUDE.md # Documentacion del proyecto
├── build.zig # Sistema de build ├── build.zig # Sistema de build
├── src/ ├── src/
│ ├── root.zig # Exports publicos + Document legacy │ ├── root.zig # Exports publicos
│ ├── pdf.zig # Pdf facade (API principal) │ ├── pdf.zig # Pdf facade (API principal)
│ ├── page.zig # Page + sistema de texto │ ├── page.zig # Page + texto + links
│ ├── content_stream.zig # Content stream (operadores PDF) │ ├── content_stream.zig # Operadores PDF
│ ├── table.zig # Table helper │ ├── table.zig # Table helper
│ ├── pagination.zig # Numeracion de paginas, headers, footers │ ├── pagination.zig # Paginacion, headers, footers
│ ├── links.zig # Links/URLs (estructura) │ ├── links.zig # Link types
│ ├── fonts/ │ ├── fonts/
│ │ ├── mod.zig # Exports de fonts
│ │ └── type1.zig # 14 fuentes Type1 + metricas │ │ └── type1.zig # 14 fuentes Type1 + metricas
│ ├── graphics/ │ ├── graphics/
│ │ ├── mod.zig # Exports de graphics
│ │ └── color.zig # Color (RGB, CMYK, Gray) │ │ └── color.zig # Color (RGB, CMYK, Gray)
│ ├── images/ │ ├── images/
│ │ ├── mod.zig # Exports + detectFormat() │ │ ├── jpeg.zig # JPEG parser
│ │ ├── image_info.zig # ImageInfo struct │ │ ├── png.zig # PNG metadata
│ │ ├── jpeg.zig # JPEG parser (DCT passthrough) │ │ └── image_info.zig # ImageInfo struct
│ │ └── png.zig # PNG metadata parser
│ ├── objects/ │ ├── objects/
│ │ ├── mod.zig # Exports de objects │ │ └── base.zig # PageSize, Orientation
│ │ └── base.zig # PageSize, Orientation, Unit
│ └── output/ │ └── output/
│ ├── mod.zig # Exports de output │ └── producer.zig # Serializa PDF + images + links
│ └── producer.zig # OutputProducer (serializa PDF + imagenes)
└── examples/ └── examples/
├── hello.zig # Ejemplo basico ├── hello.zig
├── invoice.zig # Factura ejemplo ├── invoice.zig
├── text_demo.zig # Demo sistema de texto ├── text_demo.zig
├── image_demo.zig # Demo imagenes ├── image_demo.zig
├── table_demo.zig # Demo tablas ├── table_demo.zig
└── pagination_demo.zig # Demo paginacion ├── pagination_demo.zig
└── links_demo.zig
``` ```
--- ---
## Roadmap ## API Quick Reference
### Fase 1 - Core + Refactoring (COMPLETADO) ### Documento Basico
- [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
```zig ```zig
const zpdf = @import("zpdf"); const zpdf = @import("zpdf");
var doc = zpdf.Pdf.init(allocator, .{ var doc = zpdf.Pdf.init(allocator, .{});
.page_size = .a4,
.orientation = .portrait,
});
defer doc.deinit(); defer doc.deinit();
doc.setTitle("Mi Documento"); doc.setTitle("Mi Documento");
doc.setAuthor("zpdf");
var page = try doc.addPage(.{}); 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 ```zig
// cell() - celda con bordes y alineacion // cell() simple
try page.cell(100, 20, "Hello", zpdf.Border.all, .center, true); 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); try page.multiCell(400, null, texto_largo, zpdf.Border.none, .left, false);
// ln() - salto de linea // Table helper
page.ln(20);
```
### Table Helper
```zig
const widths = [_]f32{ 200, 100, 100 }; const widths = [_]f32{ 200, 100, 100 };
var table = zpdf.Table.init(page, .{ var table = zpdf.Table.init(page, .{
.x = 50,
.y = 700,
.col_widths = &widths, .col_widths = &widths,
.header_bg = zpdf.Color.rgb(41, 98, 255),
}); });
try table.header(&.{ "Producto", "Qty", "Precio" });
table.setColumnAlign(0, .left); try table.row(&.{ "Item A", "10", "$50" });
table.setColumnAlign(1, .center); try table.footer(&.{ "Total", "", "$50" });
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" });
``` ```
### Paginacion y Headers/Footers ### Imagenes
```zig ```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, .{ try zpdf.Pagination.addPageNumbers(&doc, .{
.format = "Page {PAGE} of {PAGES}", .format = "Page {PAGE} of {PAGES}",
.position = .bottom_center, .position = .bottom_center,
}); });
// Header con linea separadora // Header con linea
try zpdf.addHeader(&doc, "Documento Confidencial", .{ try zpdf.addHeader(&doc, "Documento", .{
.alignment = .center,
.draw_line = true, .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 path
ZIG=/mnt/cello2/arno/re/recode/zig/zig-0.15.2/zig-x86_64-linux-0.15.2/zig ZIG=/mnt/cello2/arno/re/recode/zig/zig-0.15.2/zig-x86_64-linux-0.15.2/zig
# Compilar todo # Compilar
$ZIG build $ZIG build
# Tests # Tests
$ZIG build test $ZIG build test
# Ejecutar ejemplos # Ejemplos
./zig-out/bin/hello ./zig-out/bin/hello
./zig-out/bin/invoice ./zig-out/bin/invoice
./zig-out/bin/text_demo ./zig-out/bin/text_demo
./zig-out/bin/image_demo ./zig-out/bin/image_demo
./zig-out/bin/table_demo ./zig-out/bin/table_demo
./zig-out/bin/pagination_demo ./zig-out/bin/pagination_demo
./zig-out/bin/links_demo
``` ```
--- ---
## Historial de Desarrollo ## Roadmap Completado
### 2025-12-08 - v0.4 (Imagenes + Utilidades) ### Fase 1 - Core (COMPLETADO)
- **Fase 3 - Imagenes**: - [x] Estructura PDF 1.4
- JPEG embedding con DCT passthrough (sin re-encoding) - [x] Paginas multiples
- PNG metadata parsing (embedding pendiente) - [x] Texto basico
- image() y imageFit() en Page - [x] Graficos (lineas, rectangulos)
- XObject generation en OutputProducer - [x] Colores
- **Fase 4 - Utilidades**:
- Table helper completo (header, row, footer, estilos) ### Fase 2 - Texto Avanzado (COMPLETADO)
- Alineacion por columna en tablas - [x] cell() / cellAdvanced()
- Pagination.addPageNumbers() con formato {PAGE}/{PAGES} - [x] multiCell() con word wrap
- addHeader() y addFooterWithLine() con lineas separadoras - [x] Alineacion
- drawLink() y writeLink() para texto estilo link - [x] Bordes
- 6 ejemplos funcionales
- ~70 tests pasando ### 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) ### 2025-12-08 - v0.3 (Imagenes)
- Modulo images/ (jpeg.zig, png.zig, image_info.zig) - JPEG embedding
- addJpegImage() y addJpegImageFromFile() en Pdf - image() / imageFit()
- image_demo.zig ejemplo
- 66 tests pasando
### 2025-12-08 - v0.2 (Sistema de Texto) ### 2025-12-08 - v0.2 (Texto)
- Refactoring modular completo - cell() / multiCell()
- Sistema de texto: cell, multiCell, ln, alineacion, bordes - Word wrap, alineacion
- Nueva API Pdf
- 52 tests
### 2025-12-08 - v0.1 (Core) ### 2025-12-08 - v0.1 (Core)
- Estructura inicial, Document, Page, Font, Color - Estructura inicial
- Graficos basicos, serializacion PDF 1.4
--- ---
## Equipo y Metodologia ## Equipo
### Normas de Trabajo
**IMPORTANTE**: Todas las normas de trabajo estan en:
``` ```
/mnt/cello2/arno/re/recode/TEAM_STANDARDS/ /mnt/cello2/arno/re/recode/TEAM_STANDARDS/
```
### Control de Versiones
```bash
git remote: git@git.reugenio.com:reugenio/zpdf.git 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** **zpdf - Generador PDF para Zig**
*v0.4 - 2025-12-08* *v0.5 - Feature Complete - 2025-12-08*

View file

@ -137,4 +137,23 @@ pub fn build(b: *std.Build) void {
run_pagination_demo.step.dependOn(b.getInstallStep()); run_pagination_demo.step.dependOn(b.getInstallStep());
const pagination_demo_step = b.step("pagination_demo", "Run pagination demo example"); const pagination_demo_step = b.step("pagination_demo", "Run pagination demo example");
pagination_demo_step.dependOn(&run_pagination_demo.step); 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);
} }

177
examples/links_demo.zig Normal file
View file

@ -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", .{});
}

View file

@ -10,6 +10,7 @@ const std = @import("std");
const base = @import("../objects/base.zig"); const base = @import("../objects/base.zig");
const Font = @import("../fonts/type1.zig").Font; const Font = @import("../fonts/type1.zig").Font;
const ImageInfo = @import("../images/image_info.zig").ImageInfo; const ImageInfo = @import("../images/image_info.zig").ImageInfo;
const Link = @import("../links.zig").Link;
/// Image reference for serialization /// Image reference for serialization
pub const ImageData = struct { pub const ImageData = struct {
@ -24,6 +25,7 @@ pub const PageData = struct {
content: []const u8, content: []const u8,
fonts_used: []const Font, fonts_used: []const Font,
images_used: []const ImageData = &[_]ImageData{}, images_used: []const ImageData = &[_]ImageData{},
links: []const Link = &[_]Link{},
}; };
/// Generates a complete PDF document. /// Generates a complete PDF document.
@ -84,19 +86,27 @@ pub const OutputProducer = struct {
} }
const fonts = fonts_list.items; 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: // Calculate object IDs:
// 1 = Catalog // 1 = Catalog
// 2 = Pages (root) // 2 = Pages (root)
// 3 = Info (optional) // 3 = Info (optional)
// 4..4+num_fonts-1 = Font objects // 4..4+num_fonts-1 = Font objects
// 4+num_fonts..4+num_fonts+num_images-1 = Image XObjects // 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 catalog_id: u32 = 1;
const pages_root_id: u32 = 2; const pages_root_id: u32 = 2;
const info_id: u32 = 3; const info_id: u32 = 3;
const first_font_id: u32 = 4; const first_font_id: u32 = 4;
const first_image_id: u32 = first_font_id + @as(u32, @intCast(fonts.len)); 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 // Object 1: Catalog
try self.beginObject(catalog_id); try self.beginObject(catalog_id);
@ -194,7 +204,57 @@ pub const OutputProducer = struct {
try self.endObject(); 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 // Page and Content objects
var link_offset: u32 = 0;
for (pages, 0..) |page, i| { for (pages, 0..) |page, i| {
const page_obj_id = first_page_id + @as(u32, @intCast(i * 2)); const page_obj_id = first_page_id + @as(u32, @intCast(i * 2));
const content_obj_id = page_obj_id + 1; 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("/MediaBox [0 0 {d:.2} {d:.2}]\n", .{ page.width, page.height });
try writer.print("/Contents {d} 0 R\n", .{content_obj_id}); 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 // Resources
try writer.writeAll("/Resources <<\n"); try writer.writeAll("/Resources <<\n");
try writer.writeAll(" /Font <<\n"); try writer.writeAll(" /Font <<\n");
@ -229,6 +299,9 @@ pub const OutputProducer = struct {
try writer.writeAll(">>\n"); try writer.writeAll(">>\n");
try self.endObject(); try self.endObject();
// Track link offset for next page
link_offset += @as(u32, @intCast(page.links.len));
// Content stream // Content stream
try self.beginObject(content_obj_id); try self.beginObject(content_obj_id);
try writer.print("<< /Length {d} >>\n", .{page.content.len}); try writer.print("<< /Length {d} >>\n", .{page.content.len});

View file

@ -12,6 +12,7 @@ const Color = @import("graphics/color.zig").Color;
const Font = @import("fonts/type1.zig").Font; const Font = @import("fonts/type1.zig").Font;
const PageSize = @import("objects/base.zig").PageSize; const PageSize = @import("objects/base.zig").PageSize;
const ImageInfo = @import("images/image_info.zig").ImageInfo; const ImageInfo = @import("images/image_info.zig").ImageInfo;
const Link = @import("links.zig").Link;
/// Text alignment options /// Text alignment options
pub const Align = enum { pub const Align = enum {
@ -66,6 +67,9 @@ pub const Page = struct {
/// Images used on this page (for resource dictionary) /// Images used on this page (for resource dictionary)
images_used: std.ArrayListUnmanaged(ImageRef), images_used: std.ArrayListUnmanaged(ImageRef),
/// Links on this page (for annotations)
links: std.ArrayListUnmanaged(Link),
const Self = @This(); const Self = @This();
/// Graphics state for the page /// Graphics state for the page
@ -114,6 +118,7 @@ pub const Page = struct {
.state = .{}, .state = .{},
.fonts_used = std.AutoHashMap(Font, void).init(allocator), .fonts_used = std.AutoHashMap(Font, void).init(allocator),
.images_used = .{}, .images_used = .{},
.links = .{},
}; };
} }
@ -122,6 +127,7 @@ pub const Page = struct {
self.content.deinit(); self.content.deinit();
self.fonts_used.deinit(); self.fonts_used.deinit();
self.images_used.deinit(self.allocator); self.images_used.deinit(self.allocator);
self.links.deinit(self.allocator);
} }
// ========================================================================= // =========================================================================
@ -701,6 +707,59 @@ pub const Page = struct {
self.state.x += width; self.state.x += width;
return 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;
}
}; };
// ============================================================================= // =============================================================================

View file

@ -242,6 +242,7 @@ pub const Pdf = struct {
.height = page.height, .height = page.height,
.content = page.getContent(), .content = page.getContent(),
.fonts_used = fonts_slice, .fonts_used = fonts_slice,
.links = page.getLinks(),
}); });
} }