#[cfg(not(feature = "std"))]
use alloc::sync::Arc;
#[cfg(feature = "std")]
use std::sync::Arc;
use crate::djvu_document::DjVuPage;
use crate::djvu_render::{
RenderError, RenderOptions, RenderRect, Resampling, combine_rotations, render_region,
};
use crate::info::Rotation;
use crate::pixmap::Pixmap;
#[derive(Debug, thiserror::Error)]
pub enum TileError {
#[error(transparent)]
Render(#[from] RenderError),
#[error("tile size must be non-zero")]
InvalidTileSize,
#[error("tile operation cancelled")]
Cancelled,
#[error("tile ({col}, {row}) out of range for a {cols}x{rows} tile grid")]
OutOfRange {
col: u32,
row: u32,
cols: u32,
rows: u32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileRect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileLayout {
full_width: u32,
full_height: u32,
output_width: u32,
output_height: u32,
tile_size: u32,
rotation: Rotation,
}
impl TileLayout {
pub fn new(page: &DjVuPage, opts: &RenderOptions, tile_size: u32) -> Result<Self, TileError> {
if tile_size == 0 {
return Err(TileError::InvalidTileSize);
}
if opts.resampling == Resampling::Lanczos3 {
return Err(TileError::Render(RenderError::UnsupportedOption(
"Resampling::Lanczos3 does not commute with per-tile assembly (#691)",
)));
}
if opts.aa {
return Err(TileError::Render(RenderError::UnsupportedOption(
"the aa halving post-pass does not commute with per-tile assembly (#691); \
request the target size directly instead",
)));
}
let full_width = opts.width.max(1);
let full_height = opts.height.max(1);
let rotation = combine_rotations(page.rotation(), opts.rotation);
let (output_width, output_height) = match rotation {
Rotation::None | Rotation::Rot180 => (full_width, full_height),
Rotation::Cw90 | Rotation::Ccw90 => (full_height, full_width),
};
Ok(TileLayout {
full_width,
full_height,
output_width,
output_height,
tile_size,
rotation,
})
}
pub fn output_width(&self) -> u32 {
self.output_width
}
pub fn output_height(&self) -> u32 {
self.output_height
}
pub fn tile_size(&self) -> u32 {
self.tile_size
}
pub fn cols(&self) -> u32 {
self.output_width.div_ceil(self.tile_size)
}
pub fn rows(&self) -> u32 {
self.output_height.div_ceil(self.tile_size)
}
pub fn tile_count(&self) -> u64 {
u64::from(self.cols()) * u64::from(self.rows())
}
pub fn tile_rect(&self, col: u32, row: u32) -> Result<TileRect, TileError> {
let (cols, rows) = (self.cols(), self.rows());
if col >= cols || row >= rows {
return Err(TileError::OutOfRange {
col,
row,
cols,
rows,
});
}
let x = col * self.tile_size;
let y = row * self.tile_size;
Ok(TileRect {
x,
y,
width: self.tile_size.min(self.output_width - x),
height: self.tile_size.min(self.output_height - y),
})
}
fn to_render_rect(self, r: TileRect) -> RenderRect {
let (fw, fh) = (self.full_width, self.full_height);
match self.rotation {
Rotation::None => RenderRect {
x: r.x,
y: r.y,
width: r.width,
height: r.height,
},
Rotation::Cw90 => RenderRect {
x: r.y,
y: fh - r.x - r.width,
width: r.height,
height: r.width,
},
Rotation::Rot180 => RenderRect {
x: fw - r.x - r.width,
y: fh - r.y - r.height,
width: r.width,
height: r.height,
},
Rotation::Ccw90 => RenderRect {
x: fw - r.y - r.height,
y: r.x,
width: r.height,
height: r.width,
},
}
}
}
pub fn render_tile(
page: &DjVuPage,
opts: &RenderOptions,
tile_size: u32,
col: u32,
row: u32,
) -> Result<Pixmap, TileError> {
let layout = TileLayout::new(page, opts, tile_size)?;
let rect = layout.tile_rect(col, row)?;
Ok(render_region(page, layout.to_render_rect(rect), opts)?)
}
#[cfg(feature = "std")]
pub fn render_tile_cached(
page: &DjVuPage,
opts: &RenderOptions,
tile_size: u32,
col: u32,
row: u32,
) -> Result<Pixmap, TileError> {
let layout = TileLayout::new(page, opts, tile_size)?;
let rect = layout.tile_rect(col, row)?;
Ok(crate::djvu_render::render_region_tiled(
page,
layout.to_render_rect(rect),
opts,
)?)
}
#[derive(Debug, Clone, Default)]
pub struct TileCancelToken {
flag: Arc<core::sync::atomic::AtomicBool>,
}
impl TileCancelToken {
pub fn new() -> Self {
Self::default()
}
pub fn cancel(&self) {
self.flag.store(true, core::sync::atomic::Ordering::Relaxed);
}
pub fn is_cancelled(&self) -> bool {
self.flag.load(core::sync::atomic::Ordering::Relaxed)
}
fn as_flag(&self) -> &core::sync::atomic::AtomicBool {
&self.flag
}
}
#[derive(Debug, Clone, Default)]
pub struct TileRenderControls {
pub quality_step: Option<usize>,
pub cancel: Option<TileCancelToken>,
#[cfg(feature = "std")]
pub use_cache: bool,
}
pub fn render_tile_with(
page: &DjVuPage,
opts: &RenderOptions,
tile_size: u32,
col: u32,
row: u32,
controls: &TileRenderControls,
) -> Result<Pixmap, TileError> {
let layout = TileLayout::new(page, opts, tile_size)?;
let rect = layout.tile_rect(col, row)?;
let cancel = controls.cancel.as_ref();
if cancel.is_some_and(TileCancelToken::is_cancelled) {
return Err(TileError::Cancelled);
}
let flag = cancel.map(TileCancelToken::as_flag);
let render_rect = layout.to_render_rect(rect);
if let Some(step) = controls.quality_step {
let steps = crate::djvu_render::progressive_steps(page);
if step >= steps {
return Err(TileError::Render(RenderError::ChunkOutOfRange {
chunk_n: step,
max: steps - 1,
}));
}
if page.bg44_chunks().is_empty() {
return Ok(render_region(page, render_rect, opts)?);
}
return crate::djvu_render::render_region_progressive(page, render_rect, opts, step, flag)?
.ok_or(TileError::Cancelled);
}
#[cfg(feature = "std")]
if controls.use_cache {
return crate::djvu_render::render_region_tiled_cancellable(page, render_rect, opts, flag)?
.ok_or(TileError::Cancelled);
}
Ok(render_region(page, render_rect, opts)?)
}
#[cfg(feature = "std")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileCacheUsage {
pub bytes: usize,
pub budget: usize,
pub tiles: usize,
}
#[cfg(feature = "std")]
pub fn tile_cache_usage(page: &DjVuPage) -> TileCacheUsage {
let layers = page.render_layers();
TileCacheUsage {
bytes: layers.tile_cache_bytes(),
budget: layers.tile_cache_budget(),
tiles: layers.tile_cache_len(),
}
}
#[cfg(feature = "std")]
pub fn set_tile_cache_budget(page: &DjVuPage, max_bytes: usize) {
page.render_layers().set_tile_cache_budget(max_bytes);
}
#[cfg(feature = "std")]
pub fn clear_tile_cache(page: &DjVuPage) -> usize {
page.render_layers().clear_tile_cache()
}
#[cfg(feature = "std")]
pub fn invalidate_tile_region(
page: &DjVuPage,
opts: &RenderOptions,
region: TileRect,
) -> Result<usize, TileError> {
let layout = TileLayout::new(page, opts, 1)?;
let x = region.x.min(layout.output_width);
let y = region.y.min(layout.output_height);
let width = region.width.min(layout.output_width - x);
let height = region.height.min(layout.output_height - y);
if width == 0 || height == 0 {
return Ok(0);
}
let rect = layout.to_render_rect(TileRect {
x,
y,
width,
height,
});
Ok(page
.render_layers()
.remove_tiles_intersecting(rect, layout.full_width, layout.full_height))
}
#[cfg(all(feature = "std", feature = "parallel"))]
pub fn prefetch_tiles(
doc: &std::sync::Arc<crate::djvu_document::DjVuDocument>,
page_index: usize,
opts: &RenderOptions,
tile_size: u32,
col: u32,
row: u32,
radius: u32,
) -> Result<u64, TileError> {
prefetch_tiles_inner(doc, page_index, opts, tile_size, col, row, radius, None)
}
#[cfg(all(feature = "std", feature = "parallel"))]
#[allow(clippy::too_many_arguments)]
pub fn prefetch_tiles_cancellable(
doc: &std::sync::Arc<crate::djvu_document::DjVuDocument>,
page_index: usize,
opts: &RenderOptions,
tile_size: u32,
col: u32,
row: u32,
radius: u32,
cancel: &TileCancelToken,
) -> Result<u64, TileError> {
if cancel.is_cancelled() {
return Err(TileError::Cancelled);
}
prefetch_tiles_inner(
doc,
page_index,
opts,
tile_size,
col,
row,
radius,
Some(cancel.clone()),
)
}
#[cfg(all(feature = "std", feature = "parallel"))]
#[allow(clippy::too_many_arguments)]
fn prefetch_tiles_inner(
doc: &std::sync::Arc<crate::djvu_document::DjVuDocument>,
page_index: usize,
opts: &RenderOptions,
tile_size: u32,
col: u32,
row: u32,
radius: u32,
cancel: Option<TileCancelToken>,
) -> Result<u64, TileError> {
let Ok(page) = doc.page(page_index) else {
return Ok(0);
};
let layout = TileLayout::new(page, opts, tile_size)?;
layout.tile_rect(col, row)?;
let c0 = col.saturating_sub(radius);
let c1 = col.saturating_add(radius).min(layout.cols() - 1);
let r0 = row.saturating_sub(radius);
let r1 = row.saturating_add(radius).min(layout.rows() - 1);
let scheduled = u64::from(c1 - c0 + 1) * u64::from(r1 - r0 + 1);
let doc = std::sync::Arc::clone(doc);
let opts = opts.clone();
rayon::spawn(move || {
let Ok(page) = doc.page(page_index) else {
return;
};
let controls = TileRenderControls {
quality_step: None,
cancel,
use_cache: true,
};
for r in r0..=r1 {
for c in c0..=c1 {
if controls
.cancel
.as_ref()
.is_some_and(TileCancelToken::is_cancelled)
{
return;
}
let _ = render_tile_with(page, &opts, tile_size, c, r, &controls);
}
}
});
Ok(scheduled)
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::djvu_document::DjVuDocument;
use crate::djvu_render::{
UserRotation, progressive_steps, render_pixmap, render_progressive_step,
};
fn assets_path() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("references/djvujs/library/assets")
}
fn load_doc(filename: &str) -> DjVuDocument {
let data = std::fs::read(assets_path().join(filename))
.unwrap_or_else(|_| panic!("{filename} must exist"));
DjVuDocument::parse(&data).unwrap_or_else(|e| panic!("parse failed: {e}"))
}
fn assemble<F>(layout: &TileLayout, mut tile: F) -> Pixmap
where
F: FnMut(u32, u32) -> Pixmap,
{
let mut out = Pixmap::white(layout.output_width(), layout.output_height());
let stride = layout.output_width() as usize * 4;
for row in 0..layout.rows() {
for col in 0..layout.cols() {
let rect = layout.tile_rect(col, row).unwrap();
let pm = tile(col, row);
assert_eq!((pm.width, pm.height), (rect.width, rect.height));
for y in 0..rect.height as usize {
let src = y * rect.width as usize * 4;
let dst = (rect.y as usize + y) * stride + rect.x as usize * 4;
out.data[dst..dst + rect.width as usize * 4]
.copy_from_slice(&pm.data[src..src + rect.width as usize * 4]);
}
}
}
out
}
#[test]
fn assembled_tiles_match_full_render_all_rotations() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
for rotation in [
UserRotation::None,
UserRotation::Cw90,
UserRotation::Rot180,
UserRotation::Ccw90,
] {
let opts = RenderOptions {
width: 61, height: 83,
rotation,
..Default::default()
};
let full = render_pixmap(page, &opts).unwrap();
let layout = TileLayout::new(page, &opts, 32).unwrap();
assert_eq!(
(full.width, full.height),
(layout.output_width(), layout.output_height())
);
let stitched = assemble(&layout, |c, r| render_tile(page, &opts, 32, c, r).unwrap());
assert_eq!(
full.data, stitched.data,
"stitched tiles must be byte-identical to the full render (rotation {rotation:?})"
);
}
}
#[test]
fn assembled_tiles_match_full_render_info_rotation() {
let doc = load_doc("boy_jb2_rotate90.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 45,
height: 57,
rotation: UserRotation::Cw90,
..Default::default()
};
let full = render_pixmap(page, &opts).unwrap();
let layout = TileLayout::new(page, &opts, 16).unwrap();
let stitched = assemble(&layout, |c, r| render_tile(page, &opts, 16, c, r).unwrap());
assert_eq!(full.data, stitched.data);
}
#[test]
fn assembled_tiles_match_full_render_bilevel_downscale() {
let doc = load_doc("boy_jb2.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 40,
height: 52,
..Default::default()
};
let full = render_pixmap(page, &opts).unwrap();
let layout = TileLayout::new(page, &opts, 16).unwrap();
let stitched = assemble(&layout, |c, r| render_tile(page, &opts, 16, c, r).unwrap());
assert_eq!(full.data, stitched.data);
}
#[test]
fn tile_pixels_independent_of_request_order() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 61,
height: 83,
..Default::default()
};
let layout = TileLayout::new(page, &opts, 32).unwrap();
let coords: Vec<(u32, u32)> = (0..layout.rows())
.flat_map(|r| (0..layout.cols()).map(move |c| (c, r)))
.collect();
let reference: Vec<Pixmap> = coords
.iter()
.map(|&(c, r)| render_tile(page, &opts, 32, c, r).unwrap())
.collect();
for (i, &(c, r)) in coords.iter().enumerate().rev() {
let pm = render_tile_cached(page, &opts, 32, c, r).unwrap();
assert_eq!(pm.data, reference[i].data, "reverse pass, tile ({c}, {r})");
}
for (i, &(c, r)) in coords.iter().enumerate() {
let pm = render_tile_cached(page, &opts, 32, c, r).unwrap();
assert_eq!(pm.data, reference[i].data, "warm pass, tile ({c}, {r})");
}
}
#[test]
fn single_tile_equals_full_render() {
let doc = load_doc("boy.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 50,
height: 70,
..Default::default()
};
let layout = TileLayout::new(page, &opts, 1024).unwrap();
assert_eq!((layout.cols(), layout.rows()), (1, 1));
let tile = render_tile(page, &opts, 1024, 0, 0).unwrap();
let full = render_pixmap(page, &opts).unwrap();
assert_eq!(tile.data, full.data);
}
#[test]
fn layout_geometry_and_errors() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 100,
height: 65,
..Default::default()
};
let layout = TileLayout::new(page, &opts, 32).unwrap();
assert_eq!((layout.cols(), layout.rows()), (4, 3));
assert_eq!(layout.tile_count(), 12);
assert_eq!(
layout.tile_rect(0, 0).unwrap(),
TileRect {
x: 0,
y: 0,
width: 32,
height: 32
}
);
assert_eq!(
layout.tile_rect(3, 2).unwrap(),
TileRect {
x: 96,
y: 64,
width: 4,
height: 1
}
);
assert!(matches!(
layout.tile_rect(4, 0),
Err(TileError::OutOfRange {
col: 4,
row: 0,
cols: 4,
rows: 3
})
));
assert!(matches!(
render_tile(page, &opts, 32, 0, 3),
Err(TileError::OutOfRange { .. })
));
assert!(matches!(
TileLayout::new(page, &opts, 0),
Err(TileError::InvalidTileSize)
));
let lanczos = RenderOptions {
resampling: Resampling::Lanczos3,
..opts
};
assert!(matches!(
TileLayout::new(page, &lanczos, 32),
Err(TileError::Render(RenderError::UnsupportedOption(_)))
));
let aa = RenderOptions { aa: true, ..opts };
assert!(matches!(
TileLayout::new(page, &aa, 32),
Err(TileError::Render(RenderError::UnsupportedOption(_)))
));
}
#[test]
fn rotated_layout_swaps_display_dimensions() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 80,
height: 60,
rotation: UserRotation::Cw90,
..Default::default()
};
let layout = TileLayout::new(page, &opts, 32).unwrap();
assert_eq!(
(layout.output_width(), layout.output_height()),
(60, 80),
"Cw90 display canvas must be opts.height × opts.width"
);
}
fn warm_grid(page: &DjVuPage, opts: &RenderOptions) {
let layout = TileLayout::new(page, opts, 256).unwrap();
for row in 0..layout.rows() {
for col in 0..layout.cols() {
render_tile_cached(page, opts, 256, col, row).unwrap();
}
}
}
#[test]
fn cache_usage_budget_and_clear() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 600,
height: 800,
..Default::default()
};
let fresh = tile_cache_usage(page);
assert_eq!((fresh.bytes, fresh.tiles), (0, 0));
assert_eq!(fresh.budget, 8 * 1024 * 1024, "default budget is 8 MiB");
warm_grid(page, &opts);
let warm = tile_cache_usage(page);
assert!(warm.bytes > 0 && warm.tiles > 0);
set_tile_cache_budget(page, 300_000);
let shrunk = tile_cache_usage(page);
assert!(shrunk.bytes <= 300_000, "usage {} > budget", shrunk.bytes);
assert!(shrunk.tiles < warm.tiles);
assert_eq!(shrunk.budget, 300_000);
let cached = render_tile_cached(page, &opts, 256, 0, 0).unwrap();
let direct = render_tile(page, &opts, 256, 0, 0).unwrap();
assert_eq!(cached.data, direct.data);
assert!(tile_cache_usage(page).bytes <= 300_000);
set_tile_cache_budget(page, 0);
let cached = render_tile_cached(page, &opts, 256, 1, 1).unwrap();
let direct = render_tile(page, &opts, 256, 1, 1).unwrap();
assert_eq!(cached.data, direct.data);
assert_eq!(tile_cache_usage(page).bytes, 0);
set_tile_cache_budget(page, 8 * 1024 * 1024);
warm_grid(page, &opts);
let before = tile_cache_usage(page);
let freed = clear_tile_cache(page);
assert_eq!(freed, before.bytes);
let after = tile_cache_usage(page);
assert_eq!((after.bytes, after.tiles), (0, 0));
assert_eq!(after.budget, 8 * 1024 * 1024);
}
#[test]
fn invalidate_region_drops_overlapping_tiles_across_scales() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let big = RenderOptions {
width: 600,
height: 800,
..Default::default()
};
let small = RenderOptions {
width: 300,
height: 400,
..Default::default()
};
warm_grid(page, &big); warm_grid(page, &small); let reference = render_tile_cached(page, &big, 256, 0, 0).unwrap();
let before = tile_cache_usage(page);
assert_eq!(before.tiles, 16);
let freed = invalidate_tile_region(
page,
&big,
TileRect {
x: 0,
y: 0,
width: 300,
height: 800,
},
)
.unwrap();
assert!(freed > 0);
let after = tile_cache_usage(page);
assert_eq!(after.tiles, 6, "only right-column tiles survive");
assert_eq!(after.bytes, before.bytes - freed);
let rerendered = render_tile_cached(page, &big, 256, 0, 0).unwrap();
assert_eq!(rerendered.data, reference.data);
assert_eq!(
invalidate_tile_region(
page,
&big,
TileRect {
x: 599,
y: 799,
width: 0,
height: 0,
},
)
.unwrap(),
0
);
}
#[test]
fn invalidate_maps_display_rect_through_rotation() {
let identity = RenderOptions {
width: 600,
height: 800,
..Default::default()
};
let rotated = RenderOptions {
rotation: UserRotation::Cw90,
..identity.clone()
};
let doc_a = load_doc("chicken.djvu");
let page_a = doc_a.page(0).unwrap();
warm_grid(page_a, &identity);
let doc_b = load_doc("chicken.djvu");
let page_b = doc_b.page(0).unwrap();
warm_grid(page_b, &identity);
let freed_identity = invalidate_tile_region(
page_a,
&identity,
TileRect {
x: 0,
y: 0,
width: 300,
height: 800,
},
)
.unwrap();
let freed_rotated = invalidate_tile_region(
page_b,
&rotated,
TileRect {
x: 0,
y: 0,
width: 800,
height: 300,
},
)
.unwrap();
assert!(freed_identity > 0);
assert_eq!(freed_identity, freed_rotated);
assert_eq!(
tile_cache_usage(page_a).tiles,
tile_cache_usage(page_b).tiles
);
}
#[cfg(feature = "parallel")]
#[test]
fn prefetch_tiles_warms_cache() {
let doc = std::sync::Arc::new(load_doc("chicken.djvu"));
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 600,
height: 800,
..Default::default()
};
assert_eq!(prefetch_tiles(&doc, 99, &opts, 256, 0, 0, 1).unwrap(), 0);
assert!(matches!(
prefetch_tiles(&doc, 0, &opts, 256, 99, 0, 1),
Err(TileError::OutOfRange { .. })
));
let scheduled = prefetch_tiles(&doc, 0, &opts, 256, 0, 0, 1).unwrap();
assert_eq!(scheduled, 4);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
while tile_cache_usage(page).tiles < 4 && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(
tile_cache_usage(page).tiles >= 4,
"prefetch never landed: {:?}",
tile_cache_usage(page)
);
let warm = render_tile_cached(page, &opts, 256, 0, 0).unwrap();
let direct = render_tile(page, &opts, 256, 0, 0).unwrap();
assert_eq!(warm.data, direct.data);
}
#[test]
fn progressive_tiles_match_progressive_frames() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let steps = progressive_steps(page);
assert!(steps >= 2, "need a multi-chunk BG44 page");
let opts = RenderOptions {
width: 61,
height: 83,
..Default::default()
};
let layout = TileLayout::new(page, &opts, 32).unwrap();
let mut frames = Vec::new();
for step in 0..steps {
let full = render_progressive_step(page, &opts, step).unwrap();
let controls = TileRenderControls {
quality_step: Some(step),
..Default::default()
};
let stitched = assemble(&layout, |c, r| {
render_tile_with(page, &opts, 32, c, r, &controls).unwrap()
});
assert_eq!(
full.data, stitched.data,
"stitched step-{step} tiles must match the progressive frame"
);
frames.push(full.data);
}
assert_ne!(
frames[0],
frames[steps - 1],
"the first and last quality steps must differ (refinement adds detail)"
);
}
#[test]
fn progressive_tiles_match_under_rotation() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 61,
height: 83,
rotation: UserRotation::Cw90,
..Default::default()
};
let full = render_progressive_step(page, &opts, 0).unwrap();
let layout = TileLayout::new(page, &opts, 32).unwrap();
let controls = TileRenderControls {
quality_step: Some(0),
..Default::default()
};
let stitched = assemble(&layout, |c, r| {
render_tile_with(page, &opts, 32, c, r, &controls).unwrap()
});
assert_eq!(full.data, stitched.data);
}
#[test]
fn render_tile_with_matches_dedicated_entry_points() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 61,
height: 83,
..Default::default()
};
let direct = render_tile(page, &opts, 32, 1, 1).unwrap();
let default = render_tile_with(page, &opts, 32, 1, 1, &TileRenderControls::default());
assert_eq!(default.unwrap().data, direct.data);
let cached = render_tile_with(
page,
&opts,
32,
1,
1,
&TileRenderControls {
use_cache: true,
..Default::default()
},
);
assert_eq!(cached.unwrap().data, direct.data);
}
#[test]
fn quality_steps_on_bilevel_page() {
let doc = load_doc("boy_jb2.djvu");
let page = doc.page(0).unwrap();
assert_eq!(progressive_steps(page), 1);
let opts = RenderOptions {
width: 40,
height: 52,
..Default::default()
};
let direct = render_tile(page, &opts, 16, 0, 0).unwrap();
let step0 = render_tile_with(
page,
&opts,
16,
0,
0,
&TileRenderControls {
quality_step: Some(0),
..Default::default()
},
)
.unwrap();
assert_eq!(step0.data, direct.data);
assert!(matches!(
render_tile_with(
page,
&opts,
16,
0,
0,
&TileRenderControls {
quality_step: Some(1),
..Default::default()
},
),
Err(TileError::Render(RenderError::ChunkOutOfRange {
chunk_n: 1,
max: 0
}))
));
}
#[test]
fn cancelled_token_aborts_every_mode() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 61,
height: 83,
..Default::default()
};
let token = TileCancelToken::new();
assert!(!token.is_cancelled());
let shared = token.clone();
shared.cancel();
assert!(token.is_cancelled(), "clones share one flag");
for controls in [
TileRenderControls {
cancel: Some(token.clone()),
..Default::default()
},
TileRenderControls {
cancel: Some(token.clone()),
use_cache: true,
..Default::default()
},
TileRenderControls {
cancel: Some(token.clone()),
quality_step: Some(0),
..Default::default()
},
] {
assert!(matches!(
render_tile_with(page, &opts, 32, 0, 0, &controls),
Err(TileError::Cancelled)
));
}
let live = TileCancelToken::new();
let with_token = render_tile_with(
page,
&opts,
32,
0,
0,
&TileRenderControls {
cancel: Some(live),
..Default::default()
},
)
.unwrap();
let direct = render_tile(page, &opts, 32, 0, 0).unwrap();
assert_eq!(with_token.data, direct.data);
}
#[cfg(feature = "parallel")]
#[test]
fn prefetch_tiles_cancellable_behaviour() {
let doc = std::sync::Arc::new(load_doc("chicken.djvu"));
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 600,
height: 800,
..Default::default()
};
let cancelled = TileCancelToken::new();
cancelled.cancel();
assert!(matches!(
prefetch_tiles_cancellable(&doc, 0, &opts, 256, 0, 0, 1, &cancelled),
Err(TileError::Cancelled)
));
assert_eq!(
tile_cache_usage(page).tiles,
0,
"a pre-cancelled prefetch must not warm anything"
);
let live = TileCancelToken::new();
let scheduled = prefetch_tiles_cancellable(&doc, 0, &opts, 256, 0, 0, 1, &live).unwrap();
assert_eq!(scheduled, 4);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
while tile_cache_usage(page).tiles < 4 && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(tile_cache_usage(page).tiles >= 4);
let warm = render_tile_cached(page, &opts, 256, 0, 0).unwrap();
let direct = render_tile(page, &opts, 256, 0, 0).unwrap();
assert_eq!(warm.data, direct.data);
}
}