ironpress

August 25, 2026 · View on GitHub

4

ironpress

Fast, in-process HTML/CSS/Markdown to PDF rendering in pure Rust. No browser, subprocess, or system dependencies.

docs.rs CI codecov deps.rs MSRV License: MIT Downloads WASM Playground Parity report

Website | Get Started | Playground | HTML-to-PDF guide | Parity report | Wiki

Runtime Documentation Package
Rust Get started with Rust Crates.io
C Get started with C GitHub release
C++ Get started with C++ GitHub release
.NET Get started with .NET NuGet
Java Get started with Java Maven Central
Python Get started with Python PyPI
Ruby Get started with Ruby Gem
JavaScript Get started in the browser · Get started with Node.js npm

ironpress turns HTML, CSS, or Markdown into PDF bytes inside your application. Its rendering engine handles document layout, font shaping, SVG, math, and PDF serialization without launching Chrome. Use it from Rust, C, C++, .NET, Java, the CLI, Python, Ruby, Node.js, or browser WebAssembly.

Why ironpress?

  • Simple deployment: one library or binary, with no browser runtime to install or manage.
  • Document-focused layout: flexbox, grid, tables, multi-column layout, @page, headers, and footers.
  • Production typography: font subsetting, core Unicode coverage, optional regional CJK/emoji packs, SVG, and math.
  • Multiple runtimes: the same Rust core ships to Rust, C, C++, .NET, Java, Python, Ruby, and WebAssembly.
  • Defensive defaults: HTML/SVG sanitization, constrained file access, and opt-in remote fetching policies.

Quick start

use ironpress::html_to_pdf;

let pdf = html_to_pdf("<h1>Hello</h1><p>World</p>").unwrap();
std::fs::write("output.pdf", pdf).unwrap();
let pdf = ironpress::markdown_to_pdf("# Hello\n\nWorld").unwrap();

Choose a complete getting-started guide for Rust, the CLI, C, C++, .NET, Java, Python, Ruby, browser JavaScript, or Node.js. For a deeper Rust walkthrough, see HTML-to-PDF rendering without Headless Chrome, or paste a document into the browser playground.

Performance

Criterion measures complete, in-process conversion to PDF bytes:

DocumentRepresentative medianApprox. conversions/sec
Simple HTML (<h1> + <p>)0.93 ms1,080
Styled HTML (CSS, lists, links)3.5 ms285
Table (5 rows, styled headers)5.9 ms170
Markdown (headings, code, lists)7.0 ms143
Full report (tables, flex, progress bars)15.9 ms63
Full report + header/footer15.9 ms63
200 CJK text-emphasis spans310 ms3.2

Measured on macOS ARM (Apple M2) with Rust 1.94.0 at commit 600aadb. The benchmark profile uses opt-level=3, fat LTO, and one codegen unit. Results depend on hardware and document content; conversions/sec is not a page-throughput claim. Reproduce the measurements with:

cargo bench --bench conversion

CLI

cargo install ironpress

ironpress input.html output.pdf
ironpress document.md output.pdf
ironpress --page-size letter --landscape --margin 54 input.html output.pdf
ironpress --header "Report" --footer "Page {page} of {pages}" input.html output.pdf
echo '<h1>Hello</h1>' | ironpress --stdin output.pdf

Builder API

use ironpress::{HtmlConverter, Margin, PageSize, RasterQuality};

let pdf = HtmlConverter::new()
    .page_size(PageSize::LETTER)
    .margin(Margin::uniform(54.0))
    .raster_quality(RasterQuality {
        background_dpi: 144.0,
        ..RasterQuality::default()
    })
    .header("My Document")
    .footer("Page {page} of {pages}")
    .convert("<h1>Custom page</h1>")
    .unwrap();

RasterQuality keeps source-image, filter, and flattened-background resolution in one physical-DPI policy. Its default preserves sharp source/filter output while using 192 DPI for flattened synthetic backgrounds; lowering one field does not change page geometry. The CLI exposes the same controls through --image-dpi, --filter-dpi, and --background-raster-dpi.

Features at a glance

