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:
parent
f9189253d7
commit
236cf7f328
9 changed files with 1376 additions and 218 deletions
376
CLAUDE.md
376
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.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
|
> **Fuente principal**: fpdf2 (Python) - https://github.com/py-pdf/fpdf2
|
||||||
|
|
||||||
## Descripcion del Proyecto
|
## Descripcion del Proyecto
|
||||||
|
|
@ -22,48 +22,53 @@
|
||||||
|
|
||||||
## Estado Actual del Proyecto
|
## Estado Actual del Proyecto
|
||||||
|
|
||||||
### Implementacion v0.2 (Sistema de Texto Completo)
|
### Implementacion v0.4
|
||||||
|
|
||||||
| Componente | Estado | Archivo |
|
| Componente | Estado | Archivo |
|
||||||
|------------|--------|---------|
|
|------------|--------|---------|
|
||||||
| **Pdf (API Nueva)** | | |
|
| **Pdf (API Principal)** | | |
|
||||||
| Pdf init/deinit | OK | `src/pdf.zig` |
|
| Pdf init/deinit | OK | `src/pdf.zig` |
|
||||||
| setTitle, setAuthor, setSubject | OK | `src/pdf.zig` |
|
| setTitle, setAuthor, setSubject | OK | `src/pdf.zig` |
|
||||||
| addPage (with options) | OK | `src/pdf.zig` |
|
| addPage (with options) | OK | `src/pdf.zig` |
|
||||||
|
| addJpegImage / addJpegImageFromFile | OK | `src/pdf.zig` |
|
||||||
| output() / render() | OK | `src/pdf.zig` |
|
| output() / render() | OK | `src/pdf.zig` |
|
||||||
| save() | 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** | | |
|
||||||
| Page init/deinit | OK | `src/page.zig` |
|
| Page init/deinit | OK | `src/page.zig` |
|
||||||
| setFont / getFont / getFontSize | OK | `src/page.zig` |
|
| setFont / getFont / getFontSize | OK | `src/page.zig` |
|
||||||
| setFillColor / setStrokeColor / setTextColor | 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` |
|
| setXY / setX / setY / getX / getY | OK | `src/page.zig` |
|
||||||
| setMargins / setCellMargin | OK | `src/page.zig` |
|
| setMargins / setCellMargin | OK | `src/page.zig` |
|
||||||
| drawText | OK | `src/page.zig` |
|
| drawText / writeText | OK | `src/page.zig` |
|
||||||
| writeText | OK | `src/page.zig` |
|
| **drawLink / writeLink** | OK | `src/page.zig` |
|
||||||
| **cell()** | OK | `src/page.zig` |
|
| cell() / cellAdvanced() | OK | `src/page.zig` |
|
||||||
| **cellAdvanced()** | OK | `src/page.zig` |
|
| multiCell() | OK | `src/page.zig` |
|
||||||
| **multiCell()** | OK | `src/page.zig` |
|
| ln() | OK | `src/page.zig` |
|
||||||
| **ln()** | OK | `src/page.zig` |
|
| drawLine / drawRect / fillRect | OK | `src/page.zig` |
|
||||||
| **getStringWidth()** | OK | `src/page.zig` |
|
| **image() / imageFit()** | OK | `src/page.zig` |
|
||||||
| **getEffectiveWidth()** | OK | `src/page.zig` |
|
| **Table Helper** | | |
|
||||||
| drawLine | OK | `src/page.zig` |
|
| Table.init() | OK | `src/table.zig` |
|
||||||
| drawRect / fillRect / drawFilledRect | OK | `src/page.zig` |
|
| Table.header() | OK | `src/table.zig` |
|
||||||
| rect (with RenderStyle) | OK | `src/page.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** | | |
|
| **Types** | | |
|
||||||
| PageSize enum (A4, Letter, A3, A5, Legal) | OK | `src/objects/base.zig` |
|
| 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` |
|
| Font enum (14 Type1 fonts) | OK | `src/fonts/type1.zig` |
|
||||||
| Color struct (RGB, CMYK, Grayscale) | OK | `src/graphics/color.zig` |
|
| Color struct (RGB, CMYK, Grayscale) | OK | `src/graphics/color.zig` |
|
||||||
| **Align enum (left, center, right)** | OK | `src/page.zig` |
|
| Align enum / Border struct | OK | `src/page.zig` |
|
||||||
| **Border packed struct** | OK | `src/page.zig` |
|
| TableOptions struct | OK | `src/table.zig` |
|
||||||
| **CellPosition enum** | OK | `src/page.zig` |
|
| PageNumberOptions / HeaderOptions / FooterOptions | OK | `src/pagination.zig` |
|
||||||
| ContentStream | OK | `src/content_stream.zig` |
|
|
||||||
| OutputProducer | OK | `src/output/producer.zig` |
|
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|
||||||
|
|
@ -76,7 +81,12 @@
|
||||||
| fonts/type1.zig | 5 | OK |
|
| fonts/type1.zig | 5 | OK |
|
||||||
| objects/base.zig | 5 | OK |
|
| objects/base.zig | 5 | OK |
|
||||||
| output/producer.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
|
### Ejemplos
|
||||||
|
|
||||||
|
|
@ -84,7 +94,10 @@
|
||||||
|---------|-------------|--------|
|
|---------|-------------|--------|
|
||||||
| hello.zig | PDF minimo con texto y formas | OK |
|
| hello.zig | PDF minimo con texto y formas | OK |
|
||||||
| invoice.zig | Factura completa realista | 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
|
├── build.zig # Sistema de build
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── root.zig # Exports publicos + Document legacy
|
│ ├── root.zig # Exports publicos + Document legacy
|
||||||
│ ├── pdf.zig # Pdf facade (API nueva)
|
│ ├── pdf.zig # Pdf facade (API principal)
|
||||||
│ ├── page.zig # Page + sistema de texto
|
│ ├── page.zig # Page + sistema de texto
|
||||||
│ ├── content_stream.zig # Content stream (operadores PDF)
|
│ ├── content_stream.zig # Content stream (operadores PDF)
|
||||||
|
│ ├── table.zig # Table helper
|
||||||
|
│ ├── pagination.zig # Numeracion de paginas, headers, footers
|
||||||
|
│ ├── links.zig # Links/URLs (estructura)
|
||||||
│ ├── fonts/
|
│ ├── fonts/
|
||||||
│ │ ├── mod.zig # Exports de 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
|
│ │ ├── mod.zig # Exports de graphics
|
||||||
│ │ └── color.zig # Color (RGB, CMYK, Gray)
|
│ │ └── 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/
|
│ ├── objects/
|
||||||
│ │ ├── mod.zig # Exports de objects
|
│ │ ├── mod.zig # Exports de objects
|
||||||
│ │ └── base.zig # PageSize, Orientation, Unit
|
│ │ └── base.zig # PageSize, Orientation, Unit
|
||||||
│ └── output/
|
│ └── output/
|
||||||
│ ├── mod.zig # Exports de output
|
│ ├── mod.zig # Exports de output
|
||||||
│ └── producer.zig # OutputProducer (serializa PDF)
|
│ └── producer.zig # OutputProducer (serializa PDF + imagenes)
|
||||||
├── examples/
|
└── examples/
|
||||||
│ ├── hello.zig # Ejemplo basico
|
├── hello.zig # Ejemplo basico
|
||||||
│ ├── invoice.zig # Factura ejemplo
|
├── invoice.zig # Factura ejemplo
|
||||||
│ └── text_demo.zig # Demo sistema de texto
|
├── text_demo.zig # Demo sistema de texto
|
||||||
└── docs/
|
├── image_demo.zig # Demo imagenes
|
||||||
├── PLAN_MAESTRO_ZPDF.md
|
├── table_demo.zig # Demo tablas
|
||||||
├── ARQUITECTURA_FPDF2.md
|
└── pagination_demo.zig # Demo paginacion
|
||||||
└── ARQUITECTURA_ZPDF.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -132,180 +152,135 @@ zpdf/
|
||||||
- [x] Lineas y rectangulos
|
- [x] Lineas y rectangulos
|
||||||
- [x] Colores RGB, CMYK, Grayscale
|
- [x] Colores RGB, CMYK, Grayscale
|
||||||
- [x] Serializacion correcta
|
- [x] Serializacion correcta
|
||||||
- [x] Refactoring modular (separar en archivos)
|
- [x] Refactoring modular
|
||||||
- [x] Arreglar errores Zig 0.15 (ArrayListUnmanaged)
|
|
||||||
|
|
||||||
### Fase 2 - Sistema de Texto (COMPLETADO)
|
### Fase 2 - Sistema de Texto (COMPLETADO)
|
||||||
- [x] cell() - celda con bordes, relleno, alineacion
|
- [x] cell() - celda con bordes, relleno, alineacion
|
||||||
- [x] cellAdvanced() - cell con control de posicion
|
|
||||||
- [x] multiCell() - texto con word wrap automatico
|
- [x] multiCell() - texto con word wrap automatico
|
||||||
- [x] ln() - salto de linea
|
- [x] ln() - salto de linea
|
||||||
- [x] getStringWidth() - ancho de texto
|
|
||||||
- [x] Alineacion (left, center, right)
|
- [x] Alineacion (left, center, right)
|
||||||
- [x] Bordes configurables (Border packed struct)
|
- [x] Bordes configurables
|
||||||
- [x] Margenes de pagina
|
|
||||||
- [x] 18 tests para sistema de texto
|
|
||||||
|
|
||||||
### Fase 3 - Imagenes (PENDIENTE)
|
### Fase 3 - Imagenes (COMPLETADO)
|
||||||
- [ ] JPEG embebido
|
- [x] JPEG embebido (DCT passthrough, sin re-encoding)
|
||||||
- [ ] PNG embebido (con alpha)
|
- [x] PNG metadata parsing (embedding pendiente por zlib API)
|
||||||
- [ ] Escalado y posicionamiento
|
- [x] image() - dibujar imagen en posicion
|
||||||
- [ ] Aspect ratio preservation
|
- [x] imageFit() - escalar manteniendo aspect ratio
|
||||||
|
|
||||||
### Fase 4 - Utilidades (PENDIENTE)
|
### Fase 4 - Utilidades (COMPLETADO)
|
||||||
- [ ] Helper para tablas
|
- [x] Table helper (header, row, footer, estilos, alineacion por columna)
|
||||||
- [ ] Numeracion de paginas
|
- [x] Numeracion de paginas (Pagination.addPageNumbers)
|
||||||
- [ ] Headers/footers automaticos
|
- [x] Headers automaticos (addHeader con linea separadora)
|
||||||
- [ ] Links/URLs
|
- [x] Footers automaticos (addFooter, addFooterWithLine)
|
||||||
|
- [x] Links visuales (drawLink, writeLink - azul + subrayado)
|
||||||
|
|
||||||
### Fase 5 - Avanzado (FUTURO)
|
### Fase 5 - Avanzado (FUTURO)
|
||||||
|
- [ ] Link annotations (clickeables en PDF)
|
||||||
|
- [ ] PNG embedding completo
|
||||||
- [ ] Fuentes TTF embebidas
|
- [ ] Fuentes TTF embebidas
|
||||||
- [ ] Compresion de streams (zlib)
|
- [ ] Compresion de streams (zlib)
|
||||||
- [ ] Bookmarks/outline
|
- [ ] Bookmarks/outline
|
||||||
- [ ] Forms (campos rellenables)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API Actual
|
## API Actual
|
||||||
|
|
||||||
### API Nueva (Pdf)
|
### Crear Documento y Paginas
|
||||||
|
|
||||||
```zig
|
```zig
|
||||||
const zpdf = @import("zpdf");
|
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, .{
|
var doc = zpdf.Pdf.init(allocator, .{
|
||||||
.page_size = .a4,
|
.page_size = .a4,
|
||||||
.orientation = .portrait,
|
.orientation = .portrait,
|
||||||
});
|
});
|
||||||
defer doc.deinit();
|
defer doc.deinit();
|
||||||
|
|
||||||
// Metadatos
|
|
||||||
doc.setTitle("Mi Documento");
|
doc.setTitle("Mi Documento");
|
||||||
doc.setAuthor("zpdf");
|
doc.setAuthor("zpdf");
|
||||||
|
|
||||||
// Agregar pagina
|
|
||||||
var page = try doc.addPage(.{});
|
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");
|
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
|
### Sistema de Texto
|
||||||
|
|
||||||
```zig
|
```zig
|
||||||
// cell(width, height, text, border, align, fill)
|
// cell() - celda con bordes y alineacion
|
||||||
// width=0 extiende hasta margen derecho
|
try page.cell(100, 20, "Hello", zpdf.Border.all, .center, true);
|
||||||
// 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);
|
|
||||||
|
|
||||||
// cellAdvanced - control de posicion despues de la celda
|
// multiCell() - word wrap automatico
|
||||||
try page.cellAdvanced(100, 20, "A", Border.all, .left, false, .right); // mover a la derecha
|
try page.multiCell(400, null, texto_largo, zpdf.Border.none, .left, false);
|
||||||
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
|
// ln() - salto de linea
|
||||||
try page.multiCell(200, 15, "Texto largo que se ajusta automaticamente al ancho especificado.", Border.all, .left, true);
|
page.ln(20);
|
||||||
|
|
||||||
// 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");
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Bordes
|
### Table Helper
|
||||||
|
|
||||||
```zig
|
```zig
|
||||||
const Border = packed struct {
|
const widths = [_]f32{ 200, 100, 100 };
|
||||||
left: bool,
|
var table = zpdf.Table.init(page, .{
|
||||||
top: bool,
|
.x = 50,
|
||||||
right: bool,
|
.y = 700,
|
||||||
bottom: bool,
|
.col_widths = &widths,
|
||||||
};
|
.header_bg = zpdf.Color.rgb(41, 98, 255),
|
||||||
|
});
|
||||||
|
|
||||||
Border.none // sin bordes
|
table.setColumnAlign(0, .left);
|
||||||
Border.all // todos los bordes
|
table.setColumnAlign(1, .center);
|
||||||
Border{ .left = true, .bottom = true } // bordes especificos
|
table.setColumnAlign(2, .right);
|
||||||
Border.fromInt(0b1111) // desde entero (LTRB)
|
|
||||||
|
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
|
```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);
|
// Header con linea separadora
|
||||||
try page.cell(100, 20, "Center", Border.all, .center, false);
|
try zpdf.addHeader(&doc, "Documento Confidencial", .{
|
||||||
try page.cell(100, 20, "Right", Border.all, .right, false);
|
.alignment = .center,
|
||||||
|
.draw_line = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Footer con linea
|
||||||
|
try zpdf.addFooterWithLine(&doc, "Copyright 2025", .{
|
||||||
|
.alignment = .left,
|
||||||
|
.draw_line = true,
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Colores
|
### Imagenes JPEG
|
||||||
|
|
||||||
```zig
|
```zig
|
||||||
// Predefinidos
|
// Cargar imagen desde archivo
|
||||||
zpdf.Color.black
|
const img_idx = try doc.addJpegImageFromFile("foto.jpg");
|
||||||
zpdf.Color.white
|
const info = doc.getImage(img_idx).?;
|
||||||
zpdf.Color.red
|
|
||||||
zpdf.Color.green
|
|
||||||
zpdf.Color.blue
|
|
||||||
zpdf.Color.light_gray
|
|
||||||
zpdf.Color.medium_gray
|
|
||||||
|
|
||||||
// RGB (0-255)
|
// Dibujar en la pagina
|
||||||
zpdf.Color.rgb(41, 98, 255)
|
try page.image(img_idx, info, 50, 500, 200, 150);
|
||||||
|
|
||||||
// Hex
|
// O escalar automaticamente manteniendo aspect ratio
|
||||||
zpdf.Color.hex(0xFF8000)
|
try page.imageFit(img_idx, info, 50, 500, 400, 300);
|
||||||
|
```
|
||||||
|
|
||||||
// CMYK (0.0-1.0)
|
### Links Visuales
|
||||||
zpdf.Color.cmyk(0.0, 1.0, 1.0, 0.0)
|
|
||||||
|
|
||||||
// Grayscale (0.0-1.0)
|
```zig
|
||||||
zpdf.Color.gray(0.5)
|
// 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 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
|
# Compilar todo
|
||||||
$ZIG build
|
$ZIG build
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
$ZIG build test
|
$ZIG build test
|
||||||
|
|
||||||
# Ejecutar ejemplos
|
# Ejecutar ejemplos
|
||||||
$ZIG build && ./zig-out/bin/hello
|
./zig-out/bin/hello
|
||||||
$ZIG build && ./zig-out/bin/invoice
|
./zig-out/bin/invoice
|
||||||
$ZIG build && ./zig-out/bin/text_demo
|
./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 |
|
### 2025-12-08 - v0.3 (Imagenes)
|
||||||
|---------|-----------|
|
- Modulo images/ (jpeg.zig, png.zig, image_info.zig)
|
||||||
| Helvetica | helvetica, helvetica_bold, helvetica_oblique, helvetica_bold_oblique |
|
- addJpegImage() y addJpegImageFromFile() en Pdf
|
||||||
| Times | times_roman, times_bold, times_italic, times_bold_italic |
|
- image_demo.zig ejemplo
|
||||||
| Courier | courier, courier_bold, courier_oblique, courier_bold_oblique |
|
- 66 tests pasando
|
||||||
| Otros | symbol, zapf_dingbats |
|
|
||||||
|
|
||||||
---
|
### 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
|
### 2025-12-08 - v0.1 (Core)
|
||||||
|
- Estructura inicial, Document, Page, Font, Color
|
||||||
| Nombre | Puntos | Milimetros |
|
- Graficos basicos, serializacion PDF 1.4
|
||||||
|--------|--------|------------|
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Equipo y Metodologia
|
## Equipo y Metodologia
|
||||||
|
|
||||||
### Normas de Trabajo Centralizadas
|
### Normas de Trabajo
|
||||||
|
|
||||||
**IMPORTANTE**: Todas las normas de trabajo estan en:
|
**IMPORTANTE**: Todas las normas de trabajo estan en:
|
||||||
```
|
```
|
||||||
|
|
@ -367,50 +355,18 @@ PDF incluye 14 fuentes estandar que no necesitan embeber:
|
||||||
### Control de Versiones
|
### Control de Versiones
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Remote
|
|
||||||
git remote: git@git.reugenio.com:reugenio/zpdf.git
|
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
|
## 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 1.4 Spec**: https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.4.pdf
|
||||||
- **pdf-nano (Zig)**: https://github.com/GregorBudweiser/pdf-nano
|
- **pdf-nano (Zig)**: https://github.com/GregorBudweiser/pdf-nano
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**zpdf - Generador PDF para Zig**
|
**zpdf - Generador PDF para Zig**
|
||||||
*v0.2 - 2025-12-08*
|
*v0.4 - 2025-12-08*
|
||||||
|
|
|
||||||
38
build.zig
38
build.zig
|
|
@ -99,4 +99,42 @@ pub fn build(b: *std.Build) void {
|
||||||
run_image_demo.step.dependOn(b.getInstallStep());
|
run_image_demo.step.dependOn(b.getInstallStep());
|
||||||
const image_demo_step = b.step("image_demo", "Run image demo example");
|
const image_demo_step = b.step("image_demo", "Run image demo example");
|
||||||
image_demo_step.dependOn(&run_image_demo.step);
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
115
examples/pagination_demo.zig
Normal file
115
examples/pagination_demo.zig
Normal 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
157
examples/table_demo.zig
Normal 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
162
src/links.zig
Normal 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);
|
||||||
|
}
|
||||||
45
src/page.zig
45
src/page.zig
|
|
@ -656,6 +656,51 @@ pub const Page = struct {
|
||||||
}
|
}
|
||||||
return fonts.toOwnedSlice(self.allocator) catch &[_]Font{};
|
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
353
src/pagination.zig
Normal 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());
|
||||||
|
}
|
||||||
22
src/root.zig
22
src/root.zig
|
|
@ -70,6 +70,25 @@ pub const images = @import("images/mod.zig");
|
||||||
pub const ImageInfo = images.ImageInfo;
|
pub const ImageInfo = images.ImageInfo;
|
||||||
pub const ImageFormat = images.ImageFormat;
|
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)
|
// Backwards Compatibility - Old API (Document)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -232,4 +251,7 @@ comptime {
|
||||||
_ = @import("images/image_info.zig");
|
_ = @import("images/image_info.zig");
|
||||||
_ = @import("images/jpeg.zig");
|
_ = @import("images/jpeg.zig");
|
||||||
_ = @import("images/png.zig");
|
_ = @import("images/png.zig");
|
||||||
|
_ = @import("table.zig");
|
||||||
|
_ = @import("pagination.zig");
|
||||||
|
_ = @import("links.zig");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
310
src/table.zig
Normal file
310
src/table.zig
Normal 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);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue