#[cfg(not(feature = "std"))]
use alloc::{borrow::Cow, vec, vec::Vec};
#[cfg(feature = "std")]
use std::borrow::Cow;
use crate::djvu_document::DjVuPage;
use crate::iw44::Iw44Image;
use crate::pixmap::{GrayPixmap, Pixmap};
#[derive(Debug, thiserror::Error)]
pub enum RenderError {
#[error("IW44 decode error: {0}")]
Iw44(#[from] crate::error::Iw44Error),
#[error("JB2 decode error: {0}")]
Jb2(#[from] crate::error::Jb2Error),
#[error("buffer too small: need {need} bytes, got {got}")]
BufTooSmall { need: usize, got: usize },
#[error("invalid render dimensions: {width}x{height}")]
InvalidDimensions { width: u32, height: u32 },
#[error("chunk index {chunk_n} out of range (max {max})")]
ChunkOutOfRange { chunk_n: usize, max: usize },
#[error("BZZ error: {0}")]
Bzz(#[from] crate::error::BzzError),
#[cfg(feature = "std")]
#[error("JPEG decode error: {0}")]
Jpeg(String),
#[error("document error: {0}")]
Doc(#[from] crate::djvu_document::DocError),
#[error("unsupported render option: {0}")]
UnsupportedOption(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UserRotation {
#[default]
None,
Cw90,
Rot180,
Ccw90,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Resampling {
#[default]
Bilinear,
Lanczos3,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RenderOptions {
pub width: u32,
pub height: u32,
#[deprecated(
since = "0.20.1",
note = "scale is derived from `width` by the render pipeline and is no longer read; \
build options via `RenderOptions::fit_to_width`/`fit_to_box`. \
This field is ignored and will be removed in a future release."
)]
pub scale: f32,
pub bold: u8,
pub aa: bool,
pub rotation: UserRotation,
pub permissive: bool,
pub resampling: Resampling,
}
impl Default for RenderOptions {
#[allow(deprecated)] fn default() -> Self {
RenderOptions {
width: 0,
height: 0,
scale: 1.0,
bold: 0,
aa: false,
rotation: UserRotation::None,
permissive: false,
resampling: Resampling::Bilinear,
}
}
}
#[allow(deprecated)] impl RenderOptions {
pub fn fit_to_width(page: &crate::djvu_document::DjVuPage, width: u32) -> Self {
let (dw, dh) = display_dimensions(page);
let height = if dw == 0 {
width
} else {
((dh as f64 * width as f64) / dw as f64).round() as u32
}
.max(1);
let scale = width as f32 / dw.max(1) as f32;
RenderOptions {
width,
height,
scale,
..Default::default()
}
}
pub fn fit_to_height(page: &crate::djvu_document::DjVuPage, height: u32) -> Self {
let (dw, dh) = display_dimensions(page);
let width = if dh == 0 {
height
} else {
((dw as f64 * height as f64) / dh as f64).round() as u32
}
.max(1);
let scale = height as f32 / dh.max(1) as f32;
RenderOptions {
width,
height,
scale,
..Default::default()
}
}
pub fn fit_to_box(
page: &crate::djvu_document::DjVuPage,
max_width: u32,
max_height: u32,
) -> Self {
let (dw, dh) = display_dimensions(page);
if dw == 0 || dh == 0 {
return RenderOptions {
width: max_width.max(1),
height: max_height.max(1),
scale: 1.0,
..Default::default()
};
}
let scale_w = max_width as f64 / dw as f64;
let scale_h = max_height as f64 / dh as f64;
let scale = if scale_w < scale_h { scale_w } else { scale_h };
let width = (dw as f64 * scale).round() as u32;
let height = (dh as f64 * scale).round() as u32;
RenderOptions {
width: width.max(1),
height: height.max(1),
scale: scale as f32,
..Default::default()
}
}
pub fn can_stream(&self, page: &crate::djvu_document::DjVuPage) -> bool {
!self.aa
&& (self.resampling == Resampling::Bilinear
|| (page.width() as u32 == self.width && page.height() as u32 == self.height))
&& page.rotation() == crate::info::Rotation::None
&& self.rotation == UserRotation::None
}
pub(crate) fn decode_scale(&self, page: &crate::djvu_document::DjVuPage) -> f32 {
self.width as f32 / (page.width() as u32).max(1) as f32
}
}
pub(crate) fn display_dimensions(page: &crate::djvu_document::DjVuPage) -> (u32, u32) {
let w = page.width() as u32;
let h = page.height() as u32;
match page.rotation() {
crate::info::Rotation::Cw90 | crate::info::Rotation::Ccw90 => (h, w),
_ => (w, h),
}
}
const DISPLAY_GAMMA: f32 = 2.2;
fn build_gamma_lut(gamma: f32) -> [u8; 256] {
let mut lut = [0u8; 256];
let exponent = if gamma <= 0.0 || !gamma.is_finite() {
1.0_f32 } else {
gamma / DISPLAY_GAMMA
};
if (exponent - 1.0).abs() < 1e-4 {
for (i, v) in lut.iter_mut().enumerate() {
*v = i as u8;
}
return lut;
}
for (i, v) in lut.iter_mut().enumerate() {
let linear = i as f32 / 255.0;
let corrected = linear.powf(exponent);
*v = (corrected * 255.0 + 0.5) as u8;
}
lut
}
const FRACBITS: u32 = 4;
const FRAC: u32 = 1 << FRACBITS;
const FRAC_MASK: u32 = FRAC - 1;
const MASK_EXPAND: [[u8; 8]; 256] = {
let mut lut = [[0u8; 8]; 256];
let mut b = 0usize;
while b < 256 {
let mut bit = 0usize;
while bit < 8 {
lut[b][bit] = if (b >> (7 - bit)) & 1 != 0 {
0xFF
} else {
0x00
};
bit += 1;
}
b += 1;
}
lut
};
const BILEVEL_RGBA: [[u8; 32]; 256] = {
let mut lut = [[0u8; 32]; 256];
let mut mb = 0usize;
while mb < 256 {
let mut bit = 0usize;
while bit < 8 {
let ch = if (mb >> (7 - bit)) & 1 != 0 {
0u8
} else {
255u8
};
lut[mb][bit * 4] = ch;
lut[mb][bit * 4 + 1] = ch;
lut[mb][bit * 4 + 2] = ch;
lut[mb][bit * 4 + 3] = 255;
bit += 1;
}
mb += 1;
}
lut
};
#[cfg(feature = "std")]
#[allow(unsafe_code)]
#[inline]
fn rgb_to_rgba(src: &[u8], dst: &mut [u8]) {
let pixel_count = src.len() / 3;
debug_assert_eq!(dst.len(), pixel_count * 4);
#[cfg(target_arch = "x86_64")]
if is_x86_feature_detected!("ssse3") {
unsafe {
let safe_chunks = if src.len() >= 16 {
((src.len() - 16) / 12 + 1).min(pixel_count / 4)
} else {
0
};
rgb_to_rgba_ssse3(src, dst, pixel_count, safe_chunks);
}
return;
}
rgb_to_rgba_scalar(src, dst, 0, pixel_count);
}
#[cfg(all(feature = "std", target_arch = "x86_64"))]
#[allow(unsafe_code, unsafe_op_in_unsafe_fn)]
#[target_feature(enable = "ssse3")]
unsafe fn rgb_to_rgba_ssse3(src: &[u8], dst: &mut [u8], pixel_count: usize, safe_chunks: usize) {
use core::arch::x86_64::*;
let shuf = _mm_set_epi8(
-1, 11, 10, 9, -1, 8, 7, 6, -1, 5, 4, 3, -1, 2, 1, 0, );
let alpha_or = _mm_set1_epi32(0xFF000000u32 as i32);
for i in 0..safe_chunks {
let v = _mm_loadu_si128(src.as_ptr().add(i * 12) as *const __m128i);
_mm_storeu_si128(
dst.as_mut_ptr().add(i * 16) as *mut __m128i,
_mm_or_si128(_mm_shuffle_epi8(v, shuf), alpha_or),
);
}
rgb_to_rgba_scalar(src, dst, safe_chunks * 4, pixel_count);
}
#[cfg(feature = "std")]
#[inline]
fn rgb_to_rgba_scalar(src: &[u8], dst: &mut [u8], start: usize, end: usize) {
for i in start..end {
dst[i * 4] = src[i * 3];
dst[i * 4 + 1] = src[i * 3 + 1];
dst[i * 4 + 2] = src[i * 3 + 2];
dst[i * 4 + 3] = 255;
}
}
#[inline]
fn plane_q24(plane: Option<&Pixmap>, page_w: u32, page_h: u32) -> (u64, u64) {
match plane {
Some(p) if page_w > 0 && page_h > 0 => (
((p.width as u64) << 24) / page_w as u64,
((p.height as u64) << 24) / page_h as u64,
),
_ => (0, 0),
}
}
#[inline]
fn map_plane_center_frac(page_frac: u32, q24: u64) -> u32 {
let centered = (((page_frac as u64 + (FRAC / 2) as u64) * q24) >> 24) as u32;
centered.saturating_sub(FRAC / 2)
}
#[inline]
fn fg_q24(fg: Option<&Pixmap>, page_w: u32, page_h: u32) -> (u64, u64) {
match fg {
Some(p) if page_w > 0 && page_h > 0 && p.width > 0 && p.height > 0 => {
let sx = page_w.div_ceil(p.width).max(1);
(
(1u64 << 24) / sx as u64,
((p.height as u64) << 24) / page_h as u64,
)
}
_ => plane_q24(fg, page_w, page_h),
}
}
#[inline]
fn bg_q24(bg: Option<&Pixmap>, page_w: u32, page_h: u32) -> (u64, u64) {
match bg {
Some(p) if page_w > 0 && page_h > 0 && p.width > 0 && p.height > 0 => {
let sx = page_w.div_ceil(p.width).max(1);
let sy = page_h.div_ceil(p.height).max(1);
((1u64 << 24) / sx as u64, (1u64 << 24) / sy as u64)
}
_ => plane_q24(bg, page_w, page_h),
}
}
#[inline]
#[cfg_attr(not(test), allow(dead_code))]
fn sample_bilinear(pm: &Pixmap, fx: u32, fy: u32) -> (u8, u8, u8) {
let x0 = (fx >> FRACBITS).min(pm.width.saturating_sub(1));
let y0 = (fy >> FRACBITS).min(pm.height.saturating_sub(1));
let x1 = (x0 + 1).min(pm.width.saturating_sub(1));
let y1 = (y0 + 1).min(pm.height.saturating_sub(1));
let tx = fx & FRAC_MASK; let ty = fy & FRAC_MASK;
let (r00, g00, b00) = pm.get_rgb(x0, y0);
let (r10, g10, b10) = pm.get_rgb(x1, y0);
let (r01, g01, b01) = pm.get_rgb(x0, y1);
let (r11, g11, b11) = pm.get_rgb(x1, y1);
let lerp = |a: u8, b: u8, c: u8, d: u8| -> u8 {
let top = a as u32 * (FRAC - tx) + b as u32 * tx;
let bot = c as u32 * (FRAC - tx) + d as u32 * tx;
let numerator = top * (FRAC - ty) + bot * ty;
((numerator + (1 << (2 * FRACBITS - 1))) >> (2 * FRACBITS)) as u8
};
(
lerp(r00, r10, r01, r11),
lerp(g00, g10, g01, g11),
lerp(b00, b10, b01, b11),
)
}
#[inline]
fn bilinear_from_rows(row0: &[u8], row1: &[u8], width: u32, fx: u32, ty: u32) -> (u8, u8, u8) {
let w = width.saturating_sub(1) as usize;
let x0 = (fx >> FRACBITS) as usize;
let x0 = x0.min(w);
let x1 = (x0 + 1).min(w);
let tx = fx & FRAC_MASK;
let get = |row: &[u8], x: usize| -> (u8, u8, u8) {
let off = x * 4;
if let Some(q) = row.get(off..off + 4) {
(q[0], q[1], q[2])
} else {
(0, 0, 0)
}
};
let (r00, g00, b00) = get(row0, x0);
let (r10, g10, b10) = get(row0, x1);
let (r01, g01, b01) = get(row1, x0);
let (r11, g11, b11) = get(row1, x1);
let itx = FRAC - tx;
let ity = FRAC - ty;
let w00 = itx * ity;
let w10 = tx * ity;
let w01 = itx * ty;
let w11 = tx * ty;
let blend = |a: u8, b: u8, c: u8, d: u8| -> u8 {
((a as u32 * w00 + b as u32 * w10 + c as u32 * w01 + d as u32 * w11 + 128) >> 8) as u8
};
(
blend(r00, r10, r01, r11),
blend(g00, g10, g01, g11),
blend(b00, b10, b01, b11),
)
}
#[inline]
#[cfg_attr(not(test), allow(dead_code))]
fn sample_nearest(pm: &Pixmap, fx: u32, fy: u32) -> (u8, u8, u8) {
let x = ((fx + FRAC / 2) >> FRACBITS).min(pm.width.saturating_sub(1));
let y = ((fy + FRAC / 2) >> FRACBITS).min(pm.height.saturating_sub(1));
pm.get_rgb(x, y)
}
#[inline]
fn sample_area_avg(pm: &Pixmap, fx: u32, fy: u32, fx_step: u32, fy_step: u32) -> (u8, u8, u8) {
let (x0, x1) = area_range(pm.width, fx, fx_step);
let (y0, y1) = area_range(pm.height, fy, fy_step);
sample_area_avg_bounds(pm, x0, x1, y0, y1)
}
#[inline]
fn sample_area_avg_bounds(pm: &Pixmap, x0: u32, x1: u32, y0: u32, y1: u32) -> (u8, u8, u8) {
let cols = (x1 - x0) as usize;
let rows = (y1 - y0) as usize;
if cols <= 1 && rows <= 1 {
return pm.get_rgb(x0, y0);
}
let mut r_sum = 0u32;
let mut g_sum = 0u32;
let mut b_sum = 0u32;
let pw = pm.width as usize;
for sy in y0..y1 {
let row_off = sy as usize * pw * 4 + x0 as usize * 4;
if let Some(row) = pm.data.get(row_off..row_off + cols * 4) {
for chunk in row.chunks_exact(4) {
r_sum += chunk[0] as u32;
g_sum += chunk[1] as u32;
b_sum += chunk[2] as u32;
}
}
}
let count = (rows * cols) as u32;
if count == 0 {
return (255, 255, 255);
}
let half = count >> 1;
if count.is_power_of_two() {
let shift = count.trailing_zeros();
(
((r_sum + half) >> shift) as u8,
((g_sum + half) >> shift) as u8,
((b_sum + half) >> shift) as u8,
)
} else {
(
((r_sum + half) / count) as u8,
((g_sum + half) / count) as u8,
((b_sum + half) / count) as u8,
)
}
}
#[inline]
fn mask_box_coverage(
mask: &crate::bitmap::Bitmap,
fx: u32,
fy: u32,
fx_step: u32,
fy_step: u32,
) -> u8 {
let x0 = (fx >> FRACBITS).min(mask.width.saturating_sub(1));
let y0 = (fy >> FRACBITS).min(mask.height.saturating_sub(1));
let x1 = ((fx + fx_step) >> FRACBITS).min(mask.width);
let y1 = ((fy + fy_step) >> FRACBITS).min(mask.height);
let total = (x1 - x0) * (y1 - y0);
if total == 0 {
return 0;
}
let stride = mask.row_stride();
let byte_lo = x0 as usize / 8;
let byte_hi = (x1 as usize).div_ceil(8); let first_mask = 0xFF_u8 >> (x0 % 8);
let end_mask = if x1.is_multiple_of(8) {
0xFF_u8
} else {
0xFF_u8 << (8 - x1 % 8)
};
let mut count = 0u32;
if byte_hi == byte_lo + 1 {
let combined = first_mask & end_mask;
for sy in y0..y1 {
count += (mask.data[sy as usize * stride + byte_lo] & combined).count_ones();
}
} else {
for sy in y0..y1 {
let row = &mask.data[sy as usize * stride..];
count += (row[byte_lo] & first_mask).count_ones();
for byte in row[(byte_lo + 1)..(byte_hi - 1)].iter() {
count += byte.count_ones();
}
count += (row[byte_hi - 1] & end_mask).count_ones();
}
}
((count as u64 * 255 + total as u64 / 2) / total as u64) as u8
}
#[inline]
fn mask_box_center_fg(
mask: &crate::bitmap::Bitmap,
fx: u32,
fy: u32,
fx_step: u32,
fy_step: u32,
) -> (u32, u32) {
let cx = (fx + fx_step / 2) >> FRACBITS;
let cy = (fy + fy_step / 2) >> FRACBITS;
(
cx.min(mask.width.saturating_sub(1)),
cy.min(mask.height.saturating_sub(1)),
)
}
fn aa_downscale(pm: &Pixmap) -> Pixmap {
let out_w = (pm.width / 2).max(1);
let out_h = (pm.height / 2).max(1);
let mut out = Pixmap::white(out_w, out_h);
for y in 0..out_h {
for x in 0..out_w {
let sx = (x * 2).min(pm.width.saturating_sub(1));
let sy = (y * 2).min(pm.height.saturating_sub(1));
let sx1 = (sx + 1).min(pm.width.saturating_sub(1));
let sy1 = (sy + 1).min(pm.height.saturating_sub(1));
let (r00, g00, b00) = pm.get_rgb(sx, sy);
let (r10, g10, b10) = pm.get_rgb(sx1, sy);
let (r01, g01, b01) = pm.get_rgb(sx, sy1);
let (r11, g11, b11) = pm.get_rgb(sx1, sy1);
let avg = |a: u8, b: u8, c: u8, d: u8| -> u8 {
((a as u32 + b as u32 + c as u32 + d as u32 + 2) / 4) as u8
};
out.set_rgb(
x,
y,
avg(r00, r10, r01, r11),
avg(g00, g10, g01, g11),
avg(b00, b10, b01, b11),
);
}
}
out
}
fn rotation_to_steps(r: crate::info::Rotation) -> u8 {
use crate::info::Rotation;
match r {
Rotation::None => 0,
Rotation::Cw90 => 1,
Rotation::Rot180 => 2,
Rotation::Ccw90 => 3,
}
}
fn user_rotation_to_steps(r: UserRotation) -> u8 {
match r {
UserRotation::None => 0,
UserRotation::Cw90 => 1,
UserRotation::Rot180 => 2,
UserRotation::Ccw90 => 3,
}
}
fn combine_rotations(info: crate::info::Rotation, user: UserRotation) -> crate::info::Rotation {
use crate::info::Rotation;
let steps = (rotation_to_steps(info) + user_rotation_to_steps(user)) % 4;
match steps {
0 => Rotation::None,
1 => Rotation::Cw90,
2 => Rotation::Rot180,
3 => Rotation::Ccw90,
_ => unreachable!(),
}
}
fn rotate_pixmap(src: Pixmap, rotation: crate::info::Rotation) -> Pixmap {
use crate::info::Rotation;
match rotation {
Rotation::None => src,
Rotation::Cw90 => {
let w = src.height;
let h = src.width;
let mut out = Pixmap::white(w, h);
const TILE: u32 = 32;
let (sw, sh, ow) = (src.width as usize, src.height as usize, w as usize);
let mut ty = 0;
while ty < src.height {
let mut tx = 0;
while tx < src.width {
let y_end = (ty + TILE).min(src.height);
let x_end = (tx + TILE).min(src.width);
for y in ty..y_end {
let src_row = y as usize * sw * 4;
let dst_col = sh - 1 - y as usize;
for x in tx..x_end {
let si = src_row + x as usize * 4;
let di = (x as usize * ow + dst_col) * 4;
out.data[di..di + 4].copy_from_slice(&src.data[si..si + 4]);
}
}
tx += TILE;
}
ty += TILE;
}
out
}
Rotation::Rot180 => {
let mut out = Pixmap::white(src.width, src.height);
for y in 0..src.height {
for x in 0..src.width {
let (r, g, b) = src.get_rgb(x, y);
out.set_rgb(src.width - 1 - x, src.height - 1 - y, r, g, b);
}
}
out
}
Rotation::Ccw90 => {
let w = src.height;
let h = src.width;
let mut out = Pixmap::white(w, h);
const TILE: u32 = 32;
let (sw, ow) = (src.width as usize, w as usize);
let mut ty = 0;
while ty < src.height {
let mut tx = 0;
while tx < src.width {
let y_end = (ty + TILE).min(src.height);
let x_end = (tx + TILE).min(src.width);
for y in ty..y_end {
let src_row = y as usize * sw * 4;
for x in tx..x_end {
let si = src_row + x as usize * 4;
let di = ((sw - 1 - x as usize) * ow + y as usize) * 4;
out.data[di..di + 4].copy_from_slice(&src.data[si..si + 4]);
}
}
tx += TILE;
}
ty += TILE;
}
out
}
}
}
use crate::fgbz::{FgbzPalette, PaletteColor, parse_fgbz};
fn best_iw44_subsample(scale: f32) -> u32 {
if scale <= 0.0 || !scale.is_finite() || scale >= 1.0 {
return 1;
}
let max_sub = (1.5_f32 / scale).round() as u32;
let mut s = 1u32;
while s * 2 <= max_sub {
s *= 2;
}
s.min(8)
}
#[cfg(feature = "std")]
#[derive(Default)]
pub(crate) struct PageLayers {
bg44: std::sync::OnceLock<Option<Iw44Image>>,
bg44_partial: std::sync::OnceLock<Option<Iw44Image>>,
mask: std::sync::OnceLock<Option<crate::bitmap::Bitmap>>,
mask_sub4: std::sync::OnceLock<Option<crate::bitmap::Bitmap>>,
fg44: std::sync::OnceLock<Option<Pixmap>>,
bg_rgb_s1: std::sync::OnceLock<Option<Pixmap>>,
bg_rgb_s2: std::sync::OnceLock<Option<Pixmap>>,
mask_indexed: std::sync::OnceLock<Option<(crate::bitmap::Bitmap, Vec<i32>)>>,
}
#[cfg(feature = "std")]
impl PageLayers {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn bg44(&self, page: &DjVuPage) -> Option<&Iw44Image> {
self.bg44
.get_or_init(|| {
let chunks = page.bg44_chunks();
if chunks.is_empty() {
return None;
}
let mut img = Iw44Image::new();
for chunk_data in &chunks {
if img.decode_chunk(chunk_data).is_err() {
break;
}
}
if img.width == 0 { None } else { Some(img) }
})
.as_ref()
}
pub(crate) fn bg44_partial(&self, page: &DjVuPage) -> Option<&Iw44Image> {
self.bg44_partial
.get_or_init(|| {
let chunks = page.bg44_chunks();
if chunks.is_empty() {
return None;
}
let mut img = Iw44Image::new();
if img.decode_chunk(chunks[0]).is_err() {
return None;
}
if img.width == 0 { None } else { Some(img) }
})
.as_ref()
}
pub(crate) fn mask(&self, page: &DjVuPage) -> Option<&crate::bitmap::Bitmap> {
self.mask
.get_or_init(|| page.extract_mask().ok().flatten())
.as_ref()
}
pub(crate) fn mask_sub4(&self, page: &DjVuPage) -> Option<&crate::bitmap::Bitmap> {
self.mask_sub4
.get_or_init(|| {
let src = self.mask(page)?;
Some(downsample_mask_4x(src))
})
.as_ref()
}
pub(crate) fn fg44(&self, page: &DjVuPage) -> Option<&Pixmap> {
self.fg44
.get_or_init(|| page.extract_foreground().ok().flatten())
.as_ref()
}
pub(crate) fn bg_rgb_s1(&self, page: &DjVuPage) -> Option<&Pixmap> {
self.bg_rgb_s1
.get_or_init(|| {
let img = self.bg44(page)?;
img.to_rgb_subsample(1).ok()
})
.as_ref()
}
pub(crate) fn bg_rgb_s2(&self, page: &DjVuPage) -> Option<&Pixmap> {
self.bg_rgb_s2
.get_or_init(|| {
let img = self.bg44(page)?;
img.to_rgb_subsample(2).ok()
})
.as_ref()
}
pub(crate) fn mask_indexed(
&self,
page: &DjVuPage,
) -> Option<&(crate::bitmap::Bitmap, Vec<i32>)> {
self.mask_indexed
.get_or_init(|| page.extract_mask_indexed().ok().flatten())
.as_ref()
}
}
#[cfg(feature = "std")]
fn downsample_mask_4x(src: &crate::bitmap::Bitmap) -> crate::bitmap::Bitmap {
let out_w = src.width.div_ceil(4);
let out_h = src.height.div_ceil(4);
let mut out = crate::bitmap::Bitmap::new(out_w, out_h);
for oy in 0..out_h {
for ox in 0..out_w {
'outer: for dy in 0..4u32 {
for dx in 0..4u32 {
let sx = ox * 4 + dx;
let sy = oy * 4 + dy;
if sx < src.width && sy < src.height && src.get(sx, sy) {
out.set(ox, oy, true);
break 'outer;
}
}
}
}
}
out
}
#[cfg(feature = "std")]
fn page_mask_sub4(page: &DjVuPage) -> Option<&crate::bitmap::Bitmap> {
page.render_layers().mask_sub4(page)
}
fn decode_background_chunks<'a>(
page: &'a DjVuPage,
max_chunks: usize,
subsample: u32,
) -> Result<Option<Cow<'a, Pixmap>>, RenderError> {
if max_chunks == usize::MAX {
let bg44_chunks = page.bg44_chunks();
if !bg44_chunks.is_empty() {
if subsample == 1 {
let _ = page
.decoded_bg44()
.ok_or(RenderError::Iw44(crate::Iw44Error::Invalid))?;
return Ok(page.decoded_bg_rgb_s1().map(Cow::Borrowed));
}
if subsample == 2 {
let _ = page
.decoded_bg44()
.ok_or(RenderError::Iw44(crate::Iw44Error::Invalid))?;
return Ok(page.decoded_bg_rgb_s2().map(Cow::Borrowed));
}
let img = if subsample >= 4 {
page.decoded_bg44_partial()
} else {
page.decoded_bg44()
};
let img = img.ok_or(RenderError::Iw44(crate::Iw44Error::Invalid))?;
return Ok(Some(Cow::Owned(img.to_rgb_subsample(subsample)?)));
}
} else {
let bg44_chunks = page.bg44_chunks();
if !bg44_chunks.is_empty() {
let mut img = Iw44Image::new();
for chunk_data in bg44_chunks.iter().take(max_chunks) {
img.decode_chunk(chunk_data)?;
}
return Ok(Some(Cow::Owned(img.to_rgb_subsample(subsample)?)));
}
}
#[cfg(feature = "std")]
if let Some(pm) = decode_bgjp(page)? {
return Ok(Some(Cow::Owned(pm)));
}
Ok(None)
}
fn decode_background_chunks_permissive<'a>(
page: &'a DjVuPage,
max_chunks: usize,
subsample: u32,
) -> Option<Cow<'a, Pixmap>> {
let bg44_chunks = page.bg44_chunks();
if !bg44_chunks.is_empty() {
if max_chunks == usize::MAX && subsample == 1 {
return page.decoded_bg_rgb_s1().map(Cow::Borrowed);
}
if max_chunks == usize::MAX && subsample == 2 {
return page.decoded_bg_rgb_s2().map(Cow::Borrowed);
}
let mut img = Iw44Image::new();
for chunk_data in bg44_chunks.iter().take(max_chunks) {
if img.decode_chunk(chunk_data).is_err() {
break; }
}
return img.to_rgb_subsample(subsample).ok().map(Cow::Owned);
}
#[cfg(feature = "std")]
{
decode_bgjp(page).ok().flatten().map(Cow::Owned)
}
#[cfg(not(feature = "std"))]
None
}
fn decode_mask<'a>(
page: &'a DjVuPage,
) -> Result<Option<Cow<'a, crate::bitmap::Bitmap>>, RenderError> {
match page.decoded_mask() {
Some(bm) => Ok(Some(Cow::Borrowed(bm))),
None if page.find_chunk(b"Sjbz").is_some() => {
page.extract_mask()
.map_err(RenderError::from)
.map(|opt| opt.map(Cow::Owned))
}
None => Ok(None),
}
}
type IndexedMask<'a> = (Cow<'a, crate::bitmap::Bitmap>, Cow<'a, [i32]>);
fn decode_mask_indexed<'a>(page: &'a DjVuPage) -> Result<Option<IndexedMask<'a>>, RenderError> {
match page.decoded_mask_indexed() {
Some((bm, blit)) => Ok(Some((Cow::Borrowed(bm), Cow::Borrowed(blit.as_slice())))),
None if page.find_chunk(b"Sjbz").is_some() || page.find_chunk(b"Smmr").is_some() => page
.extract_mask_indexed()
.map_err(RenderError::from)
.map(|opt| opt.map(|(bm, blit)| (Cow::Owned(bm), Cow::Owned(blit)))),
None => Ok(None),
}
}
fn decode_fg_palette_full(page: &DjVuPage) -> Result<Option<FgbzPalette>, RenderError> {
let fgbz = match page.find_chunk(b"FGbz") {
Some(data) => data,
None => return Ok(None),
};
let pal = parse_fgbz(fgbz)?;
if pal.colors.is_empty() {
return Ok(None);
}
Ok(Some(pal))
}
fn decode_fg44<'a>(page: &'a DjVuPage) -> Result<Option<Cow<'a, Pixmap>>, RenderError> {
let fg44_chunks = page.fg44_chunks();
if !fg44_chunks.is_empty() {
return Ok(page.decoded_fg44().map(Cow::Borrowed));
}
#[cfg(feature = "std")]
if let Some(pm) = decode_fgjp(page)? {
return Ok(Some(Cow::Owned(pm)));
}
Ok(None)
}
struct DecodedLayers<'a> {
bg: Option<Cow<'a, Pixmap>>,
fg_palette: Option<FgbzPalette>,
mask: Option<Cow<'a, crate::bitmap::Bitmap>>,
blit_map: Option<Cow<'a, [i32]>>,
fg44: Option<Cow<'a, Pixmap>>,
}
fn decode_layers<'a>(
page: &'a DjVuPage,
opts: &RenderOptions,
bg_subsample: u32,
bg_chunk_limit: usize,
) -> Result<DecodedLayers<'a>, RenderError> {
let bg;
let fg_palette;
let mask;
let blit_map;
let fg44;
if opts.permissive {
bg = decode_background_chunks_permissive(page, bg_chunk_limit, bg_subsample);
fg_palette = decode_fg_palette_full(page).ok().flatten();
let indexed = if fg_palette.is_some() {
decode_mask_indexed(page).ok().flatten()
} else {
None
};
if let Some((bm, bm_map)) = indexed {
mask = Some(bm);
blit_map = Some(bm_map);
} else {
mask = decode_mask(page).ok().flatten();
blit_map = None;
}
fg44 = decode_fg44(page).ok().flatten();
} else {
#[cfg(feature = "parallel")]
let (bg_res, fg_res) = rayon::join(
|| decode_background_chunks(page, bg_chunk_limit, bg_subsample),
|| decode_foreground_strict(page),
);
#[cfg(not(feature = "parallel"))]
let (bg_res, fg_res) = (
decode_background_chunks(page, bg_chunk_limit, bg_subsample),
decode_foreground_strict(page),
);
bg = bg_res?;
let fg = fg_res?;
fg_palette = fg.fg_palette;
mask = fg.mask;
blit_map = fg.blit_map;
fg44 = fg.fg44;
}
let mask = if opts.bold > 0 {
mask.map(|m| Cow::Owned(m.into_owned().dilate_n(opts.bold as u32)))
} else {
mask
};
Ok(DecodedLayers {
bg,
fg_palette,
mask,
blit_map,
fg44,
})
}
struct ForegroundLayers<'a> {
fg_palette: Option<FgbzPalette>,
mask: Option<Cow<'a, crate::bitmap::Bitmap>>,
blit_map: Option<Cow<'a, [i32]>>,
fg44: Option<Cow<'a, Pixmap>>,
}
fn decode_foreground_strict<'a>(page: &'a DjVuPage) -> Result<ForegroundLayers<'a>, RenderError> {
let fg_palette = decode_fg_palette_full(page)?;
let (mask, blit_map) = if fg_palette.is_some() {
match decode_mask_indexed(page)? {
Some((bm, bm_map)) => (Some(bm), Some(bm_map)),
None => (None, None),
}
} else {
(decode_mask(page)?, None)
};
let fg44 = decode_fg44(page)?;
Ok(ForegroundLayers {
fg_palette,
mask,
blit_map,
fg44,
})
}
#[cfg(feature = "std")]
fn decode_bgjp(page: &DjVuPage) -> Result<Option<Pixmap>, RenderError> {
let data = match page.find_chunk(b"BGjp") {
Some(d) => d,
None => return Ok(None),
};
Ok(Some(decode_jpeg_to_pixmap(data)?))
}
#[cfg(feature = "std")]
fn decode_fgjp(page: &DjVuPage) -> Result<Option<Pixmap>, RenderError> {
let data = match page.find_chunk(b"FGjp") {
Some(d) => d,
None => return Ok(None),
};
Ok(Some(decode_jpeg_to_pixmap(data)?))
}
#[cfg(feature = "std")]
fn decode_jpeg_to_pixmap(data: &[u8]) -> Result<Pixmap, RenderError> {
use zune_jpeg::JpegDecoder;
use zune_jpeg::zune_core::bytestream::ZCursor;
let cursor = ZCursor::new(data);
let mut decoder = JpegDecoder::new(cursor);
decoder
.decode_headers()
.map_err(|e| RenderError::Jpeg(format!("{e:?}")))?;
let info = decoder
.info()
.ok_or_else(|| RenderError::Jpeg("missing image info after decode_headers".to_owned()))?;
let w = info.width as usize;
let h = info.height as usize;
let rgb = decoder
.decode()
.map_err(|e| RenderError::Jpeg(format!("{e:?}")))?;
let pixel_count = w * h;
let rgb = if rgb.len() >= pixel_count * 3 {
rgb
} else {
let mut padded = rgb;
padded.resize(pixel_count * 3, 0);
padded
};
let mut rgba = vec![0u8; pixel_count * 4];
rgb_to_rgba(&rgb[..pixel_count * 3], &mut rgba);
Ok(Pixmap {
width: w as u32,
height: h as u32,
data: rgba,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RenderRect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
struct CompositeContext<'a> {
opts: &'a RenderOptions,
page_w: u32,
page_h: u32,
bg: Option<&'a Pixmap>,
bg_x_q24: u64,
bg_y_q24: u64,
mask: Option<&'a crate::bitmap::Bitmap>,
mask_shift: u32,
fg_palette: Option<&'a FgbzPalette>,
blit_map: Option<&'a [i32]>,
fg44: Option<&'a Pixmap>,
fg_x_q24: u64,
fg_y_q24: u64,
gamma_lut: &'a [u8; 256],
gamma_is_identity: bool,
offset_x: u32,
offset_y: u32,
out_w: u32,
out_h: u32,
}
#[derive(Clone, Copy)]
struct AreaAvgX {
fx: u32,
bg_fx: u32,
bg_x0: u32,
bg_x1: u32,
}
fn area_range(limit: u32, f: u32, step: u32) -> (u32, u32) {
(
(f >> FRACBITS).min(limit.saturating_sub(1)),
((f + step) >> FRACBITS).min(limit),
)
}
fn precompute_area_avg_x(
ctx: &CompositeContext<'_>,
fx_step: u32,
bg_fx_step: u32,
) -> Vec<AreaAvgX> {
let mut xs = Vec::with_capacity(ctx.out_w as usize);
let bg_w = ctx.bg.map_or(0, |bg| bg.width);
for ox in 0..ctx.out_w {
let fx = (ox + ctx.offset_x) * fx_step;
let bg_fx = ((fx as u64 * ctx.bg_x_q24) >> 24) as u32;
let (bg_x0, bg_x1) = area_range(bg_w, bg_fx, bg_fx_step);
xs.push(AreaAvgX {
fx,
bg_fx,
bg_x0,
bg_x1,
});
}
xs
}
impl<'a> CompositeContext<'a> {
#[allow(clippy::too_many_arguments)]
fn from_layers(
page: &DjVuPage,
opts: &'a RenderOptions,
bg: Option<&'a Pixmap>,
mask: Option<&'a crate::bitmap::Bitmap>,
mask_shift: u32,
fg_palette: Option<&'a FgbzPalette>,
blit_map: Option<&'a [i32]>,
fg44: Option<&'a Pixmap>,
gamma_lut: &'a [u8; 256],
offset: (u32, u32),
out: (u32, u32),
) -> Self {
let page_w = page.width() as u32;
let page_h = page.height() as u32;
let (fg_x_q24, fg_y_q24) = fg_q24(fg44, page_w, page_h);
let (bg_x_q24, bg_y_q24) = bg_q24(bg, page_w, page_h);
CompositeContext {
opts,
page_w,
page_h,
bg,
bg_x_q24,
bg_y_q24,
mask,
mask_shift,
fg_palette,
blit_map,
fg44,
fg_x_q24,
fg_y_q24,
gamma_lut,
gamma_is_identity: gamma_lut.iter().enumerate().all(|(i, &v)| v == i as u8),
offset_x: offset.0,
offset_y: offset.1,
out_w: out.0,
out_h: out.1,
}
}
}
#[cfg(feature = "std")]
fn resolve_sub4_mask<'a>(
page: &'a DjVuPage,
bg_subsample: u32,
opts: &RenderOptions,
full_mask: Option<&'a crate::bitmap::Bitmap>,
fg_palette: Option<&FgbzPalette>,
) -> (Option<&'a crate::bitmap::Bitmap>, u32) {
if bg_subsample >= 4 && opts.bold == 0 && fg_palette.is_none() {
(page_mask_sub4(page), 2)
} else {
(full_mask, 0)
}
}
#[cfg(not(feature = "std"))]
fn resolve_sub4_mask<'a>(
_page: &'a DjVuPage,
_bg_subsample: u32,
_opts: &RenderOptions,
full_mask: Option<&'a crate::bitmap::Bitmap>,
_fg_palette: Option<&FgbzPalette>,
) -> (Option<&'a crate::bitmap::Bitmap>, u32) {
(full_mask, 0)
}
#[inline]
fn lookup_palette_color(
pal: &FgbzPalette,
blit_map: Option<&[i32]>,
mask: Option<&crate::bitmap::Bitmap>,
px: u32,
py: u32,
) -> PaletteColor {
if let Some(bm) = blit_map
&& let Some(m) = mask
{
let mi = py as usize * m.width as usize + px as usize;
if mi < bm.len() {
let blit_idx = bm[mi];
if blit_idx >= 0 {
if !pal.indices.is_empty() {
let bi = blit_idx as usize;
if bi < pal.indices.len() {
let ci = pal.indices[bi] as usize;
if ci < pal.colors.len() {
return pal.colors[ci];
}
}
} else {
let ci = blit_idx as usize;
if ci < pal.colors.len() {
return pal.colors[ci];
}
}
}
}
}
pal.colors.first().copied().unwrap_or_default()
}
fn composite_into(ctx: &CompositeContext<'_>, buf: &mut [u8]) -> Result<(), RenderError> {
let full_w = ctx.opts.width;
let full_h = ctx.opts.height;
let fx_step = ((ctx.page_w as u64 * FRAC as u64) / full_w.max(1) as u64) as u32;
let fy_step = ((ctx.page_h as u64 * FRAC as u64) / full_h.max(1) as u64) as u32;
let row_stride = ctx.out_w as usize * 4;
if ctx.bg.is_none() && ctx.fg44.is_none() && ctx.fg_palette.is_none() {
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
let n = ctx.out_h as usize * row_stride;
buf[..n]
.par_chunks_exact_mut(row_stride)
.enumerate()
.for_each(|(oy, row)| {
composite_rows_bilevel_one(ctx, oy as u32, fx_step, fy_step, row);
});
}
#[cfg(not(feature = "parallel"))]
for (oy, row) in buf[..ctx.out_h as usize * row_stride]
.chunks_exact_mut(row_stride)
.enumerate()
{
composite_rows_bilevel_one(ctx, oy as u32, fx_step, fy_step, row);
}
return Ok(());
}
let downscale = fx_step > FRAC || fy_step > FRAC;
let bg_fx_step = ((fx_step as u64 * ctx.bg_x_q24) >> 24) as u32;
let bg_fy_step = ((fy_step as u64 * ctx.bg_y_q24) >> 24) as u32;
let area_avg_x = downscale.then(|| precompute_area_avg_x(ctx, fx_step, bg_fx_step));
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
let n = ctx.out_h as usize * row_stride;
buf[..n]
.par_chunks_exact_mut(row_stride)
.enumerate()
.for_each(|(oy, row)| {
if downscale {
composite_rows_area_avg_one(
ctx,
oy as u32,
fx_step,
fy_step,
bg_fx_step,
bg_fy_step,
row,
area_avg_x.as_deref(),
);
} else {
composite_rows_bilinear_one(ctx, oy as u32, fx_step, fy_step, row);
}
});
}
#[cfg(not(feature = "parallel"))]
for (oy, row) in buf[..ctx.out_h as usize * row_stride]
.chunks_exact_mut(row_stride)
.enumerate()
{
if downscale {
composite_rows_area_avg_one(
ctx,
oy as u32,
fx_step,
fy_step,
bg_fx_step,
bg_fy_step,
row,
area_avg_x.as_deref(),
);
} else {
composite_rows_bilinear_one(ctx, oy as u32, fx_step, fy_step, row);
}
}
Ok(())
}
fn composite_rows<F>(ctx: &CompositeContext<'_>, mut sink: F) -> Result<(), RenderError>
where
F: FnMut(usize, &[u8]),
{
let full_w = ctx.opts.width;
let full_h = ctx.opts.height;
let fx_step = ((ctx.page_w as u64 * FRAC as u64) / full_w.max(1) as u64) as u32;
let fy_step = ((ctx.page_h as u64 * FRAC as u64) / full_h.max(1) as u64) as u32;
let row_stride = ctx.out_w as usize * 4;
if ctx.bg.is_none() && ctx.fg44.is_none() && ctx.fg_palette.is_none() {
let mut row_buf = vec![0u8; row_stride];
for oy in 0..ctx.out_h {
composite_rows_bilevel_one(ctx, oy, fx_step, fy_step, &mut row_buf);
sink(oy as usize, &row_buf);
}
return Ok(());
}
let mut row_buf = vec![0u8; row_stride];
let downscale = fx_step > FRAC || fy_step > FRAC;
let bg_fx_step = ((fx_step as u64 * ctx.bg_x_q24) >> 24) as u32;
let bg_fy_step = ((fy_step as u64 * ctx.bg_y_q24) >> 24) as u32;
let area_avg_x = downscale.then(|| precompute_area_avg_x(ctx, fx_step, bg_fx_step));
for oy in 0..ctx.out_h {
if downscale {
composite_rows_area_avg_one(
ctx,
oy,
fx_step,
fy_step,
bg_fx_step,
bg_fy_step,
&mut row_buf,
area_avg_x.as_deref(),
);
} else {
composite_rows_bilinear_one(ctx, oy, fx_step, fy_step, &mut row_buf);
}
sink(oy as usize, &row_buf);
}
Ok(())
}
#[inline]
fn composite_rows_bilevel_one(
ctx: &CompositeContext<'_>,
oy: u32,
fx_step: u32,
fy_step: u32,
row_buf: &mut [u8],
) {
let mask = match ctx.mask {
Some(m) => m,
None => {
for chunk in row_buf.chunks_exact_mut(4) {
chunk[0] = 255;
chunk[1] = 255;
chunk[2] = 255;
chunk[3] = 255;
}
return;
}
};
if fx_step == FRAC && fy_step == FRAC {
let stride = mask.row_stride();
let py = (oy + ctx.offset_y).min(ctx.page_h.saturating_sub(1)) as usize;
let mask_row = &mask.data[py * stride..(py + 1) * stride];
if !mask_row.iter().any(|&b| b != 0) {
row_buf.fill(255);
return;
}
let out_w = row_buf.len() / 4;
let ox0 = ctx.offset_x as usize;
if ctx.offset_x.is_multiple_of(8) && ox0 + out_w <= mask.width as usize {
let mb0 = ox0 / 8; let nb_full = out_w / 8; let nb_rem = out_w % 8; for byte_idx in 0..nb_full {
let mb = mask_row[mb0 + byte_idx];
let src = &BILEVEL_RGBA[mb as usize];
row_buf[byte_idx * 32..(byte_idx + 1) * 32].copy_from_slice(src);
}
if nb_rem > 0 {
let mb = mask_row[mb0 + nb_full];
let src = &BILEVEL_RGBA[mb as usize];
let base = nb_full * 32;
row_buf[base..base + nb_rem * 4].copy_from_slice(&src[..nb_rem * 4]);
}
return;
}
for (ox, pixel) in row_buf.chunks_exact_mut(4).enumerate() {
let px = (ox as u32 + ctx.offset_x).min(ctx.page_w.saturating_sub(1)) as usize;
let is_fg = ((mask_row[px >> 3] >> (7 - (px & 7))) & 1) as u32;
let ch = (is_fg.wrapping_sub(1) & 0xFF) as u8; pixel[0] = ch;
pixel[1] = ch;
pixel[2] = ch;
pixel[3] = 255;
}
return;
}
let downscale = fx_step > FRAC || fy_step > FRAC;
let fy = (oy + ctx.offset_y) * fy_step;
let py = (fy >> FRACBITS).min(ctx.page_h.saturating_sub(1));
if downscale && ctx.mask_shift == 0 {
let stride = mask.row_stride();
let y0 = (fy >> FRACBITS).min(mask.height.saturating_sub(1)) as usize;
let y1 = ((fy + fy_step) >> FRACBITS).min(mask.height) as usize;
if y1 <= y0 || !mask.data[y0 * stride..y1 * stride].iter().any(|&b| b != 0) {
row_buf.fill(255);
return;
}
}
for (ox, pixel) in row_buf.chunks_exact_mut(4).enumerate() {
let fx = (ox as u32 + ctx.offset_x) * fx_step;
let px = (fx >> FRACBITS).min(ctx.page_w.saturating_sub(1));
let ch = if downscale {
if ctx.mask_shift > 0 {
let dpx = fx >> (FRACBITS + ctx.mask_shift);
let dpy = fy >> (FRACBITS + ctx.mask_shift);
if dpx < mask.width && dpy < mask.height && mask.get(dpx, dpy) {
0u8
} else {
255u8
}
} else {
255 - mask_box_coverage(mask, fx, fy, fx_step, fy_step)
}
} else if px < mask.width && py < mask.height && mask.get(px, py) {
0u8
} else {
255u8
};
pixel[0] = ch;
pixel[1] = ch;
pixel[2] = ch;
pixel[3] = 255;
}
}
#[inline]
fn composite_rows_bilinear_one(
ctx: &CompositeContext<'_>,
oy: u32,
fx_step: u32,
fy_step: u32,
row_buf: &mut [u8],
) {
let (page_w, page_h) = (ctx.page_w, ctx.page_h);
let fy = (oy + ctx.offset_y) * fy_step;
let py = (fy >> FRACBITS).min(page_h.saturating_sub(1));
if fx_step == FRAC && fy_step == FRAC && ctx.bg_x_q24 == (1 << 24) && ctx.bg_y_q24 == (1 << 24)
{
if ctx.offset_x == 0
&& ctx.fg_palette.is_none()
&& ctx.fg44.is_none()
&& let Some(bg) = ctx.bg
{
let bg_py = py.min(bg.height.saturating_sub(1)) as usize;
let bg_stride = bg.width as usize * 4;
let bg_row = bg.data.get(bg_py * bg_stride..).unwrap_or(&[]);
let lut = &ctx.gamma_lut;
if let Some(mask) = ctx.mask {
let mask_stride = mask.row_stride();
let mask_py = py.min(mask.height.saturating_sub(1)) as usize;
let mask_row = mask.data.get(mask_py * mask_stride..).unwrap_or(&[]);
let bg_max_px = (bg.width as usize).saturating_sub(1);
let mask_limit = mask.width as usize;
let out_w = row_buf.len() / 4;
macro_rules! a2_has_mask_loop {
($write:expr) => {
for mb_idx in 0..out_w.div_ceil(8) {
let mb = mask_row.get(mb_idx).copied().unwrap_or(0);
let exp = &MASK_EXPAND[mb as usize];
for j in 0..8usize {
let ox = mb_idx * 8 + j;
if ox >= out_w {
break;
}
let fg_m = if ox < mask_limit { exp[j] } else { 0u8 };
let px = ox.min(bg_max_px);
let off = px * 4;
let pixel = &mut row_buf[ox * 4..(ox + 1) * 4];
let (r, g, b) = if let Some(q) = bg_row.get(off..off + 4) {
(q[0] & !fg_m, q[1] & !fg_m, q[2] & !fg_m)
} else {
(!fg_m, !fg_m, !fg_m)
};
$write(pixel, r, g, b);
}
}
};
}
if ctx.gamma_is_identity {
a2_has_mask_loop!(|pixel: &mut [u8], r, g, b| {
pixel[0] = r;
pixel[1] = g;
pixel[2] = b;
pixel[3] = 255;
});
} else {
a2_has_mask_loop!(|pixel: &mut [u8], r, g, b| {
pixel[0] = lut[r as usize];
pixel[1] = lut[g as usize];
pixel[2] = lut[b as usize];
pixel[3] = 255;
});
}
} else {
if ctx.gamma_is_identity {
let out_w = row_buf.len() / 4;
if bg.width as usize >= out_w {
row_buf[..out_w * 4].copy_from_slice(&bg_row[..out_w * 4]);
} else {
for (ox, pixel) in row_buf.chunks_exact_mut(4).enumerate() {
let px = ox.min((bg.width as usize).saturating_sub(1));
let off = px * 4;
if let Some(q) = bg_row.get(off..off + 4) {
pixel[0] = q[0];
pixel[1] = q[1];
pixel[2] = q[2];
} else {
pixel[0] = 255;
pixel[1] = 255;
pixel[2] = 255;
}
pixel[3] = 255;
}
}
} else {
for (ox, pixel) in row_buf.chunks_exact_mut(4).enumerate() {
let px = ox.min((bg.width as usize).saturating_sub(1));
let off = px * 4;
if let Some(q) = bg_row.get(off..off + 4) {
pixel[0] = lut[q[0] as usize];
pixel[1] = lut[q[1] as usize];
pixel[2] = lut[q[2] as usize];
} else {
pixel[0] = 255;
pixel[1] = 255;
pixel[2] = 255;
}
pixel[3] = 255;
}
}
}
return;
}
let fg_rows_1x1 = ctx.fg44.filter(|_| ctx.fg_palette.is_none()).map(|fg| {
let fg_fy = map_plane_center_frac(fy, ctx.fg_y_q24);
let y0 = (fg_fy >> FRACBITS).min(fg.height.saturating_sub(1)) as usize;
let y1 = (y0 + 1).min(fg.height.saturating_sub(1) as usize);
let ty = fg_fy & FRAC_MASK;
let stride = fg.width as usize * 4;
let row0 = fg.data.get(y0 * stride..).unwrap_or(&[]);
let row1 = fg.data.get(y1 * stride..).unwrap_or(&[]);
(row0, row1, fg.width, ty)
});
let bg_row_1x1 = ctx.bg.and_then(|bg| {
let by = (py as usize).min(bg.height.saturating_sub(1) as usize);
let stride = bg.width as usize * 4;
bg.data.get(by * stride..).map(|row| (row, bg.width))
});
let mask_row_1x1 = ctx.mask.and_then(|m| {
if py >= m.height {
return None;
}
let stride = m.row_stride();
m.data.get(py as usize * stride..).map(|row| (row, m.width))
});
{
let row_is_all_bg = match mask_row_1x1 {
None => true,
Some((mask_row, mask_w)) => {
let check_bytes = (mask_w as usize).div_ceil(8).min(mask_row.len());
mask_row[..check_bytes].iter().all(|&b| b == 0)
}
};
if row_is_all_bg && ctx.gamma_is_identity {
let out_w = row_buf.len() / 4;
let offset_x = ctx.offset_x as usize;
if let Some((bg_row, bg_w)) = bg_row_1x1 {
if offset_x + out_w <= bg_w as usize {
row_buf.copy_from_slice(&bg_row[offset_x * 4..(offset_x + out_w) * 4]);
return;
}
} else {
row_buf.fill(255);
return;
}
} else if row_is_all_bg {
let out_w = row_buf.len() / 4;
let offset_x = ctx.offset_x as usize;
let lut = &ctx.gamma_lut;
if let Some((bg_row, bg_w)) = bg_row_1x1 {
if offset_x + out_w <= bg_w as usize {
let src = &bg_row[offset_x * 4..(offset_x + out_w) * 4];
for (chunk, s) in row_buf.chunks_exact_mut(4).zip(src.chunks_exact(4)) {
chunk[0] = lut[s[0] as usize];
chunk[1] = lut[s[1] as usize];
chunk[2] = lut[s[2] as usize];
chunk[3] = 255;
}
return;
}
} else {
let white = lut[255];
for chunk in row_buf.chunks_exact_mut(4) {
chunk[0] = white;
chunk[1] = white;
chunk[2] = white;
chunk[3] = 255;
}
return;
}
}
}
const G1_MAX: usize = 4096;
let mut g1_buf = [0u8; G1_MAX];
let g1_mask: &[u8] = if let Some((mask_row, mask_w)) = mask_row_1x1 {
let mw = mask_w as usize;
if mw <= G1_MAX {
let nb = mw.div_ceil(8);
for (i, &mb) in mask_row[..nb].iter().enumerate() {
let exp = &MASK_EXPAND[mb as usize];
let base = i * 8;
g1_buf[base..base + 8].copy_from_slice(exp);
}
&g1_buf[..mw]
} else {
&g1_buf[..0] }
} else {
&g1_buf[..0] };
for (ox, pixel) in row_buf.chunks_exact_mut(4).enumerate() {
let fx = (ox as u32 + ctx.offset_x) * fx_step;
let px = (fx >> FRACBITS).min(page_w.saturating_sub(1));
let is_fg = if g1_mask.is_empty() {
mask_row_1x1.is_some_and(|(row, mask_w)| {
let pxu = px as usize;
pxu < mask_w as usize
&& (row.get(pxu >> 3).copied().unwrap_or(0) >> (7 - (pxu & 7))) & 1 != 0
})
} else {
g1_mask.get(px as usize).copied().unwrap_or(0) != 0
};
let (r, g, b) = if is_fg {
if let Some(pal) = ctx.fg_palette {
let color = lookup_palette_color(pal, ctx.blit_map, ctx.mask, px, py);
(color.r, color.g, color.b)
} else if let Some((fg_row0, fg_row1, fg_w, fg_ty)) = fg_rows_1x1 {
let fg_fx = map_plane_center_frac(fx, ctx.fg_x_q24);
bilinear_from_rows(fg_row0, fg_row1, fg_w, fg_fx, fg_ty)
} else {
(0, 0, 0)
}
} else if let Some((bg_row, bg_w)) = bg_row_1x1 {
let bx = (px as usize).min(bg_w.saturating_sub(1) as usize);
let off = bx * 4;
bg_row
.get(off..off + 4)
.map_or((255, 255, 255), |q| (q[0], q[1], q[2]))
} else {
(255, 255, 255)
};
if ctx.gamma_is_identity {
pixel[0] = r;
pixel[1] = g;
pixel[2] = b;
} else {
pixel[0] = ctx.gamma_lut[r as usize];
pixel[1] = ctx.gamma_lut[g as usize];
pixel[2] = ctx.gamma_lut[b as usize];
}
pixel[3] = 255;
}
return;
}
let bg_fy_hoist = ctx.bg.map(|_| map_plane_center_frac(fy, ctx.bg_y_q24));
let bg_fx_step_q: u64 = fx_step as u64 * ctx.bg_x_q24;
let mut bg_fx_q: u64 = (ctx.offset_x as u64 * fx_step as u64 + FRAC as u64 / 2) * ctx.bg_x_q24;
let mask_hoist = ctx.mask.and_then(|m| {
if py >= m.height {
return None;
}
let stride = m.row_stride();
m.data.get(py as usize * stride..).map(|row| (row, m.width))
});
let mask_all_bg = match mask_hoist {
None => true,
Some((mask_row, mask_w)) => {
let nb = (mask_w as usize).div_ceil(8).min(mask_row.len());
!mask_row[..nb].iter().any(|&b| b != 0)
}
};
let bg_rows = ctx.bg.map(|bg| {
let bg_fy = bg_fy_hoist.unwrap_or(0);
let y0 = (bg_fy >> FRACBITS).min(bg.height.saturating_sub(1)) as usize;
let y1 = (y0 + 1).min(bg.height.saturating_sub(1) as usize);
let ty = bg_fy & FRAC_MASK;
let stride = bg.width as usize * 4;
let row0 = bg.data.get(y0 * stride..).unwrap_or(&[]);
let row1 = bg.data.get(y1 * stride..).unwrap_or(&[]);
(row0, row1, bg.width, ty)
});
for (ox, pixel) in row_buf.chunks_exact_mut(4).enumerate() {
let fx = (ox as u32 + ctx.offset_x) * fx_step;
let px = (fx >> FRACBITS).min(page_w.saturating_sub(1));
let is_fg = !mask_all_bg
&& mask_hoist.is_some_and(|(mask_row, mask_w)| {
let pxu = px as usize;
pxu < mask_w as usize
&& (mask_row.get(pxu >> 3).copied().unwrap_or(0) >> (7 - (pxu & 7))) & 1 != 0
});
let (r, g, b) = if is_fg {
if let Some(pal) = ctx.fg_palette {
let color = lookup_palette_color(pal, ctx.blit_map, ctx.mask, px, py);
(color.r, color.g, color.b)
} else if let Some(fg) = ctx.fg44 {
let fg_fx = map_plane_center_frac(fx, ctx.fg_x_q24);
let fg_fy = map_plane_center_frac(fy, ctx.fg_y_q24);
sample_bilinear(fg, fg_fx, fg_fy)
} else {
(0, 0, 0)
}
} else if let Some((row0, row1, bg_w, ty)) = bg_rows {
let bg_fx = ((bg_fx_q >> 24) as u32).saturating_sub(FRAC / 2);
bilinear_from_rows(row0, row1, bg_w, bg_fx, ty)
} else {
(255, 255, 255)
};
if ctx.gamma_is_identity {
pixel[0] = r;
pixel[1] = g;
pixel[2] = b;
} else {
pixel[0] = ctx.gamma_lut[r as usize];
pixel[1] = ctx.gamma_lut[g as usize];
pixel[2] = ctx.gamma_lut[b as usize];
}
pixel[3] = 255;
bg_fx_q = bg_fx_q.wrapping_add(bg_fx_step_q);
}
}
#[inline]
#[allow(clippy::too_many_arguments)]
fn composite_rows_area_avg_one(
ctx: &CompositeContext<'_>,
oy: u32,
fx_step: u32,
fy_step: u32,
bg_fx_step: u32,
bg_fy_step: u32,
row_buf: &mut [u8],
area_avg_x: Option<&[AreaAvgX]>,
) {
let fy = (oy + ctx.offset_y) * fy_step;
let bg_fy = ((fy as u64 * ctx.bg_y_q24) >> 24) as u32;
let bg_y = ctx.bg.map(|bg| area_range(bg.height, bg_fy, bg_fy_step));
let mask_all_bg = ctx.mask_shift == 0
&& ctx.mask.is_none_or(|m| {
let stride = m.row_stride();
let y0 = (fy >> FRACBITS).min(m.height.saturating_sub(1)) as usize;
let y1 = ((fy + fy_step) >> FRACBITS).min(m.height) as usize;
y1 <= y0 || !m.data[y0 * stride..y1 * stride].iter().any(|&b| b != 0)
});
for (ox, pixel) in row_buf.chunks_exact_mut(4).enumerate() {
let fallback;
let ax = if let Some(ax) = area_avg_x.and_then(|xs| xs.get(ox)) {
*ax
} else {
let fx = (ox as u32 + ctx.offset_x) * fx_step;
let bg_fx = ((fx as u64 * ctx.bg_x_q24) >> 24) as u32;
let (bg_x0, bg_x1) = area_range(ctx.bg.map_or(0, |bg| bg.width), bg_fx, bg_fx_step);
fallback = AreaAvgX {
fx,
bg_fx,
bg_x0,
bg_x1,
};
fallback
};
let fx = ax.fx;
let coverage: u8 = if mask_all_bg {
0
} else if let Some(m) = ctx.mask {
if ctx.mask_shift > 0 {
let px = fx >> (FRACBITS + ctx.mask_shift);
let pym = fy >> (FRACBITS + ctx.mask_shift);
if px < m.width && pym < m.height && m.get(px, pym) {
255
} else {
0
}
} else {
mask_box_coverage(m, fx, fy, fx_step, fy_step)
}
} else {
0
};
let bg_sample = || -> (u8, u8, u8) {
if let Some(bg) = ctx.bg {
if let Some((bg_y0, bg_y1)) = bg_y {
sample_area_avg_bounds(bg, ax.bg_x0, ax.bg_x1, bg_y0, bg_y1)
} else {
sample_area_avg(bg, ax.bg_fx, bg_fy, bg_fx_step, bg_fy_step)
}
} else {
(255, 255, 255)
}
};
let fg_sample = || -> (u8, u8, u8) {
if let Some(pal) = ctx.fg_palette {
let (cx, cy) = mask_box_center_fg(ctx.mask.unwrap(), fx, fy, fx_step, fy_step);
let color = lookup_palette_color(pal, ctx.blit_map, ctx.mask, cx, cy);
(color.r, color.g, color.b)
} else if let Some(fg) = ctx.fg44 {
let fg_fx = ((fx as u64 * ctx.fg_x_q24) >> 24) as u32;
let fg_fy = ((fy as u64 * ctx.fg_y_q24) >> 24) as u32;
let fg_fx_step = ((fx_step as u64 * ctx.fg_x_q24) >> 24) as u32;
let fg_fy_step = ((fy_step as u64 * ctx.fg_y_q24) >> 24) as u32;
sample_area_avg(fg, fg_fx, fg_fy, fg_fx_step, fg_fy_step)
} else {
(0, 0, 0)
}
};
let (r, g, b) = if coverage == 0 {
bg_sample()
} else if coverage == 255 {
fg_sample()
} else {
let (fr, fg_, fb) = fg_sample();
let (br, bg_, bb) = bg_sample();
let c = coverage as u32;
let ic = 255 - c;
let mix = |f: u8, b: u8| ((c * f as u32 + ic * b as u32 + 127) / 255) as u8;
(mix(fr, br), mix(fg_, bg_), mix(fb, bb))
};
if ctx.gamma_is_identity {
pixel[0] = r;
pixel[1] = g;
pixel[2] = b;
} else {
pixel[0] = ctx.gamma_lut[r as usize];
pixel[1] = ctx.gamma_lut[g as usize];
pixel[2] = ctx.gamma_lut[b as usize];
}
pixel[3] = 255;
}
}
pub(crate) fn render_rows<F>(
page: &DjVuPage,
opts: &RenderOptions,
sink: F,
) -> Result<(), RenderError>
where
F: FnMut(usize, &[u8]),
{
let w = opts.width;
let h = opts.height;
if w == 0 || h == 0 {
return Err(RenderError::InvalidDimensions {
width: w,
height: h,
});
}
let gamma_lut = build_gamma_lut(page.gamma());
let bg_subsample = best_iw44_subsample(opts.decode_scale(page));
let DecodedLayers {
bg,
fg_palette,
mask,
blit_map,
fg44,
} = decode_layers(page, opts, bg_subsample, usize::MAX)?;
let (ctx_mask, mask_shift) = resolve_sub4_mask(
page,
bg_subsample,
opts,
mask.as_deref(),
fg_palette.as_ref(),
);
let ctx = CompositeContext::from_layers(
page,
opts,
bg.as_deref(),
ctx_mask,
mask_shift,
fg_palette.as_ref(),
blit_map.as_deref(),
fg44.as_deref(),
&gamma_lut,
(0, 0),
(w, h),
);
composite_rows(&ctx, sink)
}
pub fn render_into(
page: &DjVuPage,
opts: &RenderOptions,
buf: &mut [u8],
) -> Result<(), RenderError> {
let w = opts.width;
let h = opts.height;
if w == 0 || h == 0 {
return Err(RenderError::InvalidDimensions {
width: w,
height: h,
});
}
let need = (w as usize)
.checked_mul(h as usize)
.and_then(|n| n.checked_mul(4))
.unwrap_or(usize::MAX);
if buf.len() < need {
return Err(RenderError::BufTooSmall {
need,
got: buf.len(),
});
}
let gamma_lut = build_gamma_lut(page.gamma());
let bg_subsample = best_iw44_subsample(opts.decode_scale(page));
let DecodedLayers {
bg,
fg_palette,
mask,
blit_map,
fg44,
} = decode_layers(page, opts, bg_subsample, usize::MAX)?;
let (ctx_mask, mask_shift) = resolve_sub4_mask(
page,
bg_subsample,
opts,
mask.as_deref(),
fg_palette.as_ref(),
);
let ctx = CompositeContext::from_layers(
page,
opts,
bg.as_deref(),
ctx_mask,
mask_shift,
fg_palette.as_ref(),
blit_map.as_deref(),
fg44.as_deref(),
&gamma_lut,
(0, 0),
(w, h),
);
composite_into(&ctx, buf)?;
Ok(())
}
fn native_render_opts(page: &DjVuPage, opts: &RenderOptions) -> RenderOptions {
RenderOptions {
width: page.width() as u32,
height: page.height() as u32,
bold: opts.bold,
permissive: opts.permissive,
..Default::default()
}
}
fn apply_lanczos_postpass<F>(
pm: Pixmap,
page: &DjVuPage,
opts: &RenderOptions,
full: (u32, u32),
out: (u32, u32),
render_native: F,
) -> Pixmap
where
F: FnOnce(&RenderOptions) -> Result<Pixmap, RenderError>,
{
if opts.resampling != Resampling::Lanczos3 {
return pm;
}
let (full_w, full_h) = full;
let need_scale = page.width() as u32 != full_w || page.height() as u32 != full_h;
if !need_scale {
return pm;
}
let native_opts = native_render_opts(page, opts);
match render_native(&native_opts) {
Ok(native_pm) => crate::pixmap::scale_lanczos3(&native_pm, out.0, out.1),
Err(_) => pm,
}
}
pub fn render_pixmap(page: &DjVuPage, opts: &RenderOptions) -> Result<Pixmap, RenderError> {
let w = opts.width;
let h = opts.height;
if w == 0 || h == 0 {
return Err(RenderError::InvalidDimensions {
width: w,
height: h,
});
}
const MAX_RENDER_PIXELS: usize = 512 * 1024 * 1024;
if (w as usize).saturating_mul(h as usize) > MAX_RENDER_PIXELS {
return Err(RenderError::InvalidDimensions {
width: w,
height: h,
});
}
let mut pm = Pixmap::white(w, h);
if opts.permissive {
let row_stride = w as usize * 4;
render_rows(page, opts, |y, row| {
let start = y * row_stride;
pm.data[start..start + row_stride].copy_from_slice(row);
})?;
} else {
render_into(page, opts, &mut pm.data)?;
}
if opts.aa {
pm = aa_downscale(&pm);
}
let pm = apply_lanczos_postpass(pm, page, opts, (w, h), (w, h), |native_opts| {
render_pixmap(page, native_opts)
});
Ok(rotate_pixmap(
pm,
combine_rotations(page.rotation(), opts.rotation),
))
}
pub fn render_streaming<F>(
page: &DjVuPage,
opts: &RenderOptions,
sink: F,
) -> Result<(), RenderError>
where
F: FnMut(usize, &[u8]),
{
if opts.aa {
return Err(RenderError::UnsupportedOption(
"anti-aliasing requires a full pixmap; use render_pixmap",
));
}
let lanczos_with_scaling = opts.resampling == Resampling::Lanczos3
&& (page.width() as u32 != opts.width || page.height() as u32 != opts.height);
if lanczos_with_scaling {
return Err(RenderError::UnsupportedOption(
"Lanczos-3 resampling at scaled output requires a full pixmap; use render_pixmap",
));
}
if combine_rotations(page.rotation(), opts.rotation) != crate::info::Rotation::None {
return Err(RenderError::UnsupportedOption(
"rotation requires a full pixmap; use render_pixmap",
));
}
render_rows(page, opts, sink)
}
pub fn render_region(
page: &DjVuPage,
region: RenderRect,
opts: &RenderOptions,
) -> Result<Pixmap, RenderError> {
if region.width == 0 || region.height == 0 {
return Err(RenderError::InvalidDimensions {
width: region.width,
height: region.height,
});
}
let full_w = opts.width.max(1);
let full_h = opts.height.max(1);
let gamma_lut = build_gamma_lut(page.gamma());
let bg_subsample = best_iw44_subsample(opts.decode_scale(page));
let DecodedLayers {
bg,
fg_palette,
mask,
blit_map,
fg44,
} = decode_layers(page, opts, bg_subsample, usize::MAX)?;
let out_w = region.width;
let out_h = region.height;
let mut pm = Pixmap::white(out_w, out_h);
let region_opts = RenderOptions {
width: full_w,
height: full_h,
..*opts
};
let ctx = CompositeContext::from_layers(
page,
®ion_opts,
bg.as_deref(),
mask.as_deref(),
0,
fg_palette.as_ref(),
blit_map.as_deref(),
fg44.as_deref(),
&gamma_lut,
(region.x, region.y),
(out_w, out_h),
);
composite_into(&ctx, &mut pm.data)?;
let pm = apply_lanczos_postpass(
pm,
page,
opts,
(full_w, full_h),
(out_w, out_h),
|native_opts| render_region(page, region, native_opts),
);
Ok(rotate_pixmap(
pm,
combine_rotations(page.rotation(), opts.rotation),
))
}
pub fn render_gray8(page: &DjVuPage, opts: &RenderOptions) -> Result<GrayPixmap, RenderError> {
Ok(render_pixmap(page, opts)?.to_gray8())
}
#[cfg(feature = "parallel")]
pub fn render_pages_parallel(
doc: &crate::djvu_document::DjVuDocument,
dpi: u32,
) -> Vec<Result<Pixmap, RenderError>> {
use rayon::prelude::*;
let count = doc.page_count();
(0..count)
.into_par_iter()
.map(|i| {
let page = doc.page(i)?;
let native_dpi = page.dpi() as f32;
let scale = dpi as f32 / native_dpi;
let w = ((page.width() as f32 * scale).round() as u32).max(1);
let h = ((page.height() as f32 * scale).round() as u32).max(1);
let opts = RenderOptions {
width: w,
height: h,
..Default::default()
};
render_pixmap(page, &opts)
})
.collect()
}
pub fn render_coarse(page: &DjVuPage, opts: &RenderOptions) -> Result<Option<Pixmap>, RenderError> {
let w = opts.width;
let h = opts.height;
if w == 0 || h == 0 {
return Err(RenderError::InvalidDimensions {
width: w,
height: h,
});
}
let bg_subsample = best_iw44_subsample(opts.decode_scale(page));
let bg = decode_background_chunks(page, 1, bg_subsample)?;
let bg = match bg {
Some(b) => b,
None => return Ok(None),
};
let gamma_lut = build_gamma_lut(page.gamma());
let mut pm = Pixmap::white(w, h);
{
let ctx = CompositeContext::from_layers(
page,
opts,
Some(&bg),
None,
0,
None,
None,
None,
&gamma_lut,
(0, 0),
(w, h),
);
composite_into(&ctx, &mut pm.data)?;
}
Ok(Some(rotate_pixmap(
pm,
combine_rotations(page.rotation(), opts.rotation),
)))
}
pub fn render_progressive(
page: &DjVuPage,
opts: &RenderOptions,
chunk_n: usize,
) -> Result<Pixmap, RenderError> {
let w = opts.width;
let h = opts.height;
if w == 0 || h == 0 {
return Err(RenderError::InvalidDimensions {
width: w,
height: h,
});
}
let n_bg44 = page.bg44_chunks().len();
let max_chunk = n_bg44.saturating_sub(1);
if n_bg44 > 0 && chunk_n > max_chunk {
return Err(RenderError::ChunkOutOfRange {
chunk_n,
max: max_chunk,
});
}
let gamma_lut = build_gamma_lut(page.gamma());
let bg_subsample = best_iw44_subsample(opts.decode_scale(page));
let DecodedLayers {
bg,
fg_palette,
mask,
blit_map,
fg44,
} = decode_layers(page, opts, bg_subsample, chunk_n + 1)?;
let mut pm = Pixmap::white(w, h);
{
let ctx = CompositeContext::from_layers(
page,
opts,
bg.as_deref(),
mask.as_deref(),
0,
fg_palette.as_ref(),
blit_map.as_deref(),
fg44.as_deref(),
&gamma_lut,
(0, 0),
(w, h),
);
composite_into(&ctx, &mut pm.data)?;
}
let pm = apply_lanczos_postpass(pm, page, opts, (w, h), (w, h), |native_opts| {
render_progressive(page, native_opts, chunk_n)
});
Ok(rotate_pixmap(
pm,
combine_rotations(page.rotation(), opts.rotation),
))
}
pub fn progressive_steps(page: &DjVuPage) -> usize {
page.bg44_chunks().len().max(1)
}
pub fn render_progressive_step(
page: &DjVuPage,
opts: &RenderOptions,
step: usize,
) -> Result<Pixmap, RenderError> {
if page.bg44_chunks().is_empty() {
render_pixmap(page, opts)
} else {
render_progressive(page, opts, step)
}
}
pub fn render_progressive_all(
page: &DjVuPage,
opts: &RenderOptions,
) -> Result<Vec<Pixmap>, RenderError> {
let steps = progressive_steps(page);
let mut frames = Vec::with_capacity(steps);
for step in 0..steps {
frames.push(render_progressive_step(page, opts, step)?);
}
Ok(frames)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::djvu_document::DjVuDocument;
fn assets_path() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("references/djvujs/library/assets")
}
fn load_doc(filename: &str) -> DjVuDocument {
let data = std::fs::read(assets_path().join(filename))
.unwrap_or_else(|_| panic!("{filename} must exist"));
DjVuDocument::parse(&data).unwrap_or_else(|e| panic!("parse failed: {e}"))
}
fn identity_lut() -> [u8; 256] {
core::array::from_fn(|i| i as u8)
}
#[allow(clippy::too_many_arguments)]
fn synth_ctx<'a>(
opts: &'a RenderOptions,
page_w: u32,
page_h: u32,
bg: Option<&'a Pixmap>,
mask: Option<&'a crate::bitmap::Bitmap>,
gamma_lut: &'a [u8; 256],
out_w: u32,
out_h: u32,
) -> CompositeContext<'a> {
let (fg_x_q24, fg_y_q24) = fg_q24(None, page_w, page_h);
let (bg_x_q24, bg_y_q24) = bg_q24(bg, page_w, page_h);
CompositeContext {
opts,
page_w,
page_h,
bg,
bg_x_q24,
bg_y_q24,
mask,
mask_shift: 0,
fg_palette: None,
blit_map: None,
fg44: None,
fg_x_q24,
fg_y_q24,
gamma_lut,
gamma_is_identity: gamma_lut.iter().enumerate().all(|(i, &v)| v == i as u8),
offset_x: 0,
offset_y: 0,
out_w,
out_h,
}
}
#[test]
fn composite_bilevel_one_maps_mask_to_black_and_white() {
let opts = RenderOptions::default();
let mut mask = crate::bitmap::Bitmap::new(8, 1);
mask.set_black(0, 0);
mask.set_black(3, 0);
let lut = identity_lut();
let ctx = synth_ctx(&opts, 8, 1, None, Some(&mask), &lut, 8, 1);
let mut row = vec![0u8; 8 * 4];
composite_rows_bilevel_one(&ctx, 0, FRAC, FRAC, &mut row);
for x in 0..8usize {
let p = &row[x * 4..x * 4 + 4];
if x == 0 || x == 3 {
assert_eq!(p, [0, 0, 0, 255], "foreground pixel at x={x}");
} else {
assert_eq!(p, [255, 255, 255, 255], "background pixel at x={x}");
}
}
}
#[test]
fn composite_bilinear_one_copies_background_1to1_unsubsampled() {
let opts = RenderOptions::default();
let bg = Pixmap::new(4, 2, 10, 20, 30, 255);
let mask = crate::bitmap::Bitmap::new(4, 2); let lut = identity_lut();
let ctx = synth_ctx(&opts, 4, 2, Some(&bg), Some(&mask), &lut, 4, 2);
let mut row = vec![0u8; 4 * 4];
composite_rows_bilinear_one(&ctx, 0, FRAC, FRAC, &mut row);
for x in 0..4usize {
let p = &row[x * 4..x * 4 + 4];
assert_eq!(&p[..3], &[10, 20, 30], "background colour at x={x}");
assert_eq!(p[3], 255, "opaque at x={x}");
}
}
#[test]
fn composite_bilinear_one_upsamples_subsampled_background() {
let opts = RenderOptions::default();
let bg = Pixmap::new(4, 2, 70, 90, 110, 255); let mask = crate::bitmap::Bitmap::new(8, 2); let lut = identity_lut();
let ctx = synth_ctx(&opts, 8, 2, Some(&bg), Some(&mask), &lut, 8, 2);
assert_ne!(
ctx.bg_x_q24,
1 << 24,
"test must exercise the subsampled path"
);
let mut row = vec![0u8; 8 * 4];
composite_rows_bilinear_one(&ctx, 0, FRAC, FRAC, &mut row);
for x in 0..8usize {
let p = &row[x * 4..x * 4 + 4];
assert_eq!(&p[..3], &[70, 90, 110], "upsampled bg colour at x={x}");
assert_eq!(p[3], 255, "opaque at x={x}");
}
}
#[test]
fn composite_area_avg_one_averages_uniform_background_on_downscale() {
let opts = RenderOptions::default();
let bg = Pixmap::new(4, 2, 40, 80, 120, 255);
let mask = crate::bitmap::Bitmap::new(4, 2); let lut = identity_lut();
let ctx = synth_ctx(&opts, 4, 2, Some(&bg), Some(&mask), &lut, 2, 1);
let fx_step = 2 * FRAC;
let fy_step = 2 * FRAC;
let bg_fx_step = ((fx_step as u64 * ctx.bg_x_q24) >> 24) as u32;
let bg_fy_step = ((fy_step as u64 * ctx.bg_y_q24) >> 24) as u32;
let xs = precompute_area_avg_x(&ctx, fx_step, bg_fx_step);
let mut row = vec![0u8; 2 * 4];
composite_rows_area_avg_one(
&ctx,
0,
fx_step,
fy_step,
bg_fx_step,
bg_fy_step,
&mut row,
Some(&xs),
);
for x in 0..2usize {
let p = &row[x * 4..x * 4 + 4];
assert_eq!(&p[..3], &[40, 80, 120], "averaged background at x={x}");
assert_eq!(p[3], 255, "opaque at x={x}");
}
}
#[test]
fn fg_q24_maps_endpoints_into_fg_space() {
let fg = Pixmap::white(189, 306);
let (qx, qy) = fg_q24(Some(&fg), 2260, 3669);
assert!(qx > 0 && qy > 0);
let frac = 1u64 << FRACBITS;
let last_x = 2259u64 * frac;
let fg_fx = (last_x * qx) >> 24;
let fg_px = fg_fx >> FRACBITS;
assert_eq!(fg_px, (fg.width as u64) - 1);
let last_y = 3668u64 * frac;
let fg_fy = (last_y * qy) >> 24;
let fg_py = fg_fy >> FRACBITS;
assert_eq!(fg_py, (fg.height as u64) - 1);
}
#[test]
fn fg_q24_returns_zero_when_fg_is_none() {
assert_eq!(fg_q24(None, 100, 100), (0, 0));
}
#[test]
fn bg_q24_maps_non_pow2_subsample() {
let bg = Pixmap::white(754, 1223);
let (qx, qy) = bg_q24(Some(&bg), 2260, 3669);
assert_eq!(qx, (1u64 << 24) / 3);
assert_eq!(qy, (1u64 << 24) / 3);
let last_x = 2259u32 * FRAC;
let bg_px = (map_plane_center_frac(last_x, qx) as u64) >> FRACBITS;
assert!(bg_px < bg.width as u64);
let last_y = 3668u32 * FRAC;
let bg_py = (map_plane_center_frac(last_y, qy) as u64) >> FRACBITS;
assert!(bg_py < bg.height as u64);
}
#[test]
fn bg_q24_returns_zero_when_bg_is_none() {
assert_eq!(bg_q24(None, 100, 100), (0, 0));
}
#[test]
fn plane_q24_some_branch_with_zero_dimension_plane() {
let zero_width_fg = Pixmap::new(0, 10, 0, 0, 0, 0);
let (qx, qy) = fg_q24(Some(&zero_width_fg), 100, 100);
assert_eq!(qx, 0); assert_eq!(qy, (10u64 << 24) / 100); }
#[test]
fn fg_q24_uses_integer_horizontal_cell_pitch() {
let fg = Pixmap::white(189, 306);
let (qx, qy) = fg_q24(Some(&fg), 2260, 3669);
assert_eq!(qx, (1u64 << 24) / 12);
assert_eq!(qy, ((306u64) << 24) / 3669);
}
#[test]
fn bg_q24_uses_integer_cell_pitch_for_padded_edges() {
let bg = Pixmap::white(754, 1223);
let (qx, qy) = bg_q24(Some(&bg), 2260, 3669);
assert_eq!(qx, (1u64 << 24) / 3);
assert_eq!(qy, (1u64 << 24) / 3);
}
#[test]
fn map_plane_center_frac_aligns_pixel_centers() {
let q24 = (1u64 << 24) / 2;
assert_eq!(map_plane_center_frac(0, q24), 0);
assert_eq!(map_plane_center_frac(FRAC, q24), FRAC / 4);
}
#[test]
fn sample_bilinear_rounds_to_nearest() {
let mut pm = Pixmap::new(2, 2, 0, 0, 0, 255);
pm.set_rgb(1, 1, 255, 255, 255);
assert_eq!(sample_bilinear(&pm, FRAC / 2, FRAC / 2), (64, 64, 64));
}
#[test]
fn sample_nearest_rounds_to_nearest_pixel() {
let mut pm = Pixmap::new(2, 1, 10, 20, 30, 255);
pm.set_rgb(1, 0, 200, 210, 220);
assert_eq!(sample_nearest(&pm, FRAC / 2 - 1, 0), (10, 20, 30));
assert_eq!(sample_nearest(&pm, FRAC / 2, 0), (200, 210, 220));
}
#[test]
fn mask_box_coverage_values() {
use crate::bitmap::Bitmap;
let mut bm = Bitmap::new(4, 1);
bm.set(0, 0, true);
bm.set(2, 0, true);
bm.set(3, 0, true);
let step = 4 * FRAC;
assert_eq!(mask_box_coverage(&bm, 0, 0, step, FRAC), 191);
let mut bm_full = Bitmap::new(2, 2);
bm_full.set(0, 0, true);
bm_full.set(1, 0, true);
bm_full.set(0, 1, true);
bm_full.set(1, 1, true);
assert_eq!(mask_box_coverage(&bm_full, 0, 0, 2 * FRAC, 2 * FRAC), 255);
let bm_empty = Bitmap::new(2, 2);
assert_eq!(mask_box_coverage(&bm_empty, 0, 0, 2 * FRAC, 2 * FRAC), 0);
}
#[test]
fn render_options_default() {
let opts = RenderOptions::default();
assert_eq!(opts.width, 0);
assert_eq!(opts.height, 0);
assert_eq!(opts.bold, 0);
assert!(!opts.aa);
assert_eq!(opts.resampling, Resampling::Bilinear);
}
#[test]
fn render_options_construction() {
let opts = RenderOptions {
width: 400,
height: 300,
bold: 1,
aa: true,
rotation: UserRotation::Cw90,
..Default::default()
};
assert_eq!(opts.width, 400);
assert_eq!(opts.height, 300);
assert_eq!(opts.bold, 1);
assert!(opts.aa);
assert_eq!(opts.rotation, UserRotation::Cw90);
}
#[test]
fn fit_to_width_preserves_aspect() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let pw = page.width() as u32;
let ph = page.height() as u32;
let opts = RenderOptions::fit_to_width(page, 800);
assert_eq!(opts.width, 800);
let expected_h = ((ph as f64 * 800.0) / pw as f64).round() as u32;
assert_eq!(opts.height, expected_h);
assert!((opts.decode_scale(page) - 800.0 / pw as f32).abs() < 0.01);
}
#[test]
fn fit_to_height_preserves_aspect() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let pw = page.width() as u32;
let ph = page.height() as u32;
let opts = RenderOptions::fit_to_height(page, 600);
assert_eq!(opts.height, 600);
let expected_w = ((pw as f64 * 600.0) / ph as f64).round() as u32;
assert_eq!(opts.width, expected_w);
assert!((opts.decode_scale(page) - 600.0 / ph as f32).abs() < 0.01);
}
#[test]
fn fit_to_box_constrains_both() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions::fit_to_box(page, 10000, 100);
assert!(opts.width <= 10000);
assert!(opts.height <= 100);
assert!(opts.width > 0 && opts.height > 0);
let opts = RenderOptions::fit_to_box(page, 100, 10000);
assert!(opts.width <= 100);
assert!(opts.height <= 10000);
assert!(opts.width > 0 && opts.height > 0);
}
#[test]
fn fit_to_box_square() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions::fit_to_box(page, 500, 500);
assert!(opts.width <= 500);
assert!(opts.height <= 500);
assert!(opts.width >= 490 || opts.height >= 490);
}
#[test]
fn fit_to_width_rotation_aware() {
let doc = load_doc("boy_jb2_rotate90.djvu");
let page = doc.page(0).unwrap();
let pw = page.width() as u32;
let ph = page.height() as u32;
let (dw, dh) = (ph, pw);
let opts = RenderOptions::fit_to_width(page, 400);
assert_eq!(opts.width, 400);
let expected_h = ((dh as f64 * 400.0) / dw as f64).round() as u32;
assert_eq!(opts.height, expected_h);
}
#[test]
fn render_into_invalid_dimensions() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 0,
height: 100,
..Default::default()
};
let mut buf = vec![0u8; 400];
let err = render_into(page, &opts, &mut buf).unwrap_err();
assert!(
matches!(err, RenderError::InvalidDimensions { .. }),
"expected InvalidDimensions, got {err:?}"
);
}
#[test]
fn render_into_buf_too_small() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 10,
height: 10,
..Default::default()
};
let mut buf = vec![0u8; 10]; let err = render_into(page, &opts, &mut buf).unwrap_err();
assert!(
matches!(err, RenderError::BufTooSmall { need: 400, got: 10 }),
"expected BufTooSmall, got {err:?}"
);
}
#[test]
fn render_into_fills_buffer_no_alloc() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let w = 50u32;
let h = 40u32;
let opts = RenderOptions {
width: w,
height: h,
..Default::default()
};
let mut buf = vec![0u8; (w * h * 4) as usize];
render_into(page, &opts, &mut buf).expect("render_into should succeed");
assert!(
buf.iter().any(|&b| b != 0),
"rendered buffer should contain non-zero pixels"
);
}
#[test]
fn render_into_reuse_buffer() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let w = 30u32;
let h = 20u32;
let opts = RenderOptions {
width: w,
height: h,
..Default::default()
};
let mut buf = vec![0u8; (w * h * 4) as usize];
render_into(page, &opts, &mut buf).expect("first render_into should succeed");
let first = buf.clone();
render_into(page, &opts, &mut buf).expect("second render_into should succeed");
assert_eq!(
first, buf,
"repeated render_into should produce identical output"
);
}
#[test]
fn gamma_lut_standard_is_identity() {
let lut = build_gamma_lut(2.2);
for (i, &val) in lut.iter().enumerate() {
assert_eq!(
val, i as u8,
"gamma=2.2 LUT at {i}: expected {i}, got {val}"
);
}
}
#[test]
fn gamma_lut_linear_source_brightens() {
let lut_linear = build_gamma_lut(1.0); let mid = 128u8;
let corrected = lut_linear[mid as usize];
assert!(
corrected > mid,
"linear-source LUT at mid ({corrected}) should be brighter than {mid}"
);
}
#[test]
fn gamma_lut_zero_is_identity() {
let lut = build_gamma_lut(0.0);
for (i, &val) in lut.iter().enumerate() {
assert_eq!(val, i as u8, "zero gamma should produce identity LUT");
}
}
#[test]
fn render_coarse_returns_pixmap() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 60,
height: 80,
..Default::default()
};
let result = render_coarse(page, &opts).expect("render_coarse should succeed");
if let Some(pm) = result {
assert_eq!(pm.width, 60);
assert_eq!(pm.height, 80);
assert_eq!(pm.data.len(), 60 * 80 * 4);
}
}
#[test]
fn render_progressive_each_chunk() {
let doc = load_doc("boy.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 80,
height: 100,
..Default::default()
};
let n_bg44 = page.bg44_chunks().len();
for chunk_n in 0..n_bg44 {
let pm = render_progressive(page, &opts, chunk_n)
.unwrap_or_else(|e| panic!("render_progressive chunk {chunk_n} failed: {e}"));
assert_eq!(pm.width, 80);
assert_eq!(pm.height, 100);
assert_eq!(pm.data.len(), 80 * 100 * 4);
assert!(
pm.data.iter().any(|&b| b != 0),
"chunk {chunk_n}: rendered frame should not be all-zero"
);
}
}
#[test]
fn render_progressive_all_seals_chunk_loop() {
let doc = load_doc("boy.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 80,
height: 100,
..Default::default()
};
let steps = progressive_steps(page);
assert_eq!(steps, page.bg44_chunks().len().max(1));
let frames = render_progressive_all(page, &opts).expect("progressive_all must succeed");
assert_eq!(frames.len(), steps, "one frame per progressive step");
let full = render_pixmap(page, &opts).expect("render_pixmap must succeed");
assert_eq!(
frames.last().unwrap().data,
full.data,
"final progressive frame must equal the full render"
);
let first = render_progressive_step(page, &opts, 0).expect("step 0 must succeed");
assert_eq!(first.data, frames[0].data);
}
#[test]
fn render_progressive_chunk_out_of_range() {
let doc = load_doc("boy.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 40,
height: 50,
..Default::default()
};
let n_bg44 = page.bg44_chunks().len();
if n_bg44 == 0 {
return;
}
let err = render_progressive(page, &opts, n_bg44 + 10).unwrap_err();
assert!(
matches!(err, RenderError::ChunkOutOfRange { .. }),
"expected ChunkOutOfRange, got {err:?}"
);
}
#[test]
fn render_pixmap_gamma_differs_from_identity() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let w = 40u32;
let h = 53u32;
let opts = RenderOptions {
width: w,
height: h,
..Default::default()
};
let pm_gamma = render_pixmap(page, &opts).expect("render with gamma should succeed");
let lut_identity = build_gamma_lut(1.0);
let pm_identity = render_pixmap(page, &opts).expect("render for identity should succeed");
for i in 0..pm_identity.data.len().saturating_sub(3) {
if i % 4 != 3 {
let _ = lut_identity[pm_identity.data[i] as usize];
}
}
assert_eq!(pm_gamma.width, w);
assert_eq!(pm_gamma.height, h);
assert!(
pm_gamma.data.iter().any(|&b| b != 255),
"should have non-white pixels"
);
}
#[test]
fn render_bilevel_page_has_black_pixels() {
let doc = load_doc("boy_jb2.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 60,
height: 80,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("render bilevel should succeed");
assert_eq!(pm.width, 60);
assert_eq!(pm.height, 80);
assert!(
pm.data
.chunks_exact(4)
.any(|px| px[0] == 0 && px[1] == 0 && px[2] == 0),
"bilevel page should contain black pixels"
);
}
#[test]
fn render_with_aa() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 40,
height: 54,
aa: true,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("render with AA should succeed");
assert_eq!(pm.width, 20);
assert_eq!(pm.height, 27);
}
#[test]
fn rotate_pixmap_none_is_identity() {
let mut pm = Pixmap::white(3, 2);
pm.set_rgb(0, 0, 255, 0, 0);
let rotated = rotate_pixmap(pm.clone(), crate::info::Rotation::None);
assert_eq!(rotated.width, 3);
assert_eq!(rotated.height, 2);
assert_eq!(rotated.get_rgb(0, 0), (255, 0, 0));
}
#[test]
fn rotate_pixmap_cw90_swaps_dims() {
let mut pm = Pixmap::white(4, 2);
pm.set_rgb(0, 0, 255, 0, 0); let rotated = rotate_pixmap(pm, crate::info::Rotation::Cw90);
assert_eq!(rotated.width, 2);
assert_eq!(rotated.height, 4);
assert_eq!(rotated.get_rgb(1, 0), (255, 0, 0));
}
#[test]
fn rotate_pixmap_180_preserves_dims() {
let mut pm = Pixmap::white(3, 2);
pm.set_rgb(0, 0, 255, 0, 0); let rotated = rotate_pixmap(pm, crate::info::Rotation::Rot180);
assert_eq!(rotated.width, 3);
assert_eq!(rotated.height, 2);
assert_eq!(rotated.get_rgb(2, 1), (255, 0, 0));
}
#[test]
fn rotate_pixmap_ccw90_swaps_dims() {
let mut pm = Pixmap::white(4, 2);
pm.set_rgb(0, 0, 255, 0, 0); let rotated = rotate_pixmap(pm, crate::info::Rotation::Ccw90);
assert_eq!(rotated.width, 2);
assert_eq!(rotated.height, 4);
assert_eq!(rotated.get_rgb(0, 3), (255, 0, 0));
}
#[test]
fn render_pixmap_rotation_90_swaps_dimensions() {
let doc = load_doc("boy_jb2_rotate90.djvu");
let page = doc.page(0).expect("page 0");
let orig_w = page.width();
let orig_h = page.height();
let opts = RenderOptions {
width: orig_w as u32,
height: orig_h as u32,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("render should succeed");
assert_eq!(
pm.width, orig_h as u32,
"rotated width should be original height"
);
assert_eq!(
pm.height, orig_w as u32,
"rotated height should be original width"
);
}
#[test]
fn render_pixmap_rotation_180_preserves_dimensions() {
let doc = load_doc("boy_jb2_rotate180.djvu");
let page = doc.page(0).expect("page 0");
let orig_w = page.width();
let orig_h = page.height();
let opts = RenderOptions {
width: orig_w as u32,
height: orig_h as u32,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("render should succeed");
assert_eq!(pm.width, orig_w as u32);
assert_eq!(pm.height, orig_h as u32);
}
#[test]
fn render_pixmap_rotation_270_swaps_dimensions() {
let doc = load_doc("boy_jb2_rotate270.djvu");
let page = doc.page(0).expect("page 0");
let orig_w = page.width();
let orig_h = page.height();
let opts = RenderOptions {
width: orig_w as u32,
height: orig_h as u32,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("render should succeed");
assert_eq!(
pm.width, orig_h as u32,
"rotated width should be original height"
);
assert_eq!(
pm.height, orig_w as u32,
"rotated height should be original width"
);
}
#[test]
fn combine_rotations_identity() {
use crate::info::Rotation;
assert_eq!(
combine_rotations(Rotation::None, UserRotation::None),
Rotation::None
);
}
#[test]
fn combine_rotations_info_only() {
use crate::info::Rotation;
assert_eq!(
combine_rotations(Rotation::Cw90, UserRotation::None),
Rotation::Cw90
);
}
#[test]
fn combine_rotations_user_only() {
use crate::info::Rotation;
assert_eq!(
combine_rotations(Rotation::None, UserRotation::Ccw90),
Rotation::Ccw90
);
}
#[test]
fn combine_rotations_sum() {
use crate::info::Rotation;
assert_eq!(
combine_rotations(Rotation::Cw90, UserRotation::Cw90),
Rotation::Rot180
);
assert_eq!(
combine_rotations(Rotation::Cw90, UserRotation::Ccw90),
Rotation::None
);
assert_eq!(
combine_rotations(Rotation::Rot180, UserRotation::Rot180),
Rotation::None
);
}
#[test]
fn user_rotation_cw90_swaps_dimensions() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let pw = page.width() as u32;
let ph = page.height() as u32;
let opts = RenderOptions {
width: pw,
height: ph,
rotation: UserRotation::Cw90,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("render");
assert_eq!(pm.width, ph, "user Cw90 should swap: width becomes height");
assert_eq!(pm.height, pw, "user Cw90 should swap: height becomes width");
}
#[test]
fn user_rotation_180_preserves_dimensions() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let pw = page.width() as u32;
let ph = page.height() as u32;
let opts = RenderOptions {
width: pw,
height: ph,
rotation: UserRotation::Rot180,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("render");
assert_eq!(pm.width, pw);
assert_eq!(pm.height, ph);
}
#[test]
fn user_rotation_default_is_none() {
assert_eq!(UserRotation::default(), UserRotation::None);
let opts = RenderOptions::default();
assert_eq!(opts.rotation, UserRotation::None);
}
#[test]
fn fgbz_palette_page_renders_multiple_colors() {
let doc = load_doc("irish.djvu");
let page = doc.page(0).expect("page 0");
let w = page.width() as u32;
let h = page.height() as u32;
let opts = RenderOptions {
width: w,
height: h,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("render should succeed");
let mut fg_colors = std::collections::HashSet::new();
for y in 0..h {
for x in 0..w {
let (r, g, b) = pm.get_rgb(x, y);
if r > 240 && g > 240 && b > 240 {
continue;
}
fg_colors.insert((r, g, b));
}
}
assert!(
fg_colors.len() > 1,
"multi-color palette page should have >1 distinct foreground colors, got {}",
fg_colors.len()
);
}
#[test]
fn lookup_palette_color_uses_blit_map() {
let pal = FgbzPalette {
colors: vec![
PaletteColor { r: 255, g: 0, b: 0 }, PaletteColor { r: 0, g: 0, b: 255 }, ],
indices: vec![1, 0], };
let bm = crate::bitmap::Bitmap::new(2, 1);
let blit_map = vec![0i32, 1i32];
let c0 = lookup_palette_color(&pal, Some(&blit_map), Some(&bm), 0, 0);
assert_eq!(
(c0.r, c0.g, c0.b),
(0, 0, 255),
"blit 0 → indices[0]=1 → blue"
);
let c1 = lookup_palette_color(&pal, Some(&blit_map), Some(&bm), 1, 0);
assert_eq!(
(c1.r, c1.g, c1.b),
(255, 0, 0),
"blit 1 → indices[1]=0 → red"
);
}
#[test]
fn lookup_palette_color_fallback_without_blit_map() {
let pal = FgbzPalette {
colors: vec![PaletteColor { r: 0, g: 128, b: 0 }],
indices: vec![],
};
let c = lookup_palette_color(&pal, None, None, 0, 0);
assert_eq!(
(c.r, c.g, c.b),
(0, 128, 0),
"should fall back to first color"
);
}
fn load_bgjp_doc() -> DjVuDocument {
load_doc("bgjp_test.djvu")
}
#[test]
fn bgjp_fixture_loads() {
let doc = load_bgjp_doc();
let page = doc.page(0).unwrap();
assert_eq!(page.width(), 4);
assert_eq!(page.height(), 4);
}
#[test]
fn bgjp_chunk_present() {
let doc = load_bgjp_doc();
let page = doc.page(0).unwrap();
assert!(
page.find_chunk(b"BGjp").is_some(),
"fixture must have a BGjp chunk"
);
assert!(
page.bg44_chunks().is_empty(),
"fixture must NOT have BG44 chunks"
);
}
#[test]
fn decode_bgjp_returns_pixmap() {
let doc = load_bgjp_doc();
let page = doc.page(0).unwrap();
let pm = decode_bgjp(page).expect("decode_bgjp must not error");
assert!(pm.is_some(), "decode_bgjp must return Some(Pixmap)");
let pm = pm.unwrap();
assert_eq!(pm.width, 4);
assert_eq!(pm.height, 4);
assert_eq!(pm.data.len(), 4 * 4 * 4); }
#[test]
fn decode_bgjp_returns_none_without_chunk() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let pm = decode_bgjp(page).expect("should not error");
assert!(pm.is_none());
}
#[test]
fn decode_jpeg_to_pixmap_alpha_is_255() {
let doc = load_bgjp_doc();
let page = doc.page(0).unwrap();
let data = page.find_chunk(b"BGjp").unwrap();
let pm = decode_jpeg_to_pixmap(data).expect("decode must succeed");
for chunk in pm.data.chunks_exact(4) {
assert_eq!(chunk[3], 255, "alpha must be 255 for every pixel");
}
}
#[test]
fn render_pixmap_uses_bgjp_background() {
let doc = load_bgjp_doc();
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 4,
height: 4,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("render must succeed");
assert_eq!(pm.width, 4);
assert_eq!(pm.height, 4);
}
#[test]
fn render_coarse_uses_bgjp_background() {
let doc = load_bgjp_doc();
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 4,
height: 4,
..Default::default()
};
let pm = render_coarse(page, &opts).expect("render_coarse must succeed");
assert!(pm.is_some(), "must return Some when BGjp present");
let pm = pm.unwrap();
assert_eq!(pm.width, 4);
assert_eq!(pm.height, 4);
}
#[test]
fn render_pixmap_lanczos3_correct_dimensions() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let pw = page.width() as u32;
let ph = page.height() as u32;
let tw = pw / 2;
let th = ph / 2;
let opts = RenderOptions {
width: tw,
height: th,
resampling: Resampling::Lanczos3,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("Lanczos3 render must succeed");
assert_eq!(pm.width, tw);
assert_eq!(pm.height, th);
}
#[test]
fn lanczos3_differs_from_bilinear_at_half_scale() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let pw = page.width() as u32;
let ph = page.height() as u32;
let tw = pw / 2;
let th = ph / 2;
let bilinear = render_pixmap(
page,
&RenderOptions {
width: tw,
height: th,
resampling: Resampling::Bilinear,
..Default::default()
},
)
.unwrap();
let lanczos = render_pixmap(
page,
&RenderOptions {
width: tw,
height: th,
resampling: Resampling::Lanczos3,
..Default::default()
},
)
.unwrap();
assert_eq!(bilinear.width, lanczos.width);
assert_eq!(bilinear.height, lanczos.height);
let differ = bilinear
.data
.iter()
.zip(lanczos.data.iter())
.any(|(a, b)| a != b);
assert!(
differ,
"Lanczos3 and bilinear must produce different pixel values"
);
}
#[test]
fn resampling_default_is_bilinear() {
let opts = RenderOptions::default();
assert_eq!(opts.resampling, Resampling::Bilinear);
}
#[test]
fn render_region_allocates_proportionally() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions::fit_to_width(page, 1000);
let region = RenderRect {
x: 0,
y: 0,
width: 256,
height: 256,
};
let pm = render_region(page, region, &opts).expect("render_region should succeed");
assert_eq!(pm.width, 256);
assert_eq!(pm.height, 256);
assert_eq!(pm.data.len(), 256 * 256 * 4);
assert!(
pm.data.len() <= 512 * 1024,
"region allocation {} exceeds 512 KB",
pm.data.len()
);
}
#[test]
fn render_region_matches_full_render() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 100,
height: 80,
..Default::default()
};
let full = render_pixmap(page, &opts).expect("full render should succeed");
let region = RenderRect {
x: 10,
y: 10,
width: 30,
height: 20,
};
let part = render_region(page, region, &opts).expect("region render should succeed");
assert_eq!(part.width, 30);
assert_eq!(part.height, 20);
for ry in 0..20u32 {
for rx in 0..30u32 {
let full_base = ((10 + ry) as usize * 100 + (10 + rx) as usize) * 4;
let part_base = (ry as usize * 30 + rx as usize) * 4;
assert_eq!(
&full.data[full_base..full_base + 4],
&part.data[part_base..part_base + 4],
"pixel mismatch at region ({rx},{ry}) / full ({},{} )",
10 + rx,
10 + ry
);
}
}
}
#[test]
fn render_region_bilevel_byte_aligned_offset_matches_full() {
let doc = load_doc("boy_jb2.djvu");
let page = doc.page(0).unwrap();
let full_w = page.width() as u32;
let full_h = page.height() as u32;
let opts = RenderOptions {
width: full_w,
height: full_h,
..Default::default()
};
let full = render_pixmap(page, &opts).expect("full render should succeed");
for &x in &[16u32, 17u32] {
let region = RenderRect {
x,
y: 8,
width: 64,
height: 32,
};
let part = render_region(page, region, &opts).expect("region render should succeed");
for ry in 0..region.height {
for rx in 0..region.width {
let fb = (((region.y + ry) * full_w + (x + rx)) * 4) as usize;
let pb = ((ry * region.width + rx) * 4) as usize;
assert_eq!(
&full.data[fb..fb + 4],
&part.data[pb..pb + 4],
"mismatch at x={x} region ({rx},{ry})"
);
}
}
}
}
#[test]
fn render_region_invalid_dimensions() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 100,
height: 100,
..Default::default()
};
let region = RenderRect {
x: 0,
y: 0,
width: 0,
height: 50,
};
let err = render_region(page, region, &opts).unwrap_err();
assert!(
matches!(err, RenderError::InvalidDimensions { .. }),
"expected InvalidDimensions, got {err:?}"
);
}
#[test]
fn render_pixmap_still_works_after_refactor() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 80,
height: 60,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("render_pixmap should succeed");
assert_eq!(pm.width, 80);
assert_eq!(pm.height, 60);
assert_eq!(pm.data.len(), 80 * 60 * 4);
}
#[test]
fn best_iw44_subsample_values() {
assert_eq!(best_iw44_subsample(1.0), 1, "scale=1.0 → subsample=1");
assert_eq!(best_iw44_subsample(0.5), 2, "scale=0.5 → subsample=2");
assert_eq!(
best_iw44_subsample(0.375),
4,
"scale=0.375 → subsample=4 (1.5/0.375=4.0, allows 1.5× upscale)"
);
assert_eq!(best_iw44_subsample(0.25), 4, "scale=0.25 → subsample=4");
assert_eq!(
best_iw44_subsample(0.1),
8,
"scale=0.1 → subsample=8 (capped)"
);
assert_eq!(
best_iw44_subsample(0.0),
1,
"scale=0.0 → subsample=1 (edge case)"
);
assert_eq!(
best_iw44_subsample(-1.0),
1,
"scale<0 → subsample=1 (edge case)"
);
assert_eq!(
best_iw44_subsample(2.0),
1,
"scale>1.0 → subsample=1 (no upscaling needed)"
);
}
#[test]
#[allow(deprecated)] fn decode_subsample_derives_from_width_not_scale_field() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let (dw, _) = display_dimensions(page);
let opts = RenderOptions {
width: dw / 4,
..Default::default()
};
assert!((opts.decode_scale(page) - 0.25).abs() < 0.01);
assert_eq!(best_iw44_subsample(opts.decode_scale(page)), 4);
for misleading in [1.0_f32, 0.0, 0.5, 4.0] {
let mut o = RenderOptions {
width: dw / 4,
..Default::default()
};
o.scale = misleading;
assert_eq!(
best_iw44_subsample(o.decode_scale(page)),
4,
"scale={misleading} must not change the width-derived subsample",
);
}
}
#[test]
fn decode_scale_uses_native_width_for_rotated_page() {
let doc = load_doc("boy_jb2_rotate90.djvu");
let page = doc.page(0).unwrap();
let pw = page.width() as u32;
let (dw, _) = display_dimensions(page);
assert_ne!(dw, pw, "fixture must be a non-square INFO-rotated page");
let opts = RenderOptions {
width: pw / 2,
height: (page.height() as u32) / 2,
..Default::default()
};
let native = opts.width as f32 / pw as f32; let display = opts.width as f32 / dw as f32; assert!(
(opts.decode_scale(page) - native).abs() < 1e-4,
"decode_scale {} should equal the native-width ratio {native}",
opts.decode_scale(page),
);
assert!(
(opts.decode_scale(page) - display).abs() > 1e-2,
"decode_scale must not follow the rotation-swapped display width ({display})",
);
}
#[test]
fn render_pixmap_subsampled_bg_correct_dimensions() {
let doc = load_doc("boy.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: (page.width() as f32 * 0.5) as u32,
height: (page.height() as f32 * 0.5) as u32,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("subsampled render should succeed");
assert_eq!(pm.width, opts.width);
assert_eq!(pm.height, opts.height);
assert_eq!(
pm.data.len() as u64,
opts.width as u64 * opts.height as u64 * 4
);
}
#[test]
fn decoded_bg44_cache_produces_identical_pixels_on_second_render() {
let doc = load_doc("boy.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: page.width() as u32,
height: page.height() as u32,
..Default::default()
};
let pm1 = render_pixmap(page, &opts).expect("first render should succeed");
let pm2 = render_pixmap(page, &opts).expect("second render should succeed");
assert_eq!(
pm1.data, pm2.data,
"cached render must produce identical pixels"
);
}
#[test]
fn decoded_bg44_is_populated_after_render() {
let doc = load_doc("boy.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: page.width() as u32,
height: page.height() as u32,
..Default::default()
};
render_pixmap(page, &opts).expect("render should succeed");
let cached = page
.decoded_bg44()
.expect("cache should be populated after render");
assert_eq!(
cached.width,
page.width() as u32,
"cached bg44 width must equal page width"
);
assert_eq!(
cached.height,
page.height() as u32,
"cached bg44 height must equal page height"
);
}
#[test]
fn downsample_mask_4x_max_pools_each_block() {
let mut src = crate::bitmap::Bitmap::new(8, 8);
src.set(1, 2, true); src.set(6, 5, true); let out = downsample_mask_4x(&src);
assert_eq!((out.width, out.height), (2, 2));
assert!(out.get(0, 0), "top-left block had a set bit");
assert!(!out.get(1, 0), "top-right block was empty");
assert!(!out.get(0, 1), "bottom-left block was empty");
assert!(out.get(1, 1), "bottom-right block had a set bit");
}
#[test]
fn downsample_mask_4x_rounds_up_ragged_edges() {
let mut src = crate::bitmap::Bitmap::new(5, 5);
src.set(4, 4, true); let out = downsample_mask_4x(&src);
assert_eq!((out.width, out.height), (2, 2));
assert!(out.get(1, 1), "ragged corner block must capture its bit");
assert!(!out.get(0, 0));
}
#[test]
fn render_region_applies_rotation() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 80,
height: 60,
rotation: UserRotation::Cw90,
..Default::default()
};
let region = RenderRect {
x: 0,
y: 0,
width: 40,
height: 20,
};
let part = render_region(page, region, &opts).expect("region render should succeed");
assert_eq!(
part.width, 20,
"expected width=20 (was region.height) after CW90 rotation"
);
assert_eq!(
part.height, 40,
"expected height=40 (was region.width) after CW90 rotation"
);
}
#[test]
fn render_streaming_byte_identical_to_render_pixmap_color() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let w = 60u32;
let h = 80u32;
let opts = RenderOptions {
width: w,
height: h,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("render_pixmap should succeed");
let row_stride = w as usize * 4;
let mut streamed = vec![0u8; w as usize * h as usize * 4];
render_streaming(page, &opts, |y, row| {
assert_eq!(row.len(), row_stride);
let start = y * row_stride;
streamed[start..start + row_stride].copy_from_slice(row);
})
.expect("render_streaming should succeed");
assert_eq!(
pm.data, streamed,
"render_streaming must be byte-identical to render_pixmap when no post-processing options are set"
);
}
#[test]
fn render_streaming_byte_identical_to_render_pixmap_bilevel() {
let doc = load_doc("boy_jb2.djvu");
let page = doc.page(0).unwrap();
let w = 50u32;
let h = 70u32;
let opts = RenderOptions {
width: w,
height: h,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("render_pixmap should succeed");
let row_stride = w as usize * 4;
let mut streamed = vec![0u8; w as usize * h as usize * 4];
render_streaming(page, &opts, |y, row| {
let start = y * row_stride;
streamed[start..start + row_stride].copy_from_slice(row);
})
.expect("render_streaming should succeed");
assert_eq!(pm.data, streamed);
}
#[test]
fn render_streaming_rejects_aa() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 60,
height: 80,
aa: true,
..Default::default()
};
let err = render_streaming(page, &opts, |_, _| {}).unwrap_err();
assert!(
matches!(err, RenderError::UnsupportedOption(_)),
"expected UnsupportedOption, got {err:?}"
);
}
#[test]
fn render_streaming_rejects_lanczos_with_scaling() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: page.width() as u32 / 2,
height: page.height() as u32 / 2,
resampling: Resampling::Lanczos3,
..Default::default()
};
let err = render_streaming(page, &opts, |_, _| {}).unwrap_err();
assert!(matches!(err, RenderError::UnsupportedOption(_)));
}
#[test]
fn render_streaming_allows_lanczos_at_native_size() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: page.width() as u32,
height: page.height() as u32,
resampling: Resampling::Lanczos3,
..Default::default()
};
let mut row_count = 0usize;
render_streaming(page, &opts, |_, _| row_count += 1).expect("should succeed at native");
assert_eq!(row_count, page.height() as usize);
}
#[test]
fn render_streaming_rejects_user_rotation() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 60,
height: 80,
rotation: UserRotation::Cw90,
..Default::default()
};
let err = render_streaming(page, &opts, |_, _| {}).unwrap_err();
assert!(matches!(err, RenderError::UnsupportedOption(_)));
}
#[test]
fn render_streaming_rejects_zero_dimensions() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 0,
height: 80,
..Default::default()
};
let err = render_streaming(page, &opts, |_, _| {}).unwrap_err();
assert!(matches!(err, RenderError::InvalidDimensions { .. }));
}
#[test]
fn permissive_render_iw44_jb2_page() {
let doc = load_doc("czech.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 40,
height: 40,
permissive: true,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("permissive render should not error");
assert_eq!(pm.width, 40);
assert_eq!(pm.height, 40);
}
#[test]
fn oversized_render_dimensions_are_rejected() {
let doc = load_doc("czech.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 60_000,
height: 60_000, permissive: true,
..Default::default()
};
assert!(matches!(
render_pixmap(page, &opts),
Err(RenderError::InvalidDimensions { .. })
));
}
#[test]
fn permissive_render_bgjp_page() {
let doc = load_doc("bgjp_test.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 4,
height: 4,
permissive: true,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("permissive render of BGjp page should succeed");
assert_eq!(pm.width, 4);
assert_eq!(pm.height, 4);
}
#[test]
fn permissive_render_fgbz_page() {
let doc = load_doc("navm_fgbz.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 40,
height: 40,
permissive: true,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("permissive render of FGbz page should succeed");
assert!(pm.width > 0 && pm.height > 0);
}
#[test]
fn render_gray8_produces_grayscale_output() {
let doc = load_doc("boy_jb2.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 20,
height: 20,
..Default::default()
};
let gpm = render_gray8(page, &opts).expect("render_gray8 should succeed");
assert_eq!(gpm.width, 20);
assert_eq!(gpm.height, 20);
assert_eq!(gpm.data.len(), 20 * 20);
}
#[test]
fn render_coarse_rejects_zero_dimensions() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 0,
height: 50,
..Default::default()
};
let err = render_coarse(page, &opts).unwrap_err();
assert!(matches!(err, RenderError::InvalidDimensions { .. }));
}
#[test]
fn render_coarse_jb2_only_page_returns_none() {
let doc = load_doc("boy_jb2.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 40,
height: 40,
..Default::default()
};
let result = render_coarse(page, &opts).expect("should not error");
assert!(
result.is_none(),
"JB2-only page has no BG44 so render_coarse yields None"
);
}
#[test]
fn render_progressive_rejects_zero_dimensions() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 0,
height: 50,
..Default::default()
};
let err = render_progressive(page, &opts, 0).unwrap_err();
assert!(matches!(err, RenderError::InvalidDimensions { .. }));
}
#[test]
fn render_progressive_jb2_only_page_succeeds() {
let doc = load_doc("boy_jb2.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 40,
height: 40,
..Default::default()
};
let result = render_progressive(page, &opts, 0)
.expect("render_progressive should succeed even without BG44");
assert_eq!(result.width, 40);
assert_eq!(result.height, 40);
}
#[test]
fn lanczos3_at_native_resolution_skips_rerender() {
let doc = load_doc("bgjp_test.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: page.width() as u32,
height: page.height() as u32,
resampling: Resampling::Lanczos3,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("lanczos at native size must succeed");
assert_eq!(pm.width, page.width() as u32);
assert_eq!(pm.height, page.height() as u32);
}
#[test]
fn render_pixmap_rejects_zero_width() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 0,
height: 50,
..Default::default()
};
let err = render_pixmap(page, &opts).unwrap_err();
assert!(matches!(err, RenderError::InvalidDimensions { .. }));
}
fn make_doc_with_dims(w: u16, h: u16) -> Vec<u8> {
use crate::iff::{Chunk, DjvuFile, emit};
let mut info = vec![0u8; 10];
info[0] = (w >> 8) as u8;
info[1] = w as u8;
info[2] = (h >> 8) as u8;
info[3] = h as u8;
let file = DjvuFile {
root: Chunk::Form {
secondary_id: *b"DJVU",
length: 0,
children: vec![Chunk::Leaf {
id: *b"INFO",
data: info,
}],
},
};
emit(&file)
}
#[test]
fn fit_to_width_zero_page_width_uses_requested_width() {
let bytes = make_doc_with_dims(0, 100);
let doc = DjVuDocument::parse(&bytes).unwrap();
let page = doc.page(0).unwrap();
let opts = RenderOptions::fit_to_width(page, 40);
assert_eq!(opts.width, 40);
assert_eq!(opts.height, 40); }
#[test]
fn fit_to_height_zero_page_height_uses_requested_height() {
let bytes = make_doc_with_dims(100, 0);
let doc = DjVuDocument::parse(&bytes).unwrap();
let page = doc.page(0).unwrap();
let opts = RenderOptions::fit_to_height(page, 60);
assert_eq!(opts.height, 60);
assert_eq!(opts.width, 60); }
#[test]
fn fit_to_box_zero_page_dims_falls_back_to_box_max() {
let bytes = make_doc_with_dims(0, 0);
let doc = DjVuDocument::parse(&bytes).unwrap();
let page = doc.page(0).unwrap();
let opts = RenderOptions::fit_to_box(page, 80, 60);
assert_eq!(opts.width, 80);
assert_eq!(opts.height, 60);
}
#[test]
fn can_stream_true_at_native_resolution_with_lanczos3() {
let doc = load_doc("chicken.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: page.width() as u32,
height: page.height() as u32,
resampling: Resampling::Lanczos3,
..Default::default()
};
assert!(opts.can_stream(page));
}
#[test]
fn render_bilevel_at_tiny_scale_exercises_bg44_partial_empty() {
let doc = load_doc("boy_jb2.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 10,
height: 10,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("tiny bilevel render should succeed");
assert!(pm.width > 0 && pm.height > 0);
}
#[test]
fn render_with_bold_dilation_produces_output() {
let doc = load_doc("boy_jb2.djvu");
let page = doc.page(0).unwrap();
let opts = RenderOptions {
width: 40,
height: 40,
bold: 1,
..Default::default()
};
let pm = render_pixmap(page, &opts).expect("bold render should succeed");
assert_eq!(pm.width, 40);
assert_eq!(pm.height, 40);
}
}