1use crate::color::ColorType;
26use crate::error::{Error, Result};
27use std::f32::consts::PI;
28
29const MAX_DIMENSION: u32 = 1 << 24; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum ResizeAlgorithm {
35 Nearest,
38 #[default]
41 Bilinear,
42 Lanczos3,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct ResizeOptions {
67 pub src_width: u32,
69 pub src_height: u32,
71 pub dst_width: u32,
73 pub dst_height: u32,
75 pub color_type: ColorType,
77 pub algorithm: ResizeAlgorithm,
79}
80
81impl ResizeOptions {
82 pub fn builder(src_width: u32, src_height: u32) -> ResizeOptionsBuilder {
87 ResizeOptionsBuilder::new(src_width, src_height)
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct ResizeOptionsBuilder {
94 src_width: u32,
95 src_height: u32,
96 dst_width: u32,
97 dst_height: u32,
98 color_type: ColorType,
99 algorithm: ResizeAlgorithm,
100}
101
102impl ResizeOptionsBuilder {
103 pub fn new(src_width: u32, src_height: u32) -> Self {
105 Self {
106 src_width,
107 src_height,
108 dst_width: src_width,
109 dst_height: src_height,
110 color_type: ColorType::Rgba,
111 algorithm: ResizeAlgorithm::default(),
112 }
113 }
114
115 pub fn dst(mut self, width: u32, height: u32) -> Self {
117 self.dst_width = width;
118 self.dst_height = height;
119 self
120 }
121
122 pub fn color_type(mut self, color_type: ColorType) -> Self {
124 self.color_type = color_type;
125 self
126 }
127
128 pub fn algorithm(mut self, algorithm: ResizeAlgorithm) -> Self {
130 self.algorithm = algorithm;
131 self
132 }
133
134 #[must_use]
136 pub fn build(self) -> ResizeOptions {
137 ResizeOptions {
138 src_width: self.src_width,
139 src_height: self.src_height,
140 dst_width: self.dst_width,
141 dst_height: self.dst_height,
142 color_type: self.color_type,
143 algorithm: self.algorithm,
144 }
145 }
146}
147
148#[must_use = "resizing produces pixel data that should be used"]
163pub fn resize(data: &[u8], options: &ResizeOptions) -> Result<Vec<u8>> {
164 let mut output = Vec::new();
165 resize_into(&mut output, data, options)?;
166 Ok(output)
167}
168
169#[must_use = "this `Result` may indicate a resize error"]
180pub fn resize_into(output: &mut Vec<u8>, data: &[u8], options: &ResizeOptions) -> Result<()> {
181 resize_impl(
182 output,
183 data,
184 options.src_width,
185 options.src_height,
186 options.dst_width,
187 options.dst_height,
188 options.color_type,
189 options.algorithm,
190 )
191}
192
193#[allow(clippy::too_many_arguments)]
195fn resize_impl(
196 output: &mut Vec<u8>,
197 data: &[u8],
198 src_width: u32,
199 src_height: u32,
200 dst_width: u32,
201 dst_height: u32,
202 color_type: ColorType,
203 algorithm: ResizeAlgorithm,
204) -> Result<()> {
205 if src_width == 0 || src_height == 0 {
207 return Err(Error::InvalidDimensions {
208 width: src_width,
209 height: src_height,
210 });
211 }
212
213 if dst_width == 0 || dst_height == 0 {
215 return Err(Error::InvalidDimensions {
216 width: dst_width,
217 height: dst_height,
218 });
219 }
220
221 if src_width > MAX_DIMENSION
223 || src_height > MAX_DIMENSION
224 || dst_width > MAX_DIMENSION
225 || dst_height > MAX_DIMENSION
226 {
227 return Err(Error::ImageTooLarge {
228 width: src_width.max(dst_width),
229 height: src_height.max(dst_height),
230 max: MAX_DIMENSION,
231 });
232 }
233
234 let bytes_per_pixel = color_type.bytes_per_pixel();
235
236 let expected_len = (src_width as usize)
238 .checked_mul(src_height as usize)
239 .and_then(|v| v.checked_mul(bytes_per_pixel))
240 .ok_or(Error::InvalidDataLength {
241 expected: usize::MAX,
242 actual: data.len(),
243 })?;
244
245 if data.len() != expected_len {
246 return Err(Error::InvalidDataLength {
247 expected: expected_len,
248 actual: data.len(),
249 });
250 }
251
252 let output_len = (dst_width as usize)
254 .checked_mul(dst_height as usize)
255 .and_then(|v| v.checked_mul(bytes_per_pixel))
256 .ok_or(Error::InvalidDataLength {
257 expected: usize::MAX,
258 actual: 0,
259 })?;
260
261 output.clear();
262 output.resize(output_len, 0);
263
264 match algorithm {
266 ResizeAlgorithm::Nearest => resize_nearest(
267 output,
268 data,
269 src_width as usize,
270 src_height as usize,
271 dst_width as usize,
272 dst_height as usize,
273 bytes_per_pixel,
274 ),
275 ResizeAlgorithm::Bilinear => resize_bilinear(
276 output,
277 data,
278 src_width as usize,
279 src_height as usize,
280 dst_width as usize,
281 dst_height as usize,
282 bytes_per_pixel,
283 ),
284 ResizeAlgorithm::Lanczos3 => resize_lanczos3(
285 output,
286 data,
287 src_width as usize,
288 src_height as usize,
289 dst_width as usize,
290 dst_height as usize,
291 bytes_per_pixel,
292 ),
293 }
294
295 Ok(())
296}
297
298fn resize_nearest(
300 output: &mut [u8],
301 data: &[u8],
302 src_width: usize,
303 src_height: usize,
304 dst_width: usize,
305 dst_height: usize,
306 bytes_per_pixel: usize,
307) {
308 let x_ratio = src_width as f32 / dst_width as f32;
309 let y_ratio = src_height as f32 / dst_height as f32;
310
311 for dst_y in 0..dst_height {
312 let src_y = ((dst_y as f32 + 0.5) * y_ratio - 0.5)
313 .round()
314 .max(0.0)
315 .min((src_height - 1) as f32) as usize;
316
317 for dst_x in 0..dst_width {
318 let src_x = ((dst_x as f32 + 0.5) * x_ratio - 0.5)
319 .round()
320 .max(0.0)
321 .min((src_width - 1) as f32) as usize;
322
323 let src_idx = (src_y * src_width + src_x) * bytes_per_pixel;
324 let dst_idx = (dst_y * dst_width + dst_x) * bytes_per_pixel;
325
326 output[dst_idx..dst_idx + bytes_per_pixel]
327 .copy_from_slice(&data[src_idx..src_idx + bytes_per_pixel]);
328 }
329 }
330}
331
332fn resize_bilinear(
334 output: &mut [u8],
335 data: &[u8],
336 src_width: usize,
337 src_height: usize,
338 dst_width: usize,
339 dst_height: usize,
340 bytes_per_pixel: usize,
341) {
342 let x_ratio = if dst_width > 1 {
343 (src_width - 1) as f32 / (dst_width - 1) as f32
344 } else {
345 0.0
346 };
347 let y_ratio = if dst_height > 1 {
348 (src_height - 1) as f32 / (dst_height - 1) as f32
349 } else {
350 0.0
351 };
352
353 for dst_y in 0..dst_height {
354 let src_y_f = dst_y as f32 * y_ratio;
355 let src_y0 = src_y_f.floor() as usize;
356 let src_y1 = (src_y0 + 1).min(src_height - 1);
357 let y_frac = src_y_f - src_y0 as f32;
358
359 for dst_x in 0..dst_width {
360 let src_x_f = dst_x as f32 * x_ratio;
361 let src_x0 = src_x_f.floor() as usize;
362 let src_x1 = (src_x0 + 1).min(src_width - 1);
363 let x_frac = src_x_f - src_x0 as f32;
364
365 let idx00 = (src_y0 * src_width + src_x0) * bytes_per_pixel;
367 let idx01 = (src_y0 * src_width + src_x1) * bytes_per_pixel;
368 let idx10 = (src_y1 * src_width + src_x0) * bytes_per_pixel;
369 let idx11 = (src_y1 * src_width + src_x1) * bytes_per_pixel;
370
371 let dst_idx = (dst_y * dst_width + dst_x) * bytes_per_pixel;
372
373 for c in 0..bytes_per_pixel {
375 let p00 = data[idx00 + c] as f32;
376 let p01 = data[idx01 + c] as f32;
377 let p10 = data[idx10 + c] as f32;
378 let p11 = data[idx11 + c] as f32;
379
380 let top = p00 * (1.0 - x_frac) + p01 * x_frac;
382 let bottom = p10 * (1.0 - x_frac) + p11 * x_frac;
383 let value = top * (1.0 - y_frac) + bottom * y_frac;
384
385 output[dst_idx + c] = value.round().clamp(0.0, 255.0) as u8;
386 }
387 }
388 }
389}
390
391#[inline]
393fn lanczos_kernel(x: f32, a: f32) -> f32 {
394 if x.abs() < f32::EPSILON {
395 1.0
396 } else if x.abs() >= a {
397 0.0
398 } else {
399 let pi_x = PI * x;
400 let pi_x_a = PI * x / a;
401 (a * pi_x.sin() * pi_x_a.sin()) / (pi_x * pi_x_a)
402 }
403}
404
405#[derive(Clone)]
407struct Contribution {
408 start: usize,
410 weights: Vec<f32>,
412}
413
414fn precompute_contributions(src_size: usize, dst_size: usize) -> Vec<Contribution> {
417 const A: f32 = 3.0;
418
419 let scale = src_size as f32 / dst_size as f32;
420 let filter_scale = scale.max(1.0);
422 let support = A * filter_scale;
423
424 let mut contributions = Vec::with_capacity(dst_size);
425
426 for dst_idx in 0..dst_size {
427 let src_center = (dst_idx as f32 + 0.5) * scale - 0.5;
429
430 let start = ((src_center - support).floor() as isize).max(0) as usize;
432 let end = ((src_center + support).ceil() as usize + 1).min(src_size);
433
434 let mut weights = Vec::with_capacity(end - start);
436 let mut weight_sum = 0.0f32;
437
438 for src_idx in start..end {
439 let x = (src_idx as f32 - src_center) / filter_scale;
440 let w = lanczos_kernel(x, A);
441 weights.push(w);
442 weight_sum += w;
443 }
444
445 if weight_sum.abs() > f32::EPSILON {
447 for w in &mut weights {
448 *w /= weight_sum;
449 }
450 }
451
452 contributions.push(Contribution { start, weights });
453 }
454
455 contributions
456}
457
458#[inline]
460fn resample_row_horizontal(
461 src_row: &[u8],
462 dst_row: &mut [u8],
463 contributions: &[Contribution],
464 bytes_per_pixel: usize,
465) {
466 for (dst_x, contrib) in contributions.iter().enumerate() {
467 let dst_idx = dst_x * bytes_per_pixel;
468
469 let mut channel_sums = [0.0f32; 4];
471
472 for (i, &weight) in contrib.weights.iter().enumerate() {
473 let src_x = contrib.start + i;
474 let src_idx = src_x * bytes_per_pixel;
475 for c in 0..bytes_per_pixel {
476 channel_sums[c] += src_row[src_idx + c] as f32 * weight;
477 }
478 }
479
480 for c in 0..bytes_per_pixel {
481 dst_row[dst_idx + c] = channel_sums[c].round().clamp(0.0, 255.0) as u8;
482 }
483 }
484}
485
486#[inline]
488fn resample_column_vertical(
489 temp: &[u8],
490 dst_row: &mut [u8],
491 contrib: &Contribution,
492 temp_width: usize,
493 bytes_per_pixel: usize,
494) {
495 let row_stride = temp_width * bytes_per_pixel;
496
497 for dst_x in 0..temp_width {
498 let dst_idx = dst_x * bytes_per_pixel;
499 let mut channel_sums = [0.0f32; 4];
500
501 for (i, &weight) in contrib.weights.iter().enumerate() {
502 let src_y = contrib.start + i;
503 let src_idx = src_y * row_stride + dst_x * bytes_per_pixel;
504 for c in 0..bytes_per_pixel {
505 channel_sums[c] += temp[src_idx + c] as f32 * weight;
506 }
507 }
508
509 for c in 0..bytes_per_pixel {
510 dst_row[dst_idx + c] = channel_sums[c].round().clamp(0.0, 255.0) as u8;
511 }
512 }
513}
514
515fn resize_lanczos3(
518 output: &mut [u8],
519 data: &[u8],
520 src_width: usize,
521 src_height: usize,
522 dst_width: usize,
523 dst_height: usize,
524 bytes_per_pixel: usize,
525) {
526 let h_contribs = precompute_contributions(src_width, dst_width);
528 let v_contribs = precompute_contributions(src_height, dst_height);
529
530 let temp_size = src_height * dst_width * bytes_per_pixel;
532 let mut temp = vec![0u8; temp_size];
533
534 #[cfg(feature = "parallel")]
536 {
537 use rayon::prelude::*;
538
539 let src_row_stride = src_width * bytes_per_pixel;
540 let temp_row_stride = dst_width * bytes_per_pixel;
541
542 temp.par_chunks_mut(temp_row_stride)
543 .enumerate()
544 .for_each(|(y, temp_row)| {
545 let src_start = y * src_row_stride;
546 let src_row = &data[src_start..src_start + src_row_stride];
547 resample_row_horizontal(src_row, temp_row, &h_contribs, bytes_per_pixel);
548 });
549 }
550
551 #[cfg(not(feature = "parallel"))]
552 {
553 let src_row_stride = src_width * bytes_per_pixel;
554 let temp_row_stride = dst_width * bytes_per_pixel;
555
556 for y in 0..src_height {
557 let src_start = y * src_row_stride;
558 let src_row = &data[src_start..src_start + src_row_stride];
559 let temp_start = y * temp_row_stride;
560 let temp_row = &mut temp[temp_start..temp_start + temp_row_stride];
561 resample_row_horizontal(src_row, temp_row, &h_contribs, bytes_per_pixel);
562 }
563 }
564
565 #[cfg(feature = "parallel")]
567 {
568 use rayon::prelude::*;
569
570 let dst_row_stride = dst_width * bytes_per_pixel;
571
572 output
573 .par_chunks_mut(dst_row_stride)
574 .enumerate()
575 .for_each(|(dst_y, dst_row)| {
576 resample_column_vertical(
577 &temp,
578 dst_row,
579 &v_contribs[dst_y],
580 dst_width,
581 bytes_per_pixel,
582 );
583 });
584 }
585
586 #[cfg(not(feature = "parallel"))]
587 {
588 let dst_row_stride = dst_width * bytes_per_pixel;
589
590 for dst_y in 0..dst_height {
591 let dst_start = dst_y * dst_row_stride;
592 let dst_row = &mut output[dst_start..dst_start + dst_row_stride];
593 resample_column_vertical(
594 &temp,
595 dst_row,
596 &v_contribs[dst_y],
597 dst_width,
598 bytes_per_pixel,
599 );
600 }
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 fn test_resize(
610 data: &[u8],
611 src_width: u32,
612 src_height: u32,
613 dst_width: u32,
614 dst_height: u32,
615 color_type: ColorType,
616 algorithm: ResizeAlgorithm,
617 ) -> Result<Vec<u8>> {
618 let options = ResizeOptions::builder(src_width, src_height)
619 .dst(dst_width, dst_height)
620 .color_type(color_type)
621 .algorithm(algorithm)
622 .build();
623 resize(data, &options)
624 }
625
626 #[allow(dead_code)]
628 #[allow(clippy::too_many_arguments)]
629 fn test_resize_into(
630 output: &mut Vec<u8>,
631 data: &[u8],
632 src_width: u32,
633 src_height: u32,
634 dst_width: u32,
635 dst_height: u32,
636 color_type: ColorType,
637 algorithm: ResizeAlgorithm,
638 ) -> Result<()> {
639 let options = ResizeOptions::builder(src_width, src_height)
640 .dst(dst_width, dst_height)
641 .color_type(color_type)
642 .algorithm(algorithm)
643 .build();
644 resize_into(output, data, &options)
645 }
646
647 #[test]
648 fn test_resize_nearest_basic() {
649 let pixels = vec![
651 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255, ];
656
657 let result = test_resize(
658 &pixels,
659 2,
660 2,
661 4,
662 4,
663 ColorType::Rgba,
664 ResizeAlgorithm::Nearest,
665 )
666 .unwrap();
667 assert_eq!(result.len(), 4 * 4 * 4);
668 }
669
670 #[test]
671 fn test_resize_bilinear_basic() {
672 let pixels = vec![
674 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0, ];
679
680 let result = test_resize(
681 &pixels,
682 2,
683 2,
684 4,
685 4,
686 ColorType::Rgb,
687 ResizeAlgorithm::Bilinear,
688 )
689 .unwrap();
690 assert_eq!(result.len(), 4 * 4 * 3);
691 }
692
693 #[test]
694 fn test_resize_lanczos3_basic() {
695 let pixels = vec![0u8; 4 * 4];
697
698 let result = test_resize(
699 &pixels,
700 4,
701 4,
702 2,
703 2,
704 ColorType::Gray,
705 ResizeAlgorithm::Lanczos3,
706 )
707 .unwrap();
708 assert_eq!(result.len(), 2 * 2);
709 }
710
711 #[test]
712 fn test_resize_same_size() {
713 let pixels = vec![128u8; 8 * 8 * 4];
715
716 let result = test_resize(
717 &pixels,
718 8,
719 8,
720 8,
721 8,
722 ColorType::Rgba,
723 ResizeAlgorithm::Bilinear,
724 )
725 .unwrap();
726 assert_eq!(result.len(), pixels.len());
727 }
728
729 #[test]
730 fn test_resize_downscale() {
731 let pixels: Vec<u8> = (0..16 * 16 * 3).map(|i| (i % 256) as u8).collect();
733
734 let result = test_resize(
735 &pixels,
736 16,
737 16,
738 4,
739 4,
740 ColorType::Rgb,
741 ResizeAlgorithm::Lanczos3,
742 )
743 .unwrap();
744 assert_eq!(result.len(), 4 * 4 * 3);
745 }
746
747 #[test]
748 fn test_resize_upscale() {
749 let pixels: Vec<u8> = (0..4 * 4 * 3).map(|i| (i % 256) as u8).collect();
751
752 let result = test_resize(
753 &pixels,
754 4,
755 4,
756 16,
757 16,
758 ColorType::Rgb,
759 ResizeAlgorithm::Bilinear,
760 )
761 .unwrap();
762 assert_eq!(result.len(), 16 * 16 * 3);
763 }
764
765 #[test]
766 fn test_resize_non_square() {
767 let pixels = vec![200u8; 8 * 4 * 4];
769
770 let result = test_resize(
771 &pixels,
772 8,
773 4,
774 4,
775 8,
776 ColorType::Rgba,
777 ResizeAlgorithm::Nearest,
778 )
779 .unwrap();
780 assert_eq!(result.len(), 4 * 8 * 4);
781 }
782
783 #[test]
784 fn test_resize_invalid_src_dimensions() {
785 let pixels = vec![0u8; 0];
786 let result = test_resize(
787 &pixels,
788 0,
789 10,
790 5,
791 5,
792 ColorType::Rgb,
793 ResizeAlgorithm::Nearest,
794 );
795 assert!(matches!(result, Err(Error::InvalidDimensions { .. })));
796 }
797
798 #[test]
799 fn test_resize_invalid_dst_dimensions() {
800 let pixels = vec![0u8; 10 * 10 * 3];
801 let result = test_resize(
802 &pixels,
803 10,
804 10,
805 0,
806 5,
807 ColorType::Rgb,
808 ResizeAlgorithm::Nearest,
809 );
810 assert!(matches!(result, Err(Error::InvalidDimensions { .. })));
811 }
812
813 #[test]
814 fn test_resize_invalid_data_length() {
815 let pixels = vec![0u8; 10]; let result = test_resize(
817 &pixels,
818 10,
819 10,
820 5,
821 5,
822 ColorType::Rgb,
823 ResizeAlgorithm::Nearest,
824 );
825 assert!(matches!(result, Err(Error::InvalidDataLength { .. })));
826 }
827
828 #[test]
829 fn test_resize_1x1_to_larger() {
830 let pixels = vec![255, 128, 64, 255]; let result = test_resize(
834 &pixels,
835 1,
836 1,
837 4,
838 4,
839 ColorType::Rgba,
840 ResizeAlgorithm::Bilinear,
841 )
842 .unwrap();
843 assert_eq!(result.len(), 4 * 4 * 4);
844
845 for i in 0..16 {
847 assert_eq!(result[i * 4], 255);
848 assert_eq!(result[i * 4 + 1], 128);
849 assert_eq!(result[i * 4 + 2], 64);
850 assert_eq!(result[i * 4 + 3], 255);
851 }
852 }
853
854 #[test]
855 fn test_resize_to_1x1() {
856 let pixels = vec![128u8; 4 * 4 * 3];
858
859 let result = test_resize(
860 &pixels,
861 4,
862 4,
863 1,
864 1,
865 ColorType::Rgb,
866 ResizeAlgorithm::Lanczos3,
867 )
868 .unwrap();
869 assert_eq!(result.len(), 3);
870 }
871
872 #[test]
873 fn test_resize_gray_alpha() {
874 let pixels = vec![100, 200, 150, 250]; let result = test_resize(
878 &pixels,
879 2,
880 1,
881 4,
882 2,
883 ColorType::GrayAlpha,
884 ResizeAlgorithm::Bilinear,
885 )
886 .unwrap();
887 assert_eq!(result.len(), 4 * 2 * 2);
888 }
889
890 #[test]
891 fn test_resize_buffer_reuse() {
892 let mut output = Vec::with_capacity(1024);
893 let pixels = vec![128u8; 8 * 8 * 4];
894
895 let options1 = ResizeOptions::builder(8, 8)
896 .dst(4, 4)
897 .color_type(ColorType::Rgba)
898 .algorithm(ResizeAlgorithm::Nearest)
899 .build();
900 resize_into(&mut output, &pixels, &options1).unwrap();
901
902 let first_cap = output.capacity();
903 assert_eq!(output.len(), 4 * 4 * 4);
904
905 let options2 = ResizeOptions::builder(8, 8)
907 .dst(4, 4)
908 .color_type(ColorType::Rgba)
909 .algorithm(ResizeAlgorithm::Bilinear)
910 .build();
911 resize_into(&mut output, &pixels, &options2).unwrap();
912
913 assert!(output.capacity() >= first_cap);
915 }
916
917 #[test]
918 fn test_resize_algorithm_default() {
919 assert_eq!(ResizeAlgorithm::default(), ResizeAlgorithm::Bilinear);
921 }
922
923 #[test]
924 fn test_lanczos_kernel() {
925 assert!((lanczos_kernel(0.0, 3.0) - 1.0).abs() < 0.001);
927
928 assert!(lanczos_kernel(3.0, 3.0).abs() < 0.001);
930 assert!(lanczos_kernel(4.0, 3.0).abs() < f32::EPSILON);
931
932 assert!((lanczos_kernel(1.5, 3.0) - lanczos_kernel(-1.5, 3.0)).abs() < 0.001);
934 }
935
936 #[test]
937 fn test_resize_large_dimension_error() {
938 let pixels = vec![0u8; 3];
939 let result = test_resize(
940 &pixels,
941 1,
942 1,
943 (1 << 25) as u32,
944 1,
945 ColorType::Rgb,
946 ResizeAlgorithm::Nearest,
947 );
948 assert!(matches!(result, Err(Error::ImageTooLarge { .. })));
949 }
950
951 #[test]
952 fn test_all_algorithms_produce_valid_output() {
953 let pixels: Vec<u8> = (0..32 * 32 * 4).map(|i| (i % 256) as u8).collect();
954
955 for algo in [
956 ResizeAlgorithm::Nearest,
957 ResizeAlgorithm::Bilinear,
958 ResizeAlgorithm::Lanczos3,
959 ] {
960 let result = test_resize(&pixels, 32, 32, 16, 16, ColorType::Rgba, algo).unwrap();
961 assert_eq!(result.len(), 16 * 16 * 4);
962
963 assert!(!result.is_empty());
965 }
966 }
967
968 #[test]
971 fn test_precompute_contributions_basic() {
972 let contribs = precompute_contributions(100, 50); assert_eq!(contribs.len(), 50);
976
977 for contrib in &contribs {
979 let sum: f32 = contrib.weights.iter().sum();
980 assert!(
981 (sum - 1.0).abs() < 0.01,
982 "Weights should sum to 1.0, got {sum}"
983 );
984 }
985 }
986
987 #[test]
988 fn test_precompute_contributions_upscale() {
989 let contribs = precompute_contributions(50, 100); assert_eq!(contribs.len(), 100);
993
994 for contrib in &contribs {
995 let sum: f32 = contrib.weights.iter().sum();
996 assert!(
997 (sum - 1.0).abs() < 0.01,
998 "Weights should sum to 1.0, got {sum}"
999 );
1000 assert!(
1002 contrib.weights.len() <= 8,
1003 "Kernel too large: {}",
1004 contrib.weights.len()
1005 );
1006 }
1007 }
1008
1009 #[test]
1010 fn test_precompute_contributions_same_size() {
1011 let contribs = precompute_contributions(100, 100);
1013
1014 assert_eq!(contribs.len(), 100);
1015
1016 for (i, contrib) in contribs.iter().enumerate() {
1017 let max_weight = contrib.weights.iter().cloned().fold(0.0f32, f32::max);
1019 assert!(
1020 max_weight > 0.9,
1021 "Max weight at {i} should be near 1.0, got {max_weight}"
1022 );
1023 }
1024 }
1025
1026 #[test]
1027 fn test_lanczos3_large_downscale() {
1028 let src_w = 500;
1030 let src_h = 600;
1031 let dst_w = 100;
1032 let dst_h = 120;
1033
1034 let pixels: Vec<u8> = (0..(src_w * src_h * 4))
1035 .map(|i| ((i * 7) % 256) as u8)
1036 .collect();
1037
1038 let result = test_resize(
1039 &pixels,
1040 src_w as u32,
1041 src_h as u32,
1042 dst_w as u32,
1043 dst_h as u32,
1044 ColorType::Rgba,
1045 ResizeAlgorithm::Lanczos3,
1046 )
1047 .unwrap();
1048
1049 assert_eq!(result.len(), dst_w * dst_h * 4);
1050
1051 let sum: u64 = result.iter().map(|&x| x as u64).sum();
1053 let avg = sum as f64 / result.len() as f64;
1054 assert!(
1055 avg > 10.0 && avg < 245.0,
1056 "Average pixel value {avg} is suspicious"
1057 );
1058 }
1059
1060 #[test]
1061 fn test_lanczos3_preserves_solid_color() {
1062 let color = [100u8, 150, 200, 255];
1064 let pixels: Vec<u8> = (0..64 * 64).flat_map(|_| color).collect();
1065
1066 let result = test_resize(
1067 &pixels,
1068 64,
1069 64,
1070 32,
1071 32,
1072 ColorType::Rgba,
1073 ResizeAlgorithm::Lanczos3,
1074 )
1075 .unwrap();
1076
1077 for i in 0..(32 * 32) {
1079 let idx = i * 4;
1080 for c in 0..4 {
1081 let diff = (result[idx + c] as i32 - color[c] as i32).abs();
1082 assert!(diff <= 1, "Color drift too large at pixel {i}, channel {c}");
1083 }
1084 }
1085 }
1086
1087 #[test]
1088 fn test_lanczos3_upscale_quality() {
1089 let pixels = vec![
1092 0, 0, 0, 255, 255, 255, 255, 255, 128, 128, 128, 255, 64, 64, 64, 255, ];
1097
1098 let result = test_resize(
1099 &pixels,
1100 2,
1101 2,
1102 8,
1103 8,
1104 ColorType::Rgba,
1105 ResizeAlgorithm::Lanczos3,
1106 )
1107 .unwrap();
1108
1109 assert_eq!(result.len(), 8 * 8 * 4);
1110
1111 assert!(result[0] < 30, "Top-left should be near black");
1114 let tr_idx = 7 * 4;
1116 assert!(result[tr_idx] > 200, "Top-right should be near white");
1117 }
1118
1119 #[test]
1120 fn test_lanczos3_asymmetric_resize() {
1121 let pixels: Vec<u8> = (0..100 * 50 * 3).map(|i| (i % 256) as u8).collect();
1123
1124 let result = test_resize(
1125 &pixels,
1126 100,
1127 50,
1128 25,
1129 200, ColorType::Rgb,
1131 ResizeAlgorithm::Lanczos3,
1132 )
1133 .unwrap();
1134
1135 assert_eq!(result.len(), 25 * 200 * 3);
1136 }
1137
1138 #[test]
1139 fn test_resample_row_horizontal_basic() {
1140 let contribs = precompute_contributions(4, 2);
1142 let src_row = vec![100u8, 150, 200, 250]; let mut dst_row = vec![0u8; 2];
1144
1145 resample_row_horizontal(&src_row, &mut dst_row, &contribs, 1);
1146
1147 assert!(dst_row[0] > 50 && dst_row[0] < 200);
1149 assert!(dst_row[1] > 100 && dst_row[1] < 255);
1150 }
1151
1152 #[test]
1153 fn test_lanczos3_1x1_edge_case() {
1154 let pixels = vec![128, 64, 192, 255]; let result = test_resize(
1158 &pixels,
1159 1,
1160 1,
1161 10,
1162 10,
1163 ColorType::Rgba,
1164 ResizeAlgorithm::Lanczos3,
1165 )
1166 .unwrap();
1167
1168 assert_eq!(result.len(), 10 * 10 * 4);
1169
1170 for i in 0..100 {
1172 let idx = i * 4;
1173 assert_eq!(result[idx], 128, "R mismatch at {i}");
1174 assert_eq!(result[idx + 1], 64, "G mismatch at {i}");
1175 assert_eq!(result[idx + 2], 192, "B mismatch at {i}");
1176 assert_eq!(result[idx + 3], 255, "A mismatch at {i}");
1177 }
1178 }
1179
1180 #[test]
1181 fn test_contribution_bounds_are_valid() {
1182 for (src, dst) in [(100, 10), (10, 100), (1000, 50), (50, 1000)] {
1184 let contribs = precompute_contributions(src, dst);
1185
1186 for (i, contrib) in contribs.iter().enumerate() {
1187 assert!(
1188 contrib.start < src,
1189 "Contribution {} start {} >= src {}",
1190 i,
1191 contrib.start,
1192 src
1193 );
1194 let end = contrib.start + contrib.weights.len();
1195 assert!(end <= src, "Contribution {i} end {end} > src {src}");
1196 }
1197 }
1198 }
1199
1200 #[test]
1201 fn test_lanczos3_extreme_downscale() {
1202 let pixels: Vec<u8> = (0..1000 * 1000 * 3).map(|i| (i % 256) as u8).collect();
1204
1205 let result = test_resize(
1206 &pixels,
1207 1000,
1208 1000,
1209 10,
1210 10,
1211 ColorType::Rgb,
1212 ResizeAlgorithm::Lanczos3,
1213 )
1214 .unwrap();
1215
1216 assert_eq!(result.len(), 10 * 10 * 3);
1217 }
1218
1219 #[test]
1220 fn test_lanczos3_prime_dimensions() {
1221 let pixels: Vec<u8> = (0..97 * 89 * 4).map(|i| (i % 256) as u8).collect();
1223
1224 let result = test_resize(
1225 &pixels,
1226 97,
1227 89,
1228 41,
1229 37,
1230 ColorType::Rgba,
1231 ResizeAlgorithm::Lanczos3,
1232 )
1233 .unwrap();
1234
1235 assert_eq!(result.len(), 41 * 37 * 4);
1236 }
1237}