Open-source project
gastongouron/ironpress avatar
gastongouron/ironpress

ironpress converts HTML and Markdown to PDF without launching a browser

Pure Rust PDF converter, no browser, no external dependencies. Supports HTML with inline CSS, Markdown, and document conversion with a built-in layout engine.

663 stars54 forksRustMIT

At a glance

What is it?
ironpress is a pure Rust PDF converter with its own layout engine, font shaping, SVG and math support, shipped as bindings for Rust, C, C++, .NET, Java, Python, Ruby and WebAssembly. It removes the Chrome dependency that headless renderers impose, and the trade is that you inherit one engine's coverage rather than a browser's.
Who is it for?
Adopt ironpress when you need PDF bytes from HTML or Markdown inside a service or a WASM bundle and cannot or will not ship a browser runtime, especially for CJK text where the 310 ms median for 200 emphasis spans on an Apple M2 is a documented cost you should measure against your own documents.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A PDF engine that never starts Chrome

The pitch in one line from the README: fast, in-process HTML, CSS and Markdown to PDF rendering in pure Rust, with no browser, subprocess or system dependencies. That last clause is the reason the project exists. The common way to produce a PDF from HTML is to drive a headless browser, which means shipping Chromium, managing its lifecycle, and accepting the memory and startup cost of a full browser in a server that renders documents rather than browsing the web.

ironpress replaces that with a rendering engine written in Rust that handles document layout, font shaping, SVG, math and PDF serialization inside your process. The output is PDF bytes, and the entry point is a function call. The Cargo.toml makes the packaging commitment explicit: the crate description repeats that there is no browser and no system dependencies, and it declares a license of MIT.

The layout scope is document-oriented rather than general web-oriented. The README lists flexbox, grid, tables, multi-column layout, @page rules, headers and footers as the supported feature set. That is the vocabulary of reports, invoices and articles, not of a marketing site. A project that needs to reproduce a full web page in pixel-perfect detail is asking this engine for something it does not claim.

The other half of the design is defensive. The README lists HTML and SVG sanitization, constrained file access and opt-in remote fetching policies as defaults. Untrusted input turning into a PDF is a common server-side attack surface, and ironpress treats the fetcher as something you switch on rather than something you switch off.

Typography, subsetting and the CJK and emoji packs

Font handling is where a home-grown PDF engine usually shows its limits, so it is worth reading the typography line closely. The README claims font subsetting, core Unicode coverage, optional regional CJK and emoji packs, SVG and math under the heading of production typography. Font subsetting matters for output size, since a PDF that embeds a full Unicode font for a page of Latin text is wasteful, and the repository carries a font-packs directory at the top level for the regional additions.

The shaping stack is visible in the dependency list. rustybuzz and skrifa handle text shaping and font reading, fontdb provides the font database, and a pinned skrifa version with an exact equals requirement suggests the maintainers have hit a compatibility problem and locked the dependency down. The same care appears with lightningcss, which is pinned to an exact alpha version, and with parcel_selectors, where a comment in Cargo.toml explains that the transitive selector API is being held at 0.28.2 because a later release moved to an incompatible cssparser.

That is a useful signal about the project even before you read a benchmark. The dependency choices show the maintainers treating upstream churn as a real source of breakage and pinning accordingly.

The alternative to all of this is the browser, which ships with a mature text stack and does not need a regional font pack to render Japanese. Whether ironpress's font coverage is enough for your documents is the question to answer before committing, and the CJK pack is an optional feature rather than something included by default.

Two function calls and you have a PDF

The quick start is short enough to read at a glance. In Rust, the HTML entry point takes a string and returns the PDF bytes:

rust
use ironpress::html_to_pdf;

let pdf = html_to_pdf("<h1>Hello</h1><p>World</p>").unwrap();
std::fs::write("output.pdf", pdf).unwrap();

The Markdown path is the same shape, and the two functions live in the same crate, so a service that accepts both document types does not need two dependencies:

rust
let pdf = ironpress::markdown_to_pdf("# Hello\n\nWorld").unwrap();

