use crate::{
djvu_document::DjVuPage,
djvu_render::{self, RenderError, RenderOptions},
pixmap::{GrayPixmap, Pixmap},
};
#[derive(Debug, thiserror::Error)]
pub enum AsyncRenderError {
#[error("render error: {0}")]
Render(#[from] RenderError),
#[error("spawn_blocking join error: {0}")]
Join(String),
}
pub async fn render_pixmap_async(
page: &DjVuPage,
opts: RenderOptions,
) -> Result<Pixmap, AsyncRenderError> {
let page = page.clone();
tokio::task::spawn_blocking(move || {
djvu_render::render_pixmap(&page, &opts).map_err(AsyncRenderError::Render)
})
.await
.map_err(|e| AsyncRenderError::Join(e.to_string()))?
}
pub async fn render_gray8_async(
page: &DjVuPage,
opts: RenderOptions,
) -> Result<GrayPixmap, AsyncRenderError> {
let page = page.clone();
tokio::task::spawn_blocking(move || {
djvu_render::render_gray8(&page, &opts).map_err(AsyncRenderError::Render)
})
.await
.map_err(|e| AsyncRenderError::Join(e.to_string()))?
}
#[cfg(test)]
mod tests {
use super::*;
use crate::djvu_document::DjVuDocument;
fn assets_path() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("references/djvujs/library/assets")
}
fn load_doc(name: &str) -> DjVuDocument {
let data =
std::fs::read(assets_path().join(name)).unwrap_or_else(|_| panic!("{name} must exist"));
DjVuDocument::parse(&data).unwrap_or_else(|e| panic!("{e}"))
}
#[tokio::test]
async fn render_pixmap_async_correct_dims() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let pw = page.width() as u32;
let ph = page.height() as u32;
let opts = RenderOptions {
width: pw,
height: ph,
..Default::default()
};
let pm = render_pixmap_async(page, opts)
.await
.expect("async render must succeed");
assert_eq!(pm.width, pw);
assert_eq!(pm.height, ph);
}
#[tokio::test]
async fn render_gray8_async_correct_dims() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let pw = page.width() as u32;
let ph = page.height() as u32;
let opts = RenderOptions {
width: pw,
height: ph,
..Default::default()
};
let gm = render_gray8_async(page, opts)
.await
.expect("async gray render must succeed");
assert_eq!(gm.width, pw);
assert_eq!(gm.height, ph);
assert_eq!(gm.data.len(), (pw * ph) as usize);
}
#[tokio::test]
async fn async_matches_sync() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let pw = page.width() as u32;
let ph = page.height() as u32;
let opts = RenderOptions {
width: pw,
height: ph,
..Default::default()
};
let sync_pm = djvu_render::render_pixmap(page, &opts).expect("sync render must succeed");
let async_pm = render_pixmap_async(page, opts.clone())
.await
.expect("async render must succeed");
assert_eq!(
sync_pm.data, async_pm.data,
"async and sync renders must match"
);
}
#[tokio::test]
async fn concurrent_render_multiple_tasks() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let pw = page.width() as u32;
let ph = page.height() as u32;
let opts = RenderOptions {
width: pw / 2,
height: ph / 2,
scale: 0.5,
..Default::default()
};
let handles: Vec<_> = (0..4)
.map(|_| {
let page_clone = page.clone();
let opts_clone = opts.clone();
tokio::spawn(async move { render_pixmap_async(&page_clone, opts_clone).await })
})
.collect();
for handle in handles {
let pm = handle
.await
.expect("task must not panic")
.expect("render must succeed");
assert_eq!(pm.width, pw / 2);
assert_eq!(pm.height, ph / 2);
}
}
#[test]
fn async_render_error_display() {
let err = AsyncRenderError::Render(crate::djvu_render::RenderError::InvalidDimensions {
width: 0,
height: 0,
});
let s = err.to_string();
assert!(
s.contains("render error"),
"error must mention 'render error'"
);
}
}