Engineering guide

HTML to PDF in Rust
without Headless Chrome

A practical look at in-process document rendering: what changes when the browser disappears, how to get started, and when that trade is worth making.

12 minute read Rust · PDF · Rendering

The deployment problem behind HTML to PDF

HTML is a convenient document template language. Your team may already have components for invoices, statements, tickets, or reports, and CSS makes those templates easy to iterate on. The usual conversion path, however, is to start a browser, load the document, ask it to print, and collect the resulting bytes.

That works and remains the right choice for genuinely browser-like pages, but it also makes a browser part of your production architecture. Images need the correct executable and shared libraries. Workers need process lifecycle management. Cold starts, browser upgrades, timeouts, and untrusted network requests become operational concerns alongside the document itself.

The useful question is not “can a browser print this?”

It is “does this document need a browser, or does it need a deterministic print renderer?”

ironpress explores the second option. It is a Rust library that parses HTML and CSS, computes a paged layout, shapes text, and writes PDF bytes in the same process as your application. There is no browser executable and no subprocess boundary.

What “in process” actually means

Removing the browser does not remove the work. It replaces a broad browser engine with a purpose-built document pipeline. ironpress owns five stages:

  1. Parse and sanitize the supplied HTML, stylesheets, and resources.
  2. Build a style tree with selectors, inheritance, variables, and print rules.
  3. Lay out pages using block, flex, grid, table, and multi-column algorithms.
  4. Shape and draw text, images, SVG, borders, gradients, and math.
  5. Serialize PDF with fonts, links, bookmarks, headers, and footers.

This narrower scope is the source of both the benefits and the limits. There is no JavaScript runtime, DOM mutation, or screen media emulation. In return, conversion is a normal function call, output does not depend on a separately installed browser, and the same core can compile to WebAssembly.

Convert HTML to PDF from Rust

Add the crate to an existing Rust project:

cargo add ironpress

The smallest API accepts HTML and returns the complete PDF as bytes:

use ironpress::html_to_pdf;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let html = r#"
        <style>
          body { font-family: sans-serif; color: #172033; }
          h1 { color: #295dff; }
        </style>
        <h1>Quarterly report</h1>
        <p>Generated inside the Rust process.</p>
    "#;

    let pdf = html_to_pdf(html)?;
    std::fs::write("report.pdf", pdf)?;
    Ok(())
}

For page settings, resources, headers, footers, or security policy, use the builder API:

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

let pdf = HtmlConverter::new()
    .page_size(PageSize::A4)
    .margin(Margin::uniform(48.0))
    .header("Northstar Studio")
    .footer("Page {page} of {pages}")
    .convert("<h1>Invoice #042</h1>")?;

The same engine is packaged for Python, Ruby, and WebAssembly. You can evaluate its rendering directly in the client-side playground; the HTML never leaves the browser.

Design CSS for pages, not screens

A document is a sequence of fixed pages rather than an infinite viewport. Start with explicit page intent. The @page rule defines size and margins, while break properties keep logical sections together.

@page {
  size: A4;
  margin: 18mm 16mm 20mm;
}

.invoice-line {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 12pt;
  break-inside: avoid;
}

h2 {
  break-after: avoid;
}

Treat print templates as their own interface. Use physical units deliberately, make page breaks part of the design, embed the fonts you need, and test representative long content, not only the tidy one-page example.

Fonts, SVG, and math

ironpress embeds custom TTF fonts with subsetting and uses Unicode fallback for scripts outside the base PDF fonts. Inline SVG stays vector where supported, and LaTeX-style expressions can be used for technical documents. Those features avoid the “everything became a screenshot” failure mode of raster-first pipelines.

Measure the whole conversion

PDF benchmarks are easy to misread. Parsing a fragment is not the same as producing final bytes, and pages per second is not the same as documents per second. ironpress benchmarks the complete in-process call and reports representative medians.

DocumentMedianConversions/sec
Simple HTML0.93 ms1,080
Styled HTML3.5 ms285
Five-row table5.9 ms170
Full report15.9 ms63

These figures were measured on an Apple M2 with an optimized Rust benchmark profile. They describe the included samples, not a universal throughput promise. Document complexity, fonts, images, and hardware all matter. Reproduce them in the repository with:

cargo bench --bench conversion

Keep the resource boundary explicit

Rendering untrusted HTML is an input-security problem even without JavaScript. Documents can reference local paths, remote URLs, large images, deeply nested markup, or hostile SVG. A renderer should not quietly inherit all the access of its host process.

ironpress sanitizes HTML by default. Local files require an explicit base path or resource root. Remote fetching is a feature you opt into, with controls for hosts, address classes, redirects, and response size. Production services should still apply network, time, memory, and request limits outside the library.

Library policy is one layer, not the entire sandbox.

For hostile documents, combine renderer controls with process and network isolation appropriate to your service.

When to use ironpress and when not to

The renderer should match the source document. The following decision table is a more useful starting point than treating every PDF tool as interchangeable.

ApproachBest fitMain tradeoff
ironpress Known HTML/CSS templates, embedded conversion, serverless, offline, WASM No JavaScript or exact browser-print parity
Headless browser Existing web pages, client-rendered content, exact Chrome behavior Browser runtime and process operations
Low-level PDF library Custom graphics and full control over drawing commands You build layout and text flow
Document DSL Typeset documents designed in that language from the start Templates are not HTML/CSS

Choose a browser when the source depends on JavaScript, browser layout quirks, canvas, or an exact screenshot of a web application. Choose ironpress when you control a print-oriented template and want conversion to behave like the rest of your Rust code: a library call with explicit inputs, outputs, and policies.

Try your own document

The playground runs ironpress through WebAssembly, entirely in your browser. Paste HTML or Markdown and inspect the PDF output.

Open the playground