# Pdfium Core Architecture

> Use when starting a pdfium-render project, choosing an integration approach, or debugging confusing lifetime and threading errors, and you need the conceptual map of how the crate is built. Prevents the four wrong-mental-model mistakes: treating pdfium-render as a pure-Rust PDF parser, expecting compile-time linking of PDFium, storing a parent handle and a child handle together in one struct, and expecting multi-threading to speed up rendering. Covers the PDFium-to-pdfium-render wrapper relationship, the late run-time binding model, the lifetime-bound ownership tree (Pdfium to PdfDocument to PdfPages to PdfPage), the thread-safety stance, the 0.8.x to 0.9.x changes, and when to drop to raw FFI. Keywords: pdfium-render, PDFium, architecture, ownership tree, late binding, run-time binding, RAII wrapper, lifetime error, Send Sync, thread safety, thread_safe feature, raw FFI, FPDF_, PdfiumLibraryBindings, Pdfium, PdfDocument, PdfPage, "library not found", "cannot return value referencing local variable", "borrowe

- Skill: `impertio-studio/pdfium-core-architecture` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add impertio-studio/pdfium-core-architecture`
- Raw SKILL.md: https://api.skillmd.com/api/skills/impertio-studio/pdfium-core-architecture/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: MIT
- Author: Impertio-Studio (https://skillmd.com/u/impertio-studio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/impertio-studio/pdfium-core-architecture

---


# pdfium-render Architecture

This is the conceptual entry skill for the pdfium-render skill package. It is
a map, not an API reference. It explains how the crate is built so that every
later decision (binding, memory, rendering, text, objects) is made with the
correct mental model. For exact API signatures see the bindings-setup and
memory skills cross-referenced at the bottom.

## What pdfium-render Is

`pdfium-render` is an idiomatic high-level Rust wrapper around **PDFium**, the
C++ PDF library that Google's Chromium project uses for PDF rendering. PDFium
exposes a flat C FFI surface of `FPDF_*` functions (for example
`FPDF_LoadDocument`, `FPDFText_GetText`, `FPDF_RenderPageBitmap`).

`pdfium-render` does **not** reimplement PDF parsing. It binds to a compiled
PDFium library and presents a safe, RAII-driven Rust API on top of the raw FFI.
Every operation (load, render, extract text, edit, save) is ultimately a call
into PDFium.

ALWAYS treat pdfium-render as a binding layer. NEVER assume it can open or
parse a PDF without a PDFium library present at run time, because all parsing
happens inside PDFium's C++ code.

## Quick Reference: The Mental Model

```text
A pdfium-render program has exactly three stages:

  1. BIND    Locate and load a PDFium library at run time.
             -> Box<dyn PdfiumLibraryBindings>
  2. WRAP    Hand the bindings to the root object.
             -> Pdfium
  3. USE     Load documents, walk pages, render, extract, edit, save.
             -> PdfDocument<'a> -> PdfPages<'a> -> PdfPage -> objects/text/...

The root object (Pdfium) must outlive everything produced from it.
```

| Concern | One-line answer | Detail skill |
|---------|-----------------|--------------|
| Where does the library come from | Loaded at run time, not linked at build time | pdfium-core-bindings-setup |
| Who owns the bindings | The `Pdfium` root object | pdfium-core-bindings-setup |
| Why do lifetimes appear everywhere | Every child handle borrows its parent | pdfium-core-memory |
| Can I render on many threads | No speedup; PDFium is not thread safe | this skill, section 4 |
| Can I call `FPDF_*` directly | Yes, through the bindings trait | pdfium-core-raw-ffi |

## 1. The Wrapper Relationship

Two distinct things share the name "pdfium":

- **PDFium** : Google's C++ library. It does the actual PDF work. It is
  distributed as a compiled binary (`libpdfium.so`, `libpdfium.dylib`,
  `pdfium.dll`, or a static `libpdfium.a`).
- **pdfium-render** : the Rust crate. It is a thin, safe, idiomatic layer
  that calls into PDFium and adds RAII cleanup, `Result`-based error
  handling, and Rust ownership semantics.

ALWAYS keep this split in mind when diagnosing problems: a missing-library or
missing-symbol error is a PDFium-binary problem (see pdfium-errors-binding),
while a borrow-checker or lifetime error is a pdfium-render API-shape problem
(see pdfium-core-memory).

## 2. Late Run-Time Binding

The single most important architectural decision is **late (run-time)
binding**. `pdfium-render` does not link PDFium at compile time by default.
Instead it locates and loads a PDFium library while the program is running.

Binding produces a `Box<dyn PdfiumLibraryBindings>`. `Pdfium::new(bindings)`
then wraps that boxed trait object into the `Pdfium` root.

Why late binding exists:

1. **WASM compatibility** : a Rust application using pdfium-render can be
   compiled to WASM and run in a browser alongside a separately packaged WASM
   build of PDFium. Compile-time linking cannot express that.
2. **Flexible library selection** : the same compiled Rust binary can use a
   system-installed PDFium, a PDFium shipped next to the executable, or a
   statically linked one, decided at run time.
3. **Idiomatic error handling** : a missing PDFium library becomes a
   `Result::Err`, not a load-time crash.

The `static` feature plus `bind_to_statically_linked_library()` opt back in to
compile-time linking when a single self-contained executable is required. That
is the exception; run-time binding is the default and the design center.

ALWAYS perform binding exactly once per process and reuse the `Pdfium`
instance. NEVER re-bind on every request (for example per HTTP request),
because loading the library is slow and wasteful (see anti-patterns).

## 3. The Ownership Tree

`pdfium-render` models the PDF object hierarchy as a strict tree of
**lifetime-bound handles**. Each handle borrows from its parent. A child can
never outlive its parent.

```text
Pdfium                         root : owns Box<dyn PdfiumLibraryBindings>
  |
  +-- PdfDocument<'a>          'a is tied to the &Pdfium that created it
        |
        +-- PdfPages<'a>       collection of pages (pages(), pages_mut())
        |     +-- PdfPage      a single page
        |           +-- objects()      -> &PdfPageObjects<'a>
        |           +-- text()         -> Result<PdfPageText, PdfiumError>
        |           +-- annotations()  -> &PdfPageAnnotations<'a>
        |           +-- boundaries()   -> &PdfPageBoundaries<'a>
        |
        +-- form()        -> Option<&PdfForm>
        +-- metadata()    -> document metadata collection
        +-- attachments() -> document attachments collection
        +-- bookmarks()   -> document bookmark tree
        +-- permissions() -> document permission flags
        +-- fonts()       -> document font collection
        +-- signatures()  -> document signature collection
```

The lifetime relationship is visible in the signatures. `Pdfium::load_pdf_from_file`
is declared as:

```rust
pub fn load_pdf_from_file<'a>(
    &'a self,
    path: &(impl AsRef<Path> + ?Sized),
    password: Option<&str>,
) -> Result<PdfDocument<'a>, PdfiumError>
```

The returned `PdfDocument<'a>` carries the same `'a` as the `&'a self`
reference to `Pdfium`. The compiler therefore guarantees the document cannot
outlive the `Pdfium` root. The same pattern repeats down the tree: a `PdfPage`
borrows from its `PdfDocument`, page objects borrow from their `PdfPage`.

ALWAYS keep the `Pdfium` root alive in an enclosing scope for as long as any
document, page, or page object derived from it is in use. NEVER store a parent
handle and a child handle together in the same struct, because that is a
self-referential borrow the compiler rejects (see pdfium-core-memory for the
correct pattern and anti-patterns for the failure).

## 4. Thread-Safety Stance

PDFium makes no guarantees about thread safety and must be assumed **not**
thread safe. The PDFium authors explicitly recommend parallel **processing**,
not multi-threading, to handle multiple documents at once.

`pdfium-render` exposes the optional `thread_safe` feature (enabled by
default). It works as follows:

- It locks every call into PDFium behind a single global mutex. Each thread
  must acquire that mutex before any PDFium call.
- This **prevents segfaults** caused by concurrent access to PDFium's internal
  state.
- It provides **no performance benefit**. Because every call serializes
  through one mutex, two threads rendering pages run no faster than one.

Version note: release **0.9.0** additionally implements `Send` and `Sync` for
all object instances, so handles can be moved between threads. `Send`/`Sync`
plus the `thread_safe` mutex make multi-threaded code *sound*, but they do not
make it *fast*.

```text
Decision: I need to process many PDFs faster.

  Multi-threading with thread_safe ........ SOUND, but NO speedup (mutex).
  Multi-threading without thread_safe ..... UNSOUND, random segfaults.
  Process-level parallelism ............... CORRECT : one Pdfium per process.
```

ALWAYS use process-level parallelism (a separate process, each with its own
`Pdfium` instance) when real throughput matters. NEVER disable the
`thread_safe` feature while still calling PDFium from multiple threads,
because concurrent access to PDFium causes random segfaults. The canonical
`examples/thread_safe.rs` in the repository demonstrates this.

## 5. When To Reach For Raw FFI

The safe high-level API is built on top of the `PdfiumLibraryBindings` trait,
which is the Rust representation of PDFium's complete `FPDF_*` C surface. The
trait is reachable, so existing C or C++ PDFium code can be ported while
keeping late binding and WASM support.

Reach for raw FFI ONLY when:

- A specific `FPDF_*` function has no safe wrapper yet in pdfium-render.
- You are porting existing C or C++ PDFium code and want a direct mapping.

Version note: in **0.9.0** every `FPDF_*` function on `PdfiumLibraryBindings`
is marked `unsafe`, matching the inherent unsafety of C FFI. In 0.8.x they
were safe-looking. In **0.9.2** `PdfiumLibraryBindingsAccessor` became public.

ALWAYS prefer the safe high-level API. NEVER drop to raw `FPDF_*` calls for
work the safe API already covers, because raw calls bypass the RAII cleanup
and lifetime guarantees this architecture provides. For the raw-FFI mechanics
see pdfium-core-raw-ffi.

## 6. Version Awareness (0.8.x to 0.9.x)

This package targets both the 0.8.x and 0.9.x lines. The default API surface
is **0.9.x**. The dominant architectural event is the **0.9.0 cleanup
release**, which removed every previously deprecated item, simplified lifetime
handling across all object instances, implemented `Send`/`Sync`, changed
`PdfPageIndex` from `u16` to `c_int`, and marked all `FPDF_*` bindings
`unsafe`.

The practical consequence for architecture: code written against old
tutorials uses removed names. The most common upgrade traps are
`PdfBitmapConfig` (use `PdfRenderConfig`), `PdfPage::get_bitmap()` (use
`render_with_config()`), `as_bytes()` (use `as_raw_bytes()` /
`as_rgba_bytes()`), `set_matrix()` (use `apply_matrix()`),
`load_pdf_from_bytes()` (use `load_pdf_from_byte_slice()` /
`load_pdf_from_byte_vec()`), and `Pdfium::get_bindings()` (use `bindings()`).

ALWAYS write new code against the 0.9.x API names. NEVER copy an example using
a removed 0.8.x name without updating it. The full removed-name table lives in
references/anti-patterns.md and in pdfium-core-bindings-setup.

## Decision Tree: Integration Approach

```text
How will the PDFium binary be delivered?

  Native app, simplest path
    -> Dynamic binding. Ship libpdfium.{so,dylib,dll} next to the executable.
       Bind with the fallback pattern or Pdfium::default().
       See pdfium-core-bindings-setup.

  Single self-contained executable required
    -> Static linking. Enable the `static` feature, set PDFIUM_STATIC_LIB_PATH,
       call bind_to_statically_linked_library().
       See pdfium-core-bindings-setup.

  Browser / WebAssembly target
    -> Run-time binding to a separately packaged WASM PDFium module.
       Use the paulocoutinhox build (growable heap).
       See pdfium-impl-wasm.

  Porting existing C / C++ PDFium code
    -> Raw FFI through PdfiumLibraryBindings, then migrate to the safe API.
       See pdfium-core-raw-ffi.
```

## ALWAYS / NEVER

- ALWAYS bind PDFium exactly once per process and reuse the `Pdfium`
  instance for that process's lifetime.
- ALWAYS keep the `Pdfium` root alive in an enclosing scope for as long as
  any document or page derived from it is used.
- ALWAYS use process-level parallelism for throughput; one `Pdfium` per
  process.
- ALWAYS write new code against 0.9.x API names; the default API surface of
  this package is 0.9.x.
- NEVER treat pdfium-render as a pure-Rust PDF parser; it requires a PDFium
  binary at run time.
- NEVER store a parent handle (`Pdfium`, `PdfDocument`) together with a child
  handle (`PdfPage`, page objects) in the same struct; it is a
  self-referential borrow the compiler rejects.
- NEVER expect the `thread_safe` mutex to speed up rendering; it only
  prevents segfaults.
- NEVER disable `thread_safe` while still calling PDFium from multiple
  threads.
- NEVER re-bind the library per request; binding is slow.

## Cross-References

- **pdfium-core-bindings-setup** : the three bind functions, the fallback
  pattern, `Pdfium::default()`, feature flags, obtaining PDFium binaries,
  per-platform library file names, build environment variables.
- **pdfium-core-memory** : the lifetime model in depth, the correct way to
  hold handles, drop order, and how to avoid self-referential structs.
- **pdfium-core-raw-ffi** : the `PdfiumLibraryBindings` trait, calling
  `FPDF_*` directly, and the 0.9.0 `unsafe` change.
- **pdfium-errors-binding** : diagnosing a failed bind (missing file, wrong
  architecture, feature mismatch, missing C++ runtime).

## Reference Files

- `references/methods.md` : API signatures of the architectural types
  (`Pdfium`, `PdfiumLibraryBindings`, `PdfDocument`, `PdfPages`, `PdfPage`)
  with 0.8.x / 0.9.x version annotations.
- `references/examples.md` : verified Rust code showing the three-stage
  program, walking the ownership tree, and the thread-safety pattern.
- `references/anti-patterns.md` : real failures from the architecture (self-
  referential structs, dropped roots, threading misuse, removed names) with
  why each fails and the fix.

## Sources

All API names and signatures in this skill were verified on 2026-05-20 via
WebFetch against the sources in the package `SOURCES.md`:

- `https://docs.rs/pdfium-render/latest/pdfium_render/prelude/struct.Pdfium.html`
- `https://docs.rs/pdfium-render/latest/pdfium_render/prelude/struct.PdfDocument.html`
- `https://docs.rs/pdfium-render/latest/pdfium_render/prelude/struct.PdfPage.html`
- `https://docs.rs/pdfium-render/latest/pdfium_render/prelude/struct.PdfPages.html`
- `https://docs.rs/pdfium-render/latest/pdfium_render/prelude/trait.PdfiumLibraryBindings.html`
- `https://github.com/ajrcarey/pdfium-render` (README, design philosophy,
  thread-safety statement, version history).

