Skip to content

Instantly share code, notes, and snippets.

@hjanuschka
Last active February 2, 2026 09:57
Show Gist options
  • Select an option

  • Save hjanuschka/4b23e2067344b88fe27a8dd4a2a6e048 to your computer and use it in GitHub Desktop.

Select an option

Save hjanuschka/4b23e2067344b88fe27a8dd4a2a6e048 to your computer and use it in GitHub Desktop.
Design Doc: JPEG XL (JXL) image support in PDFium (Rust-only)

One-page overview: JPEG XL (JXL) image decoding in PDFium (jxl-rs, Rust-only)

Overview

Add JPEG XL (JXL) image decoding support to PDFium using jxl-rs, a pure Rust decoder. This enables PDFs that embed JXL-compressed image XObjects (per the upcoming PDF Association / ISO standardization work) to render correctly in PDFium.

JXL benefits:

  • 30–50% better compression than JPEG (typical for photographic content)
  • Lossless JPEG transcoding (recompress JPEG sources without generation loss)
  • Progressive decoding
  • Animation support in the format

PDF behavior:

  • PDFs do not have an animation model for image XObjects; therefore animated JXL must render as the first frame only (deterministic output).

Security rationale:

  • Using a Rust decoder provides memory safety guarantees for image parsing/decoding, reducing attack surface compared to C/C++ decoders.

Platforms

PDFium runs wherever Chromium/PDFium runs. At minimum (Chromium distribution):

  • macOS
  • Windows
  • Linux
  • ChromeOS
  • Android
  • Android WebView

(For standalone embedders, platform coverage depends on the adopted Rust toolchain/build policy.)

Team

  • Author: Helmut Januschka

Bug

  • TBD (file a PDFium/Chromium bug for “JXL-in-PDF decode via jxl-rs”)

CL

  • TBD

Code affected (expected)

  • PDF image filter dispatch: CPDF_DIB::CreateDecoder() adds a new JXL filter branch
  • New codec glue: LoadJxlBitmap() (whole-image decode analogous to existing LoadJpxBitmap())
  • Build configuration:
    • new buildflag(s) like pdf_enable_rust and pdf_enable_jxl_decode
    • GN plumbing to build Rust + cxx bridge for standalone PDFium
  • Third-party Rust crates: vendored jxl-rs + dependency closure (for standalone PDFium embedders)
  • Testing/fuzzing:
    • regression PDFs containing JXL images
    • fuzz target(s) for the JXL decode entry point

Design Doc: JPEG XL (JXL) image support in PDFium (Rust-only)

Status: Draft (investigation + proposal; no code changes yet)

Last updated: 2026-02-02

Authors / owners: (fill in)

Reviewers: (fill in)

Related:


1. Summary

Add support for embedding and decoding JPEG XL (JXL) images in PDFs rendered by PDFium.

Key constraints:

  • Rust-only decoder: must use jxl-rs. C++ libjxl is not an option.
  • Animation: if the embedded JXL is animated, PDFium renders only the first frame (PDF image XObjects are not animated).
  • Embedders: PDFium is used standalone by external embedders; therefore we likely need a standalone PDFium Rust toolchain story and an in-tree vendored copy of the required Rust crates.

2. Goals and non-goals

2.1 Goals (checkable)

  • A PDF with an image XObject using the standardized JXL filter renders correctly in PDFium.
  • Animated JXL is handled deterministically by rendering frame 0 only.
  • Decoding is robust against malicious inputs (pixel limits, allocation guards, timeouts where applicable).
  • Unit/regression tests exist for:
    • basic decode
    • truncated/corrupt streams
    • very large dimensions / allocation refusal
    • animated file → first frame rendered
  • Fuzz coverage exists for the new JXL decoding entry points.

2.2 Non-goals

  • Not adding any encoding support (PDF creation with JXL is out of scope for PDFium).
  • Not supporting JXL animation playback in PDFs.
  • Not integrating C++ libjxl (explicitly disallowed).
  • Not solving broader Chromium Skia/libjxl migration.

3. Background

3.1 Current PDFium state

PDFium decodes PDF image XObjects via CPDF_DIB, with filter dispatch in:

  • pdfium/core/fpdfapi/page/cpdf_dib.cppCPDF_DIB::CreateDecoder()

Supported image filters today include /DCTDecode (JPEG), /JPXDecode (JPEG2000), /JBIG2Decode, etc. There is no JXL filter recognized.

A repository search shows no integration with JXL decoders. The only JXL mention found in this checkout is a TIFF compression constant in third_party/libtiff, unrelated to PDF image XObjects.

3.2 Chromium reference implementation (how it’s done elsewhere)

Chromium recently integrated JXL decoding in Blink using the Rust crate jxl-rs via a cxx bridge:

  • //third_party/rust/jxl/v0_3/wrapper exposes C++-friendly APIs.
  • Blink consumes it in JXLImageDecoder.

This is not directly reusable in PDFium (different image pipeline), but it is the reference for:

  • how to vend and build jxl-rs in-tree,
  • safe decoding limits,
  • the C++/Rust boundary design.

4. Constraints and requirements

4.1 PDF-level spec dependency

We need the standard’s definition of how JXL is represented in PDF:

  • Filter name (e.g. /JXLDecode vs something else)
  • DecodeParms (if any)
  • Interaction with /ColorSpace, embedded ICC profiles, and alpha/transparency

Plan: implement once the spec is concrete; until then, keep the filter string and semantics behind a feature flag and/or allow experimental builds.

4.2 Rust in standalone PDFium ("rust-ification")

If PDFium should support JXL when embedded outside Chromium, PDFium needs a supported way to:

  • build Rust code (toolchain policy)
  • vendor Rust crates (no network at build time)
  • link Rust + C++ (via cxx or equivalent)

This is a prerequisite for a Rust-only codec.


5. Proposed approach

5.1 High-level architecture

Add a new PDF image filter decoder path:

  1. PDF parser sees an image XObject with Filter = the standardized JXL filter name.
  2. CPDF_DIB::CreateDecoder() dispatches to a new function LoadJxlBitmap() (analogous to the existing LoadJpxBitmap() for JPEG2000).
  3. LoadJxlBitmap() decodes the JXL codestream via jxl-rs into a CFX_DIBitmap.
  4. PDFium’s rendering pipeline consumes the decoded bitmap as it would for other image formats.

5.2 Decode strategy (whole-image decode)

Use a whole-image decode into a cached bitmap (like JPXDecode) rather than streaming scanlines because:

  • the current jxl-rs wrapper offers “decode frame into a buffer” APIs,
  • PDFium already supports whole-image caching for formats that don’t map well to incremental scanline decode.

5.3 Animation handling

If the JXL has multiple frames:

  • decode frame 0 only
  • ignore loop counts / subsequent frames

Rationale: PDF image XObjects are static.

5.4 Output pixel format

Prefer decoding to BGRA8 (where possible):

  • aligns with many PDFium paths and avoids per-pixel swizzling
  • supported by the Chromium jxl-rs wrapper API (Bgra8)

6. Detailed design

6.1 New filter name handling

Implementation needs the filter name from the spec. Placeholder here:

  • "JXLDecode" (TBD)

Where to hook:

  • CPDF_DIB::CreateDecoder()
    • add a new branch similar to the existing JPXDecode branch

6.2 New decoder entry point: LoadJxlBitmap()

Add a function similar in role to LoadJpxBitmap():

Responsibilities:

  • Parse enough headers to get width/height and sanity-check them.
  • Enforce pixel limit to prevent decompression bombs.
  • Decode frame 0 to BGRA8 buffer.
  • Return a CFX_DIBitmap (cached bitmap) or failure.

6.3 Rust/C++ boundary

Two deployment scenarios:

A) Chromium-embedded PDFium (fastest path)

  • Depend on Chromium’s existing Rust crate targets:
    • //third_party/rust/jxl/v0_3/wrapper:jxl_rs
  • Follow Blink’s example for cxx bridge integration.

B) Standalone / embeddable PDFium (required for external users)

PDFium needs its own vendored copy of the crates:

  • Add under pdfium/third_party/rust/... (exact layout TBD)
  • Add GN templates / toolchain support needed to build Rust + cxx

Note: This is the biggest “pre-step” and should be treated as a separate milestone (see rollout plan).

6.4 Limits, safety, and resource usage

Minimum safety features required:

  • Pixel limit: cap decoded pixels (and/or pixels×channels) before allocation.
  • Dimension sanity: reject absurd width/height values early.
  • Buffer size checks: ensure width * height * bytes_per_pixel cannot overflow.
  • Fail closed: invalid/truncated streams should fail decoding rather than produce partially initialized buffers.

