feat: v0.4 - Utilities (Table, Pagination, Headers/Footers, Links)

Phase 4 Implementation:
- Table helper (src/table.zig):
  - Table.init() with TableOptions (col_widths, colors, fonts)
  - header(), row(), rowStyled(), footer() methods
  - setColumnAlign() for per-column alignment
  - separator() and space() utilities

- Pagination module (src/pagination.zig):
  - Pagination.addPageNumbers() with {PAGE}/{PAGES} format
  - addHeader() with optional separator line
  - addFooter() and addFooterWithLine()
  - Position enum (bottom_left, bottom_center, etc.)

- Links visual styling (src/links.zig, src/page.zig):
  - PageLinks struct for link storage
  - Page.drawLink() - blue underlined text
  - Page.writeLink() - link at current position

- Examples:
  - table_demo.zig - 3 table styles (product, invoice, employee)
  - pagination_demo.zig - 5 pages with headers/footers/numbers

~70 tests passing, 6 examples working.

🤖 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:45:33 +01:00
parent f9189253d7
commit 236cf7f328
9 changed files with 1376 additions and 218 deletions

376
CLAUDE.md
View file

@ -2,7 +2,7 @@
> **Ultima actualizacion**: 2025-12-08
> **Lenguaje**: Zig 0.15.2
> **Estado**: v0.2 - Sistema de texto completo (cell, multiCell, alignment)
> **Estado**: v0.4 - Imagenes + Utilidades (Table, Pagination, Headers/Footers, Links)
> **Fuente principal**: fpdf2 (Python) - https://github.com/py-pdf/fpdf2
## Descripcion del Proyecto
@ -22,48 +22,53 @@
## Estado Actual del Proyecto
### Implementacion v0.2 (Sistema de Texto Completo)
### Implementacion v0.4
| Componente | Estado | Archivo |
|------------|--------|---------|
| **Pdf (API Nueva)** | | |
| **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` |
| **Document (Legacy)** | | |
| Document init/deinit | OK | `src/root.zig` |
| addPage (standard sizes) | OK | `src/root.zig` |
| render() / saveToFile() | OK | `src/root.zig` |
| **Page** | | |
| Page init/deinit | OK | `src/page.zig` |
| setFont / getFont / getFontSize | OK | `src/page.zig` |
| setFillColor / setStrokeColor / setTextColor | OK | `src/page.zig` |
| setLineWidth | OK | `src/page.zig` |
| setXY / setX / setY / getX / getY | OK | `src/page.zig` |
| setMargins / setCellMargin | OK | `src/page.zig` |
| drawText | OK | `src/page.zig` |
| writeText | OK | `src/page.zig` |
| **cell()** | OK | `src/page.zig` |
| **cellAdvanced()** | OK | `src/page.zig` |
| **multiCell()** | OK | `src/page.zig` |
| **ln()** | OK | `src/page.zig` |
| **getStringWidth()** | OK | `src/page.zig` |
| **getEffectiveWidth()** | OK | `src/page.zig` |
| drawLine | OK | `src/page.zig` |
| drawRect / fillRect / drawFilledRect | OK | `src/page.zig` |
| rect (with RenderStyle) | 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` |
| Orientation enum (portrait, landscape) | 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 (left, center, right)** | OK | `src/page.zig` |
| **Border packed struct** | OK | `src/page.zig` |
| **CellPosition enum** | OK | `src/page.zig` |
| ContentStream | OK | `src/content_stream.zig` |
| OutputProducer | OK | `src/output/producer.zig` |
| Align enum / Border struct | OK | `src/page.zig` |
| TableOptions struct | OK | `src/table.zig` |
| PageNumberOptions / HeaderOptions / FooterOptions | OK | `src/pagination.zig` |
### Tests
@ -76,7 +81,12 @@
| fonts/type1.zig | 5 | OK |
| objects/base.zig | 5 | OK |
| output/producer.zig | 5 | OK |
| **Total** | **52** | 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
@ -84,7 +94,10 @@
|---------|-------------|--------|
| hello.zig | PDF minimo con texto y formas | OK |
| invoice.zig | Factura completa realista | OK |
| **text_demo.zig** | Demo sistema de texto (cells, tables, multiCell) | 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 |
---
@ -96,29 +109,36 @@ zpdf/
├── build.zig # Sistema de build
├── src/
│ ├── root.zig # Exports publicos + Document legacy
│ ├── pdf.zig # Pdf facade (API nueva)
│ ├── pdf.zig # Pdf facade (API principal)
│ ├── page.zig # Page + sistema de texto
│ ├── content_stream.zig # Content stream (operadores PDF)
│ ├── table.zig # Table helper
│ ├── pagination.zig # Numeracion de paginas, headers, footers
│ ├── links.zig # Links/URLs (estructura)
│ ├── 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
│ ├── objects/
│ │ ├── mod.zig # Exports de objects
│ │ └── base.zig # PageSize, Orientation, Unit
│ └── output/
│ ├── mod.zig # Exports de output
│ └── producer.zig # OutputProducer (serializa PDF)
├── examples/
│ ├── hello.zig # Ejemplo basico
│ ├── invoice.zig # Factura ejemplo
│ └── text_demo.zig # Demo sistema de texto
└── docs/
├── PLAN_MAESTRO_ZPDF.md
├── ARQUITECTURA_FPDF2.md
└── ARQUITECTURA_ZPDF.md
│ └── producer.zig # OutputProducer (serializa PDF + imagenes)
└── 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
```
---
@ -132,180 +152,135 @@ zpdf/
- [x] Lineas y rectangulos
- [x] Colores RGB, CMYK, Grayscale
- [x] Serializacion correcta
- [x] Refactoring modular (separar en archivos)
- [x] Arreglar errores Zig 0.15 (ArrayListUnmanaged)
- [x] Refactoring modular
### Fase 2 - Sistema de Texto (COMPLETADO)
- [x] cell() - celda con bordes, relleno, alineacion
- [x] cellAdvanced() - cell con control de posicion
- [x] multiCell() - texto con word wrap automatico
- [x] ln() - salto de linea
- [x] getStringWidth() - ancho de texto
- [x] Alineacion (left, center, right)
- [x] Bordes configurables (Border packed struct)
- [x] Margenes de pagina
- [x] 18 tests para sistema de texto
- [x] Bordes configurables
### Fase 3 - Imagenes (PENDIENTE)
- [ ] JPEG embebido
- [ ] PNG embebido (con alpha)
- [ ] Escalado y posicionamiento
- [ ] Aspect ratio preservation
### 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 (PENDIENTE)
- [ ] Helper para tablas
- [ ] Numeracion de paginas
- [ ] Headers/footers automaticos
- [ ] Links/URLs
### 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
- [ ] Forms (campos rellenables)
---
## API Actual
### API Nueva (Pdf)
### Crear Documento y Paginas
```zig
const zpdf = @import("zpdf");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Crear documento
var doc = zpdf.Pdf.init(allocator, .{
.page_size = .a4,
.orientation = .portrait,
});
defer doc.deinit();
// Metadatos
doc.setTitle("Mi Documento");
doc.setAuthor("zpdf");
// Agregar pagina
var page = try doc.addPage(.{});
// Configurar fuente y posicion
try page.setFont(.helvetica_bold, 24);
page.setXY(50, 800);
page.setMargins(50, 50, 50);
// cell() - celda simple
try page.cell(0, 30, "Titulo", zpdf.Border.none, .center, false);
page.ln(35);
// Tabla con cells
try page.setFont(.helvetica, 12);
page.setFillColor(zpdf.Color.light_gray);
try page.cell(150, 20, "Columna 1", zpdf.Border.all, .center, true);
try page.cell(150, 20, "Columna 2", zpdf.Border.all, .center, true);
page.ln(null);
// multiCell - texto con word wrap
const texto_largo = "Este es un texto largo que se ajustara automaticamente...";
try page.multiCell(400, null, texto_largo, zpdf.Border.all, .left, false);
// Guardar
try doc.save("documento.pdf");
}
```
### API Legacy (Document)
```zig
const pdf = @import("zpdf");
var doc = pdf.Document.init(allocator);
defer doc.deinit();
var page = try doc.addPage(.a4);
try page.setFont(.helvetica_bold, 24);
try page.drawText(50, 750, "Titulo");
try doc.saveToFile("documento.pdf");
```
### Sistema de Texto
```zig
// cell(width, height, text, border, align, fill)
// width=0 extiende hasta margen derecho
// width=null ajusta al ancho del texto
try page.cell(100, 20, "Hello", Border.all, .left, false);
try page.cell(0, 20, "Full width", Border.none, .center, true);
// cell() - celda con bordes y alineacion
try page.cell(100, 20, "Hello", zpdf.Border.all, .center, true);
// cellAdvanced - control de posicion despues de la celda
try page.cellAdvanced(100, 20, "A", Border.all, .left, false, .right); // mover a la derecha
try page.cellAdvanced(100, 20, "B", Border.all, .left, false, .next_line); // nueva linea
try page.cellAdvanced(100, 20, "C", Border.all, .left, false, .below); // debajo (mismo X)
// multiCell() - word wrap automatico
try page.multiCell(400, null, texto_largo, zpdf.Border.none, .left, false);
// multiCell - word wrap automatico
try page.multiCell(200, 15, "Texto largo que se ajusta automaticamente al ancho especificado.", Border.all, .left, true);
// ln(height) - salto de linea
page.ln(20); // salto de 20 puntos
page.ln(null); // salto del tamano de fuente actual
// getStringWidth - ancho del texto
const width = page.getStringWidth("Hello World");
// ln() - salto de linea
page.ln(20);
```
### Bordes
### Table Helper
```zig
const Border = packed struct {
left: bool,
top: bool,
right: bool,
bottom: bool,
};
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),
});
Border.none // sin bordes
Border.all // todos los bordes
Border{ .left = true, .bottom = true } // bordes especificos
Border.fromInt(0b1111) // desde entero (LTRB)
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" });
```
### Alineacion
### Paginacion y Headers/Footers
```zig
const Align = enum { left, center, right };
// Agregar numeros de pagina a todas las paginas
try zpdf.Pagination.addPageNumbers(&doc, .{
.format = "Page {PAGE} of {PAGES}",
.position = .bottom_center,
});
try page.cell(100, 20, "Left", Border.all, .left, false);
try page.cell(100, 20, "Center", Border.all, .center, false);
try page.cell(100, 20, "Right", Border.all, .right, false);
// Header con linea separadora
try zpdf.addHeader(&doc, "Documento Confidencial", .{
.alignment = .center,
.draw_line = true,
});
// Footer con linea
try zpdf.addFooterWithLine(&doc, "Copyright 2025", .{
.alignment = .left,
.draw_line = true,
});
```
### Colores
### Imagenes JPEG
```zig
// Predefinidos
zpdf.Color.black
zpdf.Color.white
zpdf.Color.red
zpdf.Color.green
zpdf.Color.blue
zpdf.Color.light_gray
zpdf.Color.medium_gray
// Cargar imagen desde archivo
const img_idx = try doc.addJpegImageFromFile("foto.jpg");
const info = doc.getImage(img_idx).?;
// RGB (0-255)
zpdf.Color.rgb(41, 98, 255)
// Dibujar en la pagina
try page.image(img_idx, info, 50, 500, 200, 150);
// Hex
zpdf.Color.hex(0xFF8000)
// O escalar automaticamente manteniendo aspect ratio
try page.imageFit(img_idx, info, 50, 500, 400, 300);
```
// CMYK (0.0-1.0)
zpdf.Color.cmyk(0.0, 1.0, 1.0, 0.0)
### Links Visuales
// Grayscale (0.0-1.0)
zpdf.Color.gray(0.5)
```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");
```
---
@ -316,48 +291,61 @@ zpdf.Color.gray(0.5)
# Zig path
ZIG=/mnt/cello2/arno/re/recode/zig/zig-0.15.2/zig-x86_64-linux-0.15.2/zig
# Compilar
# Compilar todo
$ZIG build
# Tests
$ZIG build test
# Ejecutar ejemplos
$ZIG build && ./zig-out/bin/hello
$ZIG build && ./zig-out/bin/invoice
$ZIG build && ./zig-out/bin/text_demo
./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
```
---
## Fuentes Type1 Built-in
## Historial de Desarrollo
PDF incluye 14 fuentes estandar que no necesitan embeber:
### 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
| Familia | Variantes |
|---------|-----------|
| Helvetica | helvetica, helvetica_bold, helvetica_oblique, helvetica_bold_oblique |
| Times | times_roman, times_bold, times_italic, times_bold_italic |
| Courier | courier, courier_bold, courier_oblique, courier_bold_oblique |
| Otros | symbol, zapf_dingbats |
### 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
---
### 2025-12-08 - v0.2 (Sistema de Texto)
- Refactoring modular completo
- Sistema de texto: cell, multiCell, ln, alineacion, bordes
- Nueva API Pdf
- 52 tests
## Tamanos de Pagina
| Nombre | Puntos | Milimetros |
|--------|--------|------------|
| A4 | 595 x 842 | 210 x 297 |
| A3 | 842 x 1191 | 297 x 420 |
| A5 | 420 x 595 | 148 x 210 |
| Letter | 612 x 792 | 216 x 279 |
| Legal | 612 x 1008 | 216 x 356 |
### 2025-12-08 - v0.1 (Core)
- Estructura inicial, Document, Page, Font, Color
- Graficos basicos, serializacion PDF 1.4
---
## Equipo y Metodologia
### Normas de Trabajo Centralizadas
### Normas de Trabajo
**IMPORTANTE**: Todas las normas de trabajo estan en:
```
@ -367,50 +355,18 @@ PDF incluye 14 fuentes estandar que no necesitan embeber:
### Control de Versiones
```bash
# Remote
git remote: git@git.reugenio.com:reugenio/zpdf.git
# Branches
main # Codigo estable
```
---
## Historial de Desarrollo
### 2025-12-08 - v0.2 (Sistema de Texto Completo)
- Refactoring modular completo (fonts/, graphics/, objects/, output/)
- Arreglados errores Zig 0.15 (ArrayListUnmanaged API)
- Sistema de texto completo:
- cell() con bordes, relleno, alineacion
- cellAdvanced() con control de posicion
- multiCell() con word wrap automatico
- ln() para saltos de linea
- getStringWidth() para calcular anchos
- Margenes de pagina configurables
- Nueva API Pdf (mas limpia que Document legacy)
- 52 tests unitarios pasando
- Nuevo ejemplo: text_demo.zig
### 2025-12-08 - v0.1 (Core Funcional)
- Estructura inicial del proyecto
- Document, Page, Color, Font, PageSize types
- Texto con 14 fuentes Type1 standard
- Graficos: lineas, rectangulos (stroke, fill, both)
- Colores RGB
- Serializacion PDF 1.4 correcta
- 6 tests unitarios pasando
- Ejemplos: hello.zig, invoice.zig funcionales
---
## Referencias
- **fpdf2 (Python)**: https://github.com/py-pdf/fpdf2 - Fuente principal
- **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.2 - 2025-12-08*
*v0.4 - 2025-12-08*

View file

@ -99,4 +99,42 @@ pub fn build(b: *std.Build) void {
run_image_demo.step.dependOn(b.getInstallStep());
const image_demo_step = b.step("image_demo", "Run image demo example");
image_demo_step.dependOn(&run_image_demo.step);
// Example: table_demo
const table_demo_exe = b.addExecutable(.{
.name = "table_demo",
.root_module = b.createModule(.{
.root_source_file = b.path("examples/table_demo.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "zpdf", .module = zpdf_mod },
},
}),
});
b.installArtifact(table_demo_exe);
const run_table_demo = b.addRunArtifact(table_demo_exe);
run_table_demo.step.dependOn(b.getInstallStep());
const table_demo_step = b.step("table_demo", "Run table demo example");
table_demo_step.dependOn(&run_table_demo.step);
// Example: pagination_demo
const pagination_demo_exe = b.addExecutable(.{
.name = "pagination_demo",
.root_module = b.createModule(.{
.root_source_file = b.path("examples/pagination_demo.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "zpdf", .module = zpdf_mod },
},
}),
});
b.installArtifact(pagination_demo_exe);
const run_pagination_demo = b.addRunArtifact(pagination_demo_exe);
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);
}

View file

@ -0,0 +1,115 @@
//! Pagination Demo - Demonstrates automatic page numbering and footers
//!
//! Shows how to add page numbers and footers to multi-page documents.
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 - Pagination Demo\n", .{});
var doc = pdf.Pdf.init(allocator, .{});
defer doc.deinit();
doc.setTitle("Pagination Demo");
doc.setAuthor("zpdf");
// Create multiple pages with content
const num_pages: usize = 5;
for (0..num_pages) |i| {
var page = try doc.addPage(.{});
page.setMargins(50, 50, 50);
// Page title
try page.setFont(.helvetica_bold, 24);
page.setFillColor(pdf.Color.rgb(41, 98, 255));
page.setXY(50, 780);
var title_buf: [64]u8 = undefined;
const title = std.fmt.bufPrint(&title_buf, "Chapter {d}", .{i + 1}) catch "Chapter";
try page.cell(0, 30, title, pdf.Border.none, .center, false);
page.ln(50);
// Content
try page.setFont(.helvetica, 12);
page.setFillColor(pdf.Color.black);
const lorem =
\\Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
\\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
\\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
\\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
\\eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt
\\in culpa qui officia deserunt mollit anim id est laborum.
;
// Multiple paragraphs to fill the page
for (0..4) |_| {
try page.multiCell(500, null, lorem, pdf.Border.none, .left, false);
page.ln(15);
}
// Section heading
try page.setFont(.helvetica_bold, 14);
page.setFillColor(pdf.Color.rgb(51, 51, 51));
try page.cell(0, 20, "Key Points", pdf.Border.none, .left, false);
page.ln(25);
// Bullet points
try page.setFont(.helvetica, 11);
page.setFillColor(pdf.Color.black);
const points = [_][]const u8{
"First important point about this chapter",
"Second key consideration to remember",
"Third aspect worth noting",
"Fourth element of significance",
};
for (points) |point| {
var point_buf: [256]u8 = undefined;
const bullet_text = std.fmt.bufPrint(&point_buf, " * {s}", .{point}) catch point;
try page.cell(0, 16, bullet_text, pdf.Border.none, .left, false);
page.ln(18);
}
}
// Add header to all pages (with line separator)
try pdf.addHeader(&doc, "zpdf Library - Multi-Page Document Example", .{
.alignment = .center,
.margin = 30,
.font_size = 9,
.color = pdf.Color.medium_gray,
.draw_line = true,
.line_color = pdf.Color.light_gray,
});
// Add page numbers to all pages
try pdf.Pagination.addPageNumbers(&doc, .{
.format = "Page {PAGE} of {PAGES}",
.position = .bottom_center,
.font = .helvetica,
.font_size = 9,
.color = pdf.Color.medium_gray,
});
// Add footer text to all pages (with line separator)
try pdf.addFooterWithLine(&doc, "Confidential Document - For Internal Use Only", .{
.alignment = .left,
.margin = 30,
.font_size = 8,
.color = pdf.Color.light_gray,
.draw_line = true,
});
// Save
const filename = "pagination_demo.pdf";
try doc.save(filename);
std.debug.print("Created: {s} ({d} pages)\n", .{ filename, num_pages });
std.debug.print("Done!\n", .{});
}

157
examples/table_demo.zig Normal file
View file

@ -0,0 +1,157 @@
//! Table Demo - Demonstrates the Table helper for easy table creation
//!
//! Shows how to create formatted tables with headers, data rows,
//! alternating colors, and custom styling.
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 - Table Demo\n", .{});
var doc = pdf.Pdf.init(allocator, .{});
defer doc.deinit();
doc.setTitle("Table Demo");
doc.setAuthor("zpdf");
var page = try doc.addPage(.{});
page.setMargins(50, 50, 50);
// Title
try page.setFont(.helvetica_bold, 24);
page.setFillColor(pdf.Color.rgb(41, 98, 255));
page.setXY(50, 800);
try page.cell(0, 30, "Table Helper Demo", pdf.Border.none, .center, false);
page.ln(50);
// =========================================================================
// Example 1: Simple Product Table
// =========================================================================
try page.setFont(.helvetica_bold, 14);
page.setFillColor(pdf.Color.black);
try page.cell(0, 20, "Example 1: Product Inventory", pdf.Border.none, .left, false);
page.ln(25);
const col_widths = [_]f32{ 200, 80, 80, 80 };
var table = pdf.Table.init(page, .{
.x = 50,
.y = page.getY(),
.col_widths = &col_widths,
});
// Set column alignments
table.setColumnAlign(0, .left); // Product name
table.setColumnAlign(1, .center); // SKU
table.setColumnAlign(2, .right); // Quantity
table.setColumnAlign(3, .right); // Price
try table.header(&.{ "Product", "SKU", "Qty", "Price" });
try table.row(&.{ "Widget Pro", "WP-001", "150", "$29.99" });
try table.row(&.{ "Gadget Plus", "GP-042", "75", "$49.99" });
try table.row(&.{ "Super Tool", "ST-108", "200", "$19.99" });
try table.row(&.{ "Mega Device", "MD-255", "50", "$99.99" });
try table.row(&.{ "Mini Helper", "MH-033", "300", "$9.99" });
try table.footer(&.{ "Total Items", "", "775", "" });
// =========================================================================
// Example 2: Invoice-style Table
// =========================================================================
page.setXY(50, table.getY() - 40);
try page.setFont(.helvetica_bold, 14);
page.setFillColor(pdf.Color.black);
try page.cell(0, 20, "Example 2: Invoice Items", pdf.Border.none, .left, false);
page.ln(25);
const invoice_widths = [_]f32{ 250, 60, 80, 100 };
var invoice_table = pdf.Table.init(page, .{
.x = 50,
.y = page.getY(),
.col_widths = &invoice_widths,
.header_bg = pdf.Color.rgb(51, 51, 51),
.header_fg = pdf.Color.white,
.odd_row_bg = pdf.Color.rgb(250, 250, 250),
});
invoice_table.setColumnAlign(0, .left);
invoice_table.setColumnAlign(1, .center);
invoice_table.setColumnAlign(2, .right);
invoice_table.setColumnAlign(3, .right);
try invoice_table.header(&.{ "Description", "Qty", "Unit Price", "Total" });
try invoice_table.row(&.{ "Web Development Services", "40", "$75.00", "$3,000.00" });
try invoice_table.row(&.{ "UI/UX Design Package", "1", "$1,500.00", "$1,500.00" });
try invoice_table.row(&.{ "Server Hosting (Annual)", "1", "$480.00", "$480.00" });
try invoice_table.row(&.{ "SSL Certificate", "1", "$50.00", "$50.00" });
try invoice_table.row(&.{ "Technical Support (hours)", "10", "$50.00", "$500.00" });
try invoice_table.separator();
invoice_table.space(5);
// Subtotal, Tax, Total using styled rows
try invoice_table.rowStyled(
&.{ "", "", "Subtotal:", "$5,530.00" },
pdf.Color.white,
pdf.Color.black,
.helvetica,
10,
);
try invoice_table.rowStyled(
&.{ "", "", "Tax (8%):", "$442.40" },
pdf.Color.white,
pdf.Color.black,
.helvetica,
10,
);
try invoice_table.rowStyled(
&.{ "", "", "Total:", "$5,972.40" },
pdf.Color.rgb(41, 98, 255),
pdf.Color.white,
.helvetica_bold,
11,
);
// =========================================================================
// Example 3: Compact Data Table
// =========================================================================
page.setXY(50, invoice_table.getY() - 40);
try page.setFont(.helvetica_bold, 14);
page.setFillColor(pdf.Color.black);
try page.cell(0, 20, "Example 3: Employee Directory", pdf.Border.none, .left, false);
page.ln(25);
const emp_widths = [_]f32{ 120, 150, 100, 120 };
var emp_table = pdf.Table.init(page, .{
.x = 50,
.y = page.getY(),
.col_widths = &emp_widths,
.header_bg = pdf.Color.rgb(76, 175, 80),
.body_font_size = 9,
.header_font_size = 10,
.padding = 3,
});
try emp_table.header(&.{ "Name", "Email", "Department", "Phone" });
try emp_table.row(&.{ "John Smith", "john@company.com", "Engineering", "+1-555-0101" });
try emp_table.row(&.{ "Sarah Johnson", "sarah@company.com", "Marketing", "+1-555-0102" });
try emp_table.row(&.{ "Mike Wilson", "mike@company.com", "Sales", "+1-555-0103" });
try emp_table.row(&.{ "Emily Davis", "emily@company.com", "HR", "+1-555-0104" });
try emp_table.row(&.{ "Chris Brown", "chris@company.com", "Finance", "+1-555-0105" });
// Footer
page.setXY(50, 50);
try page.setFont(.helvetica, 9);
page.setFillColor(pdf.Color.medium_gray);
try page.cell(0, 15, "Generated with zpdf - Table Helper Demo", pdf.Border.none, .center, false);
// Save
const filename = "table_demo.pdf";
try doc.save(filename);
std.debug.print("Created: {s}\n", .{filename});
std.debug.print("Done!\n", .{});
}

162
src/links.zig Normal file
View file

@ -0,0 +1,162 @@
//! Links - URL and internal link support for PDF documents
//!
//! Provides support for clickable links in PDF documents:
//! - External URLs (http, https, mailto, etc.)
//! - Internal page links (jump to page)
//!
//! Links are implemented as PDF Annotations.
const std = @import("std");
/// Link type
pub const LinkType = enum {
/// External URL (opens in browser)
url,
/// Internal link to a page
internal,
};
/// A link annotation
pub const Link = struct {
/// Link type
link_type: LinkType,
/// Target URL or page number
target: union(LinkType) {
url: []const u8,
internal: usize,
},
/// Link rectangle (x, y, width, height in points)
rect: Rect,
pub const Rect = struct {
x: f32,
y: f32,
width: f32,
height: f32,
};
};
/// Link storage for a page
pub const PageLinks = struct {
links: std.ArrayListUnmanaged(Link),
const Self = @This();
pub fn init() Self {
return .{
.links = .{},
};
}
pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
self.links.deinit(allocator);
}
/// Add a URL link
pub fn addUrlLink(
self: *Self,
allocator: std.mem.Allocator,
url: []const u8,
x: f32,
y: f32,
width: f32,
height: f32,
) !void {
try self.links.append(allocator, .{
.link_type = .url,
.target = .{ .url = url },
.rect = .{ .x = x, .y = y, .width = width, .height = height },
});
}
/// Add an internal page link
pub fn addInternalLink(
self: *Self,
allocator: std.mem.Allocator,
page_num: usize,
x: f32,
y: f32,
width: f32,
height: f32,
) !void {
try self.links.append(allocator, .{
.link_type = .internal,
.target = .{ .internal = page_num },
.rect = .{ .x = x, .y = y, .width = width, .height = height },
});
}
/// Get all links
pub fn getLinks(self: *const Self) []const Link {
return self.links.items;
}
};
/// Writes link annotations to PDF format
pub fn writeLinkAnnotation(writer: anytype, link: Link, annot_id: u32) !void {
try writer.print("{d} 0 obj\n", .{annot_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 parentheses in URL
for (link.target.url) |c| {
switch (c) {
'(', ')' => try writer.print("\\{c}", .{c}),
'\\' => try writer.writeAll("\\\\"),
else => try writer.writeByte(c),
}
}
try writer.writeAll(") >>\n");
},
.internal => {
// Internal page link (GoTo action)
try writer.print("/Dest [page{d} /XYZ null null null]\n", .{link.target.internal});
},
}
try writer.writeAll(">>\nendobj\n");
}
// =============================================================================
// Tests
// =============================================================================
test "PageLinks add URL link" {
const allocator = std.testing.allocator;
var links = PageLinks.init();
defer links.deinit(allocator);
try links.addUrlLink(allocator, "https://example.com", 100, 200, 150, 20);
try std.testing.expectEqual(@as(usize, 1), links.links.items.len);
try std.testing.expectEqual(LinkType.url, links.links.items[0].link_type);
}
test "PageLinks add internal link" {
const allocator = std.testing.allocator;
var links = PageLinks.init();
defer links.deinit(allocator);
try links.addInternalLink(allocator, 5, 100, 200, 150, 20);
try std.testing.expectEqual(@as(usize, 1), links.links.items.len);
try std.testing.expectEqual(LinkType.internal, links.links.items[0].link_type);
try std.testing.expectEqual(@as(usize, 5), links.links.items[0].target.internal);
}

View file

@ -656,6 +656,51 @@ pub const Page = struct {
}
return fonts.toOwnedSlice(self.allocator) catch &[_]Font{};
}
// =========================================================================
// Link Drawing (Visual Style)
// =========================================================================
/// Draws text styled as a link (blue and underlined).
/// Note: This is visual styling only. For clickable links in the PDF,
/// link annotations need to be added separately.
///
/// Returns the width of the drawn text for annotation placement.
pub fn drawLink(self: *Self, x: f32, y: f32, text: []const u8) !f32 {
// Save current colors
const saved_fill = self.state.fill_color;
const saved_stroke = self.state.stroke_color;
// Set link color (blue)
const link_color = Color.rgb(0, 102, 204);
self.setFillColor(link_color);
self.setStrokeColor(link_color);
// Draw text
try self.drawText(x, y, text);
// Calculate text width
const text_width = self.getStringWidth(text);
// Draw underline
const underline_y = y - 2; // Slightly below text baseline
try self.setLineWidth(0.5);
try self.drawLine(x, underline_y, x + text_width, underline_y);
// Restore colors
self.setFillColor(saved_fill);
self.setStrokeColor(saved_stroke);
return text_width;
}
/// Draws text styled as a link at the current position.
/// Advances the X position after drawing.
pub fn writeLink(self: *Self, text: []const u8) !f32 {
const width = try self.drawLink(self.state.x, self.state.y, text);
self.state.x += width;
return width;
}
};
// =============================================================================

353
src/pagination.zig Normal file
View file

@ -0,0 +1,353 @@
//! Pagination - Page numbering and footer utilities
//!
//! Provides helpers for adding page numbers and automatic footers
//! to PDF documents.
//!
//! Example:
//! ```zig
//! var doc = pdf.Pdf.init(allocator, .{});
//!
//! // Add pages...
//!
//! // Add page numbers to all pages
//! try pdf.Pagination.addPageNumbers(&doc, .{
//! .format = "Page {PAGE} of {PAGES}",
//! .position = .bottom_center,
//! });
//! ```
const std = @import("std");
const Pdf = @import("pdf.zig").Pdf;
const Page = @import("page.zig").Page;
const Color = @import("graphics/color.zig").Color;
const Font = @import("fonts/type1.zig").Font;
const Align = @import("page.zig").Align;
const Border = @import("page.zig").Border;
/// Position for page numbers
pub const Position = enum {
bottom_left,
bottom_center,
bottom_right,
top_left,
top_center,
top_right,
};
/// Options for page numbering
pub const PageNumberOptions = struct {
/// Format string. Use {PAGE} for current page, {PAGES} for total.
format: []const u8 = "Page {PAGE} of {PAGES}",
/// Position on page
position: Position = .bottom_center,
/// Font to use
font: Font = .helvetica,
/// Font size
font_size: f32 = 9,
/// Text color
color: Color = Color.medium_gray,
/// Margin from edge (in points)
margin: f32 = 30,
/// Starting page number (1-based)
start_page: usize = 1,
/// Skip first N pages (useful for title pages)
skip_pages: usize = 0,
};
/// Pagination utilities
pub const Pagination = struct {
/// Adds page numbers to all pages in the document
pub fn addPageNumbers(doc: *Pdf, options: PageNumberOptions) !void {
const total_pages = doc.pageCount();
if (total_pages == 0) return;
for (0..total_pages) |i| {
// Skip specified pages
if (i < options.skip_pages) continue;
const page = doc.getPage(i) orelse continue;
const page_num = options.start_page + i - options.skip_pages;
try addPageNumberToPage(page, page_num, total_pages - options.skip_pages, options);
}
}
/// Adds a page number to a single page
pub fn addPageNumberToPage(
page: *Page,
current_page: usize,
total_pages: usize,
options: PageNumberOptions,
) !void {
// Format the page number string
var buf: [256]u8 = undefined;
const text = formatPageNumber(&buf, options.format, current_page, total_pages);
// Save current state
const saved_x = page.getX();
const saved_y = page.getY();
// Set font and color
try page.setFont(options.font, options.font_size);
page.setFillColor(options.color);
// Calculate position
const text_width = options.font.stringWidth(text, options.font_size);
const page_width = page.width;
const page_height = page.height;
var x: f32 = undefined;
var y: f32 = undefined;
switch (options.position) {
.bottom_left => {
x = options.margin;
y = options.margin;
},
.bottom_center => {
x = (page_width - text_width) / 2;
y = options.margin;
},
.bottom_right => {
x = page_width - options.margin - text_width;
y = options.margin;
},
.top_left => {
x = options.margin;
y = page_height - options.margin;
},
.top_center => {
x = (page_width - text_width) / 2;
y = page_height - options.margin;
},
.top_right => {
x = page_width - options.margin - text_width;
y = page_height - options.margin;
},
}
// Draw the page number
try page.drawText(x, y, text);
// Restore position
page.setXY(saved_x, saved_y);
}
/// Formats a page number string
fn formatPageNumber(buf: []u8, format: []const u8, current: usize, total: usize) []const u8 {
var result: []u8 = buf[0..0];
var i: usize = 0;
while (i < format.len) {
if (i + 6 <= format.len and std.mem.eql(u8, format[i .. i + 6], "{PAGE}")) {
// Insert current page number
const num_str = std.fmt.bufPrint(buf[result.len..], "{d}", .{current}) catch break;
result = buf[0 .. result.len + num_str.len];
i += 6;
} else if (i + 7 <= format.len and std.mem.eql(u8, format[i .. i + 7], "{PAGES}")) {
// Insert total pages
const num_str = std.fmt.bufPrint(buf[result.len..], "{d}", .{total}) catch break;
result = buf[0 .. result.len + num_str.len];
i += 7;
} else {
// Copy character
if (result.len < buf.len) {
buf[result.len] = format[i];
result = buf[0 .. result.len + 1];
}
i += 1;
}
}
return result;
}
/// Adds a simple footer text to all pages
pub fn addFooter(doc: *Pdf, text: []const u8, options: FooterOptions) !void {
const total_pages = doc.pageCount();
if (total_pages == 0) return;
for (0..total_pages) |i| {
if (i < options.skip_pages) continue;
const page = doc.getPage(i) orelse continue;
try addFooterToPage(page, text, options);
}
}
/// Adds a footer to a single page
pub fn addFooterToPage(page: *Page, text: []const u8, options: FooterOptions) !void {
const saved_x = page.getX();
const saved_y = page.getY();
try page.setFont(options.font, options.font_size);
page.setFillColor(options.color);
const text_width = options.font.stringWidth(text, options.font_size);
const page_width = page.width;
const x = switch (options.alignment) {
.left => options.margin,
.center => (page_width - text_width) / 2,
.right => page_width - options.margin - text_width,
};
try page.drawText(x, options.margin, text);
page.setXY(saved_x, saved_y);
}
};
/// Options for footer text
pub const FooterOptions = struct {
font: Font = .helvetica,
font_size: f32 = 9,
color: Color = Color.medium_gray,
margin: f32 = 30,
alignment: Align = .center,
skip_pages: usize = 0,
/// Draw a separator line above footer
draw_line: bool = false,
line_color: Color = Color.light_gray,
};
/// Options for header text
pub const HeaderOptions = struct {
font: Font = .helvetica,
font_size: f32 = 9,
color: Color = Color.medium_gray,
margin: f32 = 30,
alignment: Align = .center,
skip_pages: usize = 0,
/// Draw a separator line below header
draw_line: bool = false,
line_color: Color = Color.light_gray,
};
// =============================================================================
// Header/Footer Extensions
// =============================================================================
/// Adds a header text to all pages
pub fn addHeader(doc: *Pdf, text: []const u8, options: HeaderOptions) !void {
const total_pages = doc.pageCount();
if (total_pages == 0) return;
for (0..total_pages) |i| {
if (i < options.skip_pages) continue;
const page = doc.getPage(i) orelse continue;
try addHeaderToPage(page, text, options);
}
}
/// Adds a header to a single page
pub fn addHeaderToPage(page: *Page, text: []const u8, options: HeaderOptions) !void {
const saved_x = page.getX();
const saved_y = page.getY();
try page.setFont(options.font, options.font_size);
page.setFillColor(options.color);
const text_width = options.font.stringWidth(text, options.font_size);
const page_width = page.width;
const page_height = page.height;
const x = switch (options.alignment) {
.left => options.margin,
.center => (page_width - text_width) / 2,
.right => page_width - options.margin - text_width,
};
const y = page_height - options.margin;
try page.drawText(x, y, text);
// Draw separator line if requested
if (options.draw_line) {
page.setStrokeColor(options.line_color);
try page.setLineWidth(0.5);
try page.drawLine(options.margin, y - options.font_size - 5, page_width - options.margin, y - options.font_size - 5);
}
page.setXY(saved_x, saved_y);
}
/// Extended footer with line support
pub fn addFooterWithLine(doc: *Pdf, text: []const u8, options: FooterOptions) !void {
const total_pages = doc.pageCount();
if (total_pages == 0) return;
for (0..total_pages) |i| {
if (i < options.skip_pages) continue;
const page = doc.getPage(i) orelse continue;
try addFooterToPageWithLine(page, text, options);
}
}
/// Adds a footer to a single page with optional line
fn addFooterToPageWithLine(page: *Page, text: []const u8, options: FooterOptions) !void {
const saved_x = page.getX();
const saved_y = page.getY();
try page.setFont(options.font, options.font_size);
page.setFillColor(options.color);
const text_width = options.font.stringWidth(text, options.font_size);
const page_width = page.width;
const x = switch (options.alignment) {
.left => options.margin,
.center => (page_width - text_width) / 2,
.right => page_width - options.margin - text_width,
};
// Draw separator line if requested
if (options.draw_line) {
page.setStrokeColor(options.line_color);
try page.setLineWidth(0.5);
try page.drawLine(options.margin, options.margin + options.font_size + 5, page_width - options.margin, options.margin + options.font_size + 5);
}
try page.drawText(x, options.margin, text);
page.setXY(saved_x, saved_y);
}
// =============================================================================
// Tests
// =============================================================================
test "formatPageNumber" {
var buf: [256]u8 = undefined;
const result1 = Pagination.formatPageNumber(&buf, "Page {PAGE} of {PAGES}", 3, 10);
try std.testing.expectEqualStrings("Page 3 of 10", result1);
const result2 = Pagination.formatPageNumber(&buf, "{PAGE}/{PAGES}", 1, 5);
try std.testing.expectEqualStrings("1/5", result2);
const result3 = Pagination.formatPageNumber(&buf, "- {PAGE} -", 7, 20);
try std.testing.expectEqualStrings("- 7 -", result3);
}
test "Pagination addPageNumbers" {
const allocator = std.testing.allocator;
var doc = Pdf.init(allocator, .{});
defer doc.deinit();
// Add multiple pages
_ = try doc.addPage(.{});
_ = try doc.addPage(.{});
_ = try doc.addPage(.{});
// Add page numbers
try Pagination.addPageNumbers(&doc, .{
.format = "Page {PAGE}",
.position = .bottom_center,
});
try std.testing.expectEqual(@as(usize, 3), doc.pageCount());
}

