#![cfg_attr(not(feature = "std"), no_std)]
#![deny(unsafe_code)]
#[cfg(not(feature = "std"))]
extern crate alloc;
pub mod iff;
pub mod error;
pub(crate) mod info;
#[allow(unused_imports)]
pub(crate) use djvu_zp as zp_impl;
#[allow(dead_code)]
pub mod bzz;
pub use bzz as bzz_new;
#[cfg(feature = "std")]
pub mod bzz_encode;
#[cfg(feature = "std")]
pub mod djvm;
pub(crate) mod dirm;
pub mod jb2;
pub mod iw44;
pub use iw44 as iw44_new;
#[cfg(feature = "std")]
pub mod iw44_encode;
#[cfg(feature = "std")]
pub mod jb2_encode;
#[cfg(feature = "std")]
pub mod fgbz_encode;
#[cfg(feature = "std")]
pub mod djvu_encode;
#[cfg(feature = "std")]
pub mod thumbnail;
#[cfg(feature = "std")]
pub mod segment;
#[cfg(feature = "std")]
pub mod png_io;
pub mod djvu_document;
#[cfg(feature = "std")]
pub mod djvu_mut;
pub mod djvu_render;
#[cfg(feature = "std")]
pub mod quality;
pub(crate) mod fgbz;
pub mod text;
pub mod annotation;
pub mod metadata;
mod sexp;
#[cfg(feature = "std")]
mod export_common;
#[cfg(feature = "std")]
pub mod pdf;
#[cfg(feature = "epub")]
pub mod epub;
#[cfg(feature = "tiff")]
pub mod tiff_export;
#[cfg(feature = "async")]
pub mod djvu_async;
#[cfg(feature = "image")]
pub mod image_compat;
#[cfg(feature = "std")]
pub mod text_serialize;
#[cfg(feature = "std")]
pub mod ocr;
#[cfg(feature = "ocr-tesseract")]
pub mod ocr_tesseract;
#[cfg(feature = "ocr-onnx")]
pub mod ocr_onnx;
#[cfg(feature = "ocr-neural")]
pub mod ocr_neural;
#[cfg(feature = "std")]
pub mod text_encode;
#[cfg(feature = "std")]
pub mod navm_encode;
pub mod smmr;
#[cfg(feature = "std")]
pub mod chunk_encode;
#[cfg(feature = "wasm")]
pub mod wasm;
#[cfg(feature = "std")]
mod foreign;
#[cfg(feature = "std")]
#[allow(unsafe_code)]
pub mod ffi;
pub use error::{BzzError, DjVuError, IffError, Iw44Error, Jb2Error};
pub use djvu_document::{DjVuBookmark, DjVuDocument, DjVuPage, DocError};
pub use info::{PageInfo, Rotation};
#[doc(hidden)]
pub(crate) mod bitmap;
#[doc(hidden)]
pub(crate) mod pixmap;
pub use bitmap::Bitmap;
pub use pixmap::{GrayPixmap, Pixmap};
#[cfg(feature = "std")]
pub use text::{TextLayer, TextZone, TextZoneKind};
#[cfg(feature = "std")]
pub type Bookmark = DjVuBookmark;
#[cfg(feature = "std")]
pub use error::LegacyError as Error;
#[cfg(feature = "std")]
pub struct Document {
doc: DjVuDocument,
}
#[cfg(feature = "std")]
impl Document {
pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, Error> {
let data = std::fs::read(path.as_ref())
.map_err(|e| Error::FormatError(format!("failed to read file: {}", e)))?;
Self::from_bytes(data)
}
pub fn open_dir(path: impl AsRef<std::path::Path>) -> Result<Self, Error> {
let path = path.as_ref();
let data = std::fs::read(path)
.map_err(|e| Error::FormatError(format!("failed to read file: {}", e)))?;
let base_dir = path.parent().unwrap_or(std::path::Path::new("."));
let doc = DjVuDocument::parse_from_dir(&data, base_dir)
.map_err(|e| Error::FormatError(e.to_string()))?;
Ok(Document { doc })
}
pub fn from_reader(reader: impl std::io::Read) -> Result<Self, Error> {
let mut reader = reader;
let mut data = Vec::new();
reader
.read_to_end(&mut data)
.map_err(|e| Error::FormatError(format!("failed to read: {}", e)))?;
Self::from_bytes(data)
}
pub fn from_bytes(data: Vec<u8>) -> Result<Self, Error> {
let backing: std::sync::Arc<dyn AsRef<[u8]> + Send + Sync> = std::sync::Arc::new(data);
let doc =
DjVuDocument::parse_backed(backing).map_err(|e| Error::FormatError(e.to_string()))?;
Ok(Document { doc })
}
pub fn bookmarks(&self) -> Result<Vec<Bookmark>, Error> {
Ok(self.doc.bookmarks().to_vec())
}
pub fn page_count(&self) -> usize {
self.doc.page_count()
}
pub fn page(&self, index: usize) -> Result<Page<'_>, Error> {
let page = self
.doc
.page(index)
.map_err(|e| Error::FormatError(e.to_string()))?;
Ok(Page { page, index })
}
pub fn thumbnails(&self, max_w: u32, max_h: u32) -> Vec<Result<Pixmap, Error>> {
self.thumbnails_with_strategy(max_w, max_h, ThumbnailStrategy::Auto)
}
pub fn thumbnails_with_strategy(
&self,
max_w: u32,
max_h: u32,
strategy: ThumbnailStrategy,
) -> Vec<Result<Pixmap, Error>> {
let indices: Vec<usize> = (0..self.page_count()).collect();
let build = |&i: &usize| -> Result<Pixmap, Error> {
let page = self.page(i)?;
page.thumbnail_with_strategy(max_w, max_h, strategy)
};
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
indices.par_iter().map(build).collect()
}
#[cfg(not(feature = "parallel"))]
{
indices.iter().map(build).collect()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ThumbnailStrategy {
#[default]
Auto,
RenderOnly,
Th44Only,
}
#[cfg(feature = "std")]
pub struct Page<'a> {
page: &'a DjVuPage,
index: usize,
}
#[cfg(feature = "std")]
impl<'a> Page<'a> {
pub fn width(&self) -> u32 {
self.page.width() as u32
}
pub fn height(&self) -> u32 {
self.page.height() as u32
}
pub fn display_width(&self) -> u32 {
self.display_dims().0
}
pub fn display_height(&self) -> u32 {
self.display_dims().1
}
fn display_dims(&self) -> (u32, u32) {
djvu_render::display_dimensions(self.page)
}
fn opts_for_scale(&self, scale: f32) -> djvu_render::RenderOptions {
let (dw, dh) = self.display_dims();
let w = ((dw as f32 * scale).round() as u32).max(1);
let h = ((dh as f32 * scale).round() as u32).max(1);
djvu_render::RenderOptions {
width: w,
height: h,
..Default::default()
}
}
fn opts_for_size(&self, width: u32, height: u32) -> djvu_render::RenderOptions {
let mut opts = djvu_render::RenderOptions::fit_to_width(self.page, width);
opts.height = height;
opts
}
pub fn render_with(&self, opts: &djvu_render::RenderOptions) -> Result<Pixmap, Error> {
djvu_render::render_pixmap(self.page, opts).map_err(Self::render_err)
}
pub fn dpi(&self) -> u16 {
self.page.dpi()
}
pub fn size_at_dpi(&self, target_dpi: f32) -> (u32, u32) {
export_common::size_at_dpi(self.page, target_dpi)
}
pub fn index(&self) -> usize {
self.index
}
pub fn rotation(&self) -> info::Rotation {
self.page.rotation()
}
fn render_err(e: djvu_render::RenderError) -> Error {
Error::FormatError(e.to_string())
}
pub fn decode_mask(&self) -> Result<Option<Bitmap>, Error> {
self.page
.extract_mask()
.map_err(|e| Error::FormatError(e.to_string()))
}
pub fn render(&self) -> Result<Pixmap, Error> {
self.render_with(&self.opts_for_scale(1.0))
}
pub fn render_to_size(&self, width: u32, height: u32) -> Result<Pixmap, Error> {
self.render_with(&self.opts_for_size(width, height))
}
pub fn thumbnail(&self) -> Result<Option<Pixmap>, Error> {
self.page
.thumbnail()
.map_err(|e| Error::FormatError(e.to_string()))
}
pub fn thumbnail_with_strategy(
&self,
max_w: u32,
max_h: u32,
strategy: ThumbnailStrategy,
) -> Result<Pixmap, Error> {
match strategy {
ThumbnailStrategy::RenderOnly => self.render_fit_to_box(max_w, max_h),
ThumbnailStrategy::Th44Only => {
let pm = self.thumbnail()?.ok_or_else(|| {
Error::FormatError(format!(
"page {} has no embedded TH44 thumbnail (Th44Only strategy)",
self.index
))
})?;
Ok(Self::fit_within(pm, max_w, max_h))
}
ThumbnailStrategy::Auto => match self.thumbnail()? {
Some(pm) => Ok(Self::fit_within(pm, max_w, max_h)),
None => self.render_fit_to_box(max_w, max_h),
},
}
}
fn render_fit_to_box(&self, max_w: u32, max_h: u32) -> Result<Pixmap, Error> {
let opts = djvu_render::RenderOptions::fit_to_box(self.page, max_w, max_h);
self.render_with(&opts)
}
fn fit_within(pm: Pixmap, max_w: u32, max_h: u32) -> Pixmap {
let max_w = max_w.max(1);
let max_h = max_h.max(1);
if pm.width <= max_w && pm.height <= max_h {
return pm;
}
let scale_w = max_w as f64 / pm.width.max(1) as f64;
let scale_h = max_h as f64 / pm.height.max(1) as f64;
let scale = scale_w.min(scale_h);
let tw = ((pm.width as f64 * scale).round() as u32).max(1);
let th = ((pm.height as f64 * scale).round() as u32).max(1);
crate::pixmap::scale_lanczos3(&pm, tw, th)
}
pub fn text_layer(&self) -> Result<Option<TextLayer>, Error> {
self.page
.text_layer()
.map_err(|e| Error::FormatError(e.to_string()))
}
pub fn text(&self) -> Result<Option<String>, Error> {
Ok(self.text_layer()?.map(|tl| tl.text))
}
}
#[cfg(feature = "std")]
#[allow(dead_code)]
const _: () = {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
fn assertions() {
assert_send::<Document>();
assert_sync::<Document>();
}
};
#[cfg(test)]
mod tests {
use super::*;
fn chicken() -> &'static std::path::Path {
std::path::Path::new("references/djvujs/library/assets/chicken.djvu")
}
fn chicken_bytes() -> Vec<u8> {
std::fs::read(chicken()).unwrap()
}
#[test]
fn document_open_succeeds() {
let doc = Document::open(chicken()).unwrap();
assert!(doc.page_count() > 0);
}
#[test]
fn document_from_reader_succeeds() {
let f = std::fs::File::open(chicken()).unwrap();
let doc = Document::from_reader(f).unwrap();
assert!(doc.page_count() > 0);
}
#[test]
fn document_from_bytes_succeeds() {
let doc = Document::from_bytes(chicken_bytes()).unwrap();
assert!(doc.page_count() > 0);
}
#[test]
fn document_bookmarks_returns_vec() {
let doc = Document::open(chicken()).unwrap();
let bm = doc.bookmarks().unwrap();
assert!(bm.is_empty() || !bm.is_empty());
}
#[test]
fn document_page_out_of_bounds_returns_error() {
let doc = Document::open(chicken()).unwrap();
assert!(doc.page(999).is_err());
}
#[test]
fn page_dimensions_and_dpi() {
let doc = Document::open(chicken()).unwrap();
let page = doc.page(0).unwrap();
assert!(page.width() > 0);
assert!(page.height() > 0);
assert_eq!(page.display_width(), page.width());
assert_eq!(page.display_height(), page.height());
assert!(page.dpi() > 0);
assert_eq!(page.index(), 0);
let _ = page.rotation();
}
#[test]
fn page_size_at_dpi() {
let doc = Document::open(chicken()).unwrap();
let page = doc.page(0).unwrap();
let (w, h) = page.size_at_dpi(72.0);
assert!(w > 0 && h > 0);
}
#[test]
fn page_render_and_render_to_size() {
let doc = Document::open(chicken()).unwrap();
let page = doc.page(0).unwrap();
let pm = page.render().unwrap();
assert!(pm.width > 0 && pm.height > 0);
let pm2 = page.render_to_size(50, 60).unwrap();
assert_eq!(pm2.width, 50);
assert_eq!(pm2.height, 60);
}
#[test]
fn page_render_with_opts() {
use crate::djvu_render::RenderOptions;
let doc = Document::open(chicken()).unwrap();
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 32,
height: 32,
..Default::default()
};
let pm = page.render_with(&opts).unwrap();
assert!(pm.width > 0);
}
#[test]
fn page_decode_mask_does_not_panic() {
let doc = Document::open(chicken()).unwrap();
let page = doc.page(0).unwrap();
let _ = page.decode_mask().unwrap();
}
#[test]
fn page_thumbnail_does_not_panic() {
let doc = Document::open(chicken()).unwrap();
let page = doc.page(0).unwrap();
let _ = page.thumbnail().unwrap();
}
#[test]
fn page_text_layer_and_text() {
let doc = Document::open(chicken()).unwrap();
let page = doc.page(0).unwrap();
let _ = page.text_layer().unwrap();
let _ = page.text().unwrap();
}
#[test]
fn page_render_with_invalid_dims_returns_error() {
use crate::djvu_render::RenderOptions;
let doc = Document::open(chicken()).unwrap();
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 0,
height: 0,
..Default::default()
};
let err = page.render_with(&opts).unwrap_err();
assert!(
matches!(err, Error::FormatError(_)),
"invalid dims must return FormatError, got {err:?}"
);
}
#[test]
fn document_open_dir_bundled_succeeds() {
let doc = Document::open_dir(chicken()).unwrap();
assert!(doc.page_count() > 0);
}
fn th44_bundle_bytes() -> Vec<u8> {
let mut p1 = Bitmap::new(64, 48);
for y in 4..44 {
p1.set_black(8, y);
p1.set_black(y % 60, 8);
}
let mut p2 = Bitmap::new(64, 48);
for x in 4..60 {
p2.set_black(x, 20);
}
crate::jb2_encode::encode_djvm_bundle_jb2_with_thumbnails(
&[p1, p2],
100, crate::jb2_encode::BUNDLE_DEFAULT_DPI,
true,
)
}
fn corrupt_all_sjbz_payloads(bundle: &mut [u8]) {
let mut search_from = 0usize;
let mut corrupted = 0u32;
while let Some(rel) = bundle[search_from..].windows(4).position(|w| w == b"Sjbz") {
let pos = search_from + rel;
let len_start = pos + 4;
let len = u32::from_be_bytes([
bundle[len_start],
bundle[len_start + 1],
bundle[len_start + 2],
bundle[len_start + 3],
]) as usize;
let payload_start = len_start + 4;
for b in &mut bundle[payload_start..payload_start + len] {
*b = 0xFF;
}
corrupted += 1;
search_from = payload_start + len;
}
assert!(corrupted > 0, "test bundle must contain Sjbz chunks");
}
#[test]
fn thumbnails_auto_uses_th44_and_matches_th44_only() {
let bundle = th44_bundle_bytes();
let doc = Document::from_bytes(bundle).unwrap();
assert_eq!(doc.page_count(), 2);
let auto = doc.thumbnails(128, 128);
let th44_only = doc.thumbnails_with_strategy(128, 128, ThumbnailStrategy::Th44Only);
assert_eq!(auto.len(), 2);
assert_eq!(th44_only.len(), 2);
for i in 0..2 {
let a = auto[i].as_ref().expect("Auto must succeed");
let t = th44_only[i].as_ref().expect("Th44Only must succeed");
assert_eq!(a.width, t.width);
assert_eq!(a.height, t.height);
assert_eq!(
a.data, t.data,
"Auto must match Th44Only when TH44 is present"
);
}
}
#[test]
fn thumbnails_th44_path_never_touches_corrupted_jb2_background() {
let mut bundle = th44_bundle_bytes();
corrupt_all_sjbz_payloads(&mut bundle);
let doc = Document::from_bytes(bundle).unwrap();
assert_eq!(doc.page_count(), 2);
let render_only = doc.thumbnails_with_strategy(128, 128, ThumbnailStrategy::RenderOnly);
for (i, r) in render_only.iter().enumerate() {
assert!(
r.is_err(),
"page {i}: RenderOnly must fail against a corrupted Sjbz chunk"
);
}
let th44_only = doc.thumbnails_with_strategy(128, 128, ThumbnailStrategy::Th44Only);
for (i, r) in th44_only.iter().enumerate() {
assert!(
r.is_ok(),
"page {i}: Th44Only must succeed even with a corrupted Sjbz chunk, got {r:?}"
);
}
let auto = doc.thumbnails(128, 128);
for (i, r) in auto.iter().enumerate() {
assert!(
r.is_ok(),
"page {i}: Auto must take the TH44 fast path and never touch the corrupted background, got {r:?}"
);
}
}
#[test]
fn thumbnails_th44_only_errors_without_th44() {
let doc = Document::open(chicken()).unwrap();
let results = doc.thumbnails_with_strategy(128, 128, ThumbnailStrategy::Th44Only);
assert_eq!(results.len(), doc.page_count());
for r in &results {
assert!(r.is_err(), "Th44Only must error when no TH44 chunk exists");
}
}
#[test]
fn thumbnails_auto_falls_back_to_render_without_th44() {
let doc = Document::open(chicken()).unwrap();
let auto = doc.thumbnails(64, 64);
let render_only = doc.thumbnails_with_strategy(64, 64, ThumbnailStrategy::RenderOnly);
assert_eq!(auto.len(), render_only.len());
for (a, r) in auto.iter().zip(render_only.iter()) {
let a = a.as_ref().expect("Auto must succeed via render fallback");
let r = r.as_ref().expect("RenderOnly must succeed");
assert_eq!(a.width, r.width);
assert_eq!(a.height, r.height);
assert_eq!(
a.data, r.data,
"Auto fallback must match RenderOnly exactly"
);
}
}
#[test]
fn thumbnails_never_upscale_a_small_th44_thumbnail() {
let bundle = th44_bundle_bytes();
let doc = Document::from_bytes(bundle).unwrap();
let page = doc.page(0).unwrap();
let native = page.thumbnail().unwrap().expect("page has TH44");
let grid = page
.thumbnail_with_strategy(4096, 4096, ThumbnailStrategy::Th44Only)
.unwrap();
assert_eq!(grid.width, native.width);
assert_eq!(grid.height, native.height);
}
#[test]
fn thumbnails_downscale_an_oversized_th44_thumbnail() {
let bundle = th44_bundle_bytes();
let doc = Document::from_bytes(bundle).unwrap();
let page = doc.page(0).unwrap();
let native = page.thumbnail().unwrap().expect("page has TH44");
assert!(native.width > 16 || native.height > 16);
let grid = page
.thumbnail_with_strategy(16, 16, ThumbnailStrategy::Th44Only)
.unwrap();
assert!(grid.width <= 16 && grid.height <= 16);
assert!(grid.width >= 1 && grid.height >= 1);
}
}