pub use djvu_pixmap::{GrayPixmap, Pixmap};
#[inline]
fn lanczos3_kernel(x: f32) -> f32 {
let ax = x.abs();
if ax >= 3.0 {
return 0.0;
}
if ax < 1e-6 {
return 1.0;
}
let pi_x = core::f32::consts::PI * ax;
let sinc_x = pi_x.sin() / pi_x;
let pi_x3 = pi_x / 3.0;
let sinc_x3 = pi_x3.sin() / pi_x3;
sinc_x * sinc_x3
}
pub(crate) fn scale_lanczos3(src: &Pixmap, dst_w: u32, dst_h: u32) -> Pixmap {
let src_w = src.width;
let src_h = src.height;
if src_w == dst_w && src_h == dst_h {
return src.clone();
}
if dst_w == 0 || dst_h == 0 {
return Pixmap::white(dst_w.max(1), dst_h.max(1));
}
let h_scale = src_w as f32 / dst_w as f32;
let h_support = (3.0_f32 * h_scale.max(1.0)).ceil() as i32;
let mut mid = Pixmap::new(dst_w, src_h, 255, 255, 255, 255);
for oy in 0..src_h {
for ox in 0..dst_w {
let cx = (ox as f32 + 0.5) * h_scale - 0.5;
let x0 = (cx.floor() as i32 - h_support + 1).max(0);
let x1 = (cx.floor() as i32 + h_support).min(src_w as i32 - 1);
let mut r = 0.0_f32;
let mut g = 0.0_f32;
let mut b = 0.0_f32;
let mut w_sum = 0.0_f32;
for sx in x0..=x1 {
let w = lanczos3_kernel((sx as f32 - cx) / h_scale.max(1.0));
let (pr, pg, pb) = src.get_rgb(sx as u32, oy);
r += pr as f32 * w;
g += pg as f32 * w;
b += pb as f32 * w;
w_sum += w;
}
let norm = if w_sum.abs() > 1e-6 { 1.0 / w_sum } else { 1.0 };
mid.set_rgb(
ox,
oy,
(r * norm).round().clamp(0.0, 255.0) as u8,
(g * norm).round().clamp(0.0, 255.0) as u8,
(b * norm).round().clamp(0.0, 255.0) as u8,
);
}
}
let v_scale = src_h as f32 / dst_h as f32;
let v_support = (3.0_f32 * v_scale.max(1.0)).ceil() as i32;
let mut out = Pixmap::new(dst_w, dst_h, 255, 255, 255, 255);
for oy in 0..dst_h {
let cy = (oy as f32 + 0.5) * v_scale - 0.5;
let y0 = (cy.floor() as i32 - v_support + 1).max(0);
let y1 = (cy.floor() as i32 + v_support).min(src_h as i32 - 1);
for ox in 0..dst_w {
let mut r = 0.0_f32;
let mut g = 0.0_f32;
let mut b = 0.0_f32;
let mut w_sum = 0.0_f32;
for sy in y0..=y1 {
let w = lanczos3_kernel((sy as f32 - cy) / v_scale.max(1.0));
let (pr, pg, pb) = mid.get_rgb(ox, sy as u32);
r += pr as f32 * w;
g += pg as f32 * w;
b += pb as f32 * w;
w_sum += w;
}
let norm = if w_sum.abs() > 1e-6 { 1.0 / w_sum } else { 1.0 };
out.set_rgb(
ox,
oy,
(r * norm).round().clamp(0.0, 255.0) as u8,
(g * norm).round().clamp(0.0, 255.0) as u8,
(b * norm).round().clamp(0.0, 255.0) as u8,
);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lanczos3_kernel_unity_at_zero() {
assert!((lanczos3_kernel(0.0) - 1.0).abs() < 1e-5);
}
#[test]
fn lanczos3_kernel_zero_outside_support() {
assert_eq!(lanczos3_kernel(3.0), 0.0);
assert_eq!(lanczos3_kernel(-3.5), 0.0);
assert_eq!(lanczos3_kernel(10.0), 0.0);
}
#[test]
fn scale_lanczos3_correct_dimensions() {
let src = Pixmap::white(100, 80);
let dst = scale_lanczos3(&src, 50, 40);
assert_eq!(dst.width, 50);
assert_eq!(dst.height, 40);
}
#[test]
fn scale_lanczos3_noop_when_same_size() {
let src = Pixmap::new(4, 4, 200, 100, 50, 255);
let dst = scale_lanczos3(&src, 4, 4);
assert_eq!(dst.width, 4);
assert_eq!(dst.height, 4);
assert_eq!(dst.data, src.data);
}
#[test]
fn scale_lanczos3_preserves_solid_color() {
let src = Pixmap::new(20, 20, 200, 0, 0, 255);
let dst = scale_lanczos3(&src, 10, 10);
assert_eq!(dst.width, 10);
assert_eq!(dst.height, 10);
for chunk in dst.data.chunks_exact(4) {
let (r, g, b) = (chunk[0], chunk[1], chunk[2]);
assert!(
(r as i32 - 200).abs() <= 5 && g <= 5 && b <= 5,
"expected near-red (200,0,0), got ({r},{g},{b})"
);
}
}
}