Skip to content

Repository files navigation

firecrawl-pdfium

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.

CI crates.io docs.rs MSRV license

Why this crate

  • 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 sys module 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 + Sync because of that discipline, with the invariants written down next to each unsafe 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.

Install

cargo add firecrawl-pdfium

The 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).

Getting a PDFium binary

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-rs

How the library is found

Pdfium::load() tries, in order — first hit wins:

  1. The PDFIUM_LIB_PATH environment 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.
  2. The directory containing the current executable.
  3. ./target/pdfium/<platform>/lib then .../bin (the archives use lib everywhere except Windows, which uses bin; both are probed on every platform) — the layout produced by cargo xtask fetch-pdfium.
  4. 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.

Quickstart

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).

Coordinate mapping

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).

Concurrency

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 support

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.

Performance

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.

Feature status and limitations

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.

Versioning and PDFium compatibility

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.

Minimum supported Rust version

Rust 1.77. MSRV bumps are minor version changes and the MSRV stays at least six months behind current stable; see docs/VERSIONING.md.

License

The crate is licensed under either of

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.

Acknowledgements

  • 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.

About

Safe, self-contained Rust bindings for PDFium: open, inspect, and render PDFs to owned pixel buffers.

Topics

Resources

Contributing

Security policy

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages