use crate::bitmap::Bitmap;
use crate::clip::{Clip, ClipResult};
use crate::pipe::{self, PipeSrc, PipeState};
use crate::types::PixelMode;
use color::Pixel;
use color::convert::splash_floor;
const MAX_NCOMPS: usize = 8;
pub trait ImageSource: Send {
fn get_row(&mut self, y: u32, row_buf: &mut [u8]);
}
pub trait MaskSource: Send {
fn get_row(&mut self, y: u32, row_buf: &mut [u8]);
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ImageResult {
Ok,
ZeroImage,
SingularMatrix,
ArbitraryTransformSkipped,
}
#[inline]
fn coord_lower(x: f64) -> i32 {
splash_floor(x)
}
#[inline]
fn coord_upper(x: f64) -> i32 {
splash_floor(x) + 1
}
#[inline]
fn check_det(a: f64, b: f64, c: f64, d: f64, eps: f64) -> bool {
#[expect(
clippy::suboptimal_flops,
reason = "matches the C++ arithmetic exactly; no numerics benefit here"
)]
{
(a * d - b * c).abs() >= eps
}
}
fn unpack_mask_row(packed: &[u8], width: usize, out: &mut [u8]) {
debug_assert!(
packed.len() >= width.div_ceil(8),
"unpack_mask_row: packed buffer too short ({} < {})",
packed.len(),
width.div_ceil(8),
);
debug_assert_eq!(
out.len(),
width,
"unpack_mask_row: out length must equal width"
);
for (i, slot) in out.iter_mut().enumerate() {
let byte = packed.get(i / 8).copied().unwrap_or(0);
let bit = (byte >> (7 - (i % 8))) & 1;
*slot = if bit != 0 { 255 } else { 0 };
}
}
struct ImageBounds {
x0: i32,
y0: i32,
x1: i32,
y1: i32,
vflip: bool,
}
fn compute_axis_aligned_bounds(matrix: &[f64; 6]) -> Result<ImageBounds, ImageResult> {
if !check_det(matrix[0], matrix[1], matrix[2], matrix[3], 1e-6) {
return Err(ImageResult::SingularMatrix);
}
let minor_zero = matrix[1] == 0.0 && matrix[2] == 0.0;
if !minor_zero || matrix[0] <= 0.0 {
return Err(ImageResult::ArbitraryTransformSkipped);
}
let (y0, y1, vflip) = if matrix[3] > 0.0 {
(
coord_lower(matrix[5]),
coord_upper(matrix[3] + matrix[5]),
false,
)
} else if matrix[3] < 0.0 {
(
coord_lower(matrix[3] + matrix[5]),
coord_upper(matrix[5]),
true,
)
} else {
return Err(ImageResult::SingularMatrix);
};
let x0 = coord_lower(matrix[4]);
let x1 = coord_upper(matrix[0] + matrix[4]);
let x1 = if x0 == x1 { x1 + 1 } else { x1 };
let y1 = if y0 == y1 { y1 + 1 } else { y1 };
Ok(ImageBounds {
x0,
y0,
x1,
y1,
vflip,
})
}
fn vflip_rows(data: &mut [u8], row_stride: usize) {
if row_stride == 0 {
return;
}
let nrows = data.len() / row_stride;
let mut lo = 0usize;
let mut hi = nrows.saturating_sub(1);
while lo < hi {
let (lower, upper) = data.split_at_mut(hi * row_stride);
lower[lo * row_stride..lo * row_stride + row_stride]
.swap_with_slice(&mut upper[..row_stride]);
lo += 1;
hi -= 1;
}
}
#[inline]
const fn bresenham_step(acc: &mut usize, q: usize, scaled: usize, p: usize) -> usize {
*acc += q;
if *acc >= scaled {
*acc -= scaled;
p + 1
} else {
p
}
}
const MASK_SAT_FACTOR: u32 = 255u32 << 23;
const IMAGE_SAT_FACTOR: u32 = 1u32 << 23;
struct MaskAsImage<'a> {
mask: &'a mut dyn MaskSource,
packed_buf: Vec<u8>,
src_w: usize,
}
impl<'a> MaskAsImage<'a> {
fn new(mask: &'a mut dyn MaskSource, src_w: usize) -> Self {
Self {
mask,
packed_buf: vec![0u8; src_w.div_ceil(8)],
src_w,
}
}
}
impl ImageSource for MaskAsImage<'_> {
fn get_row(&mut self, y: u32, row_buf: &mut [u8]) {
self.mask.get_row(y, &mut self.packed_buf);
unpack_mask_row(&self.packed_buf, self.src_w, row_buf);
}
}
fn scale_mask(
mask_src: &mut dyn MaskSource,
src_w: usize,
src_h: usize,
scaled_w: usize,
scaled_h: usize,
) -> Vec<u8> {
let mut adapter = MaskAsImage::new(mask_src, src_w);
scale_image_inner(
&mut adapter,
src_w,
src_h,
scaled_w,
scaled_h,
1,
MASK_SAT_FACTOR,
)
}
#[inline]
fn saturate_scaled(sum: u32, d: u32) -> u8 {
let scaled = ((u64::from(sum) * u64::from(d)) >> 23).min(255);
u8::try_from(scaled).expect("scaled box-filter pixel was just clamped to <= 255")
}
#[inline]
fn xdown_divisors(sat_factor: u32, y_step: usize, xp: usize) -> (u32, u32) {
let d_full = if xp > 0 {
let denom = u32::try_from(y_step.saturating_mul(xp))
.expect("y_step * xp fits in u32 for practical image sizes");
sat_factor / denom
} else {
0
};
let denom_plus = u32::try_from(y_step.saturating_mul(xp + 1))
.expect("y_step * (xp+1) fits in u32 for practical image sizes");
let d_plus_one = sat_factor / denom_plus;
(d_full, d_plus_one)
}
fn scale_image_inner(
image_src: &mut dyn ImageSource,
src_w: usize,
src_h: usize,
scaled_w: usize,
scaled_h: usize,
ncomps: usize,
sat_factor: u32,
) -> Vec<u8> {
let mut dest = vec![0u8; scaled_w * scaled_h * ncomps];
let mut line_buf = vec![0u8; src_w * ncomps];
if scaled_h < src_h {
if scaled_w < src_w {
scale_kernel_ydown_xdown(
image_src,
src_w,
src_h,
scaled_w,
scaled_h,
ncomps,
sat_factor,
&mut dest,
&mut line_buf,
);
} else {
scale_kernel_ydown_xup(
image_src,
src_w,
src_h,
scaled_w,
scaled_h,
ncomps,
sat_factor,
&mut dest,
&mut line_buf,
);
}
} else if scaled_w < src_w {
scale_kernel_yup_xdown(
image_src,
src_w,
src_h,
scaled_w,
scaled_h,
ncomps,
sat_factor,
&mut dest,
&mut line_buf,
);
} else {
scale_kernel_yup_xup(
image_src,
src_w,
src_h,
scaled_w,
scaled_h,
ncomps,
&mut dest,
&mut line_buf,
);
}
dest
}
#[expect(
clippy::too_many_arguments,
reason = "kernel is private; all params are necessary to share the body across mask + image"
)]
fn scale_kernel_ydown_xdown(
image_src: &mut dyn ImageSource,
src_w: usize,
src_h: usize,
scaled_w: usize,
scaled_h: usize,
ncomps: usize,
sat_factor: u32,
dest: &mut [u8],
line_buf: &mut [u8],
) {
let yp = src_h / scaled_h;
let yq = src_h % scaled_h;
let xp = src_w / scaled_w;
let xq = src_w % scaled_w;
let mut pix_buf = vec![0u32; src_w * ncomps];
let mut yt = 0usize;
let mut dest_off = 0usize;
let mut src_y = 0u32;
for _dy in 0..scaled_h {
let y_step = bresenham_step(&mut yt, yq, scaled_h, yp);
pix_buf.fill(0);
for _ in 0..y_step {
image_src.get_row(src_y, line_buf);
src_y += 1;
for (pix, &lb) in pix_buf.iter_mut().zip(line_buf.iter()) {
*pix += u32::from(lb);
}
}
let (d_full, d_plus_one) = xdown_divisors(sat_factor, y_step, xp);
let mut xt = 0usize;
let mut xx = 0usize;
for _dx in 0..scaled_w {
let x_step = bresenham_step(&mut xt, xq, scaled_w, xp);
let d = if x_step == xp + 1 { d_plus_one } else { d_full };
for c in 0..ncomps {
let sum: u32 = (0..x_step).map(|i| pix_buf[(xx + i) * ncomps + c]).sum();
dest[dest_off + c] = saturate_scaled(sum, d);
}
xx += x_step;
dest_off += ncomps;
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "kernel is private; all params are necessary to share the body across mask + image"
)]
fn scale_kernel_ydown_xup(
image_src: &mut dyn ImageSource,
src_w: usize,
src_h: usize,
scaled_w: usize,
scaled_h: usize,
ncomps: usize,
sat_factor: u32,
dest: &mut [u8],
line_buf: &mut [u8],
) {
let yp = src_h / scaled_h;
let yq = src_h % scaled_h;
let xp = scaled_w / src_w;
let xq = scaled_w % src_w;
let mut pix_buf = vec![0u32; src_w * ncomps];
let mut yt = 0usize;
let mut dest_off = 0usize;
let mut src_y = 0u32;
for _dy in 0..scaled_h {
let y_step = bresenham_step(&mut yt, yq, scaled_h, yp);
pix_buf.fill(0);
for _ in 0..y_step {
image_src.get_row(src_y, line_buf);
src_y += 1;
for (pix, &lb) in pix_buf.iter_mut().zip(line_buf.iter()) {
*pix += u32::from(lb);
}
}
let d = sat_factor
/ u32::try_from(y_step).expect("y_step ≤ src_h fits in u32 for practical image sizes");
let mut xt = 0usize;
let mut pix_vals = [0u8; MAX_NCOMPS];
for sx in 0..src_w {
let x_step = bresenham_step(&mut xt, xq, src_w, xp);
let base = sx * ncomps;
for c in 0..ncomps {
pix_vals[c] = saturate_scaled(pix_buf[base + c], d);
}
for _ in 0..x_step {
dest[dest_off..dest_off + ncomps].copy_from_slice(&pix_vals[..ncomps]);
dest_off += ncomps;
}
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "kernel is private; all params are necessary to share the body across mask + image"
)]
fn scale_kernel_yup_xdown(
image_src: &mut dyn ImageSource,
src_w: usize,
src_h: usize,
scaled_w: usize,
scaled_h: usize,
ncomps: usize,
sat_factor: u32,
dest: &mut [u8],
line_buf: &mut [u8],
) {
let yp = scaled_h / src_h;
let yq = scaled_h % src_h;
let xp = src_w / scaled_w;
let xq = src_w % scaled_w;
let (d_full, d_plus_one) = xdown_divisors(sat_factor, 1, xp);
let mut yt = 0usize;
let mut dest_off = 0usize;
for sy in 0..src_h {
let y_step = bresenham_step(&mut yt, yq, src_h, yp);
let src_y = u32::try_from(sy)
.expect("source row index ≤ src_h fits in u32 for practical image sizes");
image_src.get_row(src_y, line_buf);
let row_start = dest_off;
let mut xt = 0usize;
let mut xx = 0usize;
for dx in 0..scaled_w {
let x_step = bresenham_step(&mut xt, xq, scaled_w, xp);
let d = if x_step == xp + 1 { d_plus_one } else { d_full };
for c in 0..ncomps {
let sum: u32 = (0..x_step)
.map(|i| u32::from(line_buf[(xx + i) * ncomps + c]))
.sum();
dest[row_start + dx * ncomps + c] = saturate_scaled(sum, d);
}
xx += x_step;
}
dest_off += scaled_w * ncomps;
for i in 1..y_step {
dest.copy_within(
row_start..row_start + scaled_w * ncomps,
row_start + i * scaled_w * ncomps,
);
}
dest_off += (y_step - 1) * scaled_w * ncomps;
}
}
#[expect(
clippy::too_many_arguments,
reason = "kernel is private; all params are necessary to share the body across mask + image"
)]
fn scale_kernel_yup_xup(
image_src: &mut dyn ImageSource,
src_w: usize,
src_h: usize,
scaled_w: usize,
scaled_h: usize,
ncomps: usize,
dest: &mut [u8],
line_buf: &mut [u8],
) {
let yp = scaled_h / src_h;
let yq = scaled_h % src_h;
let xp = scaled_w / src_w;
let xq = scaled_w % src_w;
let mut yt = 0usize;
let mut dest_off = 0usize;
for sy in 0..src_h {
let y_step = bresenham_step(&mut yt, yq, src_h, yp);
let src_y = u32::try_from(sy)
.expect("source row index ≤ src_h fits in u32 for practical image sizes");
image_src.get_row(src_y, line_buf);
let row_start = dest_off;
let mut xt = 0usize;
let mut xx = 0usize;
for sx in 0..src_w {
let x_step = bresenham_step(&mut xt, xq, src_w, xp);
let pix_start = sx * ncomps;
for j in 0..x_step {
let off = row_start + (xx + j) * ncomps;
dest[off..off + ncomps].copy_from_slice(&line_buf[pix_start..pix_start + ncomps]);
}
xx += x_step;
}
dest_off += scaled_w * ncomps;
for i in 1..y_step {
dest.copy_within(
row_start..row_start + scaled_w * ncomps,
row_start + i * scaled_w * ncomps,
);
}
dest_off += (y_step - 1) * scaled_w * ncomps;
}
}
fn scale_image(
image_src: &mut dyn ImageSource,
src_w: usize,
src_h: usize,
scaled_w: usize,
scaled_h: usize,
ncomps: usize,
) -> Vec<u8> {
scale_image_inner(
image_src,
src_w,
src_h,
scaled_w,
scaled_h,
ncomps,
IMAGE_SAT_FACTOR,
)
}
struct ImageRowPattern<'a> {
data: &'a [u8],
}
impl crate::pipe::Pattern for ImageRowPattern<'_> {
fn fill_span(&self, _y: i32, _x0: i32, _x1: i32, out: &mut [u8]) {
assert_eq!(
out.len(),
self.data.len(),
"ImageRowPattern::fill_span: out.len()={} != data.len()={} \
(ncomps/P::BYTES mismatch — check draw_image caller)",
out.len(),
self.data.len(),
);
out.copy_from_slice(self.data);
}
fn is_static_color(&self) -> bool {
false
}
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors Splash::blitMask API; all params necessary"
)]
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
reason = "run_shape.len() / scaled_w / scaled_h ≤ bitmap dims ≤ i32::MAX for practical image sizes"
)]
fn blit_mask<P: Pixel>(
bitmap: &mut Bitmap<P>,
clip: &Clip,
pipe: &PipeState<'_>,
src: &PipeSrc<'_>,
scaled_mask: &[u8],
scaled_w: i32,
scaled_h: i32,
x_dest: i32,
y_dest: i32,
clip_all_inside: bool,
) {
#[expect(
clippy::cast_possible_wrap,
reason = "bitmap dims ≤ i32::MAX in practice"
)]
let bmp_w = bitmap.width as i32;
#[expect(
clippy::cast_possible_wrap,
reason = "bitmap dims ≤ i32::MAX in practice"
)]
let bmp_h = bitmap.height as i32;
for dy in 0..scaled_h {
let y = y_dest + dy;
if y < 0 || y >= bmp_h {
continue;
}
#[expect(clippy::cast_sign_loss, reason = "dy ≥ 0")]
let row_off = dy as usize * scaled_w as usize;
#[expect(clippy::cast_sign_loss, reason = "y ≥ 0 by guard above")]
let y_u = y as u32;
let mut run_start: Option<i32> = None;
let mut run_shape: Vec<u8> = Vec::new();
macro_rules! flush_run {
() => {
if let Some(rs) = run_start.take() {
let rx1 = rs + run_shape.len() as i32 - 1;
#[expect(clippy::cast_sign_loss, reason = "rs ≥ 0")]
let byte_off = rs as usize * P::BYTES;
#[expect(clippy::cast_sign_loss, reason = "rx1 ≥ rs ≥ 0")]
let byte_end = (rx1 as usize + 1) * P::BYTES;
#[expect(clippy::cast_sign_loss, reason = "rs ≥ 0")]
let alpha_lo = rs as usize;
#[expect(clippy::cast_sign_loss, reason = "rx1 ≥ rs ≥ 0")]
let alpha_hi = rx1 as usize;
let (row, alpha) = bitmap.row_and_alpha_mut(y_u);
let dst_pixels = &mut row[byte_off..byte_end];
let dst_alpha = alpha.map(|a| &mut a[alpha_lo..=alpha_hi]);
pipe::render_span::<P>(
pipe,
src,
dst_pixels,
dst_alpha,
Some(&run_shape),
rs,
rx1,
y,
);
run_shape.clear();
}
};
}
for dx in 0..scaled_w {
let x = x_dest + dx;
if x < 0 || x >= bmp_w {
flush_run!();
continue;
}
#[expect(clippy::cast_sign_loss, reason = "dx ≥ 0")]
let coverage = scaled_mask[row_off + dx as usize];
let inside_clip = clip_all_inside || clip.test(x, y);
if coverage > 0 && inside_clip {
if run_start.is_none() {
run_start = Some(x);
}
run_shape.push(coverage);
} else {
flush_run!();
}
}
flush_run!();
}
}
#[expect(
clippy::too_many_arguments,
reason = "all context is necessary for a span emit"
)]
fn emit_image_span<P: Pixel>(
bitmap: &mut Bitmap<P>,
pipe: &PipeState<'_>,
img_row: &[u8],
ncomps: usize,
x_src_off: usize,
x0: i32,
x1: i32,
y: i32,
) {
#[expect(clippy::cast_sign_loss, reason = "x1 ≥ x0 ≥ 0 by caller invariant")]
let count = (x1 - x0 + 1) as usize;
let data = &img_row[x_src_off * ncomps..(x_src_off + count) * ncomps];
let row_src = PipeSrc::Pattern(&ImageRowPattern { data });
#[expect(clippy::cast_sign_loss, reason = "x0 ≥ 0 by caller invariant")]
let byte_off = x0 as usize * P::BYTES;
#[expect(clippy::cast_sign_loss, reason = "x1 ≥ x0 ≥ 0 by caller invariant")]
let byte_end = (x1 as usize + 1) * P::BYTES;
#[expect(clippy::cast_sign_loss, reason = "x0 ≥ 0 by caller invariant")]
let (row, alpha) = bitmap.row_and_alpha_mut(y as u32);
let dst_pixels = &mut row[byte_off..byte_end];
#[expect(clippy::cast_sign_loss, reason = "x0/x1 ≥ 0 by caller invariant")]
let dst_alpha = alpha.map(|a| &mut a[x0 as usize..=x1 as usize]);
pipe::render_span::<P>(pipe, &row_src, dst_pixels, dst_alpha, None, x0, x1, y);
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors Splash::blitImage API; all params necessary"
)]
fn blit_image<P: Pixel>(
bitmap: &mut Bitmap<P>,
clip: &Clip,
pipe: &PipeState<'_>,
scaled_img: &[u8],
scaled_w: i32,
scaled_h: i32,
x_dest: i32,
y_dest: i32,
clip_res: ClipResult,
) {
let ncomps = P::BYTES;
debug_assert!(
ncomps <= MAX_NCOMPS,
"blit_image: P::BYTES={ncomps} exceeds MAX_NCOMPS={MAX_NCOMPS}",
);
#[expect(
clippy::cast_possible_wrap,
reason = "bitmap dims ≤ i32::MAX in practice"
)]
let bmp_w = bitmap.width as i32;
#[expect(
clippy::cast_possible_wrap,
reason = "bitmap dims ≤ i32::MAX in practice"
)]
let bmp_h = bitmap.height as i32;
let clip_all_inside = clip_res == ClipResult::AllInside;
for dy in 0..scaled_h {
let y = y_dest + dy;
if y < 0 || y >= bmp_h {
continue;
}
#[expect(clippy::cast_sign_loss, reason = "dy ≥ 0 and scaled_w ≥ 0")]
let img_row_off = dy as usize * scaled_w as usize * ncomps;
#[expect(
clippy::cast_sign_loss,
reason = "scaled_w ≥ 0 (it is the dest rect width)"
)]
let img_row = &scaled_img[img_row_off..img_row_off + scaled_w as usize * ncomps];
let x_lo = x_dest.max(0);
let x_hi = (x_dest + scaled_w - 1).min(bmp_w - 1);
if x_lo > x_hi {
continue;
}
if clip_all_inside {
#[expect(clippy::cast_sign_loss, reason = "x_lo ≥ x_dest ≥ 0 after clamp")]
let x_src_off = (x_lo - x_dest) as usize;
emit_image_span::<P>(bitmap, pipe, img_row, ncomps, x_src_off, x_lo, x_hi, y);
} else {
let mut run_x0: Option<i32> = None;
let mut run_x1 = x_lo;
for dx in 0..scaled_w {
let x = x_dest + dx;
let in_bmp = x >= x_lo && x <= x_hi;
let visible = in_bmp && clip.test(x, y);
if visible {
if run_x0.is_none() {
run_x0 = Some(x);
}
run_x1 = x;
} else if let Some(x0) = run_x0.take() {
#[expect(clippy::cast_sign_loss, reason = "x0 ≥ x_dest ≥ 0 inside bmp bounds")]
let x_src_off = (x0 - x_dest) as usize;
emit_image_span::<P>(bitmap, pipe, img_row, ncomps, x_src_off, x0, run_x1, y);
}
}
if let Some(x0) = run_x0 {
#[expect(clippy::cast_sign_loss, reason = "x0 ≥ x_dest ≥ 0 inside bmp bounds")]
let x_src_off = (x0 - x_dest) as usize;
emit_image_span::<P>(bitmap, pipe, img_row, ncomps, x_src_off, x0, run_x1, y);
}
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors Splash::fillImageMask API; all params necessary"
)]
pub fn fill_image_mask<P: Pixel>(
bitmap: &mut Bitmap<P>,
clip: &Clip,
pipe: &PipeState<'_>,
src: &PipeSrc<'_>,
mask_src: &mut dyn MaskSource,
src_w: u32,
src_h: u32,
matrix: &[f64; 6],
) -> ImageResult {
if src_w == 0 || src_h == 0 {
return ImageResult::ZeroImage;
}
let bounds = match compute_axis_aligned_bounds(matrix) {
Ok(b) => b,
Err(e) => return e,
};
let ImageBounds {
x0,
y0,
x1,
y1,
vflip,
} = bounds;
let clip_res = clip.test_rect(x0, y0, x1 - 1, y1 - 1);
if clip_res == ClipResult::AllOutside {
return ImageResult::Ok;
}
#[expect(
clippy::cast_sign_loss,
reason = "x1 > x0 is guaranteed by compute_axis_aligned_bounds"
)]
let scaled_w = (x1 - x0) as usize;
#[expect(
clippy::cast_sign_loss,
reason = "y1 > y0 is guaranteed by compute_axis_aligned_bounds"
)]
let scaled_h = (y1 - y0) as usize;
let mut scaled = scale_mask(mask_src, src_w as usize, src_h as usize, scaled_w, scaled_h);
if vflip {
vflip_rows(&mut scaled, scaled_w);
}
blit_mask::<P>(
bitmap,
clip,
pipe,
src,
&scaled,
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
reason = "scaled_w ≤ bitmap.width ≤ i32::MAX"
)]
{
scaled_w as i32
},
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
reason = "scaled_h ≤ bitmap.height ≤ i32::MAX"
)]
{
scaled_h as i32
},
x0,
y0,
clip_res == ClipResult::AllInside,
);
ImageResult::Ok
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors Splash::drawImage API; all params necessary"
)]
pub fn draw_image<P: Pixel>(
bitmap: &mut Bitmap<P>,
clip: &Clip,
pipe: &PipeState<'_>,
image_src: &mut dyn ImageSource,
src_mode: PixelMode,
src_w: u32,
src_h: u32,
matrix: &[f64; 6],
) -> ImageResult {
let _ = src_mode;
let ncomps = P::BYTES;
debug_assert!(
ncomps <= MAX_NCOMPS,
"draw_image: P::BYTES={ncomps} exceeds MAX_NCOMPS={MAX_NCOMPS}",
);
if src_w == 0 || src_h == 0 {
return ImageResult::ZeroImage;
}
let bounds = match compute_axis_aligned_bounds(matrix) {
Ok(b) => b,
Err(e) => return e,
};
let ImageBounds {
x0,
y0,
x1,
y1,
vflip,
} = bounds;
let clip_res = clip.test_rect(x0, y0, x1 - 1, y1 - 1);
if clip_res == ClipResult::AllOutside {
return ImageResult::Ok;
}
#[expect(
clippy::cast_sign_loss,
reason = "x1 > x0 is guaranteed by compute_axis_aligned_bounds"
)]
let scaled_w = (x1 - x0) as usize;
#[expect(
clippy::cast_sign_loss,
reason = "y1 > y0 is guaranteed by compute_axis_aligned_bounds"
)]
let scaled_h = (y1 - y0) as usize;
let mut scaled = scale_image(
image_src,
src_w as usize,
src_h as usize,
scaled_w,
scaled_h,
ncomps,
);
if vflip {
vflip_rows(&mut scaled, scaled_w * ncomps);
}
blit_image::<P>(
bitmap,
clip,
pipe,
&scaled,
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
reason = "scaled_w ≤ bitmap.width ≤ i32::MAX"
)]
{
scaled_w as i32
},
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
reason = "scaled_h ≤ bitmap.height ≤ i32::MAX"
)]
{
scaled_h as i32
},
x0,
y0,
clip_res,
);
ImageResult::Ok
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bitmap::Bitmap;
use crate::clip::Clip;
use crate::pipe::PipeSrc;
use crate::testutil::{make_clip, simple_pipe};
use color::Rgb8;
struct SolidMask;
impl MaskSource for SolidMask {
fn get_row(&mut self, _y: u32, row_buf: &mut [u8]) {
row_buf.fill(0xFF);
}
}
struct CheckerMask;
impl MaskSource for CheckerMask {
fn get_row(&mut self, _y: u32, row_buf: &mut [u8]) {
for (i, b) in row_buf.iter_mut().enumerate() {
*b = if i % 2 == 0 { 0xAA } else { 0x55 };
}
}
}
struct SolidColor {
r: u8,
g: u8,
b: u8,
}
impl ImageSource for SolidColor {
fn get_row(&mut self, _y: u32, row_buf: &mut [u8]) {
for chunk in row_buf.chunks_exact_mut(3) {
chunk[0] = self.r;
chunk[1] = self.g;
chunk[2] = self.b;
}
}
}
#[test]
fn fill_image_mask_solid_paints_rect() {
let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
let clip = make_clip(8, 8);
let pipe = simple_pipe();
let color = [255u8, 0, 0]; let src = PipeSrc::Solid(&color);
let mut mask = SolidMask;
let mat = [4.0f64, 0.0, 0.0, 4.0, 2.0, 2.0];
let result = fill_image_mask::<Rgb8>(&mut bmp, &clip, &pipe, &src, &mut mask, 4, 4, &mat);
assert_eq!(result, ImageResult::Ok);
for y in 2..6u32 {
for x in 2..6usize {
assert_eq!(bmp.row(y)[x].r, 255, "row={y} col={x}");
}
}
assert_eq!(bmp.row(0)[0].r, 0, "outside should be unpainted");
}
#[test]
fn fill_image_mask_vflip_no_crash() {
let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
let clip = make_clip(8, 8);
let pipe = simple_pipe();
let color = [0u8, 255, 0];
let src = PipeSrc::Solid(&color);
let mut mask = SolidMask;
let mat = [4.0f64, 0.0, 0.0, -4.0, 2.0, 6.0];
let result = fill_image_mask::<Rgb8>(&mut bmp, &clip, &pipe, &src, &mut mask, 4, 4, &mat);
assert_eq!(result, ImageResult::Ok);
}
#[test]
fn fill_image_mask_singular_matrix() {
let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
let clip = make_clip(8, 8);
let pipe = simple_pipe();
let color = [0u8, 0, 0];
let src = PipeSrc::Solid(&color);
let mut mask = SolidMask;
let mat = [0.0f64, 0.0, 0.0, 0.0, 0.0, 0.0]; let result = fill_image_mask::<Rgb8>(&mut bmp, &clip, &pipe, &src, &mut mask, 4, 4, &mat);
assert_eq!(result, ImageResult::SingularMatrix);
}
#[test]
fn draw_image_solid_color() {
let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
let clip = make_clip(8, 8);
let pipe = simple_pipe();
let mut img_src = SolidColor { r: 0, g: 0, b: 200 };
let mat = [4.0f64, 0.0, 0.0, 4.0, 0.0, 0.0];
let result = draw_image::<Rgb8>(
&mut bmp,
&clip,
&pipe,
&mut img_src,
crate::types::PixelMode::Rgb8,
4,
4,
&mat,
);
assert_eq!(result, ImageResult::Ok);
for y in 0..5u32 {
for x in 0..5usize {
assert_eq!(bmp.row(y)[x].b, 200, "row={y} col={x}");
}
}
assert_eq!(bmp.row(6)[0].b, 0, "row 6 should be unpainted");
}
#[test]
fn draw_image_arbitrary_transform_skipped() {
let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
let clip = make_clip(8, 8);
let pipe = simple_pipe();
let mut img_src = SolidColor {
r: 100,
g: 100,
b: 100,
};
let mat = [2.0f64, 1.0, 1.0, 2.0, 0.0, 0.0];
let result = draw_image::<Rgb8>(
&mut bmp,
&clip,
&pipe,
&mut img_src,
crate::types::PixelMode::Rgb8,
4,
4,
&mat,
);
assert_eq!(result, ImageResult::ArbitraryTransformSkipped);
}
#[test]
fn draw_image_zero_size() {
let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
let clip = make_clip(8, 8);
let pipe = simple_pipe();
let mut img_src = SolidColor { r: 1, g: 2, b: 3 };
let mat = [4.0f64, 0.0, 0.0, 4.0, 0.0, 0.0];
let result = draw_image::<Rgb8>(
&mut bmp,
&clip,
&pipe,
&mut img_src,
crate::types::PixelMode::Rgb8,
0,
4,
&mat,
);
assert_eq!(result, ImageResult::ZeroImage);
}
#[test]
fn draw_image_upsample_2x2_to_4x4() {
let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
let clip = make_clip(8, 8);
let pipe = simple_pipe();
let mut img_src = SolidColor {
r: 128,
g: 64,
b: 32,
};
let mat = [4.0f64, 0.0, 0.0, 4.0, 0.0, 0.0];
let result = draw_image::<Rgb8>(
&mut bmp,
&clip,
&pipe,
&mut img_src,
crate::types::PixelMode::Rgb8,
2,
2,
&mat,
);
assert_eq!(result, ImageResult::Ok);
for y in 0..4u32 {
for x in 0..4usize {
assert_eq!(bmp.row(y)[x].r, 128, "row={y} col={x} R");
assert_eq!(bmp.row(y)[x].g, 64, "row={y} col={x} G");
assert_eq!(bmp.row(y)[x].b, 32, "row={y} col={x} B");
}
}
}
#[test]
fn draw_image_downsample_4x4_to_2x2() {
let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
let clip = make_clip(8, 8);
let pipe = simple_pipe();
let mut img_src = SolidColor {
r: 200,
g: 100,
b: 50,
};
let mat = [2.0f64, 0.0, 0.0, 2.0, 0.0, 0.0];
let result = draw_image::<Rgb8>(
&mut bmp,
&clip,
&pipe,
&mut img_src,
crate::types::PixelMode::Rgb8,
4,
4,
&mat,
);
assert_eq!(result, ImageResult::Ok);
for y in 0..2u32 {
for x in 0..2usize {
assert_eq!(bmp.row(y)[x].r, 200, "row={y} col={x} R");
}
}
}
#[test]
fn fill_image_mask_checker_partial() {
let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
let clip = make_clip(8, 8);
let pipe = simple_pipe();
let color = [255u8, 255, 0]; let src = PipeSrc::Solid(&color);
let mut mask = CheckerMask;
let mat = [8.0f64, 0.0, 0.0, 8.0, 0.0, 0.0];
let result = fill_image_mask::<Rgb8>(&mut bmp, &clip, &pipe, &src, &mut mask, 8, 8, &mat);
assert_eq!(result, ImageResult::Ok);
let any_painted = (0..8u32).any(|y| (0..8usize).any(|x| bmp.row(y)[x].r > 0));
assert!(any_painted, "at least some pixels must be painted");
}
#[test]
fn unpack_mask_row_aa() {
let packed = [0xAAu8];
let mut out = [0u8; 8];
unpack_mask_row(&packed, 8, &mut out);
assert_eq!(out, [255, 0, 255, 0, 255, 0, 255, 0]);
}
#[test]
fn scale_mask_identity_solid() {
struct IdentityMask;
impl MaskSource for IdentityMask {
fn get_row(&mut self, _y: u32, row_buf: &mut [u8]) {
row_buf.fill(0xFF);
}
}
let mut ms = IdentityMask;
let out = scale_mask(&mut ms, 4, 1, 4, 1);
assert_eq!(out, [255u8, 255, 255, 255]);
}
#[test]
fn vflip_rows_three_rows() {
let mut data = vec![
1u8, 2, 3, 4, 5, 6,
]; vflip_rows(&mut data, 2);
assert_eq!(data, [5, 6, 3, 4, 1, 2]);
}
#[test]
fn vflip_rows_single_row_noop() {
let mut data = vec![7u8, 8, 9];
vflip_rows(&mut data, 3);
assert_eq!(data, [7, 8, 9]);
}
#[test]
fn vflip_rows_empty_noop() {
let mut data: Vec<u8> = vec![];
vflip_rows(&mut data, 1);
assert!(data.is_empty());
}
#[test]
fn draw_image_vflip_reverses_rows() {
struct TwoRowImage;
impl ImageSource for TwoRowImage {
fn get_row(&mut self, y: u32, row_buf: &mut [u8]) {
let (r, g, b) = if y == 0 { (255, 0, 0) } else { (0, 0, 255) };
for chunk in row_buf.chunks_exact_mut(3) {
chunk[0] = r;
chunk[1] = g;
chunk[2] = b;
}
}
}
let mut bmp: Bitmap<Rgb8> = Bitmap::new(4, 4, 1, false);
let clip = make_clip(4, 4);
let pipe = simple_pipe();
let mat = [2.0f64, 0.0, 0.0, -2.0, 0.0, 2.0];
let result = draw_image::<Rgb8>(
&mut bmp,
&clip,
&pipe,
&mut TwoRowImage,
crate::types::PixelMode::Rgb8,
2,
2,
&mat,
);
assert_eq!(result, ImageResult::Ok);
let has_red = (0..3u32).any(|y| bmp.row(y)[0].r == 255 && bmp.row(y)[0].b == 0);
let has_blue = (0..3u32).any(|y| bmp.row(y)[0].b == 255 && bmp.row(y)[0].r == 0);
assert!(has_red, "vflip: expected red pixels in output");
assert!(has_blue, "vflip: expected blue pixels in output");
}
#[test]
fn draw_image_partial_clip_paints_only_inside() {
let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
let clip = Clip::new(2.0, 0.0, 4.999, 7.999, false);
let pipe = simple_pipe();
let mut img_src = SolidColor {
r: 255,
g: 255,
b: 255,
};
let mat = [8.0f64, 0.0, 0.0, 8.0, 0.0, 0.0];
let result = draw_image::<Rgb8>(
&mut bmp,
&clip,
&pipe,
&mut img_src,
crate::types::PixelMode::Rgb8,
8,
8,
&mat,
);
assert_eq!(result, ImageResult::Ok);
for y in 0..8u32 {
assert_eq!(bmp.row(y)[0].r, 0, "col 0 should be clipped");
assert_eq!(bmp.row(y)[1].r, 0, "col 1 should be clipped");
assert_eq!(bmp.row(y)[2].r, 255, "col 2 should be painted (y={y})");
assert_eq!(bmp.row(y)[3].r, 255, "col 3 should be painted (y={y})");
assert_eq!(bmp.row(y)[5].r, 0, "col 5 should be clipped");
}
}
fn fnv1a64(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &b in bytes {
h ^= u64::from(b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
struct GoldenMask;
impl MaskSource for GoldenMask {
fn get_row(&mut self, y: u32, row_buf: &mut [u8]) {
for (byte_idx, slot) in row_buf.iter_mut().enumerate() {
let mut packed: u8 = 0;
#[expect(
clippy::cast_possible_truncation,
reason = "callers pass widths ≤ 11 px; byte_idx ≤ 1 fits in u32"
)]
let byte_idx_u32 = byte_idx as u32;
for bit in 0..8u32 {
let x = byte_idx_u32 * 8 + bit;
let on = (x.wrapping_mul(7).wrapping_add(y.wrapping_mul(13))) % 5 < 3;
if on {
packed |= 1 << (7 - bit);
}
}
*slot = packed;
}
}
}
struct GoldenImage {
ncomps: usize,
}
impl ImageSource for GoldenImage {
fn get_row(&mut self, y: u32, row_buf: &mut [u8]) {
let width = row_buf.len() / self.ncomps;
#[expect(
clippy::cast_possible_truncation,
reason = "callers bound width ≤ 11 and ncomps ≤ 4; 0xFF mask is intentional"
)]
for x in 0..width {
for c in 0..self.ncomps {
let mul = 17u32 + c as u32;
let add = 23u32 + c as u32;
let v = (x as u32)
.wrapping_mul(mul)
.wrapping_add(y.wrapping_mul(add));
row_buf[x * self.ncomps + c] = (v & 0xFF) as u8;
}
}
}
}
#[test]
fn scale_mask_ydown_xdown_golden() {
let out = scale_mask(&mut GoldenMask, 11, 13, 5, 7);
assert_eq!(out.len(), 5 * 7);
assert_eq!(fnv1a64(&out), 0xC72C_2A67_D157_65F4);
}
#[test]
fn scale_mask_ydown_xup_golden() {
let out = scale_mask(&mut GoldenMask, 11, 13, 23, 7);
assert_eq!(out.len(), 23 * 7);
assert_eq!(fnv1a64(&out), 0x3C80_F065_9EB6_35B6);
}
#[test]
fn scale_mask_yup_xdown_golden() {
let out = scale_mask(&mut GoldenMask, 11, 13, 5, 29);
assert_eq!(out.len(), 5 * 29);
assert_eq!(fnv1a64(&out), 0x8056_EED7_DF0E_665E);
}
#[test]
fn scale_mask_yup_xup_golden() {
let out = scale_mask(&mut GoldenMask, 5, 7, 23, 29);
assert_eq!(out.len(), 23 * 29);
assert_eq!(fnv1a64(&out), 0x5940_5245_567D_707F);
}
#[test]
fn scale_image_ydown_xdown_golden() {
let mut src = GoldenImage { ncomps: 3 };
let out = scale_image(&mut src, 11, 13, 5, 7, 3);
assert_eq!(out.len(), 5 * 7 * 3);
assert_eq!(fnv1a64(&out), 0x6CDB_D839_4499_6365);
}
#[test]
fn scale_image_ydown_xup_golden() {
let mut src = GoldenImage { ncomps: 3 };
let out = scale_image(&mut src, 11, 13, 23, 7, 3);
assert_eq!(out.len(), 23 * 7 * 3);
assert_eq!(fnv1a64(&out), 0xA8DF_24A4_C3D9_F281);
}
#[test]
fn scale_image_yup_xdown_golden() {
let mut src = GoldenImage { ncomps: 3 };
let out = scale_image(&mut src, 11, 13, 5, 29, 3);
assert_eq!(out.len(), 5 * 29 * 3);
assert_eq!(fnv1a64(&out), 0xBB21_A5B6_484F_2159);
}
#[test]
fn scale_image_yup_xup_golden() {
let mut src = GoldenImage { ncomps: 4 };
let out = scale_image(&mut src, 5, 7, 23, 29, 4);
assert_eq!(out.len(), 23 * 29 * 4);
assert_eq!(fnv1a64(&out), 0xA063_5327_4D9F_A0C1);
}
}