Fulgur
A modern, lightweight alternative to wkhtmltopdf. Converts HTML/CSS to PDF without a browser engine.
Built in Rust for server-side workloads where memory footprint and startup time matter.
Why Fulgur?
- No browser required — No Chromium, no WebKit, no headless browser. Single binary, instant cold start.
- Low memory footprint — Designed for server-side batch processing without blowing up your container's memory limits.
- Deterministic output — Same input always produces the same PDF, byte for byte. Safe for CI/CD and automated pipelines.
- Template + JSON data — Feed an HTML template and a JSON file to generate PDFs at scale. Built-in MiniJinja engine.
- Offline by design — No network access. All assets (fonts, images, CSS) are explicitly bundled.
Features
- HTML/CSS to PDF conversion with automatic page splitting
- CSS pagination control (
break-before,break-after,break-inside, orphans/widows) - CSS Generated Content for Paged Media (page counters, running headers/footers, margin boxes)
- Template engine with JSON data binding (MiniJinja)
- Image embedding (PNG / JPEG / GIF)
- Custom font bundling with subsetting
- External CSS injection
- Page sizes (A4 / Letter / A3) with landscape support
- PDF metadata (title, author, keywords, language)
- PDF bookmarks from heading structure
Installation
CLI Usage
# Basic conversion
# Read from stdin
|
# Page options
# Custom fonts and CSS
# Images
# Template + JSON data
Template Example
template.html:
Invoice #{{ invoice_number }}
{{ customer_name }}
{% for item in items %}
{{ item.name }}{{ item.price }}
{% endfor %}
data.json:
Options
| Option | Description | Default |
|---|---|---|
-o, --output |
Output PDF file path (required, use - for stdout) |
— |
-s, --size |
Page size (A4, Letter, A3) | A4 |
-l, --landscape |
Landscape orientation | false |
--margin |
Page margins in mm (CSS shorthand: "20", "20 30", "10 20 30", "10 20 30 40") |
— |
--title |
PDF title metadata | — |
-f, --font |
Font files to bundle (repeatable) | — |
--css |
CSS files to include (repeatable) | — |
-i, --image |
Image files to bundle as name=path (repeatable) | — |
-d, --data |
JSON data file for template mode (use - for stdin) |
— |
--stdin |
Read HTML from stdin | false |
Library Usage
use Engine;
use ;
// Basic conversion
let engine = builder.build;
let pdf = engine.render_html?;
// With page options
let engine = builder
.page_size
.margin
.title
.build;
let pdf = engine.render_html?;
engine.render_html_to_file?;
// Template + JSON
let engine = builder
.template
.data
.build;
let pdf = engine.render?;
Architecture
Fulgur integrates Blitz (HTML parsing, CSS style resolution, layout) with Krilla (PDF generation) through a pagination-aware layout abstraction.
HTML/CSS input
↓
Blitz (HTML parse → DOM → style resolution → Taffy layout)
↓
DOM → Pageable conversion (BlockPageable / ParagraphPageable / ImagePageable)
↓
Pagination (split Pageable tree at page boundaries)
↓
Krilla rendering (Pageable.draw() per page → PDF Surface)
↓
PDF bytes
Project Structure
crates/
├── fulgur/ # Core library (conversion, layout, rendering)
└── fulgur-cli/ # CLI tool
Development
# Build
# Test
# Run CLI directly
Determinism and fonts
Fulgur aims for byte-identical PDF output from identical input. The core pipeline (Blitz → Taffy → Parley → Krilla) is deterministic, but there is one known environment dependency you should be aware of:
- Blitz
blitz-dom0.2.4 uses a process-globalfontdb::Database::load_system_fonts()call for parsing inline<svg>elements. Fulgur cannot currently override it, so the font chosen for<text>elements inside SVG — and the default fallback for HTML text when no bundled fonts are supplied — depends on which.ttf/.otffiles are installed on the host. The same HTML can therefore produce different PDFs on two machines if their system font sets differ.
To get reproducible output today, pin the font environment via fontconfig:
# Point fontconfig at a controlled set of font files.
The repository ships a pinned Noto Sans bundle under examples/.fonts/
together with a matching examples/.fontconfig/fonts.conf, which is what
mise run update-examples and the GitHub Actions regen workflows use to
keep examples/*/index.pdf byte-identical across environments. See
examples/.fonts/README.md for the exact font list and re-fetch
instructions. Making this configurable at the library API level (so
fulgur::Engine callers get determinism without touching fontconfig) is
tracked as a follow-up — once landed, library callers will be able to
supply their own font database directly.
Security
If you plan to accept untrusted HTML templates or JSON data (e.g. in a multi-tenant SaaS), see the threat model (日本語) for the full analysis of attack vectors and mitigations.