djvu-rs
Read, render, convert, and create DjVu files. Pure-Rust library with a CLI,
WebAssembly, and Python bindings — on crates.io,
PyPI, and npm
as djvu-rs. MIT licensed, no GPL dependencies, written from the public DjVu v3
specification.
| Your task | How |
|---|---|
| Convert DjVu → PDF, EPUB, TIFF, PNG, CBZ | djvu render or djvu_to_pdf / djvu_to_epub / djvu_to_tiff |
| Extract text (plain, hOCR, ALTO XML) | djvu text or page.text(), to_hocr / to_alto |
| Render pages to RGBA pixels | render_pixmap — sync, async, or parallel |
| Build a zoomable viewer (tiles) | djvu_tile — cached, prefetchable, cancellable tile rendering |
| Show DjVu in the browser | WebAssembly bindings, incl. lazy HTTP-Range loading |
| Read DjVu from Python | pip install djvu-rs — PyO3 bindings |
| Create DjVu from images (PNG/JPEG/TIFF) | djvu encode or PageEncoder |
| Add an OCR text layer to a scan | djvu ocr (Tesseract) |
| Merge, split, edit documents | djvu merge / djvu split, DocumentEditor, DjVuDocumentMut |
| Stream huge books page-by-page | Lazy async loading — first pixel after ~29 KB of a 100 MB file |
Every Rust example below is a complete program, compiled as a doctest on every CI run — what you copy-paste is guaranteed to build against the current API.
Quick start
use ;
Text extraction
use DjVuDocument;
PDF export
Requires the pdf feature flag: djvu-rs = { version = "…", features = ["pdf"] }.
The PDF keeps selectable text, bookmarks, and hyperlinks, and embeds the
IW44/JB2 image data losslessly.
use ;
CLI
The djvu binary is enabled by the cli feature.
# Install
# Document info (--json for machine-readable output, --count for page count only)
# Inspect IFF chunk identities, offsets, sizes, and bundled component relationships
# Layered validation: structural, dependency, codec, and resource findings with
# stable codes (--strict makes warnings fail the exit code; --decode-pages adds
# full codec decodes; --limits gates size/page/pixel/memory budgets before decode)
# Semantic comparison of two documents: pages, text, annotations, metadata,
# bookmarks, and the component graph (--plane filters the compared planes)
# Render page 1 to PNG at 200 DPI
# Render all pages to a PDF, EPUB, or CBZ
# Render a single layer (mask, foreground, background), with optional rotation
# Extract text from page 2 (plain), or from all pages as hOCR / ALTO XML
# Merge documents into a bundled DJVM / extract a page range
# Preview safe cleanup as machine-readable JSON, or write an optimized copy
# (--max-ssim-loss is reserved for the planned archival re-encode; the current
# lossless cleanup is pixel-exact by construction and reports this)
# Encode an image (PNG, JPEG, or TIFF) into a single-page DjVu (bilevel JB2, lossless)
# TIFF input requires building/installing with --features tiff (cli alone does not enable it).
# Opt into a DjVuLibre-compatible G4/MMR mask for fax/scanner workflows
# Encode into a layered lossy DjVu (JB2 mask + IW44 background + FGbz foreground color)
# Use the conservative archival color profile
# Opt into adaptive mask segmentation for uneven scans
# Cap the IW44 background at a bits-per-pixel budget (smaller file, lower quality)
# Composite transparent PNG/TIFF pixels onto a solid colour (hex or white/black)
# Refuse ICC-profiled input instead of silently dropping the profile
# Encode a directory of images into a bundled DJVM with shared Djbz
# Embed TH44 color thumbnails while bundling (multi-page layered)
# Raw BZZ compression utilities
For single image input (PNG, JPEG, or TIFF), --quality lossless
luminance-thresholds the image into a JB2 mask and writes INFO + Sjbz.
--bilevel-codec smmr is an explicit single-image opt-in that writes a
DjVuLibre-compatible Smmr G4/MMR mask instead; it preserves the default JB2
path and is not available for directory bundles. The Smmr path is intended for
fax/scanner interoperability and is usually larger than JB2.
--quality quality uses the layered encoder (INFO + Sjbz + BG44... plus
FGbz when colored foreground is detected) for color input. --quality archival uses the same layered shape with a denser background sample grid.
Directory input supports all three profiles, and both directory paths share a
Djbz symbol dictionary across pages: lossless uses the shared-Djbz
multi-page JB2 path, while quality / archival bundle layered pages that
keep their own Sjbz, BG44, and optional FGbz chunks on top of the shared
dictionary. --shared-dict-pages sets the page-count threshold for promoting
a symbol into the shared dictionary on either path.
Layered quality / archival encodes default to fixed BT.601 thresholding.
--binarization sauvola opts into adaptive local thresholding for mixed or
uneven lighting; tune it with --sauvola-window and --sauvola-k.
--bg-inpaint fills fully masked background blocks from neighbouring unmasked
pixels, which can reduce dark boxes under heavy text strokes.
--block-classify routes photo and halftone blocks wholly to the background
layer instead of shredding them into mask speckle (mixed text+photo layouts);
pair it with --adaptive-bg-subsample, which densifies the background grid
where unmasked detail warrants it, so routed photos keep their detail. These
knobs are opt-in, only affect layered profiles, and do not change lossless
JB2 defaults.
Library callers can use the same controls with PageEncoder::with_segment_options.
For newly encoded pages, PageEncoder::with_metadata emits a METz chunk;
for existing documents, DjVuDocumentMut::page_mut(...).set_metadata(...)
performs a mutation while preserving untouched chunks. These are deliberately
separate fresh-encode and mutation APIs.
Python
=
=
= # or .to_numpy()
=
PyO3 bindings live in djvu-py/. Wheels track the crate version
(CPython 3.9–3.13 on manylinux/musllinux, macOS, and Windows). The bindings
cover the reading surface: open documents, render pages (including region and
progressive rendering, with zero-copy numpy/PIL paths), and extract the text
layer. Encode, mutation, and PDF/EPUB/TIFF export stay on the Rust crate / CLI
for now. See djvu-py/README.md and
docs/packaging.md.
WebAssembly
import init from 'djvu-rs';
await ;
console.log;
const doc = ;
console.log;
const page = doc.;
const pixels = page.; // Uint8ClampedArray, RGBA
const img = ;
ctx.;
The npm package ships TypeScript declarations plus scalar and simd128 wasm
artifacts; at runtime a tiny WebAssembly.validate() probe selects SIMD when
supported. Package versions match the Rust crate — see
docs/packaging.md.
To rebuild the package from this repository:
See examples/wasm/ for a complete drag-and-drop demo, and
examples/wasm/range_lazy.md for lazy loading
over HTTP Range requests (wasm-lazy feature) — the browser fetches only
the index plus the pages actually opened.
Advanced usage
TIFF export
Requires the tiff feature flag: djvu-rs = { version = "…", features = ["tiff"] }.
use ;
EPUB export
Requires the epub feature flag: djvu-rs = { version = "…", features = ["epub"] }.
Produces EPUB 3 with page images, an invisible text overlay, and bookmarks as
navigation.
use ;
hOCR and ALTO XML export
use ;
Async render
Requires the async feature flag: djvu-rs = { version = "…", features = ["async"] }.
The render entry points are synchronous and CPU-bound; run them on the
blocking thread pool with tokio::task::spawn_blocking so they stay off the
async runtime. The render error type stays the typed RenderError — there is
no wrapper enum.
use ;
async
For progressive (per-BG44-chunk) rendering, djvu_async::render_progressive_stream
yields a Stream of frames, each produced on the blocking pool.
Lazy async loading
Requires the async feature flag. The lazy loader keeps a seekable async
reader and fetches page/component byte ranges only when page_async(i) is
called. Parsed pages are cached as Arc<DjVuPage>.
use from_async_reader_lazy;
async
Supported shapes: single-page FORM:DJVU and bundled FORM:DJVM, including
shared DJVI dictionaries referenced via INCL. For browser-local !Send
readers on wasm32, use from_async_reader_lazy_local.
See examples/async_lazy_first_page.rs
for a native first-page latency probe and
examples/wasm/range_lazy.md for the HTTP
Range: bytes=start-end integration shape.
Serde support
Requires the serde feature flag: djvu-rs = { version = "…", features = ["serde"] }.
All public data types (DjVuBookmark, TextZone, MapArea, PageInfo, etc.) implement
Serialize and Deserialize.
use DjVuDocument;
image-rs integration
Requires the image feature flag: djvu-rs = { version = "…", features = ["image"] }.
use ;
use DynamicImage;
Tile rendering
For viewer engines: djvu_tile
renders a page as a display-space tile grid over the region renderer. Tile
pixels are byte-identical to the same rectangle of a full-page render, in any
request order.
use ;
use ;
render_tile_cached memoizes composited tiles per page; the cache is
tile-granular and controllable (tile_cache_usage, set_tile_cache_budget,
clear_tile_cache, invalidate_tile_region). render_tile_with +
TileRenderControls / TileCancelToken add progressive quality steps and
cooperative cancellation, and with the parallel feature prefetch_tiles /
prefetch_tiles_cancellable warm the cache in the background with a bounded
worker pool. The full contract lives in
docs/tile-rendering.md.
Encoding & low-level API
JB2 bilevel image encoder
use ;
IW44 wavelet encoder
use ;
Iw44EncodeOptions fields (all have sensible defaults):
| Field | Default | Description |
|---|---|---|
slices_per_chunk |
10 | Slices packed into each BG44/FG44 chunk |
total_slices |
100 | Total refinement slices to encode |
chroma_delay |
0 | Y slices before Cb/Cr encoding begins |
chroma_half |
false | Legacy no-op; IW44 v1.2 always emits full-resolution chroma |
Bookmark encoder
use ;
Annotation encoder
use ;
Indirect multi-page documents
Create an indirect DJVM index file that references per-page .djvu files:
use create_indirect;
Load an indirect document by resolving component files from a directory:
use DjVuDocument;
Applications that need the full DIRM component identity can use
DjVuDocument::parse_with_component_resolver. Its
ComponentResolver receives a ComponentId containing both the external name
and its ComponentKind (Page, Shared, or Thumbnail), and is called for
every directory entry. Page/shared/thumbnail FORM mismatches and resolver
failures surface as typed errors; shared Djbz dictionaries referenced by
INCL are connected to the parsed pages. See
docs/indirect-djvm-resolver.md.
Two mutation paths cover indirect documents:
DjVuDocumentMut::from_indirect_resolved resolves the component files and
rebundles them into a mutable bundled document, and IndirectRewritePlan
rewrites individual component files on disk while keeping the document
indirect (each file is renamed atomically, but the multi-file commit as a
whole is not transactional). Opening an indirect index directly with
DjVuDocumentMut::from_bytes and calling page_mut remains unsupported; see
docs/indirect-djvm-mutation.md.
The reverse direction is covered too: djvm::to_indirect splits a bundled
FORM:DJVM into an indirect index plus standalone component files, keeping
component ids, names, titles, and the document NAVM stable. Related
bundled-document operations in the same module: djvm::remove_pages deletes
pages with an explicit UnreachablePolicy (preserve or garbage-collect shared
components that lose their last including page),
djvm::dedup_shared_components merges byte-identical shared components, and
djvm::DjvmStreamWriter writes a bundle to any io::Write sink with memory
bounded to the spooled component being appended.
Typed document editing
DocumentEditor provides a versioned, typed operation list with a semantic
dry-run plan and validation of every operation before bytes are emitted. The
current schema covers page text, page annotations, page/document METa/METz
metadata, and bundled-document NAVM bookmarks:
use ;
use DjVuMetadata;
DocumentEditor::apply_to_path stages output beside the destination and
renames it only after validation, serialization, and sync succeed. The first
slice intentionally does not yet cover the declarative CLI, XMP, thumbnails,
page insertion/deletion/reordering/extraction, semantic diff, or multi-file
indirect-DJVM commits; those require separate operation and commit contracts.
With the serde feature, requests and plans are JSON-serializable using the
versioned schema.
Low-level IFF access
use parse_form;
OCR recognition backends
The supported OCR recognition path is the ocr-tesseract feature, which uses a
system Tesseract installation and tessdata files. Recognized text is embedded
into the output document as a compressed TXTz text layer, page by page:
# Requires Tesseract + the requested language data, e.g. eng.traineddata.
Library callers can attach recognized text at encode time instead, via
PageEncoder::with_ocr_text_layer (or with_text_layer for an existing
TextLayer).
ocr-onnx is experimental but now CLI-live (#693): --backend onnx runs the
full PP-OCR neural pipeline — DBNet text detection plus Cyrillic PP-OCRv5 CTC
line recognition (its pinned dictionary also covers Latin, digits, and
punctuation) assembled into a page → line → word text layer with heuristic
word rectangles. Models come only from the pinned manifest with mandatory
SHA-256 verification (docs/ocr-model-manifest.toml, fetched explicitly via
scripts/fetch_ocr_models.sh — weights are never committed and never
downloaded implicitly; directory override: DJVU_OCR_MODELS_DIR). The
--model flag is not used by this backend, and OcrOptions
(languages/dpi) are advisory and ignored. Recognition quality of the
pinned models is gated by a deterministic synthetic corpus with a recorded
CER/WER/IoU baseline (docs/ocr-model-metrics.md). ocr-neural is a placeholder only: CandleBackend now
returns a clear unsupported-backend error instead of constructing a backend that
always fails at recognition time. The compatibility feature name
ocr-neural-candle is a no-op and no longer pulls Candle/tokenizers into
--all-features builds.
Format coverage
Chunk-level coverage of the DjVu v3 format, for readers who need to know exactly what decodes and what encodes:
The fresh-encode versus existing-document mutation contract is expanded in
docs/writer-coverage.md.
| Format element | Decode | Encode |
|---|---|---|
IFF container (FORM:DJVU, FORM:DJVM) |
✓ zero-copy parser | ✓ |
JB2 bilevel images (Sjbz), shared dictionaries (Djbz via INCL) |
✓ ZP arithmetic coding + symbol dictionary | ✓ incl. multi-page shared Djbz |
IW44 wavelet images (BG44 / FG44) |
✓ planar YCbCr, multiple refinement chunks | ✓ color and grayscale |
G4/MMR fax images (Smmr, ITU-T T.6) |
✓ | ✓ (explicit BilevelCodec::Smmr / --bilevel-codec smmr) |
JPEG background/foreground (BGjp / FGjp) |
✓ | — (encoder emits IW44) |
Foreground palette (FGbz) |
✓ | ✓ (layered encoder) |
| BZZ compression (BWT + MTF + ZP) | ✓ | ✓ |
Text layer (TXTa / TXTz), zone hierarchy down to characters |
✓ | ✓ (incl. OCR injection) |
Annotations (ANTa / ANTz): hyperlinks, map areas, colors |
✓ | ✓ |
Bookmarks (NAVM) |
✓ | ✓ |
Multi-page directory (DIRM), bundled and indirect |
✓ | ✓ (DjVuLibre-clean directory v1) |
Thumbnails (TH44) |
✓ | ✓ (--thumbnails) |
Metadata (METa / METz) |
✓ | ✓ (PageEncoder::with_metadata; PageMut::set_metadata) |
Legacy standalone FORM:BM44 / FORM:PM44 files |
✓ | — |
| Unknown chunk IDs | preserved byte-exact for round-trip | n/a |
The codec internals are also published as standalone workspace crates for
focused consumers: djvu-iff, djvu-bzz,
djvu-bitmap, djvu-jb2,
djvu-pixmap, djvu-iw44, and
djvu-zp. All of them (and the codec modules of the main
crate) are no_std-compatible with alloc only, and are continuously fuzzed
via in-tree libFuzzer targets and OSS-Fuzz project files.
Status & limitations
Honest boundaries, so you can decide fast:
- Library + CLI, not a viewer. There is no GUI; the WASM demo is the closest thing to one.
- Python bindings cover the reading surface only. Open, render, and text extraction ship in the PyPI wheels; encode, mutation, and PDF/EPUB/TIFF export stay on the Rust crate / CLI for now.
- Indirect DJVM mutation is indirect-only via two paths.
DjVuDocumentMut::from_bytes+page_muton an indirect index errors; usefrom_indirect_resolved(rebundles) orIndirectRewritePlan(rewrites component files; per-file atomic, whole-commit not transactional). - Lazy async loading does not cover indirect DJVM — bundled
FORM:DJVMand single-pageFORM:DJVUonly; indirect returns a cleanUnsupportederror. create_indirectdoes not emit sharedDJVIdictionary components — build a bundled document withdjvu mergewhen pages share a dictionary, or convert an existing bundled document withdjvm::to_indirect, which preserves shared components.- Encoder size parity is corpus- and profile-dependent. Run the
reproducible
encoder parity scorecardto compare the same raster through DjVuLibre 3.5.29'sc44/cjb2and the archival-safePageEncoderprofiles. The 2026-07-16 snapshot ranges from 1.025–1.040×c44for IW44 photo pages — at matched-or-better fidelity (decoded PSNR/SSIM meet or exceedc44on the measured pages) — and 0.952–2.100×cjb2for the public direct JB2 lossless profile; every measured output passed its interop/fidelity gate. The earlier IW44 gap (up to 1.345×, and lower fidelity) came from two encoder bugs since fixed: an activation threshold that stranded dense-page coefficients (IW44_LUMA_PLATEAU) and a colour transform that did not match the decoder's PigeonYCbCrbasis (IW44_PIGEON_COLOR); seePERF_EXPERIMENTS.md. Same-size record-6 and lossy rec-7 remain experimental and are tracked indocs/jb2-size-gap-plan.md. - Document optimization is conservative in the first slice.
djvu optimizecurrently removes only semantically inertFREEpadding and reports unmet size targets; archival codec search, progress callbacks, and cancellation remain planned. Seedocs/optimizer.md; it always writes a separate output file. - OCR: Tesseract is the supported recognition backend.
OcrOptions(languages, dpi) are honored by Tesseract only; theocr-onnxneural pipeline is CLI-live but experimental (fixed pinned models, options ignored) andocr-neuralis a placeholder that returns an error.
Feature flags
| Flag | Default | Description |
|---|---|---|
std |
enabled | DjVuDocument, file I/O, rendering — the decode-only surface |
pdf |
disabled | PDF export via djvu_to_pdf (owns miniz_oxide + jpeg-encoder) |
cli |
disabled | Build the djvu command-line binary (implies pdf and cbz) |
cbz |
disabled | CBZ (comic-book ZIP) export — backs render --format cbz (owns zip) |
tiff |
disabled | TIFF export (djvu_to_tiff) and TIFF encode input for djvu encode / decode_image_to_pixmap |
async |
disabled | Async render API and lazy AsyncRead + AsyncSeek document loading |
parallel |
disabled | Parallel multi-page render via rayon (render_pages_parallel) |
jpeg |
disabled | Standalone JPEG decode without full std (JPEG is included in std by default) |
mmap |
disabled | Memory-mapped file I/O via memmap2 (MmapDocument::open) |
serde |
disabled | Serialize + Deserialize for all public data types |
image |
disabled | image::ImageDecoder impl via DjVuDecoder — integrates with the image crate |
epub |
disabled | EPUB 3 export via djvu_to_epub — page images, text overlay, bookmarks as nav (owns zip) |
wasm |
disabled | WebAssembly bindings via wasm-bindgen (WasmDocument, WasmPage) |
wasm-lazy |
disabled | Lazy Range-based document loading in the browser: a JS (offset, len) reader fetches only the pages you open |
wasm-threads |
disabled | wasm32 thread pool (rayon via Web Workers); requires a nightly toolchain, not part of the stable CI gate |
ocr-tesseract |
disabled | OCR recognition via a system Tesseract installation (the supported OCR backend) |
ocr-onnx |
disabled | Experimental neural OCR via tract-onnx (#693): pinned manifest + SHA-256-verified weights, DBNet detection, Cyrillic CTC recognition, CLI --backend onnx |
ocr-neural |
disabled | Placeholder backend only — CandleBackend::load returns a clear unsupported error |
ocr-neural-candle |
disabled | Deprecated no-op alias for ocr-neural |
experimental |
disabled | Experimental JB2 encoder paths used by internal example binaries |
iw44-probe |
disabled | IW44 encoder diagnostics probe (dev-only) |
alloc-profile |
disabled | dhat allocation-profiling harness for examples/alloc_profile.rs (dev-only) |
Without std, the crate provides IFF parsing, BZZ decompression, JB2/IW44 decoding,
text/annotation parsing — all codec primitives that work on byte slices.
API stability & compatibility
The full contract lives in docs/api-compatibility.md
(policy) and docs/feature-matrix.md (supported
combinations and targets), and is enforced in CI. In short:
- Stable surface — the document model (
Document/Page,DjVuDocument/DjVuPage), the render entry points, the codec entry points, the parsers, and the writerdjvu_to_*functions. Follows SemVer; breakage is caught bycargo-semver-checks. - Experimental / placeholder —
experimental,iw44-probe,alloc-profile,ocr-onnx,wasm-threads, and theocr-neuralplaceholder. These may change in any release and are the ones marked Experimental/Placeholder in the feature table above. - Deprecated (kept for ≥ 2 minor releases / 90 days) — the
bzz_newandiw44_newmodule aliases and theocr-neural-candlefeature alias. - MSRV — Rust 1.88, a required CI gate.
- Thread-safety —
Document,DjVuDocument,DjVuPage, pixel buffers, and the parsed content/error types areSend + Sync; the mutable editor isSend;LazyDocument<R>inherits its thread-safety fromR. Asserted intests/send_sync_contract.rs. - Untrusted input — no public parse/decode/render entry point panics on any
input; malformed bytes surface as typed errors. Covered by
tests/panic_free_corpus.rs, proptests, and libFuzzer/OSS-Fuzz targets. - Resource limits — decode/render inherit documented, bounded memory/work
ceilings; exceeding one returns a typed error naming the codec and axis. The
ceilings are caller-configurable: pass
ResourceLimitsviaParseOptionstoDjVuDocument::parse_with_options(pages inherit them at render time), or userender_pixmap_with_limits/render_into_with_limitsdirectly. SeeSECURITY.md.
Performance
See BENCHMARKS_RESULTS.md for Criterion numbers,
methodology, and a DjVuLibre comparison (run via
scripts/bench_djvulibre.sh +
scripts/djvulibre_compare.py).
Historical multi-platform results are in BENCHMARKS.md,
including the local WASM scalar-vs-simd128 harness.
Recent targeted experiments are recorded in PERF_EXPERIMENTS.md, including:
- #233 lazy async loading: a 100 MiB padded 520-page DJVM reached first pixel in 491.469 ms while reading only 28,578 bytes at simulated 12.5 MiB/s throughput.
- #189 x86-64-v3 AVX2 validation: existing AVX2 decode paths showed
iw44_decode_corpus_color-18.88% andiw44_decode_first_chunk-4.85% on GitHub-hosted x86_64, with one sub4 partial-decode regression recorded for follow-up. - #258 shared-Djbz clustering: Hamming shared clustering was rejected as default; byte-exact shared-Djbz remains the measured safe path.
Minimum supported Rust version (MSRV)
Rust 1.88 (edition 2024 — let-chains stabilized in 1.88)
Roadmap
See GitHub milestones for the full roadmap and progress tracking.
License
MIT. See LICENSE.
Specification
Written from the public DjVu v3 specification:
- https://www.sndjvu.org/spec.html
- https://web.archive.org/web/20251005122807/http://www.djvu.org/docs/DjVu3Spec.djvu (the spec is itself a DjVu file; archived copy — djvu.org and djvu.sourceforge.net no longer serve the original)
No code derived from GPL-licensed DjVuLibre or any other GPL source. All algorithms are independent implementations from the spec.