The Markdown implementation sits on pulldown-cmark, and the HTML side on html5ever with markup5ever_rcdom for the DOM, so both input formats go through a proper parser rather than a regex pass. The Cargo.toml also mentions streaming output in its description, which matters for large documents, since a converter that buffers an entire document in memory before writing a single page is a different proposition from one that can emit pages as it lays them out.

For the rest of the languages, the README directs you to getting-started guides for Rust, the CLI, C, C++, .NET, Java, Python, Ruby, browser JavaScript and Node.js, and to a browser playground where you can paste a document and see the output without installing anything.

One Rust core published to eight language ecosystems

The bindings table is the unusual part of this project. The same core is distributed as a crate on crates.io for Rust, as a GitHub release for C and C++, as a NuGet package for .NET, as a Maven Central artefact for Java, as a PyPI package for Python, as a gem for Ruby, and as an npm package for JavaScript, with separate guides for browser JavaScript and Node.js.

The repository layout backs this up. A bindings directory sits at the top level, and Cargo.toml declares a workspace whose members are bindings/c, bindings/python and bindings/ruby/ext/ironpress, so the C, Python and Ruby bindings are built from the same source tree. The library target is declared as both cdylib and rlib, which is what lets one build serve as a native library for FFI and as a normal Rust library at the same time. A patch section pointing ironpress at the local path keeps the workspace using the working copy rather than a published version.

For a team with an existing .NET or Java service, this is the deciding factor. A PDF generator that has to be reached through a subprocess, a sidecar container or a hosted API is a different operational commitment from a package that links into the process. The C API and the C++ package are what make that possible for teams that are not on a managed runtime.

The cost of the breadth is that the core and its wrappers have to be released in step, and the versions across those ecosystems are published separately. Pinning a consistent version across six package managers is a chore the README does not describe for you.

Feature flags keep async, remote and wasm optional

The features table in Cargo.toml is short and does real work. The default feature set is empty, meaning the base crate pulls in no optional machinery. Three features exist beyond that: async enables tokio, remote enables bogon, core-net, ureq and url, and wasm enables wasm-bindgen, js-sys and getrandom with the wasm_js feature. The wasm target is what backs the browser playground and the WebAssembly badge, and the crate is published to npm for JavaScript consumers.

The remote feature deserves attention. Fetching remote resources while rendering a document is how a converter ends up as a server side request forgery vector, so keeping it behind a feature flag and describing it as an opt-in remote fetching policy is a deliberate design decision rather than an oversight. A deployment that renders documents from untrusted authors should leave it off.

There is also an async path, which is the answer for anyone who wanted to hand ironpress a request handler and keep the process responsive. Whether that path is a complete async API or a wrapper over the synchronous core is not described in the README, so anyone planning to use it should check the API documentation on docs.rs rather than assume.

The project also carries a fuzz directory and a tests directory, plus a codecov configuration, which is a sensible setup for a parser that consumes untrusted HTML.

Reading the Criterion numbers without over-reading them

The README publishes a benchmark table measured with Criterion over complete in-process conversion to PDF bytes. Simple HTML with an h1 and a paragraph has a representative median of 0.93 ms, styled HTML with lists and links is 3.5 ms, a five row table is 5.9 ms, Markdown with headings and code is 7.0 ms, and a full report with tables, flex and progress bars is 15.9 ms, the same figure for a full report with a header and footer.

The measurements were taken on macOS ARM with an Apple M2, Rust 1.94.0 at a named commit, using a benchmark profile with opt-level=3, fat LTO and one codegen unit. The README is careful about what the numbers mean, stating that results depend on hardware and document content and that conversions per second is not a page throughput claim. That caveat is the correct one, and the numbers should be read as a shape rather than a promise.

The row that deserves a second look is the last one, 200 CJK text emphasis spans at a 310 ms median and about 3.2 conversions per second. Compared with the 15.9 ms full report, that is a large jump for a small amount of text, and it points at CJK shaping as the expensive part of the pipeline rather than page count as the limiting factor.

