djvu-rs
Read, render, convert, and create DjVu files. Pure-Rust library with a CLI, WebAssembly, and Python bindings — 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 |
| Show DjVu in the browser | WebAssembly bindings, incl. lazy HTTP-Range loading |
| Read DjVu from Python | PyO3 bindings, built from source |
| 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, 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)
# 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
# Encode an image (PNG, JPEG, or TIFF) into a single-page DjVu (bilevel JB2, lossless)
# 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)
# 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;
--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. 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.
Python
PyO3 bindings live in djvu-py/. They are not published to PyPI
yet — build them from the repository (requires a Rust toolchain):
# or, for development: pip install maturin && cd djvu-py && maturin develop --release
=
=
= # or .to_numpy()
=
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. See djvu-py/README.md.
WebAssembly
Build the browser package with wasm-pack through the checked-in wrapper:
This produces examples/wasm/pkg/ with one JavaScript entry point, a scalar
fallback .wasm, and a simd128 .wasm. At runtime the loader validates a
tiny WebAssembly SIMD probe and selects the faster simd128 artifact when the
browser supports it, otherwise it loads the scalar artifact.
Then use in JavaScript/TypeScript:
import init from './pkg/djvu_rs.js';
await ;
console.log;
const doc = ;
console.log;
const page = doc.;
const pixels = page.; // Uint8ClampedArray, RGBA
const img = ;
ctx.;
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.
The generated npm package follows the Rust crate version; there is no separate
WASM release train. The local pkg/ directory is ignored wasm-pack output, so
regenerate it with make wasm from the checked-in Cargo.toml before
publishing instead of editing generated pkg/package.json by hand.
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;
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;
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.
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 an experimental library-level CTC helper; the CLI accepts
--backend onnx --model <path> but does not treat it as a stable backend
because no specific model family, preprocessing contract, or
fixture is guaranteed yet. 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:
| 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) |
✓ | — |
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) |
✓ | — |
Legacy standalone FORM:BM44 / FORM:PM44 files |
— (clean NotDjVu error) |
— |
| 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 are source-only for now. The
djvu-pypackage is not published to PyPI yet — install it from the repository checkout. - 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.- Legacy standalone
FORM:BM44/FORM:PM44files (pre-v3 DjVu) do not parse — they fail with a cleanNotDjVuerror rather than decoding. - Encoded files run larger than DjVuLibre's encoders — measured ~14% on
IW44 color output vs
c44, and the JB2 encoder lacks same-size record-6 refinement vscjb2. A size gap, not a fidelity gap; tracked indocs/jb2-size-gap-plan.md. - OCR: Tesseract is the only supported recognition backend.
OcrOptions(languages, dpi) are honored by Tesseract only;ocr-onnxis experimental 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 via the tiff crate |
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 ONNX CTC recognition helper via tract-onnx; no stable model contract |
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.
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://djvu.sourceforge.net/spec/DjVu3Spec.djvu (the spec is itself a DjVu file)
No code derived from GPL-licensed DjVuLibre or any other GPL source. All algorithms are independent implementations from the spec.