AreaHighlightsDetails
HTML50+ elements: headings, tables, lists, forms, media, <img>, inline <svg>Layout Engine
CSSFlexbox, grid, multi-column, calc(), variables, @media, @page, @font-faceCSS Support
FontsCore Unicode fonts, custom TTF subsetting, system discovery, optional CJK/emoji packsFont System
MathLaTeX via $...$ / $$...$$: fractions, roots, matrices, Greek, operatorsMath Engine
SVGVector rendering: path, shapes, gradients, transforms, clip paths, viewBoxLayout Engine
ImagesJPEG + PNG, data URIs, local files, remote URLs (remote feature)Architecture
PDFPDF 1.4, bookmarks, link annotations, headers/footers, gradients, streaming outputPDF Rendering
C ABIStable native API with versioned Linux, macOS, and Windows librariesC binding
C++Move-only C++17 RAII owners over the stable native ABIC++ binding
.NETManaged HtmlConverter, typed failures, and RID-native assets.NET binding
JavaJava 17 HtmlConverter, typed failures, and packaged native assetsJava binding
WASMnpm install ironpress - runs in browsers and Node.jsWASM & Playground
Testing3,200+ unit tests, property-based tests, 6 fuzz targets, 1,664-fixture parity corpusTesting Strategy

Custom fonts

