1#[cfg(not(feature = "std"))]
49use alloc::sync::Arc;
50#[cfg(feature = "std")]
51use std::sync::Arc;
52
53use crate::djvu_document::DjVuPage;
54use crate::djvu_render::{
55 RenderError, RenderOptions, RenderRect, Resampling, combine_rotations, render_region,
56};
57use crate::info::Rotation;
58use crate::pixmap::Pixmap;
59
60#[derive(Debug, thiserror::Error)]
62#[non_exhaustive]
63pub enum TileError {
64 #[error(transparent)]
66 Render(#[from] RenderError),
67
68 #[error("tile size must be non-zero")]
70 InvalidTileSize,
71
72 #[error("tile operation cancelled")]
75 Cancelled,
76
77 #[error("tile ({col}, {row}) out of range for a {cols}x{rows} tile grid")]
79 OutOfRange {
80 col: u32,
82 row: u32,
84 cols: u32,
86 rows: u32,
88 },
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct TileRect {
99 pub x: u32,
101 pub y: u32,
103 pub width: u32,
105 pub height: u32,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct TileLayout {
118 full_width: u32,
120 full_height: u32,
121 output_width: u32,
123 output_height: u32,
124 tile_size: u32,
125 rotation: Rotation,
127}
128
129impl TileLayout {
130 pub fn new(page: &DjVuPage, opts: &RenderOptions, tile_size: u32) -> Result<Self, TileError> {
143 if tile_size == 0 {
144 return Err(TileError::InvalidTileSize);
145 }
146 if opts.resampling == Resampling::Lanczos3 {
147 return Err(TileError::Render(RenderError::UnsupportedOption(
148 "Resampling::Lanczos3 does not commute with per-tile assembly (#691)",
149 )));
150 }
151 if opts.aa {
152 return Err(TileError::Render(RenderError::UnsupportedOption(
153 "the aa halving post-pass does not commute with per-tile assembly (#691); \
154 request the target size directly instead",
155 )));
156 }
157 let full_width = opts.width.max(1);
158 let full_height = opts.height.max(1);
159 let rotation = combine_rotations(page.rotation(), opts.rotation);
160 let (output_width, output_height) = match rotation {
161 Rotation::None | Rotation::Rot180 => (full_width, full_height),
162 Rotation::Cw90 | Rotation::Ccw90 => (full_height, full_width),
163 };
164 Ok(TileLayout {
165 full_width,
166 full_height,
167 output_width,
168 output_height,
169 tile_size,
170 rotation,
171 })
172 }
173
174 pub fn output_width(&self) -> u32 {
176 self.output_width
177 }
178
179 pub fn output_height(&self) -> u32 {
181 self.output_height
182 }
183
184 pub fn tile_size(&self) -> u32 {
186 self.tile_size
187 }
188
189 pub fn cols(&self) -> u32 {
191 self.output_width.div_ceil(self.tile_size)
192 }
193
194 pub fn rows(&self) -> u32 {
196 self.output_height.div_ceil(self.tile_size)
197 }
198
199 pub fn tile_count(&self) -> u64 {
201 u64::from(self.cols()) * u64::from(self.rows())
202 }
203
204 pub fn tile_rect(&self, col: u32, row: u32) -> Result<TileRect, TileError> {
212 let (cols, rows) = (self.cols(), self.rows());
213 if col >= cols || row >= rows {
214 return Err(TileError::OutOfRange {
215 col,
216 row,
217 cols,
218 rows,
219 });
220 }
221 let x = col * self.tile_size;
222 let y = row * self.tile_size;
223 Ok(TileRect {
224 x,
225 y,
226 width: self.tile_size.min(self.output_width - x),
227 height: self.tile_size.min(self.output_height - y),
228 })
229 }
230
231 fn to_render_rect(self, r: TileRect) -> RenderRect {
247 let (fw, fh) = (self.full_width, self.full_height);
248 match self.rotation {
249 Rotation::None => RenderRect {
250 x: r.x,
251 y: r.y,
252 width: r.width,
253 height: r.height,
254 },
255 Rotation::Cw90 => RenderRect {
256 x: r.y,
257 y: fh - r.x - r.width,
258 width: r.height,
259 height: r.width,
260 },
261 Rotation::Rot180 => RenderRect {
262 x: fw - r.x - r.width,
263 y: fh - r.y - r.height,
264 width: r.width,
265 height: r.height,
266 },
267 Rotation::Ccw90 => RenderRect {
268 x: fw - r.y - r.height,
269 y: r.x,
270 width: r.height,
271 height: r.width,
272 },
273 }
274 }
275}
276
277pub fn render_tile(
294 page: &DjVuPage,
295 opts: &RenderOptions,
296 tile_size: u32,
297 col: u32,
298 row: u32,
299) -> Result<Pixmap, TileError> {
300 let layout = TileLayout::new(page, opts, tile_size)?;
301 let rect = layout.tile_rect(col, row)?;
302 Ok(render_region(page, layout.to_render_rect(rect), opts)?)
303}
304
305#[cfg(feature = "std")]
318pub fn render_tile_cached(
319 page: &DjVuPage,
320 opts: &RenderOptions,
321 tile_size: u32,
322 col: u32,
323 row: u32,
324) -> Result<Pixmap, TileError> {
325 let layout = TileLayout::new(page, opts, tile_size)?;
326 let rect = layout.tile_rect(col, row)?;
327 Ok(crate::djvu_render::render_region_tiled(
328 page,
329 layout.to_render_rect(rect),
330 opts,
331 )?)
332}
333
334#[derive(Debug, Clone, Default)]
349pub struct TileCancelToken {
350 flag: Arc<core::sync::atomic::AtomicBool>,
351}
352
353impl TileCancelToken {
354 pub fn new() -> Self {
356 Self::default()
357 }
358
359 pub fn cancel(&self) {
361 self.flag.store(true, core::sync::atomic::Ordering::Relaxed);
362 }
363
364 pub fn is_cancelled(&self) -> bool {
366 self.flag.load(core::sync::atomic::Ordering::Relaxed)
367 }
368
369 fn as_flag(&self) -> &core::sync::atomic::AtomicBool {
371 &self.flag
372 }
373}
374
375#[derive(Debug, Clone, Default)]
380pub struct TileRenderControls {
381 pub quality_step: Option<usize>,
392
393 pub cancel: Option<TileCancelToken>,
395
396 #[cfg(feature = "std")]
401 pub use_cache: bool,
402}
403
404pub fn render_tile_with(
425 page: &DjVuPage,
426 opts: &RenderOptions,
427 tile_size: u32,
428 col: u32,
429 row: u32,
430 controls: &TileRenderControls,
431) -> Result<Pixmap, TileError> {
432 let layout = TileLayout::new(page, opts, tile_size)?;
433 let rect = layout.tile_rect(col, row)?;
434 let cancel = controls.cancel.as_ref();
435 if cancel.is_some_and(TileCancelToken::is_cancelled) {
436 return Err(TileError::Cancelled);
437 }
438 let flag = cancel.map(TileCancelToken::as_flag);
439 let render_rect = layout.to_render_rect(rect);
440
441 if let Some(step) = controls.quality_step {
442 let steps = crate::djvu_render::progressive_steps(page);
443 if step >= steps {
444 return Err(TileError::Render(RenderError::ChunkOutOfRange {
445 chunk_n: step,
446 max: steps - 1,
447 }));
448 }
449 if page.bg44_chunks().is_empty() {
450 return Ok(render_region(page, render_rect, opts)?);
453 }
454 return crate::djvu_render::render_region_progressive(page, render_rect, opts, step, flag)?
455 .ok_or(TileError::Cancelled);
456 }
457
458 #[cfg(feature = "std")]
459 if controls.use_cache {
460 return crate::djvu_render::render_region_tiled_cancellable(page, render_rect, opts, flag)?
461 .ok_or(TileError::Cancelled);
462 }
463
464 Ok(render_region(page, render_rect, opts)?)
465}
466
467#[cfg(feature = "std")]
474#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475pub struct TileCacheUsage {
476 pub bytes: usize,
478 pub budget: usize,
481 pub tiles: usize,
483}
484
485#[cfg(feature = "std")]
489pub fn tile_cache_usage(page: &DjVuPage) -> TileCacheUsage {
490 let layers = page.render_layers();
491 TileCacheUsage {
492 bytes: layers.tile_cache_bytes(),
493 budget: layers.tile_cache_budget(),
494 tiles: layers.tile_cache_len(),
495 }
496}
497
498#[cfg(feature = "std")]
512pub fn set_tile_cache_budget(page: &DjVuPage, max_bytes: usize) {
513 page.render_layers().set_tile_cache_budget(max_bytes);
514}
515
516#[cfg(feature = "std")]
522pub fn clear_tile_cache(page: &DjVuPage) -> usize {
523 page.render_layers().clear_tile_cache()
524}
525
526#[cfg(feature = "std")]
542pub fn invalidate_tile_region(
543 page: &DjVuPage,
544 opts: &RenderOptions,
545 region: TileRect,
546) -> Result<usize, TileError> {
547 let layout = TileLayout::new(page, opts, 1)?;
550 let x = region.x.min(layout.output_width);
551 let y = region.y.min(layout.output_height);
552 let width = region.width.min(layout.output_width - x);
553 let height = region.height.min(layout.output_height - y);
554 if width == 0 || height == 0 {
555 return Ok(0);
556 }
557 let rect = layout.to_render_rect(TileRect {
558 x,
559 y,
560 width,
561 height,
562 });
563 Ok(page
564 .render_layers()
565 .remove_tiles_intersecting(rect, layout.full_width, layout.full_height))
566}
567
568#[cfg(all(feature = "std", feature = "parallel"))]
588pub fn prefetch_tiles(
589 doc: &std::sync::Arc<crate::djvu_document::DjVuDocument>,
590 page_index: usize,
591 opts: &RenderOptions,
592 tile_size: u32,
593 col: u32,
594 row: u32,
595 radius: u32,
596) -> Result<u64, TileError> {
597 prefetch_tiles_inner(doc, page_index, opts, tile_size, col, row, radius, None)
598}
599
600#[cfg(all(feature = "std", feature = "parallel"))]
614#[allow(clippy::too_many_arguments)]
615pub fn prefetch_tiles_cancellable(
616 doc: &std::sync::Arc<crate::djvu_document::DjVuDocument>,
617 page_index: usize,
618 opts: &RenderOptions,
619 tile_size: u32,
620 col: u32,
621 row: u32,
622 radius: u32,
623 cancel: &TileCancelToken,
624) -> Result<u64, TileError> {
625 if cancel.is_cancelled() {
626 return Err(TileError::Cancelled);
627 }
628 prefetch_tiles_inner(
629 doc,
630 page_index,
631 opts,
632 tile_size,
633 col,
634 row,
635 radius,
636 Some(cancel.clone()),
637 )
638}
639
640#[cfg(all(feature = "std", feature = "parallel"))]
641#[allow(clippy::too_many_arguments)]
642fn prefetch_tiles_inner(
643 doc: &std::sync::Arc<crate::djvu_document::DjVuDocument>,
644 page_index: usize,
645 opts: &RenderOptions,
646 tile_size: u32,
647 col: u32,
648 row: u32,
649 radius: u32,
650 cancel: Option<TileCancelToken>,
651) -> Result<u64, TileError> {
652 let Ok(page) = doc.page(page_index) else {
653 return Ok(0);
654 };
655 let layout = TileLayout::new(page, opts, tile_size)?;
656 layout.tile_rect(col, row)?;
657 let c0 = col.saturating_sub(radius);
658 let c1 = col.saturating_add(radius).min(layout.cols() - 1);
659 let r0 = row.saturating_sub(radius);
660 let r1 = row.saturating_add(radius).min(layout.rows() - 1);
661 let scheduled = u64::from(c1 - c0 + 1) * u64::from(r1 - r0 + 1);
662 let doc = std::sync::Arc::clone(doc);
663 let opts = opts.clone();
664 rayon::spawn(move || {
665 let Ok(page) = doc.page(page_index) else {
666 return;
667 };
668 let controls = TileRenderControls {
669 quality_step: None,
670 cancel,
671 use_cache: true,
672 };
673 for r in r0..=r1 {
674 for c in c0..=c1 {
675 if controls
676 .cancel
677 .as_ref()
678 .is_some_and(TileCancelToken::is_cancelled)
679 {
680 return;
681 }
682 let _ = render_tile_with(page, &opts, tile_size, c, r, &controls);
683 }
684 }
685 });
686 Ok(scheduled)
687}
688
689#[cfg(all(test, feature = "std"))]
690mod tests {
691 use super::*;
692 use crate::djvu_document::DjVuDocument;
693 use crate::djvu_render::{
694 UserRotation, progressive_steps, render_pixmap, render_progressive_step,
695 };
696
697 fn assets_path() -> std::path::PathBuf {
698 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
699 .join("references/djvujs/library/assets")
700 }
701
702 fn load_doc(filename: &str) -> DjVuDocument {
703 let data = std::fs::read(assets_path().join(filename))
704 .unwrap_or_else(|_| panic!("{filename} must exist"));
705 DjVuDocument::parse(&data).unwrap_or_else(|e| panic!("parse failed: {e}"))
706 }
707
708 fn assemble<F>(layout: &TileLayout, mut tile: F) -> Pixmap
710 where
711 F: FnMut(u32, u32) -> Pixmap,
712 {
713 let mut out = Pixmap::white(layout.output_width(), layout.output_height());
714 let stride = layout.output_width() as usize * 4;
715 for row in 0..layout.rows() {
716 for col in 0..layout.cols() {
717 let rect = layout.tile_rect(col, row).unwrap();
718 let pm = tile(col, row);
719 assert_eq!((pm.width, pm.height), (rect.width, rect.height));
720 for y in 0..rect.height as usize {
721 let src = y * rect.width as usize * 4;
722 let dst = (rect.y as usize + y) * stride + rect.x as usize * 4;
723 out.data[dst..dst + rect.width as usize * 4]
724 .copy_from_slice(&pm.data[src..src + rect.width as usize * 4]);
725 }
726 }
727 }
728 out
729 }
730
731 #[test]
734 fn assembled_tiles_match_full_render_all_rotations() {
735 let doc = load_doc("chicken.djvu");
736 let page = doc.page(0).unwrap();
737 for rotation in [
738 UserRotation::None,
739 UserRotation::Cw90,
740 UserRotation::Rot180,
741 UserRotation::Ccw90,
742 ] {
743 let opts = RenderOptions {
744 width: 61, height: 83,
746 rotation,
747 ..Default::default()
748 };
749 let full = render_pixmap(page, &opts).unwrap();
750 let layout = TileLayout::new(page, &opts, 32).unwrap();
751 assert_eq!(
752 (full.width, full.height),
753 (layout.output_width(), layout.output_height())
754 );
755 let stitched = assemble(&layout, |c, r| render_tile(page, &opts, 32, c, r).unwrap());
756 assert_eq!(
757 full.data, stitched.data,
758 "stitched tiles must be byte-identical to the full render (rotation {rotation:?})"
759 );
760 }
761 }
762
763 #[test]
766 fn assembled_tiles_match_full_render_info_rotation() {
767 let doc = load_doc("boy_jb2_rotate90.djvu");
768 let page = doc.page(0).unwrap();
769 let opts = RenderOptions {
770 width: 45,
771 height: 57,
772 rotation: UserRotation::Cw90,
773 ..Default::default()
774 };
775 let full = render_pixmap(page, &opts).unwrap();
776 let layout = TileLayout::new(page, &opts, 16).unwrap();
777 let stitched = assemble(&layout, |c, r| render_tile(page, &opts, 16, c, r).unwrap());
778 assert_eq!(full.data, stitched.data);
779 }
780
781 #[test]
784 fn assembled_tiles_match_full_render_bilevel_downscale() {
785 let doc = load_doc("boy_jb2.djvu");
786 let page = doc.page(0).unwrap();
787 let opts = RenderOptions {
788 width: 40,
789 height: 52,
790 ..Default::default()
791 };
792 let full = render_pixmap(page, &opts).unwrap();
793 let layout = TileLayout::new(page, &opts, 16).unwrap();
794 let stitched = assemble(&layout, |c, r| render_tile(page, &opts, 16, c, r).unwrap());
795 assert_eq!(full.data, stitched.data);
796 }
797
798 #[test]
801 fn tile_pixels_independent_of_request_order() {
802 let doc = load_doc("chicken.djvu");
803 let page = doc.page(0).unwrap();
804 let opts = RenderOptions {
805 width: 61,
806 height: 83,
807 ..Default::default()
808 };
809 let layout = TileLayout::new(page, &opts, 32).unwrap();
810 let coords: Vec<(u32, u32)> = (0..layout.rows())
811 .flat_map(|r| (0..layout.cols()).map(move |c| (c, r)))
812 .collect();
813
814 let reference: Vec<Pixmap> = coords
816 .iter()
817 .map(|&(c, r)| render_tile(page, &opts, 32, c, r).unwrap())
818 .collect();
819
820 for (i, &(c, r)) in coords.iter().enumerate().rev() {
822 let pm = render_tile_cached(page, &opts, 32, c, r).unwrap();
823 assert_eq!(pm.data, reference[i].data, "reverse pass, tile ({c}, {r})");
824 }
825 for (i, &(c, r)) in coords.iter().enumerate() {
827 let pm = render_tile_cached(page, &opts, 32, c, r).unwrap();
828 assert_eq!(pm.data, reference[i].data, "warm pass, tile ({c}, {r})");
829 }
830 }
831
832 #[test]
834 fn single_tile_equals_full_render() {
835 let doc = load_doc("boy.djvu");
836 let page = doc.page(0).unwrap();
837 let opts = RenderOptions {
838 width: 50,
839 height: 70,
840 ..Default::default()
841 };
842 let layout = TileLayout::new(page, &opts, 1024).unwrap();
843 assert_eq!((layout.cols(), layout.rows()), (1, 1));
844 let tile = render_tile(page, &opts, 1024, 0, 0).unwrap();
845 let full = render_pixmap(page, &opts).unwrap();
846 assert_eq!(tile.data, full.data);
847 }
848
849 #[test]
851 fn layout_geometry_and_errors() {
852 let doc = load_doc("chicken.djvu");
853 let page = doc.page(0).unwrap();
854 let opts = RenderOptions {
855 width: 100,
856 height: 65,
857 ..Default::default()
858 };
859 let layout = TileLayout::new(page, &opts, 32).unwrap();
860 assert_eq!((layout.cols(), layout.rows()), (4, 3));
861 assert_eq!(layout.tile_count(), 12);
862 assert_eq!(
864 layout.tile_rect(0, 0).unwrap(),
865 TileRect {
866 x: 0,
867 y: 0,
868 width: 32,
869 height: 32
870 }
871 );
872 assert_eq!(
873 layout.tile_rect(3, 2).unwrap(),
874 TileRect {
875 x: 96,
876 y: 64,
877 width: 4,
878 height: 1
879 }
880 );
881 assert!(matches!(
882 layout.tile_rect(4, 0),
883 Err(TileError::OutOfRange {
884 col: 4,
885 row: 0,
886 cols: 4,
887 rows: 3
888 })
889 ));
890 assert!(matches!(
891 render_tile(page, &opts, 32, 0, 3),
892 Err(TileError::OutOfRange { .. })
893 ));
894
895 assert!(matches!(
896 TileLayout::new(page, &opts, 0),
897 Err(TileError::InvalidTileSize)
898 ));
899
900 let lanczos = RenderOptions {
901 resampling: Resampling::Lanczos3,
902 ..opts
903 };
904 assert!(matches!(
905 TileLayout::new(page, &lanczos, 32),
906 Err(TileError::Render(RenderError::UnsupportedOption(_)))
907 ));
908 let aa = RenderOptions { aa: true, ..opts };
909 assert!(matches!(
910 TileLayout::new(page, &aa, 32),
911 Err(TileError::Render(RenderError::UnsupportedOption(_)))
912 ));
913 }
914
915 #[test]
917 fn rotated_layout_swaps_display_dimensions() {
918 let doc = load_doc("chicken.djvu");
919 let page = doc.page(0).unwrap();
920 let opts = RenderOptions {
921 width: 80,
922 height: 60,
923 rotation: UserRotation::Cw90,
924 ..Default::default()
925 };
926 let layout = TileLayout::new(page, &opts, 32).unwrap();
927 assert_eq!(
928 (layout.output_width(), layout.output_height()),
929 (60, 80),
930 "Cw90 display canvas must be opts.height × opts.width"
931 );
932 }
933
934 fn warm_grid(page: &DjVuPage, opts: &RenderOptions) {
937 let layout = TileLayout::new(page, opts, 256).unwrap();
938 for row in 0..layout.rows() {
939 for col in 0..layout.cols() {
940 render_tile_cached(page, opts, 256, col, row).unwrap();
941 }
942 }
943 }
944
945 #[test]
948 fn cache_usage_budget_and_clear() {
949 let doc = load_doc("chicken.djvu");
950 let page = doc.page(0).unwrap();
951 let opts = RenderOptions {
952 width: 600,
953 height: 800,
954 ..Default::default()
955 };
956
957 let fresh = tile_cache_usage(page);
958 assert_eq!((fresh.bytes, fresh.tiles), (0, 0));
959 assert_eq!(fresh.budget, 8 * 1024 * 1024, "default budget is 8 MiB");
960
961 warm_grid(page, &opts);
962 let warm = tile_cache_usage(page);
963 assert!(warm.bytes > 0 && warm.tiles > 0);
964
965 set_tile_cache_budget(page, 300_000);
967 let shrunk = tile_cache_usage(page);
968 assert!(shrunk.bytes <= 300_000, "usage {} > budget", shrunk.bytes);
969 assert!(shrunk.tiles < warm.tiles);
970 assert_eq!(shrunk.budget, 300_000);
971
972 let cached = render_tile_cached(page, &opts, 256, 0, 0).unwrap();
974 let direct = render_tile(page, &opts, 256, 0, 0).unwrap();
975 assert_eq!(cached.data, direct.data);
976 assert!(tile_cache_usage(page).bytes <= 300_000);
977
978 set_tile_cache_budget(page, 0);
980 let cached = render_tile_cached(page, &opts, 256, 1, 1).unwrap();
981 let direct = render_tile(page, &opts, 256, 1, 1).unwrap();
982 assert_eq!(cached.data, direct.data);
983 assert_eq!(tile_cache_usage(page).bytes, 0);
984
985 set_tile_cache_budget(page, 8 * 1024 * 1024);
987 warm_grid(page, &opts);
988 let before = tile_cache_usage(page);
989 let freed = clear_tile_cache(page);
990 assert_eq!(freed, before.bytes);
991 let after = tile_cache_usage(page);
992 assert_eq!((after.bytes, after.tiles), (0, 0));
993 assert_eq!(after.budget, 8 * 1024 * 1024);
994 }
995
996 #[test]
999 fn invalidate_region_drops_overlapping_tiles_across_scales() {
1000 let doc = load_doc("chicken.djvu");
1001 let page = doc.page(0).unwrap();
1002 let big = RenderOptions {
1003 width: 600,
1004 height: 800,
1005 ..Default::default()
1006 };
1007 let small = RenderOptions {
1008 width: 300,
1009 height: 400,
1010 ..Default::default()
1011 };
1012 warm_grid(page, &big); warm_grid(page, &small); let reference = render_tile_cached(page, &big, 256, 0, 0).unwrap();
1015 let before = tile_cache_usage(page);
1016 assert_eq!(before.tiles, 16);
1017
1018 let freed = invalidate_tile_region(
1022 page,
1023 &big,
1024 TileRect {
1025 x: 0,
1026 y: 0,
1027 width: 300,
1028 height: 800,
1029 },
1030 )
1031 .unwrap();
1032 assert!(freed > 0);
1033 let after = tile_cache_usage(page);
1034 assert_eq!(after.tiles, 6, "only right-column tiles survive");
1035 assert_eq!(after.bytes, before.bytes - freed);
1036
1037 let rerendered = render_tile_cached(page, &big, 256, 0, 0).unwrap();
1039 assert_eq!(rerendered.data, reference.data);
1040
1041 assert_eq!(
1043 invalidate_tile_region(
1044 page,
1045 &big,
1046 TileRect {
1047 x: 599,
1048 y: 799,
1049 width: 0,
1050 height: 0,
1051 },
1052 )
1053 .unwrap(),
1054 0
1055 );
1056 }
1057
1058 #[test]
1061 fn invalidate_maps_display_rect_through_rotation() {
1062 let identity = RenderOptions {
1063 width: 600,
1064 height: 800,
1065 ..Default::default()
1066 };
1067 let rotated = RenderOptions {
1068 rotation: UserRotation::Cw90,
1069 ..identity.clone()
1070 };
1071
1072 let doc_a = load_doc("chicken.djvu");
1074 let page_a = doc_a.page(0).unwrap();
1075 warm_grid(page_a, &identity);
1076 let doc_b = load_doc("chicken.djvu");
1077 let page_b = doc_b.page(0).unwrap();
1078 warm_grid(page_b, &identity);
1079
1080 let freed_identity = invalidate_tile_region(
1084 page_a,
1085 &identity,
1086 TileRect {
1087 x: 0,
1088 y: 0,
1089 width: 300,
1090 height: 800,
1091 },
1092 )
1093 .unwrap();
1094 let freed_rotated = invalidate_tile_region(
1095 page_b,
1096 &rotated,
1097 TileRect {
1098 x: 0,
1099 y: 0,
1100 width: 800,
1101 height: 300,
1102 },
1103 )
1104 .unwrap();
1105 assert!(freed_identity > 0);
1106 assert_eq!(freed_identity, freed_rotated);
1107 assert_eq!(
1108 tile_cache_usage(page_a).tiles,
1109 tile_cache_usage(page_b).tiles
1110 );
1111 }
1112
1113 #[cfg(feature = "parallel")]
1116 #[test]
1117 fn prefetch_tiles_warms_cache() {
1118 let doc = std::sync::Arc::new(load_doc("chicken.djvu"));
1119 let page = doc.page(0).unwrap();
1120 let opts = RenderOptions {
1121 width: 600,
1122 height: 800,
1123 ..Default::default()
1124 };
1125
1126 assert_eq!(prefetch_tiles(&doc, 99, &opts, 256, 0, 0, 1).unwrap(), 0);
1127 assert!(matches!(
1128 prefetch_tiles(&doc, 0, &opts, 256, 99, 0, 1),
1129 Err(TileError::OutOfRange { .. })
1130 ));
1131
1132 let scheduled = prefetch_tiles(&doc, 0, &opts, 256, 0, 0, 1).unwrap();
1134 assert_eq!(scheduled, 4);
1135
1136 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
1137 while tile_cache_usage(page).tiles < 4 && std::time::Instant::now() < deadline {
1138 std::thread::sleep(std::time::Duration::from_millis(10));
1139 }
1140 assert!(
1141 tile_cache_usage(page).tiles >= 4,
1142 "prefetch never landed: {:?}",
1143 tile_cache_usage(page)
1144 );
1145 let warm = render_tile_cached(page, &opts, 256, 0, 0).unwrap();
1146 let direct = render_tile(page, &opts, 256, 0, 0).unwrap();
1147 assert_eq!(warm.data, direct.data);
1148 }
1149
1150 #[test]
1153 fn progressive_tiles_match_progressive_frames() {
1154 let doc = load_doc("chicken.djvu");
1155 let page = doc.page(0).unwrap();
1156 let steps = progressive_steps(page);
1157 assert!(steps >= 2, "need a multi-chunk BG44 page");
1158 let opts = RenderOptions {
1159 width: 61,
1160 height: 83,
1161 ..Default::default()
1162 };
1163 let layout = TileLayout::new(page, &opts, 32).unwrap();
1164 let mut frames = Vec::new();
1165 for step in 0..steps {
1166 let full = render_progressive_step(page, &opts, step).unwrap();
1167 let controls = TileRenderControls {
1168 quality_step: Some(step),
1169 ..Default::default()
1170 };
1171 let stitched = assemble(&layout, |c, r| {
1172 render_tile_with(page, &opts, 32, c, r, &controls).unwrap()
1173 });
1174 assert_eq!(
1175 full.data, stitched.data,
1176 "stitched step-{step} tiles must match the progressive frame"
1177 );
1178 frames.push(full.data);
1179 }
1180 assert_ne!(
1181 frames[0],
1182 frames[steps - 1],
1183 "the first and last quality steps must differ (refinement adds detail)"
1184 );
1185 }
1186
1187 #[test]
1190 fn progressive_tiles_match_under_rotation() {
1191 let doc = load_doc("chicken.djvu");
1192 let page = doc.page(0).unwrap();
1193 let opts = RenderOptions {
1194 width: 61,
1195 height: 83,
1196 rotation: UserRotation::Cw90,
1197 ..Default::default()
1198 };
1199 let full = render_progressive_step(page, &opts, 0).unwrap();
1200 let layout = TileLayout::new(page, &opts, 32).unwrap();
1201 let controls = TileRenderControls {
1202 quality_step: Some(0),
1203 ..Default::default()
1204 };
1205 let stitched = assemble(&layout, |c, r| {
1206 render_tile_with(page, &opts, 32, c, r, &controls).unwrap()
1207 });
1208 assert_eq!(full.data, stitched.data);
1209 }
1210
1211 #[test]
1214 fn render_tile_with_matches_dedicated_entry_points() {
1215 let doc = load_doc("chicken.djvu");
1216 let page = doc.page(0).unwrap();
1217 let opts = RenderOptions {
1218 width: 61,
1219 height: 83,
1220 ..Default::default()
1221 };
1222 let direct = render_tile(page, &opts, 32, 1, 1).unwrap();
1223
1224 let default = render_tile_with(page, &opts, 32, 1, 1, &TileRenderControls::default());
1225 assert_eq!(default.unwrap().data, direct.data);
1226
1227 let cached = render_tile_with(
1228 page,
1229 &opts,
1230 32,
1231 1,
1232 1,
1233 &TileRenderControls {
1234 use_cache: true,
1235 ..Default::default()
1236 },
1237 );
1238 assert_eq!(cached.unwrap().data, direct.data);
1239 }
1240
1241 #[test]
1244 fn quality_steps_on_bilevel_page() {
1245 let doc = load_doc("boy_jb2.djvu");
1246 let page = doc.page(0).unwrap();
1247 assert_eq!(progressive_steps(page), 1);
1248 let opts = RenderOptions {
1249 width: 40,
1250 height: 52,
1251 ..Default::default()
1252 };
1253 let direct = render_tile(page, &opts, 16, 0, 0).unwrap();
1254 let step0 = render_tile_with(
1255 page,
1256 &opts,
1257 16,
1258 0,
1259 0,
1260 &TileRenderControls {
1261 quality_step: Some(0),
1262 ..Default::default()
1263 },
1264 )
1265 .unwrap();
1266 assert_eq!(step0.data, direct.data);
1267
1268 assert!(matches!(
1269 render_tile_with(
1270 page,
1271 &opts,
1272 16,
1273 0,
1274 0,
1275 &TileRenderControls {
1276 quality_step: Some(1),
1277 ..Default::default()
1278 },
1279 ),
1280 Err(TileError::Render(RenderError::ChunkOutOfRange {
1281 chunk_n: 1,
1282 max: 0
1283 }))
1284 ));
1285 }
1286
1287 #[test]
1290 fn cancelled_token_aborts_every_mode() {
1291 let doc = load_doc("chicken.djvu");
1292 let page = doc.page(0).unwrap();
1293 let opts = RenderOptions {
1294 width: 61,
1295 height: 83,
1296 ..Default::default()
1297 };
1298
1299 let token = TileCancelToken::new();
1300 assert!(!token.is_cancelled());
1301 let shared = token.clone();
1302 shared.cancel();
1303 assert!(token.is_cancelled(), "clones share one flag");
1304
1305 for controls in [
1306 TileRenderControls {
1307 cancel: Some(token.clone()),
1308 ..Default::default()
1309 },
1310 TileRenderControls {
1311 cancel: Some(token.clone()),
1312 use_cache: true,
1313 ..Default::default()
1314 },
1315 TileRenderControls {
1316 cancel: Some(token.clone()),
1317 quality_step: Some(0),
1318 ..Default::default()
1319 },
1320 ] {
1321 assert!(matches!(
1322 render_tile_with(page, &opts, 32, 0, 0, &controls),
1323 Err(TileError::Cancelled)
1324 ));
1325 }
1326
1327 let live = TileCancelToken::new();
1329 let with_token = render_tile_with(
1330 page,
1331 &opts,
1332 32,
1333 0,
1334 0,
1335 &TileRenderControls {
1336 cancel: Some(live),
1337 ..Default::default()
1338 },
1339 )
1340 .unwrap();
1341 let direct = render_tile(page, &opts, 32, 0, 0).unwrap();
1342 assert_eq!(with_token.data, direct.data);
1343 }
1344
1345 #[cfg(feature = "parallel")]
1348 #[test]
1349 fn prefetch_tiles_cancellable_behaviour() {
1350 let doc = std::sync::Arc::new(load_doc("chicken.djvu"));
1351 let page = doc.page(0).unwrap();
1352 let opts = RenderOptions {
1353 width: 600,
1354 height: 800,
1355 ..Default::default()
1356 };
1357
1358 let cancelled = TileCancelToken::new();
1359 cancelled.cancel();
1360 assert!(matches!(
1361 prefetch_tiles_cancellable(&doc, 0, &opts, 256, 0, 0, 1, &cancelled),
1362 Err(TileError::Cancelled)
1363 ));
1364 assert_eq!(
1365 tile_cache_usage(page).tiles,
1366 0,
1367 "a pre-cancelled prefetch must not warm anything"
1368 );
1369
1370 let live = TileCancelToken::new();
1371 let scheduled = prefetch_tiles_cancellable(&doc, 0, &opts, 256, 0, 0, 1, &live).unwrap();
1372 assert_eq!(scheduled, 4);
1373 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
1374 while tile_cache_usage(page).tiles < 4 && std::time::Instant::now() < deadline {
1375 std::thread::sleep(std::time::Duration::from_millis(10));
1376 }
1377 assert!(tile_cache_usage(page).tiles >= 4);
1378 let warm = render_tile_cached(page, &opts, 256, 0, 0).unwrap();
1379 let direct = render_tile(page, &opts, 256, 0, 0).unwrap();
1380 assert_eq!(warm.data, direct.data);
1381 }
1382}