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:
parent
236cf7f328
commit
1838594104
6 changed files with 512 additions and 255 deletions
434
CLAUDE.md
434
CLAUDE.md
|
|
@ -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*
|
||||||
|
|
|
||||||
19
build.zig
19
build.zig
|
|
@ -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
177
examples/links_demo.zig
Normal 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", .{});
|
||||||
|
}
|
||||||
|
|
@ -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});
|
||||||
|
|
|
||||||
59
src/page.zig
59
src/page.zig
|
|
@ -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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue