#[cfg(not(feature = "std"))]
use alloc::{vec, vec::Vec};
use crate::djvu_document::DjVuPage;
use crate::iw44_new::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;
#[allow(unsafe_code)]
#[inline]
fn fill_alpha_255(buf: &mut [u8]) {
debug_assert_eq!(buf.len() % 4, 0);
#[cfg(target_arch = "x86_64")]
unsafe {
fill_alpha_255_sse2(buf);
}
#[cfg(not(target_arch = "x86_64"))]
for pixel in buf.chunks_exact_mut(4) {
pixel[3] = 255;
}
}
#[cfg(target_arch = "x86_64")]
#[allow(unsafe_code, unsafe_op_in_unsafe_fn)]
#[target_feature(enable = "sse2")]
unsafe fn fill_alpha_255_sse2(buf: &mut [u8]) {
use core::arch::x86_64::*;
let alpha_mask = _mm_set1_epi32(0xFF000000u32 as i32);
let ptr = buf.as_mut_ptr();
let chunks = buf.len() / 16;
for i in 0..chunks {
let p = ptr.add(i * 16) as *mut __m128i;
_mm_storeu_si128(
p,
_mm_or_si128(_mm_loadu_si128(p as *const __m128i), alpha_mask),
);
}
for i in (chunks * 4)..(buf.len() / 4) {
buf[i * 4 + 3] = 255;
}
}
#[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]
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;
let v = (numerator + (1 << (2 * FRACBITS - 1))) >> (2 * FRACBITS);
v.min(255) as u8
};
(
lerp(r00, r10, r01, r11),
lerp(g00, g10, g01, g11),
lerp(b00, b10, b01, b11),
)
}
#[inline]
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 = (fx >> FRACBITS).min(pm.width.saturating_sub(1));
let y0 = (fy >> FRACBITS).min(pm.height.saturating_sub(1));
let x1 = ((fx + fx_step) >> FRACBITS).min(pm.width);
let y1 = ((fy + fy_step) >> FRACBITS).min(pm.height);
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_any(
mask: &crate::bitmap::Bitmap,
fx: u32,
fy: u32,
fx_step: u32,
fy_step: u32,
) -> bool {
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);
for sy in y0..y1 {
for sx in x0..x1 {
if mask.get(sx, sy) {
return true;
}
}
}
false
}
#[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);
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.height - 1 - y, x, r, g, b);
}
}
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);
for y in 0..src.height {
for x in 0..src.width {
let (r, g, b) = src.get_rgb(x, y);
out.set_rgb(y, src.width - 1 - x, r, g, b);
}
}
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>>,
}
#[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()
}
}
#[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(
page: &DjVuPage,
max_chunks: usize,
subsample: u32,
) -> Result<Option<Pixmap>, RenderError> {
if max_chunks == usize::MAX {
let bg44_chunks = page.bg44_chunks();
if !bg44_chunks.is_empty() {
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(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(img.to_rgb_subsample(subsample)?));
}
}
#[cfg(feature = "std")]
if let Some(pm) = decode_bgjp(page)? {
return Ok(Some(pm));
}
Ok(None)
}
fn decode_background_chunks_permissive(
page: &DjVuPage,
max_chunks: usize,
subsample: u32,
) -> Option<Pixmap> {
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) {
if img.decode_chunk(chunk_data).is_err() {
break; }
}
return img.to_rgb_subsample(subsample).ok();
}
#[cfg(feature = "std")]
{
decode_bgjp(page).ok().flatten()
}
#[cfg(not(feature = "std"))]
None
}
fn decode_mask(page: &DjVuPage) -> Result<Option<crate::bitmap::Bitmap>, RenderError> {
match page.decoded_mask() {
Some(bm) => Ok(Some(bm.clone())),
None if page.find_chunk(b"Sjbz").is_some() => {
page.extract_mask().map_err(RenderError::from)
}
None => Ok(None),
}
}
fn decode_mask_indexed(
page: &DjVuPage,
) -> Result<Option<(crate::bitmap::Bitmap, Vec<i32>)>, RenderError> {
page.extract_mask_indexed().map_err(RenderError::from)
}
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(page: &DjVuPage) -> Result<Option<Pixmap>, RenderError> {
let fg44_chunks = page.fg44_chunks();
if !fg44_chunks.is_empty() {
return Ok(page.decoded_fg44().cloned());
}
#[cfg(feature = "std")]
if let Some(pm) = decode_fgjp(page)? {
return Ok(Some(pm));
}
Ok(None)
}
struct DecodedLayers {
bg: Option<Pixmap>,
fg_palette: Option<FgbzPalette>,
mask: Option<crate::bitmap::Bitmap>,
blit_map: Option<Vec<i32>>,
fg44: Option<Pixmap>,
}
fn decode_layers(
page: &DjVuPage,
opts: &RenderOptions,
bg_subsample: u32,
bg_chunk_limit: usize,
) -> Result<DecodedLayers, 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 {
bg = decode_background_chunks(page, bg_chunk_limit, bg_subsample)?;
let fg = decode_foreground_strict(page)?;
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| m.dilate_n(opts.bold as u32))
} else {
mask
};
Ok(DecodedLayers {
bg,
fg_palette,
mask,
blit_map,
fg44,
})
}
struct ForegroundLayers {
fg_palette: Option<FgbzPalette>,
mask: Option<crate::bitmap::Bitmap>,
blit_map: Option<Vec<i32>>,
fg44: Option<Pixmap>,
}
fn decode_foreground_strict(page: &DjVuPage) -> Result<ForegroundLayers, 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],
offset_x: u32,
offset_y: u32,
out_w: u32,
out_h: u32,
}
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,
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;
#[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,
);
} 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,
);
} else {
composite_rows_bilinear_one(ctx, oy as u32, fx_step, fy_step, row);
}
}
fill_alpha_255(buf);
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;
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,
);
} else {
composite_rows_bilinear_one(ctx, oy, fx_step, fy_step, &mut row_buf);
}
fill_alpha_255(&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];
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));
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 is_fg = if downscale {
if ctx.mask_shift > 0 {
let dpx = fx >> (FRACBITS + ctx.mask_shift);
let dpy = fy >> (FRACBITS + ctx.mask_shift);
dpx < mask.width && dpy < mask.height && mask.get(dpx, dpy)
} else {
mask_box_any(mask, fx, fy, fx_step, fy_step)
}
} else {
px < mask.width && py < mask.height && mask.get(px, py)
};
if is_fg {
pixel[0] = 0;
pixel[1] = 0;
pixel[2] = 0;
pixel[3] = 255;
} else {
pixel[0] = 255;
pixel[1] = 255;
pixel[2] = 255;
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()
{
if 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(&[]);
for (ox, pixel) in row_buf.chunks_exact_mut(4).enumerate() {
let px = ox.min((bg.width as usize).saturating_sub(1));
let is_fg = px < mask.width as usize
&& (mask_row.get(ox >> 3).copied().unwrap_or(0)
>> (7 - (ox & 7)))
& 1
!= 0;
let (r, g, b) = if is_fg {
(0u8, 0u8, 0u8)
} else {
let off = px * 4;
if let Some(q) = bg_row.get(off..off + 3) {
(q[0], q[1], q[2])
} else {
(255, 255, 255)
}
};
pixel[0] = lut[r as usize];
pixel[1] = lut[g as usize];
pixel[2] = lut[b as usize];
}
} 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 + 3) {
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;
}
}
}
return;
}
}
let bg_fy = map_plane_center_frac(fy, ctx.bg_y_q24);
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 = ctx
.mask
.is_some_and(|m| px < m.width && py < m.height && m.get(px, py));
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_nearest(fg, fg_fx, fg_fy)
} else {
(0, 0, 0)
}
} else if let Some(bg) = ctx.bg {
let bg_fx = map_plane_center_frac(fx, ctx.bg_x_q24);
sample_nearest(bg, bg_fx, bg_fy)
} else {
(255, 255, 255)
};
pixel[0] = ctx.gamma_lut[r as usize];
pixel[1] = ctx.gamma_lut[g as usize];
pixel[2] = ctx.gamma_lut[b as usize];
}
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(page_w.saturating_sub(1));
let is_fg = ctx
.mask
.is_some_and(|m| px < m.width && py < m.height && m.get(px, py));
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_nearest(fg, fg_fx, fg_fy)
} else {
(0, 0, 0)
}
} else if let Some(bg) = ctx.bg {
let bg_fx = map_plane_center_frac(fx, ctx.bg_x_q24);
let bg_fy = map_plane_center_frac(fy, ctx.bg_y_q24);
sample_bilinear(bg, bg_fx, bg_fy)
} else {
(255, 255, 255)
};
pixel[0] = ctx.gamma_lut[r as usize];
pixel[1] = ctx.gamma_lut[g as usize];
pixel[2] = ctx.gamma_lut[b as usize];
}
}
#[inline]
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],
) {
let fy = (oy + ctx.offset_y) * fy_step;
let bg_fy = ((fy as u64 * ctx.bg_y_q24) >> 24) as u32;
for (ox, pixel) in row_buf.chunks_exact_mut(4).enumerate() {
let fx = (ox as u32 + ctx.offset_x) * fx_step;
let is_fg = ctx.mask.is_some_and(|m| {
if ctx.mask_shift > 0 {
let px = fx >> (FRACBITS + ctx.mask_shift);
let py = fy >> (FRACBITS + ctx.mask_shift);
px < m.width && py < m.height && m.get(px, py)
} else {
mask_box_any(m, fx, fy, fx_step, fy_step)
}
});
let (r, g, b) = if is_fg {
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)
}
} else if let Some(bg) = ctx.bg {
let bg_fx = ((fx as u64 * ctx.bg_x_q24) >> 24) as u32;
sample_area_avg(bg, bg_fx, bg_fy, bg_fx_step, bg_fy_step)
} else {
(255, 255, 255)
};
pixel[0] = ctx.gamma_lut[r as usize];
pixel[1] = ctx.gamma_lut[g as usize];
pixel[2] = ctx.gamma_lut[b as usize];
}
}
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_ref(), fg_palette.as_ref());
let ctx = CompositeContext::from_layers(
page,
opts,
bg.as_ref(),
ctx_mask,
mask_shift,
fg_palette.as_ref(),
blit_map.as_deref(),
fg44.as_ref(),
&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_ref(), fg_palette.as_ref());
let ctx = CompositeContext::from_layers(
page,
opts,
bg.as_ref(),
ctx_mask,
mask_shift,
fg_palette.as_ref(),
blit_map.as_deref(),
fg44.as_ref(),
&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,
});
}
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_ref(),
mask.as_ref(),
0,
fg_palette.as_ref(),
blit_map.as_deref(),
fg44.as_ref(),
&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_ref(),
mask.as_ref(),
0,
fg_palette.as_ref(),
blit_map.as_deref(),
fg44.as_ref(),
&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}"))
}
#[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 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_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 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);
}
}