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

View file

@ -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);
}

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 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});

View file

@ -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;
}
};
// =============================================================================

View file

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