View file

@ -70,6 +70,25 @@ pub const images = @import("images/mod.zig");
pub const ImageInfo = images.ImageInfo;
pub const ImageFormat = images.ImageFormat;
/// Table helper
pub const table = @import("table.zig");
pub const Table = table.Table;
pub const TableOptions = table.TableOptions;
/// Pagination (page numbers, headers, footers)
pub const pagination = @import("pagination.zig");
pub const Pagination = pagination.Pagination;
pub const PageNumberOptions = pagination.PageNumberOptions;
pub const FooterOptions = pagination.FooterOptions;
pub const HeaderOptions = pagination.HeaderOptions;
pub const addHeader = pagination.addHeader;
pub const addFooterWithLine = pagination.addFooterWithLine;
/// Links (URL annotations)
pub const links = @import("links.zig");
pub const Link = links.Link;
pub const PageLinks = links.PageLinks;
// =============================================================================
// Backwards Compatibility - Old API (Document)
// =============================================================================
@ -232,4 +251,7 @@ comptime {
_ = @import("images/image_info.zig");
_ = @import("images/jpeg.zig");
_ = @import("images/png.zig");
_ = @import("table.zig");
_ = @import("pagination.zig");
_ = @import("links.zig");
}

310
src/table.zig Normal file
View file

@ -0,0 +1,310 @@
//! Table Helper - Simplified table creation for PDF documents
//!
//! Provides a high-level API for creating tables with:
//! - Configurable column widths
//! - Header rows with different styling
//! - Alternating row colors
//! - Borders and padding
//!
//! Example:
//! ```zig
//! var table = Table.init(&page, .{
//! .x = 50,
//! .y = 700,
//! .col_widths = &.{ 200, 100, 100 },
//! });
//!
//! try table.header(&.{ "Product", "Qty", "Price" });
//! try table.row(&.{ "Widget A", "10", "$5.00" });
//! try table.row(&.{ "Widget B", "5", "$12.50" });
//! ```
const std = @import("std");
const Page = @import("page.zig").Page;
const Border = @import("page.zig").Border;
const Align = @import("page.zig").Align;
const Color = @import("graphics/color.zig").Color;
const Font = @import("fonts/type1.zig").Font;
/// Table configuration options
pub const TableOptions = struct {
/// X position of table (left edge)
x: f32 = 50,
/// Y position of table (top edge)
y: f32 = 750,
/// Column widths in points
col_widths: []const f32,
/// Row height (null = auto from font size)
row_height: ?f32 = null,
/// Cell padding
padding: f32 = 4,
/// Border style
border: Border = Border.all,
/// Header background color
header_bg: Color = Color.rgb(41, 98, 255),
/// Header text color
header_fg: Color = Color.white,
/// Header font
header_font: Font = .helvetica_bold,
/// Header font size
header_font_size: f32 = 11,
/// Body font
body_font: Font = .helvetica,
/// Body font size
body_font_size: f32 = 10,
/// Body text color
body_fg: Color = Color.black,
/// Even row background color
even_row_bg: Color = Color.white,
/// Odd row background color
odd_row_bg: Color = Color.rgb(245, 245, 245),
/// Default text alignment
default_align: Align = .left,
};
/// Column alignment override
pub const ColumnAlign = struct {
index: usize,
alignment: Align,
};
/// Table builder for creating formatted tables
pub const Table = struct {
page: *Page,
options: TableOptions,
current_y: f32,
row_count: usize,
col_aligns: [16]Align, // Max 16 columns with custom alignment
col_align_count: usize,
const Self = @This();
/// Initialize a new table
pub fn init(page: *Page, options: TableOptions) Self {
var col_aligns: [16]Align = undefined;
for (&col_aligns) |*a| {
a.* = options.default_align;
}
return .{
.page = page,
.options = options,
.current_y = options.y,
.row_count = 0,
.col_aligns = col_aligns,
.col_align_count = 0,
};
}
/// Set alignment for a specific column
pub fn setColumnAlign(self: *Self, col: usize, alignment: Align) void {
if (col < 16) {
self.col_aligns[col] = alignment;
}
}
/// Set alignments for multiple columns
pub fn setColumnAligns(self: *Self, aligns: []const ColumnAlign) void {
for (aligns) |ca| {
self.setColumnAlign(ca.index, ca.alignment);
}
}
/// Get the total width of the table
pub fn totalWidth(self: *const Self) f32 {
var total: f32 = 0;
for (self.options.col_widths) |w| {
total += w;
}
return total;
}
/// Get the current Y position (bottom of last row)
pub fn getY(self: *const Self) f32 {
return self.current_y;
}
/// Draw a header row with special styling
pub fn header(self: *Self, cells: []const []const u8) !void {
const row_h = self.options.row_height orelse (self.options.header_font_size + self.options.padding * 2);
try self.page.setFont(self.options.header_font, self.options.header_font_size);
self.page.setFillColor(self.options.header_bg);
self.page.setTextColor(self.options.header_fg);
var x = self.options.x;
self.page.setXY(x, self.current_y);
for (cells, 0..) |cell_text, i| {
if (i >= self.options.col_widths.len) break;
const col_w = self.options.col_widths[i];
const cell_align = if (i < 16) self.col_aligns[i] else self.options.default_align;
try self.page.cell(col_w, row_h, cell_text, self.options.border, cell_align, true);
x += col_w;
}
self.current_y -= row_h;
self.page.setXY(self.options.x, self.current_y);
// Reset text color for body rows
self.page.setTextColor(self.options.body_fg);
}
/// Draw a data row
pub fn row(self: *Self, cells: []const []const u8) !void {
const row_h = self.options.row_height orelse (self.options.body_font_size + self.options.padding * 2);
try self.page.setFont(self.options.body_font, self.options.body_font_size);
self.page.setTextColor(self.options.body_fg);
// Alternating row colors
const bg_color = if (self.row_count % 2 == 0)
self.options.even_row_bg
else
self.options.odd_row_bg;
self.page.setFillColor(bg_color);
var x = self.options.x;
self.page.setXY(x, self.current_y);
for (cells, 0..) |cell_text, i| {
if (i >= self.options.col_widths.len) break;
const col_w = self.options.col_widths[i];
const cell_align = if (i < 16) self.col_aligns[i] else self.options.default_align;
try self.page.cell(col_w, row_h, cell_text, self.options.border, cell_align, true);
x += col_w;
}
self.current_y -= row_h;
self.row_count += 1;
self.page.setXY(self.options.x, self.current_y);
}
/// Draw a row with custom styling
pub fn rowStyled(
self: *Self,
cells: []const []const u8,
bg_color: Color,
fg_color: Color,
font: Font,
font_size: f32,
) !void {
const row_h = self.options.row_height orelse (font_size + self.options.padding * 2);
try self.page.setFont(font, font_size);
self.page.setFillColor(bg_color);
self.page.setTextColor(fg_color);
var x = self.options.x;
self.page.setXY(x, self.current_y);
for (cells, 0..) |cell_text, i| {
if (i >= self.options.col_widths.len) break;
const col_w = self.options.col_widths[i];
const cell_align = if (i < 16) self.col_aligns[i] else self.options.default_align;
try self.page.cell(col_w, row_h, cell_text, self.options.border, cell_align, true);
x += col_w;
}
self.current_y -= row_h;
self.row_count += 1;
self.page.setXY(self.options.x, self.current_y);
// Reset colors
self.page.setTextColor(self.options.body_fg);
}
/// Draw a footer/total row with special styling
pub fn footer(self: *Self, cells: []const []const u8) !void {
try self.rowStyled(
cells,
Color.rgb(230, 230, 230), // Light gray background
Color.black,
.helvetica_bold,
self.options.body_font_size,
);
}
/// Draw a separator line
pub fn separator(self: *Self) !void {
const total_w = self.totalWidth();
self.page.setStrokeColor(Color.medium_gray);
try self.page.drawLine(
self.options.x,
self.current_y,
self.options.x + total_w,
self.current_y,
);
}
/// Skip vertical space
pub fn space(self: *Self, height: f32) void {
self.current_y -= height;
self.page.setXY(self.options.x, self.current_y);
}
};
// =============================================================================
// Tests
// =============================================================================
test "Table init" {
const allocator = std.testing.allocator;
var page = Page.init(allocator, .a4);
defer page.deinit();
const widths = [_]f32{ 100, 150, 100 };
var table = Table.init(&page, .{
.col_widths = &widths,
});
try std.testing.expectApproxEqAbs(@as(f32, 350), table.totalWidth(), 0.01);
}
test "Table column alignment" {
const allocator = std.testing.allocator;
var page = Page.init(allocator, .a4);
defer page.deinit();
const widths = [_]f32{ 100, 100, 100 };
var table = Table.init(&page, .{
.col_widths = &widths,
});
table.setColumnAlign(0, .left);
table.setColumnAlign(1, .center);
table.setColumnAlign(2, .right);
try std.testing.expectEqual(Align.left, table.col_aligns[0]);
try std.testing.expectEqual(Align.center, table.col_aligns[1]);
try std.testing.expectEqual(Align.right, table.col_aligns[2]);
}
test "Table header and row" {
const allocator = std.testing.allocator;
var page = Page.init(allocator, .a4);
defer page.deinit();
try page.setFont(.helvetica, 10);
const widths = [_]f32{ 150, 100, 100 };
var table = Table.init(&page, .{
.col_widths = &widths,
.y = 700,
});
try table.header(&.{ "Product", "Qty", "Price" });
try table.row(&.{ "Widget A", "10", "$5.00" });
try table.row(&.{ "Widget B", "5", "$12.50" });
// Y should have moved down
try std.testing.expect(table.getY() < 700);
try std.testing.expectEqual(@as(usize, 2), table.row_count);
}