//! johnmcm.com's DjVu module, for /djvu-to-pdf and /open-file/djvu. //! //! djvu-rs (MIT, written from the DjVu specification) does the work. Its own //! WebAssembly bindings (WasmDocument, WasmPage: rendering and the text layer) //! are re-exported unchanged; the npm build of them has no PDF export or //! bookmarks, so this crate adds two functions: //! //! - `outline_json`: the book's bookmarks, each resolved to a page index. //! - `export_pdf`: the book (or a run of its pages) as a PDF, with its //! invisible text layer, bookmarks and links, reporting progress. use djvu_rs::djvm; use djvu_rs::djvu_document::{DjVuBookmark, DjVuDocument}; use djvu_rs::pdf::{djvu_to_pdf_to_writer_with_observer, PdfOptions}; use djvu_rs::ExportObserver; use serde_json::{json, Value}; use wasm_bindgen::prelude::*; pub use djvu_rs::wasm::*; fn js_err(e: impl std::fmt::Display) -> JsError { JsError::new(&e.to_string()) } /// The page a bookmark's DjVu URL points at: "#12", "#page=12", "#page_12" /// (1-based numbers, as djvu-rs's PDF outline reads them) or "#name", the id /// of a page component. fn bookmark_page(url: &str, page_ids: &[String]) -> Option { let frag = url.strip_prefix('#')?; if let Some(rest) = frag.strip_prefix("page") { let digits = rest.strip_prefix('=').or_else(|| rest.strip_prefix('_')).unwrap_or(rest).trim(); if let Ok(n) = digits.parse::() { return Some(n.saturating_sub(1)); } } if let Ok(n) = frag.trim().parse::() { return Some((n.max(1) - 1) as usize); } page_ids.iter().position(|id| id == frag) } fn bookmark_json(b: &DjVuBookmark, page_ids: &[String]) -> Value { json!({ "title": b.title, "page": bookmark_page(&b.url, page_ids), "children": b.children.iter().map(|c| bookmark_json(c, page_ids)).collect::>(), }) } /// `[{ title, page (0-based, or null), children }]` for the book's bookmarks. #[wasm_bindgen] pub fn outline_json(data: &[u8]) -> Result { let doc = DjVuDocument::parse(data).map_err(js_err)?; let page_ids: Vec = doc .component_directory() .map_err(js_err)? .into_iter() .filter(|c| c.kind == 'P') .map(|c| c.id) .collect(); let list: Vec = doc.bookmarks().iter().map(|b| bookmark_json(b, &page_ids)).collect(); Ok(Value::Array(list).to_string()) } struct Progress<'a> { report: &'a js_sys::Function, } impl ExportObserver for Progress<'_> { fn on_progress(&mut self, done: usize, total: usize) { let _ = self.report.call2(&JsValue::NULL, &JsValue::from(done as u32), &JsValue::from(total as u32)); } } /// The book as a PDF. /// /// `start`..`end` (0-based, end exclusive) picks a run of pages; the whole /// book when it covers every page. A run is cut out as a DjVu document of its /// own first, which has no bookmarks. `jpeg_quality` 0 keeps every image /// lossless (Deflate); `dpi` 0 keeps each page's own resolution; `g4` tries /// fax (CCITT G4) compression on black-and-white layers and keeps whichever /// is smaller. `progress(done, total)` is called after each page. #[wasm_bindgen] pub fn export_pdf( data: &[u8], start: u32, end: u32, jpeg_quality: u8, dpi: u32, g4: bool, progress: &js_sys::Function, ) -> Result, JsError> { let whole = DjVuDocument::parse(data).map_err(js_err)?; let count = whole.page_count(); let (start, end) = (start as usize, (end as usize).min(count)); let doc = if start == 0 && end == count { whole } else { drop(whole); let part = djvm::split(data, start, end).map_err(js_err)?; DjVuDocument::parse(&part).map_err(js_err)? }; let opts = PdfOptions { jpeg_quality: if jpeg_quality == 0 { None } else { Some(jpeg_quality) }, output_dpi: dpi, adaptive_raster: jpeg_quality != 0, ccitt_g4: g4, mrc: false, }; let mut out = Vec::new(); let mut observer = Progress { report: progress }; // Links name pages of the whole book; the patched exporter resolves them // against it, shifts them for a page range and drops those left out. djvu_rs::set_link_scope(data, start, end); let written = djvu_to_pdf_to_writer_with_observer(&doc, &opts, &mut out, &mut observer); djvu_rs::clear_link_scope(); written.map_err(js_err)?; Ok(out) }