1use crate::bitmap::Bitmap;
39use crate::clip::{Clip, ClipResult};
40use crate::pipe::{self, PipeSrc, PipeState};
41use crate::types::PixelMode;
42use color::Pixel;
43use color::convert::splash_floor;
44
45const MAX_NCOMPS: usize = 8;
50
51pub trait ImageSource: Send {
57 fn get_row(&mut self, y: u32, row_buf: &mut [u8]);
61}
62
63pub trait MaskSource: Send {
74 fn get_row(&mut self, y: u32, row_buf: &mut [u8]);
79}
80
81#[derive(Copy, Clone, Debug, PartialEq, Eq)]
85pub enum ImageResult {
86 Ok,
88 ZeroImage,
90 SingularMatrix,
92 ArbitraryTransformSkipped,
95}
96
97#[inline]
101fn coord_lower(x: f64) -> i32 {
102 splash_floor(x)
103}
104
105#[inline]
107fn coord_upper(x: f64) -> i32 {
108 splash_floor(x) + 1
109}
110
111#[inline]
115fn check_det(a: f64, b: f64, c: f64, d: f64, eps: f64) -> bool {
116 #[expect(
117 clippy::suboptimal_flops,
118 reason = "matches the C++ arithmetic exactly; no numerics benefit here"
119 )]
120 {
121 (a * d - b * c).abs() >= eps
122 }
123}
124
125fn unpack_mask_row(packed: &[u8], width: usize, out: &mut [u8]) {
134 debug_assert!(
135 packed.len() >= width.div_ceil(8),
136 "unpack_mask_row: packed buffer too short ({} < {})",
137 packed.len(),
138 width.div_ceil(8),
139 );
140 debug_assert_eq!(
141 out.len(),
142 width,
143 "unpack_mask_row: out length must equal width"
144 );
145 for (i, slot) in out.iter_mut().enumerate() {
146 let byte = packed.get(i / 8).copied().unwrap_or(0);
148 let bit = (byte >> (7 - (i % 8))) & 1;
149 *slot = if bit != 0 { 255 } else { 0 };
150 }
151}
152
153struct ImageBounds {
157 x0: i32,
158 y0: i32,
159 x1: i32,
161 y1: i32,
163 vflip: bool,
165}
166
167fn compute_axis_aligned_bounds(matrix: &[f64; 6]) -> Result<ImageBounds, ImageResult> {
172 if !check_det(matrix[0], matrix[1], matrix[2], matrix[3], 1e-6) {
173 return Err(ImageResult::SingularMatrix);
174 }
175 let minor_zero = matrix[1] == 0.0 && matrix[2] == 0.0;
176 if !minor_zero || matrix[0] <= 0.0 {
177 return Err(ImageResult::ArbitraryTransformSkipped);
178 }
179
180 let (y0, y1, vflip) = if matrix[3] > 0.0 {
181 (
182 coord_lower(matrix[5]),
183 coord_upper(matrix[3] + matrix[5]),
184 false,
185 )
186 } else if matrix[3] < 0.0 {
187 (
188 coord_lower(matrix[3] + matrix[5]),
189 coord_upper(matrix[5]),
190 true,
191 )
192 } else {
193 return Err(ImageResult::SingularMatrix);
195 };
196
197 let x0 = coord_lower(matrix[4]);
198 let x1 = coord_upper(matrix[0] + matrix[4]);
199
200 let x1 = if x0 == x1 { x1 + 1 } else { x1 };
202 let y1 = if y0 == y1 { y1 + 1 } else { y1 };
203
204 Ok(ImageBounds {
205 x0,
206 y0,
207 x1,
208 y1,
209 vflip,
210 })
211}
212
213fn vflip_rows(data: &mut [u8], row_stride: usize) {
219 if row_stride == 0 {
220 return;
221 }
222 let nrows = data.len() / row_stride;
223 let mut lo = 0usize;
224 let mut hi = nrows.saturating_sub(1);
225 while lo < hi {
226 let (lower, upper) = data.split_at_mut(hi * row_stride);
228 lower[lo * row_stride..lo * row_stride + row_stride]
229 .swap_with_slice(&mut upper[..row_stride]);
230 lo += 1;
231 hi -= 1;
232 }
233}
234
235#[inline]
245const fn bresenham_step(acc: &mut usize, q: usize, scaled: usize, p: usize) -> usize {
246 *acc += q;
247 if *acc >= scaled {
248 *acc -= scaled;
249 p + 1
250 } else {
251 p
252 }
253}
254
255const MASK_SAT_FACTOR: u32 = 255u32 << 23;
263
264const IMAGE_SAT_FACTOR: u32 = 1u32 << 23;
267
268struct MaskAsImage<'a> {
275 mask: &'a mut dyn MaskSource,
276 packed_buf: Vec<u8>,
279 src_w: usize,
280}
281
282impl<'a> MaskAsImage<'a> {
283 fn new(mask: &'a mut dyn MaskSource, src_w: usize) -> Self {
284 Self {
285 mask,
286 packed_buf: vec![0u8; src_w.div_ceil(8)],
287 src_w,
288 }
289 }
290}
291
292impl ImageSource for MaskAsImage<'_> {
293 fn get_row(&mut self, y: u32, row_buf: &mut [u8]) {
294 self.mask.get_row(y, &mut self.packed_buf);
295 unpack_mask_row(&self.packed_buf, self.src_w, row_buf);
296 }
297}
298
299fn scale_mask(
306 mask_src: &mut dyn MaskSource,
307 src_w: usize,
308 src_h: usize,
309 scaled_w: usize,
310 scaled_h: usize,
311) -> Vec<u8> {
312 let mut adapter = MaskAsImage::new(mask_src, src_w);
313 scale_image_inner(
314 &mut adapter,
315 src_w,
316 src_h,
317 scaled_w,
318 scaled_h,
319 1,
320 MASK_SAT_FACTOR,
321 )
322}
323
324#[inline]
330fn saturate_scaled(sum: u32, d: u32) -> u8 {
331 let scaled = ((u64::from(sum) * u64::from(d)) >> 23).min(255);
332 u8::try_from(scaled).expect("scaled box-filter pixel was just clamped to <= 255")
333}
334
335#[inline]
345fn xdown_divisors(sat_factor: u32, y_step: usize, xp: usize) -> (u32, u32) {
346 let d_full = if xp > 0 {
347 let denom = u32::try_from(y_step.saturating_mul(xp))
348 .expect("y_step * xp fits in u32 for practical image sizes");
349 sat_factor / denom
350 } else {
351 0
352 };
353 let denom_plus = u32::try_from(y_step.saturating_mul(xp + 1))
354 .expect("y_step * (xp+1) fits in u32 for practical image sizes");
355 let d_plus_one = sat_factor / denom_plus;
356 (d_full, d_plus_one)
357}
358
359fn scale_image_inner(
366 image_src: &mut dyn ImageSource,
367 src_w: usize,
368 src_h: usize,
369 scaled_w: usize,
370 scaled_h: usize,
371 ncomps: usize,
372 sat_factor: u32,
373) -> Vec<u8> {
374 let mut dest = vec![0u8; scaled_w * scaled_h * ncomps];
375 let mut line_buf = vec![0u8; src_w * ncomps];
376
377 if scaled_h < src_h {
378 if scaled_w < src_w {
379 scale_kernel_ydown_xdown(
380 image_src,
381 src_w,
382 src_h,
383 scaled_w,
384 scaled_h,
385 ncomps,
386 sat_factor,
387 &mut dest,
388 &mut line_buf,
389 );
390 } else {
391 scale_kernel_ydown_xup(
392 image_src,
393 src_w,
394 src_h,
395 scaled_w,
396 scaled_h,
397 ncomps,
398 sat_factor,
399 &mut dest,
400 &mut line_buf,
401 );
402 }
403 } else if scaled_w < src_w {
404 scale_kernel_yup_xdown(
405 image_src,
406 src_w,
407 src_h,
408 scaled_w,
409 scaled_h,
410 ncomps,
411 sat_factor,
412 &mut dest,
413 &mut line_buf,
414 );
415 } else {
416 scale_kernel_yup_xup(
417 image_src,
418 src_w,
419 src_h,
420 scaled_w,
421 scaled_h,
422 ncomps,
423 &mut dest,
424 &mut line_buf,
425 );
426 }
427
428 dest
429}
430
431#[expect(
436 clippy::too_many_arguments,
437 reason = "kernel is private; all params are necessary to share the body across mask + image"
438)]
439fn scale_kernel_ydown_xdown(
440 image_src: &mut dyn ImageSource,
441 src_w: usize,
442 src_h: usize,
443 scaled_w: usize,
444 scaled_h: usize,
445 ncomps: usize,
446 sat_factor: u32,
447 dest: &mut [u8],
448 line_buf: &mut [u8],
449) {
450 let yp = src_h / scaled_h;
451 let yq = src_h % scaled_h;
452 let xp = src_w / scaled_w;
453 let xq = src_w % scaled_w;
454
455 let mut pix_buf = vec![0u32; src_w * ncomps];
456 let mut yt = 0usize;
457 let mut dest_off = 0usize;
458 let mut src_y = 0u32;
459
460 for _dy in 0..scaled_h {
461 let y_step = bresenham_step(&mut yt, yq, scaled_h, yp);
462
463 pix_buf.fill(0);
464 for _ in 0..y_step {
465 image_src.get_row(src_y, line_buf);
466 src_y += 1;
467 for (pix, &lb) in pix_buf.iter_mut().zip(line_buf.iter()) {
468 *pix += u32::from(lb);
469 }
470 }
471
472 let (d_full, d_plus_one) = xdown_divisors(sat_factor, y_step, xp);
473
474 let mut xt = 0usize;
475 let mut xx = 0usize;
476 for _dx in 0..scaled_w {
477 let x_step = bresenham_step(&mut xt, xq, scaled_w, xp);
478 let d = if x_step == xp + 1 { d_plus_one } else { d_full };
479 for c in 0..ncomps {
480 let sum: u32 = (0..x_step).map(|i| pix_buf[(xx + i) * ncomps + c]).sum();
481 dest[dest_off + c] = saturate_scaled(sum, d);
482 }
483 xx += x_step;
484 dest_off += ncomps;
485 }
486 }
487}
488
489#[expect(
493 clippy::too_many_arguments,
494 reason = "kernel is private; all params are necessary to share the body across mask + image"
495)]
496fn scale_kernel_ydown_xup(
497 image_src: &mut dyn ImageSource,
498 src_w: usize,
499 src_h: usize,
500 scaled_w: usize,
501 scaled_h: usize,
502 ncomps: usize,
503 sat_factor: u32,
504 dest: &mut [u8],
505 line_buf: &mut [u8],
506) {
507 let yp = src_h / scaled_h;
508 let yq = src_h % scaled_h;
509 let xp = scaled_w / src_w;
510 let xq = scaled_w % src_w;
511
512 let mut pix_buf = vec![0u32; src_w * ncomps];
513 let mut yt = 0usize;
514 let mut dest_off = 0usize;
515 let mut src_y = 0u32;
516
517 for _dy in 0..scaled_h {
518 let y_step = bresenham_step(&mut yt, yq, scaled_h, yp);
519
520 pix_buf.fill(0);
521 for _ in 0..y_step {
522 image_src.get_row(src_y, line_buf);
523 src_y += 1;
524 for (pix, &lb) in pix_buf.iter_mut().zip(line_buf.iter()) {
525 *pix += u32::from(lb);
526 }
527 }
528
529 let d = sat_factor
530 / u32::try_from(y_step).expect("y_step ≤ src_h fits in u32 for practical image sizes");
531 let mut xt = 0usize;
532
533 let mut pix_vals = [0u8; MAX_NCOMPS];
534 for sx in 0..src_w {
535 let x_step = bresenham_step(&mut xt, xq, src_w, xp);
536 let base = sx * ncomps;
537 for c in 0..ncomps {
538 pix_vals[c] = saturate_scaled(pix_buf[base + c], d);
539 }
540 for _ in 0..x_step {
541 dest[dest_off..dest_off + ncomps].copy_from_slice(&pix_vals[..ncomps]);
542 dest_off += ncomps;
543 }
544 }
545 }
546}
547
548#[expect(
552 clippy::too_many_arguments,
553 reason = "kernel is private; all params are necessary to share the body across mask + image"
554)]
555fn scale_kernel_yup_xdown(
556 image_src: &mut dyn ImageSource,
557 src_w: usize,
558 src_h: usize,
559 scaled_w: usize,
560 scaled_h: usize,
561 ncomps: usize,
562 sat_factor: u32,
563 dest: &mut [u8],
564 line_buf: &mut [u8],
565) {
566 let yp = scaled_h / src_h;
567 let yq = scaled_h % src_h;
568 let xp = src_w / scaled_w;
569 let xq = src_w % scaled_w;
570
571 let (d_full, d_plus_one) = xdown_divisors(sat_factor, 1, xp);
573
574 let mut yt = 0usize;
575 let mut dest_off = 0usize;
576
577 for sy in 0..src_h {
578 let y_step = bresenham_step(&mut yt, yq, src_h, yp);
579
580 let src_y = u32::try_from(sy)
581 .expect("source row index ≤ src_h fits in u32 for practical image sizes");
582 image_src.get_row(src_y, line_buf);
583
584 let row_start = dest_off;
585 let mut xt = 0usize;
586 let mut xx = 0usize;
587
588 for dx in 0..scaled_w {
589 let x_step = bresenham_step(&mut xt, xq, scaled_w, xp);
590 let d = if x_step == xp + 1 { d_plus_one } else { d_full };
591 for c in 0..ncomps {
592 let sum: u32 = (0..x_step)
593 .map(|i| u32::from(line_buf[(xx + i) * ncomps + c]))
594 .sum();
595 dest[row_start + dx * ncomps + c] = saturate_scaled(sum, d);
596 }
597 xx += x_step;
598 }
599 dest_off += scaled_w * ncomps;
600
601 for i in 1..y_step {
602 dest.copy_within(
603 row_start..row_start + scaled_w * ncomps,
604 row_start + i * scaled_w * ncomps,
605 );
606 }
607 dest_off += (y_step - 1) * scaled_w * ncomps;
608 }
609}
610
611#[expect(
618 clippy::too_many_arguments,
619 reason = "kernel is private; all params are necessary to share the body across mask + image"
620)]
621fn scale_kernel_yup_xup(
622 image_src: &mut dyn ImageSource,
623 src_w: usize,
624 src_h: usize,
625 scaled_w: usize,
626 scaled_h: usize,
627 ncomps: usize,
628 dest: &mut [u8],
629 line_buf: &mut [u8],
630) {
631 let yp = scaled_h / src_h;
632 let yq = scaled_h % src_h;
633 let xp = scaled_w / src_w;
634 let xq = scaled_w % src_w;
635
636 let mut yt = 0usize;
637 let mut dest_off = 0usize;
638
639 for sy in 0..src_h {
640 let y_step = bresenham_step(&mut yt, yq, src_h, yp);
641
642 let src_y = u32::try_from(sy)
643 .expect("source row index ≤ src_h fits in u32 for practical image sizes");
644 image_src.get_row(src_y, line_buf);
645
646 let row_start = dest_off;
647 let mut xt = 0usize;
648 let mut xx = 0usize;
649
650 for sx in 0..src_w {
651 let x_step = bresenham_step(&mut xt, xq, src_w, xp);
652 let pix_start = sx * ncomps;
653 for j in 0..x_step {
654 let off = row_start + (xx + j) * ncomps;
655 dest[off..off + ncomps].copy_from_slice(&line_buf[pix_start..pix_start + ncomps]);
656 }
657 xx += x_step;
658 }
659 dest_off += scaled_w * ncomps;
660
661 for i in 1..y_step {
662 dest.copy_within(
663 row_start..row_start + scaled_w * ncomps,
664 row_start + i * scaled_w * ncomps,
665 );
666 }
667 dest_off += (y_step - 1) * scaled_w * ncomps;
668 }
669}
670
671fn scale_image(
678 image_src: &mut dyn ImageSource,
679 src_w: usize,
680 src_h: usize,
681 scaled_w: usize,
682 scaled_h: usize,
683 ncomps: usize,
684) -> Vec<u8> {
685 scale_image_inner(
686 image_src,
687 src_w,
688 src_h,
689 scaled_w,
690 scaled_h,
691 ncomps,
692 IMAGE_SAT_FACTOR,
693 )
694}
695
696struct ImageRowPattern<'a> {
706 data: &'a [u8],
708}
709
710impl crate::pipe::Pattern for ImageRowPattern<'_> {
711 fn fill_span(&self, _y: i32, _x0: i32, _x1: i32, out: &mut [u8]) {
712 assert_eq!(
713 out.len(),
714 self.data.len(),
715 "ImageRowPattern::fill_span: out.len()={} != data.len()={} \
716 (ncomps/P::BYTES mismatch — check draw_image caller)",
717 out.len(),
718 self.data.len(),
719 );
720 out.copy_from_slice(self.data);
721 }
722
723 fn is_static_color(&self) -> bool {
724 false
725 }
726}
727
728#[expect(
736 clippy::too_many_arguments,
737 reason = "mirrors Splash::blitMask API; all params necessary"
738)]
739#[expect(
740 clippy::cast_possible_truncation,
741 clippy::cast_possible_wrap,
742 reason = "run_shape.len() / scaled_w / scaled_h ≤ bitmap dims ≤ i32::MAX for practical image sizes"
743)]
744fn blit_mask<P: Pixel>(
745 bitmap: &mut Bitmap<P>,
746 clip: &Clip,
747 pipe: &PipeState<'_>,
748 src: &PipeSrc<'_>,
749 scaled_mask: &[u8],
750 scaled_w: i32,
751 scaled_h: i32,
752 x_dest: i32,
753 y_dest: i32,
754 clip_all_inside: bool,
755) {
756 #[expect(
757 clippy::cast_possible_wrap,
758 reason = "bitmap dims ≤ i32::MAX in practice"
759 )]
760 let bmp_w = bitmap.width as i32;
761 #[expect(
762 clippy::cast_possible_wrap,
763 reason = "bitmap dims ≤ i32::MAX in practice"
764 )]
765 let bmp_h = bitmap.height as i32;
766
767 for dy in 0..scaled_h {
768 let y = y_dest + dy;
769 if y < 0 || y >= bmp_h {
770 continue;
771 }
772 #[expect(clippy::cast_sign_loss, reason = "dy ≥ 0")]
773 let row_off = dy as usize * scaled_w as usize;
774 #[expect(clippy::cast_sign_loss, reason = "y ≥ 0 by guard above")]
775 let y_u = y as u32;
776
777 let mut run_start: Option<i32> = None;
778 let mut run_shape: Vec<u8> = Vec::new();
779
780 macro_rules! flush_run {
781 () => {
782 if let Some(rs) = run_start.take() {
783 let rx1 = rs + run_shape.len() as i32 - 1;
784 #[expect(clippy::cast_sign_loss, reason = "rs ≥ 0")]
785 let byte_off = rs as usize * P::BYTES;
786 #[expect(clippy::cast_sign_loss, reason = "rx1 ≥ rs ≥ 0")]
787 let byte_end = (rx1 as usize + 1) * P::BYTES;
788 #[expect(clippy::cast_sign_loss, reason = "rs ≥ 0")]
789 let alpha_lo = rs as usize;
790 #[expect(clippy::cast_sign_loss, reason = "rx1 ≥ rs ≥ 0")]
791 let alpha_hi = rx1 as usize;
792 let (row, alpha) = bitmap.row_and_alpha_mut(y_u);
793 let dst_pixels = &mut row[byte_off..byte_end];
794 let dst_alpha = alpha.map(|a| &mut a[alpha_lo..=alpha_hi]);
795 pipe::render_span::<P>(
796 pipe,
797 src,
798 dst_pixels,
799 dst_alpha,
800 Some(&run_shape),
801 rs,
802 rx1,
803 y,
804 );
805 run_shape.clear();
806 }
807 };
808 }
809
810 for dx in 0..scaled_w {
811 let x = x_dest + dx;
812 if x < 0 || x >= bmp_w {
813 flush_run!();
814 continue;
815 }
816 #[expect(clippy::cast_sign_loss, reason = "dx ≥ 0")]
817 let coverage = scaled_mask[row_off + dx as usize];
818 let inside_clip = clip_all_inside || clip.test(x, y);
819
820 if coverage > 0 && inside_clip {
821 if run_start.is_none() {
822 run_start = Some(x);
823 }
824 run_shape.push(coverage);
825 } else {
826 flush_run!();
827 }
828 }
829 flush_run!();
830 }
831}
832
833#[expect(
841 clippy::too_many_arguments,
842 reason = "all context is necessary for a span emit"
843)]
844fn emit_image_span<P: Pixel>(
845 bitmap: &mut Bitmap<P>,
846 pipe: &PipeState<'_>,
847 img_row: &[u8],
848 ncomps: usize,
849 x_src_off: usize,
850 x0: i32,
851 x1: i32,
852 y: i32,
853) {
854 #[expect(clippy::cast_sign_loss, reason = "x1 ≥ x0 ≥ 0 by caller invariant")]
855 let count = (x1 - x0 + 1) as usize;
856 let data = &img_row[x_src_off * ncomps..(x_src_off + count) * ncomps];
857 let row_src = PipeSrc::Pattern(&ImageRowPattern { data });
858 #[expect(clippy::cast_sign_loss, reason = "x0 ≥ 0 by caller invariant")]
859 let byte_off = x0 as usize * P::BYTES;
860 #[expect(clippy::cast_sign_loss, reason = "x1 ≥ x0 ≥ 0 by caller invariant")]
861 let byte_end = (x1 as usize + 1) * P::BYTES;
862 #[expect(clippy::cast_sign_loss, reason = "x0 ≥ 0 by caller invariant")]
863 let (row, alpha) = bitmap.row_and_alpha_mut(y as u32);
864 let dst_pixels = &mut row[byte_off..byte_end];
865 #[expect(clippy::cast_sign_loss, reason = "x0/x1 ≥ 0 by caller invariant")]
866 let dst_alpha = alpha.map(|a| &mut a[x0 as usize..=x1 as usize]);
867 pipe::render_span::<P>(pipe, &row_src, dst_pixels, dst_alpha, None, x0, x1, y);
868}
869
870#[expect(
880 clippy::too_many_arguments,
881 reason = "mirrors Splash::blitImage API; all params necessary"
882)]
883fn blit_image<P: Pixel>(
884 bitmap: &mut Bitmap<P>,
885 clip: &Clip,
886 pipe: &PipeState<'_>,
887 scaled_img: &[u8],
888 scaled_w: i32,
889 scaled_h: i32,
890 x_dest: i32,
891 y_dest: i32,
892 clip_res: ClipResult,
893) {
894 let ncomps = P::BYTES;
895 debug_assert!(
896 ncomps <= MAX_NCOMPS,
897 "blit_image: P::BYTES={ncomps} exceeds MAX_NCOMPS={MAX_NCOMPS}",
898 );
899
900 #[expect(
901 clippy::cast_possible_wrap,
902 reason = "bitmap dims ≤ i32::MAX in practice"
903 )]
904 let bmp_w = bitmap.width as i32;
905 #[expect(
906 clippy::cast_possible_wrap,
907 reason = "bitmap dims ≤ i32::MAX in practice"
908 )]
909 let bmp_h = bitmap.height as i32;
910
911 let clip_all_inside = clip_res == ClipResult::AllInside;
912
913 for dy in 0..scaled_h {
914 let y = y_dest + dy;
915 if y < 0 || y >= bmp_h {
916 continue;
917 }
918 #[expect(clippy::cast_sign_loss, reason = "dy ≥ 0 and scaled_w ≥ 0")]
919 let img_row_off = dy as usize * scaled_w as usize * ncomps;
920 #[expect(
921 clippy::cast_sign_loss,
922 reason = "scaled_w ≥ 0 (it is the dest rect width)"
923 )]
924 let img_row = &scaled_img[img_row_off..img_row_off + scaled_w as usize * ncomps];
925
926 let x_lo = x_dest.max(0);
927 let x_hi = (x_dest + scaled_w - 1).min(bmp_w - 1);
928 if x_lo > x_hi {
929 continue;
930 }
931
932 if clip_all_inside {
933 #[expect(clippy::cast_sign_loss, reason = "x_lo ≥ x_dest ≥ 0 after clamp")]
934 let x_src_off = (x_lo - x_dest) as usize;
935 emit_image_span::<P>(bitmap, pipe, img_row, ncomps, x_src_off, x_lo, x_hi, y);
936 } else {
937 let mut run_x0: Option<i32> = None;
940 let mut run_x1 = x_lo; for dx in 0..scaled_w {
943 let x = x_dest + dx;
944 let in_bmp = x >= x_lo && x <= x_hi;
945 let visible = in_bmp && clip.test(x, y);
946
947 if visible {
948 if run_x0.is_none() {
949 run_x0 = Some(x);
950 }
951 run_x1 = x;
952 } else if let Some(x0) = run_x0.take() {
953 #[expect(clippy::cast_sign_loss, reason = "x0 ≥ x_dest ≥ 0 inside bmp bounds")]
955 let x_src_off = (x0 - x_dest) as usize;
956 emit_image_span::<P>(bitmap, pipe, img_row, ncomps, x_src_off, x0, run_x1, y);
957 }
958 }
959 if let Some(x0) = run_x0 {
961 #[expect(clippy::cast_sign_loss, reason = "x0 ≥ x_dest ≥ 0 inside bmp bounds")]
962 let x_src_off = (x0 - x_dest) as usize;
963 emit_image_span::<P>(bitmap, pipe, img_row, ncomps, x_src_off, x0, run_x1, y);
964 }
965 }
966 }
967}
968
969#[expect(
987 clippy::too_many_arguments,
988 reason = "mirrors Splash::fillImageMask API; all params necessary"
989)]
990pub fn fill_image_mask<P: Pixel>(
991 bitmap: &mut Bitmap<P>,
992 clip: &Clip,
993 pipe: &PipeState<'_>,
994 src: &PipeSrc<'_>,
995 mask_src: &mut dyn MaskSource,
996 src_w: u32,
997 src_h: u32,
998 matrix: &[f64; 6],
999) -> ImageResult {
1000 if src_w == 0 || src_h == 0 {
1001 return ImageResult::ZeroImage;
1002 }
1003
1004 let bounds = match compute_axis_aligned_bounds(matrix) {
1005 Ok(b) => b,
1006 Err(e) => return e,
1007 };
1008 let ImageBounds {
1009 x0,
1010 y0,
1011 x1,
1012 y1,
1013 vflip,
1014 } = bounds;
1015
1016 let clip_res = clip.test_rect(x0, y0, x1 - 1, y1 - 1);
1017 if clip_res == ClipResult::AllOutside {
1018 return ImageResult::Ok;
1019 }
1020
1021 #[expect(
1022 clippy::cast_sign_loss,
1023 reason = "x1 > x0 is guaranteed by compute_axis_aligned_bounds"
1024 )]
1025 let scaled_w = (x1 - x0) as usize;
1026 #[expect(
1027 clippy::cast_sign_loss,
1028 reason = "y1 > y0 is guaranteed by compute_axis_aligned_bounds"
1029 )]
1030 let scaled_h = (y1 - y0) as usize;
1031
1032 let mut scaled = scale_mask(mask_src, src_w as usize, src_h as usize, scaled_w, scaled_h);
1033
1034 if vflip {
1035 vflip_rows(&mut scaled, scaled_w);
1036 }
1037
1038 blit_mask::<P>(
1039 bitmap,
1040 clip,
1041 pipe,
1042 src,
1043 &scaled,
1044 #[expect(
1045 clippy::cast_possible_truncation,
1046 clippy::cast_possible_wrap,
1047 reason = "scaled_w ≤ bitmap.width ≤ i32::MAX"
1048 )]
1049 {
1050 scaled_w as i32
1051 },
1052 #[expect(
1053 clippy::cast_possible_truncation,
1054 clippy::cast_possible_wrap,
1055 reason = "scaled_h ≤ bitmap.height ≤ i32::MAX"
1056 )]
1057 {
1058 scaled_h as i32
1059 },
1060 x0,
1061 y0,
1062 clip_res == ClipResult::AllInside,
1063 );
1064
1065 ImageResult::Ok
1066}
1067
1068#[expect(
1086 clippy::too_many_arguments,
1087 reason = "mirrors Splash::drawImage API; all params necessary"
1088)]
1089pub fn draw_image<P: Pixel>(
1090 bitmap: &mut Bitmap<P>,
1091 clip: &Clip,
1092 pipe: &PipeState<'_>,
1093 image_src: &mut dyn ImageSource,
1094 src_mode: PixelMode,
1095 src_w: u32,
1096 src_h: u32,
1097 matrix: &[f64; 6],
1098) -> ImageResult {
1099 let _ = src_mode;
1101
1102 let ncomps = P::BYTES;
1103 debug_assert!(
1104 ncomps <= MAX_NCOMPS,
1105 "draw_image: P::BYTES={ncomps} exceeds MAX_NCOMPS={MAX_NCOMPS}",
1106 );
1107
1108 if src_w == 0 || src_h == 0 {
1109 return ImageResult::ZeroImage;
1110 }
1111
1112 let bounds = match compute_axis_aligned_bounds(matrix) {
1113 Ok(b) => b,
1114 Err(e) => return e,
1115 };
1116 let ImageBounds {
1117 x0,
1118 y0,
1119 x1,
1120 y1,
1121 vflip,
1122 } = bounds;
1123
1124 let clip_res = clip.test_rect(x0, y0, x1 - 1, y1 - 1);
1125 if clip_res == ClipResult::AllOutside {
1126 return ImageResult::Ok;
1127 }
1128
1129 #[expect(
1130 clippy::cast_sign_loss,
1131 reason = "x1 > x0 is guaranteed by compute_axis_aligned_bounds"
1132 )]
1133 let scaled_w = (x1 - x0) as usize;
1134 #[expect(
1135 clippy::cast_sign_loss,
1136 reason = "y1 > y0 is guaranteed by compute_axis_aligned_bounds"
1137 )]
1138 let scaled_h = (y1 - y0) as usize;
1139
1140 let mut scaled = scale_image(
1141 image_src,
1142 src_w as usize,
1143 src_h as usize,
1144 scaled_w,
1145 scaled_h,
1146 ncomps,
1147 );
1148
1149 if vflip {
1150 vflip_rows(&mut scaled, scaled_w * ncomps);
1151 }
1152
1153 blit_image::<P>(
1154 bitmap,
1155 clip,
1156 pipe,
1157 &scaled,
1158 #[expect(
1159 clippy::cast_possible_truncation,
1160 clippy::cast_possible_wrap,
1161 reason = "scaled_w ≤ bitmap.width ≤ i32::MAX"
1162 )]
1163 {
1164 scaled_w as i32
1165 },
1166 #[expect(
1167 clippy::cast_possible_truncation,
1168 clippy::cast_possible_wrap,
1169 reason = "scaled_h ≤ bitmap.height ≤ i32::MAX"
1170 )]
1171 {
1172 scaled_h as i32
1173 },
1174 x0,
1175 y0,
1176 clip_res,
1177 );
1178
1179 ImageResult::Ok
1180}
1181
1182#[cfg(test)]
1187mod tests {
1188 use super::*;
1189 use crate::bitmap::Bitmap;
1190 use crate::clip::Clip;
1191 use crate::pipe::PipeSrc;
1192 use crate::testutil::{make_clip, simple_pipe};
1193 use color::Rgb8;
1194
1195 struct SolidMask;
1199 impl MaskSource for SolidMask {
1200 fn get_row(&mut self, _y: u32, row_buf: &mut [u8]) {
1201 row_buf.fill(0xFF);
1202 }
1203 }
1204
1205 struct CheckerMask;
1207 impl MaskSource for CheckerMask {
1208 fn get_row(&mut self, _y: u32, row_buf: &mut [u8]) {
1209 for (i, b) in row_buf.iter_mut().enumerate() {
1210 *b = if i % 2 == 0 { 0xAA } else { 0x55 };
1211 }
1212 }
1213 }
1214
1215 struct SolidColor {
1217 r: u8,
1218 g: u8,
1219 b: u8,
1220 }
1221 impl ImageSource for SolidColor {
1222 fn get_row(&mut self, _y: u32, row_buf: &mut [u8]) {
1223 for chunk in row_buf.chunks_exact_mut(3) {
1224 chunk[0] = self.r;
1225 chunk[1] = self.g;
1226 chunk[2] = self.b;
1227 }
1228 }
1229 }
1230
1231 #[test]
1236 fn fill_image_mask_solid_paints_rect() {
1237 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
1238 let clip = make_clip(8, 8);
1239 let pipe = simple_pipe();
1240 let color = [255u8, 0, 0]; let src = PipeSrc::Solid(&color);
1242
1243 let mut mask = SolidMask;
1244 let mat = [4.0f64, 0.0, 0.0, 4.0, 2.0, 2.0];
1246 let result = fill_image_mask::<Rgb8>(&mut bmp, &clip, &pipe, &src, &mut mask, 4, 4, &mat);
1247
1248 assert_eq!(result, ImageResult::Ok);
1249 for y in 2..6u32 {
1250 for x in 2..6usize {
1251 assert_eq!(bmp.row(y)[x].r, 255, "row={y} col={x}");
1252 }
1253 }
1254 assert_eq!(bmp.row(0)[0].r, 0, "outside should be unpainted");
1255 }
1256
1257 #[test]
1259 fn fill_image_mask_vflip_no_crash() {
1260 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
1261 let clip = make_clip(8, 8);
1262 let pipe = simple_pipe();
1263 let color = [0u8, 255, 0];
1264 let src = PipeSrc::Solid(&color);
1265
1266 let mut mask = SolidMask;
1267 let mat = [4.0f64, 0.0, 0.0, -4.0, 2.0, 6.0];
1269 let result = fill_image_mask::<Rgb8>(&mut bmp, &clip, &pipe, &src, &mut mask, 4, 4, &mat);
1270 assert_eq!(result, ImageResult::Ok);
1271 }
1272
1273 #[test]
1275 fn fill_image_mask_singular_matrix() {
1276 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
1277 let clip = make_clip(8, 8);
1278 let pipe = simple_pipe();
1279 let color = [0u8, 0, 0];
1280 let src = PipeSrc::Solid(&color);
1281 let mut mask = SolidMask;
1282
1283 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);
1285 assert_eq!(result, ImageResult::SingularMatrix);
1286 }
1287
1288 #[test]
1290 fn draw_image_solid_color() {
1291 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
1292 let clip = make_clip(8, 8);
1293 let pipe = simple_pipe();
1294
1295 let mut img_src = SolidColor { r: 0, g: 0, b: 200 };
1296 let mat = [4.0f64, 0.0, 0.0, 4.0, 0.0, 0.0];
1297 let result = draw_image::<Rgb8>(
1298 &mut bmp,
1299 &clip,
1300 &pipe,
1301 &mut img_src,
1302 crate::types::PixelMode::Rgb8,
1303 4,
1304 4,
1305 &mat,
1306 );
1307
1308 assert_eq!(result, ImageResult::Ok);
1309 for y in 0..5u32 {
1311 for x in 0..5usize {
1312 assert_eq!(bmp.row(y)[x].b, 200, "row={y} col={x}");
1313 }
1314 }
1315 assert_eq!(bmp.row(6)[0].b, 0, "row 6 should be unpainted");
1317 }
1318
1319 #[test]
1321 fn draw_image_arbitrary_transform_skipped() {
1322 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
1323 let clip = make_clip(8, 8);
1324 let pipe = simple_pipe();
1325 let mut img_src = SolidColor {
1326 r: 100,
1327 g: 100,
1328 b: 100,
1329 };
1330
1331 let mat = [2.0f64, 1.0, 1.0, 2.0, 0.0, 0.0];
1332 let result = draw_image::<Rgb8>(
1333 &mut bmp,
1334 &clip,
1335 &pipe,
1336 &mut img_src,
1337 crate::types::PixelMode::Rgb8,
1338 4,
1339 4,
1340 &mat,
1341 );
1342 assert_eq!(result, ImageResult::ArbitraryTransformSkipped);
1343 }
1344
1345 #[test]
1347 fn draw_image_zero_size() {
1348 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
1349 let clip = make_clip(8, 8);
1350 let pipe = simple_pipe();
1351 let mut img_src = SolidColor { r: 1, g: 2, b: 3 };
1352
1353 let mat = [4.0f64, 0.0, 0.0, 4.0, 0.0, 0.0];
1354 let result = draw_image::<Rgb8>(
1355 &mut bmp,
1356 &clip,
1357 &pipe,
1358 &mut img_src,
1359 crate::types::PixelMode::Rgb8,
1360 0,
1361 4,
1362 &mat,
1363 );
1364 assert_eq!(result, ImageResult::ZeroImage);
1365 }
1366
1367 #[test]
1369 fn draw_image_upsample_2x2_to_4x4() {
1370 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
1371 let clip = make_clip(8, 8);
1372 let pipe = simple_pipe();
1373
1374 let mut img_src = SolidColor {
1375 r: 128,
1376 g: 64,
1377 b: 32,
1378 };
1379 let mat = [4.0f64, 0.0, 0.0, 4.0, 0.0, 0.0];
1380 let result = draw_image::<Rgb8>(
1381 &mut bmp,
1382 &clip,
1383 &pipe,
1384 &mut img_src,
1385 crate::types::PixelMode::Rgb8,
1386 2,
1387 2,
1388 &mat,
1389 );
1390 assert_eq!(result, ImageResult::Ok);
1391 for y in 0..4u32 {
1392 for x in 0..4usize {
1393 assert_eq!(bmp.row(y)[x].r, 128, "row={y} col={x} R");
1394 assert_eq!(bmp.row(y)[x].g, 64, "row={y} col={x} G");
1395 assert_eq!(bmp.row(y)[x].b, 32, "row={y} col={x} B");
1396 }
1397 }
1398 }
1399
1400 #[test]
1402 fn draw_image_downsample_4x4_to_2x2() {
1403 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
1404 let clip = make_clip(8, 8);
1405 let pipe = simple_pipe();
1406
1407 let mut img_src = SolidColor {
1408 r: 200,
1409 g: 100,
1410 b: 50,
1411 };
1412 let mat = [2.0f64, 0.0, 0.0, 2.0, 0.0, 0.0];
1413 let result = draw_image::<Rgb8>(
1414 &mut bmp,
1415 &clip,
1416 &pipe,
1417 &mut img_src,
1418 crate::types::PixelMode::Rgb8,
1419 4,
1420 4,
1421 &mat,
1422 );
1423 assert_eq!(result, ImageResult::Ok);
1424 for y in 0..2u32 {
1425 for x in 0..2usize {
1426 assert_eq!(bmp.row(y)[x].r, 200, "row={y} col={x} R");
1427 }
1428 }
1429 }
1430
1431 #[test]
1433 fn fill_image_mask_checker_partial() {
1434 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
1435 let clip = make_clip(8, 8);
1436 let pipe = simple_pipe();
1437 let color = [255u8, 255, 0]; let src = PipeSrc::Solid(&color);
1439
1440 let mut mask = CheckerMask;
1441 let mat = [8.0f64, 0.0, 0.0, 8.0, 0.0, 0.0];
1442 let result = fill_image_mask::<Rgb8>(&mut bmp, &clip, &pipe, &src, &mut mask, 8, 8, &mat);
1443 assert_eq!(result, ImageResult::Ok);
1444 let any_painted = (0..8u32).any(|y| (0..8usize).any(|x| bmp.row(y)[x].r > 0));
1445 assert!(any_painted, "at least some pixels must be painted");
1446 }
1447
1448 #[test]
1450 fn unpack_mask_row_aa() {
1451 let packed = [0xAAu8];
1452 let mut out = [0u8; 8];
1453 unpack_mask_row(&packed, 8, &mut out);
1454 assert_eq!(out, [255, 0, 255, 0, 255, 0, 255, 0]);
1455 }
1456
1457 #[test]
1459 fn scale_mask_identity_solid() {
1460 struct IdentityMask;
1461 impl MaskSource for IdentityMask {
1462 fn get_row(&mut self, _y: u32, row_buf: &mut [u8]) {
1463 row_buf.fill(0xFF);
1464 }
1465 }
1466 let mut ms = IdentityMask;
1467 let out = scale_mask(&mut ms, 4, 1, 4, 1);
1468 assert_eq!(out, [255u8, 255, 255, 255]);
1469 }
1470
1471 #[test]
1473 fn vflip_rows_three_rows() {
1474 let mut data = vec![
1475 1u8, 2, 3, 4, 5, 6,
1478 ]; vflip_rows(&mut data, 2);
1480 assert_eq!(data, [5, 6, 3, 4, 1, 2]);
1481 }
1482
1483 #[test]
1485 fn vflip_rows_single_row_noop() {
1486 let mut data = vec![7u8, 8, 9];
1487 vflip_rows(&mut data, 3);
1488 assert_eq!(data, [7, 8, 9]);
1489 }
1490
1491 #[test]
1493 fn vflip_rows_empty_noop() {
1494 let mut data: Vec<u8> = vec![];
1495 vflip_rows(&mut data, 1);
1496 assert!(data.is_empty());
1497 }
1498
1499 #[test]
1501 fn draw_image_vflip_reverses_rows() {
1502 struct TwoRowImage;
1504 impl ImageSource for TwoRowImage {
1505 fn get_row(&mut self, y: u32, row_buf: &mut [u8]) {
1506 let (r, g, b) = if y == 0 { (255, 0, 0) } else { (0, 0, 255) };
1507 for chunk in row_buf.chunks_exact_mut(3) {
1508 chunk[0] = r;
1509 chunk[1] = g;
1510 chunk[2] = b;
1511 }
1512 }
1513 }
1514
1515 let mut bmp: Bitmap<Rgb8> = Bitmap::new(4, 4, 1, false);
1516 let clip = make_clip(4, 4);
1517 let pipe = simple_pipe();
1518
1519 let mat = [2.0f64, 0.0, 0.0, -2.0, 0.0, 2.0];
1522 let result = draw_image::<Rgb8>(
1523 &mut bmp,
1524 &clip,
1525 &pipe,
1526 &mut TwoRowImage,
1527 crate::types::PixelMode::Rgb8,
1528 2,
1529 2,
1530 &mat,
1531 );
1532 assert_eq!(result, ImageResult::Ok);
1533 let has_red = (0..3u32).any(|y| bmp.row(y)[0].r == 255 && bmp.row(y)[0].b == 0);
1537 let has_blue = (0..3u32).any(|y| bmp.row(y)[0].b == 255 && bmp.row(y)[0].r == 0);
1538 assert!(has_red, "vflip: expected red pixels in output");
1539 assert!(has_blue, "vflip: expected blue pixels in output");
1540 }
1541
1542 #[test]
1544 fn draw_image_partial_clip_paints_only_inside() {
1545 let mut bmp: Bitmap<Rgb8> = Bitmap::new(8, 8, 1, false);
1546 let clip = Clip::new(2.0, 0.0, 4.999, 7.999, false);
1548 let pipe = simple_pipe();
1549
1550 let mut img_src = SolidColor {
1551 r: 255,
1552 g: 255,
1553 b: 255,
1554 };
1555 let mat = [8.0f64, 0.0, 0.0, 8.0, 0.0, 0.0];
1557 let result = draw_image::<Rgb8>(
1558 &mut bmp,
1559 &clip,
1560 &pipe,
1561 &mut img_src,
1562 crate::types::PixelMode::Rgb8,
1563 8,
1564 8,
1565 &mat,
1566 );
1567 assert_eq!(result, ImageResult::Ok);
1568
1569 for y in 0..8u32 {
1570 assert_eq!(bmp.row(y)[0].r, 0, "col 0 should be clipped");
1572 assert_eq!(bmp.row(y)[1].r, 0, "col 1 should be clipped");
1573 assert_eq!(bmp.row(y)[2].r, 255, "col 2 should be painted (y={y})");
1575 assert_eq!(bmp.row(y)[3].r, 255, "col 3 should be painted (y={y})");
1576 assert_eq!(bmp.row(y)[5].r, 0, "col 5 should be clipped");
1578 }
1579 }
1580
1581 fn fnv1a64(bytes: &[u8]) -> u64 {
1597 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1598 for &b in bytes {
1599 h ^= u64::from(b);
1600 h = h.wrapping_mul(0x0000_0100_0000_01b3);
1601 }
1602 h
1603 }
1604
1605 struct GoldenMask;
1611 impl MaskSource for GoldenMask {
1612 fn get_row(&mut self, y: u32, row_buf: &mut [u8]) {
1613 for (byte_idx, slot) in row_buf.iter_mut().enumerate() {
1614 let mut packed: u8 = 0;
1615 #[expect(
1618 clippy::cast_possible_truncation,
1619 reason = "callers pass widths ≤ 11 px; byte_idx ≤ 1 fits in u32"
1620 )]
1621 let byte_idx_u32 = byte_idx as u32;
1622 for bit in 0..8u32 {
1623 let x = byte_idx_u32 * 8 + bit;
1624 let on = (x.wrapping_mul(7).wrapping_add(y.wrapping_mul(13))) % 5 < 3;
1625 if on {
1626 packed |= 1 << (7 - bit);
1627 }
1628 }
1629 *slot = packed;
1630 }
1631 }
1632 }
1633
1634 struct GoldenImage {
1639 ncomps: usize,
1640 }
1641 impl ImageSource for GoldenImage {
1642 fn get_row(&mut self, y: u32, row_buf: &mut [u8]) {
1643 let width = row_buf.len() / self.ncomps;
1644 #[expect(
1648 clippy::cast_possible_truncation,
1649 reason = "callers bound width ≤ 11 and ncomps ≤ 4; 0xFF mask is intentional"
1650 )]
1651 for x in 0..width {
1652 for c in 0..self.ncomps {
1653 let mul = 17u32 + c as u32;
1654 let add = 23u32 + c as u32;
1655 let v = (x as u32)
1656 .wrapping_mul(mul)
1657 .wrapping_add(y.wrapping_mul(add));
1658 row_buf[x * self.ncomps + c] = (v & 0xFF) as u8;
1659 }
1660 }
1661 }
1662 }
1663
1664 #[test]
1668 fn scale_mask_ydown_xdown_golden() {
1669 let out = scale_mask(&mut GoldenMask, 11, 13, 5, 7);
1670 assert_eq!(out.len(), 5 * 7);
1671 assert_eq!(fnv1a64(&out), 0xC72C_2A67_D157_65F4);
1672 }
1673
1674 #[test]
1676 fn scale_mask_ydown_xup_golden() {
1677 let out = scale_mask(&mut GoldenMask, 11, 13, 23, 7);
1678 assert_eq!(out.len(), 23 * 7);
1679 assert_eq!(fnv1a64(&out), 0x3C80_F065_9EB6_35B6);
1680 }
1681
1682 #[test]
1684 fn scale_mask_yup_xdown_golden() {
1685 let out = scale_mask(&mut GoldenMask, 11, 13, 5, 29);
1686 assert_eq!(out.len(), 5 * 29);
1687 assert_eq!(fnv1a64(&out), 0x8056_EED7_DF0E_665E);
1688 }
1689
1690 #[test]
1692 fn scale_mask_yup_xup_golden() {
1693 let out = scale_mask(&mut GoldenMask, 5, 7, 23, 29);
1694 assert_eq!(out.len(), 23 * 29);
1695 assert_eq!(fnv1a64(&out), 0x5940_5245_567D_707F);
1696 }
1697
1698 #[test]
1700 fn scale_image_ydown_xdown_golden() {
1701 let mut src = GoldenImage { ncomps: 3 };
1702 let out = scale_image(&mut src, 11, 13, 5, 7, 3);
1703 assert_eq!(out.len(), 5 * 7 * 3);
1704 assert_eq!(fnv1a64(&out), 0x6CDB_D839_4499_6365);
1705 }
1706
1707 #[test]
1709 fn scale_image_ydown_xup_golden() {
1710 let mut src = GoldenImage { ncomps: 3 };
1711 let out = scale_image(&mut src, 11, 13, 23, 7, 3);
1712 assert_eq!(out.len(), 23 * 7 * 3);
1713 assert_eq!(fnv1a64(&out), 0xA8DF_24A4_C3D9_F281);
1714 }
1715
1716 #[test]
1718 fn scale_image_yup_xdown_golden() {
1719 let mut src = GoldenImage { ncomps: 3 };
1720 let out = scale_image(&mut src, 11, 13, 5, 29, 3);
1721 assert_eq!(out.len(), 5 * 29 * 3);
1722 assert_eq!(fnv1a64(&out), 0xBB21_A5B6_484F_2159);
1723 }
1724
1725 #[test]
1728 fn scale_image_yup_xup_golden() {
1729 let mut src = GoldenImage { ncomps: 4 };
1730 let out = scale_image(&mut src, 5, 7, 23, 29, 4);
1731 assert_eq!(out.len(), 23 * 29 * 4);
1732 assert_eq!(fnv1a64(&out), 0xA063_5327_4D9F_A0C1);
1733 }
1734}