let pdf = HtmlConverter::new()
    .add_font("Inter", std::fs::read("Inter.ttf").unwrap())
    .convert(r#"<p style="font-family: Inter">Shaped with HarfBuzz</p>"#)
    .unwrap();

Fonts are shaped with rustybuzz, subset to used glyphs only, and embedded as CIDFontType2. Core includes Latin, Arabic, Hebrew, and common Unicode coverage. Native builds may also discover system fonts.

Full regional CJK and monochrome emoji coverage is distributed as five optional packs: cjk-jp, cjk-kr, cjk-sc, cjk-tc, and emoji. The renderer never downloads them. Load only the packs your application needs:

use ironpress::{FontPack, FontPackKind, HtmlConverter};

let japanese = FontPack::parse(
    FontPackKind::CjkJapanese,
    std::fs::read("ironpress-font-cjk-jp.ttf").unwrap(),
)
.unwrap();
let pdf = HtmlConverter::new()
    .add_font_pack(japanese)
    .convert("<p lang='ja'>日本語</p>")
    .unwrap();

Use lang on the document or a nested element to select the correct regional CJK glyph forms. See Font System and font-packs/README.md.

Math

The equation $E = mc^2$ is famous.

$$\sum_{k=1}^{n} k = \frac{n(n+1)}{2}$$

Full LaTeX support: fractions, roots, matrices, Greek letters, operators, delimiters, accents. See Math Engine.

Language bindings

The C ABI ships as versioned static and shared libraries in GitHub Releases. Relocatable CMake targets serve C and C++ consumers, while Unix archives also provide pkg-config metadata. The ABI uses opaque handles, explicit allocation ownership, stable status codes, and no ambient error state. See the C binding guide.

Python and Ruby expose the same reusable converter controls as WebAssembly: page geometry, quality settings, sanitization, headers and footers, custom fonts, and optional CJK or emoji packs. See the complete binding capability matrix.

dotnet add package Ironpress
using Ironpress;

using var converter = new HtmlConverter()
    .SetPageSize(PageSize.Letter)
    .SetFooter("Page {page} of {pages}");
byte[] pdf = converter.ConvertHtml("<h1>Hello</h1>");
<dependency>
  <groupId>io.github.gastongouron</groupId>
  <artifactId>ironpress</artifactId>
  <version>1.5.5</version>
</dependency>
try (var converter = new HtmlConverter()) {
    byte[] pdf = converter.convertHtml("<h1>Hello</h1>");
}
pip install ironpress
import ironpress
converter = ironpress.HtmlConverter()
converter.page_size("Letter")
converter.footer("Page {page} of {pages}")
pdf = converter.convert("<h1>Hello</h1>")
gem install ironpress
require "ironpress"
converter = Ironpress::HtmlConverter.new
  .page_size("Letter")
  .footer("Page {page} of {pages}")
pdf = converter.convert("<h1>Hello</h1>")

WASM

npm install ironpress

Browser entry point:

import init, { HtmlConverter } from 'ironpress';
await init();

const converter = new HtmlConverter();
converter.pageSize('Letter');
converter.footer('Page {page} of {pages}');
const pdf = converter.htmlToPdf('<h1>Hello</h1>');
const blob = new Blob([pdf], { type: 'application/pdf' });
converter.free();

Node.js entry point:

import init, { HtmlConverter } from 'ironpress/node';

await init();

const converter = new HtmlConverter();
converter.pageSize('Letter');
const pdf = converter.htmlToPdf('<h1>Hello from Node.js</h1>');
converter.free();

ironpress/node locates and loads the WebAssembly binary shipped in the npm package. Applications do not need to resolve or read it themselves. This entry point uses the portable WebAssembly contract: document resources, custom fonts, and optional font packs remain caller-provided bytes. Local paths, direct file output, streaming, and remote fetching are not available.

See WASM & Playground.

Security

HTML is sanitized by default. Scripts, iframes, event handlers, and javascript: URLs are removed. SVG sanitization and image decoder limits also apply.

Local files are denied unless base_path or resource_root grants a canonical directory. Traversal and symlink escapes outside that directory are rejected.

Remote fetching is disabled unless the crate is built with remote:

cargo add ironpress --features remote

With that feature, public HTTP and HTTPS resources are allowed by default. Loopback, private, link-local, metadata, multicast, documentation, and reserved addresses are denied. Redirects are checked again, DNS results are pinned to the connection, and response bodies are limited to 10 MB by default.

Use an allow list for a known CDN, or combine it with deny_public_ips(true) to deny every host that was not explicitly allowed:

use ironpress::{HtmlConverter, NetworkPolicy, RemoteHost};

let cdn: RemoteHost = "cdn.example.com".parse().expect("valid host");
let policy = NetworkPolicy::default()
    .with_allow_list([cdn])
    .deny_public_ips(true)
    .max_redirects(4)
    .max_body_size(2 * 1024 * 1024);

let pdf = HtmlConverter::new()
    .network_policy(policy)
    .convert("<img src='https://cdn.example.com/logo.png'>")
    .expect("conversion succeeds");

A deny-list match always wins. An allow-list match explicitly trusts that host and bypasses its IP-class check.

Environment proxies are respected. They are operator configuration, not document input. If the proxy resolves the target hostname, Ironpress can still check target IP literals and host lists, but the proxy must enforce the final IP policy.

For a server that converts untrusted documents, also enforce egress outside the process:

  • Block cloud metadata, loopback, private, and link-local networks at the host or network namespace.
  • Restrict outbound DNS and traffic to required destinations or a controlled proxy.
  • Apply time, memory, and process limits to conversions.
  • Treat image malware scanning as a server concern. Ironpress controls resource access; it is not an antivirus.

HTML sanitization, local-file access, and remote access are independent. Calling .sanitize(false) does not disable either resource policy.

Migration note: .sanitize(false) no longer grants implicit access to files in the process working directory. Configure .base_path(...) for document assets, and .resource_root(...) when those assets need a broader directory boundary.

See Resource Security for the complete threat model, proxy boundary, and server deployment guidance.

How it works

HTML/Markdown → Sanitize → Parse (html5ever) → Style cascade → Layout engine → PDF 1.4

See Architecture for the full pipeline.

Visual parity harness

tests/parity/ is an adversarial HTML/CSS corpus with one focused fixture per feature, value, or interaction. Ironpress produces the candidate PDF; the declared oracle renderer's PDF is committed. At test time both PDFs go through the same discovered pdftoppm executable with the same 300 DPI arguments. A fixture passes only when a fixed, same-coordinate human-visibility policy finds no visible defect. Every raw RGBA difference remains in the evidence; the harness never translates, registers, or fixture-tunes either raster.

scripts/parity.sh                       # run the complete exact parity gate
scripts/parity-gen-refs.sh <category>   # regenerate oracle PDFs explicitly
scripts/parity-gen-refs.sh --check      # authenticate the complete corpus
  • Run it: scripts/parity.sh supplies a fresh invocation identity, renders every fixture in-process, rasterizes candidate and oracle PDFs symmetrically, and verifies that JSON, Markdown, and HTML all belong to that invocation.
  • Read it: the HTML parity report provides the complete visual evidence; tests/parity/REPORT.md is the compact, problem-first summary, and tests/parity/report.json contains the complete machine result.
  • Oracles: committed PDFs are the source of truth. Oracle-preview, candidate, and diff PNGs are generated report evidence and are intentionally ignored. refs.lock authenticates each fixture, oracle PDF, manifest entry, renderer, fonts, and generator provenance. Every future oracle PDF is generated only by the pinned Chromium Fontations/Foundation launcher; authenticated historical non-Chromium PDFs are evidence-only and cannot be regenerated.
  • Baseline: tests/parity/baseline.json is a separately reviewed regression snapshot. Updating it is explicit; retained FAILs remain current-health failures while their exact rasters become protected against movement or worsening.
  • CI: .github/workflows/parity.yml runs the same browser-free gate, checks refs.lock, and uploads the current report and evidence even when defects make the gate fail.

License

MIT