use std::sync::Arc;
use wasm_bindgen::prelude::*;
use crate::{
djvu_document::DjVuDocument,
djvu_render::{
RenderOptions, Resampling, UserRotation, render_coarse, render_pixmap, render_progressive,
},
};
#[wasm_bindgen]
pub struct WasmDocument {
inner: Arc<DjVuDocument>,
}
#[wasm_bindgen]
impl WasmDocument {
pub fn from_bytes(data: &[u8]) -> Result<WasmDocument, JsError> {
let doc = DjVuDocument::parse(data).map_err(|e| JsError::new(&e.to_string()))?;
Ok(WasmDocument {
inner: Arc::new(doc),
})
}
pub fn page_count(&self) -> u32 {
self.inner.page_count() as u32
}
pub fn page(&self, index: u32) -> Result<WasmPage, JsError> {
let count = self.inner.page_count();
if index as usize >= count {
return Err(JsError::new(&format!(
"page index {index} out of range (document has {count} pages)"
)));
}
Ok(WasmPage {
doc: Arc::clone(&self.inner),
index: index as usize,
})
}
}
#[wasm_bindgen]
pub struct WasmPage {
doc: Arc<DjVuDocument>,
index: usize,
}
#[wasm_bindgen]
impl WasmPage {
pub fn dpi(&self) -> u32 {
self.doc
.page(self.index)
.map(|p| p.dpi() as u32)
.unwrap_or(300)
}
pub fn width_at(&self, target_dpi: u32) -> u32 {
self.doc
.page(self.index)
.map(|p| {
let scale = target_dpi as f32 / p.dpi() as f32;
((p.width() as f32 * scale).round() as u32).max(1)
})
.unwrap_or(1)
}
pub fn height_at(&self, target_dpi: u32) -> u32 {
self.doc
.page(self.index)
.map(|p| {
let scale = target_dpi as f32 / p.dpi() as f32;
((p.height() as f32 * scale).round() as u32).max(1)
})
.unwrap_or(1)
}
pub fn text(&self) -> Result<Option<String>, JsError> {
let page = self
.doc
.page(self.index)
.map_err(|e| JsError::new(&e.to_string()))?;
page.text().map_err(|e| JsError::new(&e.to_string()))
}
pub fn text_zones_json(&self, target_dpi: u32) -> Result<Option<String>, JsError> {
let page = self
.doc
.page(self.index)
.map_err(|e| JsError::new(&e.to_string()))?;
let scale = target_dpi as f32 / page.dpi() as f32;
let render_w = ((page.width() as f32 * scale).round() as u32).max(1);
let render_h = ((page.height() as f32 * scale).round() as u32).max(1);
let Some(layer) = page
.text_layer_at_size(render_w, render_h)
.map_err(|e| JsError::new(&e.to_string()))?
else {
return Ok(None);
};
let mut buf = String::from("[");
let mut first = true;
for zone in &layer.zones {
collect_leaf_zones(zone, &mut buf, &mut first);
}
buf.push(']');
Ok(Some(buf))
}
pub fn bg44_chunk_count(&self) -> u32 {
self.doc
.page(self.index)
.map(|p| p.bg44_chunks().len() as u32)
.unwrap_or(0)
}
pub fn render_coarse(
&self,
target_dpi: u32,
) -> Result<Option<js_sys::Uint8ClampedArray>, JsError> {
let page = self
.doc
.page(self.index)
.map_err(|e| JsError::new(&e.to_string()))?;
let opts = render_opts_for_dpi(page, target_dpi);
let pm = render_coarse(page, &opts).map_err(|e| JsError::new(&e.to_string()))?;
Ok(pm.map(|p| {
let arr = js_sys::Uint8ClampedArray::new_with_length(p.data.len() as u32);
arr.copy_from(&p.data);
arr
}))
}
pub fn render_progressive(
&self,
target_dpi: u32,
chunk_n: u32,
) -> Result<js_sys::Uint8ClampedArray, JsError> {
let page = self
.doc
.page(self.index)
.map_err(|e| JsError::new(&e.to_string()))?;
let opts = render_opts_for_dpi(page, target_dpi);
let pm = render_progressive(page, &opts, chunk_n as usize)
.map_err(|e| JsError::new(&e.to_string()))?;
let arr = js_sys::Uint8ClampedArray::new_with_length(pm.data.len() as u32);
arr.copy_from(&pm.data);
Ok(arr)
}
pub fn render(&self, target_dpi: u32) -> Result<js_sys::Uint8ClampedArray, JsError> {
let page = self
.doc
.page(self.index)
.map_err(|e| JsError::new(&e.to_string()))?;
let opts = render_opts_for_dpi(page, target_dpi);
let pm = render_pixmap(page, &opts).map_err(|e| JsError::new(&e.to_string()))?;
let arr = js_sys::Uint8ClampedArray::new_with_length(pm.data.len() as u32);
arr.copy_from(&pm.data);
Ok(arr)
}
}
fn render_opts_for_dpi(page: &crate::djvu_document::DjVuPage, target_dpi: u32) -> RenderOptions {
let scale = target_dpi as f32 / page.dpi() as f32;
let w = ((page.width() as f32 * scale).round() as u32).max(1);
let h = ((page.height() as f32 * scale).round() as u32).max(1);
RenderOptions {
width: w,
height: h,
scale,
bold: 0,
aa: false,
rotation: UserRotation::None,
permissive: true,
resampling: Resampling::Bilinear,
}
}
fn collect_leaf_zones(zone: &crate::text::TextZone, buf: &mut String, first: &mut bool) {
if zone.children.is_empty() {
let t = zone.text.trim();
if t.is_empty() {
return;
}
if !*first {
buf.push(',');
}
*first = false;
buf.push_str("{\"t\":\"");
json_escape_into(t, buf);
buf.push_str("\",\"x\":");
buf.push_str(&zone.rect.x.to_string());
buf.push_str(",\"y\":");
buf.push_str(&zone.rect.y.to_string());
buf.push_str(",\"w\":");
buf.push_str(&zone.rect.width.to_string());
buf.push_str(",\"h\":");
buf.push_str(&zone.rect.height.to_string());
buf.push('}');
} else {
for child in &zone.children {
collect_leaf_zones(child, buf, first);
}
}
}
fn json_escape_into(s: &str, buf: &mut String) {
for ch in s.chars() {
match ch {
'"' => buf.push_str("\\\""),
'\\' => buf.push_str("\\\\"),
'\n' => buf.push_str("\\n"),
'\r' => buf.push_str("\\r"),
'\t' => buf.push_str("\\t"),
c if (c as u32) < 0x20 => {
buf.push_str(&format!("\\u{:04x}", c as u32));
}
c => buf.push(c),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(test)]
mod native_tests {
use super::*;
fn boy_bytes() -> Vec<u8> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/boy.djvu");
std::fs::read(&path).expect("boy.djvu not found in tests/fixtures/")
}
#[test]
fn wasm_document_from_bytes_page_count() {
let bytes = boy_bytes();
let doc = DjVuDocument::parse(&bytes).expect("parse failed");
assert_eq!(doc.page_count(), 1);
}
#[test]
fn wasm_document_from_bytes_invalid_returns_error() {
assert!(DjVuDocument::parse(b"not a djvu file").is_err());
}
#[test]
fn wasm_page_dpi() {
let bytes = boy_bytes();
let doc = DjVuDocument::parse(&bytes).unwrap();
assert_eq!(doc.page(0).unwrap().dpi(), 100);
}
#[test]
fn wasm_page_dimensions_at_dpi() {
let bytes = boy_bytes();
let doc = DjVuDocument::parse(&bytes).unwrap();
let page = doc.page(0).unwrap();
let scale = 50_f32 / page.dpi() as f32; let w = ((page.width() as f32 * scale).round() as u32).max(1);
let h = ((page.height() as f32 * scale).round() as u32).max(1);
assert_eq!(w, 96);
assert_eq!(h, 128);
}
#[test]
fn wasm_page_render_pixel_count() {
let bytes = boy_bytes();
let doc = DjVuDocument::parse(&bytes).unwrap();
let page = doc.page(0).unwrap();
let scale = 150_f32 / page.dpi() as f32;
let w = ((page.width() as f32 * scale).round() as u32).max(1);
let h = ((page.height() as f32 * scale).round() as u32).max(1);
let opts = RenderOptions {
width: w,
height: h,
scale,
bold: 0,
aa: false,
rotation: UserRotation::None,
permissive: false,
resampling: Resampling::Bilinear,
};
let pm = render_pixmap(page, &opts).expect("render failed");
assert_eq!(pm.data.len(), (w * h * 4) as usize);
}
#[test]
fn wasm_render_coarse_color_page() {
let bytes = boy_bytes();
let doc = DjVuDocument::parse(&bytes).unwrap();
let page = doc.page(0).unwrap();
let scale = 150_f32 / page.dpi() as f32;
let w = ((page.width() as f32 * scale).round() as u32).max(1);
let h = ((page.height() as f32 * scale).round() as u32).max(1);
let opts = render_opts_for_dpi(page, 150);
let result = render_coarse(page, &opts).expect("render_coarse failed");
let pm = result.expect("expected Some for color page");
assert_eq!(pm.data.len(), (w * h * 4) as usize);
}
#[test]
fn wasm_bg44_chunk_count_color_page() {
let bytes = boy_bytes();
let doc = DjVuDocument::parse(&bytes).unwrap();
assert!(doc.page(0).unwrap().bg44_chunks().len() > 0);
}
#[test]
fn wasm_render_progressive_chunk0() {
let bytes = boy_bytes();
let doc = DjVuDocument::parse(&bytes).unwrap();
let page = doc.page(0).unwrap();
let scale = 150_f32 / page.dpi() as f32;
let w = ((page.width() as f32 * scale).round() as u32).max(1);
let h = ((page.height() as f32 * scale).round() as u32).max(1);
let opts = render_opts_for_dpi(page, 150);
let pm = render_progressive(page, &opts, 0).expect("render_progressive failed");
assert_eq!(pm.data.len(), (w * h * 4) as usize);
}
}