The last row is also why the CJK font packs are optional rather than bundled. A document in Japanese or Chinese is the case where a home-grown engine has to work hardest, and it is the case where the browser-based alternative has the least trouble. The README links a parity report for exactly this kind of comparison, and reading it for your own document types is the sensible next step rather than trusting the aggregate numbers.

Where a hand-written layout engine is the wrong answer

The fundamental trade is between control and coverage. A browser engine has spent two decades accumulating behaviour for every corner of CSS, and a subset of that behaviour is what your users' documents actually rely on. A dedicated document engine can implement exactly the subset it claims, with a layout model designed for pages rather than for scrolling viewports, but the moment a document uses a feature outside that set the output degrades. A flexbox gap computed slightly differently, a table that reflows differently, a float that a browser resolves and ironpress does not, all of it produces a PDF that is wrong in ways that are tedious to find.

The second limitation is layout fidelity against print expectations. The engine supports @page rules, headers and footers, multi-column layout and tables, which is a strong list, but the README does not describe paged media support at the level of page breaks inside a block, orphans and widows, or footnote placement, and those are exactly the features a report generator needs.

The third is the ecosystem. CSS evolves continuously and browsers track it, so a document that renders correctly in Chrome today may rely on a property ironpress has not implemented. A custom engine improves only as fast as its maintainer implements features, and the parity report is the place where that gap is recorded rather than hidden.

The cases where ironpress is clearly right are narrow and valuable: a service that must render untrusted HTML to PDF without giving it a browser, a WASM bundle that has to produce a document client side, a C or C++ program that links a PDF writer rather than shelling out, and a deployment where a Chromium install is not permitted.

Editorial conclusion

Adopt ironpress when you need PDF bytes from HTML or Markdown inside a service or a WASM bundle and cannot or will not ship a browser runtime, especially for CJK text where the 310 ms median for 200 emphasis spans on an Apple M2 is a documented cost you should measure against your own documents. Do not adopt it when pixel parity with Chrome is the requirement, since the project's own parity report exists precisely because coverage differs, and check the 15.9 ms full-report figure on your hardware before designing around it. Verify first that your Rust toolchain meets the stated minimum of 1.88, that the crate version you pin matches the 1.7.0 in Cargo.toml, that any remote content is handled by opting into the remote feature rather than by default, and that the fonts you rely on are covered by the core Unicode set or by a regional CJK and emoji pack.

Frequently asked questions

Does ironpress need Chrome or a headless browser to make a PDF?

No. The README describes it as in-process HTML, CSS and Markdown to PDF rendering in pure Rust, with no browser, subprocess or system dependencies, and the rendering engine handles layout, font shaping, SVG, math and PDF serialization internally.

Which languages can I use ironpress from?

The bindings table lists Rust, C, C++, .NET, Java, Python, Ruby and JavaScript, with separate guides for browser JavaScript and Node.js. The C, Python and Ruby bindings are workspace members of the same Cargo workspace, and the library target is built as both cdylib and rlib.

What Rust version does ironpress require?

The Cargo.toml sets rust-version to 1.88 with edition 2024, and the README carries an MSRV 1.88 badge. The benchmark table was measured with Rust 1.94.0 on an Apple M2, which is the version used for the published numbers rather than the minimum supported one.

How does ironpress handle remote images and fonts in a document?

Remote fetching is opt-in. The features table in Cargo.toml has a remote feature that pulls in bogon, core-net, ureq and url, and the README describes opt-in remote fetching policies as a default behaviour, with HTML and SVG sanitization and constrained file access alongside it.

Is ironpress fast enough for CJK documents?

The published Criterion table reports a 310 ms median for 200 CJK text emphasis spans, about 3.2 conversions per second, against 15.9 ms for a full report. The README notes that results depend on hardware and document content, and links a parity report for comparisons against other rendering approaches.

Official sources

  1. gastongouron/ironpress on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/gastongouron-ironpress.svg)](https://hysenlabs.com/projects/gastongouron-ironpress)

Was this useful?