Where to take guidance:

  • Blink’s JXLImageDecoder has a clear threat model (malicious images) and uses pixel limits.

6.5 Color management / ICC

This depends on the PDF spec’s mapping rules:

Possible outcomes:

  • If embedded ICC is authoritative: create/attach an ICC-based colorspace for the image.
  • If PDF /ColorSpace is authoritative: decode into a representation compatible with that colorspace (may require wrapper enhancements).

Plan: implement the minimal viable behavior first (likely decode to sRGB-ish BGRA), then refine once the spec clarifies.

6.6 Alpha / transparency

Open question until spec confirms semantics.

Options:

  • If inline alpha is allowed: decode BGRA and propagate alpha as part of the bitmap.
  • If PDF requires a separate /SMask: we may need to split channels and synthesize a soft mask.

Initial plan: decode and preserve alpha channel in the bitmap if present; if PDF pipeline requires /SMask, follow up with a spec-compliant mapping.


7. Alternatives considered

7.1 Use Skia’s JXL codec (libjxl)

Rejected: depends on C++ libjxl.

7.2 Integrate C++ libjxl directly

Rejected: explicitly disallowed.


8. Testing strategy

  • Unit tests

    • Add small JXL test vectors and embed them into PDFs as image XObjects using the standardized filter name.
    • Verify pixel output / rendering results.
  • Animated JXL test

    • Use a small multi-frame JXL.
    • Verify output equals expected first frame.
  • Robustness tests

    • Truncated stream
    • Corrupt headers
    • Huge dimensions that must be rejected
  • Fuzzing

    • Add a fuzzer entry feeding arbitrary bytes into the JXL decode path with strict resource limits.

9. Rollout / milestone plan (concrete checklist)

Milestone 0: Specification + samples

  • Obtain/track the actual PDF Association/ISO draft text for JXL-in-PDF.
  • Identify the exact filter name and any DecodeParms.
  • Collect sample PDFs and/or write a generator to embed JXL codestreams into a PDF image XObject.

Milestone 1: Standalone PDFium Rust toolchain policy ("rust-ification")

  • Decide policy knobs:
    • pdf_enable_rust (default off)
    • pdf_enable_jxl_decode (requires pdf_enable_rust)
  • Choose toolchain strategy for embedders:
    • minimum supported Rust version and docs
    • build-with-bundled-toolchain vs system Rust
  • Add GN build support for Rust + cxx in PDFium:
    • rust_static_library / cargo_crate equivalents
    • cxx C++ side build/link support
  • Vendor jxl-rs crates under pdfium/third_party/rust/:
    • include dependency closure
    • verify licensing and add license metadata
  • Add CI configuration that builds PDFium with pdf_enable_rust=true.

Milestone 2: Minimal decode implementation (frame 0)

  • Add filter dispatch in CPDF_DIB::CreateDecoder().
  • Implement LoadJxlBitmap() whole-image decode to BGRA8.
  • Enforce pixel and allocation limits.
  • Render animated JXL as frame 0.

Milestone 3: Correctness, color, alpha

  • Implement spec-correct handling of ICC / ColorSpace interactions.
  • Implement spec-correct alpha handling (inline vs /SMask).
  • Expand test suite to cover these cases.

Milestone 4: Security hardening + fuzz

  • Add dedicated fuzz target(s).
  • Verify no unbounded allocations with fuzz corpora.
  • Add regression tests for any discovered issues.

10. Open questions

  • What is the exact standardized filter name and DecodeParms?
  • How should embedded ICC profiles interact with PDF /ColorSpace?
  • What is the intended alpha model (inline alpha vs /SMask mapping)?
  • Does the standard require rejecting animated codestreams, or explicitly allow “use first frame”? (We propose first-frame behavior.)
  • What is the acceptable policy for introducing a Rust toolchain into standalone PDFium for third-party embedders?

Appendix A: Current code pointers (PDFium)

  • Image filter dispatch:
    • pdfium/core/fpdfapi/page/cpdf_dib.cpp (CPDF_DIB::CreateDecoder())
  • JPEG2000 whole-image decode precedent:
    • LoadJpxBitmap()CJPX_Decoder (OpenJPEG)

Appendix B: Chromium reference pointers (JXL via Rust)

  • Rust wrapper:
    • //third_party/rust/jxl/v0_3/wrapper (cxx bridge)
  • Blink consumer:
    • //third_party/blink/renderer/platform/image-decoders/jxl/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment