Initial commit: zpdf - PDF generation library for Zig
- Pure Zig implementation, zero dependencies - PDF 1.4 format output - Standard Type1 fonts (Helvetica, Times, Courier) - Text rendering with colors - Graphics primitives (lines, rectangles) - Hello world and invoice examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
0e17e8790b
6 changed files with 1032 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
zig-cache/
|
||||||
|
zig-out/
|
||||||
|
.zig-cache/
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
*.pdf
|
||||||
186
CLAUDE.md
Normal file
186
CLAUDE.md
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
# zpdf - Generador PDF para Zig
|
||||||
|
|
||||||
|
> **Fecha creación**: 2025-12-08
|
||||||
|
> **Versión Zig**: 0.15.2
|
||||||
|
> **Estado**: En desarrollo inicial
|
||||||
|
|
||||||
|
## Descripción del Proyecto
|
||||||
|
|
||||||
|
Librería pura Zig para generación de documentos PDF. Sin dependencias externas, compila a un binario único.
|
||||||
|
|
||||||
|
**Filosofía**:
|
||||||
|
- Zero dependencias (100% Zig)
|
||||||
|
- API simple y directa
|
||||||
|
- Enfocado en generación de facturas/documentos comerciales
|
||||||
|
- Soporte para texto, tablas, imágenes y formas básicas
|
||||||
|
|
||||||
|
## Arquitectura
|
||||||
|
|
||||||
|
```
|
||||||
|
zpdf/
|
||||||
|
├── CLAUDE.md # Este archivo
|
||||||
|
├── build.zig # Sistema de build
|
||||||
|
├── src/
|
||||||
|
│ ├── root.zig # Exports públicos
|
||||||
|
│ ├── document.zig # Documento PDF principal
|
||||||
|
│ ├── page.zig # Páginas
|
||||||
|
│ ├── stream.zig # Content streams
|
||||||
|
│ ├── text.zig # Renderizado de texto
|
||||||
|
│ ├── graphics.zig # Líneas, rectángulos, etc.
|
||||||
|
│ ├── image.zig # Imágenes embebidas
|
||||||
|
│ ├── fonts.zig # Fuentes Type1 básicas
|
||||||
|
│ └── writer.zig # Serialización PDF
|
||||||
|
└── examples/
|
||||||
|
├── hello.zig # PDF mínimo
|
||||||
|
└── invoice.zig # Factura ejemplo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Formato PDF
|
||||||
|
|
||||||
|
Usamos PDF 1.4 (compatible con todos los lectores). Estructura básica:
|
||||||
|
|
||||||
|
```
|
||||||
|
%PDF-1.4
|
||||||
|
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||||
|
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
|
||||||
|
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >> endobj
|
||||||
|
4 0 obj << /Length ... >> stream ... endstream endobj
|
||||||
|
xref
|
||||||
|
0 5
|
||||||
|
0000000000 65535 f
|
||||||
|
...
|
||||||
|
trailer << /Size 5 /Root 1 0 R >>
|
||||||
|
startxref
|
||||||
|
...
|
||||||
|
%%EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Objetivo
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const std = @import("std");
|
||||||
|
const pdf = @import("zpdf");
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var allocator = std.heap.page_allocator;
|
||||||
|
|
||||||
|
var doc = pdf.Document.init(allocator);
|
||||||
|
defer doc.deinit();
|
||||||
|
|
||||||
|
var page = try doc.addPage(.a4);
|
||||||
|
|
||||||
|
// Texto
|
||||||
|
try page.setFont(.helvetica_bold, 24);
|
||||||
|
try page.drawText(50, 750, "Factura #001");
|
||||||
|
|
||||||
|
try page.setFont(.helvetica, 12);
|
||||||
|
try page.drawText(50, 700, "Cliente: Empresa S.L.");
|
||||||
|
|
||||||
|
// Línea
|
||||||
|
try page.setLineWidth(0.5);
|
||||||
|
try page.drawLine(50, 690, 550, 690);
|
||||||
|
|
||||||
|
// Rectángulo
|
||||||
|
try page.setFillColor(.{ .r = 240, .g = 240, .b = 240 });
|
||||||
|
try page.fillRect(50, 600, 500, 20);
|
||||||
|
|
||||||
|
// Tabla (helper)
|
||||||
|
try page.drawTable(.{
|
||||||
|
.x = 50,
|
||||||
|
.y = 580,
|
||||||
|
.columns = &.{ 200, 100, 100, 100 },
|
||||||
|
.headers = &.{ "Descripción", "Cantidad", "Precio", "Total" },
|
||||||
|
.rows = &.{
|
||||||
|
&.{ "Producto A", "2", "10.00", "20.00" },
|
||||||
|
&.{ "Producto B", "1", "25.00", "25.00" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Guardar
|
||||||
|
try doc.save("factura.pdf");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Funcionalidades Planificadas
|
||||||
|
|
||||||
|
### Fase 1 - Core (Actual)
|
||||||
|
- [ ] Estructura documento PDF 1.4
|
||||||
|
- [ ] Páginas (A4, Letter, custom)
|
||||||
|
- [ ] Texto básico (fuentes Type1 built-in)
|
||||||
|
- [ ] Líneas y rectángulos
|
||||||
|
- [ ] Serialización correcta
|
||||||
|
|
||||||
|
### Fase 2 - Texto Avanzado
|
||||||
|
- [ ] Múltiples fuentes en mismo documento
|
||||||
|
- [ ] Colores (RGB, CMYK, grayscale)
|
||||||
|
- [ ] Alineación (izquierda, centro, derecha)
|
||||||
|
- [ ] Word wrap automático
|
||||||
|
|
||||||
|
### Fase 3 - Imágenes
|
||||||
|
- [ ] JPEG embebido
|
||||||
|
- [ ] PNG embebido (con alpha)
|
||||||
|
- [ ] Escalado y posicionamiento
|
||||||
|
|
||||||
|
### Fase 4 - Utilidades
|
||||||
|
- [ ] Helper para tablas
|
||||||
|
- [ ] Numeración de páginas
|
||||||
|
- [ ] Headers/footers
|
||||||
|
|
||||||
|
## Fuentes Type1 Built-in
|
||||||
|
|
||||||
|
PDF incluye 14 fuentes estándar que no necesitan embeber:
|
||||||
|
- Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique
|
||||||
|
- Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic
|
||||||
|
- Courier, Courier-Bold, Courier-Oblique, Courier-BoldOblique
|
||||||
|
- Symbol, ZapfDingbats
|
||||||
|
|
||||||
|
## Tamaños de Página
|
||||||
|
|
||||||
|
```zig
|
||||||
|
pub const PageSize = enum {
|
||||||
|
a4, // 595 x 842 points (210 x 297 mm)
|
||||||
|
letter, // 612 x 792 points (8.5 x 11 inches)
|
||||||
|
legal, // 612 x 1008 points
|
||||||
|
a3, // 842 x 1191 points
|
||||||
|
a5, // 420 x 595 points
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Referencias
|
||||||
|
|
||||||
|
- [PDF Reference 1.4](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.4.pdf)
|
||||||
|
- [pdf-nano (Zig)](https://github.com/GregorBudweiser/pdf-nano) - Referencia minimalista
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Equipo y Metodología
|
||||||
|
|
||||||
|
### Normas de Trabajo
|
||||||
|
|
||||||
|
**IMPORTANTE**: Todas las normas de trabajo están en:
|
||||||
|
```
|
||||||
|
/mnt/cello2/arno/re/recode/TEAM_STANDARDS/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Control de Versiones
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remote
|
||||||
|
git remote: git@git.reugenio.com:reugenio/zpdf.git
|
||||||
|
|
||||||
|
# Branches
|
||||||
|
main # Código estable
|
||||||
|
develop # Desarrollo activo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zig Path
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ZIG=/mnt/cello2/arno/re/recode/zig/zig-0.15.2/zig-x86_64-linux-0.15.2/zig
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas de Desarrollo
|
||||||
|
|
||||||
|
*Se irán añadiendo conforme avance el proyecto*
|
||||||
64
build.zig
Normal file
64
build.zig
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
// zpdf module
|
||||||
|
const zpdf_mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/root.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
const unit_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/root.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const run_unit_tests = b.addRunArtifact(unit_tests);
|
||||||
|
const test_step = b.step("test", "Run unit tests");
|
||||||
|
test_step.dependOn(&run_unit_tests.step);
|
||||||
|
|
||||||
|
// Example: hello
|
||||||
|
const hello_exe = b.addExecutable(.{
|
||||||
|
.name = "hello",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("examples/hello.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "zpdf", .module = zpdf_mod },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
b.installArtifact(hello_exe);
|
||||||
|
|
||||||
|
const run_hello = b.addRunArtifact(hello_exe);
|
||||||
|
run_hello.step.dependOn(b.getInstallStep());
|
||||||
|
const hello_step = b.step("hello", "Run hello example");
|
||||||
|
hello_step.dependOn(&run_hello.step);
|
||||||
|
|
||||||
|
// Example: invoice
|
||||||
|
const invoice_exe = b.addExecutable(.{
|
||||||
|
.name = "invoice",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("examples/invoice.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "zpdf", .module = zpdf_mod },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
b.installArtifact(invoice_exe);
|
||||||
|
|
||||||
|
const run_invoice = b.addRunArtifact(invoice_exe);
|
||||||
|
run_invoice.step.dependOn(b.getInstallStep());
|
||||||
|
const invoice_step = b.step("invoice", "Run invoice example");
|
||||||
|
invoice_step.dependOn(&run_invoice.step);
|
||||||
|
}
|
||||||
74
examples/hello.zig
Normal file
74
examples/hello.zig
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
//! Minimal PDF example - Hello World
|
||||||
|
|
||||||
|
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 - Hello World example\n", .{});
|
||||||
|
|
||||||
|
// Create document
|
||||||
|
var doc = pdf.Document.init(allocator);
|
||||||
|
defer doc.deinit();
|
||||||
|
|
||||||
|
// Add a page
|
||||||
|
var page = try doc.addPage(.a4);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
try page.setFont(.helvetica_bold, 36);
|
||||||
|
page.setFillColor(pdf.Color{ .r = 0, .g = 100, .b = 200 });
|
||||||
|
try page.drawText(50, 750, "Hello, PDF!");
|
||||||
|
|
||||||
|
// Subtitle
|
||||||
|
try page.setFont(.helvetica, 14);
|
||||||
|
page.setFillColor(pdf.Color.gray);
|
||||||
|
try page.drawText(50, 710, "Generated with zpdf - Pure Zig PDF library");
|
||||||
|
|
||||||
|
// Draw a line
|
||||||
|
try page.setLineWidth(1);
|
||||||
|
page.setStrokeColor(pdf.Color.light_gray);
|
||||||
|
try page.drawLine(50, 700, 545, 700);
|
||||||
|
|
||||||
|
// Body text
|
||||||
|
try page.setFont(.times_roman, 12);
|
||||||
|
page.setFillColor(pdf.Color.black);
|
||||||
|
try page.drawText(50, 670, "This PDF was generated entirely in Zig with zero external dependencies.");
|
||||||
|
try page.drawText(50, 655, "The zpdf library supports:");
|
||||||
|
try page.drawText(70, 635, "- Multiple fonts (Helvetica, Times, Courier)");
|
||||||
|
try page.drawText(70, 620, "- Colors (RGB)");
|
||||||
|
try page.drawText(70, 605, "- Lines and rectangles");
|
||||||
|
try page.drawText(70, 590, "- Multiple pages");
|
||||||
|
|
||||||
|
// Draw some shapes
|
||||||
|
try page.setLineWidth(2);
|
||||||
|
page.setStrokeColor(pdf.Color.blue);
|
||||||
|
page.setFillColor(pdf.Color{ .r = 230, .g = 240, .b = 255 });
|
||||||
|
try page.drawFilledRect(50, 500, 200, 60);
|
||||||
|
|
||||||
|
try page.setFont(.helvetica_bold, 14);
|
||||||
|
page.setFillColor(pdf.Color.blue);
|
||||||
|
try page.drawText(60, 540, "Shapes work too!");
|
||||||
|
|
||||||
|
// Red rectangle
|
||||||
|
page.setStrokeColor(pdf.Color.red);
|
||||||
|
page.setFillColor(pdf.Color{ .r = 255, .g = 230, .b = 230 });
|
||||||
|
try page.drawFilledRect(280, 500, 200, 60);
|
||||||
|
|
||||||
|
page.setFillColor(pdf.Color.red);
|
||||||
|
try page.drawText(290, 540, "Multiple colors!");
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
try page.setFont(.courier, 10);
|
||||||
|
page.setFillColor(pdf.Color.gray);
|
||||||
|
try page.drawText(50, 50, "zpdf v0.1.0 - https://git.reugenio.com/reugenio/zpdf");
|
||||||
|
|
||||||
|
// Save
|
||||||
|
const filename = "hello.pdf";
|
||||||
|
try doc.saveToFile(filename);
|
||||||
|
|
||||||
|
std.debug.print("Created: {s}\n", .{filename});
|
||||||
|
std.debug.print("Done!\n", .{});
|
||||||
|
}
|
||||||
179
examples/invoice.zig
Normal file
179
examples/invoice.zig
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
//! Invoice PDF example - Demonstrates a realistic use case
|
||||||
|
|
||||||
|
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 - Invoice example\n", .{});
|
||||||
|
|
||||||
|
var doc = pdf.Document.init(allocator);
|
||||||
|
defer doc.deinit();
|
||||||
|
|
||||||
|
var page = try doc.addPage(.a4);
|
||||||
|
|
||||||
|
// Company header
|
||||||
|
try page.setFont(.helvetica_bold, 24);
|
||||||
|
page.setFillColor(pdf.Color{ .r = 41, .g = 98, .b = 255 }); // Blue
|
||||||
|
try page.drawText(50, 780, "ACME Corporation");
|
||||||
|
|
||||||
|
try page.setFont(.helvetica, 10);
|
||||||
|
page.setFillColor(pdf.Color.gray);
|
||||||
|
try page.drawText(50, 765, "123 Business Street, City 12345");
|
||||||
|
try page.drawText(50, 753, "Tel: +34 123 456 789 | Email: info@acme.com");
|
||||||
|
try page.drawText(50, 741, "CIF: B12345678");
|
||||||
|
|
||||||
|
// Invoice title
|
||||||
|
try page.setFont(.helvetica_bold, 28);
|
||||||
|
page.setFillColor(pdf.Color.black);
|
||||||
|
try page.drawText(400, 780, "FACTURA");
|
||||||
|
|
||||||
|
try page.setFont(.helvetica, 12);
|
||||||
|
try page.drawText(400, 760, "No: FAC-2025-0001");
|
||||||
|
try page.drawText(400, 745, "Fecha: 08/12/2025");
|
||||||
|
|
||||||
|
// Separator line
|
||||||
|
try page.setLineWidth(1);
|
||||||
|
page.setStrokeColor(pdf.Color.light_gray);
|
||||||
|
try page.drawLine(50, 720, 545, 720);
|
||||||
|
|
||||||
|
// Client info box
|
||||||
|
page.setFillColor(pdf.Color{ .r = 245, .g = 245, .b = 245 });
|
||||||
|
try page.fillRect(50, 640, 250, 70);
|
||||||
|
|
||||||
|
try page.setFont(.helvetica_bold, 11);
|
||||||
|
page.setFillColor(pdf.Color.black);
|
||||||
|
try page.drawText(60, 695, "CLIENTE:");
|
||||||
|
|
||||||
|
try page.setFont(.helvetica, 11);
|
||||||
|
try page.drawText(60, 680, "Empresa Cliente S.L.");
|
||||||
|
try page.drawText(60, 667, "Calle Principal 45, 2o B");
|
||||||
|
try page.drawText(60, 654, "08001 Barcelona");
|
||||||
|
try page.drawText(60, 641, "NIF: B87654321");
|
||||||
|
|
||||||
|
// Table header
|
||||||
|
const table_top: f32 = 610;
|
||||||
|
const col1: f32 = 50;
|
||||||
|
const col2: f32 = 300;
|
||||||
|
const col3: f32 = 370;
|
||||||
|
const col4: f32 = 430;
|
||||||
|
const col5: f32 = 490;
|
||||||
|
const row_height: f32 = 25;
|
||||||
|
|
||||||
|
// Header background
|
||||||
|
page.setFillColor(pdf.Color{ .r = 41, .g = 98, .b = 255 });
|
||||||
|
try page.fillRect(col1, table_top - row_height, 495, row_height);
|
||||||
|
|
||||||
|
// Header text
|
||||||
|
try page.setFont(.helvetica_bold, 10);
|
||||||
|
page.setFillColor(pdf.Color.white);
|
||||||
|
try page.drawText(col1 + 5, table_top - 18, "DESCRIPCION");
|
||||||
|
try page.drawText(col2 + 5, table_top - 18, "CANT.");
|
||||||
|
try page.drawText(col3 + 5, table_top - 18, "PRECIO");
|
||||||
|
try page.drawText(col4 + 5, table_top - 18, "IVA");
|
||||||
|
try page.drawText(col5 + 5, table_top - 18, "TOTAL");
|
||||||
|
|
||||||
|
// Table rows
|
||||||
|
const items = [_]struct { desc: []const u8, qty: []const u8, price: []const u8, vat: []const u8, total: []const u8 }{
|
||||||
|
.{ .desc = "Servicio de consultoria", .qty = "10h", .price = "50.00", .vat = "21%", .total = "605.00" },
|
||||||
|
.{ .desc = "Desarrollo web", .qty = "1", .price = "1500.00", .vat = "21%", .total = "1815.00" },
|
||||||
|
.{ .desc = "Mantenimiento mensual", .qty = "1", .price = "200.00", .vat = "21%", .total = "242.00" },
|
||||||
|
.{ .desc = "Hosting anual", .qty = "1", .price = "120.00", .vat = "21%", .total = "145.20" },
|
||||||
|
};
|
||||||
|
|
||||||
|
try page.setFont(.helvetica, 10);
|
||||||
|
page.setFillColor(pdf.Color.black);
|
||||||
|
|
||||||
|
var y = table_top - row_height;
|
||||||
|
for (items, 0..) |item, i| {
|
||||||
|
y -= row_height;
|
||||||
|
|
||||||
|
// Alternate row background
|
||||||
|
if (i % 2 == 0) {
|
||||||
|
page.setFillColor(pdf.Color{ .r = 250, .g = 250, .b = 250 });
|
||||||
|
try page.fillRect(col1, y, 495, row_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row content
|
||||||
|
page.setFillColor(pdf.Color.black);
|
||||||
|
try page.setFont(.helvetica, 10);
|
||||||
|
try page.drawText(col1 + 5, y + 8, item.desc);
|
||||||
|
try page.drawText(col2 + 5, y + 8, item.qty);
|
||||||
|
try page.drawText(col3 + 5, y + 8, item.price);
|
||||||
|
try page.drawText(col4 + 5, y + 8, item.vat);
|
||||||
|
try page.drawText(col5 + 5, y + 8, item.total);
|
||||||
|
|
||||||
|
// Row border
|
||||||
|
page.setStrokeColor(pdf.Color.light_gray);
|
||||||
|
try page.drawLine(col1, y, col1 + 495, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table border
|
||||||
|
try page.setLineWidth(0.5);
|
||||||
|
page.setStrokeColor(pdf.Color.gray);
|
||||||
|
try page.drawRect(col1, y, 495, table_top - y - row_height);
|
||||||
|
|
||||||
|
// Vertical lines
|
||||||
|
try page.drawLine(col2, y, col2, table_top - row_height);
|
||||||
|
try page.drawLine(col3, y, col3, table_top - row_height);
|
||||||
|
try page.drawLine(col4, y, col4, table_top - row_height);
|
||||||
|
try page.drawLine(col5, y, col5, table_top - row_height);
|
||||||
|
|
||||||
|
// Totals section
|
||||||
|
const totals_y = y - 40;
|
||||||
|
|
||||||
|
try page.setFont(.helvetica, 11);
|
||||||
|
page.setFillColor(pdf.Color.black);
|
||||||
|
try page.drawText(380, totals_y, "Subtotal:");
|
||||||
|
try page.drawText(480, totals_y, "2,320.00");
|
||||||
|
|
||||||
|
try page.drawText(380, totals_y - 18, "IVA (21%):");
|
||||||
|
try page.drawText(480, totals_y - 18, "487.20");
|
||||||
|
|
||||||
|
// Total line
|
||||||
|
try page.setLineWidth(1);
|
||||||
|
page.setStrokeColor(pdf.Color.black);
|
||||||
|
try page.drawLine(380, totals_y - 30, 545, totals_y - 30);
|
||||||
|
|
||||||
|
try page.setFont(.helvetica_bold, 14);
|
||||||
|
try page.drawText(380, totals_y - 48, "TOTAL:");
|
||||||
|
try page.drawText(475, totals_y - 48, "2,807.20 EUR");
|
||||||
|
|
||||||
|
// Payment info
|
||||||
|
const payment_y = totals_y - 100;
|
||||||
|
|
||||||
|
page.setFillColor(pdf.Color{ .r = 245, .g = 245, .b = 245 });
|
||||||
|
try page.fillRect(50, payment_y - 50, 300, 70);
|
||||||
|
|
||||||
|
try page.setFont(.helvetica_bold, 10);
|
||||||
|
page.setFillColor(pdf.Color.black);
|
||||||
|
try page.drawText(60, payment_y + 5, "FORMA DE PAGO:");
|
||||||
|
|
||||||
|
try page.setFont(.helvetica, 10);
|
||||||
|
try page.drawText(60, payment_y - 10, "Transferencia bancaria");
|
||||||
|
try page.drawText(60, payment_y - 25, "IBAN: ES12 1234 5678 9012 3456 7890");
|
||||||
|
try page.drawText(60, payment_y - 40, "Vencimiento: 30 dias");
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
try page.setLineWidth(0.5);
|
||||||
|
page.setStrokeColor(pdf.Color.light_gray);
|
||||||
|
try page.drawLine(50, 80, 545, 80);
|
||||||
|
|
||||||
|
try page.setFont(.helvetica, 8);
|
||||||
|
page.setFillColor(pdf.Color.gray);
|
||||||
|
try page.drawText(50, 65, "Esta factura ha sido generada electronicamente y es valida sin firma.");
|
||||||
|
try page.drawText(50, 55, "ACME Corporation - Inscrita en el Registro Mercantil de Madrid, Tomo 12345, Folio 67, Hoja M-123456");
|
||||||
|
|
||||||
|
// Page number
|
||||||
|
try page.drawText(500, 55, "Pagina 1/1");
|
||||||
|
|
||||||
|
// Save
|
||||||
|
const filename = "invoice.pdf";
|
||||||
|
try doc.saveToFile(filename);
|
||||||
|
|
||||||
|
std.debug.print("Created: {s}\n", .{filename});
|
||||||
|
std.debug.print("Done!\n", .{});
|
||||||
|
}
|
||||||
522
src/root.zig
Normal file
522
src/root.zig
Normal file
|
|
@ -0,0 +1,522 @@
|
||||||
|
//! zpdf - PDF generation library for Zig
|
||||||
|
//!
|
||||||
|
//! A pure Zig library for creating PDF documents with zero dependencies.
|
||||||
|
//! Focused on generating invoices and business documents.
|
||||||
|
//!
|
||||||
|
//! ## Quick Start
|
||||||
|
//!
|
||||||
|
//! ```zig
|
||||||
|
//! const pdf = @import("zpdf");
|
||||||
|
//!
|
||||||
|
//! pub fn main() !void {
|
||||||
|
//! 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, "Hello, PDF!");
|
||||||
|
//!
|
||||||
|
//! try doc.saveToFile("hello.pdf");
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Public Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Standard page sizes in PDF points (1 point = 1/72 inch)
|
||||||
|
pub const PageSize = enum {
|
||||||
|
a4, // 595 x 842 points (210 x 297 mm)
|
||||||
|
a3, // 842 x 1191 points (297 x 420 mm)
|
||||||
|
a5, // 420 x 595 points (148 x 210 mm)
|
||||||
|
letter, // 612 x 792 points (8.5 x 11 inches)
|
||||||
|
legal, // 612 x 1008 points (8.5 x 14 inches)
|
||||||
|
|
||||||
|
pub fn dimensions(self: PageSize) struct { width: f32, height: f32 } {
|
||||||
|
return switch (self) {
|
||||||
|
.a4 => .{ .width = 595, .height = 842 },
|
||||||
|
.a3 => .{ .width = 842, .height = 1191 },
|
||||||
|
.a5 => .{ .width = 420, .height = 595 },
|
||||||
|
.letter => .{ .width = 612, .height = 792 },
|
||||||
|
.legal => .{ .width = 612, .height = 1008 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// PDF standard Type1 fonts (built into all PDF readers)
|
||||||
|
pub const Font = enum {
|
||||||
|
helvetica,
|
||||||
|
helvetica_bold,
|
||||||
|
helvetica_oblique,
|
||||||
|
helvetica_bold_oblique,
|
||||||
|
times_roman,
|
||||||
|
times_bold,
|
||||||
|
times_italic,
|
||||||
|
times_bold_italic,
|
||||||
|
courier,
|
||||||
|
courier_bold,
|
||||||
|
courier_oblique,
|
||||||
|
courier_bold_oblique,
|
||||||
|
symbol,
|
||||||
|
zapf_dingbats,
|
||||||
|
|
||||||
|
pub fn pdfName(self: Font) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.helvetica => "Helvetica",
|
||||||
|
.helvetica_bold => "Helvetica-Bold",
|
||||||
|
.helvetica_oblique => "Helvetica-Oblique",
|
||||||
|
.helvetica_bold_oblique => "Helvetica-BoldOblique",
|
||||||
|
.times_roman => "Times-Roman",
|
||||||
|
.times_bold => "Times-Bold",
|
||||||
|
.times_italic => "Times-Italic",
|
||||||
|
.times_bold_italic => "Times-BoldItalic",
|
||||||
|
.courier => "Courier",
|
||||||
|
.courier_bold => "Courier-Bold",
|
||||||
|
.courier_oblique => "Courier-Oblique",
|
||||||
|
.courier_bold_oblique => "Courier-BoldOblique",
|
||||||
|
.symbol => "Symbol",
|
||||||
|
.zapf_dingbats => "ZapfDingbats",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// RGB color (0-255 per channel)
|
||||||
|
pub const Color = struct {
|
||||||
|
r: u8 = 0,
|
||||||
|
g: u8 = 0,
|
||||||
|
b: u8 = 0,
|
||||||
|
|
||||||
|
pub const black = Color{ .r = 0, .g = 0, .b = 0 };
|
||||||
|
pub const white = Color{ .r = 255, .g = 255, .b = 255 };
|
||||||
|
pub const red = Color{ .r = 255, .g = 0, .b = 0 };
|
||||||
|
pub const green = Color{ .r = 0, .g = 255, .b = 0 };
|
||||||
|
pub const blue = Color{ .r = 0, .g = 0, .b = 255 };
|
||||||
|
pub const gray = Color{ .r = 128, .g = 128, .b = 128 };
|
||||||
|
pub const light_gray = Color{ .r = 200, .g = 200, .b = 200 };
|
||||||
|
|
||||||
|
/// Convert to PDF color values (0.0 - 1.0)
|
||||||
|
pub fn toFloats(self: Color) struct { r: f32, g: f32, b: f32 } {
|
||||||
|
return .{
|
||||||
|
.r = @as(f32, @floatFromInt(self.r)) / 255.0,
|
||||||
|
.g = @as(f32, @floatFromInt(self.g)) / 255.0,
|
||||||
|
.b = @as(f32, @floatFromInt(self.b)) / 255.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Page
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// A single page in a PDF document
|
||||||
|
pub const Page = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
content: std.ArrayListUnmanaged(u8),
|
||||||
|
|
||||||
|
// Current graphics state
|
||||||
|
current_font: Font = .helvetica,
|
||||||
|
current_font_size: f32 = 12,
|
||||||
|
stroke_color: Color = Color.black,
|
||||||
|
fill_color: Color = Color.black,
|
||||||
|
line_width: f32 = 1.0,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
fn init(allocator: std.mem.Allocator, size: PageSize) Self {
|
||||||
|
const dims = size.dimensions();
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.width = dims.width,
|
||||||
|
.height = dims.height,
|
||||||
|
.content = .{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initCustom(allocator: std.mem.Allocator, width: f32, height: f32) Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
.content = .{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *Self) void {
|
||||||
|
self.content.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Text Operations
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/// Sets the current font and size
|
||||||
|
pub fn setFont(self: *Self, font: Font, size: f32) !void {
|
||||||
|
self.current_font = font;
|
||||||
|
self.current_font_size = size;
|
||||||
|
// Font selection is done when drawing text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws text at the specified position (x, y from bottom-left)
|
||||||
|
pub fn drawText(self: *Self, x: f32, y: f32, text: []const u8) !void {
|
||||||
|
const writer = self.content.writer(self.allocator);
|
||||||
|
|
||||||
|
// Begin text object
|
||||||
|
try writer.writeAll("BT\n");
|
||||||
|
|
||||||
|
// Set font
|
||||||
|
try writer.print("/{s} {d} Tf\n", .{ self.current_font.pdfName(), self.current_font_size });
|
||||||
|
|
||||||
|
// Set text color
|
||||||
|
const c = self.fill_color.toFloats();
|
||||||
|
try writer.print("{d:.3} {d:.3} {d:.3} rg\n", .{ c.r, c.g, c.b });
|
||||||
|
|
||||||
|
// Position and draw
|
||||||
|
try writer.print("{d:.2} {d:.2} Td\n", .{ x, y });
|
||||||
|
|
||||||
|
// Escape special PDF characters in text
|
||||||
|
try writer.writeByte('(');
|
||||||
|
for (text) |char| {
|
||||||
|
switch (char) {
|
||||||
|
'(', ')', '\\' => {
|
||||||
|
try writer.writeByte('\\');
|
||||||
|
try writer.writeByte(char);
|
||||||
|
},
|
||||||
|
else => try writer.writeByte(char),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try writer.writeAll(") Tj\n");
|
||||||
|
|
||||||
|
// End text object
|
||||||
|
try writer.writeAll("ET\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text/fill color
|
||||||
|
pub fn setFillColor(self: *Self, color: Color) void {
|
||||||
|
self.fill_color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the stroke color for lines and shapes
|
||||||
|
pub fn setStrokeColor(self: *Self, color: Color) void {
|
||||||
|
self.stroke_color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Graphics Operations
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/// Sets the line width for stroke operations
|
||||||
|
pub fn setLineWidth(self: *Self, width: f32) !void {
|
||||||
|
self.line_width = width;
|
||||||
|
const writer = self.content.writer(self.allocator);
|
||||||
|
try writer.print("{d:.2} w\n", .{width});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a line from (x1, y1) to (x2, y2)
|
||||||
|
pub fn drawLine(self: *Self, x1: f32, y1: f32, x2: f32, y2: f32) !void {
|
||||||
|
const writer = self.content.writer(self.allocator);
|
||||||
|
|
||||||
|
// Set stroke color
|
||||||
|
const c = self.stroke_color.toFloats();
|
||||||
|
try writer.print("{d:.3} {d:.3} {d:.3} RG\n", .{ c.r, c.g, c.b });
|
||||||
|
|
||||||
|
// Draw line
|
||||||
|
try writer.print("{d:.2} {d:.2} m\n", .{ x1, y1 });
|
||||||
|
try writer.print("{d:.2} {d:.2} l\n", .{ x2, y2 });
|
||||||
|
try writer.writeAll("S\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a rectangle outline
|
||||||
|
pub fn drawRect(self: *Self, x: f32, y: f32, width: f32, height: f32) !void {
|
||||||
|
const writer = self.content.writer(self.allocator);
|
||||||
|
|
||||||
|
// Set stroke color
|
||||||
|
const c = self.stroke_color.toFloats();
|
||||||
|
try writer.print("{d:.3} {d:.3} {d:.3} RG\n", .{ c.r, c.g, c.b });
|
||||||
|
|
||||||
|
// Draw rectangle
|
||||||
|
try writer.print("{d:.2} {d:.2} {d:.2} {d:.2} re\n", .{ x, y, width, height });
|
||||||
|
try writer.writeAll("S\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fills a rectangle
|
||||||
|
pub fn fillRect(self: *Self, x: f32, y: f32, width: f32, height: f32) !void {
|
||||||
|
const writer = self.content.writer(self.allocator);
|
||||||
|
|
||||||
|
// Set fill color
|
||||||
|
const c = self.fill_color.toFloats();
|
||||||
|
try writer.print("{d:.3} {d:.3} {d:.3} rg\n", .{ c.r, c.g, c.b });
|
||||||
|
|
||||||
|
// Fill rectangle
|
||||||
|
try writer.print("{d:.2} {d:.2} {d:.2} {d:.2} re\n", .{ x, y, width, height });
|
||||||
|
try writer.writeAll("f\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a filled rectangle with stroke
|
||||||
|
pub fn drawFilledRect(self: *Self, x: f32, y: f32, width: f32, height: f32) !void {
|
||||||
|
const writer = self.content.writer(self.allocator);
|
||||||
|
|
||||||
|
// Set colors
|
||||||
|
const fc = self.fill_color.toFloats();
|
||||||
|
const sc = self.stroke_color.toFloats();
|
||||||
|
try writer.print("{d:.3} {d:.3} {d:.3} rg\n", .{ fc.r, fc.g, fc.b });
|
||||||
|
try writer.print("{d:.3} {d:.3} {d:.3} RG\n", .{ sc.r, sc.g, sc.b });
|
||||||
|
|
||||||
|
// Draw and fill rectangle
|
||||||
|
try writer.print("{d:.2} {d:.2} {d:.2} {d:.2} re\n", .{ x, y, width, height });
|
||||||
|
try writer.writeAll("B\n"); // Fill and stroke
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Document
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// A PDF document
|
||||||
|
pub const Document = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
pages: std.ArrayListUnmanaged(Page),
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Creates a new empty PDF document
|
||||||
|
pub fn init(allocator: std.mem.Allocator) Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.pages = .{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frees all resources
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
for (self.pages.items) |*page| {
|
||||||
|
page.deinit();
|
||||||
|
}
|
||||||
|
self.pages.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new page with a standard size
|
||||||
|
pub fn addPage(self: *Self, size: PageSize) !*Page {
|
||||||
|
const page = Page.init(self.allocator, size);
|
||||||
|
try self.pages.append(self.allocator, page);
|
||||||
|
return &self.pages.items[self.pages.items.len - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new page with custom dimensions (in points)
|
||||||
|
pub fn addPageCustom(self: *Self, width: f32, height: f32) !*Page {
|
||||||
|
const page = Page.initCustom(self.allocator, width, height);
|
||||||
|
try self.pages.append(self.allocator, page);
|
||||||
|
return &self.pages.items[self.pages.items.len - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the document to a byte buffer
|
||||||
|
pub fn render(self: *Self, allocator: std.mem.Allocator) ![]u8 {
|
||||||
|
var output: std.ArrayListUnmanaged(u8) = .{};
|
||||||
|
errdefer output.deinit(allocator);
|
||||||
|
|
||||||
|
const writer = output.writer(allocator);
|
||||||
|
|
||||||
|
// Track object positions for xref
|
||||||
|
var obj_positions: std.ArrayListUnmanaged(usize) = .{};
|
||||||
|
defer obj_positions.deinit(allocator);
|
||||||
|
|
||||||
|
// PDF Header
|
||||||
|
try writer.writeAll("%PDF-1.4\n");
|
||||||
|
try writer.writeAll("%\xE2\xE3\xCF\xD3\n"); // Binary marker
|
||||||
|
|
||||||
|
// Count objects we'll create
|
||||||
|
const num_pages = self.pages.items.len;
|
||||||
|
// Objects: catalog, pages, (page + content) * num_pages, fonts
|
||||||
|
const num_fonts: usize = 1; // We'll use one font resource dict
|
||||||
|
const total_objects = 2 + (num_pages * 2) + num_fonts;
|
||||||
|
|
||||||
|
// Object 1: Catalog
|
||||||
|
try obj_positions.append(allocator, output.items.len);
|
||||||
|
try writer.writeAll("1 0 obj\n");
|
||||||
|
try writer.writeAll("<< /Type /Catalog /Pages 2 0 R >>\n");
|
||||||
|
try writer.writeAll("endobj\n");
|
||||||
|
|
||||||
|
// Object 2: Pages
|
||||||
|
try obj_positions.append(allocator, output.items.len);
|
||||||
|
try writer.writeAll("2 0 obj\n");
|
||||||
|
try writer.writeAll("<< /Type /Pages /Kids [");
|
||||||
|
for (0..num_pages) |i| {
|
||||||
|
const page_obj_num = 3 + (i * 2);
|
||||||
|
try writer.print("{d} 0 R ", .{page_obj_num});
|
||||||
|
}
|
||||||
|
try writer.print("] /Count {d} >>\n", .{num_pages});
|
||||||
|
try writer.writeAll("endobj\n");
|
||||||
|
|
||||||
|
// Font resource object (after pages)
|
||||||
|
const font_obj_num = 3 + (num_pages * 2);
|
||||||
|
try obj_positions.append(allocator, output.items.len);
|
||||||
|
try writer.print("{d} 0 obj\n", .{font_obj_num});
|
||||||
|
try writer.writeAll("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\n");
|
||||||
|
try writer.writeAll("endobj\n");
|
||||||
|
|
||||||
|
// Pages and content streams
|
||||||
|
for (self.pages.items, 0..) |*page, i| {
|
||||||
|
const page_obj_num = 3 + (i * 2);
|
||||||
|
const content_obj_num = page_obj_num + 1;
|
||||||
|
|
||||||
|
// Page object
|
||||||
|
try obj_positions.append(allocator, output.items.len);
|
||||||
|
try writer.print("{d} 0 obj\n", .{page_obj_num});
|
||||||
|
try writer.print("<< /Type /Page /Parent 2 0 R ", .{});
|
||||||
|
try writer.print("/MediaBox [0 0 {d:.0} {d:.0}] ", .{ page.width, page.height });
|
||||||
|
try writer.print("/Contents {d} 0 R ", .{content_obj_num});
|
||||||
|
|
||||||
|
// Font resources - include all standard fonts
|
||||||
|
try writer.writeAll("/Resources << /Font << ");
|
||||||
|
inline for (std.meta.fields(Font)) |field| {
|
||||||
|
const font: Font = @enumFromInt(field.value);
|
||||||
|
try writer.print("/{s} << /Type /Font /Subtype /Type1 /BaseFont /{s} >> ", .{ font.pdfName(), font.pdfName() });
|
||||||
|
}
|
||||||
|
try writer.writeAll(">> >> ");
|
||||||
|
|
||||||
|
try writer.writeAll(">>\n");
|
||||||
|
try writer.writeAll("endobj\n");
|
||||||
|
|
||||||
|
// Content stream
|
||||||
|
try obj_positions.append(allocator, output.items.len);
|
||||||
|
try writer.print("{d} 0 obj\n", .{content_obj_num});
|
||||||
|
try writer.print("<< /Length {d} >>\n", .{page.content.items.len});
|
||||||
|
try writer.writeAll("stream\n");
|
||||||
|
try writer.writeAll(page.content.items);
|
||||||
|
try writer.writeAll("\nendstream\n");
|
||||||
|
try writer.writeAll("endobj\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-reference table
|
||||||
|
const xref_pos = output.items.len;
|
||||||
|
try writer.writeAll("xref\n");
|
||||||
|
try writer.print("0 {d}\n", .{total_objects + 1});
|
||||||
|
try writer.writeAll("0000000000 65535 f \n");
|
||||||
|
|
||||||
|
// Write object positions
|
||||||
|
// We need to sort by object number
|
||||||
|
// Object 1 (catalog), 2 (pages), font, then pages/contents
|
||||||
|
try writer.print("{d:0>10} 00000 n \n", .{obj_positions.items[0]}); // obj 1
|
||||||
|
try writer.print("{d:0>10} 00000 n \n", .{obj_positions.items[1]}); // obj 2
|
||||||
|
|
||||||
|
// Page objects and content streams
|
||||||
|
for (0..num_pages) |i| {
|
||||||
|
const page_pos_idx = 3 + (i * 2);
|
||||||
|
const content_pos_idx = page_pos_idx + 1;
|
||||||
|
if (page_pos_idx < obj_positions.items.len) {
|
||||||
|
try writer.print("{d:0>10} 00000 n \n", .{obj_positions.items[page_pos_idx]});
|
||||||
|
}
|
||||||
|
if (content_pos_idx < obj_positions.items.len) {
|
||||||
|
try writer.print("{d:0>10} 00000 n \n", .{obj_positions.items[content_pos_idx]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font object
|
||||||
|
try writer.print("{d:0>10} 00000 n \n", .{obj_positions.items[2]});
|
||||||
|
|
||||||
|
// Trailer
|
||||||
|
try writer.writeAll("trailer\n");
|
||||||
|
try writer.print("<< /Size {d} /Root 1 0 R >>\n", .{total_objects + 1});
|
||||||
|
try writer.writeAll("startxref\n");
|
||||||
|
try writer.print("{d}\n", .{xref_pos});
|
||||||
|
try writer.writeAll("%%EOF\n");
|
||||||
|
|
||||||
|
return output.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves the document to a file
|
||||||
|
pub fn saveToFile(self: *Self, path: []const u8) !void {
|
||||||
|
const data = try self.render(self.allocator);
|
||||||
|
defer self.allocator.free(data);
|
||||||
|
|
||||||
|
const file = try std.fs.cwd().createFile(path, .{});
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
try file.writeAll(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
test "create empty document" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
var doc = Document.init(allocator);
|
||||||
|
defer doc.deinit();
|
||||||
|
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), doc.pages.items.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "add page" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
var doc = Document.init(allocator);
|
||||||
|
defer doc.deinit();
|
||||||
|
|
||||||
|
_ = try doc.addPage(.a4);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), doc.pages.items.len);
|
||||||
|
|
||||||
|
const dims = PageSize.a4.dimensions();
|
||||||
|
try std.testing.expectEqual(@as(f32, 595), dims.width);
|
||||||
|
try std.testing.expectEqual(@as(f32, 842), dims.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "render minimal document" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
var doc = Document.init(allocator);
|
||||||
|
defer doc.deinit();
|
||||||
|
|
||||||
|
var page = try doc.addPage(.a4);
|
||||||
|
try page.drawText(100, 700, "Hello");
|
||||||
|
|
||||||
|
const pdf_data = try doc.render(allocator);
|
||||||
|
defer allocator.free(pdf_data);
|
||||||
|
|
||||||
|
// Check PDF header
|
||||||
|
try std.testing.expect(std.mem.startsWith(u8, pdf_data, "%PDF-1.4"));
|
||||||
|
// Check PDF trailer
|
||||||
|
try std.testing.expect(std.mem.endsWith(u8, pdf_data, "%%EOF\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "font names" {
|
||||||
|
try std.testing.expectEqualStrings("Helvetica", Font.helvetica.pdfName());
|
||||||
|
try std.testing.expectEqualStrings("Times-Bold", Font.times_bold.pdfName());
|
||||||
|
try std.testing.expectEqualStrings("Courier", Font.courier.pdfName());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "color conversion" {
|
||||||
|
const white = Color.white;
|
||||||
|
const floats = white.toFloats();
|
||||||
|
try std.testing.expectEqual(@as(f32, 1.0), floats.r);
|
||||||
|
try std.testing.expectEqual(@as(f32, 1.0), floats.g);
|
||||||
|
try std.testing.expectEqual(@as(f32, 1.0), floats.b);
|
||||||
|
|
||||||
|
const black = Color.black;
|
||||||
|
const black_floats = black.toFloats();
|
||||||
|
try std.testing.expectEqual(@as(f32, 0.0), black_floats.r);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "graphics operations" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
var doc = Document.init(allocator);
|
||||||
|
defer doc.deinit();
|
||||||
|
|
||||||
|
var page = try doc.addPage(.a4);
|
||||||
|
|
||||||
|
try page.setLineWidth(2.0);
|
||||||
|
try page.drawLine(0, 0, 100, 100);
|
||||||
|
try page.drawRect(50, 50, 100, 50);
|
||||||
|
|
||||||
|
page.setFillColor(Color.light_gray);
|
||||||
|
try page.fillRect(200, 200, 50, 50);
|
||||||
|
|
||||||
|
// Content should have been written
|
||||||
|
try std.testing.expect(page.content.items.len > 0);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue