Safe, self-contained Rust bindings for PDFium, Google's PDF engine: open documents from memory, render pages to owned pixel buffers, map coordinates between rendered pixels and PDF page space, extract positioned text, and draw form fields — safely usable from concurrent code.
- A small, curated, rendering-focused API. The surface is the set of
operations document pipelines actually need — load, inspect, render,
extract, fail cleanly on hostile input — wrapped completely and tested
hard, rather than a binding for every PDFium function. The raw function
table stays reachable through the public
sysmodule for anything the safe API does not cover yet. - Thread safety by serialization, not by assertion. PDFium is not
thread-safe, so every FFI call goes through one process-wide mutex; there
is no unlocked path. All handle types are
Send + Syncbecause of that discipline, with the invariants written down next to eachunsafe impl(see docs/ARCHITECTURE.md). - Owned results and first-class coordinate transforms. A rendered page is plain data — pixels, stride, format, and the pixel-to-page transform — holding no PDFium resources. OCR-style consumers can map pixel boxes back to PDF page space long after the page and document are gone.
- Batteries-included binary acquisition. The repository pins a specific upstream PDFium release with sha256 checksums, fetches and verifies it with one command, and republishes verified native archives with checksums, license texts, provenance metadata, and GitHub build attestations.
Scope note: this crate deliberately does not cover PDF editing — creating or modifying pages, objects, annotations, form field values, or signatures. It is for teams that want a smaller audited surface, strictly serialized FFI, and a verified binary supply chain for read/render/extract workloads; if you need to change PDFs, other bindings in the ecosystem cover that ground.
cargo add firecrawl-pdfiumThe crate builds everywhere with plain cargo build — it contains no build
script and never links PDFium at build time. PDFium is loaded at
runtime from a shared library (libpdfium.dylib / libpdfium.so /
pdfium.dll).
| Route | Best for | Notes |
|---|---|---|
cargo xtask fetch-pdfium |
working in this repository | Downloads the release pinned in pdfium.lock.json, verifies its sha256, and extracts to target/pdfium/<platform>/ where Pdfium::load() finds it automatically. |
native-v* releases on firecrawl/pdfium-rs |
production deployments | The pinned upstream binaries, repackaged with upstream LICENSE and licenses/ kept verbatim, plus PROVENANCE.json, SHA256SUMS, an SBOM, and GitHub build-provenance attestations. Verify with the commands below. |
| bblanchon/pdfium-binaries directly | pinning your own PDFium version | The upstream source of our binaries (weekly builds). Upstream publishes no checksum files, so record your own at download time. |
| System / distro package | base images that already ship PDFium | Install libpdfium and rely on the system loader, or point PDFIUM_LIB_PATH at it. |
Verifying a native-v* release asset:
shasum -a 256 --check SHA256SUMS --ignore-missing
gh attestation verify firecrawl-pdfium-linux-x64.tgz --repo firecrawl/pdfium-rsPdfium::load() tries, in order — first hit wins:
- The
PDFIUM_LIB_PATHenvironment variable: a path to the library file, or to a directory containing the platform library name. If set but unloadable this is a hard error; it never silently falls through. - The directory containing the current executable.
./target/pdfium/<platform>/libthen.../bin(the archives uselibeverywhere except Windows, which usesbin; both are probed on every platform) — the layout produced bycargo xtask fetch-pdfium.- The system loader's default search path, by bare library name.
For production, skip discovery entirely with Pdfium::load_from_path(...)
or Pdfium::load_from_directory(...) and an absolute path.
use firecrawl_pdfium::{Pdfium, RenderConfig};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Discovery chain: $PDFIUM_LIB_PATH -> exe dir -> ./target/pdfium -> system.
let pdfium = Pdfium::load()?;
let bytes = std::fs::read("document.pdf")?;
let doc = pdfium.load_document(bytes, None)?; // None = no password
println!("{} pages", doc.page_count());
let page = doc.page(0)?;
let rendered = page.render(&RenderConfig::new().dpi(144.0))?;
println!(
"{}x{} pixels, {} bytes/row, {:?}",
rendered.width(),
rendered.height(),
rendered.stride(),
rendered.format(),
);
// Map a pixel back into PDF page space (points, origin bottom-left):
let pt = rendered.transform().pixel_to_page((10.0, 10.0).into());
println!("pixel (10,10) is at ({:.2}, {:.2})pt", pt.x, pt.y);
Ok(())
}Encrypted documents fail with typed errors (Error::PasswordRequired,
Error::IncorrectPassword, Error::UnsupportedSecurity); garbage and
truncated input with Error::InvalidPdf; oversized render requests with
Error::RenderTooLarge, bounded by RenderConfig::max_output_bytes
(default 1 GiB) before any allocation happens. Text extraction is
bounded the same way: pages claiming more than one million characters
fail with Error::TextTooLarge before allocating (tunable via
PdfPage::text_with_limit). One inherent caveat: opening pages and text
runs PDFium's parser over attacker-controlled content, and PDFium's own
CPU/memory use during parsing cannot be capped from the embedding API —
for hard isolation of hostile input, run extraction in a separate process
(the same recommendation PDFium's other embedders follow).
Two spaces appear throughout: page space (PDF points, origin
bottom-left, y-up) and pixel space (rendered bitmap, origin top-left,
y-down). Every render carries the exact affine transform between them,
derived from PDFium's own device-to-page mapping at render time, so
/Rotate pages and extra render rotations behave exactly as PDFium
rendered them.
The OCR round trip — render a page, run OCR on the bitmap, map the resulting pixel boxes back into PDF page space — is one call:
use firecrawl_pdfium::{PageRect, PixelRect, RenderedPage};
/// Maps an OCR word box (bitmap pixels, origin top-left, y-down) into
/// PDF page space (points, origin bottom-left, y-up).
fn word_box_to_page(rendered: &RenderedPage, word: PixelRect) -> PageRect {
rendered.transform().pixel_rect_to_page(word)
}The transform is plain data on the owned render result: it stays valid
after the page, the document, and even the library handle are gone.
PdfPage::transform_for(&config) computes the same transform without
rendering, and PageTransform::page_rect_to_pixel maps the other way
(for example, to highlight extracted text on the bitmap).
PDFium is single-threaded; this crate makes it safe, not parallel.
Every FFI call is serialized through one process-wide mutex, which is what
makes Pdfium, PdfDocument, and PdfPage sound as Send + Sync — use
them from any thread, but expect calls to queue. Owned results
(RenderedPage, PageText, PageTransform) involve no lock at all.
For CPU-bound throughput, shard documents across processes — PDFium upstream's own recommendation. One process per core with a work queue saturates hardware without any unsound shortcuts.
| Platform | Status |
|---|---|
macOS arm64 (mac-arm64) |
Tier 1 — CI-tested |
macOS x64 (mac-x64) |
Tier 1 — CI-tested |
Linux x64, glibc (linux-x64) |
Tier 1 — CI-tested |
Linux arm64, glibc (linux-arm64) |
Tier 1 — CI-tested |
Windows x64 (win-x64) |
Tier 1 — CI-tested |
| Windows arm64, Linux musl, other upstream builds | Expected to work, not CI-tested |
| WebAssembly, Android, iOS | Not yet supported |
Static linking is not yet available (see below); the crate always loads a shared library at runtime.
Reference numbers from real-world documents, measured 2026-08-10 on Apple
Silicon (macOS arm64) against the pinned PDFium build (chromium/7988).
Each figure is the median wall time to open a page and render it to an
owned RGBA buffer at 200 DPI (page open included, document load
excluded), best of two serial passes, single-threaded:
| Document class | Pages timed | Median ms/page | Peak RSS |
|---|---|---|---|
| Light text digest | 6 | 4.8 | 40 MB |
| Text-dense book pages | 25 | 4.9 | 39 MB |
| Mixed content, government forms | 25 | 10.0 | 135 MB |
| Image-heavy brochure | 13 | 40.2 | 91 MB |
| Image-heavy brochure @ 300 DPI | 13 | 46.0 | 120 MB |
Document load itself is sub-millisecond to a few milliseconds for files in
the 100 KB–7 MB range. Three-channel output (PixelFormat::Bgr8) performs
within ±5% of RGBA. Remember the concurrency model when extrapolating:
renders serialize within a process, so per-process throughput is
single-core — scale with processes.
Reproduce locally with cargo xtask fetch-pdfium && cargo bench --bench render (criterion, fixture-based) or an equivalent harness over your own
corpus; absolute numbers vary with hardware and document complexity.
Implemented and tested: rendering (scale / DPI / fixed-dimension / fit sizing; BGRA, RGBA, BGR, grayscale outputs; background colors; extra rotation; anti-aliasing toggles; output-size limits), text extraction with per-character geometry, AcroForm field display, pixel/page coordinate transforms, document metadata, permissions, page labels, and typed errors for encrypted/malformed input.
Deliberately deferred (see the design document for the list and rationale): PDF editing or saving, XFA forms, JavaScript, form field values and programmatic filling, text search, annotations API, outlines/attachments, progressive rendering, and static linking. PDFium's default builds disable V8 and XFA, which matches this crate's scope.
See docs/VERSIONING.md for the full policy and the
per-release compatibility table. The short version: this crate follows
SemVer (pre-1.0: breaking changes bump the minor version) and currently
pins upstream release chromium/7988 (PDFium 153.0.7988.0) for its tested
binaries. At runtime, any PDFium build that exports the symbols this
crate binds will work; a library missing one fails fast at
Pdfium::load() with LoadError::MissingSymbol naming the symbol — never
with undefined behavior at call time.
Rust 1.77. MSRV bumps are minor version changes and the MSRV stays at least six months behind current stable; see docs/VERSIONING.md.
The crate is licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option. The crates.io package is pure Rust and ships no binaries.
The PDFium binaries distributed through this repository's native-v*
releases bundle PDFium and its statically linked third-party components,
all under permissive licenses — see
THIRD-PARTY-NOTICES.md for the complete list.
Those binaries include FreeType, whose license requires this credit:
Portions of this software are copyright © The FreeType Project (www.freetype.org). All rights reserved.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
- The PDFium project and the Chromium team, for the engine itself.
- bblanchon/pdfium-binaries, whose weekly builds this crate pins and redistributes.
- The wider ecosystem of PDFium bindings across languages, whose years of prior art informed this design.