djvu-rs 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +803 -0
- package/djvu_rs.d.ts +321 -0
- package/djvu_rs.js +56 -0
- package/package.json +44 -0
- package/scalar/LICENSE +21 -0
- package/scalar/README.md +803 -0
- package/scalar/djvu_rs.d.ts +318 -0
- package/scalar/djvu_rs.js +702 -0
- package/scalar/djvu_rs_bg.wasm +0 -0
- package/scalar/djvu_rs_bg.wasm.d.ts +53 -0
- package/simd128/LICENSE +21 -0
- package/simd128/README.md +803 -0
- package/simd128/djvu_rs.d.ts +318 -0
- package/simd128/djvu_rs.js +702 -0
- package/simd128/djvu_rs_bg.wasm +0 -0
- package/simd128/djvu_rs_bg.wasm.d.ts +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
# djvu-rs
|
|
2
|
+
|
|
3
|
+
[](https://crates.io/crates/djvu-rs)
|
|
4
|
+
[](https://docs.rs/djvu-rs)
|
|
5
|
+
[](https://github.com/matyushkin/djvu-rs/actions/workflows/ci.yml)
|
|
6
|
+
[](https://matyushkin.github.io/djvu-rs/dev/bench/)
|
|
7
|
+
[](https://matyushkin.github.io/djvu-rs/dev/conformance/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
Read, render, convert, and create DjVu files. Pure-Rust library with a CLI,
|
|
11
|
+
WebAssembly, and Python bindings — MIT licensed, no GPL dependencies, written
|
|
12
|
+
from the public DjVu v3 specification.
|
|
13
|
+
|
|
14
|
+
| Your task | How |
|
|
15
|
+
|-----------|-----|
|
|
16
|
+
| Convert DjVu → PDF, EPUB, TIFF, PNG, CBZ | [`djvu render`](#cli) or [`djvu_to_pdf`](#pdf-export) / [`djvu_to_epub`](#epub-export) / [`djvu_to_tiff`](#tiff-export) |
|
|
17
|
+
| Extract text (plain, hOCR, ALTO XML) | [`djvu text`](#cli) or [`page.text()`](#text-extraction), [`to_hocr` / `to_alto`](#hocr-and-alto-xml-export) |
|
|
18
|
+
| Render pages to RGBA pixels | [`render_pixmap`](#quick-start) — sync, [async](#async-render), or [parallel](#feature-flags) |
|
|
19
|
+
| Show DjVu in the browser | [WebAssembly bindings](#webassembly), incl. lazy HTTP-Range loading |
|
|
20
|
+
| Read DjVu from Python | [PyO3 bindings, built from source](#python) |
|
|
21
|
+
| Create DjVu from images (PNG/JPEG/TIFF) | [`djvu encode`](#cli) or [`PageEncoder`](#encoding--low-level-api) |
|
|
22
|
+
| Add an OCR text layer to a scan | [`djvu ocr`](#ocr-recognition-backends) (Tesseract) |
|
|
23
|
+
| Merge, split, edit documents | [`djvu merge` / `djvu split`](#cli), `DocumentEditor`, `DjVuDocumentMut` |
|
|
24
|
+
| Stream huge books page-by-page | [Lazy async loading](#lazy-async-loading) — first pixel after ~29 KB of a 100 MB file |
|
|
25
|
+
|
|
26
|
+
Every Rust example below is a complete program, compiled as a doctest on every
|
|
27
|
+
CI run — what you copy-paste is guaranteed to build against the current API.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```rust,no_run
|
|
32
|
+
use djvu_rs::{DjVuDocument, djvu_render::{render_pixmap, RenderOptions}};
|
|
33
|
+
|
|
34
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
35
|
+
let data = std::fs::read("file.djvu")?;
|
|
36
|
+
let doc = DjVuDocument::parse(&data)?;
|
|
37
|
+
|
|
38
|
+
println!("{} pages", doc.page_count());
|
|
39
|
+
|
|
40
|
+
let page = doc.page(0)?;
|
|
41
|
+
println!("{}×{} @ {} dpi", page.width(), page.height(), page.dpi());
|
|
42
|
+
|
|
43
|
+
let target_dpi = 150u32;
|
|
44
|
+
let opts = RenderOptions {
|
|
45
|
+
width: ((page.width() as u32 * target_dpi) / page.dpi() as u32).max(1),
|
|
46
|
+
height: ((page.height() as u32 * target_dpi) / page.dpi() as u32).max(1),
|
|
47
|
+
..Default::default()
|
|
48
|
+
};
|
|
49
|
+
let pixmap = render_pixmap(page, &opts)?;
|
|
50
|
+
// pixmap.data — RGBA bytes (width × height × 4), row-major
|
|
51
|
+
Ok(())
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Text extraction
|
|
56
|
+
|
|
57
|
+
```rust,no_run
|
|
58
|
+
use djvu_rs::DjVuDocument;
|
|
59
|
+
|
|
60
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
61
|
+
let data = std::fs::read("scanned.djvu")?;
|
|
62
|
+
let doc = DjVuDocument::parse(&data)?;
|
|
63
|
+
let page = doc.page(0)?;
|
|
64
|
+
|
|
65
|
+
if let Some(text) = page.text()? {
|
|
66
|
+
println!("{text}");
|
|
67
|
+
}
|
|
68
|
+
Ok(())
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## PDF export
|
|
73
|
+
|
|
74
|
+
Requires the `pdf` feature flag: `djvu-rs = { version = "…", features = ["pdf"] }`.
|
|
75
|
+
The PDF keeps selectable text, bookmarks, and hyperlinks, and embeds the
|
|
76
|
+
IW44/JB2 image data losslessly.
|
|
77
|
+
|
|
78
|
+
```rust,no_run
|
|
79
|
+
use djvu_rs::{DjVuDocument, pdf::djvu_to_pdf};
|
|
80
|
+
|
|
81
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
82
|
+
let data = std::fs::read("book.djvu")?;
|
|
83
|
+
let doc = DjVuDocument::parse(&data)?;
|
|
84
|
+
|
|
85
|
+
let pdf_bytes = djvu_to_pdf(&doc)?;
|
|
86
|
+
std::fs::write("book.pdf", pdf_bytes)?;
|
|
87
|
+
Ok(())
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## CLI
|
|
92
|
+
|
|
93
|
+
The `djvu` binary is enabled by the `cli` feature.
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
# Install
|
|
97
|
+
cargo install djvu-rs --features cli
|
|
98
|
+
|
|
99
|
+
# Document info (--json for machine-readable output, --count for page count only)
|
|
100
|
+
djvu info file.djvu
|
|
101
|
+
|
|
102
|
+
# Inspect IFF chunk identities, offsets, sizes, and bundled component relationships
|
|
103
|
+
djvu inspect book.djvu --json
|
|
104
|
+
|
|
105
|
+
# Layered validation: structural, dependency, codec, and resource findings with
|
|
106
|
+
# stable codes (--strict makes warnings fail the exit code; --decode-pages adds
|
|
107
|
+
# full codec decodes; --limits gates size/page/pixel/memory budgets before decode)
|
|
108
|
+
djvu validate book.djvu --strict --decode-pages --limits server.json --json
|
|
109
|
+
|
|
110
|
+
# Semantic comparison of two documents: pages, text, annotations, metadata,
|
|
111
|
+
# bookmarks, and the component graph (--plane filters the compared planes)
|
|
112
|
+
djvu diff a.djvu b.djvu --plane text --json
|
|
113
|
+
|
|
114
|
+
# Render page 1 to PNG at 200 DPI
|
|
115
|
+
djvu render file.djvu --dpi 200 --output page1.png
|
|
116
|
+
|
|
117
|
+
# Render all pages to a PDF, EPUB, or CBZ
|
|
118
|
+
djvu render file.djvu --all --format pdf --output out.pdf
|
|
119
|
+
djvu render file.djvu --all --format epub --output out.epub
|
|
120
|
+
djvu render file.djvu --all --format cbz --output out.cbz
|
|
121
|
+
|
|
122
|
+
# Render a single layer (mask, foreground, background), with optional rotation
|
|
123
|
+
djvu render file.djvu --layer mask --rotate cw90 --output mask.png
|
|
124
|
+
|
|
125
|
+
# Extract text from page 2 (plain), or from all pages as hOCR / ALTO XML
|
|
126
|
+
djvu text file.djvu --page 2
|
|
127
|
+
djvu text file.djvu --all --format hocr --output out.hocr
|
|
128
|
+
|
|
129
|
+
# Merge documents into a bundled DJVM / extract a page range
|
|
130
|
+
djvu merge a.djvu b.djvu --output merged.djvu
|
|
131
|
+
djvu split book.djvu --pages 10-25 --output chapter.djvu
|
|
132
|
+
|
|
133
|
+
# Preview safe cleanup as machine-readable JSON, or write an optimized copy
|
|
134
|
+
djvu optimize book.djvu --output optimized.djvu --preset lossless-cleanup --dry-run
|
|
135
|
+
djvu optimize book.djvu --output optimized.djvu --preset lossless-cleanup
|
|
136
|
+
djvu optimize book.djvu --output optimized.djvu --preset archival --target-size 26214400
|
|
137
|
+
djvu optimize book.djvu --output optimized.djvu --max-ssim-loss 0.001
|
|
138
|
+
|
|
139
|
+
# Encode an image (PNG, JPEG, or TIFF) into a single-page DjVu (bilevel JB2, lossless)
|
|
140
|
+
# TIFF input requires building/installing with --features tiff (cli alone does not enable it).
|
|
141
|
+
djvu encode scan.png --output scan.djvu --dpi 300
|
|
142
|
+
|
|
143
|
+
# Opt into a DjVuLibre-compatible G4/MMR mask for fax/scanner workflows
|
|
144
|
+
djvu encode scan.png --quality lossless --bilevel-codec smmr --output scan.djvu
|
|
145
|
+
|
|
146
|
+
# Encode into a layered lossy DjVu (JB2 mask + IW44 background + FGbz foreground color)
|
|
147
|
+
djvu encode scan.jpg --quality quality --output scan.djvu --dpi 300
|
|
148
|
+
|
|
149
|
+
# Use the conservative archival color profile
|
|
150
|
+
djvu encode scan.png --quality archival --output scan.djvu --dpi 300
|
|
151
|
+
|
|
152
|
+
# Opt into adaptive mask segmentation for uneven scans
|
|
153
|
+
djvu encode scan.png --quality quality --binarization sauvola --bg-inpaint --output scan.djvu
|
|
154
|
+
|
|
155
|
+
# Cap the IW44 background at a bits-per-pixel budget (smaller file, lower quality)
|
|
156
|
+
djvu encode scan.jpg --quality quality --bg-bpp 0.8 --output scan.djvu
|
|
157
|
+
|
|
158
|
+
# Encode a directory of images into a bundled DJVM with shared Djbz
|
|
159
|
+
djvu encode pages/ --output book.djvu --shared-dict-pages 2
|
|
160
|
+
|
|
161
|
+
# Embed TH44 color thumbnails while bundling (multi-page layered)
|
|
162
|
+
djvu encode pages/ --quality quality --thumbnails --output book.djvu
|
|
163
|
+
|
|
164
|
+
# Raw BZZ compression utilities
|
|
165
|
+
djvu bzz-encode notes.txt --output notes.bzz
|
|
166
|
+
djvu bzz-decode notes.bzz --output notes.txt
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
For single image input (PNG, JPEG, or TIFF), `--quality lossless`
|
|
170
|
+
luminance-thresholds the image into a JB2 mask and writes `INFO + Sjbz`.
|
|
171
|
+
`--bilevel-codec smmr` is an explicit single-image opt-in that writes a
|
|
172
|
+
DjVuLibre-compatible `Smmr` G4/MMR mask instead; it preserves the default JB2
|
|
173
|
+
path and is not available for directory bundles. The Smmr path is intended for
|
|
174
|
+
fax/scanner interoperability and is usually larger than JB2.
|
|
175
|
+
`--quality quality` uses the layered encoder (`INFO + Sjbz + BG44...` plus
|
|
176
|
+
`FGbz` when colored foreground is detected) for color input. `--quality
|
|
177
|
+
archival` uses the same layered shape with a denser background sample grid.
|
|
178
|
+
Directory input supports all three profiles, and both directory paths share a
|
|
179
|
+
Djbz symbol dictionary across pages: `lossless` uses the shared-Djbz
|
|
180
|
+
multi-page JB2 path, while `quality` / `archival` bundle layered pages that
|
|
181
|
+
keep their own `Sjbz`, `BG44`, and optional `FGbz` chunks on top of the shared
|
|
182
|
+
dictionary. `--shared-dict-pages` sets the page-count threshold for promoting
|
|
183
|
+
a symbol into the shared dictionary on either path.
|
|
184
|
+
|
|
185
|
+
Layered `quality` / `archival` encodes default to fixed BT.601 thresholding.
|
|
186
|
+
`--binarization sauvola` opts into adaptive local thresholding for mixed or
|
|
187
|
+
uneven lighting; tune it with `--sauvola-window` and `--sauvola-k`.
|
|
188
|
+
`--bg-inpaint` fills fully masked background blocks from neighbouring unmasked
|
|
189
|
+
pixels, which can reduce dark boxes under heavy text strokes. These knobs are
|
|
190
|
+
opt-in, only affect layered profiles, and do not change lossless JB2 defaults.
|
|
191
|
+
Library callers can use the same controls with `PageEncoder::with_segment_options`.
|
|
192
|
+
For newly encoded pages, `PageEncoder::with_metadata` emits a `METz` chunk;
|
|
193
|
+
for existing documents, `DjVuDocumentMut::page_mut(...).set_metadata(...)`
|
|
194
|
+
performs a mutation while preserving untouched chunks. These are deliberately
|
|
195
|
+
separate fresh-encode and mutation APIs.
|
|
196
|
+
|
|
197
|
+
## Python
|
|
198
|
+
|
|
199
|
+
```sh
|
|
200
|
+
pip install djvu-rs
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
import djvu_rs as djvu
|
|
205
|
+
|
|
206
|
+
doc = djvu.Document.open('scan.djvu')
|
|
207
|
+
print(f'{doc.page_count()} pages')
|
|
208
|
+
|
|
209
|
+
page = doc.page(0)
|
|
210
|
+
img = page.render(dpi=150).to_pil() # or .to_numpy()
|
|
211
|
+
img.save('page.png')
|
|
212
|
+
|
|
213
|
+
text = page.text()
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
PyO3 bindings live in [`djvu-py/`](djvu-py/). Wheels track the crate version
|
|
217
|
+
(CPython 3.9–3.13 on manylinux/musllinux, macOS, and Windows). The bindings
|
|
218
|
+
cover the reading surface: open documents, render pages (including region and
|
|
219
|
+
progressive rendering, with zero-copy numpy/PIL paths), and extract the text
|
|
220
|
+
layer. Encode, mutation, and PDF/EPUB/TIFF export stay on the Rust crate / CLI
|
|
221
|
+
for now. See [`djvu-py/README.md`](djvu-py/README.md) and
|
|
222
|
+
[`docs/packaging.md`](docs/packaging.md).
|
|
223
|
+
|
|
224
|
+
## WebAssembly
|
|
225
|
+
|
|
226
|
+
```sh
|
|
227
|
+
npm install djvu-rs
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
import init, { WasmDocument, selectedWasmVariant } from 'djvu-rs';
|
|
232
|
+
|
|
233
|
+
await init();
|
|
234
|
+
console.log(`djvu-rs wasm variant: ${selectedWasmVariant()}`);
|
|
235
|
+
|
|
236
|
+
const doc = WasmDocument.from_bytes(new Uint8Array(arrayBuffer));
|
|
237
|
+
console.log(doc.page_count());
|
|
238
|
+
|
|
239
|
+
const page = doc.page(0);
|
|
240
|
+
const pixels = page.render(150); // Uint8ClampedArray, RGBA
|
|
241
|
+
const img = new ImageData(pixels, page.width_at(150), page.height_at(150));
|
|
242
|
+
ctx.putImageData(img, 0, 0);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The npm package ships TypeScript declarations plus scalar and `simd128` wasm
|
|
246
|
+
artifacts; at runtime a tiny `WebAssembly.validate()` probe selects SIMD when
|
|
247
|
+
supported. Package versions match the Rust crate — see
|
|
248
|
+
[`docs/packaging.md`](docs/packaging.md).
|
|
249
|
+
|
|
250
|
+
To rebuild the package from this repository:
|
|
251
|
+
|
|
252
|
+
```sh
|
|
253
|
+
make wasm # → examples/wasm/pkg (dual scalar + simd128 loader)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
See [`examples/wasm/`](examples/wasm/) for a complete drag-and-drop demo, and
|
|
257
|
+
[`examples/wasm/range_lazy.md`](examples/wasm/range_lazy.md) for lazy loading
|
|
258
|
+
over HTTP `Range` requests (`wasm-lazy` feature) — the browser fetches only
|
|
259
|
+
the index plus the pages actually opened.
|
|
260
|
+
|
|
261
|
+
## Advanced usage
|
|
262
|
+
|
|
263
|
+
### TIFF export
|
|
264
|
+
|
|
265
|
+
Requires the `tiff` feature flag: `djvu-rs = { version = "…", features = ["tiff"] }`.
|
|
266
|
+
|
|
267
|
+
```rust,no_run
|
|
268
|
+
use djvu_rs::{DjVuDocument, tiff_export::{djvu_to_tiff, TiffOptions}};
|
|
269
|
+
|
|
270
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
271
|
+
let data = std::fs::read("scan.djvu")?;
|
|
272
|
+
let doc = DjVuDocument::parse(&data)?;
|
|
273
|
+
|
|
274
|
+
let tiff_bytes = djvu_to_tiff(&doc, &TiffOptions::default())?;
|
|
275
|
+
std::fs::write("scan.tiff", tiff_bytes)?;
|
|
276
|
+
Ok(())
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### EPUB export
|
|
281
|
+
|
|
282
|
+
Requires the `epub` feature flag: `djvu-rs = { version = "…", features = ["epub"] }`.
|
|
283
|
+
Produces EPUB 3 with page images, an invisible text overlay, and bookmarks as
|
|
284
|
+
navigation.
|
|
285
|
+
|
|
286
|
+
```rust,no_run
|
|
287
|
+
use djvu_rs::{DjVuDocument, epub::{djvu_to_epub, EpubOptions}};
|
|
288
|
+
|
|
289
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
290
|
+
let data = std::fs::read("book.djvu")?;
|
|
291
|
+
let doc = DjVuDocument::parse(&data)?;
|
|
292
|
+
|
|
293
|
+
let epub_bytes = djvu_to_epub(&doc, &EpubOptions::default())?;
|
|
294
|
+
std::fs::write("book.epub", epub_bytes)?;
|
|
295
|
+
Ok(())
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### hOCR and ALTO XML export
|
|
300
|
+
|
|
301
|
+
```rust,no_run
|
|
302
|
+
use djvu_rs::{DjVuDocument, text_serialize::{to_hocr, to_alto, HocrOptions, AltoOptions}};
|
|
303
|
+
|
|
304
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
305
|
+
let data = std::fs::read("scanned.djvu")?;
|
|
306
|
+
let doc = DjVuDocument::parse(&data)?;
|
|
307
|
+
|
|
308
|
+
// hOCR — compatible with Tesseract, ABBYY, and most OCR toolchains
|
|
309
|
+
let hocr = to_hocr(&doc, &HocrOptions::default())?;
|
|
310
|
+
std::fs::write("output.hocr", hocr)?;
|
|
311
|
+
|
|
312
|
+
// ALTO XML — used by libraries and archives (DFG, Europeana, etc.)
|
|
313
|
+
let alto = to_alto(&doc, &AltoOptions::default())?;
|
|
314
|
+
std::fs::write("output.xml", alto)?;
|
|
315
|
+
Ok(())
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Async render
|
|
320
|
+
|
|
321
|
+
Requires the `async` feature flag: `djvu-rs = { version = "…", features = ["async"] }`.
|
|
322
|
+
|
|
323
|
+
The render entry points are synchronous and CPU-bound; run them on the
|
|
324
|
+
blocking thread pool with `tokio::task::spawn_blocking` so they stay off the
|
|
325
|
+
async runtime. The render error type stays the typed `RenderError` — there is
|
|
326
|
+
no wrapper enum.
|
|
327
|
+
|
|
328
|
+
```rust,no_run
|
|
329
|
+
use djvu_rs::{DjVuDocument, djvu_render::{self, RenderOptions}};
|
|
330
|
+
|
|
331
|
+
#[tokio::main(flavor = "current_thread")]
|
|
332
|
+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
333
|
+
let data = std::fs::read("file.djvu")?;
|
|
334
|
+
let doc = DjVuDocument::parse(&data)?;
|
|
335
|
+
let page = doc.page(0)?.clone();
|
|
336
|
+
|
|
337
|
+
let target_dpi = 150u32;
|
|
338
|
+
let opts = RenderOptions {
|
|
339
|
+
width: ((page.width() as u32 * target_dpi) / page.dpi() as u32).max(1),
|
|
340
|
+
height: ((page.height() as u32 * target_dpi) / page.dpi() as u32).max(1),
|
|
341
|
+
..Default::default()
|
|
342
|
+
};
|
|
343
|
+
let pixmap = tokio::task::spawn_blocking(move || {
|
|
344
|
+
djvu_render::render_pixmap(&page, &opts)
|
|
345
|
+
})
|
|
346
|
+
.await??; // outer `?`: join error (panic); inner `?`: RenderError
|
|
347
|
+
println!("{} bytes", pixmap.data.len());
|
|
348
|
+
Ok(())
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
For progressive (per-BG44-chunk) rendering, `djvu_async::render_progressive_stream`
|
|
353
|
+
yields a `Stream` of frames, each produced on the blocking pool.
|
|
354
|
+
|
|
355
|
+
### Lazy async loading
|
|
356
|
+
|
|
357
|
+
Requires the `async` feature flag. The lazy loader keeps a seekable async
|
|
358
|
+
reader and fetches page/component byte ranges only when `page_async(i)` is
|
|
359
|
+
called. Parsed pages are cached as `Arc<DjVuPage>`.
|
|
360
|
+
|
|
361
|
+
```rust,no_run
|
|
362
|
+
use djvu_rs::djvu_async::from_async_reader_lazy;
|
|
363
|
+
|
|
364
|
+
#[tokio::main(flavor = "current_thread")]
|
|
365
|
+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
366
|
+
let file = tokio::fs::File::open("book.djvu").await?;
|
|
367
|
+
let doc = from_async_reader_lazy(file).await?;
|
|
368
|
+
println!("{} pages", doc.page_count());
|
|
369
|
+
|
|
370
|
+
let page = doc.page_async(0).await?;
|
|
371
|
+
println!("first page: {}×{}", page.width(), page.height());
|
|
372
|
+
Ok(())
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Supported shapes: single-page `FORM:DJVU` and bundled `FORM:DJVM`, including
|
|
377
|
+
shared `DJVI` dictionaries referenced via `INCL`. For browser-local `!Send`
|
|
378
|
+
readers on `wasm32`, use `from_async_reader_lazy_local`.
|
|
379
|
+
|
|
380
|
+
See [`examples/async_lazy_first_page.rs`](examples/async_lazy_first_page.rs)
|
|
381
|
+
for a native first-page latency probe and
|
|
382
|
+
[`examples/wasm/range_lazy.md`](examples/wasm/range_lazy.md) for the HTTP
|
|
383
|
+
`Range: bytes=start-end` integration shape.
|
|
384
|
+
|
|
385
|
+
### Serde support
|
|
386
|
+
|
|
387
|
+
Requires the `serde` feature flag: `djvu-rs = { version = "…", features = ["serde"] }`.
|
|
388
|
+
|
|
389
|
+
All public data types (`DjVuBookmark`, `TextZone`, `MapArea`, `PageInfo`, etc.) implement
|
|
390
|
+
`Serialize` and `Deserialize`.
|
|
391
|
+
|
|
392
|
+
```rust,no_run
|
|
393
|
+
use djvu_rs::DjVuDocument;
|
|
394
|
+
|
|
395
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
396
|
+
let data = std::fs::read("book.djvu")?;
|
|
397
|
+
let doc = DjVuDocument::parse(&data)?;
|
|
398
|
+
|
|
399
|
+
let json = serde_json::to_string_pretty(doc.bookmarks())?;
|
|
400
|
+
println!("{json}");
|
|
401
|
+
Ok(())
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### image-rs integration
|
|
406
|
+
|
|
407
|
+
Requires the `image` feature flag: `djvu-rs = { version = "…", features = ["image"] }`.
|
|
408
|
+
|
|
409
|
+
```rust,no_run
|
|
410
|
+
use djvu_rs::{DjVuDocument, image_compat::DjVuDecoder};
|
|
411
|
+
use image::DynamicImage;
|
|
412
|
+
|
|
413
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
414
|
+
let data = std::fs::read("file.djvu")?;
|
|
415
|
+
let doc = DjVuDocument::parse(&data)?;
|
|
416
|
+
let page = doc.page(0)?;
|
|
417
|
+
|
|
418
|
+
let decoder = DjVuDecoder::new(page)?.with_size(1200, 1600);
|
|
419
|
+
let img = DynamicImage::from_decoder(decoder)?;
|
|
420
|
+
img.save("page.png")?;
|
|
421
|
+
Ok(())
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Encoding & low-level API
|
|
426
|
+
|
|
427
|
+
### JB2 bilevel image encoder
|
|
428
|
+
|
|
429
|
+
```rust
|
|
430
|
+
use djvu_rs::{Bitmap, jb2_encode::encode_jb2};
|
|
431
|
+
|
|
432
|
+
fn main() {
|
|
433
|
+
let mut bm = Bitmap::new(800, 1000);
|
|
434
|
+
// ... fill bitmap pixels ...
|
|
435
|
+
let sjbz_payload = encode_jb2(&bm);
|
|
436
|
+
// Wrap in a Sjbz IFF chunk and embed in a DjVu FORM:DJVU.
|
|
437
|
+
assert!(!sjbz_payload.is_empty());
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### IW44 wavelet encoder
|
|
442
|
+
|
|
443
|
+
```rust
|
|
444
|
+
use djvu_rs::{Pixmap, iw44_encode::{encode_iw44_color, encode_iw44_gray, Iw44EncodeOptions}};
|
|
445
|
+
|
|
446
|
+
fn main() {
|
|
447
|
+
// Color: encode a Pixmap (RGBA) into BG44 chunk payloads.
|
|
448
|
+
let pixmap = Pixmap::new(640, 480, 255, 255, 255, 255);
|
|
449
|
+
let chunks: Vec<Vec<u8>> = encode_iw44_color(&pixmap, &Iw44EncodeOptions::default());
|
|
450
|
+
// Each Vec<u8> is a BG44 chunk payload; wrap each in a BG44 IFF tag.
|
|
451
|
+
|
|
452
|
+
// Grayscale: encode a GrayPixmap the same way.
|
|
453
|
+
let gray = pixmap.to_gray8();
|
|
454
|
+
let gray_chunks: Vec<Vec<u8>> = encode_iw44_gray(&gray, &Iw44EncodeOptions::default());
|
|
455
|
+
assert!(!chunks.is_empty() && !gray_chunks.is_empty());
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
`Iw44EncodeOptions` fields (all have sensible defaults):
|
|
460
|
+
|
|
461
|
+
| Field | Default | Description |
|
|
462
|
+
|-------|---------|-------------|
|
|
463
|
+
| `slices_per_chunk` | 10 | Slices packed into each BG44/FG44 chunk |
|
|
464
|
+
| `total_slices` | 100 | Total refinement slices to encode |
|
|
465
|
+
| `chroma_delay` | 0 | Y slices before Cb/Cr encoding begins |
|
|
466
|
+
| `chroma_half` | false | Legacy no-op; IW44 v1.2 always emits full-resolution chroma |
|
|
467
|
+
|
|
468
|
+
### Bookmark encoder
|
|
469
|
+
|
|
470
|
+
```rust
|
|
471
|
+
use djvu_rs::{djvu_document::DjVuBookmark, navm_encode::encode_navm};
|
|
472
|
+
|
|
473
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
474
|
+
let bookmarks = vec![
|
|
475
|
+
DjVuBookmark { title: "Chapter 1".into(), url: "#page=1".into(), children: vec![] },
|
|
476
|
+
];
|
|
477
|
+
let navm_payload = encode_navm(&bookmarks)?;
|
|
478
|
+
assert!(!navm_payload.is_empty());
|
|
479
|
+
Ok(())
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Annotation encoder
|
|
484
|
+
|
|
485
|
+
```rust
|
|
486
|
+
use djvu_rs::annotation::{Annotation, MapArea, encode_annotations, encode_annotations_bzz};
|
|
487
|
+
|
|
488
|
+
fn main() {
|
|
489
|
+
let ann = Annotation::default();
|
|
490
|
+
let areas: Vec<MapArea> = vec![];
|
|
491
|
+
|
|
492
|
+
let anta_payload = encode_annotations(&ann, &areas); // uncompressed ANTa
|
|
493
|
+
let antz_payload = encode_annotations_bzz(&ann, &areas); // BZZ-compressed ANTz
|
|
494
|
+
assert!(anta_payload.len() <= antz_payload.len() || !antz_payload.is_empty());
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Indirect multi-page documents
|
|
499
|
+
|
|
500
|
+
Create an indirect DJVM index file that references per-page `.djvu` files:
|
|
501
|
+
|
|
502
|
+
```rust,no_run
|
|
503
|
+
use djvu_rs::djvm::create_indirect;
|
|
504
|
+
|
|
505
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
506
|
+
let index = create_indirect(&["page001.djvu", "page002.djvu", "page003.djvu"])?;
|
|
507
|
+
std::fs::write("book.djvu", index)?;
|
|
508
|
+
// Distribute book.djvu alongside the individual page files.
|
|
509
|
+
Ok(())
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Load an indirect document by resolving component files from a directory:
|
|
514
|
+
|
|
515
|
+
```rust,no_run
|
|
516
|
+
use djvu_rs::DjVuDocument;
|
|
517
|
+
|
|
518
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
519
|
+
let index = std::fs::read("book.djvu")?;
|
|
520
|
+
let doc = DjVuDocument::parse_from_dir(&index, "/path/to/pages")?;
|
|
521
|
+
println!("{} pages", doc.page_count());
|
|
522
|
+
Ok(())
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Applications that need the full DIRM component identity can use
|
|
527
|
+
`DjVuDocument::parse_with_component_resolver`. Its
|
|
528
|
+
`ComponentResolver` receives a `ComponentId` containing both the external name
|
|
529
|
+
and its `ComponentKind` (`Page`, `Shared`, or `Thumbnail`), and is called for
|
|
530
|
+
every directory entry. Page/shared/thumbnail FORM mismatches and resolver
|
|
531
|
+
failures surface as typed errors; shared `Djbz` dictionaries referenced by
|
|
532
|
+
`INCL` are connected to the parsed pages. See
|
|
533
|
+
[`docs/indirect-djvm-resolver.md`](docs/indirect-djvm-resolver.md).
|
|
534
|
+
|
|
535
|
+
Two mutation paths cover indirect documents:
|
|
536
|
+
`DjVuDocumentMut::from_indirect_resolved` resolves the component files and
|
|
537
|
+
rebundles them into a mutable bundled document, and `IndirectRewritePlan`
|
|
538
|
+
rewrites individual component files on disk while keeping the document
|
|
539
|
+
indirect (each file is renamed atomically, but the multi-file commit as a
|
|
540
|
+
whole is not transactional). Opening an indirect index directly with
|
|
541
|
+
`DjVuDocumentMut::from_bytes` and calling `page_mut` remains unsupported; see
|
|
542
|
+
[`docs/indirect-djvm-mutation.md`](docs/indirect-djvm-mutation.md).
|
|
543
|
+
|
|
544
|
+
### Typed document editing
|
|
545
|
+
|
|
546
|
+
`DocumentEditor` provides a versioned, typed operation list with a semantic
|
|
547
|
+
dry-run plan and validation of every operation before bytes are emitted. The
|
|
548
|
+
current schema covers page text, page annotations, page/document METa/METz
|
|
549
|
+
metadata, and bundled-document NAVM bookmarks:
|
|
550
|
+
|
|
551
|
+
```rust,no_run
|
|
552
|
+
use djvu_rs::{DocumentEditor, EditOperation, EditRequest};
|
|
553
|
+
use djvu_rs::metadata::DjVuMetadata;
|
|
554
|
+
|
|
555
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
556
|
+
let input = std::fs::read("book.djvu")?;
|
|
557
|
+
let request = EditRequest::new(vec![EditOperation::SetDocumentMetadata {
|
|
558
|
+
metadata: DjVuMetadata {
|
|
559
|
+
title: Some("Updated title".into()),
|
|
560
|
+
..Default::default()
|
|
561
|
+
},
|
|
562
|
+
}]);
|
|
563
|
+
|
|
564
|
+
let plan = DocumentEditor::plan(&input, &request)?;
|
|
565
|
+
println!("{} operation(s), {} page(s)", plan.operations.len(), plan.page_count);
|
|
566
|
+
let edited = DocumentEditor::apply(&input, &request)?;
|
|
567
|
+
std::fs::write("edited.djvu", edited)?;
|
|
568
|
+
Ok(())
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
`DocumentEditor::apply_to_path` stages output beside the destination and
|
|
573
|
+
renames it only after validation, serialization, and sync succeed. The first
|
|
574
|
+
slice intentionally does not yet cover the declarative CLI, XMP, thumbnails,
|
|
575
|
+
page insertion/deletion/reordering/extraction, semantic diff, or multi-file
|
|
576
|
+
indirect-DJVM commits; those require separate operation and commit contracts.
|
|
577
|
+
With the `serde` feature, requests and plans are JSON-serializable using the
|
|
578
|
+
versioned schema.
|
|
579
|
+
|
|
580
|
+
### Low-level IFF access
|
|
581
|
+
|
|
582
|
+
```rust,no_run
|
|
583
|
+
use djvu_rs::iff::parse_form;
|
|
584
|
+
|
|
585
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
586
|
+
let data = std::fs::read("file.djvu")?;
|
|
587
|
+
let form = parse_form(&data)?;
|
|
588
|
+
println!("FORM type: {:?}", std::str::from_utf8(&form.form_type));
|
|
589
|
+
for chunk in &form.chunks {
|
|
590
|
+
println!(" chunk {:?} ({} bytes)", std::str::from_utf8(&chunk.id), chunk.data.len());
|
|
591
|
+
}
|
|
592
|
+
Ok(())
|
|
593
|
+
}
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### OCR recognition backends
|
|
597
|
+
|
|
598
|
+
The supported OCR recognition path is the `ocr-tesseract` feature, which uses a
|
|
599
|
+
system Tesseract installation and tessdata files. Recognized text is embedded
|
|
600
|
+
into the output document as a compressed `TXTz` text layer, page by page:
|
|
601
|
+
|
|
602
|
+
```sh
|
|
603
|
+
cargo build --features cli,ocr-tesseract
|
|
604
|
+
# Requires Tesseract + the requested language data, e.g. eng.traineddata.
|
|
605
|
+
djvu ocr scanned.djvu --backend tesseract --lang eng --output with-text.djvu
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Library callers can attach recognized text at encode time instead, via
|
|
609
|
+
`PageEncoder::with_ocr_text_layer` (or `with_text_layer` for an existing
|
|
610
|
+
`TextLayer`).
|
|
611
|
+
|
|
612
|
+
`ocr-onnx` is experimental but now CLI-live (#693): `--backend onnx` runs the
|
|
613
|
+
full PP-OCR neural pipeline — DBNet text detection plus Cyrillic PP-OCRv5 CTC
|
|
614
|
+
line recognition (its pinned dictionary also covers Latin, digits, and
|
|
615
|
+
punctuation) assembled into a `page → line → word` text layer with heuristic
|
|
616
|
+
word rectangles. Models come only from the pinned manifest with mandatory
|
|
617
|
+
SHA-256 verification (`docs/ocr-model-manifest.toml`, fetched explicitly via
|
|
618
|
+
`scripts/fetch_ocr_models.sh` — weights are never committed and never
|
|
619
|
+
downloaded implicitly; directory override: `DJVU_OCR_MODELS_DIR`). The
|
|
620
|
+
`--model` flag is not used by this backend, and `OcrOptions`
|
|
621
|
+
(`languages`/`dpi`) are advisory and ignored. Recognition quality of the
|
|
622
|
+
pinned models is gated by a deterministic synthetic corpus with a recorded
|
|
623
|
+
CER/WER/IoU baseline (`docs/ocr-model-metrics.md`). `ocr-neural` is a placeholder only: `CandleBackend` now
|
|
624
|
+
returns a clear unsupported-backend error instead of constructing a backend that
|
|
625
|
+
always fails at recognition time. The compatibility feature name
|
|
626
|
+
`ocr-neural-candle` is a no-op and no longer pulls Candle/tokenizers into
|
|
627
|
+
`--all-features` builds.
|
|
628
|
+
|
|
629
|
+
## Format coverage
|
|
630
|
+
|
|
631
|
+
Chunk-level coverage of the DjVu v3 format, for readers who need to know
|
|
632
|
+
exactly what decodes and what encodes:
|
|
633
|
+
|
|
634
|
+
The fresh-encode versus existing-document mutation contract is expanded in
|
|
635
|
+
[`docs/writer-coverage.md`](docs/writer-coverage.md).
|
|
636
|
+
|
|
637
|
+
| Format element | Decode | Encode |
|
|
638
|
+
|----------------|--------|--------|
|
|
639
|
+
| IFF container (`FORM:DJVU`, `FORM:DJVM`) | ✓ zero-copy parser | ✓ |
|
|
640
|
+
| JB2 bilevel images (`Sjbz`), shared dictionaries (`Djbz` via `INCL`) | ✓ ZP arithmetic coding + symbol dictionary | ✓ incl. multi-page shared Djbz |
|
|
641
|
+
| IW44 wavelet images (`BG44` / `FG44`) | ✓ planar YCbCr, multiple refinement chunks | ✓ color and grayscale |
|
|
642
|
+
| G4/MMR fax images (`Smmr`, ITU-T T.6) | ✓ | ✓ (explicit `BilevelCodec::Smmr` / `--bilevel-codec smmr`) |
|
|
643
|
+
| JPEG background/foreground (`BGjp` / `FGjp`) | ✓ | — (encoder emits IW44) |
|
|
644
|
+
| Foreground palette (`FGbz`) | ✓ | ✓ (layered encoder) |
|
|
645
|
+
| BZZ compression (BWT + MTF + ZP) | ✓ | ✓ |
|
|
646
|
+
| Text layer (`TXTa` / `TXTz`), zone hierarchy down to characters | ✓ | ✓ (incl. OCR injection) |
|
|
647
|
+
| Annotations (`ANTa` / `ANTz`): hyperlinks, map areas, colors | ✓ | ✓ |
|
|
648
|
+
| Bookmarks (`NAVM`) | ✓ | ✓ |
|
|
649
|
+
| Multi-page directory (`DIRM`), bundled and indirect | ✓ | ✓ (DjVuLibre-clean directory v1) |
|
|
650
|
+
| Thumbnails (`TH44`) | ✓ | ✓ (`--thumbnails`) |
|
|
651
|
+
| Metadata (`METa` / `METz`) | ✓ | ✓ (`PageEncoder::with_metadata`; `PageMut::set_metadata`) |
|
|
652
|
+
| Legacy standalone `FORM:BM44` / `FORM:PM44` files | ✓ | — |
|
|
653
|
+
| Unknown chunk IDs | preserved byte-exact for round-trip | n/a |
|
|
654
|
+
|
|
655
|
+
The codec internals are also published as standalone workspace crates for
|
|
656
|
+
focused consumers: [`djvu-iff`](crates/djvu-iff), [`djvu-bzz`](crates/djvu-bzz),
|
|
657
|
+
[`djvu-bitmap`](crates/djvu-bitmap), [`djvu-jb2`](crates/djvu-jb2),
|
|
658
|
+
[`djvu-pixmap`](crates/djvu-pixmap), [`djvu-iw44`](crates/djvu-iw44), and
|
|
659
|
+
[`djvu-zp`](crates/djvu-zp). All of them (and the codec modules of the main
|
|
660
|
+
crate) are `no_std`-compatible with `alloc` only, and are continuously fuzzed
|
|
661
|
+
via in-tree libFuzzer targets and OSS-Fuzz project files.
|
|
662
|
+
|
|
663
|
+
## Status & limitations
|
|
664
|
+
|
|
665
|
+
Honest boundaries, so you can decide fast:
|
|
666
|
+
|
|
667
|
+
- **Library + CLI, not a viewer.** There is no GUI; the WASM demo is the
|
|
668
|
+
closest thing to one.
|
|
669
|
+
- **Python bindings are source-only for now.** The `djvu-py` package is not
|
|
670
|
+
published to PyPI yet — install it from the repository checkout.
|
|
671
|
+
- **Indirect DJVM mutation is indirect-only via two paths.**
|
|
672
|
+
`DjVuDocumentMut::from_bytes` + `page_mut` on an indirect index errors;
|
|
673
|
+
use `from_indirect_resolved` (rebundles) or `IndirectRewritePlan` (rewrites
|
|
674
|
+
component files; per-file atomic, whole-commit not transactional).
|
|
675
|
+
- **Lazy async loading does not cover indirect DJVM** — bundled `FORM:DJVM`
|
|
676
|
+
and single-page `FORM:DJVU` only; indirect returns a clean `Unsupported`
|
|
677
|
+
error.
|
|
678
|
+
- **`create_indirect` does not emit shared `DJVI` dictionary components** —
|
|
679
|
+
build a bundled document with `djvu merge` when pages share a dictionary.
|
|
680
|
+
- **Encoder size parity is corpus- and profile-dependent.** Run the
|
|
681
|
+
reproducible [`encoder parity scorecard`](docs/encoder-parity.md) to compare
|
|
682
|
+
the same raster through DjVuLibre 3.5.29's `c44`/`cjb2` and the archival-safe
|
|
683
|
+
`PageEncoder` profiles. The 2026-07-16 snapshot ranges from 1.025–1.040×
|
|
684
|
+
`c44` for IW44 photo pages — at matched-or-better fidelity (decoded PSNR/SSIM
|
|
685
|
+
meet or exceed `c44` on the measured pages) — and 0.952–2.100× `cjb2` for the
|
|
686
|
+
public direct JB2 lossless profile; every measured output passed its
|
|
687
|
+
interop/fidelity gate. The earlier IW44 gap (up to 1.345×, and lower fidelity)
|
|
688
|
+
came from two encoder bugs since fixed: an activation threshold that stranded
|
|
689
|
+
dense-page coefficients (`IW44_LUMA_PLATEAU`) and a colour transform that did
|
|
690
|
+
not match the decoder's Pigeon `YCbCr` basis (`IW44_PIGEON_COLOR`); see
|
|
691
|
+
`PERF_EXPERIMENTS.md`. Same-size record-6 and lossy rec-7 remain experimental
|
|
692
|
+
and are tracked in [`docs/jb2-size-gap-plan.md`](docs/jb2-size-gap-plan.md).
|
|
693
|
+
- **Document optimization is conservative in the first slice.** `djvu optimize`
|
|
694
|
+
currently removes only semantically inert `FREE` padding and reports unmet
|
|
695
|
+
size targets; archival codec search, progress callbacks, and cancellation
|
|
696
|
+
remain planned. See [`docs/optimizer.md`](docs/optimizer.md); it always writes
|
|
697
|
+
a separate output file.
|
|
698
|
+
- **OCR: Tesseract is the supported recognition backend.** `OcrOptions`
|
|
699
|
+
(languages, dpi) are honored by Tesseract only; the `ocr-onnx` neural
|
|
700
|
+
pipeline is CLI-live but experimental (fixed pinned models, options
|
|
701
|
+
ignored) and `ocr-neural` is a placeholder that returns an error.
|
|
702
|
+
|
|
703
|
+
## Feature flags
|
|
704
|
+
|
|
705
|
+
| Flag | Default | Description |
|
|
706
|
+
|------|---------|-------------|
|
|
707
|
+
| `std` | enabled | `DjVuDocument`, file I/O, rendering — the decode-only surface |
|
|
708
|
+
| `pdf` | disabled | PDF export via `djvu_to_pdf` (owns `miniz_oxide` + `jpeg-encoder`) |
|
|
709
|
+
| `cli` | disabled | Build the `djvu` command-line binary (implies `pdf` and `cbz`) |
|
|
710
|
+
| `cbz` | disabled | CBZ (comic-book ZIP) export — backs `render --format cbz` (owns `zip`) |
|
|
711
|
+
| `tiff` | disabled | TIFF export (`djvu_to_tiff`) **and** TIFF encode input for `djvu encode` / `decode_image_to_pixmap` |
|
|
712
|
+
| `async` | disabled | Async render API and lazy `AsyncRead + AsyncSeek` document loading |
|
|
713
|
+
| `parallel` | disabled | Parallel multi-page render via `rayon` (`render_pages_parallel`) |
|
|
714
|
+
| `jpeg` | disabled | Standalone JPEG decode without full `std` (JPEG is included in `std` by default) |
|
|
715
|
+
| `mmap` | disabled | Memory-mapped file I/O via `memmap2` (`MmapDocument::open`) |
|
|
716
|
+
| `serde` | disabled | `Serialize` + `Deserialize` for all public data types |
|
|
717
|
+
| `image` | disabled | `image::ImageDecoder` impl via `DjVuDecoder` — integrates with the `image` crate |
|
|
718
|
+
| `epub` | disabled | EPUB 3 export via `djvu_to_epub` — page images, text overlay, bookmarks as nav (owns `zip`) |
|
|
719
|
+
| `wasm` | disabled | WebAssembly bindings via `wasm-bindgen` (`WasmDocument`, `WasmPage`) |
|
|
720
|
+
| `wasm-lazy` | disabled | Lazy Range-based document loading in the browser: a JS `(offset, len)` reader fetches only the pages you open |
|
|
721
|
+
| `wasm-threads` | disabled | wasm32 thread pool (rayon via Web Workers); requires a nightly toolchain, not part of the stable CI gate |
|
|
722
|
+
| `ocr-tesseract` | disabled | OCR recognition via a system Tesseract installation (the supported OCR backend) |
|
|
723
|
+
| `ocr-onnx` | disabled | Experimental neural OCR via `tract-onnx` (#693): pinned manifest + SHA-256-verified weights, DBNet detection, Cyrillic CTC recognition, CLI `--backend onnx` |
|
|
724
|
+
| `ocr-neural` | disabled | Placeholder backend only — `CandleBackend::load` returns a clear unsupported error |
|
|
725
|
+
| `ocr-neural-candle` | disabled | Deprecated no-op alias for `ocr-neural` |
|
|
726
|
+
| `experimental` | disabled | Experimental JB2 encoder paths used by internal example binaries |
|
|
727
|
+
| `iw44-probe` | disabled | IW44 encoder diagnostics probe (dev-only) |
|
|
728
|
+
| `alloc-profile` | disabled | dhat allocation-profiling harness for `examples/alloc_profile.rs` (dev-only) |
|
|
729
|
+
|
|
730
|
+
Without `std`, the crate provides IFF parsing, BZZ decompression, JB2/IW44 decoding,
|
|
731
|
+
text/annotation parsing — all codec primitives that work on byte slices.
|
|
732
|
+
|
|
733
|
+
## API stability & compatibility
|
|
734
|
+
|
|
735
|
+
The full contract lives in [`docs/api-compatibility.md`](docs/api-compatibility.md)
|
|
736
|
+
(policy) and [`docs/feature-matrix.md`](docs/feature-matrix.md) (supported
|
|
737
|
+
combinations and targets), and is enforced in CI. In short:
|
|
738
|
+
|
|
739
|
+
- **Stable surface** — the document model (`Document`/`Page`, `DjVuDocument`/
|
|
740
|
+
`DjVuPage`), the render entry points, the codec entry points, the parsers, and
|
|
741
|
+
the writer `djvu_to_*` functions. Follows SemVer; breakage is caught by
|
|
742
|
+
`cargo-semver-checks`.
|
|
743
|
+
- **Experimental / placeholder** — `experimental`, `iw44-probe`, `alloc-profile`,
|
|
744
|
+
`ocr-onnx`, `wasm-threads`, and the `ocr-neural` placeholder. These may change
|
|
745
|
+
in any release and are the ones marked *Experimental*/*Placeholder* in the
|
|
746
|
+
feature table above.
|
|
747
|
+
- **Deprecated (kept for ≥ 2 minor releases / 90 days)** — the `bzz_new` and
|
|
748
|
+
`iw44_new` module aliases and the `ocr-neural-candle` feature alias.
|
|
749
|
+
- **MSRV** — Rust 1.88, a required CI gate.
|
|
750
|
+
- **Thread-safety** — `Document`, `DjVuDocument`, `DjVuPage`, pixel buffers, and
|
|
751
|
+
the parsed content/error types are `Send + Sync`; the mutable editor is
|
|
752
|
+
`Send`; `LazyDocument<R>` inherits its thread-safety from `R`. Asserted in
|
|
753
|
+
[`tests/send_sync_contract.rs`](tests/send_sync_contract.rs).
|
|
754
|
+
- **Untrusted input** — no public parse/decode/render entry point panics on any
|
|
755
|
+
input; malformed bytes surface as typed errors. Covered by
|
|
756
|
+
[`tests/panic_free_corpus.rs`](tests/panic_free_corpus.rs), proptests, and
|
|
757
|
+
libFuzzer/OSS-Fuzz targets.
|
|
758
|
+
- **Resource limits** — decode/render inherit documented, bounded memory/work
|
|
759
|
+
ceilings; exceeding one returns a typed error naming the codec and axis. See
|
|
760
|
+
[`SECURITY.md`](SECURITY.md#decode-time-resource-ceilings).
|
|
761
|
+
|
|
762
|
+
## Performance
|
|
763
|
+
|
|
764
|
+
See [BENCHMARKS_RESULTS.md](BENCHMARKS_RESULTS.md) for Criterion numbers,
|
|
765
|
+
methodology, and a DjVuLibre comparison (run via
|
|
766
|
+
[`scripts/bench_djvulibre.sh`](scripts/bench_djvulibre.sh) +
|
|
767
|
+
[`scripts/djvulibre_compare.py`](scripts/djvulibre_compare.py)).
|
|
768
|
+
Historical multi-platform results are in [BENCHMARKS.md](BENCHMARKS.md),
|
|
769
|
+
including the local WASM scalar-vs-simd128 harness.
|
|
770
|
+
|
|
771
|
+
Recent targeted experiments are recorded in
|
|
772
|
+
[PERF_EXPERIMENTS.md](PERF_EXPERIMENTS.md), including:
|
|
773
|
+
|
|
774
|
+
- **#233 lazy async loading:** a 100 MiB padded 520-page DJVM reached first
|
|
775
|
+
pixel in **491.469 ms** while reading only **28,578 bytes** at simulated
|
|
776
|
+
12.5 MiB/s throughput.
|
|
777
|
+
- **#189 x86-64-v3 AVX2 validation:** existing AVX2 decode paths showed
|
|
778
|
+
`iw44_decode_corpus_color` **-18.88%** and `iw44_decode_first_chunk`
|
|
779
|
+
**-4.85%** on GitHub-hosted x86_64, with one sub4 partial-decode regression
|
|
780
|
+
recorded for follow-up.
|
|
781
|
+
- **#258 shared-Djbz clustering:** Hamming shared clustering was rejected as
|
|
782
|
+
default; byte-exact shared-Djbz remains the measured safe path.
|
|
783
|
+
|
|
784
|
+
## Minimum supported Rust version (MSRV)
|
|
785
|
+
|
|
786
|
+
Rust **1.88** (edition 2024 — let-chains stabilized in 1.88)
|
|
787
|
+
|
|
788
|
+
## Roadmap
|
|
789
|
+
|
|
790
|
+
See [GitHub milestones](https://github.com/matyushkin/djvu-rs/milestones) for the full roadmap and progress tracking.
|
|
791
|
+
|
|
792
|
+
## License
|
|
793
|
+
|
|
794
|
+
MIT. See [LICENSE](LICENSE).
|
|
795
|
+
|
|
796
|
+
## Specification
|
|
797
|
+
|
|
798
|
+
Written from the public DjVu v3 specification:
|
|
799
|
+
- https://www.sndjvu.org/spec.html
|
|
800
|
+
- https://djvu.sourceforge.net/spec/DjVu3Spec.djvu (the spec is itself a DjVu file)
|
|
801
|
+
|
|
802
|
+
No code derived from GPL-licensed DjVuLibre or any other GPL source.
|
|
803
|
+
All algorithms are independent implementations from the spec.
|