#![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;
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 })
}
}
#[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 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);
}
}