use std::sync::Arc;
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 = Arc::new(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 = Arc::new(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()))?
}
pub fn render_progressive_stream(
page: &DjVuPage,
opts: RenderOptions,
) -> impl futures_core::Stream<Item = Result<Pixmap, AsyncRenderError>> {
let page = Arc::new(page.clone());
let n_chunks = page.bg44_chunks().len();
async_stream::stream! {
if n_chunks == 0 {
let page = Arc::clone(&page);
let opts = opts.clone();
let result = tokio::task::spawn_blocking(move || {
djvu_render::render_pixmap(&page, &opts).map_err(AsyncRenderError::Render)
})
.await
.map_err(|e| AsyncRenderError::Join(e.to_string()));
yield result.and_then(|r| r);
} else {
for chunk_n in 0..n_chunks {
let page = Arc::clone(&page);
let opts = opts.clone();
let result = tokio::task::spawn_blocking(move || {
djvu_render::render_progressive(&page, &opts, chunk_n)
.map_err(AsyncRenderError::Render)
})
.await
.map_err(|e| AsyncRenderError::Join(e.to_string()));
yield result.and_then(|r| r);
}
}
}
}
#[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'"
);
}
#[tokio::test]
async fn progressive_stream_last_frame_matches_pixmap() {
use futures::StreamExt;
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 100,
height: 80,
..Default::default()
};
let stream = render_progressive_stream(page, opts.clone());
futures::pin_mut!(stream);
let mut frames: Vec<Pixmap> = Vec::new();
while let Some(result) = stream.next().await {
frames.push(result.expect("frame should succeed"));
}
assert!(!frames.is_empty(), "stream must yield at least one frame");
let expected = djvu_render::render_pixmap(page, &opts).expect("render_pixmap must succeed");
assert_eq!(
frames.last().unwrap().data,
expected.data,
"last frame must match render_pixmap"
);
}
#[tokio::test]
async fn progressive_stream_consistent_dimensions() {
use futures::StreamExt;
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let n_chunks = page.bg44_chunks().len();
let opts = RenderOptions {
width: 100,
height: 80,
..Default::default()
};
let stream = render_progressive_stream(page, opts);
futures::pin_mut!(stream);
let mut count = 0usize;
while let Some(result) = stream.next().await {
let frame = result.expect("frame should succeed");
assert_eq!(frame.width, 100);
assert_eq!(frame.height, 80);
count += 1;
}
let expected_count = if n_chunks == 0 { 1 } else { n_chunks };
assert_eq!(
count, expected_count,
"frame count must equal BG44 chunk count"
);
}
#[tokio::test]
async fn progressive_stream_jb2_only_yields_one_frame() {
use futures::StreamExt;
let doc = load_doc("boy_jb2.djvu");
let page = doc.page(0).unwrap();
if !page.bg44_chunks().is_empty() {
return;
}
let opts = RenderOptions {
width: 80,
height: 60,
..Default::default()
};
let stream = render_progressive_stream(page, opts);
futures::pin_mut!(stream);
let mut count = 0;
while let Some(result) = stream.next().await {
result.expect("frame should succeed");
count += 1;
}
assert_eq!(count, 1, "JB2-only page must yield exactly one frame");
}
}