1use std::sync::OnceLock;
23
24use otf_pixels_core::{
25 AccessPattern, ChannelLayout, ImageDescriptor, Op, PixelFormat, PixelsError, Region, Result,
26 SampleKind, Tile, TileBuf, TileMut,
27};
28
29use crate::filter::{Filter, Weights};
30use crate::resample::{column_f32, column_u8, column_u16, row_f32, row_u8, row_u16};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
35#[non_exhaustive]
36pub enum Fit {
37 #[default]
39 Fill,
40 Inside,
43 Outside,
46 Cover,
49 Contain,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56#[non_exhaustive]
57pub struct ResizeOptions {
58 pub filter: Filter,
60 pub fit: Fit,
62 pub without_enlargement: bool,
64 pub background: [u8; 4],
68}
69
70impl Default for ResizeOptions {
71 fn default() -> Self {
72 Self {
73 filter: Filter::default(),
74 fit: Fit::default(),
75 without_enlargement: false,
76 background: [0, 0, 0, 255],
77 }
78 }
79}
80
81impl ResizeOptions {
82 #[must_use]
84 pub const fn with_filter(mut self, filter: Filter) -> Self {
85 self.filter = filter;
86 self
87 }
88
89 #[must_use]
91 pub const fn with_fit(mut self, fit: Fit) -> Self {
92 self.fit = fit;
93 self
94 }
95
96 #[must_use]
98 pub const fn with_background(mut self, rgba: [u8; 4]) -> Self {
99 self.background = rgba;
100 self
101 }
102
103 #[must_use]
105 pub const fn without_enlargement(mut self, refuse: bool) -> Self {
106 self.without_enlargement = refuse;
107 self
108 }
109}
110
111#[derive(Debug)]
119struct Binding {
120 input: ImageDescriptor,
121 horizontal: Weights,
123 vertical: Weights,
124 layout: Layout,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129struct Layout {
130 output: (u32, u32),
132 placed: Region,
135}
136
137#[derive(Debug)]
139pub struct Resize {
140 width: u32,
141 height: u32,
142 options: ResizeOptions,
143 bound: OnceLock<Binding>,
145}
146
147impl Clone for Resize {
148 fn clone(&self) -> Self {
149 Self {
153 width: self.width,
154 height: self.height,
155 options: self.options,
156 bound: OnceLock::new(),
157 }
158 }
159}
160
161impl Resize {
162 pub fn new(width: u32, height: u32, options: ResizeOptions) -> Result<Self> {
168 if width == 0 || height == 0 {
169 return Err(PixelsError::invalid_argument(
170 "size",
171 format!("resize target {width}x{height} has no pixels"),
172 ));
173 }
174 Ok(Self {
175 width,
176 height,
177 options,
178 bound: OnceLock::new(),
179 })
180 }
181
182 pub fn to(width: u32, height: u32) -> Result<Self> {
188 Self::new(width, height, ResizeOptions::default())
189 }
190
191 #[must_use]
193 pub const fn options(&self) -> ResizeOptions {
194 self.options
195 }
196
197 #[must_use]
202 pub fn target(&self, input: &ImageDescriptor) -> (u32, u32) {
203 self.layout(input).1.output
204 }
205
206 fn layout(&self, input: &ImageDescriptor) -> ((u32, u32), Layout, (u32, u32)) {
208 let by_width = f64::from(self.width) / f64::from(input.width);
211 let by_height = f64::from(self.height) / f64::from(input.height);
212 let preserving = |scale: f64| {
213 (
214 ((f64::from(input.width) * scale).round() as u32).max(1),
215 ((f64::from(input.height) * scale).round() as u32).max(1),
216 )
217 };
218 let (mut width, mut height) = match self.options.fit {
219 Fit::Fill => (self.width, self.height),
220 Fit::Inside | Fit::Contain => preserving(by_width.min(by_height)),
221 Fit::Outside | Fit::Cover => preserving(by_width.max(by_height)),
222 };
223 if self.options.without_enlargement {
224 width = width.min(input.width);
225 height = height.min(input.height);
226 }
227 let scaled = (width.max(1), height.max(1));
228 let (sw, sh) = scaled;
229 match self.options.fit {
230 Fit::Cover => {
231 let (ow, oh) = (sw.min(self.width), sh.min(self.height));
232 let window = ((sw - ow) / 2, (sh - oh) / 2);
233 (
234 scaled,
235 Layout {
236 output: (ow, oh),
237 placed: Region::new(0, 0, ow, oh),
238 },
239 window,
240 )
241 }
242 Fit::Contain => {
243 let (ow, oh) = (self.width.max(sw), self.height.max(sh));
244 let placed = Region::new((ow - sw) / 2, (oh - sh) / 2, sw, sh);
245 (
246 scaled,
247 Layout {
248 output: (ow, oh),
249 placed,
250 },
251 (0, 0),
252 )
253 }
254 _ => (
255 scaled,
256 Layout {
257 output: scaled,
258 placed: Region::new(0, 0, sw, sh),
259 },
260 (0, 0),
261 ),
262 }
263 }
264
265 fn binding(&self, input: &ImageDescriptor) -> Result<&Binding> {
275 if let Some(bound) = self.bound.get() {
276 return check_binding(bound, input);
277 }
278 let ((width, height), layout, (wx, wy)) = self.layout(input);
279 let (visible_w, visible_h) = (layout.placed.width, layout.placed.height);
280 let candidate = Binding {
281 input: *input,
282 horizontal: Weights::build(self.options.filter, input.width, width)?
283 .window(wx, visible_w),
284 vertical: Weights::build(self.options.filter, input.height, height)?
285 .window(wy, visible_h),
286 layout,
287 };
288 let bound = self.bound.get_or_init(|| candidate);
289 check_binding(bound, input)
290 }
291
292 fn bound(&self) -> Result<&Binding> {
294 self.bound.get().ok_or_else(|| {
295 PixelsError::graph("`resize` computed before its output shape was resolved")
296 })
297 }
298}
299
300fn check_binding<'a>(bound: &'a Binding, input: &ImageDescriptor) -> Result<&'a Binding> {
302 if bound.input.width != input.width || bound.input.height != input.height {
303 return Err(PixelsError::graph(format!(
304 "`resize` is bound to a {}x{} input but was given {}x{}; \
305 build a new Resize per image",
306 bound.input.width, bound.input.height, input.width, input.height
307 )));
308 }
309 Ok(bound)
310}
311
312impl Op for Resize {
313 fn rescaled(&self) -> Option<std::sync::Arc<dyn Op>> {
320 Some(std::sync::Arc::new(self.clone()))
321 }
322 fn name(&self) -> &'static str {
323 "resize"
324 }
325
326 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
327 let input = inputs
328 .first()
329 .ok_or_else(|| PixelsError::graph("`resize` takes one input, got none"))?;
330 self.binding(input)?;
333 let (width, height) = self.target(input);
334 input.resized(width, height)
335 }
336
337 fn input_regions(&self, output: Region, inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
338 let input = inputs
339 .first()
340 .ok_or_else(|| PixelsError::graph("`resize` takes one input, got none"))?;
341 let bound = self.binding(input)?;
342 let Some(image) = bound.image_part(output) else {
346 return Ok(vec![Region::new(0, 0, 1, 1)]);
347 };
348 let (x, width) = bound.horizontal.footprint(image.x, image.width);
352 let (y, height) = bound.vertical.footprint(image.y, image.height);
353 Ok(vec![Region::new(x, y, width, height)])
354 }
355
356 fn access_pattern(&self) -> AccessPattern {
357 AccessPattern::Spatial
360 }
361
362 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
363 let input = inputs
364 .first()
365 .ok_or_else(|| PixelsError::graph("`resize` takes one input tile, got none"))?;
366
367 let format = output.pixel();
368 if input.pixel() != format {
369 return Err(PixelsError::graph(format!(
370 "`resize` input is {} but output is {format}",
371 input.pixel()
372 )));
373 }
374 let channels = format.channels();
375 let bound = self.bound()?;
376 let region = output.region();
377 if bound.layout.placed.contains(region)
378 && bound.layout.placed.x == 0
379 && bound.layout.placed.y == 0
380 {
381 return resample_tile(bound, input, output, channels);
382 }
383 let fill = background_pixel(format, self.options.background);
386 for row in output.rows_mut() {
387 for (slot, &byte) in row.iter_mut().zip(fill.iter().cycle()) {
388 *slot = byte;
389 }
390 }
391 let Some(image) = bound.image_part(region) else {
392 return Ok(());
393 };
394 let mut part = TileBuf::zeroed(image, format)?;
395 resample_tile(bound, input, &mut part.as_tile_mut()?, channels)?;
396 let (px, py) = (bound.layout.placed.x, bound.layout.placed.y);
397 let bytes = format.bytes_per_pixel();
398 let row_len = image.width as usize * bytes;
399 for (i, from) in part.bytes().chunks_exact(row_len.max(1)).enumerate() {
400 let Some(row) = output.row_mut(image.y + py + i as u32) else {
401 continue;
402 };
403 let at = (image.x + px - region.x) as usize * bytes;
404 if let Some(to) = row.get_mut(at..at + row_len) {
405 to.copy_from_slice(from);
406 }
407 }
408 Ok(())
409 }
410}
411
412impl Binding {
413 fn image_part(&self, region: Region) -> Option<Region> {
416 let placed = self.layout.placed;
417 let overlap = region.intersect(placed);
418 (overlap.width > 0 && overlap.height > 0).then(|| {
419 Region::new(
420 overlap.x - placed.x,
421 overlap.y - placed.y,
422 overlap.width,
423 overlap.height,
424 )
425 })
426 }
427}
428
429fn background_pixel(format: PixelFormat, rgba: [u8; 4]) -> Vec<u8> {
431 let [r, g, b, a] = rgba;
432 let luma = ((u32::from(r) * 299 + u32::from(g) * 587 + u32::from(b) * 114 + 500) / 1000) as u8;
433 let samples: Vec<u8> = match format.layout() {
434 ChannelLayout::Gray => vec![luma],
435 ChannelLayout::GrayAlpha => vec![luma, a],
436 ChannelLayout::Rgb => vec![r, g, b],
437 ChannelLayout::Rgba => vec![r, g, b, a],
438 };
439 match format.sample_kind() {
440 SampleKind::U8 => samples,
441 SampleKind::U16 => samples
442 .iter()
443 .flat_map(|&v| (u16::from(v) * 257).to_ne_bytes())
444 .collect(),
445 SampleKind::F32 => samples
446 .iter()
447 .flat_map(|&v| (f32::from(v) / 255.0).to_ne_bytes())
448 .collect(),
449 }
450}
451
452fn resample_tile(
459 bound: &Binding,
460 input: &Tile<'_>,
461 output: &mut TileMut<'_>,
462 channels: usize,
463) -> Result<()> {
464 let region = output.region();
465 let source = input.region();
466 let horizontal = bound
467 .horizontal
468 .for_tile(region.x, region.width, source.x)?;
469 let vertical = bound.vertical.for_tile(region.y, region.height, source.y)?;
470
471 let format = output.pixel();
472 let intermediate_row = region.width as usize * channels;
473
474 match format.sample_kind() {
475 SampleKind::U8 => {
476 let mut intermediate = vec![0_u8; intermediate_row * source.height as usize];
479 for y in 0..source.height {
480 let Some(row) = input.row(source.y + y) else {
481 continue;
482 };
483 let at = y as usize * intermediate_row;
484 let Some(target) = intermediate.get_mut(at..at + intermediate_row) else {
485 continue;
486 };
487 row_u8(row, target, channels, &horizontal);
488 }
489
490 let mut accumulator = vec![0_i32; intermediate_row];
493 let mut resampled = vec![0_u8; intermediate_row];
494 for (index, run) in vertical.runs().iter().enumerate() {
495 column_u8(
496 &intermediate,
497 intermediate_row,
498 run.start as usize,
499 vertical.quantized(run),
500 &mut accumulator,
501 &mut resampled,
502 );
503 write_row(output, region.y + index as u32, &resampled);
504 }
505 }
506 SampleKind::U16 => {
507 let mut intermediate = vec![0_u16; intermediate_row * source.height as usize];
508 for y in 0..source.height {
509 let Some(row) = input.row(source.y + y) else {
510 continue;
511 };
512 let wide = to_u16(row);
513 let at = y as usize * intermediate_row;
514 let Some(target) = intermediate.get_mut(at..at + intermediate_row) else {
515 continue;
516 };
517 row_u16(&wide, target, channels, &horizontal);
518 }
519 let mut accumulator = vec![0.0_f32; intermediate_row];
520 let mut resampled = vec![0_u16; intermediate_row];
521 for (index, run) in vertical.runs().iter().enumerate() {
522 column_u16(
523 &intermediate,
524 intermediate_row,
525 run.start as usize,
526 vertical.exact(run),
527 &mut accumulator,
528 &mut resampled,
529 );
530 let bytes: Vec<u8> = resampled.iter().flat_map(|v| v.to_ne_bytes()).collect();
531 write_row(output, region.y + index as u32, &bytes);
532 }
533 }
534 SampleKind::F32 => {
535 let mut intermediate = vec![0.0_f32; intermediate_row * source.height as usize];
536 for y in 0..source.height {
537 let Some(row) = input.row(source.y + y) else {
538 continue;
539 };
540 let floats = to_f32(row);
541 let at = y as usize * intermediate_row;
542 let Some(target) = intermediate.get_mut(at..at + intermediate_row) else {
543 continue;
544 };
545 row_f32(&floats, target, channels, &horizontal);
546 }
547 let mut accumulator = vec![0.0_f32; intermediate_row];
548 let mut resampled = vec![0.0_f32; intermediate_row];
549 for (index, run) in vertical.runs().iter().enumerate() {
550 column_f32(
551 &intermediate,
552 intermediate_row,
553 run.start as usize,
554 vertical.exact(run),
555 &mut accumulator,
556 &mut resampled,
557 );
558 let bytes: Vec<u8> = resampled.iter().flat_map(|v| v.to_ne_bytes()).collect();
559 write_row(output, region.y + index as u32, &bytes);
560 }
561 }
562 }
563 Ok(())
564}
565
566fn to_u16(row: &[u8]) -> Vec<u16> {
568 row.chunks_exact(2)
569 .map(|pair| {
570 u16::from_ne_bytes([
571 pair.first().copied().unwrap_or(0),
572 pair.get(1).copied().unwrap_or(0),
573 ])
574 })
575 .collect()
576}
577
578fn to_f32(row: &[u8]) -> Vec<f32> {
580 row.chunks_exact(4)
581 .map(|quad| {
582 let mut bytes = [0_u8; 4];
583 for (slot, &byte) in bytes.iter_mut().zip(quad) {
584 *slot = byte;
585 }
586 f32::from_ne_bytes(bytes)
587 })
588 .collect()
589}
590
591fn write_row(output: &mut TileMut<'_>, y: u32, bytes: &[u8]) {
593 let Some(row) = output.row_mut(y) else { return };
594 let len = row.len().min(bytes.len());
595 if let (Some(to), Some(from)) = (row.get_mut(..len), bytes.get(..len)) {
596 to.copy_from_slice(from);
597 }
598}
599
600#[cfg(test)]
601#[allow(
602 clippy::unwrap_used,
603 clippy::expect_used,
604 clippy::indexing_slicing,
605 clippy::panic,
606 reason = "tests operate on known-good values and assert shapes directly"
607)]
608mod tests {
609 use super::*;
610 use otf_pixels_core::{PixelFormat, TileBuf};
611
612 fn resize_whole(
614 op: &Resize,
615 input: &ImageDescriptor,
616 bytes: &[u8],
617 ) -> Result<(ImageDescriptor, Vec<u8>)> {
618 let out_desc = op.output_descriptor(std::slice::from_ref(input))?;
619 let source = TileBuf::from_vec(input.region(), input.pixel, bytes.to_vec())?;
620 let mut target = TileBuf::for_image(&out_desc)?;
621 let regions = op.input_regions(out_desc.region(), std::slice::from_ref(input))?;
622 assert_eq!(
623 regions[0],
624 input.region(),
625 "whole-image demand should be the whole input"
626 );
627 op.compute(&[source.as_tile()?], &mut target.as_tile_mut()?)?;
628 Ok((out_desc, target.into_bytes()))
629 }
630
631 fn ramp(width: u32, height: u32, format: PixelFormat) -> (ImageDescriptor, Vec<u8>) {
632 let descriptor = ImageDescriptor::new(width, height, format).unwrap();
633 let len = descriptor.byte_len().unwrap();
634 let bytes = (0..len).map(|i| ((i * 37) % 251) as u8).collect();
635 (descriptor, bytes)
636 }
637
638 #[test]
639 fn the_output_is_independent_of_how_the_image_is_tiled() {
640 let (input, bytes) = ramp(97, 71, PixelFormat::Rgb8);
644 let op = Resize::new(41, 33, ResizeOptions::default()).unwrap();
645 let (out_desc, whole) = resize_whole(&op, &input, &bytes).unwrap();
646
647 for (tile_w, tile_h) in [(8_u32, 8_u32), (16, 4), (41, 1), (1, 33), (7, 13)] {
648 let op = Resize::new(41, 33, ResizeOptions::default()).unwrap();
650 op.output_descriptor(std::slice::from_ref(&input)).unwrap();
651 let source = TileBuf::from_vec(input.region(), input.pixel, bytes.clone()).unwrap();
652 let mut target = TileBuf::for_image(&out_desc).unwrap();
653
654 let mut y = 0;
655 while y < out_desc.height {
656 let h = tile_h.min(out_desc.height - y);
657 let mut x = 0;
658 while x < out_desc.width {
659 let w = tile_w.min(out_desc.width - x);
660 let region = Region::new(x, y, w, h);
661 let demand = op
662 .input_regions(region, std::slice::from_ref(&input))
663 .unwrap();
664 let window = source.as_tile().unwrap();
665 let mut sub = TileBuf::zeroed(region, out_desc.pixel).unwrap();
666
667 let mut cut = TileBuf::zeroed(demand[0], input.pixel).unwrap();
669 otf_pixels_core::copy_region(
670 &window,
671 &mut cut.as_tile_mut().unwrap(),
672 demand[0],
673 )
674 .unwrap();
675
676 op.compute(&[cut.as_tile().unwrap()], &mut sub.as_tile_mut().unwrap())
677 .unwrap();
678 otf_pixels_core::copy_region(
679 &sub.as_tile().unwrap(),
680 &mut target.as_tile_mut().unwrap(),
681 region,
682 )
683 .unwrap();
684 x += w;
685 }
686 y += h;
687 }
688 assert_eq!(
689 target.into_bytes(),
690 whole,
691 "tiling at {tile_w}x{tile_h} changed the pixels"
692 );
693 }
694 }
695
696 fn resize_tiled(
699 op: &Resize,
700 input: &ImageDescriptor,
701 bytes: &[u8],
702 tile: (u32, u32),
703 ) -> (ImageDescriptor, Vec<u8>) {
704 let out_desc = op.output_descriptor(std::slice::from_ref(input)).unwrap();
705 let source = TileBuf::from_vec(input.region(), input.pixel, bytes.to_vec()).unwrap();
706 let mut target = TileBuf::for_image(&out_desc).unwrap();
707 let mut y = 0;
708 while y < out_desc.height {
709 let h = tile.1.min(out_desc.height - y);
710 let mut x = 0;
711 while x < out_desc.width {
712 let w = tile.0.min(out_desc.width - x);
713 let region = Region::new(x, y, w, h);
714 let demand = op
715 .input_regions(region, std::slice::from_ref(input))
716 .unwrap();
717 let mut cut = TileBuf::zeroed(demand[0], input.pixel).unwrap();
718 otf_pixels_core::copy_region(
719 &source.as_tile().unwrap(),
720 &mut cut.as_tile_mut().unwrap(),
721 demand[0],
722 )
723 .unwrap();
724 let mut sub = TileBuf::zeroed(region, out_desc.pixel).unwrap();
725 op.compute(&[cut.as_tile().unwrap()], &mut sub.as_tile_mut().unwrap())
726 .unwrap();
727 otf_pixels_core::copy_region(
728 &sub.as_tile().unwrap(),
729 &mut target.as_tile_mut().unwrap(),
730 region,
731 )
732 .unwrap();
733 x += w;
734 }
735 y += h;
736 }
737 (out_desc, target.into_bytes())
738 }
739
740 fn with_fit(width: u32, height: u32, fit: Fit) -> Resize {
741 Resize::new(
742 width,
743 height,
744 ResizeOptions::default()
745 .with_fit(fit)
746 .with_background([10, 20, 30, 255]),
747 )
748 .unwrap()
749 }
750
751 #[test]
752 fn outside_and_inside_scale_by_the_other_axis() {
753 let (input, _) = ramp(200, 100, PixelFormat::Rgb8);
754 assert_eq!(with_fit(50, 50, Fit::Inside).target(&input), (50, 25));
755 assert_eq!(with_fit(50, 50, Fit::Outside).target(&input), (100, 50));
756 assert_eq!(with_fit(50, 50, Fit::Cover).target(&input), (50, 50));
757 assert_eq!(with_fit(50, 50, Fit::Contain).target(&input), (50, 50));
758 assert_eq!(with_fit(50, 50, Fit::Fill).target(&input), (50, 50));
759 }
760
761 #[test]
762 fn cover_is_outside_then_a_centred_crop() {
763 for (w, h, format) in [
764 (200, 100, PixelFormat::Rgb8),
765 (61, 97, PixelFormat::Rgba16),
766 (90, 90, PixelFormat::Gray8),
767 ] {
768 let (input, bytes) = ramp(w, h, format);
769 let (outside_desc, outside) = resize_tiled(
770 &with_fit(40, 30, Fit::Outside),
771 &input,
772 &bytes,
773 (1000, 1000),
774 );
775 let bpp = format.bytes_per_pixel();
776 for tile in [(1000, 1000), (7, 5), (40, 1)] {
777 let (desc, cover) =
778 resize_tiled(&with_fit(40, 30, Fit::Cover), &input, &bytes, tile);
779 assert_eq!(
780 (desc.width, desc.height),
781 (40.min(outside_desc.width), 30.min(outside_desc.height))
782 );
783 let (dx, dy) = (
784 (outside_desc.width - desc.width) / 2,
785 (outside_desc.height - desc.height) / 2,
786 );
787 for y in 0..desc.height as usize {
788 let from =
789 ((y + dy as usize) * outside_desc.width as usize + dx as usize) * bpp;
790 let row = desc.width as usize * bpp;
791 assert_eq!(
792 &cover[y * row..(y + 1) * row],
793 &outside[from..from + row],
794 "{w}x{h} {format} row {y} tile {tile:?}"
795 );
796 }
797 }
798 }
799 }
800
801 #[test]
802 fn contain_is_inside_centred_on_the_background() {
803 for (w, h, format) in [
804 (200, 100, PixelFormat::Rgb8),
805 (61, 97, PixelFormat::Rgba8),
806 (30, 90, PixelFormat::Gray16),
807 ] {
808 let (input, bytes) = ramp(w, h, format);
809 let (inside_desc, inside) =
810 resize_tiled(&with_fit(40, 30, Fit::Inside), &input, &bytes, (1000, 1000));
811 let bpp = format.bytes_per_pixel();
812 let fill = background_pixel(format, [10, 20, 30, 255]);
813 for tile in [(1000, 1000), (7, 5), (3, 30), (40, 2)] {
814 let (desc, contain) =
815 resize_tiled(&with_fit(40, 30, Fit::Contain), &input, &bytes, tile);
816 assert_eq!((desc.width, desc.height), (40, 30));
817 let (px, py) = ((40 - inside_desc.width) / 2, (30 - inside_desc.height) / 2);
818 for y in 0..30_u32 {
819 for x in 0..40_u32 {
820 let at = ((y * 40 + x) as usize) * bpp;
821 let got = &contain[at..at + bpp];
822 let inside_x = x.checked_sub(px).filter(|&v| v < inside_desc.width);
823 let inside_y = y.checked_sub(py).filter(|&v| v < inside_desc.height);
824 let want = match (inside_x, inside_y) {
825 (Some(ix), Some(iy)) => {
826 let from = ((iy * inside_desc.width + ix) as usize) * bpp;
827 &inside[from..from + bpp]
828 }
829 _ => &fill[..],
830 };
831 assert_eq!(got, want, "{w}x{h} {format} at ({x}, {y}) tile {tile:?}");
832 }
833 }
834 }
835 }
836 }
837
838 #[test]
839 fn the_background_follows_the_pixel_format() {
840 assert_eq!(
841 background_pixel(PixelFormat::Rgba8, [1, 2, 3, 4]),
842 vec![1, 2, 3, 4]
843 );
844 assert_eq!(
845 background_pixel(PixelFormat::Rgb8, [1, 2, 3, 4]),
846 vec![1, 2, 3]
847 );
848 assert_eq!(
849 background_pixel(PixelFormat::Gray8, [255, 255, 255, 0]),
850 vec![255]
851 );
852 assert_eq!(
853 background_pixel(PixelFormat::GrayA8, [0, 0, 0, 9]),
854 vec![0, 9]
855 );
856 assert_eq!(
857 background_pixel(PixelFormat::Gray16, [255, 0, 0, 255]),
858 (76_u16 * 257).to_ne_bytes().to_vec()
859 );
860 }
861
862 #[test]
863 fn a_one_to_one_resize_returns_the_image_unchanged() {
864 for format in [
865 PixelFormat::Gray8,
866 PixelFormat::Rgb8,
867 PixelFormat::Rgba8,
868 PixelFormat::Gray16,
869 PixelFormat::Rgb16,
870 ] {
871 let (input, bytes) = ramp(31, 23, format);
872 let op = Resize::new(31, 23, ResizeOptions::default()).unwrap();
873 let (out_desc, out) = resize_whole(&op, &input, &bytes).unwrap();
874 assert_eq!(out_desc.width, 31);
875 assert_eq!(out, bytes, "{format} changed at 1:1");
876 }
877 }
878
879 #[test]
880 fn a_flat_image_stays_flat_at_every_scale_and_filter() {
881 for filter in [
882 Filter::Nearest,
883 Filter::Box,
884 Filter::Bilinear,
885 Filter::CatmullRom,
886 Filter::Mitchell,
887 Filter::Lanczos2,
888 Filter::Lanczos3,
889 ] {
890 for (w, h) in [(10_u32, 10_u32), (200, 150), (37, 91)] {
891 let input = ImageDescriptor::new(64, 64, PixelFormat::Rgb8).unwrap();
892 let bytes = vec![137_u8; input.byte_len().unwrap()];
893 let op = Resize::new(w, h, ResizeOptions::default().with_filter(filter)).unwrap();
894 let (_, out) = resize_whole(&op, &input, &bytes).unwrap();
895 assert!(
896 out.iter().all(|&v| v == 137),
897 "{} to {w}x{h} did not stay flat",
898 filter.as_str()
899 );
900 }
901 }
902 }
903
904 #[test]
905 fn fit_inside_preserves_the_aspect_ratio() {
906 let input = ImageDescriptor::new(1000, 500, PixelFormat::Rgb8).unwrap();
907 let op = Resize::new(100, 100, ResizeOptions::default().with_fit(Fit::Inside)).unwrap();
908 assert_eq!(
909 op.target(&input),
910 (100, 50),
911 "wide image should bind on width"
912 );
913
914 let tall = ImageDescriptor::new(500, 1000, PixelFormat::Rgb8).unwrap();
915 assert_eq!(
916 op.target(&tall),
917 (50, 100),
918 "tall image should bind on height"
919 );
920 }
921
922 #[test]
923 fn fit_fill_ignores_the_aspect_ratio() {
924 let input = ImageDescriptor::new(1000, 500, PixelFormat::Rgb8).unwrap();
925 let op = Resize::new(100, 100, ResizeOptions::default()).unwrap();
926 assert_eq!(op.target(&input), (100, 100));
927 }
928
929 #[test]
930 fn without_enlargement_leaves_a_small_image_alone() {
931 let input = ImageDescriptor::new(40, 30, PixelFormat::Rgb8).unwrap();
932 let options = ResizeOptions::default()
933 .with_fit(Fit::Inside)
934 .without_enlargement(true);
935 let op = Resize::new(1000, 1000, options).unwrap();
936 assert_eq!(op.target(&input), (40, 30));
937
938 let big = ImageDescriptor::new(4000, 3000, PixelFormat::Rgb8).unwrap();
940 assert_eq!(op.target(&big), (1000, 750));
941 }
942
943 #[test]
944 fn a_zero_target_is_an_error() {
945 assert!(Resize::new(0, 10, ResizeOptions::default()).is_err());
946 assert!(Resize::new(10, 0, ResizeOptions::default()).is_err());
947 }
948
949 #[test]
950 fn a_target_never_collapses_to_zero() {
951 let input = ImageDescriptor::new(10_000, 3, PixelFormat::Gray8).unwrap();
954 let op = Resize::new(50, 50, ResizeOptions::default().with_fit(Fit::Inside)).unwrap();
955 let (w, h) = op.target(&input);
956 assert!(w >= 1 && h >= 1, "target collapsed to {w}x{h}");
957 }
958
959 #[test]
960 fn reusing_an_op_across_two_shapes_is_an_error_not_a_wrong_answer() {
961 let first = ImageDescriptor::new(100, 100, PixelFormat::Gray8).unwrap();
965 let second = ImageDescriptor::new(200, 200, PixelFormat::Gray8).unwrap();
966 let op = Resize::to(50, 50).unwrap();
967 op.output_descriptor(std::slice::from_ref(&first)).unwrap();
968 let error = op
969 .output_descriptor(std::slice::from_ref(&second))
970 .unwrap_err();
971 assert!(error.to_string().contains("bound to"), "{error}");
972 }
973
974 #[test]
975 fn a_clone_can_be_bound_to_a_different_shape() {
976 let first = ImageDescriptor::new(100, 100, PixelFormat::Gray8).unwrap();
979 let second = ImageDescriptor::new(200, 200, PixelFormat::Gray8).unwrap();
980 let op = Resize::to(50, 50).unwrap();
981 op.output_descriptor(std::slice::from_ref(&first)).unwrap();
982 let fresh = op.clone();
983 assert!(
984 fresh
985 .output_descriptor(std::slice::from_ref(&second))
986 .is_ok()
987 );
988 }
989
990 #[test]
991 fn demand_never_reaches_outside_the_input() {
992 let input = ImageDescriptor::new(50, 40, PixelFormat::Rgb8).unwrap();
995 let op = Resize::new(200, 160, ResizeOptions::default()).unwrap();
996 let out = op.output_descriptor(std::slice::from_ref(&input)).unwrap();
997 for y in 0..out.height {
998 for x in 0..out.width {
999 let region = Region::new(x, y, 1, 1);
1000 let demand = op
1001 .input_regions(region, std::slice::from_ref(&input))
1002 .unwrap();
1003 let r = demand[0];
1004 assert!(
1005 r.x + r.width <= input.width && r.y + r.height <= input.height,
1006 "demand {r} for output {region} leaves a {}x{} input",
1007 input.width,
1008 input.height
1009 );
1010 }
1011 }
1012 }
1013
1014 #[test]
1015 fn resize_is_deterministic() {
1016 let (input, bytes) = ramp(123, 87, PixelFormat::Rgba8);
1017 let op = Resize::to(61, 43).unwrap();
1018 let (_, first) = resize_whole(&op, &input, &bytes).unwrap();
1019 for _ in 0..5 {
1020 let op = Resize::to(61, 43).unwrap();
1021 let (_, again) = resize_whole(&op, &input, &bytes).unwrap();
1022 assert_eq!(again, first, "resize is not deterministic");
1023 }
1024 }
1025}