1#[cfg(not(feature = "std"))]
39use alloc::{string::String, sync::Arc, vec, vec::Vec};
40#[cfg(feature = "std")]
41use std::sync::Arc;
42
43use crate::djvu_document::DjVuPage;
44use crate::iw44::Iw44Image;
45use crate::pixmap::{GrayPixmap, Pixmap};
46
47#[cfg(test)]
61thread_local! {
62 pub(crate) static BG44_CHUNK_DECODES: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
63}
64
65#[cfg(test)]
66fn count_bg44_chunk_decode() {
67 BG44_CHUNK_DECODES.with(|c| c.set(c.get() + 1));
68}
69
70#[cfg(test)]
74thread_local! {
75 pub(crate) static JB2_MASK_DECODES: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
76}
77
78#[cfg(test)]
79fn count_jb2_mask_decode() {
80 JB2_MASK_DECODES.with(|c| c.set(c.get() + 1));
81}
82
83#[derive(Debug, thiserror::Error)]
87#[non_exhaustive]
88pub enum RenderError {
89 #[error("IW44 decode error: {0}")]
91 Iw44(#[from] crate::error::Iw44Error),
92
93 #[error("JB2 decode error: {0}")]
95 Jb2(#[from] crate::error::Jb2Error),
96
97 #[error("buffer too small: need {need} bytes, got {got}")]
99 BufTooSmall { need: usize, got: usize },
100
101 #[error("invalid render dimensions: {width}x{height}")]
103 InvalidDimensions { width: u32, height: u32 },
104
105 #[error("chunk index {chunk_n} out of range (max {max})")]
107 ChunkOutOfRange { chunk_n: usize, max: usize },
108
109 #[error("BZZ error: {0}")]
111 Bzz(#[from] crate::error::BzzError),
112
113 #[cfg(feature = "std")]
115 #[error("JPEG decode error: {0}")]
116 Jpeg(String),
117
118 #[error("document error: {0}")]
120 Doc(#[from] crate::djvu_document::DocError),
121
122 #[error("{0}")]
124 ResourceLimit(#[from] crate::resource_limits::ResourceLimitExceeded),
125
126 #[error("unsupported render option: {0}")]
132 UnsupportedOption(&'static str),
133}
134
135impl From<crate::pixmap::PixmapError> for RenderError {
142 fn from(e: crate::pixmap::PixmapError) -> Self {
143 match e {
144 crate::pixmap::PixmapError::Overflow { width, height } => {
145 RenderError::InvalidDimensions { width, height }
146 }
147 crate::pixmap::PixmapError::TooLarge {
148 width,
149 height,
150 pixels,
151 max,
152 } => RenderError::ResourceLimit(crate::resource_limits::ResourceLimitExceeded {
153 operation: "render",
154 axis: crate::resource_limits::ResourceLimitAxis::RenderOutputPixels,
155 found: pixels as u64,
156 limit: max as u64,
157 page_number: None,
158 width: Some(width),
159 height: Some(height),
160 }),
161 #[allow(unreachable_patterns)]
164 _ => RenderError::UnsupportedOption("output pixmap size refused"),
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
177pub enum UserRotation {
178 #[default]
180 None,
181 Cw90,
183 Rot180,
185 Ccw90,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
193pub enum Resampling {
194 #[default]
196 Bilinear,
197 Lanczos3,
204}
205
206#[derive(Debug, Clone, PartialEq)]
222pub struct RenderOptions {
223 pub width: u32,
225 pub height: u32,
227 #[deprecated(
237 since = "0.20.1",
238 note = "scale is derived from `width` by the render pipeline and is no longer read; \
239 build options via `RenderOptions::fit_to_width`/`fit_to_box`. \
240 This field is ignored and will be removed in a future release."
241 )]
242 pub scale: f32,
243 pub bold: u8,
245 pub aa: bool,
247 pub rotation: UserRotation,
249 pub permissive: bool,
261 pub resampling: Resampling,
265 pub mask_aa: bool,
278}
279
280impl Default for RenderOptions {
281 #[allow(deprecated)] fn default() -> Self {
283 RenderOptions {
284 width: 0,
285 height: 0,
286 scale: 1.0,
287 bold: 0,
288 aa: false,
289 rotation: UserRotation::None,
290 permissive: false,
291 resampling: Resampling::Bilinear,
292 mask_aa: false,
293 }
294 }
295}
296
297fn effective_max_render_pixels(
298 page: &crate::djvu_document::DjVuPage,
299 limits: Option<crate::resource_limits::ResourceLimits>,
300) -> u64 {
301 limits
302 .and_then(|limits| limits.max_render_pixels)
303 .or(page
304 .resource_limits()
305 .and_then(|limits| limits.max_render_pixels))
306 .unwrap_or(crate::resource_limits::DEFAULT_MAX_RENDER_PIXELS)
307}
308
309fn check_output_pixels(
310 operation: &'static str,
311 page: &crate::djvu_document::DjVuPage,
312 limits: Option<crate::resource_limits::ResourceLimits>,
313 width: u32,
314 height: u32,
315) -> Result<(), RenderError> {
316 if width == 0 || height == 0 {
317 return Err(RenderError::InvalidDimensions { width, height });
318 }
319 let pixels = u64::from(width) * u64::from(height);
320 let limit = effective_max_render_pixels(page, limits);
321 if pixels > limit {
322 return Err(RenderError::ResourceLimit(
323 crate::resource_limits::ResourceLimitExceeded {
324 operation,
325 axis: crate::resource_limits::ResourceLimitAxis::RenderOutputPixels,
326 found: pixels,
327 limit,
328 page_number: Some(page.index() + 1),
329 width: Some(width),
330 height: Some(height),
331 },
332 ));
333 }
334 Ok(())
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339pub enum RecoveredLayer {
340 Background,
342 Foreground,
344 Mask,
346 ForegroundPalette,
348}
349
350#[derive(Debug, Clone, PartialEq, Eq)]
352pub struct RenderRecovery {
353 pub layer: RecoveredLayer,
355 pub detail: String,
357}
358
359#[derive(Debug, Clone, Default)]
364pub struct RenderReport {
365 pub recoveries: Vec<RenderRecovery>,
367}
368
369impl RenderReport {
370 pub fn is_clean(&self) -> bool {
372 self.recoveries.is_empty()
373 }
374}
375
376#[cfg(feature = "std")]
377thread_local! {
378 static RECOVERY_SINK: core::cell::RefCell<Option<Vec<RenderRecovery>>> =
382 const { core::cell::RefCell::new(None) };
383}
384
385#[cfg(feature = "std")]
387fn record_recovery(layer: RecoveredLayer, detail: impl Into<String>) {
388 RECOVERY_SINK.with(|sink| {
389 if let Some(list) = sink.borrow_mut().as_mut() {
390 list.push(RenderRecovery {
391 layer,
392 detail: detail.into(),
393 });
394 }
395 });
396}
397
398#[cfg(not(feature = "std"))]
400fn record_recovery(_layer: RecoveredLayer, _detail: &str) {}
401
402fn permissive_layer<T>(result: Result<Option<T>, RenderError>, layer: RecoveredLayer) -> Option<T> {
404 match result {
405 Ok(value) => value,
406 Err(error) => {
407 record_recovery(layer, {
408 #[cfg(feature = "std")]
409 {
410 error.to_string()
411 }
412 #[cfg(not(feature = "std"))]
413 {
414 let _ = error;
415 ""
416 }
417 });
418 None
419 }
420 }
421}
422
423#[allow(deprecated)] impl RenderOptions {
425 pub fn fit_to_width(page: &crate::djvu_document::DjVuPage, width: u32) -> Self {
428 let (dw, dh) = display_dimensions(page);
429 let height = if dw == 0 {
430 width
431 } else {
432 ((dh as f64 * width as f64) / dw as f64).round() as u32
433 }
434 .max(1);
435 let scale = width as f32 / dw.max(1) as f32;
436 RenderOptions {
437 width,
438 height,
439 scale,
440 ..Default::default()
441 }
442 }
443
444 pub fn fit_to_height(page: &crate::djvu_document::DjVuPage, height: u32) -> Self {
447 let (dw, dh) = display_dimensions(page);
448 let width = if dh == 0 {
449 height
450 } else {
451 ((dw as f64 * height as f64) / dh as f64).round() as u32
452 }
453 .max(1);
454 let scale = height as f32 / dh.max(1) as f32;
455 RenderOptions {
456 width,
457 height,
458 scale,
459 ..Default::default()
460 }
461 }
462
463 pub fn fit_to_box(
466 page: &crate::djvu_document::DjVuPage,
467 max_width: u32,
468 max_height: u32,
469 ) -> Self {
470 let (dw, dh) = display_dimensions(page);
471 if dw == 0 || dh == 0 {
472 return RenderOptions {
473 width: max_width.max(1),
474 height: max_height.max(1),
475 scale: 1.0,
476 ..Default::default()
477 };
478 }
479 let scale_w = max_width as f64 / dw as f64;
480 let scale_h = max_height as f64 / dh as f64;
481 let scale = if scale_w < scale_h { scale_w } else { scale_h };
482 let width = (dw as f64 * scale).round() as u32;
483 let height = (dh as f64 * scale).round() as u32;
484 RenderOptions {
485 width: width.max(1),
486 height: height.max(1),
487 scale: scale as f32,
488 ..Default::default()
489 }
490 }
491
492 pub fn can_stream(&self, page: &crate::djvu_document::DjVuPage) -> bool {
505 !self.aa
506 && (self.resampling == Resampling::Bilinear
507 || (page.width() as u32 == self.width && page.height() as u32 == self.height))
508 && page.rotation() == crate::info::Rotation::None
509 && self.rotation == UserRotation::None
510 }
511
512 pub(crate) fn decode_scale(&self, page: &crate::djvu_document::DjVuPage) -> f32 {
532 self.width as f32 / (page.width() as u32).max(1) as f32
533 }
534}
535
536pub(crate) fn display_dimensions(page: &crate::djvu_document::DjVuPage) -> (u32, u32) {
541 let w = page.width() as u32;
542 let h = page.height() as u32;
543 match page.rotation() {
544 crate::info::Rotation::Cw90 | crate::info::Rotation::Ccw90 => (h, w),
545 _ => (w, h),
546 }
547}
548
549const DISPLAY_GAMMA: f32 = 2.2;
553
554fn build_gamma_lut(gamma: f32) -> [u8; 256] {
567 let mut lut = [0u8; 256];
568 let exponent = if gamma <= 0.0 || !gamma.is_finite() {
569 1.0_f32 } else {
571 gamma / DISPLAY_GAMMA
572 };
573 if (exponent - 1.0).abs() < 1e-4 {
574 for (i, v) in lut.iter_mut().enumerate() {
576 *v = i as u8;
577 }
578 return lut;
579 }
580 for (i, v) in lut.iter_mut().enumerate() {
581 let linear = i as f32 / 255.0;
582 let corrected = linear.powf(exponent);
583 *v = (corrected * 255.0 + 0.5) as u8;
584 }
585 lut
586}
587
588const FRACBITS: u32 = 4;
592const FRAC: u32 = 1 << FRACBITS;
593const FRAC_MASK: u32 = FRAC - 1;
594
595const MASK_EXPAND: [[u8; 8]; 256] = {
597 let mut lut = [[0u8; 8]; 256];
598 let mut b = 0usize;
599 while b < 256 {
600 let mut bit = 0usize;
601 while bit < 8 {
602 lut[b][bit] = if (b >> (7 - bit)) & 1 != 0 {
603 0xFF
604 } else {
605 0x00
606 };
607 bit += 1;
608 }
609 b += 1;
610 }
611 lut
612};
613
614const BILEVEL_RGBA: [[u8; 32]; 256] = {
618 let mut lut = [[0u8; 32]; 256];
619 let mut mb = 0usize;
620 while mb < 256 {
621 let mut bit = 0usize;
622 while bit < 8 {
623 let ch = if (mb >> (7 - bit)) & 1 != 0 {
624 0u8
625 } else {
626 255u8
627 };
628 lut[mb][bit * 4] = ch;
629 lut[mb][bit * 4 + 1] = ch;
630 lut[mb][bit * 4 + 2] = ch;
631 lut[mb][bit * 4 + 3] = 255;
632 bit += 1;
633 }
634 mb += 1;
635 }
636 lut
637};
638
639#[cfg(feature = "std")]
649#[allow(unsafe_code)]
650#[inline]
651fn rgb_to_rgba(src: &[u8], dst: &mut [u8]) {
652 let pixel_count = src.len() / 3;
653 debug_assert_eq!(dst.len(), pixel_count * 4);
654
655 #[cfg(target_arch = "x86_64")]
656 if is_x86_feature_detected!("ssse3") {
657 unsafe {
659 let safe_chunks = if src.len() >= 16 {
662 ((src.len() - 16) / 12 + 1).min(pixel_count / 4)
663 } else {
664 0
665 };
666 rgb_to_rgba_ssse3(src, dst, pixel_count, safe_chunks);
667 }
668 return;
669 }
670
671 rgb_to_rgba_scalar(src, dst, 0, pixel_count);
672}
673
674#[cfg(all(feature = "std", target_arch = "x86_64"))]
675#[allow(unsafe_code, unsafe_op_in_unsafe_fn)]
676#[target_feature(enable = "ssse3")]
677unsafe fn rgb_to_rgba_ssse3(src: &[u8], dst: &mut [u8], pixel_count: usize, safe_chunks: usize) {
680 use core::arch::x86_64::*;
681
682 let shuf = _mm_set_epi8(
685 -1, 11, 10, 9, -1, 8, 7, 6, -1, 5, 4, 3, -1, 2, 1, 0, );
690 let alpha_or = _mm_set1_epi32(0xFF000000u32 as i32);
691
692 for i in 0..safe_chunks {
693 let v = _mm_loadu_si128(src.as_ptr().add(i * 12) as *const __m128i);
694 _mm_storeu_si128(
695 dst.as_mut_ptr().add(i * 16) as *mut __m128i,
696 _mm_or_si128(_mm_shuffle_epi8(v, shuf), alpha_or),
697 );
698 }
699
700 rgb_to_rgba_scalar(src, dst, safe_chunks * 4, pixel_count);
701}
702
703#[cfg(feature = "std")]
704#[inline]
705fn rgb_to_rgba_scalar(src: &[u8], dst: &mut [u8], start: usize, end: usize) {
706 for i in start..end {
707 dst[i * 4] = src[i * 3];
708 dst[i * 4 + 1] = src[i * 3 + 1];
709 dst[i * 4 + 2] = src[i * 3 + 2];
710 dst[i * 4 + 3] = 255;
711 }
712}
713
714#[inline]
719fn plane_q24(plane: Option<&Pixmap>, page_w: u32, page_h: u32) -> (u64, u64) {
720 match plane {
721 Some(p) if page_w > 0 && page_h > 0 => (
722 ((p.width as u64) << 24) / page_w as u64,
723 ((p.height as u64) << 24) / page_h as u64,
724 ),
725 _ => (0, 0),
726 }
727}
728
729#[inline]
734fn map_plane_center_frac(page_frac: u32, q24: u64) -> u32 {
735 let centered = (((page_frac as u64 + (FRAC / 2) as u64) * q24) >> 24) as u32;
736 centered.saturating_sub(FRAC / 2)
737}
738
739#[inline]
740fn fg_q24(fg: Option<&Pixmap>, page_w: u32, page_h: u32) -> (u64, u64) {
741 match fg {
742 Some(p) if page_w > 0 && page_h > 0 && p.width > 0 && p.height > 0 => {
743 let sx = page_w.div_ceil(p.width).max(1);
751 (
752 (1u64 << 24) / sx as u64,
753 ((p.height as u64) << 24) / page_h as u64,
754 )
755 }
756 _ => plane_q24(fg, page_w, page_h),
757 }
758}
759
760#[inline]
763fn bg_q24(bg: Option<(u32, u32)>, page_w: u32, page_h: u32) -> (u64, u64) {
764 match bg {
765 Some((w, h)) if page_w > 0 && page_h > 0 && w > 0 && h > 0 => {
766 let sx = page_w.div_ceil(w).max(1);
770 let sy = page_h.div_ceil(h).max(1);
771 ((1u64 << 24) / sx as u64, (1u64 << 24) / sy as u64)
772 }
773 _ => (0, 0),
774 }
775}
776
777#[inline]
782#[cfg_attr(not(test), allow(dead_code))]
783fn sample_bilinear(pm: &Pixmap, fx: u32, fy: u32) -> (u8, u8, u8) {
784 let x0 = (fx >> FRACBITS).min(pm.width.saturating_sub(1));
785 let y0 = (fy >> FRACBITS).min(pm.height.saturating_sub(1));
786 let x1 = (x0 + 1).min(pm.width.saturating_sub(1));
787 let y1 = (y0 + 1).min(pm.height.saturating_sub(1));
788
789 let tx = fx & FRAC_MASK; let ty = fy & FRAC_MASK;
791
792 let (r00, g00, b00) = pm.get_rgb(x0, y0);
793 let (r10, g10, b10) = pm.get_rgb(x1, y0);
794 let (r01, g01, b01) = pm.get_rgb(x0, y1);
795 let (r11, g11, b11) = pm.get_rgb(x1, y1);
796
797 let lerp = |a: u8, b: u8, c: u8, d: u8| -> u8 {
798 let top = a as u32 * (FRAC - tx) + b as u32 * tx;
799 let bot = c as u32 * (FRAC - tx) + d as u32 * tx;
800 let numerator = top * (FRAC - ty) + bot * ty;
801 ((numerator + (1 << (2 * FRACBITS - 1))) >> (2 * FRACBITS)) as u8
803 };
804
805 (
806 lerp(r00, r10, r01, r11),
807 lerp(g00, g10, g01, g11),
808 lerp(b00, b10, b01, b11),
809 )
810}
811
812#[inline]
815fn bilinear_from_rows(row0: &[u8], row1: &[u8], width: u32, fx: u32, ty: u32) -> (u8, u8, u8) {
816 let w = width.saturating_sub(1) as usize;
817 let x0 = (fx >> FRACBITS) as usize;
818 let x0 = x0.min(w);
819 let x1 = (x0 + 1).min(w);
820 let tx = fx & FRAC_MASK;
821
822 let get = |row: &[u8], x: usize| -> (u8, u8, u8) {
824 let off = x * 4;
825 if let Some(q) = row.get(off..off + 4) {
826 (q[0], q[1], q[2])
827 } else {
828 (0, 0, 0)
829 }
830 };
831 let (r00, g00, b00) = get(row0, x0);
832 let (r10, g10, b10) = get(row0, x1);
833 let (r01, g01, b01) = get(row1, x0);
834 let (r11, g11, b11) = get(row1, x1);
835
836 let itx = FRAC - tx;
840 let ity = FRAC - ty;
841 let w00 = itx * ity;
842 let w10 = tx * ity;
843 let w01 = itx * ty;
844 let w11 = tx * ty;
845 let blend = |a: u8, b: u8, c: u8, d: u8| -> u8 {
847 ((a as u32 * w00 + b as u32 * w10 + c as u32 * w01 + d as u32 * w11 + 128) >> 8) as u8
848 };
849 (
850 blend(r00, r10, r01, r11),
851 blend(g00, g10, g01, g11),
852 blend(b00, b10, b01, b11),
853 )
854}
855
856#[inline]
857#[cfg_attr(not(test), allow(dead_code))]
858fn sample_nearest(pm: &Pixmap, fx: u32, fy: u32) -> (u8, u8, u8) {
859 let x = ((fx + FRAC / 2) >> FRACBITS).min(pm.width.saturating_sub(1));
860 let y = ((fy + FRAC / 2) >> FRACBITS).min(pm.height.saturating_sub(1));
861 pm.get_rgb(x, y)
862}
863
864#[inline]
871fn sample_area_avg(pm: &Pixmap, fx: u32, fy: u32, fx_step: u32, fy_step: u32) -> (u8, u8, u8) {
872 let (x0, x1) = area_range(pm.width, fx, fx_step);
873 let (y0, y1) = area_range(pm.height, fy, fy_step);
874 sample_area_avg_bounds(PlaneView::whole(pm), x0, x1, y0, y1)
875}
876
877#[inline]
878fn sample_area_avg_bounds(pm: PlaneView<'_>, x0: u32, x1: u32, y0: u32, y1: u32) -> (u8, u8, u8) {
879 let cols = (x1 - x0) as usize;
880 let rows = (y1 - y0) as usize;
881
882 if cols <= 1 && rows <= 1 {
884 let off = x0 as usize * 4;
885 return pm
886 .row(y0)
887 .get(off..off + 4)
888 .map_or((0, 0, 0), |q| (q[0], q[1], q[2]));
889 }
890
891 let mut r_sum = 0u32;
892 let mut g_sum = 0u32;
893 let mut b_sum = 0u32;
894
895 for sy in y0..y1 {
897 let x_off = x0 as usize * 4;
898 if let Some(row) = pm.row(sy).get(x_off..x_off + cols * 4) {
899 for chunk in row.as_chunks::<4>().0 {
900 r_sum += chunk[0] as u32;
901 g_sum += chunk[1] as u32;
902 b_sum += chunk[2] as u32;
903 }
904 }
905 }
906
907 let count = (rows * cols) as u32;
908 if count == 0 {
909 return (255, 255, 255);
910 }
911
912 let half = count >> 1;
914 if count.is_power_of_two() {
915 let shift = count.trailing_zeros();
916 (
917 ((r_sum + half) >> shift) as u8,
918 ((g_sum + half) >> shift) as u8,
919 ((b_sum + half) >> shift) as u8,
920 )
921 } else {
922 (
923 ((r_sum + half) / count) as u8,
924 ((g_sum + half) / count) as u8,
925 ((b_sum + half) / count) as u8,
926 )
927 }
928}
929
930#[inline]
935fn mask_box_coverage(
936 mask: &crate::bitmap::Bitmap,
937 fx: u32,
938 fy: u32,
939 fx_step: u32,
940 fy_step: u32,
941) -> u8 {
942 let x0 = (fx >> FRACBITS).min(mask.width.saturating_sub(1));
943 let y0 = (fy >> FRACBITS).min(mask.height.saturating_sub(1));
944 let x1 = ((fx + fx_step) >> FRACBITS).min(mask.width);
945 let y1 = ((fy + fy_step) >> FRACBITS).min(mask.height);
946 let total = (x1 - x0) * (y1 - y0);
947 if total == 0 {
948 return 0;
949 }
950 let stride = mask.row_stride();
954 let byte_lo = x0 as usize / 8;
955 let byte_hi = (x1 as usize).div_ceil(8); let first_mask = 0xFF_u8 >> (x0 % 8);
957 let end_mask = if x1.is_multiple_of(8) {
958 0xFF_u8
959 } else {
960 0xFF_u8 << (8 - x1 % 8)
961 };
962 let mut count = 0u32;
963 if byte_hi == byte_lo + 1 {
964 let combined = first_mask & end_mask;
966 for sy in y0..y1 {
967 count += (mask.data[sy as usize * stride + byte_lo] & combined).count_ones();
968 }
969 } else {
970 for sy in y0..y1 {
971 let row = &mask.data[sy as usize * stride..];
972 count += (row[byte_lo] & first_mask).count_ones();
973 for byte in row[(byte_lo + 1)..(byte_hi - 1)].iter() {
974 count += byte.count_ones();
975 }
976 count += (row[byte_hi - 1] & end_mask).count_ones();
977 }
978 }
979 ((count as u64 * 255 + total as u64 / 2) / total as u64) as u8
983}
984
985#[inline]
1002fn mask_bilinear_coverage(mask: &crate::bitmap::Bitmap, fx: u32, fy: u32) -> u8 {
1003 let x0 = (fx >> FRACBITS).min(mask.width.saturating_sub(1));
1004 let y0 = (fy >> FRACBITS).min(mask.height.saturating_sub(1));
1005 let x1 = (x0 + 1).min(mask.width.saturating_sub(1));
1006 let y1 = (y0 + 1).min(mask.height.saturating_sub(1));
1007
1008 let tx = fx & FRAC_MASK;
1009 let ty = fy & FRAC_MASK;
1010
1011 let bit = |x: u32, y: u32| -> u32 { if mask.get(x, y) { 255 } else { 0 } };
1012 let v00 = bit(x0, y0);
1013 let v10 = bit(x1, y0);
1014 let v01 = bit(x0, y1);
1015 let v11 = bit(x1, y1);
1016
1017 let top = v00 * (FRAC - tx) + v10 * tx;
1018 let bot = v01 * (FRAC - tx) + v11 * tx;
1019 let numerator = top * (FRAC - ty) + bot * ty;
1020 ((numerator + (1 << (2 * FRACBITS - 1))) >> (2 * FRACBITS)) as u8
1022}
1023
1024#[inline]
1026fn mask_box_center_fg(
1027 mask: &crate::bitmap::Bitmap,
1028 fx: u32,
1029 fy: u32,
1030 fx_step: u32,
1031 fy_step: u32,
1032) -> (u32, u32) {
1033 let cx = (fx + fx_step / 2) >> FRACBITS;
1035 let cy = (fy + fy_step / 2) >> FRACBITS;
1036 (
1037 cx.min(mask.width.saturating_sub(1)),
1038 cy.min(mask.height.saturating_sub(1)),
1039 )
1040}
1041
1042fn aa_downscale(pm: &Pixmap) -> Pixmap {
1048 let out_w = (pm.width / 2).max(1);
1049 let out_h = (pm.height / 2).max(1);
1050 let mut out = Pixmap::white(out_w, out_h);
1051 for y in 0..out_h {
1052 for x in 0..out_w {
1053 let sx = (x * 2).min(pm.width.saturating_sub(1));
1054 let sy = (y * 2).min(pm.height.saturating_sub(1));
1055 let sx1 = (sx + 1).min(pm.width.saturating_sub(1));
1056 let sy1 = (sy + 1).min(pm.height.saturating_sub(1));
1057
1058 let (r00, g00, b00) = pm.get_rgb(sx, sy);
1059 let (r10, g10, b10) = pm.get_rgb(sx1, sy);
1060 let (r01, g01, b01) = pm.get_rgb(sx, sy1);
1061 let (r11, g11, b11) = pm.get_rgb(sx1, sy1);
1062
1063 let avg = |a: u8, b: u8, c: u8, d: u8| -> u8 {
1064 ((a as u32 + b as u32 + c as u32 + d as u32 + 2) / 4) as u8
1065 };
1066 out.set_rgb(
1067 x,
1068 y,
1069 avg(r00, r10, r01, r11),
1070 avg(g00, g10, g01, g11),
1071 avg(b00, b10, b01, b11),
1072 );
1073 }
1074 }
1075 out
1076}
1077
1078fn rotation_to_steps(r: crate::info::Rotation) -> u8 {
1082 use crate::info::Rotation;
1083 match r {
1084 Rotation::None => 0,
1085 Rotation::Cw90 => 1,
1086 Rotation::Rot180 => 2,
1087 Rotation::Ccw90 => 3,
1088 }
1089}
1090
1091fn user_rotation_to_steps(r: UserRotation) -> u8 {
1093 match r {
1094 UserRotation::None => 0,
1095 UserRotation::Cw90 => 1,
1096 UserRotation::Rot180 => 2,
1097 UserRotation::Ccw90 => 3,
1098 }
1099}
1100
1101pub(crate) fn combine_rotations(
1104 info: crate::info::Rotation,
1105 user: UserRotation,
1106) -> crate::info::Rotation {
1107 use crate::info::Rotation;
1108 let steps = (rotation_to_steps(info) + user_rotation_to_steps(user)) % 4;
1109 match steps {
1110 0 => Rotation::None,
1111 1 => Rotation::Cw90,
1112 2 => Rotation::Rot180,
1113 3 => Rotation::Ccw90,
1114 _ => unreachable!(),
1115 }
1116}
1117
1118fn rotate_pixmap(src: Pixmap, rotation: crate::info::Rotation) -> Pixmap {
1122 use crate::info::Rotation;
1123 match rotation {
1124 Rotation::None => src,
1125 Rotation::Cw90 => {
1126 let w = src.height;
1127 let h = src.width;
1128 let mut out = Pixmap::white(w, h);
1129 const TILE: u32 = 32;
1135 let (sw, sh, ow) = (src.width as usize, src.height as usize, w as usize);
1136 let mut ty = 0;
1137 while ty < src.height {
1138 let mut tx = 0;
1139 while tx < src.width {
1140 let y_end = (ty + TILE).min(src.height);
1141 let x_end = (tx + TILE).min(src.width);
1142 for y in ty..y_end {
1143 let src_row = y as usize * sw * 4;
1144 let dst_col = sh - 1 - y as usize;
1145 for x in tx..x_end {
1146 let si = src_row + x as usize * 4;
1147 let di = (x as usize * ow + dst_col) * 4;
1148 out.data[di..di + 4].copy_from_slice(&src.data[si..si + 4]);
1149 }
1150 }
1151 tx += TILE;
1152 }
1153 ty += TILE;
1154 }
1155 out
1156 }
1157 Rotation::Rot180 => {
1158 let mut out = Pixmap::white(src.width, src.height);
1159 for y in 0..src.height {
1160 for x in 0..src.width {
1161 let (r, g, b) = src.get_rgb(x, y);
1162 out.set_rgb(src.width - 1 - x, src.height - 1 - y, r, g, b);
1163 }
1164 }
1165 out
1166 }
1167 Rotation::Ccw90 => {
1168 let w = src.height;
1169 let h = src.width;
1170 let mut out = Pixmap::white(w, h);
1171 const TILE: u32 = 32;
1173 let (sw, ow) = (src.width as usize, w as usize);
1174 let mut ty = 0;
1175 while ty < src.height {
1176 let mut tx = 0;
1177 while tx < src.width {
1178 let y_end = (ty + TILE).min(src.height);
1179 let x_end = (tx + TILE).min(src.width);
1180 for y in ty..y_end {
1181 let src_row = y as usize * sw * 4;
1182 for x in tx..x_end {
1183 let si = src_row + x as usize * 4;
1184 let di = ((sw - 1 - x as usize) * ow + y as usize) * 4;
1185 out.data[di..di + 4].copy_from_slice(&src.data[si..si + 4]);
1186 }
1187 }
1188 tx += TILE;
1189 }
1190 ty += TILE;
1191 }
1192 out
1193 }
1194 }
1195}
1196
1197use crate::fgbz::{FgbzPalette, PaletteColor, parse_fgbz};
1203
1204fn best_iw44_subsample(scale: f32) -> u32 {
1223 if scale <= 0.0 || !scale.is_finite() || scale >= 1.0 {
1224 return 1;
1225 }
1226 let max_sub = (1.5_f32 / scale).round() as u32;
1233 let mut s = 1u32;
1234 while s * 2 <= max_sub {
1235 s *= 2;
1236 }
1237 s.min(8)
1238}
1239
1240#[cfg(feature = "std")]
1246const TILE_SIZE: u32 = 256;
1247
1248#[cfg(feature = "std")]
1256pub(crate) const TILE_CACHE_MAX_BYTES: usize = 8 * 1024 * 1024;
1257
1258#[cfg(feature = "std")]
1265type TileKey = (u32, u32, u32, u32, u8, bool);
1266
1267#[cfg(feature = "std")]
1270struct TileEntry {
1271 w: u32,
1272 h: u32,
1273 data: Vec<u8>,
1274}
1275
1276#[cfg(feature = "std")]
1280#[derive(Default)]
1281struct TileCacheState {
1282 map: std::collections::HashMap<TileKey, std::sync::Arc<TileEntry>>,
1283 order: std::collections::VecDeque<TileKey>,
1284 bytes: usize,
1285 budget: Option<usize>,
1289 tick: u64,
1292 #[cfg(test)]
1295 hits: usize,
1296 #[cfg(test)]
1297 misses: usize,
1298 #[cfg(test)]
1299 evictions: usize,
1300}
1301
1302#[cfg(feature = "std")]
1303impl TileCacheState {
1304 fn effective_budget(&self) -> usize {
1306 self.budget.unwrap_or(TILE_CACHE_MAX_BYTES)
1307 }
1308
1309 fn evict_to_budget(&mut self) {
1311 while self.bytes > self.effective_budget() {
1312 match self.order.pop_front() {
1313 Some(old_key) => {
1314 if let Some(old) = self.map.remove(&old_key) {
1315 self.bytes = self.bytes.saturating_sub(old.data.len());
1316 #[cfg(test)]
1317 {
1318 self.evictions += 1;
1319 }
1320 }
1321 }
1322 None => break,
1323 }
1324 }
1325 }
1326}
1327
1328#[cfg(feature = "std")]
1347pub(crate) type SharedAnnotations = std::sync::Arc<(
1348 crate::annotation::Annotation,
1349 Vec<crate::annotation::MapArea>,
1350)>;
1351
1352#[cfg(feature = "std")]
1379pub(crate) struct CacheSlot<T> {
1380 inner: std::sync::RwLock<Option<Option<std::sync::Arc<T>>>>,
1381 size: fn(&T) -> usize,
1384 bytes: std::sync::atomic::AtomicUsize,
1388 tick: std::sync::atomic::AtomicU64,
1391}
1392
1393#[cfg(feature = "std")]
1394impl<T> CacheSlot<T> {
1395 pub(crate) fn new(size: fn(&T) -> usize) -> Self {
1397 Self {
1398 inner: std::sync::RwLock::new(None),
1399 size,
1400 bytes: std::sync::atomic::AtomicUsize::new(0),
1401 tick: std::sync::atomic::AtomicU64::new(0),
1402 }
1403 }
1404
1405 fn touch(&self) {
1407 let t = ACCESS_TICK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1408 self.tick.store(t, std::sync::atomic::Ordering::Relaxed);
1409 }
1410
1411 fn store(&self, w: &mut Option<Option<std::sync::Arc<T>>>, value: Option<std::sync::Arc<T>>) {
1413 let bytes = value.as_deref().map_or(0, self.size);
1414 self.bytes
1415 .store(bytes, std::sync::atomic::Ordering::Relaxed);
1416 *w = Some(value);
1417 self.touch();
1418 }
1419
1420 fn read(&self) -> std::sync::RwLockReadGuard<'_, Option<Option<std::sync::Arc<T>>>> {
1421 self.inner
1422 .read()
1423 .unwrap_or_else(std::sync::PoisonError::into_inner)
1424 }
1425
1426 fn write(&self) -> std::sync::RwLockWriteGuard<'_, Option<Option<std::sync::Arc<T>>>> {
1427 self.inner
1428 .write()
1429 .unwrap_or_else(std::sync::PoisonError::into_inner)
1430 }
1431
1432 pub(crate) fn peek(&self) -> Option<std::sync::Arc<T>> {
1434 let v = self.read().as_ref()?.clone();
1435 if v.is_some() {
1436 self.touch();
1437 }
1438 v
1439 }
1440
1441 pub(crate) fn is_computed(&self) -> bool {
1443 self.read().is_some()
1444 }
1445
1446 pub(crate) fn get_or_init(
1455 &self,
1456 init: impl FnOnce() -> Option<T>,
1457 ) -> Option<std::sync::Arc<T>> {
1458 if let Some(v) = self.read().as_ref() {
1459 self.touch();
1460 return v.clone();
1461 }
1462 let computed = init().map(std::sync::Arc::new);
1463 let mut w = self.write();
1464 if let Some(v) = w.as_ref() {
1465 self.touch();
1466 return v.clone();
1467 }
1468 self.store(&mut w, computed.clone());
1469 computed
1470 }
1471
1472 pub(crate) fn set_if_empty(&self, value: Option<T>) {
1475 let mut w = self.write();
1476 if w.is_none() {
1477 self.store(&mut w, value.map(std::sync::Arc::new));
1478 }
1479 }
1480
1481 pub(crate) fn set_if_empty_arc(&self, value: Option<std::sync::Arc<T>>) {
1485 let mut w = self.write();
1486 if w.is_none() {
1487 self.store(&mut w, value);
1488 }
1489 }
1490
1491 pub(crate) fn clear(&self) {
1494 let mut w = self.write();
1495 *w = None;
1496 self.bytes.store(0, std::sync::atomic::Ordering::Relaxed);
1497 }
1498
1499 pub(crate) fn bytes(&self) -> usize {
1502 self.bytes.load(std::sync::atomic::Ordering::Relaxed)
1503 }
1504}
1505
1506#[cfg(feature = "std")]
1510pub(crate) trait CacheLayer {
1511 fn last_used(&self) -> u64;
1513 fn resident_bytes(&self) -> usize;
1515 fn drop_cached(&self);
1518}
1519
1520#[cfg(feature = "std")]
1521impl<T> CacheLayer for CacheSlot<T> {
1522 fn last_used(&self) -> u64 {
1523 self.tick.load(std::sync::atomic::Ordering::Relaxed)
1524 }
1525 fn resident_bytes(&self) -> usize {
1526 self.bytes()
1527 }
1528 fn drop_cached(&self) {
1529 self.clear();
1530 }
1531}
1532
1533#[cfg(feature = "std")]
1534impl CacheLayer for std::sync::Mutex<TileCacheState> {
1535 fn last_used(&self) -> u64 {
1536 self.lock()
1537 .unwrap_or_else(std::sync::PoisonError::into_inner)
1538 .tick
1539 }
1540 fn resident_bytes(&self) -> usize {
1541 self.lock()
1542 .unwrap_or_else(std::sync::PoisonError::into_inner)
1543 .bytes
1544 }
1545 fn drop_cached(&self) {
1548 let mut tiles = self
1549 .lock()
1550 .unwrap_or_else(std::sync::PoisonError::into_inner);
1551 *tiles = TileCacheState {
1552 budget: tiles.budget,
1553 ..TileCacheState::default()
1554 };
1555 }
1556}
1557
1558#[cfg(feature = "std")]
1559pub(crate) struct PageLayers {
1560 bg44: CacheSlot<Iw44Image>,
1561 bg44_partial: CacheSlot<Iw44Image>,
1562 mask: CacheSlot<crate::bitmap::Bitmap>,
1563 mask_sub4: CacheSlot<crate::bitmap::Bitmap>,
1564 fg44: CacheSlot<Pixmap>,
1565 bg_rgb_s1: CacheSlot<Pixmap>,
1570 bg_rgb_s2: CacheSlot<Pixmap>,
1574 bg_rgb_s4: CacheSlot<Pixmap>,
1580 bg_rgb_subhi: CacheSlot<(u32, Arc<Pixmap>)>,
1592 mask_indexed: CacheSlot<IndexedMask>,
1593 text_layer: CacheSlot<crate::text::TextLayer>,
1600 annotations: CacheSlot<(
1601 crate::annotation::Annotation,
1602 Vec<crate::annotation::MapArea>,
1603 )>,
1604 reported: std::sync::atomic::AtomicUsize,
1611 access: std::sync::atomic::AtomicU64,
1616 tile_cache: std::sync::Mutex<TileCacheState>,
1625}
1626
1627#[cfg(feature = "std")]
1628impl Drop for PageLayers {
1629 fn drop(&mut self) {
1633 let reported = *self.reported.get_mut();
1634 if reported > 0 {
1635 crate::render_cache::adjust_resident(reported, 0);
1636 }
1637 }
1638}
1639
1640#[cfg(feature = "std")]
1644static ACCESS_TICK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1645
1646#[cfg(feature = "std")]
1652fn decode_bg44_partial(page: &DjVuPage) -> Option<Iw44Image> {
1653 let chunks = page.bg44_chunks();
1654 if chunks.is_empty() {
1655 return None;
1656 }
1657 let mut img = Iw44Image::new();
1658 if img.decode_chunk(chunks[0]).is_err() {
1659 return None;
1660 }
1661 if img.width == 0 {
1662 return None;
1663 }
1664 if !iw44_reduction_is_legal(
1666 page.width() as u32,
1667 page.height() as u32,
1668 img.width,
1669 img.height,
1670 ) {
1671 return None;
1672 }
1673 Some(img)
1674}
1675
1676#[cfg(feature = "std")]
1677impl PageLayers {
1678 pub(crate) fn new() -> Self {
1692 Self {
1693 bg44: CacheSlot::new(Iw44Image::heap_bytes),
1694 bg44_partial: CacheSlot::new(Iw44Image::heap_bytes),
1695 mask: CacheSlot::new(|b| b.data.len()),
1696 mask_sub4: CacheSlot::new(|b| b.data.len()),
1697 fg44: CacheSlot::new(|p| p.data.len()),
1698 bg_rgb_s1: CacheSlot::new(|p| p.data.len()),
1699 bg_rgb_s2: CacheSlot::new(|p| p.data.len()),
1700 bg_rgb_s4: CacheSlot::new(|p| p.data.len()),
1701 bg_rgb_subhi: CacheSlot::new(|(_, p)| p.data.len()),
1702 mask_indexed: CacheSlot::new(|(b, v)| b.data.len() + v.len() * 4),
1703 text_layer: CacheSlot::new(|t| t.text.len() + count_zones(&t.zones) * 64),
1706 annotations: CacheSlot::new(|a| a.1.len() * 96 + 64),
1707 reported: std::sync::atomic::AtomicUsize::new(0),
1708 access: std::sync::atomic::AtomicU64::new(0),
1709 tile_cache: std::sync::Mutex::new(TileCacheState::default()),
1710 }
1711 }
1712
1713 pub(crate) const LAYER_COUNT: usize = 13;
1715
1716 pub(crate) fn layers(&self) -> [&dyn CacheLayer; Self::LAYER_COUNT] {
1720 [
1721 &self.bg44,
1722 &self.bg44_partial,
1723 &self.mask,
1724 &self.mask_sub4,
1725 &self.fg44,
1726 &self.bg_rgb_s1,
1727 &self.bg_rgb_s2,
1728 &self.bg_rgb_s4,
1729 &self.bg_rgb_subhi,
1730 &self.mask_indexed,
1731 &self.text_layer,
1732 &self.annotations,
1733 &self.tile_cache,
1734 ]
1735 }
1736
1737 pub(crate) fn bump_access(&self) {
1739 let t = ACCESS_TICK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1740 self.access.store(t, std::sync::atomic::Ordering::Relaxed);
1741 }
1742
1743 pub(crate) fn access_tick(&self) -> u64 {
1745 self.access.load(std::sync::atomic::Ordering::Relaxed)
1746 }
1747
1748 pub(crate) fn cached_bytes(&self) -> usize {
1754 self.layers().iter().map(|l| l.resident_bytes()).sum()
1755 }
1756
1757 pub(crate) fn downgrade(&self) {
1774 self.bg44.clear();
1775 self.bg44_partial.clear();
1776 self.mask.clear();
1777 self.fg44.clear();
1782 self.mask_indexed.clear();
1783 self.bg_rgb_s1.clear();
1784 self.tile_cache.drop_cached();
1787 self.report_bytes();
1791 }
1792
1793 pub(crate) fn evict_shared(&self) {
1804 for layer in self.layers() {
1805 layer.drop_cached();
1806 }
1807 self.report_bytes();
1808 }
1809
1810 pub(crate) fn report_bytes(&self) -> usize {
1815 use std::sync::atomic::Ordering;
1816 let now = self.cached_bytes();
1817 let prev = self.reported.swap(now, Ordering::AcqRel);
1818 crate::render_cache::adjust_resident(prev, now)
1819 }
1820
1821 fn fill<T>(&self, slot: &CacheSlot<T>, init: impl FnOnce() -> Option<T>) -> Option<Arc<T>> {
1828 let was_computed = slot.is_computed();
1829 let value = slot.get_or_init(init);
1830 if !was_computed {
1831 let total = self.report_bytes();
1832 crate::render_cache::sweep_if_over(total, Self::layer_id(slot));
1833 }
1834 value
1835 }
1836
1837 fn layer_id<L: CacheLayer>(layer: &L) -> *const () {
1839 (layer as *const L).cast()
1840 }
1841
1842 pub(crate) fn tile_cache_bytes(&self) -> usize {
1844 self.tile_cache
1845 .lock()
1846 .unwrap_or_else(std::sync::PoisonError::into_inner)
1847 .bytes
1848 }
1849
1850 fn get_tile(&self, key: TileKey) -> Option<std::sync::Arc<TileEntry>> {
1858 let mut state = self
1859 .tile_cache
1860 .lock()
1861 .unwrap_or_else(std::sync::PoisonError::into_inner);
1862 let hit = state.map.get(&key).cloned();
1863 #[cfg(test)]
1864 {
1865 if hit.is_some() {
1866 state.hits += 1;
1867 } else {
1868 state.misses += 1;
1869 }
1870 }
1871 if hit.is_some()
1872 && let Some(pos) = state.order.iter().position(|k| *k == key)
1873 {
1874 state.order.remove(pos);
1875 state.order.push_back(key);
1876 state.tick = ACCESS_TICK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1877 }
1878 hit
1879 }
1880
1881 fn insert_tile(&self, key: TileKey, entry: std::sync::Arc<TileEntry>) {
1884 let mut state = self
1885 .tile_cache
1886 .lock()
1887 .unwrap_or_else(std::sync::PoisonError::into_inner);
1888 if state.map.contains_key(&key) {
1889 return;
1890 }
1891 state.bytes += entry.data.len();
1892 state.map.insert(key, entry);
1893 state.order.push_back(key);
1894 state.tick = ACCESS_TICK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1895 state.evict_to_budget();
1896 drop(state);
1897 let total = self.report_bytes();
1900 crate::render_cache::sweep_if_over(total, Self::layer_id(&self.tile_cache));
1901 }
1902
1903 pub(crate) fn tile_cache_budget(&self) -> usize {
1905 self.tile_cache
1906 .lock()
1907 .unwrap_or_else(std::sync::PoisonError::into_inner)
1908 .effective_budget()
1909 }
1910
1911 pub(crate) fn tile_cache_len(&self) -> usize {
1913 self.tile_cache
1914 .lock()
1915 .unwrap_or_else(std::sync::PoisonError::into_inner)
1916 .map
1917 .len()
1918 }
1919
1920 pub(crate) fn set_tile_cache_budget(&self, max_bytes: usize) {
1924 let mut state = self
1925 .tile_cache
1926 .lock()
1927 .unwrap_or_else(std::sync::PoisonError::into_inner);
1928 state.budget = Some(max_bytes);
1929 state.evict_to_budget();
1930 }
1931
1932 pub(crate) fn clear_tile_cache(&self) -> usize {
1935 let mut state = self
1936 .tile_cache
1937 .lock()
1938 .unwrap_or_else(std::sync::PoisonError::into_inner);
1939 let freed = state.bytes;
1940 state.map.clear();
1941 state.order.clear();
1942 state.bytes = 0;
1943 freed
1944 }
1945
1946 pub(crate) fn remove_tiles_intersecting(
1953 &self,
1954 rect: RenderRect,
1955 canvas_w: u32,
1956 canvas_h: u32,
1957 ) -> usize {
1958 if canvas_w == 0 || canvas_h == 0 || rect.width == 0 || rect.height == 0 {
1959 return 0;
1960 }
1961 let mut state = self
1962 .tile_cache
1963 .lock()
1964 .unwrap_or_else(std::sync::PoisonError::into_inner);
1965 let mut freed = 0usize;
1966 let doomed: Vec<TileKey> = state
1967 .map
1968 .iter()
1969 .filter(|((fw, fh, tx, ty, _, _), entry)| {
1970 let x0 = u64::from(rect.x) * u64::from(*fw) / u64::from(canvas_w);
1973 let x1 = ((u64::from(rect.x) + u64::from(rect.width)) * u64::from(*fw))
1974 .div_ceil(u64::from(canvas_w));
1975 let y0 = u64::from(rect.y) * u64::from(*fh) / u64::from(canvas_h);
1976 let y1 = ((u64::from(rect.y) + u64::from(rect.height)) * u64::from(*fh))
1977 .div_ceil(u64::from(canvas_h));
1978 let (tx0, ty0) = (u64::from(*tx), u64::from(*ty));
1979 let (tx1, ty1) = (tx0 + u64::from(entry.w), ty0 + u64::from(entry.h));
1980 tx0 < x1 && tx1 > x0 && ty0 < y1 && ty1 > y0
1981 })
1982 .map(|(k, _)| *k)
1983 .collect();
1984 for key in doomed {
1985 if let Some(old) = state.map.remove(&key) {
1986 freed += old.data.len();
1987 }
1988 if let Some(pos) = state.order.iter().position(|k| *k == key) {
1989 state.order.remove(pos);
1990 }
1991 }
1992 state.bytes = state.bytes.saturating_sub(freed);
1993 freed
1994 }
1995
1996 #[cfg(test)]
1998 fn tile_cache_stats(&self) -> (usize, usize, usize) {
1999 let s = self
2000 .tile_cache
2001 .lock()
2002 .unwrap_or_else(std::sync::PoisonError::into_inner);
2003 (s.hits, s.misses, s.evictions)
2004 }
2005
2006 pub(crate) fn bg44(&self, page: &DjVuPage) -> Option<std::sync::Arc<Iw44Image>> {
2011 self.fill(&self.bg44, || {
2012 let chunks = page.bg44_chunks();
2013 if chunks.is_empty() {
2014 return None;
2015 }
2016 let mut img;
2025 let mut start = 0;
2026 if let Some(partial) = self.bg44_partial.peek() {
2027 img = (*partial).clone();
2028 start = 1;
2029 } else {
2030 img = Iw44Image::new();
2031 }
2032 for chunk_data in &chunks[start.min(chunks.len())..] {
2033 #[cfg(test)]
2034 count_bg44_chunk_decode();
2035 if img.decode_chunk(chunk_data).is_err() {
2036 return None;
2037 }
2038 }
2039 if img.width == 0 {
2040 return None;
2041 }
2042 if !iw44_reduction_is_legal(
2049 page.width() as u32,
2050 page.height() as u32,
2051 img.width,
2052 img.height,
2053 ) {
2054 return None;
2055 }
2056 Some(img)
2057 })
2058 }
2059
2060 pub(crate) fn bg44_partial(&self, page: &DjVuPage) -> Option<std::sync::Arc<Iw44Image>> {
2064 self.fill(&self.bg44_partial, || decode_bg44_partial(page))
2065 }
2066
2067 pub(crate) fn bg44_partial_cached(&self) -> Option<std::sync::Arc<Iw44Image>> {
2080 self.bg44_partial.peek()
2081 }
2082
2083 pub(crate) fn bg_rgb_subhi(&self, subsample: u32) -> Option<Arc<Pixmap>> {
2086 let slot = self.bg_rgb_subhi.peek()?;
2087 (slot.0 == subsample).then(|| slot.1.clone())
2088 }
2089
2090 pub(crate) fn store_bg_rgb_subhi(&self, subsample: u32, px: Arc<Pixmap>) {
2094 self.bg_rgb_subhi.set_if_empty(Some((subsample, px)));
2095 let total = self.report_bytes();
2096 crate::render_cache::sweep_if_over(total, Self::layer_id(&self.bg_rgb_subhi));
2097 }
2098
2099 pub(crate) fn mask(&self, page: &DjVuPage) -> Option<std::sync::Arc<crate::bitmap::Bitmap>> {
2102 self.fill(&self.mask, || {
2103 #[cfg(test)]
2104 count_jb2_mask_decode();
2105 page.extract_mask().ok().flatten()
2106 })
2107 }
2108
2109 pub(crate) fn mask_sub4(
2124 &self,
2125 page: &DjVuPage,
2126 ) -> Option<std::sync::Arc<crate::bitmap::Bitmap>> {
2127 self.fill(&self.mask_sub4, || {
2128 if let Some(full) = self.mask.peek() {
2129 return Some(downsample_mask_4x(&full));
2130 }
2131 page.extract_mask_sub4().ok().flatten()
2132 })
2133 }
2134
2135 #[cfg(test)]
2144 pub(crate) fn mask_sub4_cached(&self) -> Option<std::sync::Arc<crate::bitmap::Bitmap>> {
2145 self.mask_sub4.peek()
2146 }
2147
2148 pub(crate) fn fg44(&self, page: &DjVuPage) -> Option<std::sync::Arc<Pixmap>> {
2151 self.fill(&self.fg44, || page.extract_foreground().ok().flatten())
2152 }
2153
2154 pub(crate) fn bg_rgb_s1(&self, page: &DjVuPage) -> Option<std::sync::Arc<Pixmap>> {
2166 self.fill(&self.bg_rgb_s1, || {
2167 let img = self.bg44(page)?;
2168 if img.rgb_band_rows().is_some() {
2169 return None;
2170 }
2171 img.to_rgb_subsample(1).ok()
2172 })
2173 }
2174
2175 pub(crate) fn bg_rgb_s2(&self, page: &DjVuPage) -> Option<std::sync::Arc<Pixmap>> {
2184 self.fill(&self.bg_rgb_s2, || {
2185 let img = self.bg44(page)?;
2186 img.to_rgb_subsample(2).ok()
2187 })
2188 }
2189
2190 pub(crate) fn bg_rgb_s4(&self, page: &DjVuPage) -> Option<std::sync::Arc<Pixmap>> {
2201 self.fill(&self.bg_rgb_s4, || {
2202 let img = self.bg44_partial(page)?;
2203 img.to_rgb_subsample(4).ok()
2204 })
2205 }
2206
2207 pub(crate) fn text_layer_cached(
2218 &self,
2219 parse: impl FnOnce() -> Result<
2220 Option<std::sync::Arc<crate::text::TextLayer>>,
2221 crate::djvu_document::DocError,
2222 >,
2223 ) -> Result<Option<std::sync::Arc<crate::text::TextLayer>>, crate::djvu_document::DocError>
2224 {
2225 if self.text_layer.is_computed() {
2226 return Ok(self.text_layer.peek());
2227 }
2228 let v = parse()?;
2229 self.text_layer.set_if_empty_arc(v.clone());
2230 self.report_bytes();
2231 Ok(v)
2232 }
2233
2234 pub(crate) fn annotations_cached(
2237 &self,
2238 parse: impl FnOnce() -> Result<Option<SharedAnnotations>, crate::djvu_document::DocError>,
2239 ) -> Result<Option<SharedAnnotations>, crate::djvu_document::DocError> {
2240 if self.annotations.is_computed() {
2241 return Ok(self.annotations.peek());
2242 }
2243 let v = parse()?;
2244 self.annotations.set_if_empty_arc(v.clone());
2245 self.report_bytes();
2246 Ok(v)
2247 }
2248
2249 pub(crate) fn mask_indexed(&self, page: &DjVuPage) -> Option<Arc<IndexedMask>> {
2250 self.fill(&self.mask_indexed, || {
2251 #[cfg(test)]
2252 count_jb2_mask_decode();
2253 page.extract_mask_indexed()
2254 .ok()
2255 .flatten()
2256 .map(|(bm, blits)| (Arc::new(bm), Arc::new(blits)))
2257 })
2258 }
2259}
2260
2261#[cfg(feature = "std")]
2263fn count_zones(zones: &[crate::text::TextZone]) -> usize {
2264 zones
2265 .iter()
2266 .map(|z| 1 + count_zones(&z.children))
2267 .sum::<usize>()
2268}
2269
2270#[cfg(feature = "std")]
2276pub(crate) fn downsample_mask_4x(src: &crate::bitmap::Bitmap) -> crate::bitmap::Bitmap {
2277 let out_w = src.width.div_ceil(4);
2278 let out_h = src.height.div_ceil(4);
2279 let mut out = crate::bitmap::Bitmap::new(out_w, out_h);
2280 for oy in 0..out_h {
2281 for ox in 0..out_w {
2282 'outer: for dy in 0..4u32 {
2283 for dx in 0..4u32 {
2284 let sx = ox * 4 + dx;
2285 let sy = oy * 4 + dy;
2286 if sx < src.width && sy < src.height && src.get(sx, sy) {
2287 out.set(ox, oy, true);
2288 break 'outer;
2289 }
2290 }
2291 }
2292 }
2293 }
2294 out
2295}
2296
2297#[cfg(feature = "std")]
2302fn page_mask_sub4(page: &DjVuPage) -> Option<std::sync::Arc<crate::bitmap::Bitmap>> {
2303 page.render_layers().mask_sub4(page)
2304}
2305
2306fn iw44_reduction_is_legal(page_w: u32, page_h: u32, plane_w: u32, plane_h: u32) -> bool {
2323 if page_w == 0 || page_h == 0 || plane_w == 0 || plane_h == 0 {
2324 return false;
2325 }
2326 (1..=12u32).any(|red| page_w.div_ceil(red) == plane_w && page_h.div_ceil(red) == plane_h)
2327}
2328
2329fn decode_background_chunks(
2340 page: &DjVuPage,
2341 max_chunks: usize,
2342 subsample: u32,
2343) -> Result<Background, RenderError> {
2344 if max_chunks == usize::MAX {
2351 let bg44_chunks = page.bg44_chunks();
2352 if !bg44_chunks.is_empty() {
2353 if subsample == 1 {
2354 let img = page
2357 .decoded_bg44()
2358 .ok_or(RenderError::Iw44(crate::Iw44Error::Invalid))?;
2359 if let Some(band_rows) = img.rgb_band_rows() {
2363 return Ok(Background::Banded {
2364 image: img,
2365 band_rows,
2366 });
2367 }
2368 return Ok(Background::from(page.decoded_bg_rgb_s1()));
2369 }
2370 if subsample == 2 {
2371 if let Some(cached) = page.decoded_bg_rgb_s2() {
2379 return Ok(Background::Whole(cached));
2380 }
2381 let _ = page
2385 .decoded_bg44()
2386 .ok_or(RenderError::Iw44(crate::Iw44Error::Invalid))?;
2387 return Ok(Background::from(page.decoded_bg_rgb_s2()));
2388 }
2389 if subsample == 4 {
2390 if let Some(cached) = page.decoded_bg_rgb_s4() {
2393 return Ok(Background::Whole(cached));
2394 }
2395 let _ = page
2399 .decoded_bg44_partial()
2400 .ok_or(RenderError::Iw44(crate::Iw44Error::Invalid))?;
2401 return Ok(Background::from(page.decoded_bg_rgb_s4()));
2402 }
2403 #[cfg(feature = "std")]
2410 if let Some(cached) = page.cached_bg_rgb_subhi(subsample) {
2411 return Ok(Background::Whole(cached));
2412 }
2413 let img = if subsample >= 4 {
2414 #[cfg(feature = "std")]
2415 {
2416 match page.cached_bg44_partial() {
2421 Some(cached) => Some(cached),
2422 None => decode_bg44_partial(page).map(Arc::new),
2423 }
2424 }
2425 #[cfg(not(feature = "std"))]
2426 {
2427 page.decoded_bg44_partial()
2428 }
2429 } else {
2430 page.decoded_bg44()
2431 };
2432 let img = img.ok_or(RenderError::Iw44(crate::Iw44Error::Invalid))?;
2433 let rgb = Arc::new(img.to_rgb_subsample(subsample)?);
2434 #[cfg(feature = "std")]
2435 if subsample > 4 {
2436 page.store_bg_rgb_subhi(subsample, rgb.clone());
2437 }
2438 return Ok(Background::Whole(rgb));
2439 }
2440 } else {
2442 let bg44_chunks = page.bg44_chunks();
2443 if !bg44_chunks.is_empty() {
2444 let mut img = Iw44Image::new();
2445 for chunk_data in bg44_chunks.iter().take(max_chunks) {
2446 #[cfg(test)]
2447 count_bg44_chunk_decode();
2448 img.decode_chunk(chunk_data)?;
2449 }
2450 if !iw44_reduction_is_legal(
2452 page.width() as u32,
2453 page.height() as u32,
2454 img.width,
2455 img.height,
2456 ) {
2457 return Err(RenderError::Iw44(crate::Iw44Error::Invalid));
2458 }
2459 return Background::from_iw44(img, subsample);
2460 }
2461 }
2462
2463 #[cfg(feature = "std")]
2465 if let Some(pm) = decode_bgjp(page)? {
2466 return Ok(Background::Whole(Arc::new(pm)));
2467 }
2468
2469 Ok(Background::None)
2470}
2471
2472fn decode_background_chunks_permissive(
2478 page: &DjVuPage,
2479 max_chunks: usize,
2480 subsample: u32,
2481) -> Background {
2482 let bg44_chunks = page.bg44_chunks();
2483 if !bg44_chunks.is_empty() {
2484 let mut img = Iw44Image::new();
2485 let wanted = bg44_chunks.len().min(max_chunks);
2486 for (decoded, chunk_data) in bg44_chunks.iter().take(max_chunks).enumerate() {
2489 if img.decode_chunk(chunk_data).is_err() {
2490 record_recovery(RecoveredLayer::Background, {
2492 #[cfg(feature = "std")]
2493 {
2494 format!(
2495 "BG44 truncated at chunk {} of {wanted}; \
2496 kept {decoded} decoded chunk(s)",
2497 decoded + 1
2498 )
2499 }
2500 #[cfg(not(feature = "std"))]
2501 {
2502 let _ = (decoded, wanted);
2503 ""
2504 }
2505 });
2506 break;
2507 }
2508 }
2509 return Background::from_iw44(img, subsample).unwrap_or(Background::None);
2510 }
2511
2512 #[cfg(feature = "std")]
2514 {
2515 Background::from(decode_bgjp(page).ok().flatten().map(Arc::new))
2516 }
2517 #[cfg(not(feature = "std"))]
2518 Background::None
2519}
2520
2521fn decode_mask(page: &DjVuPage) -> Result<Option<Arc<crate::bitmap::Bitmap>>, RenderError> {
2528 match page.decoded_mask() {
2529 Some(bm) => Ok(Some(bm)),
2530 None if page.find_chunk(b"Sjbz").is_some() => {
2531 page.extract_mask()
2533 .map_err(RenderError::from)
2534 .map(|opt| opt.map(Arc::new))
2535 }
2536 None => Ok(None),
2537 }
2538}
2539
2540pub(crate) type IndexedMask = (Arc<crate::bitmap::Bitmap>, Arc<Vec<i32>>);
2551
2552fn decode_mask_indexed(page: &DjVuPage) -> Result<Option<IndexedMask>, RenderError> {
2553 match page.decoded_mask_indexed() {
2554 Some(pair) => Ok(Some((pair.0.clone(), pair.1.clone()))),
2555 None if page.find_chunk(b"Sjbz").is_some() || page.find_chunk(b"Smmr").is_some() => page
2559 .extract_mask_indexed()
2560 .map_err(RenderError::from)
2561 .map(|opt| opt.map(|(bm, blit)| (Arc::new(bm), Arc::new(blit)))),
2562 None => Ok(None),
2563 }
2564}
2565
2566fn decode_fg_palette_full(page: &DjVuPage) -> Result<Option<FgbzPalette>, RenderError> {
2568 let fgbz = match page.find_chunk(b"FGbz") {
2569 Some(data) => data,
2570 None => return Ok(None),
2571 };
2572
2573 let pal = parse_fgbz(fgbz)?;
2574 if pal.colors.is_empty() {
2575 return Ok(None);
2576 }
2577 Ok(Some(pal))
2578}
2579
2580fn decode_fg44(page: &DjVuPage) -> Result<Option<Arc<Pixmap>>, RenderError> {
2585 let fg44_chunks = page.fg44_chunks();
2586 if !fg44_chunks.is_empty() {
2587 return match page.decoded_fg44() {
2588 Some(pm) => Ok(Some(pm)),
2589 None => page
2598 .extract_foreground()
2599 .map_err(RenderError::from)
2600 .map(|opt| opt.map(Arc::new)),
2601 };
2602 }
2603
2604 #[cfg(feature = "std")]
2606 if let Some(pm) = decode_fgjp(page)? {
2607 return Ok(Some(Arc::new(pm)));
2608 }
2609
2610 Ok(None)
2611}
2612
2613struct DecodedLayers {
2617 bg: Background,
2618 fg_palette: Option<FgbzPalette>,
2619 mask: Option<Arc<crate::bitmap::Bitmap>>,
2620 blit_map: Option<Arc<Vec<i32>>>,
2621 fg44: Option<Arc<Pixmap>>,
2622}
2623
2624enum Background {
2626 None,
2628 Whole(Arc<Pixmap>),
2630 Banded {
2635 image: Arc<Iw44Image>,
2636 band_rows: u32,
2637 },
2638}
2639
2640impl Background {
2641 fn whole(&self) -> Option<&Pixmap> {
2643 match self {
2644 Background::Whole(px) => Some(px),
2645 _ => None,
2646 }
2647 }
2648
2649 fn is_some(&self) -> bool {
2651 !matches!(self, Background::None)
2652 }
2653
2654 fn from_iw44(img: Iw44Image, subsample: u32) -> Result<Self, RenderError> {
2658 Self::from_shared_iw44(&Arc::new(img), subsample)
2659 }
2660
2661 fn from_shared_iw44(img: &Arc<Iw44Image>, subsample: u32) -> Result<Self, RenderError> {
2664 if subsample == 1
2665 && let Some(band_rows) = img.rgb_band_rows()
2666 {
2667 return Ok(Background::Banded {
2668 image: img.clone(),
2669 band_rows,
2670 });
2671 }
2672 Ok(Background::Whole(Arc::new(
2673 img.to_rgb_subsample(subsample)?,
2674 )))
2675 }
2676}
2677
2678impl From<Option<Arc<Pixmap>>> for Background {
2679 fn from(px: Option<Arc<Pixmap>>) -> Self {
2680 px.map_or(Background::None, Background::Whole)
2681 }
2682}
2683
2684#[derive(Clone, Copy)]
2691struct PlaneView<'a> {
2692 px: &'a Pixmap,
2693 height: u32,
2695 row0: u32,
2697}
2698
2699impl<'a> PlaneView<'a> {
2700 fn whole(px: &'a Pixmap) -> Self {
2701 PlaneView {
2702 px,
2703 height: px.height,
2704 row0: 0,
2705 }
2706 }
2707
2708 fn band(px: &'a Pixmap, height: u32, row0: u32) -> Self {
2711 PlaneView { px, height, row0 }
2712 }
2713
2714 #[inline]
2715 fn width(&self) -> u32 {
2716 self.px.width
2717 }
2718
2719 #[inline]
2720 fn height(&self) -> u32 {
2721 self.height
2722 }
2723
2724 #[inline]
2729 fn row(&self, y: u32) -> &'a [u8] {
2730 let i = y.wrapping_sub(self.row0) as usize;
2731 debug_assert!(
2732 y >= self.row0 && i < self.px.height as usize,
2733 "plane row {y} is outside the band {}..{}",
2734 self.row0,
2735 self.row0 + self.px.height
2736 );
2737 let stride = self.px.width as usize * 4;
2738 i.checked_mul(stride)
2739 .and_then(|off| self.px.data.get(off..off + stride))
2740 .unwrap_or(&[])
2741 }
2742}
2743
2744fn decode_layers(
2758 page: &DjVuPage,
2759 opts: &RenderOptions,
2760 bg_subsample: u32,
2761 bg_chunk_limit: usize,
2762) -> Result<DecodedLayers, RenderError> {
2763 #[cfg(feature = "std")]
2785 if bg_chunk_limit == usize::MAX
2786 && bg_subsample >= 4
2787 && opts.bold == 0
2788 && page.find_chunk(b"FGbz").is_none()
2789 && page.render_layers().mask_sub4(page).is_some()
2790 {
2791 let (bg, fg44) = if opts.permissive {
2792 (
2793 decode_background_chunks_permissive(page, bg_chunk_limit, bg_subsample),
2794 permissive_layer(decode_fg44(page), RecoveredLayer::Foreground),
2795 )
2796 } else {
2797 (
2798 decode_background_chunks(page, bg_chunk_limit, bg_subsample)?,
2799 decode_fg44(page)?,
2800 )
2801 };
2802 return Ok(DecodedLayers {
2803 bg,
2804 fg_palette: None,
2805 mask: None,
2806 blit_map: None,
2807 fg44,
2808 });
2809 }
2810
2811 let bg;
2812 let fg_palette;
2813 let mask;
2814 let blit_map;
2815 let fg44;
2816
2817 if opts.permissive {
2818 bg = decode_background_chunks_permissive(page, bg_chunk_limit, bg_subsample);
2819 fg_palette = permissive_layer(
2820 decode_fg_palette_full(page),
2821 RecoveredLayer::ForegroundPalette,
2822 );
2823 let indexed = if fg_palette.is_some() {
2824 permissive_layer(decode_mask_indexed(page), RecoveredLayer::Mask)
2825 } else {
2826 None
2827 };
2828 if let Some((bm, bm_map)) = indexed {
2829 mask = Some(bm);
2830 blit_map = Some(bm_map);
2831 } else {
2832 mask = permissive_layer(decode_mask(page), RecoveredLayer::Mask);
2833 blit_map = None;
2834 }
2835 fg44 = permissive_layer(decode_fg44(page), RecoveredLayer::Foreground);
2836 } else {
2837 #[cfg(feature = "parallel")]
2843 let (bg_res, fg_res) = rayon::join(
2844 || decode_background_chunks(page, bg_chunk_limit, bg_subsample),
2845 || decode_foreground_strict(page),
2846 );
2847 #[cfg(not(feature = "parallel"))]
2848 let (bg_res, fg_res) = (
2849 decode_background_chunks(page, bg_chunk_limit, bg_subsample),
2850 decode_foreground_strict(page),
2851 );
2852 bg = bg_res?;
2853 let fg = fg_res?;
2854 fg_palette = fg.fg_palette;
2855 mask = fg.mask;
2856 blit_map = fg.blit_map;
2857 fg44 = fg.fg44;
2858 }
2859
2860 let mask = if opts.bold > 0 {
2861 mask.map(|m| Arc::new(Arc::unwrap_or_clone(m).dilate_n(opts.bold as u32)))
2862 } else {
2863 mask
2864 };
2865
2866 Ok(DecodedLayers {
2867 bg,
2868 fg_palette,
2869 mask,
2870 blit_map,
2871 fg44,
2872 })
2873}
2874
2875struct ForegroundLayers {
2878 fg_palette: Option<FgbzPalette>,
2879 mask: Option<Arc<crate::bitmap::Bitmap>>,
2880 blit_map: Option<Arc<Vec<i32>>>,
2881 fg44: Option<Arc<Pixmap>>,
2882}
2883
2884fn decode_foreground_strict(page: &DjVuPage) -> Result<ForegroundLayers, RenderError> {
2893 let fg_palette = decode_fg_palette_full(page)?;
2894 let (mask, blit_map) = if fg_palette.is_some() {
2895 match decode_mask_indexed(page)? {
2896 Some((bm, bm_map)) => (Some(bm), Some(bm_map)),
2897 None => (None, None),
2898 }
2899 } else {
2900 (decode_mask(page)?, None)
2901 };
2902 let fg44 = decode_fg44(page)?;
2903 Ok(ForegroundLayers {
2904 fg_palette,
2905 mask,
2906 blit_map,
2907 fg44,
2908 })
2909}
2910
2911#[cfg(feature = "std")]
2916fn decode_bgjp(page: &DjVuPage) -> Result<Option<Pixmap>, RenderError> {
2917 let data = match page.find_chunk(b"BGjp") {
2918 Some(d) => d,
2919 None => return Ok(None),
2920 };
2921 Ok(Some(decode_jpeg_to_pixmap(data)?))
2922}
2923
2924#[cfg(feature = "std")]
2929fn decode_fgjp(page: &DjVuPage) -> Result<Option<Pixmap>, RenderError> {
2930 let data = match page.find_chunk(b"FGjp") {
2931 Some(d) => d,
2932 None => return Ok(None),
2933 };
2934 Ok(Some(decode_jpeg_to_pixmap(data)?))
2935}
2936
2937#[cfg(feature = "std")]
2942fn decode_jpeg_to_pixmap(data: &[u8]) -> Result<Pixmap, RenderError> {
2943 use zune_jpeg::JpegDecoder;
2944 use zune_jpeg::zune_core::bytestream::ZCursor;
2945
2946 let cursor = ZCursor::new(data);
2947 let mut decoder = JpegDecoder::new(cursor);
2948 decoder
2949 .decode_headers()
2950 .map_err(|e| RenderError::Jpeg(format!("{e:?}")))?;
2951 let info = decoder
2952 .info()
2953 .ok_or_else(|| RenderError::Jpeg("missing image info after decode_headers".to_owned()))?;
2954 let w = info.width as usize;
2955 let h = info.height as usize;
2956 let rgb = decoder
2957 .decode()
2958 .map_err(|e| RenderError::Jpeg(format!("{e:?}")))?;
2959
2960 let pixel_count = w * h;
2962 let rgb = if rgb.len() >= pixel_count * 3 {
2963 rgb
2964 } else {
2965 let mut padded = rgb;
2967 padded.resize(pixel_count * 3, 0);
2968 padded
2969 };
2970 let mut rgba = vec![0u8; pixel_count * 4];
2971 rgb_to_rgba(&rgb[..pixel_count * 3], &mut rgba);
2972 Ok(Pixmap {
2973 width: w as u32,
2974 height: h as u32,
2975 data: rgba,
2976 })
2977}
2978
2979#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2985pub struct RenderRect {
2986 pub x: u32,
2988 pub y: u32,
2990 pub width: u32,
2992 pub height: u32,
2994}
2995
2996#[derive(Clone, Copy)]
3003struct CompositeContext<'a> {
3004 opts: &'a RenderOptions,
3005 page_w: u32,
3006 page_h: u32,
3007 bg: Option<PlaneView<'a>>,
3008 bg_x_q24: u64,
3013 bg_y_q24: u64,
3014 mask: Option<&'a crate::bitmap::Bitmap>,
3015 mask_shift: u32,
3018 fg_palette: Option<&'a FgbzPalette>,
3019 blit_map: Option<&'a [i32]>,
3021 fg44: Option<&'a Pixmap>,
3022 fg_x_q24: u64,
3027 fg_y_q24: u64,
3028 gamma_lut: &'a [u8; 256],
3029 gamma_is_identity: bool,
3031 offset_x: u32,
3033 offset_y: u32,
3035 out_w: u32,
3037 out_h: u32,
3039}
3040
3041#[derive(Clone, Copy)]
3042struct AreaAvgX {
3043 fx: u32,
3044 bg_x0: u32,
3045 bg_x1: u32,
3046}
3047
3048fn area_range(limit: u32, f: u32, step: u32) -> (u32, u32) {
3052 (
3053 (f >> FRACBITS).min(limit.saturating_sub(1)),
3054 ((f + step) >> FRACBITS).min(limit),
3055 )
3056}
3057
3058fn precompute_area_avg_x(
3059 ctx: &CompositeContext<'_>,
3060 fx_step: u32,
3061 bg_fx_step: u32,
3062) -> Vec<AreaAvgX> {
3063 let mut xs = Vec::with_capacity(ctx.out_w as usize);
3064 let bg_w = ctx.bg.map_or(0, |bg| bg.width());
3065 for ox in 0..ctx.out_w {
3066 let fx = (ox + ctx.offset_x) * fx_step;
3067 let bg_fx = ((fx as u64 * ctx.bg_x_q24) >> 24) as u32;
3068 let (bg_x0, bg_x1) = area_range(bg_w, bg_fx, bg_fx_step);
3069 xs.push(AreaAvgX { fx, bg_x0, bg_x1 });
3070 }
3071 xs
3072}
3073
3074#[derive(Clone, Copy)]
3079struct BilinearX {
3080 x0: u32,
3081 x1: u32,
3082 tx: u32,
3083}
3084
3085fn precompute_bilinear_x(ctx: &CompositeContext<'_>, fx_step: u32) -> Option<Vec<BilinearX>> {
3089 let bg = ctx.bg?;
3090 let clamp_w = bg.width().saturating_sub(1);
3091 let bg_fx_step_q: u64 = fx_step as u64 * ctx.bg_x_q24;
3092 let mut bg_fx_q: u64 = (ctx.offset_x as u64 * fx_step as u64 + FRAC as u64 / 2) * ctx.bg_x_q24;
3093 let mut xs = Vec::with_capacity(ctx.out_w as usize);
3094 for _ in 0..ctx.out_w {
3095 let bg_fx = ((bg_fx_q >> 24) as u32).saturating_sub(FRAC / 2);
3096 let x0 = (bg_fx >> FRACBITS).min(clamp_w);
3097 xs.push(BilinearX {
3098 x0,
3099 x1: (x0 + 1).min(clamp_w),
3100 tx: bg_fx & FRAC_MASK,
3101 });
3102 bg_fx_q = bg_fx_q.wrapping_add(bg_fx_step_q);
3103 }
3104 Some(xs)
3105}
3106
3107impl<'a> CompositeContext<'a> {
3108 #[allow(clippy::too_many_arguments)]
3122 fn from_layers(
3123 page: &DjVuPage,
3124 opts: &'a RenderOptions,
3125 bg: Option<PlaneView<'a>>,
3126 mask: Option<&'a crate::bitmap::Bitmap>,
3127 mask_shift: u32,
3128 fg_palette: Option<&'a FgbzPalette>,
3129 blit_map: Option<&'a [i32]>,
3130 fg44: Option<&'a Pixmap>,
3131 gamma_lut: &'a [u8; 256],
3132 offset: (u32, u32),
3133 out: (u32, u32),
3134 ) -> Self {
3135 let page_w = page.width() as u32;
3136 let page_h = page.height() as u32;
3137 let (fg_x_q24, fg_y_q24) = fg_q24(fg44, page_w, page_h);
3138 let (bg_x_q24, bg_y_q24) = bg_q24(bg.map(|b| (b.width(), b.height())), page_w, page_h);
3139 CompositeContext {
3140 opts,
3141 page_w,
3142 page_h,
3143 bg,
3144 bg_x_q24,
3145 bg_y_q24,
3146 mask,
3147 mask_shift,
3148 fg_palette,
3149 blit_map,
3150 fg44,
3151 fg_x_q24,
3152 fg_y_q24,
3153 gamma_lut,
3154 gamma_is_identity: gamma_lut.iter().enumerate().all(|(i, &v)| v == i as u8),
3155 offset_x: offset.0,
3156 offset_y: offset.1,
3157 out_w: out.0,
3158 out_h: out.1,
3159 }
3160 }
3161
3162 fn with_bg<'b>(&self, bg: Option<PlaneView<'b>>) -> CompositeContext<'b>
3166 where
3167 'a: 'b,
3168 {
3169 let (bg_x_q24, bg_y_q24) = bg_q24(
3170 bg.map(|b| (b.width(), b.height())),
3171 self.page_w,
3172 self.page_h,
3173 );
3174 CompositeContext {
3175 bg,
3176 bg_x_q24,
3177 bg_y_q24,
3178 ..*self
3179 }
3180 }
3181}
3182
3183enum MaskPlane<'a> {
3199 Borrowed(Option<&'a crate::bitmap::Bitmap>),
3200 #[cfg(feature = "std")]
3201 Shared(Option<std::sync::Arc<crate::bitmap::Bitmap>>),
3202}
3203
3204impl MaskPlane<'_> {
3205 #[inline]
3206 fn get(&self) -> Option<&crate::bitmap::Bitmap> {
3207 match self {
3208 MaskPlane::Borrowed(m) => *m,
3209 #[cfg(feature = "std")]
3210 MaskPlane::Shared(m) => m.as_deref(),
3211 }
3212 }
3213}
3214
3215#[cfg(feature = "std")]
3216fn resolve_sub4_mask<'a>(
3217 page: &'a DjVuPage,
3218 bg_subsample: u32,
3219 opts: &RenderOptions,
3220 full_mask: Option<&'a crate::bitmap::Bitmap>,
3221 fg_palette: Option<&FgbzPalette>,
3222) -> (MaskPlane<'a>, u32) {
3223 if bg_subsample >= 4 && opts.bold == 0 && fg_palette.is_none() {
3224 (MaskPlane::Shared(page_mask_sub4(page)), 2)
3225 } else {
3226 (MaskPlane::Borrowed(full_mask), 0)
3227 }
3228}
3229
3230#[cfg(not(feature = "std"))]
3231fn resolve_sub4_mask<'a>(
3232 _page: &'a DjVuPage,
3233 _bg_subsample: u32,
3234 _opts: &RenderOptions,
3235 full_mask: Option<&'a crate::bitmap::Bitmap>,
3236 _fg_palette: Option<&FgbzPalette>,
3237) -> (MaskPlane<'a>, u32) {
3238 (MaskPlane::Borrowed(full_mask), 0)
3239}
3240
3241#[inline]
3247fn lookup_palette_color(
3248 pal: &FgbzPalette,
3249 blit_map: Option<&[i32]>,
3250 mask: Option<&crate::bitmap::Bitmap>,
3251 px: u32,
3252 py: u32,
3253) -> PaletteColor {
3254 if let Some(bm) = blit_map
3255 && let Some(m) = mask
3256 {
3257 let mi = py as usize * m.width as usize + px as usize;
3258 if mi < bm.len() {
3259 let blit_idx = bm[mi];
3260 if blit_idx >= 0 {
3261 if !pal.indices.is_empty() {
3262 let bi = blit_idx as usize;
3264 if bi < pal.indices.len() {
3265 let ci = pal.indices[bi] as usize;
3266 if ci < pal.colors.len() {
3267 return pal.colors[ci];
3268 }
3269 }
3270 } else {
3271 let ci = blit_idx as usize;
3273 if ci < pal.colors.len() {
3274 return pal.colors[ci];
3275 }
3276 }
3277 }
3278 }
3279 }
3280 pal.colors.first().copied().unwrap_or_default()
3282}
3283
3284fn bg_rows_needed(
3293 (page_w, page_h): (u32, u32),
3294 (full_w, full_h): (u32, u32),
3295 plane: (u32, u32),
3296 rows: core::ops::Range<u32>,
3297) -> (u32, u32) {
3298 let plane_h = plane.1;
3299 if rows.is_empty() || plane_h == 0 {
3300 return (0, 0);
3301 }
3302 let fx_step = ((page_w as u64 * FRAC as u64) / full_w.max(1) as u64) as u32;
3303 let fy_step = ((page_h as u64 * FRAC as u64) / full_h.max(1) as u64) as u32;
3304 let (_, bg_y_q24) = bg_q24(Some(plane), page_w, page_h);
3305 let first = rows.start;
3306 let last = rows.end - 1;
3307 if fx_step > FRAC || fy_step > FRAC {
3308 let bg_fy_step = ((fy_step as u64 * bg_y_q24) >> 24) as u32;
3309 let row_of = |oy: u32| (((oy * fy_step) as u64 * bg_y_q24) >> 24) as u32;
3310 let (lo, _) = area_range(plane_h, row_of(first), bg_fy_step);
3311 let (y0, y1) = area_range(plane_h, row_of(last), bg_fy_step);
3312 (lo, y1.max(y0 + 1))
3313 } else {
3314 let clamp_h = plane_h - 1;
3315 let row_of =
3316 |oy: u32| (map_plane_center_frac(oy * fy_step, bg_y_q24) >> FRACBITS).min(clamp_h);
3317 let lo = row_of(first);
3318 let hi = (row_of(last) + 1).min(clamp_h) + 1;
3319 (lo, hi)
3320 }
3321}
3322
3323fn bg_band_out_rows(
3328 page: (u32, u32),
3329 full: (u32, u32),
3330 plane: (u32, u32),
3331 offset_y: u32,
3332 out_h: u32,
3333 band_rows: u32,
3334) -> u32 {
3335 let fy_step = ((page.1 as u64 * FRAC as u64) / full.1.max(1) as u64) as u32;
3336 let (_, bg_y_q24) = bg_q24(Some(plane), page.0, page.1);
3337 let per_out = (fy_step as u64 * bg_y_q24).max(1);
3340 let rows = ((band_rows.saturating_sub(4) as u64) << (FRACBITS + 24)) / per_out;
3341 let mut rows = (rows.min(out_h as u64) as u32).max(1);
3342 loop {
3344 let (lo, hi) = bg_rows_needed(page, full, plane, offset_y..offset_y + rows);
3345 if hi - lo <= band_rows || rows == 1 {
3346 return rows;
3347 }
3348 rows = (rows * 7 / 8).max(1);
3349 }
3350}
3351
3352#[allow(clippy::too_many_arguments)]
3363fn for_each_bg_band<F>(
3364 page: &DjVuPage,
3365 opts: &RenderOptions,
3366 bg: &Background,
3367 mask: Option<&crate::bitmap::Bitmap>,
3368 mask_shift: u32,
3369 fg_palette: Option<&FgbzPalette>,
3370 blit_map: Option<&[i32]>,
3371 fg44: Option<&Pixmap>,
3372 gamma_lut: &[u8; 256],
3373 offset: (u32, u32),
3374 out: (u32, u32),
3375 mut f: F,
3376) -> Result<(), RenderError>
3377where
3378 F: FnMut(&CompositeContext<'_>, u32) -> Result<(), RenderError>,
3379{
3380 let (image, band_rows) = match bg {
3381 Background::Banded { image, band_rows } => (image, *band_rows),
3382 _ => {
3383 let ctx = CompositeContext::from_layers(
3384 page,
3385 opts,
3386 bg.whole().map(PlaneView::whole),
3387 mask,
3388 mask_shift,
3389 fg_palette,
3390 blit_map,
3391 fg44,
3392 gamma_lut,
3393 offset,
3394 out,
3395 );
3396 return f(&ctx, 0);
3397 }
3398 };
3399 let plane = (image.width, image.height);
3400 let page_dims = (page.width() as u32, page.height() as u32);
3401 let full = (opts.width, opts.height);
3402 let template = CompositeContext::from_layers(
3403 page, opts, None, mask, mask_shift, fg_palette, blit_map, fg44, gamma_lut, offset, out,
3404 );
3405 let mut oy0 = 0u32;
3406 while oy0 < out.1 {
3407 let rows = bg_band_out_rows(
3408 page_dims,
3409 full,
3410 plane,
3411 offset.1 + oy0,
3412 out.1 - oy0,
3413 band_rows,
3414 );
3415 let oy1 = oy0 + rows;
3416 let (lo, hi) = bg_rows_needed(page_dims, full, plane, offset.1 + oy0..offset.1 + oy1);
3417 let band = image.rgb_rows(lo..hi)?;
3418 let view = PlaneView::band(&band, plane.1, lo);
3419 let mut ctx = template.with_bg(Some(view));
3420 ctx.offset_y = offset.1 + oy0;
3421 ctx.out_h = rows;
3422 f(&ctx, oy0)?;
3423 oy0 = oy1;
3424 }
3425 Ok(())
3426}
3427
3428fn composite_into(ctx: &CompositeContext<'_>, buf: &mut [u8]) -> Result<(), RenderError> {
3438 let full_w = ctx.opts.width;
3439 let full_h = ctx.opts.height;
3440
3441 let fx_step = ((ctx.page_w as u64 * FRAC as u64) / full_w.max(1) as u64) as u32;
3443 let fy_step = ((ctx.page_h as u64 * FRAC as u64) / full_h.max(1) as u64) as u32;
3444
3445 let row_stride = ctx.out_w as usize * 4;
3446
3447 if ctx.bg.is_none() && ctx.fg44.is_none() && ctx.fg_palette.is_none() {
3451 #[cfg(feature = "parallel")]
3452 {
3453 use rayon::prelude::*;
3454 let n = ctx.out_h as usize * row_stride;
3455 buf[..n]
3456 .par_chunks_exact_mut(row_stride)
3457 .enumerate()
3458 .for_each(|(oy, row)| {
3459 composite_rows_bilevel_one(ctx, oy as u32, fx_step, fy_step, row);
3460 });
3461 }
3462 #[cfg(not(feature = "parallel"))]
3463 for (oy, row) in buf[..ctx.out_h as usize * row_stride]
3464 .chunks_exact_mut(row_stride)
3465 .enumerate()
3466 {
3467 composite_rows_bilevel_one(ctx, oy as u32, fx_step, fy_step, row);
3468 }
3469 return Ok(());
3470 }
3471
3472 let downscale = fx_step > FRAC || fy_step > FRAC;
3473 let bg_fx_step = ((fx_step as u64 * ctx.bg_x_q24) >> 24) as u32;
3475 let bg_fy_step = ((fy_step as u64 * ctx.bg_y_q24) >> 24) as u32;
3476 let area_avg_x = downscale.then(|| precompute_area_avg_x(ctx, fx_step, bg_fx_step));
3477 let bilinear_x = (!downscale)
3478 .then(|| precompute_bilinear_x(ctx, fx_step))
3479 .flatten();
3480
3481 #[cfg(feature = "parallel")]
3482 {
3483 use rayon::prelude::*;
3484 let n = ctx.out_h as usize * row_stride;
3485 buf[..n]
3486 .par_chunks_exact_mut(row_stride)
3487 .enumerate()
3488 .for_each_init(Vec::new, |vblend, (oy, row)| {
3489 if downscale {
3490 composite_rows_area_avg_one(
3491 ctx,
3492 oy as u32,
3493 fx_step,
3494 fy_step,
3495 bg_fx_step,
3496 bg_fy_step,
3497 row,
3498 area_avg_x.as_deref(),
3499 );
3500 } else {
3501 composite_rows_bilinear_one(
3502 ctx,
3503 oy as u32,
3504 fx_step,
3505 fy_step,
3506 row,
3507 bilinear_x.as_deref(),
3508 vblend,
3509 );
3510 }
3511 });
3512 }
3513 #[cfg(not(feature = "parallel"))]
3514 {
3515 let mut vblend = Vec::new();
3516 for (oy, row) in buf[..ctx.out_h as usize * row_stride]
3517 .chunks_exact_mut(row_stride)
3518 .enumerate()
3519 {
3520 if downscale {
3521 composite_rows_area_avg_one(
3522 ctx,
3523 oy as u32,
3524 fx_step,
3525 fy_step,
3526 bg_fx_step,
3527 bg_fy_step,
3528 row,
3529 area_avg_x.as_deref(),
3530 );
3531 } else {
3532 composite_rows_bilinear_one(
3533 ctx,
3534 oy as u32,
3535 fx_step,
3536 fy_step,
3537 row,
3538 bilinear_x.as_deref(),
3539 &mut vblend,
3540 );
3541 }
3542 }
3543 }
3544
3545 Ok(())
3549}
3550
3551fn composite_rows<F>(ctx: &CompositeContext<'_>, mut sink: F) -> Result<(), RenderError>
3562where
3563 F: FnMut(usize, &[u8]),
3564{
3565 let full_w = ctx.opts.width;
3566 let full_h = ctx.opts.height;
3567
3568 let fx_step = ((ctx.page_w as u64 * FRAC as u64) / full_w.max(1) as u64) as u32;
3570 let fy_step = ((ctx.page_h as u64 * FRAC as u64) / full_h.max(1) as u64) as u32;
3571
3572 let row_stride = ctx.out_w as usize * 4;
3573
3574 if ctx.bg.is_none() && ctx.fg44.is_none() && ctx.fg_palette.is_none() {
3576 let mut row_buf = vec![0u8; row_stride];
3577 for oy in 0..ctx.out_h {
3578 composite_rows_bilevel_one(ctx, oy, fx_step, fy_step, &mut row_buf);
3579 sink(oy as usize, &row_buf);
3580 }
3581 return Ok(());
3582 }
3583
3584 let mut row_buf = vec![0u8; row_stride];
3585 let downscale = fx_step > FRAC || fy_step > FRAC;
3586
3587 let bg_fx_step = ((fx_step as u64 * ctx.bg_x_q24) >> 24) as u32;
3589 let bg_fy_step = ((fy_step as u64 * ctx.bg_y_q24) >> 24) as u32;
3590 let area_avg_x = downscale.then(|| precompute_area_avg_x(ctx, fx_step, bg_fx_step));
3591 let bilinear_x = (!downscale)
3592 .then(|| precompute_bilinear_x(ctx, fx_step))
3593 .flatten();
3594 let mut vblend = Vec::new();
3595
3596 for oy in 0..ctx.out_h {
3597 if downscale {
3598 composite_rows_area_avg_one(
3599 ctx,
3600 oy,
3601 fx_step,
3602 fy_step,
3603 bg_fx_step,
3604 bg_fy_step,
3605 &mut row_buf,
3606 area_avg_x.as_deref(),
3607 );
3608 } else {
3609 composite_rows_bilinear_one(
3610 ctx,
3611 oy,
3612 fx_step,
3613 fy_step,
3614 &mut row_buf,
3615 bilinear_x.as_deref(),
3616 &mut vblend,
3617 );
3618 }
3619 sink(oy as usize, &row_buf);
3620 }
3621
3622 Ok(())
3623}
3624
3625#[inline]
3627fn composite_rows_bilevel_one(
3628 ctx: &CompositeContext<'_>,
3629 oy: u32,
3630 fx_step: u32,
3631 fy_step: u32,
3632 row_buf: &mut [u8],
3633) {
3634 let mask = match ctx.mask {
3635 Some(m) => m,
3636 None => {
3637 for chunk in row_buf.as_chunks_mut::<4>().0 {
3638 chunk[0] = 255;
3639 chunk[1] = 255;
3640 chunk[2] = 255;
3641 chunk[3] = 255;
3642 }
3643 return;
3644 }
3645 };
3646
3647 if fx_step == FRAC && fy_step == FRAC {
3649 let stride = mask.row_stride();
3650 let py = (oy + ctx.offset_y)
3664 .min(ctx.page_h.saturating_sub(1))
3665 .min(mask.height.saturating_sub(1)) as usize;
3666 let Some(mask_row) = mask.data.get(py * stride..(py + 1) * stride) else {
3667 row_buf.fill(255);
3668 return;
3669 };
3670 if mask_row.is_empty() {
3671 row_buf.fill(255);
3672 return;
3673 }
3674
3675 if !mask_row.iter().any(|&b| b != 0) {
3680 row_buf.fill(255);
3681 return;
3682 }
3683
3684 let out_w = row_buf.len() / 4;
3693 let ox0 = ctx.offset_x as usize;
3694 if ctx.offset_x.is_multiple_of(8) && ox0 + out_w <= mask.width as usize {
3695 let mb0 = ox0 / 8; let nb_full = out_w / 8; let nb_rem = out_w % 8; for byte_idx in 0..nb_full {
3699 let mb = mask_row[mb0 + byte_idx];
3700 let src = &BILEVEL_RGBA[mb as usize];
3701 row_buf[byte_idx * 32..(byte_idx + 1) * 32].copy_from_slice(src);
3702 }
3703 if nb_rem > 0 {
3704 let mb = mask_row[mb0 + nb_full];
3705 let src = &BILEVEL_RGBA[mb as usize];
3706 let base = nb_full * 32;
3707 row_buf[base..base + nb_rem * 4].copy_from_slice(&src[..nb_rem * 4]);
3708 }
3709 return;
3710 }
3711
3712 let last_col = ctx
3726 .page_w
3727 .saturating_sub(1)
3728 .min(mask.width.saturating_sub(1));
3729 for (ox, pixel) in row_buf.as_chunks_mut::<4>().0.iter_mut().enumerate() {
3730 let px = (ox as u32 + ctx.offset_x).min(last_col) as usize;
3731 let is_fg = ((mask_row[px >> 3] >> (7 - (px & 7))) & 1) as u32;
3732 let ch = (is_fg.wrapping_sub(1) & 0xFF) as u8; pixel[0] = ch;
3734 pixel[1] = ch;
3735 pixel[2] = ch;
3736 pixel[3] = 255;
3737 }
3738 return;
3739 }
3740
3741 let downscale = fx_step > FRAC || fy_step > FRAC;
3742 let fy = (oy + ctx.offset_y) * fy_step;
3743 let py = (fy >> FRACBITS).min(ctx.page_h.saturating_sub(1));
3744
3745 if downscale && ctx.mask_shift == 0 {
3754 let stride = mask.row_stride();
3755 let y0 = (fy >> FRACBITS).min(mask.height.saturating_sub(1)) as usize;
3756 let y1 = ((fy + fy_step) >> FRACBITS).min(mask.height) as usize;
3757 if y1 <= y0 || !mask.data[y0 * stride..y1 * stride].iter().any(|&b| b != 0) {
3758 row_buf.fill(255);
3759 return;
3760 }
3761 }
3762
3763 for (ox, pixel) in row_buf.as_chunks_mut::<4>().0.iter_mut().enumerate() {
3764 let fx = (ox as u32 + ctx.offset_x) * fx_step;
3765 let px = (fx >> FRACBITS).min(ctx.page_w.saturating_sub(1));
3766
3767 let ch = if downscale {
3771 if ctx.mask_shift > 0 {
3772 let dpx = fx >> (FRACBITS + ctx.mask_shift);
3774 let dpy = fy >> (FRACBITS + ctx.mask_shift);
3775 if dpx < mask.width && dpy < mask.height && mask.get(dpx, dpy) {
3776 0u8
3777 } else {
3778 255u8
3779 }
3780 } else {
3781 255 - mask_box_coverage(mask, fx, fy, fx_step, fy_step)
3783 }
3784 } else if ctx.opts.mask_aa && ctx.mask_shift == 0 {
3785 255 - mask_bilinear_coverage(mask, fx, fy)
3792 } else if px < mask.width && py < mask.height && mask.get(px, py) {
3793 0u8
3794 } else {
3795 255u8
3796 };
3797 pixel[0] = ch;
3798 pixel[1] = ch;
3799 pixel[2] = ch;
3800 pixel[3] = 255;
3801 }
3802}
3803
3804#[inline]
3811fn composite_rows_bilinear_one(
3812 ctx: &CompositeContext<'_>,
3813 oy: u32,
3814 fx_step: u32,
3815 fy_step: u32,
3816 row_buf: &mut [u8],
3817 bx: Option<&[BilinearX]>,
3818 vblend: &mut Vec<u16>,
3819) {
3820 let (page_w, page_h) = (ctx.page_w, ctx.page_h);
3821 let fy = (oy + ctx.offset_y) * fy_step;
3822 let py = (fy >> FRACBITS).min(page_h.saturating_sub(1));
3823
3824 if fx_step == FRAC && fy_step == FRAC && ctx.bg_x_q24 == (1 << 24) && ctx.bg_y_q24 == (1 << 24)
3829 {
3830 if ctx.offset_x == 0
3835 && ctx.fg_palette.is_none()
3836 && ctx.fg44.is_none()
3837 && let Some(bg) = ctx.bg
3838 {
3839 let bg_row = bg.row(py.min(bg.height().saturating_sub(1)));
3840 let lut = &ctx.gamma_lut;
3841
3842 if let Some(mask) = ctx.mask {
3843 let mask_stride = mask.row_stride();
3845 let mask_py = py.min(mask.height.saturating_sub(1)) as usize;
3846 let mask_row = mask.data.get(mask_py * mask_stride..).unwrap_or(&[]);
3847
3848 let bg_max_px = (bg.width() as usize).saturating_sub(1);
3850 let mask_limit = mask.width as usize;
3851 let out_w = row_buf.len() / 4;
3852 macro_rules! a2_has_mask_loop {
3854 ($write:expr) => {
3855 for mb_idx in 0..out_w.div_ceil(8) {
3856 let mb = mask_row.get(mb_idx).copied().unwrap_or(0);
3857 let exp = &MASK_EXPAND[mb as usize];
3858 for j in 0..8usize {
3859 let ox = mb_idx * 8 + j;
3860 if ox >= out_w {
3861 break;
3862 }
3863 let fg_m = if ox < mask_limit { exp[j] } else { 0u8 };
3864 let px = ox.min(bg_max_px);
3865 let off = px * 4;
3866 let pixel = &mut row_buf[ox * 4..(ox + 1) * 4];
3867 let (r, g, b) = if let Some(q) = bg_row.get(off..off + 4) {
3868 (q[0] & !fg_m, q[1] & !fg_m, q[2] & !fg_m)
3869 } else {
3870 (!fg_m, !fg_m, !fg_m)
3871 };
3872 $write(pixel, r, g, b);
3873 }
3874 }
3875 };
3876 }
3877 if ctx.gamma_is_identity {
3878 a2_has_mask_loop!(|pixel: &mut [u8], r, g, b| {
3879 pixel[0] = r;
3880 pixel[1] = g;
3881 pixel[2] = b;
3882 pixel[3] = 255;
3883 });
3884 } else {
3885 a2_has_mask_loop!(|pixel: &mut [u8], r, g, b| {
3886 pixel[0] = lut[r as usize];
3887 pixel[1] = lut[g as usize];
3888 pixel[2] = lut[b as usize];
3889 pixel[3] = 255;
3890 });
3891 }
3892 } else {
3893 if ctx.gamma_is_identity {
3896 let out_w = row_buf.len() / 4;
3897 if bg.width() as usize >= out_w {
3900 row_buf[..out_w * 4].copy_from_slice(&bg_row[..out_w * 4]);
3901 } else {
3902 for (ox, pixel) in row_buf.as_chunks_mut::<4>().0.iter_mut().enumerate() {
3903 let px = ox.min((bg.width() as usize).saturating_sub(1));
3904 let off = px * 4;
3905 if let Some(q) = bg_row.get(off..off + 4) {
3906 pixel[0] = q[0];
3907 pixel[1] = q[1];
3908 pixel[2] = q[2];
3909 } else {
3910 pixel[0] = 255;
3911 pixel[1] = 255;
3912 pixel[2] = 255;
3913 }
3914 pixel[3] = 255;
3915 }
3916 }
3917 } else {
3918 for (ox, pixel) in row_buf.as_chunks_mut::<4>().0.iter_mut().enumerate() {
3919 let px = ox.min((bg.width() as usize).saturating_sub(1));
3920 let off = px * 4;
3921 if let Some(q) = bg_row.get(off..off + 4) {
3922 pixel[0] = lut[q[0] as usize];
3923 pixel[1] = lut[q[1] as usize];
3924 pixel[2] = lut[q[2] as usize];
3925 } else {
3926 pixel[0] = 255;
3927 pixel[1] = 255;
3928 pixel[2] = 255;
3929 }
3930 pixel[3] = 255;
3931 }
3932 }
3933 }
3934 return;
3935 }
3936
3937 let fg_rows_1x1 = ctx.fg44.filter(|_| ctx.fg_palette.is_none()).map(|fg| {
3942 let fg_fy = map_plane_center_frac(fy, ctx.fg_y_q24);
3943 let y0 = (fg_fy >> FRACBITS).min(fg.height.saturating_sub(1)) as usize;
3944 let y1 = (y0 + 1).min(fg.height.saturating_sub(1) as usize);
3945 let ty = fg_fy & FRAC_MASK;
3946 let stride = fg.width as usize * 4;
3947 let row0 = fg.data.get(y0 * stride..).unwrap_or(&[]);
3948 let row1 = fg.data.get(y1 * stride..).unwrap_or(&[]);
3949 (row0, row1, fg.width, ty)
3950 });
3951 let bg_row_1x1 = ctx
3954 .bg
3955 .map(|bg| (bg.row(py.min(bg.height().saturating_sub(1))), bg.width()));
3956 let mask_row_1x1 = ctx.mask.and_then(|m| {
3958 if py >= m.height {
3959 return None;
3960 }
3961 let stride = m.row_stride();
3962 m.data.get(py as usize * stride..).map(|row| (row, m.width))
3963 });
3964
3965 {
3970 let row_is_all_bg = match mask_row_1x1 {
3971 None => true,
3972 Some((mask_row, mask_w)) => {
3973 let check_bytes = (mask_w as usize).div_ceil(8).min(mask_row.len());
3974 mask_row[..check_bytes].iter().all(|&b| b == 0)
3975 }
3976 };
3977 if row_is_all_bg && ctx.gamma_is_identity {
3978 let out_w = row_buf.len() / 4;
3979 let offset_x = ctx.offset_x as usize;
3980 if let Some((bg_row, bg_w)) = bg_row_1x1 {
3981 if offset_x + out_w <= bg_w as usize {
3982 row_buf.copy_from_slice(&bg_row[offset_x * 4..(offset_x + out_w) * 4]);
3983 return;
3984 }
3985 } else {
3987 row_buf.fill(255);
3988 return;
3989 }
3990 } else if row_is_all_bg {
3991 let out_w = row_buf.len() / 4;
3996 let offset_x = ctx.offset_x as usize;
3997 let lut = &ctx.gamma_lut;
3998 if let Some((bg_row, bg_w)) = bg_row_1x1 {
3999 if offset_x + out_w <= bg_w as usize {
4000 let src = &bg_row[offset_x * 4..(offset_x + out_w) * 4];
4001 for (chunk, s) in row_buf
4002 .as_chunks_mut::<4>()
4003 .0
4004 .iter_mut()
4005 .zip(src.as_chunks::<4>().0)
4006 {
4007 chunk[0] = lut[s[0] as usize];
4008 chunk[1] = lut[s[1] as usize];
4009 chunk[2] = lut[s[2] as usize];
4010 chunk[3] = 255;
4011 }
4012 return;
4013 }
4014 } else {
4016 let white = lut[255];
4017 for chunk in row_buf.as_chunks_mut::<4>().0 {
4018 chunk[0] = white;
4019 chunk[1] = white;
4020 chunk[2] = white;
4021 chunk[3] = 255;
4022 }
4023 return;
4024 }
4025 }
4026 }
4027
4028 const G1_MAX: usize = 4096;
4033 let mut g1_buf = [0u8; G1_MAX];
4034 let g1_mask: &[u8] = if let Some((mask_row, mask_w)) = mask_row_1x1 {
4035 let mw = mask_w as usize;
4036 if mw <= G1_MAX {
4037 let nb = mw.div_ceil(8);
4038 for (i, &mb) in mask_row[..nb].iter().enumerate() {
4039 let exp = &MASK_EXPAND[mb as usize];
4040 let base = i * 8;
4041 g1_buf[base..base + 8].copy_from_slice(exp);
4043 }
4044 &g1_buf[..mw]
4045 } else {
4046 &g1_buf[..0] }
4048 } else {
4049 &g1_buf[..0] };
4051
4052 for (ox, pixel) in row_buf.as_chunks_mut::<4>().0.iter_mut().enumerate() {
4053 let fx = (ox as u32 + ctx.offset_x) * fx_step;
4054 let px = (fx >> FRACBITS).min(page_w.saturating_sub(1));
4055
4056 let is_fg = if g1_mask.is_empty() {
4060 mask_row_1x1.is_some_and(|(row, mask_w)| {
4061 let pxu = px as usize;
4062 pxu < mask_w as usize
4063 && (row.get(pxu >> 3).copied().unwrap_or(0) >> (7 - (pxu & 7))) & 1 != 0
4064 })
4065 } else {
4066 g1_mask.get(px as usize).copied().unwrap_or(0) != 0
4067 };
4068
4069 let (r, g, b) = if is_fg {
4070 if let Some(pal) = ctx.fg_palette {
4071 let color = lookup_palette_color(pal, ctx.blit_map, ctx.mask, px, py);
4072 (color.r, color.g, color.b)
4073 } else if let Some((fg_row0, fg_row1, fg_w, fg_ty)) = fg_rows_1x1 {
4074 let fg_fx = map_plane_center_frac(fx, ctx.fg_x_q24);
4075 bilinear_from_rows(fg_row0, fg_row1, fg_w, fg_fx, fg_ty)
4076 } else {
4077 (0, 0, 0)
4078 }
4079 } else if let Some((bg_row, bg_w)) = bg_row_1x1 {
4080 let bx = (px as usize).min(bg_w.saturating_sub(1) as usize);
4081 let off = bx * 4;
4082 bg_row
4083 .get(off..off + 4)
4084 .map_or((255, 255, 255), |q| (q[0], q[1], q[2]))
4085 } else {
4086 (255, 255, 255)
4087 };
4088
4089 if ctx.gamma_is_identity {
4090 pixel[0] = r;
4091 pixel[1] = g;
4092 pixel[2] = b;
4093 } else {
4094 pixel[0] = ctx.gamma_lut[r as usize];
4095 pixel[1] = ctx.gamma_lut[g as usize];
4096 pixel[2] = ctx.gamma_lut[b as usize];
4097 }
4098 pixel[3] = 255;
4099 }
4100 return;
4101 }
4102
4103 let bg_fy_hoist = ctx.bg.map(|_| map_plane_center_frac(fy, ctx.bg_y_q24));
4108 let bg_fx_step_q: u64 = fx_step as u64 * ctx.bg_x_q24;
4109 let mut bg_fx_q: u64 = (ctx.offset_x as u64 * fx_step as u64 + FRAC as u64 / 2) * ctx.bg_x_q24;
4110
4111 let mask_hoist = ctx.mask.and_then(|m| {
4113 if py >= m.height {
4114 return None;
4115 }
4116 let stride = m.row_stride();
4117 m.data.get(py as usize * stride..).map(|row| (row, m.width))
4118 });
4119
4120 let mask_all_bg = match mask_hoist {
4127 None => true,
4128 Some((mask_row, mask_w)) => {
4129 let nb = (mask_w as usize).div_ceil(8).min(mask_row.len());
4130 !mask_row[..nb].iter().any(|&b| b != 0)
4131 }
4132 };
4133
4134 let vb: Option<(&[[u16; 4]], u32, u32)> = match ctx.bg {
4146 None => None,
4147 Some(bg) => {
4148 let bg_fy = bg_fy_hoist.unwrap_or(0);
4149 let clamp_h = bg.height().saturating_sub(1);
4150 let y0 = (bg_fy >> FRACBITS).min(clamp_h);
4151 let y1 = (y0 + 1).min(clamp_h);
4152 let ty = bg_fy & FRAC_MASK;
4153 let ity = FRAC - ty;
4154 let row0 = bg.row(y0);
4155 let row1 = bg.row(y1);
4156 let clamp_w = bg.width().saturating_sub(1);
4157 let fx_at = |q: u64| ((q >> 24) as u32).saturating_sub(FRAC / 2);
4158 let col_start = (fx_at(bg_fx_q) >> FRACBITS).min(clamp_w);
4159 let last_q =
4160 bg_fx_q.wrapping_add(bg_fx_step_q.wrapping_mul(ctx.out_w.saturating_sub(1) as u64));
4161 let col_end = ((fx_at(last_q) >> FRACBITS).min(clamp_w) + 1).min(clamp_w);
4162 let ncols = (col_end - col_start + 1) as usize;
4163 vblend.clear();
4164 vblend.resize(ncols * 4, 0);
4165 for (i, v) in vblend.as_chunks_mut::<4>().0.iter_mut().enumerate() {
4166 let off = (col_start as usize + i) * 4;
4169 let p0 = row0.get(off..off + 4);
4170 let p1 = row1.get(off..off + 4);
4171 for ch in 0..4 {
4172 let a = p0.map_or(0, |q| q[ch] as u32);
4173 let b = p1.map_or(0, |q| q[ch] as u32);
4174 v[ch] = (a * ity + b * ty) as u16;
4175 }
4176 }
4177 Some((vblend.as_chunks::<4>().0, bg.width(), col_start))
4178 }
4179 };
4180
4181 let hblend =
4186 |vb_row: &[[u16; 4]], bg_w: u32, col_start: u32, e: Option<BilinearX>, bg_fx_q: u64| {
4187 let e = e.unwrap_or_else(|| {
4188 let bg_fx = ((bg_fx_q >> 24) as u32).saturating_sub(FRAC / 2);
4189 let clamp_w = bg_w.saturating_sub(1);
4190 let x0 = (bg_fx >> FRACBITS).min(clamp_w);
4191 BilinearX {
4192 x0,
4193 x1: (x0 + 1).min(clamp_w),
4194 tx: bg_fx & FRAC_MASK,
4195 }
4196 });
4197 let v0 = vb_row
4198 .get(e.x0.saturating_sub(col_start) as usize)
4199 .copied()
4200 .unwrap_or([0; 4]);
4201 let v1 = vb_row
4202 .get(e.x1.saturating_sub(col_start) as usize)
4203 .copied()
4204 .unwrap_or([0; 4]);
4205 let itx = FRAC - e.tx;
4206 let f = |i: usize| ((v0[i] as u32 * itx + v1[i] as u32 * e.tx + 128) >> 8) as u8;
4207 (f(0), f(1), f(2))
4208 };
4209
4210 let mask_upscale =
4219 ctx.opts.mask_aa && ctx.mask_shift == 0 && (fx_step < FRAC || fy_step < FRAC);
4220
4221 for (ox, pixel) in row_buf.as_chunks_mut::<4>().0.iter_mut().enumerate() {
4222 let fx = (ox as u32 + ctx.offset_x) * fx_step;
4223 let px = (fx >> FRACBITS).min(page_w.saturating_sub(1));
4224
4225 let coverage: u8 = if mask_upscale {
4233 if mask_all_bg {
4234 0
4235 } else {
4236 ctx.mask.map_or(0, |m| mask_bilinear_coverage(m, fx, fy))
4237 }
4238 } else if !mask_all_bg
4239 && mask_hoist.is_some_and(|(mask_row, mask_w)| {
4240 let pxu = px as usize;
4241 pxu < mask_w as usize
4242 && (mask_row.get(pxu >> 3).copied().unwrap_or(0) >> (7 - (pxu & 7))) & 1 != 0
4243 })
4244 {
4245 255
4246 } else {
4247 0
4248 };
4249
4250 let (r, g, b) = if coverage == 0 {
4251 if let Some((vb_row, bg_w, col_start)) = vb {
4252 hblend(
4253 vb_row,
4254 bg_w,
4255 col_start,
4256 bx.and_then(|t| t.get(ox).copied()),
4257 bg_fx_q,
4258 )
4259 } else {
4260 (255, 255, 255)
4261 }
4262 } else {
4263 let (fr, fg_g, fb) = if let Some(pal) = ctx.fg_palette {
4264 let color = lookup_palette_color(pal, ctx.blit_map, ctx.mask, px, py);
4265 (color.r, color.g, color.b)
4266 } else if let Some(fg) = ctx.fg44 {
4267 let fg_fx = map_plane_center_frac(fx, ctx.fg_x_q24);
4268 let fg_fy = map_plane_center_frac(fy, ctx.fg_y_q24);
4269 sample_bilinear(fg, fg_fx, fg_fy)
4270 } else {
4271 (0, 0, 0)
4272 };
4273 if coverage == 255 {
4274 (fr, fg_g, fb)
4275 } else {
4276 let (br, bg_g, bb) = if let Some((vb_row, bg_w, col_start)) = vb {
4279 hblend(
4280 vb_row,
4281 bg_w,
4282 col_start,
4283 bx.and_then(|t| t.get(ox).copied()),
4284 bg_fx_q,
4285 )
4286 } else {
4287 (255, 255, 255)
4288 };
4289 let cov = coverage as u32;
4290 let inv = 255 - cov;
4291 let blend =
4292 |f: u8, b: u8| -> u8 { ((f as u32 * cov + b as u32 * inv + 127) / 255) as u8 };
4293 (blend(fr, br), blend(fg_g, bg_g), blend(fb, bb))
4294 }
4295 };
4296
4297 if ctx.gamma_is_identity {
4299 pixel[0] = r;
4300 pixel[1] = g;
4301 pixel[2] = b;
4302 } else {
4303 pixel[0] = ctx.gamma_lut[r as usize];
4304 pixel[1] = ctx.gamma_lut[g as usize];
4305 pixel[2] = ctx.gamma_lut[b as usize];
4306 }
4307 pixel[3] = 255;
4308 bg_fx_q = bg_fx_q.wrapping_add(bg_fx_step_q);
4309 }
4310}
4311
4312#[inline]
4314#[allow(clippy::too_many_arguments)]
4315fn composite_rows_area_avg_one(
4316 ctx: &CompositeContext<'_>,
4317 oy: u32,
4318 fx_step: u32,
4319 fy_step: u32,
4320 bg_fx_step: u32,
4321 bg_fy_step: u32,
4322 row_buf: &mut [u8],
4323 area_avg_x: Option<&[AreaAvgX]>,
4324) {
4325 let fy = (oy + ctx.offset_y) * fy_step;
4326 let bg_fy = ((fy as u64 * ctx.bg_y_q24) >> 24) as u32;
4327 let bg_y = ctx.bg.map(|bg| area_range(bg.height(), bg_fy, bg_fy_step));
4328
4329 let mask_all_bg = ctx.mask_shift == 0
4336 && ctx.mask.is_none_or(|m| {
4337 let stride = m.row_stride();
4338 let y0 = (fy >> FRACBITS).min(m.height.saturating_sub(1)) as usize;
4339 let y1 = ((fy + fy_step) >> FRACBITS).min(m.height) as usize;
4340 y1 <= y0 || !m.data[y0 * stride..y1 * stride].iter().any(|&b| b != 0)
4341 });
4342
4343 for (ox, pixel) in row_buf.as_chunks_mut::<4>().0.iter_mut().enumerate() {
4344 let fallback;
4345 let ax = if let Some(ax) = area_avg_x.and_then(|xs| xs.get(ox)) {
4346 *ax
4347 } else {
4348 let fx = (ox as u32 + ctx.offset_x) * fx_step;
4349 let bg_fx = ((fx as u64 * ctx.bg_x_q24) >> 24) as u32;
4350 let (bg_x0, bg_x1) = area_range(ctx.bg.map_or(0, |bg| bg.width()), bg_fx, bg_fx_step);
4351 fallback = AreaAvgX { fx, bg_x0, bg_x1 };
4352 fallback
4353 };
4354 let fx = ax.fx;
4355
4356 let coverage: u8 = if mask_all_bg {
4363 0
4364 } else if let Some(m) = ctx.mask {
4365 if ctx.mask_shift > 0 {
4366 let px = fx >> (FRACBITS + ctx.mask_shift);
4367 let pym = fy >> (FRACBITS + ctx.mask_shift);
4368 if px < m.width && pym < m.height && m.get(px, pym) {
4369 255
4370 } else {
4371 0
4372 }
4373 } else {
4374 mask_box_coverage(m, fx, fy, fx_step, fy_step)
4375 }
4376 } else {
4377 0
4378 };
4379
4380 let bg_sample = || -> (u8, u8, u8) {
4381 match (ctx.bg, bg_y) {
4383 (Some(bg), Some((bg_y0, bg_y1))) => {
4384 sample_area_avg_bounds(bg, ax.bg_x0, ax.bg_x1, bg_y0, bg_y1)
4385 }
4386 _ => (255, 255, 255),
4387 }
4388 };
4389 let fg_sample = || -> (u8, u8, u8) {
4390 if let Some(pal) = ctx.fg_palette {
4391 let (cx, cy) = mask_box_center_fg(ctx.mask.unwrap(), fx, fy, fx_step, fy_step);
4392 let color = lookup_palette_color(pal, ctx.blit_map, ctx.mask, cx, cy);
4393 (color.r, color.g, color.b)
4394 } else if let Some(fg) = ctx.fg44 {
4395 let fg_fx = ((fx as u64 * ctx.fg_x_q24) >> 24) as u32;
4396 let fg_fy = ((fy as u64 * ctx.fg_y_q24) >> 24) as u32;
4397 let fg_fx_step = ((fx_step as u64 * ctx.fg_x_q24) >> 24) as u32;
4398 let fg_fy_step = ((fy_step as u64 * ctx.fg_y_q24) >> 24) as u32;
4399 sample_area_avg(fg, fg_fx, fg_fy, fg_fx_step, fg_fy_step)
4400 } else {
4401 (0, 0, 0)
4402 }
4403 };
4404
4405 let (r, g, b) = if coverage == 0 {
4406 bg_sample()
4407 } else if coverage == 255 {
4408 fg_sample()
4409 } else {
4410 let (fr, fg_, fb) = fg_sample();
4412 let (br, bg_, bb) = bg_sample();
4413 let c = coverage as u32;
4414 let ic = 255 - c;
4415 let mix = |f: u8, b: u8| ((c * f as u32 + ic * b as u32 + 127) / 255) as u8;
4416 (mix(fr, br), mix(fg_, bg_), mix(fb, bb))
4417 };
4418
4419 if ctx.gamma_is_identity {
4420 pixel[0] = r;
4421 pixel[1] = g;
4422 pixel[2] = b;
4423 } else {
4424 pixel[0] = ctx.gamma_lut[r as usize];
4425 pixel[1] = ctx.gamma_lut[g as usize];
4426 pixel[2] = ctx.gamma_lut[b as usize];
4427 }
4428 pixel[3] = 255;
4429 }
4430}
4431
4432pub(crate) fn render_rows<F>(
4446 page: &DjVuPage,
4447 opts: &RenderOptions,
4448 limits: Option<crate::resource_limits::ResourceLimits>,
4449 sink: F,
4450) -> Result<(), RenderError>
4451where
4452 F: FnMut(usize, &[u8]),
4453{
4454 let w = opts.width;
4455 let h = opts.height;
4456
4457 check_output_pixels("render_rows", page, limits, w, h)?;
4458
4459 let gamma_lut = build_gamma_lut(page.gamma());
4460
4461 let bg_subsample = best_iw44_subsample(opts.decode_scale(page));
4462
4463 let DecodedLayers {
4464 bg,
4465 fg_palette,
4466 mask,
4467 blit_map,
4468 fg44,
4469 } = decode_layers(page, opts, bg_subsample, usize::MAX)?;
4470
4471 let (mask_plane, mask_shift) = resolve_sub4_mask(
4472 page,
4473 bg_subsample,
4474 opts,
4475 mask.as_deref(),
4476 fg_palette.as_ref(),
4477 );
4478 let ctx_mask = mask_plane.get();
4479 let mut sink = sink;
4480 for_each_bg_band(
4481 page,
4482 opts,
4483 &bg,
4484 ctx_mask,
4485 mask_shift,
4486 fg_palette.as_ref(),
4487 blit_map.as_deref().map(Vec::as_slice),
4488 fg44.as_deref(),
4489 &gamma_lut,
4490 (0, 0),
4491 (w, h),
4492 |ctx, oy0| composite_rows(ctx, |y, row| sink(y + oy0 as usize, row)),
4493 )
4494}
4495
4496pub fn render_into(
4510 page: &DjVuPage,
4511 opts: &RenderOptions,
4512 buf: &mut [u8],
4513) -> Result<(), RenderError> {
4514 render_into_with_limits(page, opts, None, buf)
4515}
4516
4517pub fn render_into_with_limits(
4519 page: &DjVuPage,
4520 opts: &RenderOptions,
4521 limits: Option<crate::resource_limits::ResourceLimits>,
4522 buf: &mut [u8],
4523) -> Result<(), RenderError> {
4524 let w = opts.width;
4525 let h = opts.height;
4526
4527 check_output_pixels("render_into", page, limits, w, h)?;
4528
4529 let need = (w as usize)
4530 .checked_mul(h as usize)
4531 .and_then(|n| n.checked_mul(4))
4532 .unwrap_or(usize::MAX);
4533
4534 if buf.len() < need {
4535 return Err(RenderError::BufTooSmall {
4536 need,
4537 got: buf.len(),
4538 });
4539 }
4540
4541 let gamma_lut = build_gamma_lut(page.gamma());
4542
4543 let bg_subsample = best_iw44_subsample(opts.decode_scale(page));
4545 let DecodedLayers {
4546 bg,
4547 fg_palette,
4548 mask,
4549 blit_map,
4550 fg44,
4551 } = decode_layers(page, opts, bg_subsample, usize::MAX)?;
4552
4553 let (mask_plane, mask_shift) = resolve_sub4_mask(
4556 page,
4557 bg_subsample,
4558 opts,
4559 mask.as_deref(),
4560 fg_palette.as_ref(),
4561 );
4562 let ctx_mask = mask_plane.get();
4563 for_each_bg_band(
4564 page,
4565 opts,
4566 &bg,
4567 ctx_mask,
4568 mask_shift,
4569 fg_palette.as_ref(),
4570 blit_map.as_deref().map(Vec::as_slice),
4571 fg44.as_deref(),
4572 &gamma_lut,
4573 (0, 0),
4574 (w, h),
4575 |ctx, oy0| composite_into(ctx, band_rows_mut(buf, w, oy0, ctx.out_h)),
4576 )
4577}
4578
4579#[inline]
4581fn band_rows_mut(buf: &mut [u8], w: u32, oy0: u32, rows: u32) -> &mut [u8] {
4582 let stride = w as usize * 4;
4583 &mut buf[oy0 as usize * stride..(oy0 as usize + rows as usize) * stride]
4584}
4585
4586fn native_render_opts(page: &DjVuPage, opts: &RenderOptions) -> RenderOptions {
4594 RenderOptions {
4598 width: page.width() as u32,
4599 height: page.height() as u32,
4600 bold: opts.bold,
4601 permissive: opts.permissive,
4602 ..Default::default()
4603 }
4604}
4605
4606fn apply_lanczos_postpass<F>(
4621 pm: Pixmap,
4622 page: &DjVuPage,
4623 opts: &RenderOptions,
4624 full: (u32, u32),
4625 out: (u32, u32),
4626 render_native: F,
4627) -> Result<Pixmap, RenderError>
4628where
4629 F: FnOnce(&RenderOptions) -> Result<Pixmap, RenderError>,
4630{
4631 if opts.resampling != Resampling::Lanczos3 {
4632 return Ok(pm);
4633 }
4634 let (full_w, full_h) = full;
4635 let need_scale = page.width() as u32 != full_w || page.height() as u32 != full_h;
4636 if !need_scale {
4637 return Ok(pm);
4638 }
4639 let native_opts = native_render_opts(page, opts);
4640 match render_native(&native_opts) {
4641 Ok(native_pm) => Ok(crate::pixmap::scale_lanczos3(&native_pm, out.0, out.1)?),
4644 Err(_) => Ok(pm),
4646 }
4647}
4648
4649#[cfg(feature = "std")]
4662pub fn render_pixmap_with_report(
4663 page: &DjVuPage,
4664 opts: &RenderOptions,
4665) -> Result<(Pixmap, RenderReport), RenderError> {
4666 struct SinkGuard;
4669 impl Drop for SinkGuard {
4670 fn drop(&mut self) {
4671 RECOVERY_SINK.with(|sink| *sink.borrow_mut() = None);
4672 }
4673 }
4674 RECOVERY_SINK.with(|sink| *sink.borrow_mut() = Some(Vec::new()));
4675 let _guard = SinkGuard;
4676
4677 let pixmap = render_pixmap(page, opts)?;
4678 let recoveries = RECOVERY_SINK.with(|sink| sink.borrow_mut().take().unwrap_or_default());
4679 Ok((pixmap, RenderReport { recoveries }))
4680}
4681
4682pub fn render_pixmap(page: &DjVuPage, opts: &RenderOptions) -> Result<Pixmap, RenderError> {
4683 render_pixmap_with_limits(page, opts, None)
4684}
4685
4686pub fn render_pixmap_with_limits(
4693 page: &DjVuPage,
4694 opts: &RenderOptions,
4695 limits: Option<crate::resource_limits::ResourceLimits>,
4696) -> Result<Pixmap, RenderError> {
4697 let w = opts.width;
4698 let h = opts.height;
4699
4700 check_output_pixels("render_pixmap", page, limits, w, h)?;
4705
4706 let mut pm = Pixmap::white(w, h);
4707
4708 if opts.permissive {
4709 let row_stride = w as usize * 4;
4710 render_rows(page, opts, limits, |y, row| {
4713 let start = y * row_stride;
4714 pm.data[start..start + row_stride].copy_from_slice(row);
4715 })?;
4716 } else {
4717 render_into_with_limits(page, opts, limits, &mut pm.data)?;
4720 }
4721
4722 if opts.aa {
4723 pm = aa_downscale(&pm);
4724 }
4725
4726 let pm = apply_lanczos_postpass(pm, page, opts, (w, h), (w, h), |native_opts| {
4729 render_pixmap_with_limits(page, native_opts, limits)
4730 })?;
4731
4732 Ok(rotate_pixmap(
4733 pm,
4734 combine_rotations(page.rotation(), opts.rotation),
4735 ))
4736}
4737
4738pub fn render_streaming<F>(
4791 page: &DjVuPage,
4792 opts: &RenderOptions,
4793 sink: F,
4794) -> Result<(), RenderError>
4795where
4796 F: FnMut(usize, &[u8]),
4797{
4798 if opts.aa {
4799 return Err(RenderError::UnsupportedOption(
4800 "anti-aliasing requires a full pixmap; use render_pixmap",
4801 ));
4802 }
4803 let lanczos_with_scaling = opts.resampling == Resampling::Lanczos3
4804 && (page.width() as u32 != opts.width || page.height() as u32 != opts.height);
4805 if lanczos_with_scaling {
4806 return Err(RenderError::UnsupportedOption(
4807 "Lanczos-3 resampling at scaled output requires a full pixmap; use render_pixmap",
4808 ));
4809 }
4810 if combine_rotations(page.rotation(), opts.rotation) != crate::info::Rotation::None {
4811 return Err(RenderError::UnsupportedOption(
4812 "rotation requires a full pixmap; use render_pixmap",
4813 ));
4814 }
4815 render_rows(page, opts, None, sink)
4816}
4817
4818pub fn render_region(
4834 page: &DjVuPage,
4835 region: RenderRect,
4836 opts: &RenderOptions,
4837) -> Result<Pixmap, RenderError> {
4838 check_output_pixels("render_region", page, None, region.width, region.height)?;
4839
4840 let full_w = opts.width.max(1);
4841 let full_h = opts.height.max(1);
4842 let gamma_lut = build_gamma_lut(page.gamma());
4843
4844 let bg_subsample = best_iw44_subsample(opts.decode_scale(page));
4845 let DecodedLayers {
4846 bg,
4847 fg_palette,
4848 mask,
4849 blit_map,
4850 fg44,
4851 } = decode_layers(page, opts, bg_subsample, usize::MAX)?;
4852
4853 let out_w = region.width;
4854 let out_h = region.height;
4855 let mut pm = Pixmap::white(out_w, out_h);
4856
4857 let region_opts = RenderOptions {
4858 width: full_w,
4859 height: full_h,
4860 ..*opts
4861 };
4862 let (mask_plane, mask_shift) = resolve_sub4_mask(
4866 page,
4867 bg_subsample,
4868 opts,
4869 mask.as_deref(),
4870 fg_palette.as_ref(),
4871 );
4872 let ctx_mask = mask_plane.get();
4873 for_each_bg_band(
4874 page,
4875 ®ion_opts,
4876 &bg,
4877 ctx_mask,
4878 mask_shift,
4879 fg_palette.as_ref(),
4880 blit_map.as_deref().map(Vec::as_slice),
4881 fg44.as_deref(),
4882 &gamma_lut,
4883 (region.x, region.y),
4884 (out_w, out_h),
4885 |ctx, oy0| composite_into(ctx, band_rows_mut(&mut pm.data, out_w, oy0, ctx.out_h)),
4886 )?;
4887
4888 let pm = apply_lanczos_postpass(
4891 pm,
4892 page,
4893 opts,
4894 (full_w, full_h),
4895 (out_w, out_h),
4896 |native_opts| render_region(page, region, native_opts),
4897 )?;
4898
4899 Ok(rotate_pixmap(
4900 pm,
4901 combine_rotations(page.rotation(), opts.rotation),
4902 ))
4903}
4904
4905#[inline]
4910pub(crate) fn is_cancelled(cancel: Option<&core::sync::atomic::AtomicBool>) -> bool {
4911 cancel.is_some_and(|flag| flag.load(core::sync::atomic::Ordering::Relaxed))
4912}
4913
4914pub(crate) fn render_region_progressive(
4939 page: &DjVuPage,
4940 region: RenderRect,
4941 opts: &RenderOptions,
4942 chunk_n: usize,
4943 cancel: Option<&core::sync::atomic::AtomicBool>,
4944) -> Result<Option<Pixmap>, RenderError> {
4945 check_output_pixels(
4946 "render_region_progressive",
4947 page,
4948 None,
4949 region.width,
4950 region.height,
4951 )?;
4952 if opts.resampling == Resampling::Lanczos3 {
4953 return Err(RenderError::UnsupportedOption(
4954 "Lanczos-3 resampling is not supported for progressive region renders",
4955 ));
4956 }
4957 let n_bg44 = page.bg44_chunks().len();
4958 let max_chunk = n_bg44.saturating_sub(1);
4959 if n_bg44 > 0 && chunk_n > max_chunk {
4960 return Err(RenderError::ChunkOutOfRange {
4961 chunk_n,
4962 max: max_chunk,
4963 });
4964 }
4965 if is_cancelled(cancel) {
4966 return Ok(None);
4967 }
4968
4969 let full_w = opts.width.max(1);
4970 let full_h = opts.height.max(1);
4971 let gamma_lut = build_gamma_lut(page.gamma());
4972 let bg_subsample = best_iw44_subsample(opts.decode_scale(page));
4973 let DecodedLayers {
4974 bg,
4975 fg_palette,
4976 mask,
4977 blit_map,
4978 fg44,
4979 } = decode_layers(page, opts, bg_subsample, chunk_n + 1)?;
4980
4981 if is_cancelled(cancel) {
4982 return Ok(None);
4983 }
4984
4985 let out_w = region.width;
4986 let out_h = region.height;
4987 let mut pm = Pixmap::white(out_w, out_h);
4988 let region_opts = RenderOptions {
4989 width: full_w,
4990 height: full_h,
4991 ..*opts
4992 };
4993 for_each_bg_band(
4994 page,
4995 ®ion_opts,
4996 &bg,
4997 mask.as_deref(),
4998 0,
4999 fg_palette.as_ref(),
5000 blit_map.as_deref().map(Vec::as_slice),
5001 fg44.as_deref(),
5002 &gamma_lut,
5003 (region.x, region.y),
5004 (out_w, out_h),
5005 |ctx, oy0| composite_into(ctx, band_rows_mut(&mut pm.data, out_w, oy0, ctx.out_h)),
5006 )?;
5007
5008 Ok(Some(rotate_pixmap(
5009 pm,
5010 combine_rotations(page.rotation(), opts.rotation),
5011 )))
5012}
5013
5014#[cfg(feature = "std")]
5063pub fn render_region_tiled(
5064 page: &DjVuPage,
5065 region: RenderRect,
5066 opts: &RenderOptions,
5067) -> Result<Pixmap, RenderError> {
5068 let pm = render_region_tiled_cancellable(page, region, opts, None)?;
5069 Ok(pm.expect("uncancellable render completed"))
5071}
5072
5073#[cfg(feature = "std")]
5081pub(crate) fn render_region_tiled_cancellable(
5082 page: &DjVuPage,
5083 region: RenderRect,
5084 opts: &RenderOptions,
5085 cancel: Option<&core::sync::atomic::AtomicBool>,
5086) -> Result<Option<Pixmap>, RenderError> {
5087 check_output_pixels(
5088 "render_region_tiled",
5089 page,
5090 None,
5091 region.width,
5092 region.height,
5093 )?;
5094
5095 let full_w = opts.width.max(1);
5096 let full_h = opts.height.max(1);
5097
5098 let eligible = opts.resampling == Resampling::Bilinear
5099 && !opts.permissive
5100 && combine_rotations(page.rotation(), opts.rotation) == crate::info::Rotation::None;
5101
5102 if is_cancelled(cancel) {
5103 return Ok(None);
5104 }
5105 if !eligible {
5106 return render_region(page, region, opts).map(Some);
5107 }
5108
5109 let gamma_lut = build_gamma_lut(page.gamma());
5110 let bg_subsample = best_iw44_subsample(opts.decode_scale(page));
5111 let DecodedLayers {
5112 bg,
5113 fg_palette,
5114 mask,
5115 blit_map,
5116 fg44,
5117 } = decode_layers(page, opts, bg_subsample, usize::MAX)?;
5118
5119 let region_opts = RenderOptions {
5120 width: full_w,
5121 height: full_h,
5122 ..*opts
5123 };
5124 let (mask_plane, mask_shift) = resolve_sub4_mask(
5128 page,
5129 bg_subsample,
5130 opts,
5131 mask.as_deref(),
5132 fg_palette.as_ref(),
5133 );
5134 let ctx_mask = mask_plane.get();
5135 let ctx_template = CompositeContext::from_layers(
5138 page,
5139 ®ion_opts,
5140 bg.whole().map(PlaneView::whole),
5141 ctx_mask,
5142 mask_shift,
5143 fg_palette.as_ref(),
5144 blit_map.as_deref().map(Vec::as_slice),
5145 fg44.as_deref(),
5146 &gamma_lut,
5147 (0, 0),
5148 (full_w, full_h),
5149 );
5150 let banded = match &bg {
5153 Background::Banded { image, .. } => Some(image),
5154 _ => None,
5155 };
5156
5157 let out_w = region.width;
5158 let out_h = region.height;
5159 let mut pm = Pixmap::white(out_w, out_h);
5160 let out_stride = out_w as usize * 4;
5161
5162 let region_x1 = region.x.saturating_add(region.width).min(full_w);
5163 let region_y1 = region.y.saturating_add(region.height).min(full_h);
5164 if region_x1 <= region.x || region_y1 <= region.y {
5165 return Ok(Some(pm));
5169 }
5170 let tx0 = region.x / TILE_SIZE;
5171 let ty0 = region.y / TILE_SIZE;
5172 let tx1 = (region_x1 - 1) / TILE_SIZE;
5173 let ty1 = (region_y1 - 1) / TILE_SIZE;
5174
5175 let layers = page.render_layers();
5176 for ty in ty0..=ty1 {
5177 let tile_y0 = ty * TILE_SIZE;
5178 let tile_h = TILE_SIZE.min(full_h - tile_y0);
5179 let mut row_band: Option<(Pixmap, u32)> = None;
5181 for tx in tx0..=tx1 {
5182 if is_cancelled(cancel) {
5183 return Ok(None);
5184 }
5185 let tile_x0 = tx * TILE_SIZE;
5186 let tile_w = TILE_SIZE.min(full_w - tile_x0);
5187 let key: TileKey = (full_w, full_h, tile_x0, tile_y0, opts.bold, opts.mask_aa);
5188
5189 let tile = match layers.get_tile(key) {
5190 Some(t) => t,
5191 None => {
5192 if let Some(image) = banded
5193 && row_band.is_none()
5194 {
5195 let (lo, hi) = bg_rows_needed(
5196 (page.width() as u32, page.height() as u32),
5197 (full_w, full_h),
5198 (image.width, image.height),
5199 tile_y0..tile_y0 + tile_h,
5200 );
5201 row_band = Some((image.rgb_rows(lo..hi)?, lo));
5202 }
5203 let mut tile_ctx = match (banded, &row_band) {
5204 (Some(image), Some((band, lo))) => {
5205 ctx_template.with_bg(Some(PlaneView::band(band, image.height, *lo)))
5206 }
5207 _ => ctx_template,
5208 };
5209 tile_ctx.offset_x = tile_x0;
5210 tile_ctx.offset_y = tile_y0;
5211 tile_ctx.out_w = tile_w;
5212 tile_ctx.out_h = tile_h;
5213 let mut data = vec![0u8; tile_w as usize * tile_h as usize * 4];
5214 composite_into(&tile_ctx, &mut data)?;
5215 let entry = std::sync::Arc::new(TileEntry {
5216 w: tile_w,
5217 h: tile_h,
5218 data,
5219 });
5220 layers.insert_tile(key, entry.clone());
5221 entry
5222 }
5223 };
5224
5225 let ox0 = tile_x0.max(region.x);
5227 let oy0 = tile_y0.max(region.y);
5228 let ox1 = (tile_x0 + tile.w).min(region_x1);
5229 let oy1 = (tile_y0 + tile.h).min(region_y1);
5230 if ox0 >= ox1 || oy0 >= oy1 {
5231 continue;
5232 }
5233 let copy_w = (ox1 - ox0) as usize;
5234 let tile_stride = tile.w as usize * 4;
5235 for y in oy0..oy1 {
5236 let tile_row = (y - tile_y0) as usize;
5237 let tile_col = (ox0 - tile_x0) as usize;
5238 let src_start = tile_row * tile_stride + tile_col * 4;
5239 let dst_row = (y - region.y) as usize;
5240 let dst_col = (ox0 - region.x) as usize;
5241 let dst_start = dst_row * out_stride + dst_col * 4;
5242 pm.data[dst_start..dst_start + copy_w * 4]
5243 .copy_from_slice(&tile.data[src_start..src_start + copy_w * 4]);
5244 }
5245 }
5246 }
5247
5248 Ok(Some(pm))
5249}
5250
5251pub fn render_gray8(page: &DjVuPage, opts: &RenderOptions) -> Result<GrayPixmap, RenderError> {
5260 Ok(render_pixmap(page, opts)?.to_gray8())
5261}
5262
5263#[cfg(feature = "parallel")]
5270pub fn render_pages_parallel(
5271 doc: &crate::djvu_document::DjVuDocument,
5272 dpi: u32,
5273) -> Vec<Result<Pixmap, RenderError>> {
5274 use rayon::prelude::*;
5275
5276 let count = doc.page_count();
5277 (0..count)
5278 .into_par_iter()
5279 .map(|i| {
5280 let page = doc.page(i)?;
5281 let native_dpi = page.dpi() as f32;
5282 let scale = dpi as f32 / native_dpi;
5283 let w = ((page.width() as f32 * scale).round() as u32).max(1);
5284 let h = ((page.height() as f32 * scale).round() as u32).max(1);
5285 let opts = RenderOptions {
5286 width: w,
5287 height: h,
5288 ..Default::default()
5289 };
5290 render_pixmap(page, &opts)
5291 })
5292 .collect()
5293}
5294
5295pub fn render_coarse(page: &DjVuPage, opts: &RenderOptions) -> Result<Option<Pixmap>, RenderError> {
5299 let w = opts.width;
5300 let h = opts.height;
5301
5302 check_output_pixels("render_coarse", page, None, w, h)?;
5303
5304 let bg_subsample = best_iw44_subsample(opts.decode_scale(page));
5305 let bg = decode_background_chunks(page, 1, bg_subsample)?;
5306 if !bg.is_some() {
5307 return Ok(None);
5308 }
5309
5310 let gamma_lut = build_gamma_lut(page.gamma());
5311 let mut pm = Pixmap::white(w, h);
5312
5313 for_each_bg_band(
5314 page,
5315 opts,
5316 &bg,
5317 None,
5318 0,
5319 None,
5320 None,
5321 None,
5322 &gamma_lut,
5323 (0, 0),
5324 (w, h),
5325 |ctx, oy0| composite_into(ctx, band_rows_mut(&mut pm.data, w, oy0, ctx.out_h)),
5326 )?;
5327
5328 Ok(Some(rotate_pixmap(
5329 pm,
5330 combine_rotations(page.rotation(), opts.rotation),
5331 )))
5332}
5333
5334pub fn render_progressive(
5345 page: &DjVuPage,
5346 opts: &RenderOptions,
5347 chunk_n: usize,
5348) -> Result<Pixmap, RenderError> {
5349 let w = opts.width;
5350 let h = opts.height;
5351
5352 check_output_pixels("render_progressive", page, None, w, h)?;
5353
5354 let n_bg44 = page.bg44_chunks().len();
5355 let max_chunk = n_bg44.saturating_sub(1);
5356
5357 if n_bg44 > 0 && chunk_n > max_chunk {
5358 return Err(RenderError::ChunkOutOfRange {
5359 chunk_n,
5360 max: max_chunk,
5361 });
5362 }
5363
5364 let gamma_lut = build_gamma_lut(page.gamma());
5365
5366 let bg_subsample = best_iw44_subsample(opts.decode_scale(page));
5370 let DecodedLayers {
5371 bg,
5372 fg_palette,
5373 mask,
5374 blit_map,
5375 fg44,
5376 } = decode_layers(page, opts, bg_subsample, chunk_n + 1)?;
5377
5378 let mut pm = Pixmap::white(w, h);
5379 for_each_bg_band(
5380 page,
5381 opts,
5382 &bg,
5383 mask.as_deref(),
5384 0,
5385 fg_palette.as_ref(),
5386 blit_map.as_deref().map(Vec::as_slice),
5387 fg44.as_deref(),
5388 &gamma_lut,
5389 (0, 0),
5390 (w, h),
5391 |ctx, oy0| composite_into(ctx, band_rows_mut(&mut pm.data, w, oy0, ctx.out_h)),
5392 )?;
5393
5394 let pm = apply_lanczos_postpass(pm, page, opts, (w, h), (w, h), |native_opts| {
5397 render_progressive(page, native_opts, chunk_n)
5398 })?;
5399
5400 Ok(rotate_pixmap(
5401 pm,
5402 combine_rotations(page.rotation(), opts.rotation),
5403 ))
5404}
5405
5406pub fn progressive_steps(page: &DjVuPage) -> usize {
5413 page.bg44_chunks().len().max(1)
5414}
5415
5416pub fn render_progressive_step(
5424 page: &DjVuPage,
5425 opts: &RenderOptions,
5426 step: usize,
5427) -> Result<Pixmap, RenderError> {
5428 if page.bg44_chunks().is_empty() {
5429 render_pixmap(page, opts)
5430 } else {
5431 render_progressive(page, opts, step)
5432 }
5433}
5434
5435pub struct ProgressiveDecoder<'a> {
5451 page: &'a DjVuPage,
5452 opts: RenderOptions,
5453 w: u32,
5454 h: u32,
5455 gamma_lut: [u8; 256],
5456 bg_subsample: u32,
5457 fg_palette: Option<FgbzPalette>,
5458 mask: Option<Arc<crate::bitmap::Bitmap>>,
5459 blit_map: Option<Arc<Vec<i32>>>,
5460 fg44: Option<Arc<Pixmap>>,
5461 rotation: crate::info::Rotation,
5462 img: Arc<Iw44Image>,
5465 chunks_fed: usize,
5466}
5467
5468impl<'a> ProgressiveDecoder<'a> {
5469 pub fn new(page: &'a DjVuPage, opts: &RenderOptions) -> Result<Self, RenderError> {
5478 check_output_pixels(
5479 "render_progressive_decoder",
5480 page,
5481 None,
5482 opts.width,
5483 opts.height,
5484 )?;
5485 if opts.resampling != Resampling::Bilinear || opts.permissive {
5486 return Err(RenderError::UnsupportedOption(
5487 "ProgressiveDecoder supports only strict Bilinear rendering; \
5488 use render_progressive_all for Lanczos3 / permissive",
5489 ));
5490 }
5491
5492 let gamma_lut = build_gamma_lut(page.gamma());
5493 let bg_subsample = best_iw44_subsample(opts.decode_scale(page));
5494 let ForegroundLayers {
5495 fg_palette,
5496 mask,
5497 blit_map,
5498 fg44,
5499 } = decode_foreground_strict(page)?;
5500 let mask = if opts.bold > 0 {
5501 mask.map(|m| Arc::new(Arc::unwrap_or_clone(m).dilate_n(opts.bold as u32)))
5502 } else {
5503 mask
5504 };
5505 let rotation = combine_rotations(page.rotation(), opts.rotation);
5506
5507 Ok(Self {
5508 page,
5509 opts: opts.clone(),
5510 w: opts.width,
5511 h: opts.height,
5512 gamma_lut,
5513 bg_subsample,
5514 fg_palette,
5515 mask,
5516 blit_map,
5517 fg44,
5518 rotation,
5519 img: Arc::new(Iw44Image::new()),
5520 chunks_fed: 0,
5521 })
5522 }
5523
5524 pub fn push_bg44_chunk(&mut self, chunk: &[u8]) -> Result<Pixmap, RenderError> {
5529 #[cfg(test)]
5530 count_bg44_chunk_decode();
5531 Arc::make_mut(&mut self.img)
5533 .decode_chunk(chunk)
5534 .map_err(RenderError::Iw44)?;
5535 self.chunks_fed += 1;
5536 let bg = Background::from_shared_iw44(&self.img, self.bg_subsample)?;
5537
5538 let mut pm = Pixmap::white(self.w, self.h);
5539 for_each_bg_band(
5540 self.page,
5541 &self.opts,
5542 &bg,
5543 self.mask.as_deref(),
5544 0,
5545 self.fg_palette.as_ref(),
5546 self.blit_map.as_deref().map(Vec::as_slice),
5547 self.fg44.as_deref(),
5548 &self.gamma_lut,
5549 (0, 0),
5550 (self.w, self.h),
5551 |ctx, oy0| composite_into(ctx, band_rows_mut(&mut pm.data, self.w, oy0, ctx.out_h)),
5552 )?;
5553 Ok(rotate_pixmap(pm, self.rotation))
5554 }
5555
5556 pub fn frames_produced(&self) -> usize {
5558 self.chunks_fed
5559 }
5560}
5561
5562pub fn render_progressive_all(
5569 page: &DjVuPage,
5570 opts: &RenderOptions,
5571) -> Result<Vec<Pixmap>, RenderError> {
5572 let steps = progressive_steps(page);
5573 let bg44_chunks = page.bg44_chunks();
5574
5575 let can_stream = steps > 1
5589 && bg44_chunks.len() == steps
5590 && !opts.permissive
5591 && opts.resampling == Resampling::Bilinear
5592 && opts.width != 0
5593 && opts.height != 0;
5594
5595 if can_stream {
5596 let mut dec = ProgressiveDecoder::new(page, opts)?;
5601 let mut frames = Vec::with_capacity(steps);
5602 for chunk in bg44_chunks.iter().take(steps) {
5603 frames.push(dec.push_bg44_chunk(chunk)?);
5604 }
5605 return Ok(frames);
5606 }
5607
5608 let mut frames = Vec::with_capacity(steps);
5609 for step in 0..steps {
5610 frames.push(render_progressive_step(page, opts, step)?);
5611 }
5612 Ok(frames)
5613}
5614
5615#[cfg(test)]
5618mod tests {
5619 use super::*;
5620 use crate::djvu_document::DjVuDocument;
5621
5622 #[test]
5625 fn pixmap_too_large_maps_to_render_output_limit() {
5626 let e = RenderError::from(crate::pixmap::PixmapError::TooLarge {
5627 width: 10000,
5628 height: 10000,
5629 pixels: 100_000_000,
5630 max: Pixmap::MAX_PIXELS,
5631 });
5632 match e {
5633 RenderError::ResourceLimit(x) => {
5634 assert_eq!(
5635 x.axis,
5636 crate::resource_limits::ResourceLimitAxis::RenderOutputPixels
5637 );
5638 assert_eq!((x.found, x.limit), (100_000_000, Pixmap::MAX_PIXELS as u64));
5639 assert_eq!((x.width, x.height), (Some(10000), Some(10000)));
5640 }
5641 other => panic!("expected ResourceLimit, got {other:?}"),
5642 }
5643 }
5644
5645 #[test]
5646 fn pixmap_overflow_maps_to_invalid_dimensions() {
5647 let e = RenderError::from(crate::pixmap::PixmapError::Overflow {
5648 width: u32::MAX,
5649 height: u32::MAX,
5650 });
5651 assert!(matches!(
5652 e,
5653 RenderError::InvalidDimensions {
5654 width: u32::MAX,
5655 height: u32::MAX
5656 }
5657 ));
5658 }
5659
5660 fn assets_path() -> std::path::PathBuf {
5661 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
5662 .join("references/djvujs/library/assets")
5663 }
5664
5665 fn load_doc(filename: &str) -> DjVuDocument {
5667 let data = std::fs::read(assets_path().join(filename))
5668 .unwrap_or_else(|_| panic!("{filename} must exist"));
5669 DjVuDocument::parse(&data).unwrap_or_else(|e| panic!("parse failed: {e}"))
5670 }
5671
5672 fn composite_with_bg(
5678 page: &DjVuPage,
5679 opts: &RenderOptions,
5680 bg: &Background,
5681 offset: (u32, u32),
5682 out: (u32, u32),
5683 rows: bool,
5684 ) -> Vec<u8> {
5685 let gamma_lut = build_gamma_lut(page.gamma());
5686 let DecodedLayers {
5687 bg: _,
5688 fg_palette,
5689 mask,
5690 blit_map,
5691 fg44,
5692 } = decode_layers(page, opts, 1, usize::MAX).unwrap();
5693 let stride = out.0 as usize * 4;
5694 let mut buf = vec![0u8; stride * out.1 as usize];
5695 for_each_bg_band(
5696 page,
5697 opts,
5698 bg,
5699 mask.as_deref(),
5700 0,
5701 fg_palette.as_ref(),
5702 blit_map.as_deref().map(Vec::as_slice),
5703 fg44.as_deref(),
5704 &gamma_lut,
5705 offset,
5706 out,
5707 |ctx, oy0| {
5708 if rows {
5709 composite_rows(ctx, |y, row| {
5710 let at = (y + oy0 as usize) * stride;
5711 buf[at..at + stride].copy_from_slice(row);
5712 })
5713 } else {
5714 composite_into(ctx, band_rows_mut(&mut buf, out.0, oy0, ctx.out_h))
5715 }
5716 },
5717 )
5718 .unwrap();
5719 buf
5720 }
5721
5722 #[test]
5728 fn banded_background_composites_like_the_whole_one() {
5729 let subjects = [
5735 ("chicken.djvu", true),
5736 ("colorbook.djvu", false),
5737 ("history.djvu", false),
5738 ("carte.djvu", false),
5739 ];
5740 for (file, small) in subjects {
5741 let started = std::time::Instant::now();
5742 let doc = load_doc(file);
5743 let page = doc.page(0).unwrap();
5744 let img = page.decoded_bg44().expect("fixture has a BG44 background");
5745 assert!(
5746 img.rgb_band_rows().is_none(),
5747 "{file} is small: the production path must hold it whole"
5748 );
5749 let whole = Background::Whole(Arc::new(img.to_rgb_subsample(1).unwrap()));
5750 let (pw, ph) = (page.width() as u32, page.height() as u32);
5751 let sizes = [(pw, ph), (pw * 7 / 5, ph * 7 / 5), (pw * 5 / 7, ph * 5 / 7)];
5752 let band_sizes: &[u32] = if small { &[9, 37] } else { &[37, 300] };
5753 for (w, h) in sizes {
5754 let opts = RenderOptions {
5755 width: w,
5756 height: h,
5757 ..Default::default()
5758 };
5759 let cases: Vec<((u32, u32), (u32, u32))> = if small {
5763 vec![((0, 0), (w, h)), ((13, 29), (w - 40, h - 61))]
5764 } else {
5765 let (rw, rh) = (200, 333);
5766 vec![((13, 29), (rw, rh)), ((w - rw, h - rh), (rw, rh))]
5767 };
5768 for (offset, out) in cases {
5769 let expect = composite_with_bg(page, &opts, &whole, offset, out, false);
5770 for &band_rows in band_sizes {
5771 let banded = Background::Banded {
5772 image: img.clone(),
5773 band_rows,
5774 };
5775 for rows in [false, true] {
5776 let got = composite_with_bg(page, &opts, &banded, offset, out, rows);
5777 assert!(
5778 got == expect,
5779 "{file} at {w}x{h}, offset {offset:?}, out {out:?}, \
5780 band_rows {band_rows}, rows={rows}: banded composite differs"
5781 );
5782 }
5783 }
5784 }
5785 }
5786 println!("{file}: checked in {:?}", started.elapsed());
5787 }
5788 }
5789
5790 #[test]
5793 fn bg_band_planning_stays_inside_the_plane_and_the_budget() {
5794 let page = (2260u32, 3669u32);
5795 let plane = (754u32, 1223u32);
5796 for full in [(2260, 3669), (3164, 5137), (1614, 2621), (753, 1223)] {
5797 let (lo, hi) = bg_rows_needed(page, full, plane, 0..full.1);
5798 assert_eq!(lo, 0);
5799 assert!(
5800 hi <= plane.1,
5801 "full render at {full:?} reads {hi} > {} rows",
5802 plane.1
5803 );
5804 if full == page {
5805 assert_eq!(hi, plane.1, "a 1:1 render reads every plane row");
5806 }
5807 let mut oy = 0;
5808 while oy < full.1 {
5809 let rows = bg_band_out_rows(page, full, plane, oy, full.1 - oy, 64);
5810 let (lo, hi) = bg_rows_needed(page, full, plane, oy..oy + rows);
5811 assert!(
5812 lo < hi && hi <= plane.1,
5813 "{full:?} band at {oy}: {lo}..{hi}"
5814 );
5815 assert!(
5816 hi - lo <= 64,
5817 "{full:?} band at {oy}: {lo}..{hi} exceeds 64 rows"
5818 );
5819 oy += rows;
5820 }
5821 }
5822 assert_eq!(bg_rows_needed(page, page, plane, 5..5), (0, 0));
5823 }
5824
5825 fn identity_lut() -> [u8; 256] {
5837 core::array::from_fn(|i| i as u8)
5838 }
5839
5840 #[allow(clippy::too_many_arguments)]
5844 fn synth_ctx<'a>(
5845 opts: &'a RenderOptions,
5846 page_w: u32,
5847 page_h: u32,
5848 bg: Option<&'a Pixmap>,
5849 mask: Option<&'a crate::bitmap::Bitmap>,
5850 gamma_lut: &'a [u8; 256],
5851 out_w: u32,
5852 out_h: u32,
5853 ) -> CompositeContext<'a> {
5854 let (fg_x_q24, fg_y_q24) = fg_q24(None, page_w, page_h);
5855 let bg = bg.map(PlaneView::whole);
5856 let (bg_x_q24, bg_y_q24) = bg_q24(bg.map(|b| (b.width(), b.height())), page_w, page_h);
5857 CompositeContext {
5858 opts,
5859 page_w,
5860 page_h,
5861 bg,
5862 bg_x_q24,
5863 bg_y_q24,
5864 mask,
5865 mask_shift: 0,
5866 fg_palette: None,
5867 blit_map: None,
5868 fg44: None,
5869 fg_x_q24,
5870 fg_y_q24,
5871 gamma_lut,
5872 gamma_is_identity: gamma_lut.iter().enumerate().all(|(i, &v)| v == i as u8),
5876 offset_x: 0,
5877 offset_y: 0,
5878 out_w,
5879 out_h,
5880 }
5881 }
5882
5883 #[test]
5884 fn composite_bilevel_one_maps_mask_to_black_and_white() {
5885 let opts = RenderOptions::default();
5887 let mut mask = crate::bitmap::Bitmap::new(8, 1);
5888 mask.set_black(0, 0);
5889 mask.set_black(3, 0);
5890 let lut = identity_lut();
5891 let ctx = synth_ctx(&opts, 8, 1, None, Some(&mask), &lut, 8, 1);
5892
5893 let mut row = vec![0u8; 8 * 4];
5894 composite_rows_bilevel_one(&ctx, 0, FRAC, FRAC, &mut row);
5895
5896 for x in 0..8usize {
5897 let p = &row[x * 4..x * 4 + 4];
5898 if x == 0 || x == 3 {
5899 assert_eq!(p, [0, 0, 0, 255], "foreground pixel at x={x}");
5900 } else {
5901 assert_eq!(p, [255, 255, 255, 255], "background pixel at x={x}");
5902 }
5903 }
5904 }
5905
5906 #[test]
5907 fn composite_bilinear_one_copies_background_1to1_unsubsampled() {
5908 let opts = RenderOptions::default();
5913 let bg = Pixmap::try_new(4, 2, 10, 20, 30, 255).expect("fits the pixmap limit");
5914 let mask = crate::bitmap::Bitmap::new(4, 2); let lut = identity_lut();
5916 let ctx = synth_ctx(&opts, 4, 2, Some(&bg), Some(&mask), &lut, 4, 2);
5917
5918 let mut row = vec![0u8; 4 * 4];
5919 composite_rows_bilinear_one(&ctx, 0, FRAC, FRAC, &mut row, None, &mut Vec::new());
5920
5921 for x in 0..4usize {
5922 let p = &row[x * 4..x * 4 + 4];
5923 assert_eq!(&p[..3], &[10, 20, 30], "background colour at x={x}");
5924 assert_eq!(p[3], 255, "opaque at x={x}");
5925 }
5926 }
5927
5928 #[test]
5929 fn composite_bilinear_one_upsamples_subsampled_background() {
5930 let opts = RenderOptions::default();
5936 let bg = Pixmap::try_new(4, 2, 70, 90, 110, 255).expect("fits the pixmap limit"); let mask = crate::bitmap::Bitmap::new(8, 2); let lut = identity_lut();
5939 let ctx = synth_ctx(&opts, 8, 2, Some(&bg), Some(&mask), &lut, 8, 2);
5940 assert_ne!(
5942 ctx.bg_x_q24,
5943 1 << 24,
5944 "test must exercise the subsampled path"
5945 );
5946
5947 let mut row = vec![0u8; 8 * 4];
5948 composite_rows_bilinear_one(&ctx, 0, FRAC, FRAC, &mut row, None, &mut Vec::new());
5949
5950 for x in 0..8usize {
5951 let p = &row[x * 4..x * 4 + 4];
5952 assert_eq!(&p[..3], &[70, 90, 110], "upsampled bg colour at x={x}");
5953 assert_eq!(p[3], 255, "opaque at x={x}");
5954 }
5955 }
5956
5957 #[test]
5958 fn composite_area_avg_one_averages_uniform_background_on_downscale() {
5959 let opts = RenderOptions::default();
5962 let bg = Pixmap::try_new(4, 2, 40, 80, 120, 255).expect("fits the pixmap limit");
5963 let mask = crate::bitmap::Bitmap::new(4, 2); let lut = identity_lut();
5965 let ctx = synth_ctx(&opts, 4, 2, Some(&bg), Some(&mask), &lut, 2, 1);
5966
5967 let fx_step = 2 * FRAC;
5968 let fy_step = 2 * FRAC;
5969 let bg_fx_step = ((fx_step as u64 * ctx.bg_x_q24) >> 24) as u32;
5970 let bg_fy_step = ((fy_step as u64 * ctx.bg_y_q24) >> 24) as u32;
5971 let xs = precompute_area_avg_x(&ctx, fx_step, bg_fx_step);
5972
5973 let mut row = vec![0u8; 2 * 4];
5974 composite_rows_area_avg_one(
5975 &ctx,
5976 0,
5977 fx_step,
5978 fy_step,
5979 bg_fx_step,
5980 bg_fy_step,
5981 &mut row,
5982 Some(&xs),
5983 );
5984
5985 for x in 0..2usize {
5986 let p = &row[x * 4..x * 4 + 4];
5987 assert_eq!(&p[..3], &[40, 80, 120], "averaged background at x={x}");
5988 assert_eq!(p[3], 255, "opaque at x={x}");
5989 }
5990 }
5991
5992 #[test]
5999 fn fg_q24_maps_endpoints_into_fg_space() {
6000 let fg = Pixmap::white(189, 306);
6001 let (qx, qy) = fg_q24(Some(&fg), 2260, 3669);
6002 assert!(qx > 0 && qy > 0);
6003 let frac = 1u64 << FRACBITS;
6004 let last_x = 2259u64 * frac;
6005 let fg_fx = (last_x * qx) >> 24;
6006 let fg_px = fg_fx >> FRACBITS;
6007 assert_eq!(fg_px, (fg.width as u64) - 1);
6008 let last_y = 3668u64 * frac;
6009 let fg_fy = (last_y * qy) >> 24;
6010 let fg_py = fg_fy >> FRACBITS;
6011 assert_eq!(fg_py, (fg.height as u64) - 1);
6012 }
6013
6014 #[test]
6015 fn fg_q24_returns_zero_when_fg_is_none() {
6016 assert_eq!(fg_q24(None, 100, 100), (0, 0));
6017 }
6018
6019 #[test]
6024 fn bg_q24_maps_non_pow2_subsample() {
6025 let (qx, qy) = bg_q24(Some((754, 1223)), 2260, 3669);
6027 assert_eq!(qx, (1u64 << 24) / 3);
6028 assert_eq!(qy, (1u64 << 24) / 3);
6029
6030 let last_x = 2259u32 * FRAC;
6031 let bg_px = (map_plane_center_frac(last_x, qx) as u64) >> FRACBITS;
6032 assert!(bg_px < 754);
6033 let last_y = 3668u32 * FRAC;
6034 let bg_py = (map_plane_center_frac(last_y, qy) as u64) >> FRACBITS;
6035 assert!(bg_py < 1223);
6036 }
6037
6038 #[test]
6039 fn bg_q24_returns_zero_when_bg_is_none() {
6040 assert_eq!(bg_q24(None, 100, 100), (0, 0));
6041 }
6042
6043 #[test]
6044 fn plane_q24_some_branch_with_zero_dimension_plane() {
6045 let zero_width_fg = Pixmap::try_new(0, 10, 0, 0, 0, 0).expect("fits the pixmap limit");
6050 let (qx, qy) = fg_q24(Some(&zero_width_fg), 100, 100);
6051 assert_eq!(qx, 0); assert_eq!(qy, (10u64 << 24) / 100); }
6054
6055 #[test]
6056 fn fg_q24_uses_integer_horizontal_cell_pitch() {
6057 let fg = Pixmap::white(189, 306);
6058 let (qx, qy) = fg_q24(Some(&fg), 2260, 3669);
6059 assert_eq!(qx, (1u64 << 24) / 12);
6060 assert_eq!(qy, ((306u64) << 24) / 3669);
6061 }
6062
6063 #[test]
6064 fn bg_q24_uses_integer_cell_pitch_for_padded_edges() {
6065 let (qx, qy) = bg_q24(Some((754, 1223)), 2260, 3669);
6066 assert_eq!(qx, (1u64 << 24) / 3);
6067 assert_eq!(qy, (1u64 << 24) / 3);
6068 }
6069
6070 #[test]
6071 fn map_plane_center_frac_aligns_pixel_centers() {
6072 let q24 = (1u64 << 24) / 2;
6075 assert_eq!(map_plane_center_frac(0, q24), 0);
6076 assert_eq!(map_plane_center_frac(FRAC, q24), FRAC / 4);
6077 }
6078
6079 #[test]
6080 fn sample_bilinear_rounds_to_nearest() {
6081 let mut pm = Pixmap::try_new(2, 2, 0, 0, 0, 255).expect("fits the pixmap limit");
6082 pm.set_rgb(1, 1, 255, 255, 255);
6083
6084 assert_eq!(sample_bilinear(&pm, FRAC / 2, FRAC / 2), (64, 64, 64));
6087 }
6088
6089 #[test]
6090 fn sample_nearest_rounds_to_nearest_pixel() {
6091 let mut pm = Pixmap::try_new(2, 1, 10, 20, 30, 255).expect("fits the pixmap limit");
6092 pm.set_rgb(1, 0, 200, 210, 220);
6093
6094 assert_eq!(sample_nearest(&pm, FRAC / 2 - 1, 0), (10, 20, 30));
6095 assert_eq!(sample_nearest(&pm, FRAC / 2, 0), (200, 210, 220));
6096 }
6097
6098 #[test]
6099 fn mask_box_coverage_values() {
6100 use crate::bitmap::Bitmap;
6101 let mut bm = Bitmap::new(4, 1);
6103 bm.set(0, 0, true);
6104 bm.set(2, 0, true);
6105 bm.set(3, 0, true);
6106 let step = 4 * FRAC;
6107 assert_eq!(mask_box_coverage(&bm, 0, 0, step, FRAC), 191);
6108 let mut bm_full = Bitmap::new(2, 2);
6110 bm_full.set(0, 0, true);
6111 bm_full.set(1, 0, true);
6112 bm_full.set(0, 1, true);
6113 bm_full.set(1, 1, true);
6114 assert_eq!(mask_box_coverage(&bm_full, 0, 0, 2 * FRAC, 2 * FRAC), 255);
6115 let bm_empty = Bitmap::new(2, 2);
6117 assert_eq!(mask_box_coverage(&bm_empty, 0, 0, 2 * FRAC, 2 * FRAC), 0);
6118 }
6119
6120 #[test]
6123 fn mask_bilinear_coverage_values() {
6124 use crate::bitmap::Bitmap;
6125 let mut bm = Bitmap::new(4, 1);
6127 bm.set(0, 0, true);
6128 assert_eq!(mask_bilinear_coverage(&bm, 0, 0), 255);
6130 assert_eq!(mask_bilinear_coverage(&bm, 8, 0), 128);
6132 assert_eq!(mask_bilinear_coverage(&bm, 24, 0), 0);
6134 let mut bm_full = Bitmap::new(2, 2);
6136 bm_full.set(0, 0, true);
6137 bm_full.set(1, 0, true);
6138 bm_full.set(0, 1, true);
6139 bm_full.set(1, 1, true);
6140 assert_eq!(mask_bilinear_coverage(&bm_full, 4, 4), 255);
6141 let bm_empty = Bitmap::new(2, 2);
6143 assert_eq!(mask_bilinear_coverage(&bm_empty, 4, 4), 0);
6144 }
6145
6146 #[test]
6150 fn composite_bilevel_one_mask_aa_disabled_matches_nearest_at_upscale() {
6151 let opts = RenderOptions::default();
6152 assert!(!opts.mask_aa);
6153 let mut mask = crate::bitmap::Bitmap::new(4, 1);
6154 mask.set(0, 0, true); let lut = identity_lut();
6156 let ctx = synth_ctx(&opts, 4, 1, None, Some(&mask), &lut, 8, 2);
6157
6158 let fx_step = FRAC / 2; let fy_step = FRAC / 2;
6160 let mut row = vec![0u8; 8 * 4];
6161 composite_rows_bilevel_one(&ctx, 0, fx_step, fy_step, &mut row);
6162
6163 let expected_black = [true, true, false, false, false, false, false, false];
6166 for (x, &is_black) in expected_black.iter().enumerate() {
6167 let p = &row[x * 4..x * 4 + 4];
6168 if is_black {
6169 assert_eq!(p, [0, 0, 0, 255], "expected black at x={x}");
6170 } else {
6171 assert_eq!(p, [255, 255, 255, 255], "expected white at x={x}");
6172 }
6173 }
6174 }
6175
6176 #[test]
6181 fn composite_bilevel_one_mask_aa_enabled_smooths_edge_at_upscale() {
6182 let opts = RenderOptions {
6183 mask_aa: true,
6184 ..Default::default()
6185 };
6186 let mut mask = crate::bitmap::Bitmap::new(4, 1);
6187 mask.set(0, 0, true);
6188 let lut = identity_lut();
6189 let ctx = synth_ctx(&opts, 4, 1, None, Some(&mask), &lut, 8, 2);
6190
6191 let fx_step = FRAC / 2;
6192 let fy_step = FRAC / 2;
6193 let mut row = vec![0u8; 8 * 4];
6194 composite_rows_bilevel_one(&ctx, 0, fx_step, fy_step, &mut row);
6195
6196 assert_eq!(&row[0..4], [0, 0, 0, 255], "x=0 exact sample stays black");
6198 assert_eq!(&row[4..8], [127, 127, 127, 255], "x=1 is a blended gray");
6200 for x in 2..8usize {
6202 assert_eq!(
6203 &row[x * 4..x * 4 + 4],
6204 [255, 255, 255, 255],
6205 "x={x} stays white"
6206 );
6207 }
6208 }
6209
6210 #[test]
6213 fn composite_bilevel_one_mask_aa_is_noop_at_native_scale() {
6214 let mut mask = crate::bitmap::Bitmap::new(4, 1);
6215 mask.set(0, 0, true);
6216 let lut = identity_lut();
6217
6218 let opts_off = RenderOptions::default();
6219 let ctx_off = synth_ctx(&opts_off, 4, 1, None, Some(&mask), &lut, 4, 1);
6220 let mut row_off = vec![0u8; 4 * 4];
6221 composite_rows_bilevel_one(&ctx_off, 0, FRAC, FRAC, &mut row_off);
6222
6223 let opts_on = RenderOptions {
6224 mask_aa: true,
6225 ..Default::default()
6226 };
6227 let ctx_on = synth_ctx(&opts_on, 4, 1, None, Some(&mask), &lut, 4, 1);
6228 let mut row_on = vec![0u8; 4 * 4];
6229 composite_rows_bilevel_one(&ctx_on, 0, FRAC, FRAC, &mut row_on);
6230
6231 assert_eq!(row_off, row_on, "mask_aa must not affect native 1:1 scale");
6232 }
6233
6234 #[test]
6238 fn composite_bilevel_one_mask_aa_is_noop_at_downscale() {
6239 let mut mask = crate::bitmap::Bitmap::new(4, 1);
6240 mask.set(0, 0, true);
6241 mask.set(2, 0, true);
6242 mask.set(3, 0, true);
6243 let lut = identity_lut();
6244
6245 let fx_step = 4 * FRAC; let fy_step = FRAC;
6247
6248 let opts_off = RenderOptions::default();
6249 let ctx_off = synth_ctx(&opts_off, 4, 1, None, Some(&mask), &lut, 1, 1);
6250 let mut row_off = vec![0u8; 4];
6251 composite_rows_bilevel_one(&ctx_off, 0, fx_step, fy_step, &mut row_off);
6252
6253 let opts_on = RenderOptions {
6254 mask_aa: true,
6255 ..Default::default()
6256 };
6257 let ctx_on = synth_ctx(&opts_on, 4, 1, None, Some(&mask), &lut, 1, 1);
6258 let mut row_on = vec![0u8; 4];
6259 composite_rows_bilevel_one(&ctx_on, 0, fx_step, fy_step, &mut row_on);
6260
6261 assert_eq!(row_off, row_on, "mask_aa must not affect downscale");
6262 }
6263
6264 #[test]
6269 fn composite_bilinear_one_mask_aa_disabled_matches_nearest_at_upscale() {
6270 let opts = RenderOptions::default();
6271 let bg = Pixmap::try_new(8, 1, 200, 150, 100, 255).expect("fits the pixmap limit");
6272 let mut mask = crate::bitmap::Bitmap::new(8, 1);
6273 mask.set(0, 0, true); let lut = identity_lut();
6275 let ctx = synth_ctx(&opts, 8, 1, Some(&bg), Some(&mask), &lut, 4, 1);
6276
6277 let fx_step = FRAC / 2; let fy_step = FRAC / 2;
6279 let mut row = vec![0u8; 4 * 4];
6280 composite_rows_bilinear_one(&ctx, 0, fx_step, fy_step, &mut row, None, &mut Vec::new());
6281
6282 assert_eq!(&row[0..4], [0, 0, 0, 255], "ox=0 nearest foreground");
6285 assert_eq!(&row[4..8], [0, 0, 0, 255], "ox=1 nearest foreground");
6286 assert_eq!(&row[8..12], [200, 150, 100, 255], "ox=2 background");
6287 assert_eq!(&row[12..16], [200, 150, 100, 255], "ox=3 background");
6288 }
6289
6290 #[test]
6294 fn composite_bilinear_one_mask_aa_enabled_blends_fg_bg_at_upscale() {
6295 let opts = RenderOptions {
6296 mask_aa: true,
6297 ..Default::default()
6298 };
6299 let bg = Pixmap::try_new(8, 1, 200, 150, 100, 255).expect("fits the pixmap limit");
6300 let mut mask = crate::bitmap::Bitmap::new(8, 1);
6301 mask.set(0, 0, true);
6302 let lut = identity_lut();
6303 let ctx = synth_ctx(&opts, 8, 1, Some(&bg), Some(&mask), &lut, 4, 1);
6304
6305 let fx_step = FRAC / 2;
6306 let fy_step = FRAC / 2;
6307 let mut row = vec![0u8; 4 * 4];
6308 composite_rows_bilinear_one(&ctx, 0, fx_step, fy_step, &mut row, None, &mut Vec::new());
6309
6310 assert_eq!(
6312 &row[0..4],
6313 [0, 0, 0, 255],
6314 "ox=0 exact sample stays foreground"
6315 );
6316 assert_eq!(
6318 &row[4..8],
6319 [100, 75, 50, 255],
6320 "ox=1 is a fg/bg blend, not a hard snap"
6321 );
6322 assert_eq!(&row[8..12], [200, 150, 100, 255], "ox=2 background");
6324 assert_eq!(&row[12..16], [200, 150, 100, 255], "ox=3 background");
6325 }
6326
6327 #[test]
6333 fn composite_bilinear_one_mask_aa_is_noop_when_bg_subsampled_at_native_scale() {
6334 let bg = Pixmap::try_new(4, 1, 200, 150, 100, 255).expect("fits the pixmap limit"); let mut mask = crate::bitmap::Bitmap::new(8, 1);
6336 mask.set(0, 0, true);
6337 let lut = identity_lut();
6338
6339 let opts_off = RenderOptions::default();
6340 let ctx_off = synth_ctx(&opts_off, 8, 1, Some(&bg), Some(&mask), &lut, 8, 1);
6341 assert_ne!(
6342 ctx_off.bg_x_q24,
6343 1 << 24,
6344 "test must exercise subsampled bg"
6345 );
6346 let mut row_off = vec![0u8; 8 * 4];
6347 composite_rows_bilinear_one(&ctx_off, 0, FRAC, FRAC, &mut row_off, None, &mut Vec::new());
6348
6349 let opts_on = RenderOptions {
6350 mask_aa: true,
6351 ..Default::default()
6352 };
6353 let ctx_on = synth_ctx(&opts_on, 8, 1, Some(&bg), Some(&mask), &lut, 8, 1);
6354 let mut row_on = vec![0u8; 8 * 4];
6355 composite_rows_bilinear_one(&ctx_on, 0, FRAC, FRAC, &mut row_on, None, &mut Vec::new());
6356
6357 assert_eq!(
6358 row_off, row_on,
6359 "mask_aa must not affect native 1:1 scale even with a subsampled bg plane"
6360 );
6361 }
6362
6363 #[test]
6367 fn composite_bilinear_one_column_table_matches_fallback() {
6368 let opts = RenderOptions::default();
6369 let mut bg = Pixmap::try_new(3, 2, 0, 0, 0, 255).expect("fits the pixmap limit"); for (i, px) in bg.data.as_chunks_mut::<4>().0.iter_mut().enumerate() {
6371 px[0] = (i * 40) as u8;
6372 px[1] = (i * 25 + 7) as u8;
6373 px[2] = (255 - i * 30) as u8;
6374 }
6375 let mut mask = crate::bitmap::Bitmap::new(8, 2);
6376 mask.set(2, 0, true); let lut = identity_lut();
6378
6379 let mut ctx = synth_ctx(&opts, 8, 2, Some(&bg), Some(&mask), &lut, 16, 4);
6380 ctx.offset_x = 3;
6381
6382 let fx_step = FRAC / 2; let fy_step = FRAC / 2;
6384 let table = precompute_bilinear_x(&ctx, fx_step).expect("bg present");
6385
6386 for oy in 0..4 {
6387 let mut row_table = vec![0u8; 16 * 4];
6388 let mut row_fallback = vec![0u8; 16 * 4];
6389 composite_rows_bilinear_one(
6390 &ctx,
6391 oy,
6392 fx_step,
6393 fy_step,
6394 &mut row_table,
6395 Some(&table),
6396 &mut Vec::new(),
6397 );
6398 composite_rows_bilinear_one(
6399 &ctx,
6400 oy,
6401 fx_step,
6402 fy_step,
6403 &mut row_fallback,
6404 None,
6405 &mut Vec::new(),
6406 );
6407 assert_eq!(
6408 row_table, row_fallback,
6409 "table and fallback sampling must agree at oy={oy}"
6410 );
6411 }
6412 }
6413
6414 #[test]
6419 fn render_pixmap_mask_aa_is_noop_at_native_scale_real_doc() {
6420 let doc = load_doc("boy_jb2.djvu");
6421 let page = doc.page(0).unwrap();
6422 let (w, h) = (page.width() as u32, page.height() as u32);
6423
6424 let opts_off = RenderOptions {
6425 width: w,
6426 height: h,
6427 ..Default::default()
6428 };
6429 let opts_on = RenderOptions {
6430 width: w,
6431 height: h,
6432 mask_aa: true,
6433 ..Default::default()
6434 };
6435 let pm_off = render_pixmap(page, &opts_off).expect("render should succeed");
6436 let pm_on = render_pixmap(page, &opts_on).expect("render should succeed");
6437 assert_eq!(
6438 pm_off.data, pm_on.data,
6439 "mask_aa must be a no-op at native scale"
6440 );
6441 }
6442
6443 #[test]
6446 fn render_pixmap_mask_aa_is_noop_at_downscale_real_doc() {
6447 let doc = load_doc("boy_jb2.djvu");
6448 let page = doc.page(0).unwrap();
6449 let (w, h) = ((page.width() as u32) / 2, (page.height() as u32) / 2);
6450
6451 let opts_off = RenderOptions {
6452 width: w,
6453 height: h,
6454 ..Default::default()
6455 };
6456 let opts_on = RenderOptions {
6457 width: w,
6458 height: h,
6459 mask_aa: true,
6460 ..Default::default()
6461 };
6462 let pm_off = render_pixmap(page, &opts_off).expect("render should succeed");
6463 let pm_on = render_pixmap(page, &opts_on).expect("render should succeed");
6464 assert_eq!(
6465 pm_off.data, pm_on.data,
6466 "mask_aa must be a no-op at downscale"
6467 );
6468 }
6469
6470 #[test]
6475 fn render_pixmap_mask_aa_smooths_edges_at_zoom_real_doc() {
6476 let doc = load_doc("boy_jb2.djvu");
6477 let page = doc.page(0).unwrap();
6478 let (pw, ph) = (page.width() as u32, page.height() as u32);
6479
6480 for &zoom in &[2u32, 4u32] {
6481 let opts_off = RenderOptions {
6482 width: pw * zoom,
6483 height: ph * zoom,
6484 ..Default::default()
6485 };
6486 let opts_on = RenderOptions {
6487 width: pw * zoom,
6488 height: ph * zoom,
6489 mask_aa: true,
6490 ..Default::default()
6491 };
6492 let pm_off = render_pixmap(page, &opts_off).expect("nearest render should succeed");
6493 let pm_on = render_pixmap(page, &opts_on).expect("AA render should succeed");
6494 assert_eq!(pm_off.width, pm_on.width);
6495 assert_eq!(pm_off.height, pm_on.height);
6496
6497 assert_ne!(
6498 pm_off.data, pm_on.data,
6499 "mask_aa=true must change output at {zoom}× zoom"
6500 );
6501 let has_intermediate_gray = pm_on
6502 .data
6503 .as_chunks::<4>()
6504 .0
6505 .iter()
6506 .any(|px| px[0] == px[1] && px[1] == px[2] && px[0] != 0 && px[0] != 255);
6507 assert!(
6508 has_intermediate_gray,
6509 "mask_aa=true should introduce intermediate gray values at {zoom}× zoom"
6510 );
6511 }
6512 }
6513
6514 #[test]
6516 fn render_options_default() {
6517 let opts = RenderOptions::default();
6518 assert_eq!(opts.width, 0);
6519 assert_eq!(opts.height, 0);
6520 assert_eq!(opts.bold, 0);
6521 assert!(!opts.aa);
6522 assert_eq!(opts.resampling, Resampling::Bilinear);
6523 assert!(!opts.mask_aa, "mask_aa must default to false (opt-in)");
6524 }
6525
6526 #[test]
6528 fn render_options_construction() {
6529 let opts = RenderOptions {
6530 width: 400,
6531 height: 300,
6532 bold: 1,
6533 aa: true,
6534 rotation: UserRotation::Cw90,
6535 ..Default::default()
6536 };
6537 assert_eq!(opts.width, 400);
6538 assert_eq!(opts.height, 300);
6539 assert_eq!(opts.bold, 1);
6540 assert!(opts.aa);
6541 assert_eq!(opts.rotation, UserRotation::Cw90);
6542 }
6543
6544 #[test]
6548 fn render_progressive_all_matches_per_frame() {
6549 let doc = load_doc("chicken.djvu");
6550 let page = doc.page(0).unwrap();
6551 assert!(
6554 page.bg44_chunks().len() >= 2,
6555 "need a multi-chunk BG44 page"
6556 );
6557
6558 let opts = RenderOptions {
6559 width: page.width() as u32,
6560 height: page.height() as u32,
6561 resampling: Resampling::Bilinear,
6562 ..Default::default()
6563 };
6564
6565 let all = render_progressive_all(page, &opts).expect("progressive_all");
6566 let steps = progressive_steps(page);
6567 assert_eq!(all.len(), steps);
6568 for (step, frame) in all.iter().enumerate() {
6569 let per_frame = render_progressive_step(page, &opts, step).expect("progressive_step");
6570 assert_eq!(
6571 (frame.width, frame.height),
6572 (per_frame.width, per_frame.height),
6573 "frame {step} dimensions differ"
6574 );
6575 assert!(
6576 frame.data == per_frame.data,
6577 "frame {step} pixels differ between incremental and per-frame paths"
6578 );
6579 }
6580 }
6581
6582 #[test]
6583 fn progressive_decoder_streams_frames_matching_batch() {
6584 let doc = load_doc("chicken.djvu");
6587 let page = doc.page(0).unwrap();
6588 let chunks = page.bg44_chunks();
6589 assert!(chunks.len() >= 2, "need a multi-chunk BG44 page");
6590
6591 let opts = RenderOptions {
6592 width: page.width() as u32,
6593 height: page.height() as u32,
6594 resampling: Resampling::Bilinear,
6595 ..Default::default()
6596 };
6597
6598 let batch = render_progressive_all(page, &opts).expect("progressive_all");
6599
6600 let mut dec = ProgressiveDecoder::new(page, &opts).expect("decoder");
6601 let steps = progressive_steps(page);
6602 for (i, chunk) in chunks.iter().take(steps).enumerate() {
6603 let frame = dec.push_bg44_chunk(chunk).expect("push");
6604 assert_eq!(dec.frames_produced(), i + 1);
6605 assert_eq!(
6606 (frame.width, frame.height),
6607 (batch[i].width, batch[i].height),
6608 "streamed frame {i} dimensions differ"
6609 );
6610 assert!(
6611 frame.data == batch[i].data,
6612 "streamed frame {i} pixels differ from batch progressive_all"
6613 );
6614 }
6615 }
6616
6617 fn assert_progressive_decoder_matches_step(filename: &str) {
6624 let doc = load_doc(filename);
6625 let page = doc.page(0).unwrap();
6626 let chunks = page.bg44_chunks();
6627 assert!(
6628 chunks.len() >= 2,
6629 "{filename}: need a multi-chunk BG44 page"
6630 );
6631
6632 let opts = RenderOptions {
6633 width: page.width() as u32,
6634 height: page.height() as u32,
6635 resampling: Resampling::Bilinear,
6636 ..Default::default()
6637 };
6638
6639 let mut dec = ProgressiveDecoder::new(page, &opts).expect("decoder");
6640 let steps = progressive_steps(page);
6641 for (i, chunk) in chunks.iter().take(steps).enumerate() {
6642 let streamed = dec.push_bg44_chunk(chunk).expect("push");
6643 let stepped = render_progressive_step(page, &opts, i).expect("progressive_step");
6644 assert_eq!(
6645 (streamed.width, streamed.height),
6646 (stepped.width, stepped.height),
6647 "{filename} frame {i} dimensions differ"
6648 );
6649 assert!(
6650 streamed.data == stepped.data,
6651 "{filename}: streamed frame {i} pixels differ from render_progressive_step"
6652 );
6653 }
6654 }
6655
6656 #[test]
6657 fn progressive_decoder_matches_render_progressive_step_chicken() {
6658 assert_progressive_decoder_matches_step("chicken.djvu");
6659 }
6660
6661 #[test]
6662 fn progressive_decoder_matches_render_progressive_step_colorbook() {
6663 assert_progressive_decoder_matches_step("colorbook.djvu");
6664 }
6665
6666 #[test]
6694 fn progressive_decoder_chunk_decodes_are_on_not_on_squared() {
6695 let body = || {
6696 for filename in ["chicken.djvu", "colorbook.djvu"] {
6697 let doc = load_doc(filename);
6698 let page = doc.page(0).unwrap();
6699 let chunks = page.bg44_chunks();
6700 let n = chunks.len();
6701 assert!(n >= 3, "{filename}: need >=3 BG44 chunks");
6702
6703 let opts = RenderOptions {
6704 width: page.width() as u32,
6705 height: page.height() as u32,
6706 resampling: Resampling::Bilinear,
6707 ..Default::default()
6708 };
6709
6710 BG44_CHUNK_DECODES.with(|c| c.set(0));
6713 for step in 0..n {
6714 render_progressive_step(page, &opts, step).expect("progressive_step");
6715 }
6716 let naive = BG44_CHUNK_DECODES.with(|c| c.get());
6717 let expected_naive: usize = (1..=n).sum(); assert_eq!(
6719 naive, expected_naive,
6720 "{filename}: naive per-frame session should decode chunks \
6721 1+2+...+N = {expected_naive} times, got {naive}"
6722 );
6723
6724 BG44_CHUNK_DECODES.with(|c| c.set(0));
6726 let mut dec = ProgressiveDecoder::new(page, &opts).expect("decoder");
6727 for chunk in chunks.iter() {
6728 dec.push_bg44_chunk(chunk).expect("push");
6729 }
6730 let streamed = BG44_CHUNK_DECODES.with(|c| c.get());
6731 assert_eq!(
6732 streamed, n,
6733 "{filename}: stateful session should decode each chunk exactly \
6734 once (O(N) = {n}), got {streamed}"
6735 );
6736
6737 assert!(
6738 naive > streamed,
6739 "{filename}: naive session ({naive} decodes) should strictly \
6740 exceed the streamed session ({streamed} decodes)"
6741 );
6742 }
6743 };
6744
6745 #[cfg(feature = "parallel")]
6746 {
6747 let pool = rayon::ThreadPoolBuilder::new()
6748 .num_threads(1)
6749 .build()
6750 .expect("build single-threaded pool for deterministic thread-local counting");
6751 pool.install(body);
6752 }
6753 #[cfg(not(feature = "parallel"))]
6754 body();
6755 }
6756
6757 #[test]
6758 fn progressive_decoder_rejects_lanczos_and_zero_dims() {
6759 let doc = load_doc("chicken.djvu");
6760 let page = doc.page(0).unwrap();
6761 let mut opts = RenderOptions {
6762 width: page.width() as u32,
6763 height: page.height() as u32,
6764 resampling: Resampling::Lanczos3,
6765 ..Default::default()
6766 };
6767 assert!(matches!(
6768 ProgressiveDecoder::new(page, &opts),
6769 Err(RenderError::UnsupportedOption(_))
6770 ));
6771 opts.resampling = Resampling::Bilinear;
6772 opts.width = 0;
6773 assert!(matches!(
6774 ProgressiveDecoder::new(page, &opts),
6775 Err(RenderError::InvalidDimensions { .. })
6776 ));
6777 }
6778
6779 #[test]
6783 fn render_progressive_all_matches_per_frame_bold() {
6784 let doc = load_doc("chicken.djvu");
6785 let page = doc.page(0).unwrap();
6786 assert!(
6787 page.bg44_chunks().len() >= 2,
6788 "need a multi-chunk BG44 page"
6789 );
6790
6791 let opts = RenderOptions {
6792 width: page.width() as u32,
6793 height: page.height() as u32,
6794 resampling: Resampling::Bilinear,
6795 bold: 2,
6796 ..Default::default()
6797 };
6798
6799 let all = render_progressive_all(page, &opts).expect("progressive_all");
6800 let steps = progressive_steps(page);
6801 assert_eq!(all.len(), steps);
6802 for (step, frame) in all.iter().enumerate() {
6803 let per_frame = render_progressive_step(page, &opts, step).expect("progressive_step");
6804 assert_eq!(
6805 (frame.width, frame.height),
6806 (per_frame.width, per_frame.height),
6807 "frame {step} dimensions differ (bold)"
6808 );
6809 assert!(
6810 frame.data == per_frame.data,
6811 "frame {step} pixels differ between incremental and per-frame paths (bold)"
6812 );
6813 }
6814 }
6815
6816 #[test]
6820 fn evict_render_cache_preserves_output() {
6821 let mut doc = load_doc("chicken.djvu");
6822 let (w, h) = {
6823 let p = doc.page(0).unwrap();
6824 (p.width() as u32, p.height() as u32)
6825 };
6826 let opts = RenderOptions {
6827 width: w,
6828 height: h,
6829 ..Default::default()
6830 };
6831 let first = {
6832 let p = doc.page(0).unwrap();
6833 render_pixmap(p, &opts).unwrap()
6834 };
6835 doc.evict_render_caches();
6836 let second = {
6837 let p = doc.page(0).unwrap();
6838 render_pixmap(p, &opts).unwrap()
6839 };
6840 assert_eq!(
6841 first.data, second.data,
6842 "output changed after cache eviction"
6843 );
6844 }
6845
6846 #[test]
6849 fn enforce_cache_budget_lru_and_correctness() {
6850 let doc = load_doc("colorbook.djvu");
6851 if doc.page_count() < 3 {
6852 return; }
6854 let mut first0 = None;
6856 for i in 0..3 {
6857 let (w, h) = {
6858 let p = doc.page(i).unwrap();
6859 (p.width() as u32, p.height() as u32)
6860 };
6861 let opts = RenderOptions {
6862 width: w,
6863 height: h,
6864 ..Default::default()
6865 };
6866 let p = doc.page(i).unwrap();
6867 let pm = render_pixmap(p, &opts).unwrap();
6868 if i == 0 {
6869 first0 = Some(pm);
6870 }
6871 }
6872 let t0 = doc.page(0).unwrap().render_cache_access_tick();
6874 let t1 = doc.page(1).unwrap().render_cache_access_tick();
6875 let t2 = doc.page(2).unwrap().render_cache_access_tick();
6876 assert!(t0 < t1 && t1 < t2, "LRU ticks not ordered: {t0} {t1} {t2}");
6877
6878 assert!(doc.render_cache_bytes() > 0);
6879 let freed = doc.enforce_cache_budget(1, &[2]);
6881 assert!(freed > 0, "expected some bytes freed");
6882 assert_eq!(
6883 doc.page(0).unwrap().render_cache_bytes(),
6884 0,
6885 "page 0 not evicted"
6886 );
6887 assert_eq!(
6888 doc.page(1).unwrap().render_cache_bytes(),
6889 0,
6890 "page 1 not evicted"
6891 );
6892 assert!(
6893 doc.page(2).unwrap().render_cache_bytes() > 0,
6894 "protected page 2 was evicted"
6895 );
6896
6897 let (w, h) = {
6899 let p = doc.page(0).unwrap();
6900 (p.width() as u32, p.height() as u32)
6901 };
6902 let opts = RenderOptions {
6903 width: w,
6904 height: h,
6905 ..Default::default()
6906 };
6907 let second0 = render_pixmap(doc.page(0).unwrap(), &opts).unwrap();
6908 assert_eq!(first0.unwrap().data, second0.data);
6909 }
6910
6911 #[test]
6917 fn downgrade_render_cache_keeps_downscaled_tier_warm() {
6918 let doc = load_doc("colorbook.djvu");
6919 let (w, h) = {
6920 let p = doc.page(0).unwrap();
6921 (p.width() as u32, p.height() as u32)
6922 };
6923 let opts_s1 = RenderOptions {
6924 width: w,
6925 height: h,
6926 ..Default::default()
6927 };
6928 let opts_s2 = RenderOptions {
6930 width: w / 2,
6931 height: h / 2,
6932 ..Default::default()
6933 };
6934
6935 let first_s1 = render_pixmap(doc.page(0).unwrap(), &opts_s1).unwrap();
6936 let first_s2 = render_pixmap(doc.page(0).unwrap(), &opts_s2).unwrap();
6937 let bytes_before = doc.page(0).unwrap().render_cache_bytes();
6938 assert!(bytes_before > 0);
6939
6940 doc.downgrade_render_caches();
6941 let bytes_after = doc.page(0).unwrap().render_cache_bytes();
6942 assert!(
6943 bytes_after > 0 && bytes_after < bytes_before,
6944 "downgrade should shrink but not zero the cache: before={bytes_before} after={bytes_after}"
6945 );
6946
6947 let second_s2 = render_pixmap(doc.page(0).unwrap(), &opts_s2).unwrap();
6950 assert_eq!(first_s2.data, second_s2.data, "sub=2 output changed");
6951
6952 let second_s1 = render_pixmap(doc.page(0).unwrap(), &opts_s1).unwrap();
6955 assert_eq!(first_s1.data, second_s1.data, "sub=1 output changed");
6956 }
6957
6958 #[test]
6962 fn full_decode_resumed_from_partial_is_byte_identical() {
6963 let doc_a = load_doc("colorbook.djvu");
6964 let (w, h) = {
6965 let p = doc_a.page(0).unwrap();
6966 (p.width() as u32, p.height() as u32)
6967 };
6968 let opts_s4 = RenderOptions {
6970 width: w / 4,
6971 height: h / 4,
6972 ..Default::default()
6973 };
6974 let opts_s1 = RenderOptions {
6975 width: w,
6976 height: h,
6977 ..Default::default()
6978 };
6979 let _ = render_pixmap(doc_a.page(0).unwrap(), &opts_s4).unwrap();
6980 assert!(
6981 doc_a
6982 .page(0)
6983 .unwrap()
6984 .render_layers()
6985 .bg44_partial
6986 .is_computed(),
6987 "sub=4 render must populate the partial tier"
6988 );
6989 let resumed = render_pixmap(doc_a.page(0).unwrap(), &opts_s1).unwrap();
6990
6991 let doc_b = load_doc("colorbook.djvu");
6993 let cold = render_pixmap(doc_b.page(0).unwrap(), &opts_s1).unwrap();
6994
6995 assert_eq!(
6996 resumed.data, cold.data,
6997 "resumed full decode must be byte-identical"
6998 );
6999 }
7000
7001 #[test]
7006 fn tile_cache_back_and_forth_pan_hit_rate() {
7007 let doc = load_doc("colorbook.djvu");
7008 let page = doc.page(0).unwrap();
7009 let (w, h) = (page.width() as u32, page.height() as u32);
7010 let opts = RenderOptions {
7013 width: w * 2,
7014 height: h * 2,
7015 ..Default::default()
7016 };
7017 let vw = 1440u32.min(w * 2);
7021 let vh = 960u32.min(h * 2);
7022 let step = vw / 4;
7023 let max_x = (w * 2).saturating_sub(vw);
7024 let mut xs: Vec<u32> = (0..=(max_x / step)).map(|i| i * step).collect();
7025 let back: Vec<u32> = xs.iter().rev().skip(1).copied().collect();
7026 xs.extend(back);
7027 for &x in &xs {
7028 let _ = render_region_tiled(
7029 page,
7030 RenderRect {
7031 x,
7032 y: 0,
7033 width: vw,
7034 height: vh,
7035 },
7036 &opts,
7037 )
7038 .unwrap();
7039 }
7040 let (hits, misses, evictions) = page.render_layers().tile_cache_stats();
7041 let rate = hits as f64 / (hits + misses).max(1) as f64;
7042 println!(
7043 "tile cache back-and-forth pan: hits={hits} misses={misses} evictions={evictions} hit-rate={:.1}%",
7044 rate * 100.0
7045 );
7046 assert!(
7047 rate > 0.30,
7048 "back-and-forth pan hit rate too low: {:.1}%",
7049 rate * 100.0
7050 );
7051 }
7052
7053 #[cfg(feature = "std")]
7064 #[test]
7065 fn cold_thumbnail_sweep_skips_full_mask_decode() {
7066 let body = || {
7067 let (w, h) = {
7068 let doc = load_doc("colorbook.djvu");
7069 let page = doc.page(0).unwrap();
7070 (page.width() as u32, page.height() as u32)
7071 };
7072 let opts_s4 = RenderOptions {
7073 width: w / 4,
7074 height: h / 4,
7075 ..Default::default()
7076 };
7077 let opts_s1 = RenderOptions {
7078 width: w,
7079 height: h,
7080 ..Default::default()
7081 };
7082
7083 let doc_warm = load_doc("colorbook.djvu");
7088 let page_warm = doc_warm.page(0).unwrap();
7089 let _ = render_pixmap(page_warm, &opts_s1).unwrap();
7090 let reference = render_pixmap(page_warm, &opts_s4).unwrap();
7091
7092 let doc_cold = load_doc("colorbook.djvu");
7096 let page_cold = doc_cold.page(0).unwrap();
7097 JB2_MASK_DECODES.with(|c| c.set(0));
7098 let cold_s4 = render_pixmap(page_cold, &opts_s4).unwrap();
7099 assert_eq!(
7100 JB2_MASK_DECODES.with(|c| c.get()),
7101 0,
7102 "cold sub>=4 render must not run the full-resolution JB2 decode"
7103 );
7104
7105 assert_eq!(
7106 cold_s4.data, reference.data,
7107 "cold sub4 thumbnail-style render must match the warm-mask-sub4 render"
7108 );
7109 };
7110
7111 #[cfg(feature = "parallel")]
7112 {
7113 let pool = rayon::ThreadPoolBuilder::new()
7114 .num_threads(1)
7115 .build()
7116 .expect("build single-threaded pool for deterministic thread-local counting");
7117 pool.install(body);
7118 }
7119 #[cfg(not(feature = "parallel"))]
7120 body();
7121 }
7122
7123 #[test]
7134 fn downgrade_retains_sub4_mask_and_skips_jb2_decode() {
7135 let body = || {
7136 let doc = load_doc("colorbook.djvu");
7137 let (w, h) = {
7138 let p = doc.page(0).unwrap();
7139 (p.width() as u32, p.height() as u32)
7140 };
7141 let opts_s4 = RenderOptions {
7142 width: w / 4,
7143 height: h / 4,
7144 ..Default::default()
7145 };
7146 let opts_s1 = RenderOptions {
7147 width: w,
7148 height: h,
7149 ..Default::default()
7150 };
7151
7152 let first_s4 = render_pixmap(doc.page(0).unwrap(), &opts_s4).unwrap();
7155 let first_s1 = render_pixmap(doc.page(0).unwrap(), &opts_s1).unwrap();
7156 doc.downgrade_render_caches();
7157 assert!(
7158 doc.page(0)
7159 .unwrap()
7160 .render_layers()
7161 .mask_sub4_cached()
7162 .is_some(),
7163 "downgrade must retain mask_sub4"
7164 );
7165
7166 JB2_MASK_DECODES.with(|c| c.set(0));
7169 let second_s4 = render_pixmap(doc.page(0).unwrap(), &opts_s4).unwrap();
7170 assert_eq!(
7171 JB2_MASK_DECODES.with(|c| c.get()),
7172 0,
7173 "warm sub4 re-render after downgrade must not re-run the JB2 decode"
7174 );
7175 assert_eq!(first_s4.data, second_s4.data, "sub=4 output changed");
7176
7177 JB2_MASK_DECODES.with(|c| c.set(0));
7180 let second_s1 = render_pixmap(doc.page(0).unwrap(), &opts_s1).unwrap();
7181 assert!(
7182 JB2_MASK_DECODES.with(|c| c.get()) > 0,
7183 "full-res re-render after downgrade must cold-decode the mask"
7184 );
7185 assert_eq!(first_s1.data, second_s1.data, "sub=1 output changed");
7186 };
7187
7188 #[cfg(feature = "parallel")]
7189 {
7190 let pool = rayon::ThreadPoolBuilder::new()
7191 .num_threads(1)
7192 .build()
7193 .expect("build single-threaded pool for deterministic thread-local counting");
7194 pool.install(body);
7195 }
7196 #[cfg(not(feature = "parallel"))]
7197 body();
7198 }
7199
7200 #[test]
7207 fn downgraded_sub4_with_bold_still_full_decodes() {
7208 let body = || {
7209 let doc = load_doc("colorbook.djvu");
7210 let (w, h) = {
7211 let p = doc.page(0).unwrap();
7212 (p.width() as u32, p.height() as u32)
7213 };
7214 let opts_bold = RenderOptions {
7215 width: w / 4,
7216 height: h / 4,
7217 bold: 1,
7218 ..Default::default()
7219 };
7220 let first = render_pixmap(doc.page(0).unwrap(), &opts_bold).unwrap();
7221 let opts_s4 = RenderOptions {
7223 width: w / 4,
7224 height: h / 4,
7225 ..Default::default()
7226 };
7227 let _ = render_pixmap(doc.page(0).unwrap(), &opts_s4).unwrap();
7228 doc.downgrade_render_caches();
7229
7230 JB2_MASK_DECODES.with(|c| c.set(0));
7231 let second = render_pixmap(doc.page(0).unwrap(), &opts_bold).unwrap();
7232 assert!(
7233 JB2_MASK_DECODES.with(|c| c.get()) > 0,
7234 "bold render must not take the retained-sub4 shortcut"
7235 );
7236 assert_eq!(first.data, second.data, "bold sub=4 output changed");
7237 };
7238
7239 #[cfg(feature = "parallel")]
7240 {
7241 let pool = rayon::ThreadPoolBuilder::new()
7242 .num_threads(1)
7243 .build()
7244 .expect("build single-threaded pool for deterministic thread-local counting");
7245 pool.install(body);
7246 }
7247 #[cfg(not(feature = "parallel"))]
7248 body();
7249 }
7250
7251 #[test]
7255 fn enforce_cache_budget_with_downgrade_matches_budget_and_output() {
7256 let doc = load_doc("colorbook.djvu");
7257 if doc.page_count() < 3 {
7258 return;
7259 }
7260 let mut expected = Vec::new();
7261 for i in 0..3 {
7262 let (w, h) = {
7263 let p = doc.page(i).unwrap();
7264 (p.width() as u32, p.height() as u32)
7265 };
7266 let opts = RenderOptions {
7267 width: w,
7268 height: h,
7269 ..Default::default()
7270 };
7271 let p = doc.page(i).unwrap();
7272 expected.push(render_pixmap(p, &opts).unwrap());
7273 }
7274
7275 let total_before = doc.render_cache_bytes();
7276 assert!(total_before > 0);
7277 let budget = total_before / 2;
7278 let opts = crate::djvu_document::CacheBudgetOptions {
7279 downgrade_before_drop: true,
7280 };
7281 let _freed = doc.enforce_cache_budget_with(budget, &[], opts);
7282 assert!(
7283 doc.render_cache_bytes() <= budget,
7284 "cache not held under budget: {} > {}",
7285 doc.render_cache_bytes(),
7286 budget
7287 );
7288
7289 for (i, expected_pm) in expected.iter().enumerate().take(3) {
7292 let (w, h) = {
7293 let p = doc.page(i).unwrap();
7294 (p.width() as u32, p.height() as u32)
7295 };
7296 let opts = RenderOptions {
7297 width: w,
7298 height: h,
7299 ..Default::default()
7300 };
7301 let pm = render_pixmap(doc.page(i).unwrap(), &opts).unwrap();
7302 assert_eq!(expected_pm.data, pm.data, "page {i} output changed");
7303 }
7304 }
7305
7306 #[test]
7308 fn fit_to_width_preserves_aspect() {
7309 let doc = load_doc("chicken.djvu");
7310 let page = doc.page(0).unwrap();
7311 let pw = page.width() as u32;
7312 let ph = page.height() as u32;
7313
7314 let opts = RenderOptions::fit_to_width(page, 800);
7315 assert_eq!(opts.width, 800);
7316 let expected_h = ((ph as f64 * 800.0) / pw as f64).round() as u32;
7317 assert_eq!(opts.height, expected_h);
7318 assert!((opts.decode_scale(page) - 800.0 / pw as f32).abs() < 0.01);
7321 }
7322
7323 #[test]
7325 fn fit_to_height_preserves_aspect() {
7326 let doc = load_doc("chicken.djvu");
7327 let page = doc.page(0).unwrap();
7328 let pw = page.width() as u32;
7329 let ph = page.height() as u32;
7330
7331 let opts = RenderOptions::fit_to_height(page, 600);
7332 assert_eq!(opts.height, 600);
7333 let expected_w = ((pw as f64 * 600.0) / ph as f64).round() as u32;
7334 assert_eq!(opts.width, expected_w);
7335 assert!((opts.decode_scale(page) - 600.0 / ph as f32).abs() < 0.01);
7338 }
7339
7340 #[test]
7342 fn fit_to_box_constrains_both() {
7343 let doc = load_doc("chicken.djvu");
7344 let page = doc.page(0).unwrap();
7345
7346 let opts = RenderOptions::fit_to_box(page, 10000, 100);
7348 assert!(opts.width <= 10000);
7349 assert!(opts.height <= 100);
7350 assert!(opts.width > 0 && opts.height > 0);
7351
7352 let opts = RenderOptions::fit_to_box(page, 100, 10000);
7354 assert!(opts.width <= 100);
7355 assert!(opts.height <= 10000);
7356 assert!(opts.width > 0 && opts.height > 0);
7357 }
7358
7359 #[test]
7361 fn fit_to_box_square() {
7362 let doc = load_doc("chicken.djvu");
7363 let page = doc.page(0).unwrap();
7364
7365 let opts = RenderOptions::fit_to_box(page, 500, 500);
7366 assert!(opts.width <= 500);
7367 assert!(opts.height <= 500);
7368 assert!(opts.width >= 490 || opts.height >= 490);
7370 }
7371
7372 #[test]
7374 fn fit_to_width_rotation_aware() {
7375 let doc = load_doc("boy_jb2_rotate90.djvu");
7377 let page = doc.page(0).unwrap();
7378 let pw = page.width() as u32;
7379 let ph = page.height() as u32;
7380 let (dw, dh) = (ph, pw);
7382
7383 let opts = RenderOptions::fit_to_width(page, 400);
7384 assert_eq!(opts.width, 400);
7385 let expected_h = ((dh as f64 * 400.0) / dw as f64).round() as u32;
7386 assert_eq!(opts.height, expected_h);
7387 }
7388
7389 #[test]
7391 fn render_into_invalid_dimensions() {
7392 let doc = load_doc("chicken.djvu");
7393 let page = doc.page(0).unwrap();
7394
7395 let opts = RenderOptions {
7396 width: 0,
7397 height: 100,
7398 ..Default::default()
7399 };
7400 let mut buf = vec![0u8; 400];
7401 let err = render_into(page, &opts, &mut buf).unwrap_err();
7402 assert!(
7403 matches!(err, RenderError::InvalidDimensions { .. }),
7404 "expected InvalidDimensions, got {err:?}"
7405 );
7406 }
7407
7408 #[test]
7410 fn render_into_buf_too_small() {
7411 let doc = load_doc("chicken.djvu");
7412 let page = doc.page(0).unwrap();
7413
7414 let opts = RenderOptions {
7415 width: 10,
7416 height: 10,
7417 ..Default::default()
7418 };
7419 let mut buf = vec![0u8; 10]; let err = render_into(page, &opts, &mut buf).unwrap_err();
7421 assert!(
7422 matches!(err, RenderError::BufTooSmall { need: 400, got: 10 }),
7423 "expected BufTooSmall, got {err:?}"
7424 );
7425 }
7426
7427 #[test]
7432 fn render_into_fills_buffer_no_alloc() {
7433 let doc = load_doc("chicken.djvu");
7434 let page = doc.page(0).unwrap();
7435
7436 let w = 50u32;
7437 let h = 40u32;
7438 let opts = RenderOptions {
7439 width: w,
7440 height: h,
7441 ..Default::default()
7442 };
7443 let mut buf = vec![0u8; (w * h * 4) as usize];
7444 render_into(page, &opts, &mut buf).expect("render_into should succeed");
7445
7446 assert!(
7448 buf.iter().any(|&b| b != 0),
7449 "rendered buffer should contain non-zero pixels"
7450 );
7451 }
7452
7453 #[test]
7455 fn render_into_reuse_buffer() {
7456 let doc = load_doc("chicken.djvu");
7457 let page = doc.page(0).unwrap();
7458
7459 let w = 30u32;
7460 let h = 20u32;
7461 let opts = RenderOptions {
7462 width: w,
7463 height: h,
7464 ..Default::default()
7465 };
7466 let mut buf = vec![0u8; (w * h * 4) as usize];
7467
7468 render_into(page, &opts, &mut buf).expect("first render_into should succeed");
7470 let first = buf.clone();
7471
7472 render_into(page, &opts, &mut buf).expect("second render_into should succeed");
7474 assert_eq!(
7475 first, buf,
7476 "repeated render_into should produce identical output"
7477 );
7478 }
7479
7480 #[test]
7483 fn gamma_lut_standard_is_identity() {
7484 let lut = build_gamma_lut(2.2);
7485 for (i, &val) in lut.iter().enumerate() {
7486 assert_eq!(
7487 val, i as u8,
7488 "gamma=2.2 LUT at {i}: expected {i}, got {val}"
7489 );
7490 }
7491 }
7492
7493 #[test]
7497 fn gamma_lut_linear_source_brightens() {
7498 let lut_linear = build_gamma_lut(1.0); let mid = 128u8;
7500 let corrected = lut_linear[mid as usize];
7501 assert!(
7502 corrected > mid,
7503 "linear-source LUT at mid ({corrected}) should be brighter than {mid}"
7504 );
7505 }
7506
7507 #[test]
7509 fn gamma_lut_zero_is_identity() {
7510 let lut = build_gamma_lut(0.0);
7511 for (i, &val) in lut.iter().enumerate() {
7512 assert_eq!(val, i as u8, "zero gamma should produce identity LUT");
7513 }
7514 }
7515
7516 #[test]
7519 fn render_coarse_returns_pixmap() {
7520 let doc = load_doc("chicken.djvu");
7521 let page = doc.page(0).unwrap();
7522
7523 let opts = RenderOptions {
7524 width: 60,
7525 height: 80,
7526 ..Default::default()
7527 };
7528
7529 let result = render_coarse(page, &opts).expect("render_coarse should succeed");
7530 if let Some(pm) = result {
7532 assert_eq!(pm.width, 60);
7533 assert_eq!(pm.height, 80);
7534 assert_eq!(pm.data.len(), 60 * 80 * 4);
7535 }
7536 }
7538
7539 #[test]
7541 fn render_progressive_each_chunk() {
7542 let doc = load_doc("boy.djvu");
7544 let page = doc.page(0).unwrap();
7545
7546 let opts = RenderOptions {
7547 width: 80,
7548 height: 100,
7549 ..Default::default()
7550 };
7551
7552 let n_bg44 = page.bg44_chunks().len();
7553
7554 for chunk_n in 0..n_bg44 {
7555 let pm = render_progressive(page, &opts, chunk_n)
7556 .unwrap_or_else(|e| panic!("render_progressive chunk {chunk_n} failed: {e}"));
7557 assert_eq!(pm.width, 80);
7558 assert_eq!(pm.height, 100);
7559 assert_eq!(pm.data.len(), 80 * 100 * 4);
7560 assert!(
7562 pm.data.iter().any(|&b| b != 0),
7563 "chunk {chunk_n}: rendered frame should not be all-zero"
7564 );
7565 }
7566 }
7567
7568 #[test]
7582 fn render_progressive_step_handles_zero_length_bg44_chunk() {
7583 let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7584 .join("tests/corpus/watchmaker.djvu");
7585 let data = std::fs::read(&path).expect("watchmaker.djvu must exist");
7586 let doc = DjVuDocument::parse(&data).expect("parse failed");
7587 let page = doc.page(0).unwrap();
7588
7589 let chunks = page.bg44_chunks();
7590 assert_eq!(
7591 chunks.len(),
7592 4,
7593 "expected 4 BG44 chunks on watchmaker page 0"
7594 );
7595 assert_eq!(
7596 chunks[2].len(),
7597 2,
7598 "chunk 2 should be the zero-payload [serial, slices] header this regression covers"
7599 );
7600
7601 let opts = RenderOptions {
7602 width: page.width() as u32,
7603 height: page.height() as u32,
7604 resampling: Resampling::Bilinear,
7605 ..Default::default()
7606 };
7607
7608 for step in 0..progressive_steps(page) {
7609 render_progressive_step(page, &opts, step)
7610 .unwrap_or_else(|e| panic!("step {step} should succeed, got {e}"));
7611 }
7612 render_progressive_all(page, &opts).expect("progressive_all should succeed");
7613 render_pixmap(page, &opts).expect("render_pixmap should succeed");
7614
7615 let mut dec = ProgressiveDecoder::new(page, &opts).expect("decoder");
7616 for chunk in &chunks {
7617 dec.push_bg44_chunk(chunk)
7618 .expect("ProgressiveDecoder should also handle the zero-length chunk");
7619 }
7620 }
7621
7622 #[test]
7625 fn render_progressive_all_seals_chunk_loop() {
7626 let doc = load_doc("boy.djvu");
7627 let page = doc.page(0).unwrap();
7628 let opts = RenderOptions {
7629 width: 80,
7630 height: 100,
7631 ..Default::default()
7632 };
7633
7634 let steps = progressive_steps(page);
7636 assert_eq!(steps, page.bg44_chunks().len().max(1));
7637
7638 let frames = render_progressive_all(page, &opts).expect("progressive_all must succeed");
7639 assert_eq!(frames.len(), steps, "one frame per progressive step");
7640
7641 let full = render_pixmap(page, &opts).expect("render_pixmap must succeed");
7642 assert_eq!(
7643 frames.last().unwrap().data,
7644 full.data,
7645 "final progressive frame must equal the full render"
7646 );
7647 let first = render_progressive_step(page, &opts, 0).expect("step 0 must succeed");
7649 assert_eq!(first.data, frames[0].data);
7650 }
7651
7652 #[cfg(feature = "std")]
7658 #[test]
7659 fn render_progressive_ignores_mask_sub4_warmth() {
7660 let opts = RenderOptions {
7664 width: 61,
7665 height: 83,
7666 ..Default::default()
7667 };
7668 let cold = {
7669 let doc = load_doc("colorbook.djvu");
7670 let page = doc.page(0).unwrap();
7671 render_progressive_step(page, &opts, 1).unwrap()
7672 };
7673 let doc = load_doc("colorbook.djvu");
7674 let page = doc.page(0).unwrap();
7675 let _ = render_pixmap(page, &opts).unwrap();
7678 assert!(
7679 page.render_layers().mask_sub4_cached().is_some(),
7680 "precondition: the full render must have retained the sub4 mask"
7681 );
7682 let warm = render_progressive_step(page, &opts, 1).unwrap();
7683 assert_eq!(
7684 cold.data, warm.data,
7685 "progressive frame changed with cache warmth"
7686 );
7687 }
7688
7689 #[test]
7691 fn render_progressive_chunk_out_of_range() {
7692 let doc = load_doc("boy.djvu");
7693 let page = doc.page(0).unwrap();
7694
7695 let opts = RenderOptions {
7696 width: 40,
7697 height: 50,
7698 ..Default::default()
7699 };
7700
7701 let n_bg44 = page.bg44_chunks().len();
7702 if n_bg44 == 0 {
7703 return;
7705 }
7706
7707 let err = render_progressive(page, &opts, n_bg44 + 10).unwrap_err();
7708 assert!(
7709 matches!(err, RenderError::ChunkOutOfRange { .. }),
7710 "expected ChunkOutOfRange, got {err:?}"
7711 );
7712 }
7713
7714 #[test]
7719 fn render_pixmap_gamma_differs_from_identity() {
7720 let doc = load_doc("chicken.djvu");
7721 let page = doc.page(0).unwrap();
7722
7723 let w = 40u32;
7724 let h = 53u32; let opts = RenderOptions {
7727 width: w,
7728 height: h,
7729 ..Default::default()
7730 };
7731
7732 let pm_gamma = render_pixmap(page, &opts).expect("render with gamma should succeed");
7734
7735 let lut_identity = build_gamma_lut(1.0);
7737 let pm_identity = render_pixmap(page, &opts).expect("render for identity should succeed");
7738 for i in 0..pm_identity.data.len().saturating_sub(3) {
7740 if i % 4 != 3 {
7741 let _ = lut_identity[pm_identity.data[i] as usize];
7743 }
7744 }
7745
7746 assert_eq!(pm_gamma.width, w);
7755 assert_eq!(pm_gamma.height, h);
7756 assert!(
7757 pm_gamma.data.iter().any(|&b| b != 255),
7758 "should have non-white pixels"
7759 );
7760 }
7761
7762 #[test]
7764 fn render_bilevel_page_has_black_pixels() {
7765 let doc = load_doc("boy_jb2.djvu");
7766 let page = doc.page(0).unwrap();
7767
7768 let opts = RenderOptions {
7769 width: 60,
7770 height: 80,
7771 ..Default::default()
7772 };
7773
7774 let pm = render_pixmap(page, &opts).expect("render bilevel should succeed");
7775 assert_eq!(pm.width, 60);
7776 assert_eq!(pm.height, 80);
7777 assert!(
7779 pm.data
7780 .as_chunks::<4>()
7781 .0
7782 .iter()
7783 .any(|px| px[0] == 0 && px[1] == 0 && px[2] == 0),
7784 "bilevel page should contain black pixels"
7785 );
7786 }
7787
7788 #[test]
7790 fn render_with_aa() {
7791 let doc = load_doc("chicken.djvu");
7792 let page = doc.page(0).unwrap();
7793
7794 let opts = RenderOptions {
7795 width: 40,
7796 height: 54,
7797 aa: true,
7798 ..Default::default()
7799 };
7800 let pm = render_pixmap(page, &opts).expect("render with AA should succeed");
7802 assert_eq!(pm.width, 20);
7804 assert_eq!(pm.height, 27);
7805 }
7806
7807 #[test]
7810 fn rotate_pixmap_none_is_identity() {
7811 let mut pm = Pixmap::white(3, 2);
7812 pm.set_rgb(0, 0, 255, 0, 0);
7813 let rotated = rotate_pixmap(pm.clone(), crate::info::Rotation::None);
7814 assert_eq!(rotated.width, 3);
7815 assert_eq!(rotated.height, 2);
7816 assert_eq!(rotated.get_rgb(0, 0), (255, 0, 0));
7817 }
7818
7819 #[test]
7820 fn rotate_pixmap_cw90_swaps_dims() {
7821 let mut pm = Pixmap::white(4, 2);
7822 pm.set_rgb(0, 0, 255, 0, 0); let rotated = rotate_pixmap(pm, crate::info::Rotation::Cw90);
7824 assert_eq!(rotated.width, 2);
7825 assert_eq!(rotated.height, 4);
7826 assert_eq!(rotated.get_rgb(1, 0), (255, 0, 0));
7828 }
7829
7830 #[test]
7831 fn rotate_pixmap_180_preserves_dims() {
7832 let mut pm = Pixmap::white(3, 2);
7833 pm.set_rgb(0, 0, 255, 0, 0); let rotated = rotate_pixmap(pm, crate::info::Rotation::Rot180);
7835 assert_eq!(rotated.width, 3);
7836 assert_eq!(rotated.height, 2);
7837 assert_eq!(rotated.get_rgb(2, 1), (255, 0, 0));
7838 }
7839
7840 #[test]
7841 fn rotate_pixmap_ccw90_swaps_dims() {
7842 let mut pm = Pixmap::white(4, 2);
7843 pm.set_rgb(0, 0, 255, 0, 0); let rotated = rotate_pixmap(pm, crate::info::Rotation::Ccw90);
7845 assert_eq!(rotated.width, 2);
7846 assert_eq!(rotated.height, 4);
7847 assert_eq!(rotated.get_rgb(0, 3), (255, 0, 0));
7849 }
7850
7851 #[test]
7852 fn render_pixmap_rotation_90_swaps_dimensions() {
7853 let doc = load_doc("boy_jb2_rotate90.djvu");
7854 let page = doc.page(0).expect("page 0");
7855 let orig_w = page.width();
7856 let orig_h = page.height();
7857 let opts = RenderOptions {
7858 width: orig_w as u32,
7859 height: orig_h as u32,
7860 ..Default::default()
7861 };
7862 let pm = render_pixmap(page, &opts).expect("render should succeed");
7863 assert_eq!(
7865 pm.width, orig_h as u32,
7866 "rotated width should be original height"
7867 );
7868 assert_eq!(
7869 pm.height, orig_w as u32,
7870 "rotated height should be original width"
7871 );
7872 }
7873
7874 #[test]
7875 fn render_pixmap_rotation_180_preserves_dimensions() {
7876 let doc = load_doc("boy_jb2_rotate180.djvu");
7877 let page = doc.page(0).expect("page 0");
7878 let orig_w = page.width();
7879 let orig_h = page.height();
7880 let opts = RenderOptions {
7881 width: orig_w as u32,
7882 height: orig_h as u32,
7883 ..Default::default()
7884 };
7885 let pm = render_pixmap(page, &opts).expect("render should succeed");
7886 assert_eq!(pm.width, orig_w as u32);
7887 assert_eq!(pm.height, orig_h as u32);
7888 }
7889
7890 #[test]
7891 fn render_pixmap_rotation_270_swaps_dimensions() {
7892 let doc = load_doc("boy_jb2_rotate270.djvu");
7893 let page = doc.page(0).expect("page 0");
7894 let orig_w = page.width();
7895 let orig_h = page.height();
7896 let opts = RenderOptions {
7897 width: orig_w as u32,
7898 height: orig_h as u32,
7899 ..Default::default()
7900 };
7901 let pm = render_pixmap(page, &opts).expect("render should succeed");
7902 assert_eq!(
7903 pm.width, orig_h as u32,
7904 "rotated width should be original height"
7905 );
7906 assert_eq!(
7907 pm.height, orig_w as u32,
7908 "rotated height should be original width"
7909 );
7910 }
7911
7912 #[test]
7916 fn combine_rotations_identity() {
7917 use crate::info::Rotation;
7918 assert_eq!(
7919 combine_rotations(Rotation::None, UserRotation::None),
7920 Rotation::None
7921 );
7922 }
7923
7924 #[test]
7925 fn combine_rotations_info_only() {
7926 use crate::info::Rotation;
7927 assert_eq!(
7928 combine_rotations(Rotation::Cw90, UserRotation::None),
7929 Rotation::Cw90
7930 );
7931 }
7932
7933 #[test]
7934 fn combine_rotations_user_only() {
7935 use crate::info::Rotation;
7936 assert_eq!(
7937 combine_rotations(Rotation::None, UserRotation::Ccw90),
7938 Rotation::Ccw90
7939 );
7940 }
7941
7942 #[test]
7943 fn combine_rotations_sum() {
7944 use crate::info::Rotation;
7945 assert_eq!(
7947 combine_rotations(Rotation::Cw90, UserRotation::Cw90),
7948 Rotation::Rot180
7949 );
7950 assert_eq!(
7952 combine_rotations(Rotation::Cw90, UserRotation::Ccw90),
7953 Rotation::None
7954 );
7955 assert_eq!(
7957 combine_rotations(Rotation::Rot180, UserRotation::Rot180),
7958 Rotation::None
7959 );
7960 }
7961
7962 #[test]
7964 fn user_rotation_cw90_swaps_dimensions() {
7965 let doc = load_doc("chicken.djvu");
7966 let page = doc.page(0).unwrap();
7967 let pw = page.width() as u32;
7968 let ph = page.height() as u32;
7969
7970 let opts = RenderOptions {
7971 width: pw,
7972 height: ph,
7973 rotation: UserRotation::Cw90,
7974 ..Default::default()
7975 };
7976 let pm = render_pixmap(page, &opts).expect("render");
7977 assert_eq!(pm.width, ph, "user Cw90 should swap: width becomes height");
7978 assert_eq!(pm.height, pw, "user Cw90 should swap: height becomes width");
7979 }
7980
7981 #[test]
7983 fn user_rotation_180_preserves_dimensions() {
7984 let doc = load_doc("chicken.djvu");
7985 let page = doc.page(0).unwrap();
7986 let pw = page.width() as u32;
7987 let ph = page.height() as u32;
7988
7989 let opts = RenderOptions {
7990 width: pw,
7991 height: ph,
7992 rotation: UserRotation::Rot180,
7993 ..Default::default()
7994 };
7995 let pm = render_pixmap(page, &opts).expect("render");
7996 assert_eq!(pm.width, pw);
7997 assert_eq!(pm.height, ph);
7998 }
7999
8000 #[test]
8002 fn user_rotation_default_is_none() {
8003 assert_eq!(UserRotation::default(), UserRotation::None);
8004 let opts = RenderOptions::default();
8005 assert_eq!(opts.rotation, UserRotation::None);
8006 }
8007
8008 #[test]
8011 fn fgbz_palette_page_renders_multiple_colors() {
8012 let doc = load_doc("irish.djvu");
8014 let page = doc.page(0).expect("page 0");
8015 let w = page.width() as u32;
8016 let h = page.height() as u32;
8017 let opts = RenderOptions {
8018 width: w,
8019 height: h,
8020 ..Default::default()
8021 };
8022 let pm = render_pixmap(page, &opts).expect("render should succeed");
8023
8024 let mut fg_colors = std::collections::HashSet::new();
8026 for y in 0..h {
8027 for x in 0..w {
8028 let (r, g, b) = pm.get_rgb(x, y);
8029 if r > 240 && g > 240 && b > 240 {
8031 continue;
8032 }
8033 fg_colors.insert((r, g, b));
8034 }
8035 }
8036
8037 assert!(
8040 fg_colors.len() > 1,
8041 "multi-color palette page should have >1 distinct foreground colors, got {}",
8042 fg_colors.len()
8043 );
8044 }
8045
8046 #[test]
8047 fn lookup_palette_color_uses_blit_map() {
8048 let pal = FgbzPalette {
8049 colors: vec![
8050 PaletteColor { r: 255, g: 0, b: 0 }, PaletteColor { r: 0, g: 0, b: 255 }, ],
8053 indices: vec![1, 0], };
8055 let bm = crate::bitmap::Bitmap::new(2, 1);
8056 let blit_map = vec![0i32, 1i32]; let c0 = lookup_palette_color(&pal, Some(&blit_map), Some(&bm), 0, 0);
8059 assert_eq!(
8060 (c0.r, c0.g, c0.b),
8061 (0, 0, 255),
8062 "blit 0 → indices[0]=1 → blue"
8063 );
8064
8065 let c1 = lookup_palette_color(&pal, Some(&blit_map), Some(&bm), 1, 0);
8066 assert_eq!(
8067 (c1.r, c1.g, c1.b),
8068 (255, 0, 0),
8069 "blit 1 → indices[1]=0 → red"
8070 );
8071 }
8072
8073 #[test]
8074 fn lookup_palette_color_fallback_without_blit_map() {
8075 let pal = FgbzPalette {
8076 colors: vec![PaletteColor { r: 0, g: 128, b: 0 }],
8077 indices: vec![],
8078 };
8079 let c = lookup_palette_color(&pal, None, None, 0, 0);
8080 assert_eq!(
8081 (c.r, c.g, c.b),
8082 (0, 128, 0),
8083 "should fall back to first color"
8084 );
8085 }
8086
8087 fn load_bgjp_doc() -> DjVuDocument {
8091 load_doc("bgjp_test.djvu")
8092 }
8093
8094 #[test]
8096 fn bgjp_fixture_loads() {
8097 let doc = load_bgjp_doc();
8098 let page = doc.page(0).unwrap();
8099 assert_eq!(page.width(), 4);
8100 assert_eq!(page.height(), 4);
8101 }
8102
8103 #[test]
8105 fn bgjp_chunk_present() {
8106 let doc = load_bgjp_doc();
8107 let page = doc.page(0).unwrap();
8108 assert!(
8109 page.find_chunk(b"BGjp").is_some(),
8110 "fixture must have a BGjp chunk"
8111 );
8112 assert!(
8113 page.bg44_chunks().is_empty(),
8114 "fixture must NOT have BG44 chunks"
8115 );
8116 }
8117
8118 #[test]
8120 fn decode_bgjp_returns_pixmap() {
8121 let doc = load_bgjp_doc();
8122 let page = doc.page(0).unwrap();
8123 let pm = decode_bgjp(page).expect("decode_bgjp must not error");
8124 assert!(pm.is_some(), "decode_bgjp must return Some(Pixmap)");
8125 let pm = pm.unwrap();
8126 assert_eq!(pm.width, 4);
8127 assert_eq!(pm.height, 4);
8128 assert_eq!(pm.data.len(), 4 * 4 * 4); }
8130
8131 #[test]
8133 fn decode_bgjp_returns_none_without_chunk() {
8134 let doc = load_doc("chicken.djvu");
8135 let page = doc.page(0).unwrap();
8136 let pm = decode_bgjp(page).expect("should not error");
8137 assert!(pm.is_none());
8138 }
8139
8140 #[test]
8142 fn decode_jpeg_to_pixmap_alpha_is_255() {
8143 let doc = load_bgjp_doc();
8144 let page = doc.page(0).unwrap();
8145 let data = page.find_chunk(b"BGjp").unwrap();
8146 let pm = decode_jpeg_to_pixmap(data).expect("decode must succeed");
8147 for chunk in pm.data.as_chunks::<4>().0 {
8148 assert_eq!(chunk[3], 255, "alpha must be 255 for every pixel");
8149 }
8150 }
8151
8152 #[test]
8154 fn render_pixmap_uses_bgjp_background() {
8155 let doc = load_bgjp_doc();
8156 let page = doc.page(0).unwrap();
8157 let opts = RenderOptions {
8158 width: 4,
8159 height: 4,
8160 ..Default::default()
8161 };
8162 let pm = render_pixmap(page, &opts).expect("render must succeed");
8163 assert_eq!(pm.width, 4);
8164 assert_eq!(pm.height, 4);
8165 }
8166
8167 #[test]
8169 fn render_coarse_uses_bgjp_background() {
8170 let doc = load_bgjp_doc();
8171 let page = doc.page(0).unwrap();
8172 let opts = RenderOptions {
8173 width: 4,
8174 height: 4,
8175 ..Default::default()
8176 };
8177 let pm = render_coarse(page, &opts).expect("render_coarse must succeed");
8178 assert!(pm.is_some(), "must return Some when BGjp present");
8179 let pm = pm.unwrap();
8180 assert_eq!(pm.width, 4);
8181 assert_eq!(pm.height, 4);
8182 }
8183
8184 #[test]
8191 fn render_pixmap_lanczos3_correct_dimensions() {
8192 let doc = load_doc("chicken.djvu");
8193 let page = doc.page(0).unwrap();
8194 let pw = page.width() as u32;
8195 let ph = page.height() as u32;
8196 let tw = pw / 2;
8197 let th = ph / 2;
8198
8199 let opts = RenderOptions {
8200 width: tw,
8201 height: th,
8202 resampling: Resampling::Lanczos3,
8203 ..Default::default()
8204 };
8205 let pm = render_pixmap(page, &opts).expect("Lanczos3 render must succeed");
8206 assert_eq!(pm.width, tw);
8207 assert_eq!(pm.height, th);
8208 }
8209
8210 #[test]
8212 fn lanczos3_differs_from_bilinear_at_half_scale() {
8213 let doc = load_doc("chicken.djvu");
8214 let page = doc.page(0).unwrap();
8215 let pw = page.width() as u32;
8216 let ph = page.height() as u32;
8217 let tw = pw / 2;
8218 let th = ph / 2;
8219
8220 let bilinear = render_pixmap(
8221 page,
8222 &RenderOptions {
8223 width: tw,
8224 height: th,
8225 resampling: Resampling::Bilinear,
8226 ..Default::default()
8227 },
8228 )
8229 .unwrap();
8230
8231 let lanczos = render_pixmap(
8232 page,
8233 &RenderOptions {
8234 width: tw,
8235 height: th,
8236 resampling: Resampling::Lanczos3,
8237 ..Default::default()
8238 },
8239 )
8240 .unwrap();
8241
8242 assert_eq!(bilinear.width, lanczos.width);
8244 assert_eq!(bilinear.height, lanczos.height);
8245
8246 let differ = bilinear
8248 .data
8249 .iter()
8250 .zip(lanczos.data.iter())
8251 .any(|(a, b)| a != b);
8252 assert!(
8253 differ,
8254 "Lanczos3 and bilinear must produce different pixel values"
8255 );
8256 }
8257
8258 #[test]
8260 fn resampling_default_is_bilinear() {
8261 let opts = RenderOptions::default();
8262 assert_eq!(opts.resampling, Resampling::Bilinear);
8263 }
8264
8265 #[test]
8269 fn render_region_allocates_proportionally() {
8270 let doc = load_doc("chicken.djvu");
8271 let page = doc.page(0).unwrap();
8272 let opts = RenderOptions::fit_to_width(page, 1000);
8273 let region = RenderRect {
8274 x: 0,
8275 y: 0,
8276 width: 256,
8277 height: 256,
8278 };
8279 let pm = render_region(page, region, &opts).expect("render_region should succeed");
8280 assert_eq!(pm.width, 256);
8281 assert_eq!(pm.height, 256);
8282 assert_eq!(pm.data.len(), 256 * 256 * 4);
8283 assert!(
8284 pm.data.len() <= 512 * 1024,
8285 "region allocation {} exceeds 512 KB",
8286 pm.data.len()
8287 );
8288 }
8289
8290 #[test]
8292 fn render_region_matches_full_render() {
8293 let doc = load_doc("chicken.djvu");
8294 let page = doc.page(0).unwrap();
8295 let opts = RenderOptions {
8296 width: 100,
8297 height: 80,
8298 ..Default::default()
8299 };
8300 let full = render_pixmap(page, &opts).expect("full render should succeed");
8301 let region = RenderRect {
8302 x: 10,
8303 y: 10,
8304 width: 30,
8305 height: 20,
8306 };
8307 let part = render_region(page, region, &opts).expect("region render should succeed");
8308
8309 assert_eq!(part.width, 30);
8310 assert_eq!(part.height, 20);
8311
8312 for ry in 0..20u32 {
8313 for rx in 0..30u32 {
8314 let full_base = ((10 + ry) as usize * 100 + (10 + rx) as usize) * 4;
8315 let part_base = (ry as usize * 30 + rx as usize) * 4;
8316 assert_eq!(
8317 &full.data[full_base..full_base + 4],
8318 &part.data[part_base..part_base + 4],
8319 "pixel mismatch at region ({rx},{ry}) / full ({},{} )",
8320 10 + rx,
8321 10 + ry
8322 );
8323 }
8324 }
8325 }
8326
8327 #[test]
8335 fn render_region_tiled_matches_render_region() {
8336 let doc = load_doc("colorbook.djvu");
8337 let page = doc.page(0).unwrap();
8338 let opts = RenderOptions {
8341 width: 900,
8342 height: 700,
8343 ..Default::default()
8344 };
8345
8346 let regions = [
8349 RenderRect {
8350 x: 0,
8351 y: 0,
8352 width: 300,
8353 height: 220,
8354 },
8355 RenderRect {
8356 x: 60,
8357 y: 0,
8358 width: 300,
8359 height: 220,
8360 },
8361 RenderRect {
8362 x: 200,
8363 y: 40,
8364 width: 300,
8365 height: 220,
8366 },
8367 RenderRect {
8368 x: 400,
8369 y: 40,
8370 width: 300,
8371 height: 220,
8372 },
8373 RenderRect {
8374 x: 600,
8375 y: 480,
8376 width: 300,
8377 height: 220,
8378 }, RenderRect {
8380 x: 250,
8381 y: 250,
8382 width: 400,
8383 height: 300,
8384 }, RenderRect {
8386 x: 1,
8387 y: 1,
8388 width: 5,
8389 height: 5,
8390 }, ];
8392
8393 for region in regions {
8394 let direct = render_region(page, region, &opts).expect("render_region");
8395 let tiled = render_region_tiled(page, region, &opts).expect("render_region_tiled");
8396 assert_eq!(tiled.width, direct.width);
8397 assert_eq!(tiled.height, direct.height);
8398 assert_eq!(
8399 tiled.data, direct.data,
8400 "render_region_tiled diverged from render_region for {region:?}"
8401 );
8402 }
8403 }
8404
8405 #[test]
8409 fn render_region_tiled_repeated_region_matches() {
8410 let doc = load_doc("chicken.djvu");
8411 let page = doc.page(0).unwrap();
8412 let opts = RenderOptions {
8413 width: 640,
8414 height: 480,
8415 ..Default::default()
8416 };
8417 let region = RenderRect {
8418 x: 100,
8419 y: 100,
8420 width: 200,
8421 height: 150,
8422 };
8423
8424 let first = render_region_tiled(page, region, &opts).unwrap();
8425 assert!(page.render_cache_bytes() > 0);
8427 let second = render_region_tiled(page, region, &opts).unwrap();
8428 assert_eq!(first.data, second.data);
8429
8430 let overlapping = RenderRect {
8432 x: 150,
8433 y: 120,
8434 width: 200,
8435 height: 150,
8436 };
8437 let direct = render_region(page, overlapping, &opts).unwrap();
8438 let tiled = render_region_tiled(page, overlapping, &opts).unwrap();
8439 assert_eq!(direct.data, tiled.data);
8440 }
8441
8442 #[test]
8445 fn render_region_tiled_falls_back_for_ineligible_modes() {
8446 let doc = load_doc("chicken.djvu");
8447 let page = doc.page(0).unwrap();
8448 let region = RenderRect {
8449 x: 5,
8450 y: 5,
8451 width: 40,
8452 height: 30,
8453 };
8454
8455 let rotated_opts = RenderOptions {
8456 width: 200,
8457 height: 150,
8458 rotation: UserRotation::Cw90,
8459 ..Default::default()
8460 };
8461 assert_eq!(
8462 render_region(page, region, &rotated_opts).unwrap().data,
8463 render_region_tiled(page, region, &rotated_opts)
8464 .unwrap()
8465 .data
8466 );
8467
8468 let lanczos_opts = RenderOptions {
8469 width: 100,
8470 height: 75,
8471 resampling: Resampling::Lanczos3,
8472 ..Default::default()
8473 };
8474 assert_eq!(
8475 render_region(page, region, &lanczos_opts).unwrap().data,
8476 render_region_tiled(page, region, &lanczos_opts)
8477 .unwrap()
8478 .data
8479 );
8480
8481 let permissive_opts = RenderOptions {
8482 width: 200,
8483 height: 150,
8484 permissive: true,
8485 ..Default::default()
8486 };
8487 assert_eq!(
8488 render_region(page, region, &permissive_opts).unwrap().data,
8489 render_region_tiled(page, region, &permissive_opts)
8490 .unwrap()
8491 .data
8492 );
8493 }
8494
8495 #[test]
8499 fn render_region_tiled_cache_is_budget_bounded_and_evictable() {
8500 let mut doc = load_doc("colorbook.djvu");
8501 let (native_w, native_h) = {
8502 let p = doc.page(0).unwrap();
8503 (p.width() as u32, p.height() as u32)
8504 };
8505 let opts = RenderOptions {
8508 width: native_w.max(4000),
8509 height: native_h.max(4000),
8510 ..Default::default()
8511 };
8512 let full_w = opts.width;
8513
8514 {
8515 let page = doc.page(0).unwrap();
8516 let tiles_per_side = (full_w / TILE_SIZE).clamp(1, 12);
8518 for ty in 0..tiles_per_side {
8519 for tx in 0..tiles_per_side {
8520 let region = RenderRect {
8521 x: tx * TILE_SIZE,
8522 y: ty * TILE_SIZE,
8523 width: 8,
8524 height: 8,
8525 };
8526 let _ = render_region_tiled(page, region, &opts).unwrap();
8527 }
8528 }
8529 let bytes = page.render_layers().tile_cache_bytes();
8530 assert!(bytes > 0, "expected some tile bytes cached");
8531 assert!(
8532 bytes <= TILE_CACHE_MAX_BYTES,
8533 "tile cache exceeded its byte budget: {bytes} > {TILE_CACHE_MAX_BYTES}"
8534 );
8535 }
8536
8537 doc.evict_render_caches();
8539 assert_eq!(doc.page(0).unwrap().render_cache_bytes(), 0);
8540 }
8541
8542 #[test]
8545 fn render_region_tiled_rejects_zero_dimensions() {
8546 let doc = load_doc("chicken.djvu");
8547 let page = doc.page(0).unwrap();
8548 let opts = RenderOptions {
8549 width: 100,
8550 height: 80,
8551 ..Default::default()
8552 };
8553 let region = RenderRect {
8554 x: 0,
8555 y: 0,
8556 width: 0,
8557 height: 10,
8558 };
8559 assert!(render_region_tiled(page, region, &opts).is_err());
8560 }
8561
8562 #[test]
8566 fn render_region_bilevel_byte_aligned_offset_matches_full() {
8567 let doc = load_doc("boy_jb2.djvu");
8568 let page = doc.page(0).unwrap();
8569 let full_w = page.width() as u32;
8570 let full_h = page.height() as u32;
8571 let opts = RenderOptions {
8572 width: full_w,
8573 height: full_h,
8574 ..Default::default()
8575 };
8576 let full = render_pixmap(page, &opts).expect("full render should succeed");
8577 for &x in &[16u32, 17u32] {
8579 let region = RenderRect {
8580 x,
8581 y: 8,
8582 width: 64,
8583 height: 32,
8584 };
8585 let part = render_region(page, region, &opts).expect("region render should succeed");
8586 for ry in 0..region.height {
8587 for rx in 0..region.width {
8588 let fb = (((region.y + ry) * full_w + (x + rx)) * 4) as usize;
8589 let pb = ((ry * region.width + rx) * 4) as usize;
8590 assert_eq!(
8591 &full.data[fb..fb + 4],
8592 &part.data[pb..pb + 4],
8593 "mismatch at x={x} region ({rx},{ry})"
8594 );
8595 }
8596 }
8597 }
8598 }
8599
8600 #[test]
8602 fn render_region_invalid_dimensions() {
8603 let doc = load_doc("chicken.djvu");
8604 let page = doc.page(0).unwrap();
8605 let opts = RenderOptions {
8606 width: 100,
8607 height: 100,
8608 ..Default::default()
8609 };
8610 let region = RenderRect {
8611 x: 0,
8612 y: 0,
8613 width: 0,
8614 height: 50,
8615 };
8616 let err = render_region(page, region, &opts).unwrap_err();
8617 assert!(
8618 matches!(err, RenderError::InvalidDimensions { .. }),
8619 "expected InvalidDimensions, got {err:?}"
8620 );
8621 }
8622
8623 #[test]
8625 fn render_pixmap_still_works_after_refactor() {
8626 let doc = load_doc("chicken.djvu");
8627 let page = doc.page(0).unwrap();
8628 let opts = RenderOptions {
8629 width: 80,
8630 height: 60,
8631 ..Default::default()
8632 };
8633 let pm = render_pixmap(page, &opts).expect("render_pixmap should succeed");
8634 assert_eq!(pm.width, 80);
8635 assert_eq!(pm.height, 60);
8636 assert_eq!(pm.data.len(), 80 * 60 * 4);
8637 }
8638
8639 #[test]
8641 fn best_iw44_subsample_values() {
8642 assert_eq!(best_iw44_subsample(1.0), 1, "scale=1.0 → subsample=1");
8643 assert_eq!(best_iw44_subsample(0.5), 2, "scale=0.5 → subsample=2");
8644 assert_eq!(
8645 best_iw44_subsample(0.375),
8646 4,
8647 "scale=0.375 → subsample=4 (1.5/0.375=4.0, allows 1.5× upscale)"
8648 );
8649 assert_eq!(best_iw44_subsample(0.25), 4, "scale=0.25 → subsample=4");
8650 assert_eq!(
8651 best_iw44_subsample(0.1),
8652 8,
8653 "scale=0.1 → subsample=8 (capped)"
8654 );
8655 assert_eq!(
8656 best_iw44_subsample(0.0),
8657 1,
8658 "scale=0.0 → subsample=1 (edge case)"
8659 );
8660 assert_eq!(
8661 best_iw44_subsample(-1.0),
8662 1,
8663 "scale<0 → subsample=1 (edge case)"
8664 );
8665 assert_eq!(
8666 best_iw44_subsample(2.0),
8667 1,
8668 "scale>1.0 → subsample=1 (no upscaling needed)"
8669 );
8670 }
8671
8672 #[test]
8676 #[allow(deprecated)] fn decode_subsample_derives_from_width_not_scale_field() {
8678 let doc = load_doc("chicken.djvu");
8679 let page = doc.page(0).unwrap();
8680 let (dw, _) = display_dimensions(page);
8681
8682 let opts = RenderOptions {
8688 width: dw / 4,
8689 ..Default::default()
8690 };
8691 assert!((opts.decode_scale(page) - 0.25).abs() < 0.01);
8692 assert_eq!(best_iw44_subsample(opts.decode_scale(page)), 4);
8693
8694 for misleading in [1.0_f32, 0.0, 0.5, 4.0] {
8697 let mut o = RenderOptions {
8698 width: dw / 4,
8699 ..Default::default()
8700 };
8701 o.scale = misleading;
8702 assert_eq!(
8703 best_iw44_subsample(o.decode_scale(page)),
8704 4,
8705 "scale={misleading} must not change the width-derived subsample",
8706 );
8707 }
8708 }
8709
8710 #[test]
8716 fn decode_scale_uses_native_width_for_rotated_page() {
8717 let doc = load_doc("boy_jb2_rotate90.djvu");
8719 let page = doc.page(0).unwrap();
8720 let pw = page.width() as u32;
8721 let (dw, _) = display_dimensions(page);
8722 assert_ne!(dw, pw, "fixture must be a non-square INFO-rotated page");
8723
8724 let opts = RenderOptions {
8727 width: pw / 2,
8728 height: (page.height() as u32) / 2,
8729 ..Default::default()
8730 };
8731 let native = opts.width as f32 / pw as f32; let display = opts.width as f32 / dw as f32; assert!(
8734 (opts.decode_scale(page) - native).abs() < 1e-4,
8735 "decode_scale {} should equal the native-width ratio {native}",
8736 opts.decode_scale(page),
8737 );
8738 assert!(
8739 (opts.decode_scale(page) - display).abs() > 1e-2,
8740 "decode_scale must not follow the rotation-swapped display width ({display})",
8741 );
8742 }
8743
8744 #[test]
8746 fn render_pixmap_subsampled_bg_correct_dimensions() {
8747 let doc = load_doc("boy.djvu");
8748 let page = doc.page(0).unwrap();
8749 let opts = RenderOptions {
8751 width: (page.width() as f32 * 0.5) as u32,
8752 height: (page.height() as f32 * 0.5) as u32,
8753 ..Default::default()
8754 };
8755 let pm = render_pixmap(page, &opts).expect("subsampled render should succeed");
8756 assert_eq!(pm.width, opts.width);
8757 assert_eq!(pm.height, opts.height);
8758 assert_eq!(
8759 pm.data.len() as u64,
8760 opts.width as u64 * opts.height as u64 * 4
8761 );
8762 }
8763
8764 #[test]
8767 fn decoded_bg44_cache_produces_identical_pixels_on_second_render() {
8768 let doc = load_doc("boy.djvu");
8769 let page = doc.page(0).unwrap();
8770 let opts = RenderOptions {
8771 width: page.width() as u32,
8772 height: page.height() as u32,
8773 ..Default::default()
8774 };
8775 let pm1 = render_pixmap(page, &opts).expect("first render should succeed");
8776 let pm2 = render_pixmap(page, &opts).expect("second render should succeed");
8777 assert_eq!(
8778 pm1.data, pm2.data,
8779 "cached render must produce identical pixels"
8780 );
8781 }
8782
8783 #[test]
8786 fn decoded_bg44_is_populated_after_render() {
8787 let doc = load_doc("boy.djvu");
8788 let page = doc.page(0).unwrap();
8789 let opts = RenderOptions {
8790 width: page.width() as u32,
8791 height: page.height() as u32,
8792 ..Default::default()
8793 };
8794 render_pixmap(page, &opts).expect("render should succeed");
8796 let cached = page
8798 .decoded_bg44()
8799 .expect("cache should be populated after render");
8800 assert_eq!(
8801 cached.width,
8802 page.width() as u32,
8803 "cached bg44 width must equal page width"
8804 );
8805 assert_eq!(
8806 cached.height,
8807 page.height() as u32,
8808 "cached bg44 height must equal page height"
8809 );
8810 }
8811
8812 #[test]
8816 fn downsample_mask_4x_max_pools_each_block() {
8817 let mut src = crate::bitmap::Bitmap::new(8, 8);
8820 src.set(1, 2, true); src.set(6, 5, true); let out = downsample_mask_4x(&src);
8823 assert_eq!((out.width, out.height), (2, 2));
8824 assert!(out.get(0, 0), "top-left block had a set bit");
8825 assert!(!out.get(1, 0), "top-right block was empty");
8826 assert!(!out.get(0, 1), "bottom-left block was empty");
8827 assert!(out.get(1, 1), "bottom-right block had a set bit");
8828 }
8829
8830 #[test]
8833 fn downsample_mask_4x_rounds_up_ragged_edges() {
8834 let mut src = crate::bitmap::Bitmap::new(5, 5);
8835 src.set(4, 4, true); let out = downsample_mask_4x(&src);
8837 assert_eq!((out.width, out.height), (2, 2));
8838 assert!(out.get(1, 1), "ragged corner block must capture its bit");
8839 assert!(!out.get(0, 0));
8840 }
8841
8842 #[test]
8847 fn render_region_applies_rotation() {
8848 let doc = load_doc("chicken.djvu");
8849 let page = doc.page(0).unwrap();
8850 let opts = RenderOptions {
8852 width: 80,
8853 height: 60,
8854 rotation: UserRotation::Cw90,
8855 ..Default::default()
8856 };
8857 let region = RenderRect {
8859 x: 0,
8860 y: 0,
8861 width: 40,
8862 height: 20,
8863 };
8864 let part = render_region(page, region, &opts).expect("region render should succeed");
8865 assert_eq!(
8867 part.width, 20,
8868 "expected width=20 (was region.height) after CW90 rotation"
8869 );
8870 assert_eq!(
8871 part.height, 40,
8872 "expected height=40 (was region.width) after CW90 rotation"
8873 );
8874 }
8875
8876 #[test]
8881 fn render_region_matches_full_render_crop_at_sub4() {
8882 let doc = load_doc("boy_jb2.djvu");
8883 let page = doc.page(0).unwrap();
8884 let opts = RenderOptions {
8885 width: 40,
8886 height: 52,
8887 ..Default::default()
8888 };
8889 let full = render_pixmap(page, &opts).unwrap();
8890 let region = RenderRect {
8891 x: 8,
8892 y: 8,
8893 width: 24,
8894 height: 24,
8895 };
8896 let reg = render_region(page, region, &opts).unwrap();
8897 let mut crop = Vec::new();
8898 for y in 0..24usize {
8899 let s = ((8 + y) * 40 + 8) * 4;
8900 crop.extend_from_slice(&full.data[s..s + 24 * 4]);
8901 }
8902 assert_eq!(
8903 reg.data, crop,
8904 "render_region must be byte-identical to the matching crop of render_pixmap"
8905 );
8906 }
8907
8908 #[test]
8916 fn render_streaming_byte_identical_to_render_pixmap_color() {
8917 let doc = load_doc("chicken.djvu");
8918 let page = doc.page(0).unwrap();
8919 let w = 60u32;
8920 let h = 80u32;
8921 let opts = RenderOptions {
8922 width: w,
8923 height: h,
8924 ..Default::default()
8925 };
8926
8927 let pm = render_pixmap(page, &opts).expect("render_pixmap should succeed");
8928
8929 let row_stride = w as usize * 4;
8930 let mut streamed = vec![0u8; w as usize * h as usize * 4];
8931 render_streaming(page, &opts, |y, row| {
8932 assert_eq!(row.len(), row_stride);
8933 let start = y * row_stride;
8934 streamed[start..start + row_stride].copy_from_slice(row);
8935 })
8936 .expect("render_streaming should succeed");
8937
8938 assert_eq!(
8939 pm.data, streamed,
8940 "render_streaming must be byte-identical to render_pixmap when no post-processing options are set"
8941 );
8942 }
8943
8944 #[test]
8947 fn render_streaming_byte_identical_to_render_pixmap_bilevel() {
8948 let doc = load_doc("boy_jb2.djvu");
8949 let page = doc.page(0).unwrap();
8950 let w = 50u32;
8951 let h = 70u32;
8952 let opts = RenderOptions {
8953 width: w,
8954 height: h,
8955 ..Default::default()
8956 };
8957
8958 let pm = render_pixmap(page, &opts).expect("render_pixmap should succeed");
8959
8960 let row_stride = w as usize * 4;
8961 let mut streamed = vec![0u8; w as usize * h as usize * 4];
8962 render_streaming(page, &opts, |y, row| {
8963 let start = y * row_stride;
8964 streamed[start..start + row_stride].copy_from_slice(row);
8965 })
8966 .expect("render_streaming should succeed");
8967
8968 assert_eq!(pm.data, streamed);
8969 }
8970
8971 #[test]
8973 fn render_streaming_rejects_aa() {
8974 let doc = load_doc("chicken.djvu");
8975 let page = doc.page(0).unwrap();
8976 let opts = RenderOptions {
8977 width: 60,
8978 height: 80,
8979 aa: true,
8980 ..Default::default()
8981 };
8982 let err = render_streaming(page, &opts, |_, _| {}).unwrap_err();
8983 assert!(
8984 matches!(err, RenderError::UnsupportedOption(_)),
8985 "expected UnsupportedOption, got {err:?}"
8986 );
8987 }
8988
8989 #[test]
8991 fn render_streaming_rejects_lanczos_with_scaling() {
8992 let doc = load_doc("chicken.djvu");
8993 let page = doc.page(0).unwrap();
8994 let opts = RenderOptions {
8996 width: page.width() as u32 / 2,
8997 height: page.height() as u32 / 2,
8998 resampling: Resampling::Lanczos3,
8999 ..Default::default()
9000 };
9001 let err = render_streaming(page, &opts, |_, _| {}).unwrap_err();
9002 assert!(matches!(err, RenderError::UnsupportedOption(_)));
9003 }
9004
9005 #[test]
9008 fn render_streaming_allows_lanczos_at_native_size() {
9009 let doc = load_doc("chicken.djvu");
9010 let page = doc.page(0).unwrap();
9011 let opts = RenderOptions {
9012 width: page.width() as u32,
9013 height: page.height() as u32,
9014 resampling: Resampling::Lanczos3,
9015 ..Default::default()
9016 };
9017 let mut row_count = 0usize;
9018 render_streaming(page, &opts, |_, _| row_count += 1).expect("should succeed at native");
9019 assert_eq!(row_count, page.height() as usize);
9020 }
9021
9022 #[test]
9024 fn render_streaming_rejects_user_rotation() {
9025 let doc = load_doc("chicken.djvu");
9026 let page = doc.page(0).unwrap();
9027 let opts = RenderOptions {
9028 width: 60,
9029 height: 80,
9030 rotation: UserRotation::Cw90,
9031 ..Default::default()
9032 };
9033 let err = render_streaming(page, &opts, |_, _| {}).unwrap_err();
9034 assert!(matches!(err, RenderError::UnsupportedOption(_)));
9035 }
9036
9037 #[test]
9039 fn render_streaming_rejects_zero_dimensions() {
9040 let doc = load_doc("chicken.djvu");
9041 let page = doc.page(0).unwrap();
9042 let opts = RenderOptions {
9043 width: 0,
9044 height: 80,
9045 ..Default::default()
9046 };
9047 let err = render_streaming(page, &opts, |_, _| {}).unwrap_err();
9048 assert!(matches!(err, RenderError::InvalidDimensions { .. }));
9049 }
9050
9051 #[test]
9055 fn permissive_render_iw44_jb2_page() {
9056 let doc = load_doc("czech.djvu");
9057 let page = doc.page(0).unwrap();
9058 let opts = RenderOptions {
9059 width: 40,
9060 height: 40,
9061 permissive: true,
9062 ..Default::default()
9063 };
9064 let pm = render_pixmap(page, &opts).expect("permissive render should not error");
9065 assert_eq!(pm.width, 40);
9066 assert_eq!(pm.height, 40);
9067 }
9068
9069 #[test]
9072 fn oversized_render_dimensions_are_rejected() {
9073 let doc = load_doc("czech.djvu");
9074 let page = doc.page(0).unwrap();
9075 let opts = RenderOptions {
9076 width: 60_000,
9077 height: 60_000, permissive: true,
9079 ..Default::default()
9080 };
9081 assert!(matches!(
9082 render_pixmap(page, &opts),
9083 Err(RenderError::ResourceLimit(_))
9084 ));
9085 }
9086
9087 #[test]
9088 fn render_into_rejects_oversized_output_with_typed_limit_error() {
9089 let doc = load_doc("czech.djvu");
9090 let page = doc.page(0).unwrap();
9091 let opts = RenderOptions {
9092 width: 60_000,
9093 height: 60_000,
9094 permissive: true,
9095 ..Default::default()
9096 };
9097 let mut buf = vec![0u8; 16];
9098 assert!(matches!(
9099 render_into(page, &opts, &mut buf),
9100 Err(RenderError::ResourceLimit(_))
9101 ));
9102 }
9103
9104 #[test]
9105 fn configurable_render_limit_overrides_inherited_ceiling() {
9106 let doc = load_doc("czech.djvu");
9107 let page = doc.page(0).unwrap();
9108 let opts = RenderOptions {
9109 width: 500,
9110 height: 500,
9111 permissive: true,
9112 ..Default::default()
9113 };
9114 let limits = crate::resource_limits::ResourceLimits {
9115 max_render_pixels: Some(100_000),
9116 ..Default::default()
9117 };
9118 assert!(matches!(
9119 render_pixmap_with_limits(page, &opts, Some(limits)),
9120 Err(RenderError::ResourceLimit(exceeded)) if exceeded.operation == "render_pixmap"
9121 && exceeded.axis == crate::resource_limits::ResourceLimitAxis::RenderOutputPixels
9122 ));
9123 }
9124
9125 #[test]
9127 fn permissive_render_bgjp_page() {
9128 let doc = load_doc("bgjp_test.djvu");
9129 let page = doc.page(0).unwrap();
9130 let opts = RenderOptions {
9131 width: 4,
9132 height: 4,
9133 permissive: true,
9134 ..Default::default()
9135 };
9136 let pm = render_pixmap(page, &opts).expect("permissive render of BGjp page should succeed");
9137 assert_eq!(pm.width, 4);
9138 assert_eq!(pm.height, 4);
9139 }
9140
9141 #[test]
9143 fn permissive_render_fgbz_page() {
9144 let doc = load_doc("navm_fgbz.djvu");
9145 let page = doc.page(0).unwrap();
9146 let opts = RenderOptions {
9147 width: 40,
9148 height: 40,
9149 permissive: true,
9150 ..Default::default()
9151 };
9152 let pm = render_pixmap(page, &opts).expect("permissive render of FGbz page should succeed");
9153 assert!(pm.width > 0 && pm.height > 0);
9154 }
9155
9156 #[test]
9158 fn render_gray8_produces_grayscale_output() {
9159 let doc = load_doc("boy_jb2.djvu");
9160 let page = doc.page(0).unwrap();
9161 let opts = RenderOptions {
9162 width: 20,
9163 height: 20,
9164 ..Default::default()
9165 };
9166 let gpm = render_gray8(page, &opts).expect("render_gray8 should succeed");
9167 assert_eq!(gpm.width, 20);
9168 assert_eq!(gpm.height, 20);
9169 assert_eq!(gpm.data.len(), 20 * 20);
9170 }
9171
9172 #[test]
9174 fn render_coarse_rejects_zero_dimensions() {
9175 let doc = load_doc("chicken.djvu");
9176 let page = doc.page(0).unwrap();
9177 let opts = RenderOptions {
9178 width: 0,
9179 height: 50,
9180 ..Default::default()
9181 };
9182 let err = render_coarse(page, &opts).unwrap_err();
9183 assert!(matches!(err, RenderError::InvalidDimensions { .. }));
9184 }
9185
9186 #[test]
9188 fn render_coarse_jb2_only_page_returns_none() {
9189 let doc = load_doc("boy_jb2.djvu");
9190 let page = doc.page(0).unwrap();
9191 let opts = RenderOptions {
9192 width: 40,
9193 height: 40,
9194 ..Default::default()
9195 };
9196 let result = render_coarse(page, &opts).expect("should not error");
9197 assert!(
9198 result.is_none(),
9199 "JB2-only page has no BG44 so render_coarse yields None"
9200 );
9201 }
9202
9203 #[test]
9205 fn render_progressive_rejects_zero_dimensions() {
9206 let doc = load_doc("chicken.djvu");
9207 let page = doc.page(0).unwrap();
9208 let opts = RenderOptions {
9209 width: 0,
9210 height: 50,
9211 ..Default::default()
9212 };
9213 let err = render_progressive(page, &opts, 0).unwrap_err();
9214 assert!(matches!(err, RenderError::InvalidDimensions { .. }));
9215 }
9216
9217 #[test]
9219 fn render_progressive_jb2_only_page_succeeds() {
9220 let doc = load_doc("boy_jb2.djvu");
9221 let page = doc.page(0).unwrap();
9222 let opts = RenderOptions {
9223 width: 40,
9224 height: 40,
9225 ..Default::default()
9226 };
9227 let result = render_progressive(page, &opts, 0)
9228 .expect("render_progressive should succeed even without BG44");
9229 assert_eq!(result.width, 40);
9230 assert_eq!(result.height, 40);
9231 }
9232
9233 #[test]
9235 fn lanczos3_at_native_resolution_skips_rerender() {
9236 let doc = load_doc("bgjp_test.djvu");
9237 let page = doc.page(0).unwrap();
9238 let opts = RenderOptions {
9240 width: page.width() as u32,
9241 height: page.height() as u32,
9242 resampling: Resampling::Lanczos3,
9243 ..Default::default()
9244 };
9245 let pm = render_pixmap(page, &opts).expect("lanczos at native size must succeed");
9246 assert_eq!(pm.width, page.width() as u32);
9247 assert_eq!(pm.height, page.height() as u32);
9248 }
9249
9250 #[test]
9252 fn render_pixmap_rejects_zero_width() {
9253 let doc = load_doc("chicken.djvu");
9254 let page = doc.page(0).unwrap();
9255 let opts = RenderOptions {
9256 width: 0,
9257 height: 50,
9258 ..Default::default()
9259 };
9260 let err = render_pixmap(page, &opts).unwrap_err();
9261 assert!(matches!(err, RenderError::InvalidDimensions { .. }));
9262 }
9263
9264 fn make_doc_with_dims(w: u16, h: u16) -> Vec<u8> {
9266 use crate::iff::{Chunk, DjvuFile, emit};
9267 let mut info = vec![0u8; 10];
9268 info[0] = (w >> 8) as u8;
9269 info[1] = w as u8;
9270 info[2] = (h >> 8) as u8;
9271 info[3] = h as u8;
9272 let file = DjvuFile {
9273 root: Chunk::Form {
9274 secondary_id: *b"DJVU",
9275 length: 0,
9276 children: vec![Chunk::Leaf {
9277 id: *b"INFO",
9278 data: info,
9279 }],
9280 },
9281 };
9282 emit(&file)
9283 }
9284
9285 #[test]
9288 fn fit_to_width_zero_page_width_uses_requested_width() {
9289 let bytes = make_doc_with_dims(0, 100);
9291 let doc = DjVuDocument::parse(&bytes).unwrap();
9292 let page = doc.page(0).unwrap();
9293 let opts = RenderOptions::fit_to_width(page, 40);
9294 assert_eq!(opts.width, 40);
9295 assert_eq!(opts.height, 40); }
9297
9298 #[test]
9299 fn fit_to_height_zero_page_height_uses_requested_height() {
9300 let bytes = make_doc_with_dims(100, 0);
9302 let doc = DjVuDocument::parse(&bytes).unwrap();
9303 let page = doc.page(0).unwrap();
9304 let opts = RenderOptions::fit_to_height(page, 60);
9305 assert_eq!(opts.height, 60);
9306 assert_eq!(opts.width, 60); }
9308
9309 #[test]
9310 fn fit_to_box_zero_page_dims_falls_back_to_box_max() {
9311 let bytes = make_doc_with_dims(0, 0);
9313 let doc = DjVuDocument::parse(&bytes).unwrap();
9314 let page = doc.page(0).unwrap();
9315 let opts = RenderOptions::fit_to_box(page, 80, 60);
9316 assert_eq!(opts.width, 80);
9317 assert_eq!(opts.height, 60);
9318 }
9319
9320 #[test]
9321 fn can_stream_true_at_native_resolution_with_lanczos3() {
9322 let doc = load_doc("chicken.djvu");
9324 let page = doc.page(0).unwrap();
9325 let opts = RenderOptions {
9326 width: page.width() as u32,
9327 height: page.height() as u32,
9328 resampling: Resampling::Lanczos3,
9329 ..Default::default()
9330 };
9331 assert!(opts.can_stream(page));
9332 }
9333
9334 #[test]
9337 fn render_bilevel_at_tiny_scale_exercises_bg44_partial_empty() {
9338 let doc = load_doc("boy_jb2.djvu");
9339 let page = doc.page(0).unwrap();
9340 let opts = RenderOptions {
9342 width: 10,
9343 height: 10,
9344 ..Default::default()
9345 };
9346 let pm = render_pixmap(page, &opts).expect("tiny bilevel render should succeed");
9347 assert!(pm.width > 0 && pm.height > 0);
9348 }
9349
9350 #[test]
9352 fn render_with_bold_dilation_produces_output() {
9353 let doc = load_doc("boy_jb2.djvu");
9354 let page = doc.page(0).unwrap();
9355 let opts = RenderOptions {
9356 width: 40,
9357 height: 40,
9358 bold: 1,
9359 ..Default::default()
9360 };
9361 let pm = render_pixmap(page, &opts).expect("bold render should succeed");
9362 assert_eq!(pm.width, 40);
9363 assert_eq!(pm.height, 40);
9364 }
9365}