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,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
61#[non_exhaustive]
62pub enum Position {
63 #[default]
65 Centre,
66 North,
68 NorthEast,
70 East,
72 SouthEast,
74 South,
76 SouthWest,
78 West,
80 NorthWest,
82}
83
84impl Position {
85 const fn offset(self, outer: (u32, u32), inner: (u32, u32)) -> (u32, u32) {
88 let (spare_x, spare_y) = (
89 outer.0.saturating_sub(inner.0),
90 outer.1.saturating_sub(inner.1),
91 );
92 let x = match self {
93 Self::West | Self::NorthWest | Self::SouthWest => 0,
94 Self::East | Self::NorthEast | Self::SouthEast => spare_x,
95 Self::Centre | Self::North | Self::South => spare_x / 2,
96 };
97 let y = match self {
98 Self::North | Self::NorthWest | Self::NorthEast => 0,
99 Self::South | Self::SouthWest | Self::SouthEast => spare_y,
100 Self::Centre | Self::West | Self::East => spare_y / 2,
101 };
102 (x, y)
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
108#[non_exhaustive]
109pub struct ResizeOptions {
110 pub filter: Filter,
112 pub fit: Fit,
114 pub without_enlargement: bool,
116 pub without_reduction: bool,
119 pub position: Position,
122 pub background: [u8; 4],
126}
127
128impl Default for ResizeOptions {
129 fn default() -> Self {
130 Self {
131 filter: Filter::default(),
132 fit: Fit::default(),
133 without_enlargement: false,
134 without_reduction: false,
135 position: Position::Centre,
136 background: [0, 0, 0, 255],
137 }
138 }
139}
140
141impl ResizeOptions {
142 #[must_use]
144 pub const fn with_filter(mut self, filter: Filter) -> Self {
145 self.filter = filter;
146 self
147 }
148
149 #[must_use]
151 pub const fn with_fit(mut self, fit: Fit) -> Self {
152 self.fit = fit;
153 self
154 }
155
156 #[must_use]
158 pub const fn with_background(mut self, rgba: [u8; 4]) -> Self {
159 self.background = rgba;
160 self
161 }
162
163 #[must_use]
165 pub const fn without_enlargement(mut self, refuse: bool) -> Self {
166 self.without_enlargement = refuse;
167 self
168 }
169
170 #[must_use]
172 pub const fn without_reduction(mut self, refuse: bool) -> Self {
173 self.without_reduction = refuse;
174 self
175 }
176
177 #[must_use]
180 pub const fn with_position(mut self, position: Position) -> Self {
181 self.position = position;
182 self
183 }
184}
185
186#[derive(Debug)]
194struct Binding {
195 input: ImageDescriptor,
196 horizontal: Weights,
198 vertical: Weights,
199 layout: Layout,
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204struct Layout {
205 output: (u32, u32),
207 placed: Region,
210}
211
212#[derive(Debug)]
214pub struct Resize {
215 width: u32,
216 height: u32,
217 options: ResizeOptions,
218 bound: OnceLock<Binding>,
220}
221
222impl Clone for Resize {
223 fn clone(&self) -> Self {
224 Self {
228 width: self.width,
229 height: self.height,
230 options: self.options,
231 bound: OnceLock::new(),
232 }
233 }
234}
235
236impl Resize {
237 pub fn new(width: u32, height: u32, options: ResizeOptions) -> Result<Self> {
243 if width == 0 || height == 0 {
244 return Err(PixelsError::invalid_argument(
245 "size",
246 format!("resize target {width}x{height} has no pixels"),
247 ));
248 }
249 Ok(Self {
250 width,
251 height,
252 options,
253 bound: OnceLock::new(),
254 })
255 }
256
257 pub fn to(width: u32, height: u32) -> Result<Self> {
263 Self::new(width, height, ResizeOptions::default())
264 }
265
266 #[must_use]
268 pub const fn options(&self) -> ResizeOptions {
269 self.options
270 }
271
272 #[must_use]
277 pub fn target(&self, input: &ImageDescriptor) -> (u32, u32) {
278 self.layout(input).1.output
279 }
280
281 fn layout(&self, input: &ImageDescriptor) -> ((u32, u32), Layout, (u32, u32)) {
283 let by_width = f64::from(self.width) / f64::from(input.width);
286 let by_height = f64::from(self.height) / f64::from(input.height);
287 let preserving = |scale: f64| {
288 (
289 ((f64::from(input.width) * scale).round() as u32).max(1),
290 ((f64::from(input.height) * scale).round() as u32).max(1),
291 )
292 };
293 let (mut width, mut height) = match self.options.fit {
294 Fit::Fill => (self.width, self.height),
295 Fit::Inside | Fit::Contain => preserving(by_width.min(by_height)),
296 Fit::Outside | Fit::Cover => preserving(by_width.max(by_height)),
297 };
298 if self.options.without_enlargement {
299 width = width.min(input.width);
300 height = height.min(input.height);
301 }
302 if self.options.without_reduction {
303 width = width.max(input.width);
304 height = height.max(input.height);
305 }
306 let scaled = (width.max(1), height.max(1));
307 let (sw, sh) = scaled;
308 match self.options.fit {
309 Fit::Cover => {
310 let (ow, oh) = (sw.min(self.width), sh.min(self.height));
311 let window = self.options.position.offset((sw, sh), (ow, oh));
312 (
313 scaled,
314 Layout {
315 output: (ow, oh),
316 placed: Region::new(0, 0, ow, oh),
317 },
318 window,
319 )
320 }
321 Fit::Contain => {
322 let (ow, oh) = (self.width.max(sw), self.height.max(sh));
323 let (px, py) = self.options.position.offset((ow, oh), (sw, sh));
324 let placed = Region::new(px, py, sw, sh);
325 (
326 scaled,
327 Layout {
328 output: (ow, oh),
329 placed,
330 },
331 (0, 0),
332 )
333 }
334 _ => (
335 scaled,
336 Layout {
337 output: scaled,
338 placed: Region::new(0, 0, sw, sh),
339 },
340 (0, 0),
341 ),
342 }
343 }
344
345 fn binding(&self, input: &ImageDescriptor) -> Result<&Binding> {
355 if let Some(bound) = self.bound.get() {
356 return check_binding(bound, input);
357 }
358 let ((width, height), layout, (wx, wy)) = self.layout(input);
359 let (visible_w, visible_h) = (layout.placed.width, layout.placed.height);
360 let candidate = Binding {
361 input: *input,
362 horizontal: Weights::build(self.options.filter, input.width, width)?
363 .window(wx, visible_w),
364 vertical: Weights::build(self.options.filter, input.height, height)?
365 .window(wy, visible_h),
366 layout,
367 };
368 let bound = self.bound.get_or_init(|| candidate);
369 check_binding(bound, input)
370 }
371
372 fn bound(&self) -> Result<&Binding> {
374 self.bound.get().ok_or_else(|| {
375 PixelsError::graph("`resize` computed before its output shape was resolved")
376 })
377 }
378}
379
380fn check_binding<'a>(bound: &'a Binding, input: &ImageDescriptor) -> Result<&'a Binding> {
382 if bound.input.width != input.width || bound.input.height != input.height {
383 return Err(PixelsError::graph(format!(
384 "`resize` is bound to a {}x{} input but was given {}x{}; \
385 build a new Resize per image",
386 bound.input.width, bound.input.height, input.width, input.height
387 )));
388 }
389 Ok(bound)
390}
391
392impl Op for Resize {
393 fn rescaled(&self) -> Option<std::sync::Arc<dyn Op>> {
400 Some(std::sync::Arc::new(self.clone()))
401 }
402 fn name(&self) -> &'static str {
403 "resize"
404 }
405
406 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
407 let input = inputs
408 .first()
409 .ok_or_else(|| PixelsError::graph("`resize` takes one input, got none"))?;
410 self.binding(input)?;
413 let (width, height) = self.target(input);
414 input.resized(width, height)
415 }
416
417 fn input_regions(&self, output: Region, inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
418 let input = inputs
419 .first()
420 .ok_or_else(|| PixelsError::graph("`resize` takes one input, got none"))?;
421 let bound = self.binding(input)?;
422 let Some(image) = bound.image_part(output) else {
426 return Ok(vec![Region::new(0, 0, 1, 1)]);
427 };
428 let (x, width) = bound.horizontal.footprint(image.x, image.width);
432 let (y, height) = bound.vertical.footprint(image.y, image.height);
433 Ok(vec![Region::new(x, y, width, height)])
434 }
435
436 fn access_pattern(&self) -> AccessPattern {
437 AccessPattern::Spatial
440 }
441
442 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
443 let input = inputs
444 .first()
445 .ok_or_else(|| PixelsError::graph("`resize` takes one input tile, got none"))?;
446
447 let format = output.pixel();
448 if input.pixel() != format {
449 return Err(PixelsError::graph(format!(
450 "`resize` input is {} but output is {format}",
451 input.pixel()
452 )));
453 }
454 let channels = format.channels();
455 let bound = self.bound()?;
456 let region = output.region();
457 if bound.layout.placed.contains(region)
458 && bound.layout.placed.x == 0
459 && bound.layout.placed.y == 0
460 {
461 return resample_tile(bound, input, output, channels);
462 }
463 let fill = background_pixel(format, self.options.background);
466 for row in output.rows_mut() {
467 for (slot, &byte) in row.iter_mut().zip(fill.iter().cycle()) {
468 *slot = byte;
469 }
470 }
471 let Some(image) = bound.image_part(region) else {
472 return Ok(());
473 };
474 let mut part = TileBuf::zeroed(image, format)?;
475 resample_tile(bound, input, &mut part.as_tile_mut()?, channels)?;
476 let (px, py) = (bound.layout.placed.x, bound.layout.placed.y);
477 let bytes = format.bytes_per_pixel();
478 let row_len = image.width as usize * bytes;
479 for (i, from) in part.bytes().chunks_exact(row_len.max(1)).enumerate() {
480 let Some(row) = output.row_mut(image.y + py + i as u32) else {
481 continue;
482 };
483 let at = (image.x + px - region.x) as usize * bytes;
484 if let Some(to) = row.get_mut(at..at + row_len) {
485 to.copy_from_slice(from);
486 }
487 }
488 Ok(())
489 }
490}
491
492impl Binding {
493 fn image_part(&self, region: Region) -> Option<Region> {
496 let placed = self.layout.placed;
497 let overlap = region.intersect(placed);
498 (overlap.width > 0 && overlap.height > 0).then(|| {
499 Region::new(
500 overlap.x - placed.x,
501 overlap.y - placed.y,
502 overlap.width,
503 overlap.height,
504 )
505 })
506 }
507}
508
509pub(crate) fn background_pixel(format: PixelFormat, rgba: [u8; 4]) -> Vec<u8> {
511 let [r, g, b, a] = rgba;
512 let luma = ((u32::from(r) * 299 + u32::from(g) * 587 + u32::from(b) * 114 + 500) / 1000) as u8;
513 let samples: Vec<u8> = match format.layout() {
514 ChannelLayout::Gray => vec![luma],
515 ChannelLayout::GrayAlpha => vec![luma, a],
516 ChannelLayout::Rgb => vec![r, g, b],
517 ChannelLayout::Rgba => vec![r, g, b, a],
518 };
519 match format.sample_kind() {
520 SampleKind::U8 => samples,
521 SampleKind::U16 => samples
522 .iter()
523 .flat_map(|&v| (u16::from(v) * 257).to_ne_bytes())
524 .collect(),
525 SampleKind::F32 => samples
526 .iter()
527 .flat_map(|&v| (f32::from(v) / 255.0).to_ne_bytes())
528 .collect(),
529 }
530}
531
532fn resample_tile(
539 bound: &Binding,
540 input: &Tile<'_>,
541 output: &mut TileMut<'_>,
542 channels: usize,
543) -> Result<()> {
544 let region = output.region();
545 let source = input.region();
546 let horizontal = bound
547 .horizontal
548 .for_tile(region.x, region.width, source.x)?;
549 let vertical = bound.vertical.for_tile(region.y, region.height, source.y)?;
550
551 let format = output.pixel();
552 let intermediate_row = region.width as usize * channels;
553
554 match format.sample_kind() {
555 SampleKind::U8 => {
556 let mut intermediate = vec![0_u8; intermediate_row * source.height as usize];
559 for y in 0..source.height {
560 let Some(row) = input.row(source.y + y) else {
561 continue;
562 };
563 let at = y as usize * intermediate_row;
564 let Some(target) = intermediate.get_mut(at..at + intermediate_row) else {
565 continue;
566 };
567 row_u8(row, target, channels, &horizontal);
568 }
569
570 let mut accumulator = vec![0_i32; intermediate_row];
573 let mut resampled = vec![0_u8; intermediate_row];
574 for (index, run) in vertical.runs().iter().enumerate() {
575 column_u8(
576 &intermediate,
577 intermediate_row,
578 run.start as usize,
579 vertical.quantized(run),
580 &mut accumulator,
581 &mut resampled,
582 );
583 write_row(output, region.y + index as u32, &resampled);
584 }
585 }
586 SampleKind::U16 => {
587 let mut intermediate = vec![0_u16; intermediate_row * source.height as usize];
588 for y in 0..source.height {
589 let Some(row) = input.row(source.y + y) else {
590 continue;
591 };
592 let wide = to_u16(row);
593 let at = y as usize * intermediate_row;
594 let Some(target) = intermediate.get_mut(at..at + intermediate_row) else {
595 continue;
596 };
597 row_u16(&wide, target, channels, &horizontal);
598 }
599 let mut accumulator = vec![0.0_f32; intermediate_row];
600 let mut resampled = vec![0_u16; intermediate_row];
601 for (index, run) in vertical.runs().iter().enumerate() {
602 column_u16(
603 &intermediate,
604 intermediate_row,
605 run.start as usize,
606 vertical.exact(run),
607 &mut accumulator,
608 &mut resampled,
609 );
610 let bytes: Vec<u8> = resampled.iter().flat_map(|v| v.to_ne_bytes()).collect();
611 write_row(output, region.y + index as u32, &bytes);
612 }
613 }
614 SampleKind::F32 => {
615 let mut intermediate = vec![0.0_f32; intermediate_row * source.height as usize];
616 for y in 0..source.height {
617 let Some(row) = input.row(source.y + y) else {
618 continue;
619 };
620 let floats = to_f32(row);
621 let at = y as usize * intermediate_row;
622 let Some(target) = intermediate.get_mut(at..at + intermediate_row) else {
623 continue;
624 };
625 row_f32(&floats, target, channels, &horizontal);
626 }
627 let mut accumulator = vec![0.0_f32; intermediate_row];
628 let mut resampled = vec![0.0_f32; intermediate_row];
629 for (index, run) in vertical.runs().iter().enumerate() {
630 column_f32(
631 &intermediate,
632 intermediate_row,
633 run.start as usize,
634 vertical.exact(run),
635 &mut accumulator,
636 &mut resampled,
637 );
638 let bytes: Vec<u8> = resampled.iter().flat_map(|v| v.to_ne_bytes()).collect();
639 write_row(output, region.y + index as u32, &bytes);
640 }
641 }
642 }
643 Ok(())
644}
645
646fn to_u16(row: &[u8]) -> Vec<u16> {
648 row.chunks_exact(2)
649 .map(|pair| {
650 u16::from_ne_bytes([
651 pair.first().copied().unwrap_or(0),
652 pair.get(1).copied().unwrap_or(0),
653 ])
654 })
655 .collect()
656}
657
658fn to_f32(row: &[u8]) -> Vec<f32> {
660 row.chunks_exact(4)
661 .map(|quad| {
662 let mut bytes = [0_u8; 4];
663 for (slot, &byte) in bytes.iter_mut().zip(quad) {
664 *slot = byte;
665 }
666 f32::from_ne_bytes(bytes)
667 })
668 .collect()
669}
670
671fn write_row(output: &mut TileMut<'_>, y: u32, bytes: &[u8]) {
673 let Some(row) = output.row_mut(y) else { return };
674 let len = row.len().min(bytes.len());
675 if let (Some(to), Some(from)) = (row.get_mut(..len), bytes.get(..len)) {
676 to.copy_from_slice(from);
677 }
678}
679
680#[cfg(test)]
681#[allow(
682 clippy::unwrap_used,
683 clippy::expect_used,
684 clippy::indexing_slicing,
685 clippy::panic,
686 reason = "tests operate on known-good values and assert shapes directly"
687)]
688mod tests {
689 use super::*;
690 use otf_pixels_core::{PixelFormat, TileBuf};
691
692 fn resize_whole(
694 op: &Resize,
695 input: &ImageDescriptor,
696 bytes: &[u8],
697 ) -> Result<(ImageDescriptor, Vec<u8>)> {
698 let out_desc = op.output_descriptor(std::slice::from_ref(input))?;
699 let source = TileBuf::from_vec(input.region(), input.pixel, bytes.to_vec())?;
700 let mut target = TileBuf::for_image(&out_desc)?;
701 let regions = op.input_regions(out_desc.region(), std::slice::from_ref(input))?;
702 assert_eq!(
703 regions[0],
704 input.region(),
705 "whole-image demand should be the whole input"
706 );
707 op.compute(&[source.as_tile()?], &mut target.as_tile_mut()?)?;
708 Ok((out_desc, target.into_bytes()))
709 }
710
711 fn ramp(width: u32, height: u32, format: PixelFormat) -> (ImageDescriptor, Vec<u8>) {
712 let descriptor = ImageDescriptor::new(width, height, format).unwrap();
713 let len = descriptor.byte_len().unwrap();
714 let bytes = (0..len).map(|i| ((i * 37) % 251) as u8).collect();
715 (descriptor, bytes)
716 }
717
718 #[test]
719 fn the_output_is_independent_of_how_the_image_is_tiled() {
720 let (input, bytes) = ramp(97, 71, PixelFormat::Rgb8);
724 let op = Resize::new(41, 33, ResizeOptions::default()).unwrap();
725 let (out_desc, whole) = resize_whole(&op, &input, &bytes).unwrap();
726
727 for (tile_w, tile_h) in [(8_u32, 8_u32), (16, 4), (41, 1), (1, 33), (7, 13)] {
728 let op = Resize::new(41, 33, ResizeOptions::default()).unwrap();
730 op.output_descriptor(std::slice::from_ref(&input)).unwrap();
731 let source = TileBuf::from_vec(input.region(), input.pixel, bytes.clone()).unwrap();
732 let mut target = TileBuf::for_image(&out_desc).unwrap();
733
734 let mut y = 0;
735 while y < out_desc.height {
736 let h = tile_h.min(out_desc.height - y);
737 let mut x = 0;
738 while x < out_desc.width {
739 let w = tile_w.min(out_desc.width - x);
740 let region = Region::new(x, y, w, h);
741 let demand = op
742 .input_regions(region, std::slice::from_ref(&input))
743 .unwrap();
744 let window = source.as_tile().unwrap();
745 let mut sub = TileBuf::zeroed(region, out_desc.pixel).unwrap();
746
747 let mut cut = TileBuf::zeroed(demand[0], input.pixel).unwrap();
749 otf_pixels_core::copy_region(
750 &window,
751 &mut cut.as_tile_mut().unwrap(),
752 demand[0],
753 )
754 .unwrap();
755
756 op.compute(&[cut.as_tile().unwrap()], &mut sub.as_tile_mut().unwrap())
757 .unwrap();
758 otf_pixels_core::copy_region(
759 &sub.as_tile().unwrap(),
760 &mut target.as_tile_mut().unwrap(),
761 region,
762 )
763 .unwrap();
764 x += w;
765 }
766 y += h;
767 }
768 assert_eq!(
769 target.into_bytes(),
770 whole,
771 "tiling at {tile_w}x{tile_h} changed the pixels"
772 );
773 }
774 }
775
776 fn resize_tiled(
779 op: &Resize,
780 input: &ImageDescriptor,
781 bytes: &[u8],
782 tile: (u32, u32),
783 ) -> (ImageDescriptor, Vec<u8>) {
784 let out_desc = op.output_descriptor(std::slice::from_ref(input)).unwrap();
785 let source = TileBuf::from_vec(input.region(), input.pixel, bytes.to_vec()).unwrap();
786 let mut target = TileBuf::for_image(&out_desc).unwrap();
787 let mut y = 0;
788 while y < out_desc.height {
789 let h = tile.1.min(out_desc.height - y);
790 let mut x = 0;
791 while x < out_desc.width {
792 let w = tile.0.min(out_desc.width - x);
793 let region = Region::new(x, y, w, h);
794 let demand = op
795 .input_regions(region, std::slice::from_ref(input))
796 .unwrap();
797 let mut cut = TileBuf::zeroed(demand[0], input.pixel).unwrap();
798 otf_pixels_core::copy_region(
799 &source.as_tile().unwrap(),
800 &mut cut.as_tile_mut().unwrap(),
801 demand[0],
802 )
803 .unwrap();
804 let mut sub = TileBuf::zeroed(region, out_desc.pixel).unwrap();
805 op.compute(&[cut.as_tile().unwrap()], &mut sub.as_tile_mut().unwrap())
806 .unwrap();
807 otf_pixels_core::copy_region(
808 &sub.as_tile().unwrap(),
809 &mut target.as_tile_mut().unwrap(),
810 region,
811 )
812 .unwrap();
813 x += w;
814 }
815 y += h;
816 }
817 (out_desc, target.into_bytes())
818 }
819
820 fn with_fit(width: u32, height: u32, fit: Fit) -> Resize {
821 with_fit_at(width, height, fit, Position::Centre)
822 }
823
824 fn with_fit_at(width: u32, height: u32, fit: Fit, position: Position) -> Resize {
825 Resize::new(
826 width,
827 height,
828 ResizeOptions::default()
829 .with_fit(fit)
830 .with_position(position)
831 .with_background([10, 20, 30, 255]),
832 )
833 .unwrap()
834 }
835
836 const POSITIONS: [Position; 9] = [
837 Position::Centre,
838 Position::North,
839 Position::NorthEast,
840 Position::East,
841 Position::SouthEast,
842 Position::South,
843 Position::SouthWest,
844 Position::West,
845 Position::NorthWest,
846 ];
847
848 #[test]
849 fn positions_name_the_near_edge_the_middle_or_the_far_edge() {
850 let (outer, inner) = ((100, 60), (40, 20));
851 assert_eq!(Position::NorthWest.offset(outer, inner), (0, 0));
852 assert_eq!(Position::North.offset(outer, inner), (30, 0));
853 assert_eq!(Position::NorthEast.offset(outer, inner), (60, 0));
854 assert_eq!(Position::West.offset(outer, inner), (0, 20));
855 assert_eq!(Position::Centre.offset(outer, inner), (30, 20));
856 assert_eq!(Position::East.offset(outer, inner), (60, 20));
857 assert_eq!(Position::SouthWest.offset(outer, inner), (0, 40));
858 assert_eq!(Position::South.offset(outer, inner), (30, 40));
859 assert_eq!(Position::SouthEast.offset(outer, inner), (60, 40));
860 }
861
862 #[test]
863 fn outside_and_inside_scale_by_the_other_axis() {
864 let (input, _) = ramp(200, 100, PixelFormat::Rgb8);
865 assert_eq!(with_fit(50, 50, Fit::Inside).target(&input), (50, 25));
866 assert_eq!(with_fit(50, 50, Fit::Outside).target(&input), (100, 50));
867 assert_eq!(with_fit(50, 50, Fit::Cover).target(&input), (50, 50));
868 assert_eq!(with_fit(50, 50, Fit::Contain).target(&input), (50, 50));
869 assert_eq!(with_fit(50, 50, Fit::Fill).target(&input), (50, 50));
870 }
871
872 #[test]
873 fn cover_is_outside_then_a_crop_at_the_position() {
874 for (w, h, format, position) in [
875 (200, 100, PixelFormat::Rgb8),
876 (61, 97, PixelFormat::Rgba16),
877 (90, 90, PixelFormat::Gray8),
878 ]
879 .into_iter()
880 .flat_map(|(w, h, f)| POSITIONS.map(|p| (w, h, f, p)))
881 {
882 let (input, bytes) = ramp(w, h, format);
883 let (outside_desc, outside) = resize_tiled(
884 &with_fit(40, 30, Fit::Outside),
885 &input,
886 &bytes,
887 (1000, 1000),
888 );
889 let bpp = format.bytes_per_pixel();
890 for tile in [(1000, 1000), (7, 5), (40, 1)] {
891 let (desc, cover) = resize_tiled(
892 &with_fit_at(40, 30, Fit::Cover, position),
893 &input,
894 &bytes,
895 tile,
896 );
897 assert_eq!(
898 (desc.width, desc.height),
899 (40.min(outside_desc.width), 30.min(outside_desc.height))
900 );
901 let (dx, dy) = position.offset(
902 (outside_desc.width, outside_desc.height),
903 (desc.width, desc.height),
904 );
905 for y in 0..desc.height as usize {
906 let from =
907 ((y + dy as usize) * outside_desc.width as usize + dx as usize) * bpp;
908 let row = desc.width as usize * bpp;
909 assert_eq!(
910 &cover[y * row..(y + 1) * row],
911 &outside[from..from + row],
912 "{w}x{h} {format} {position:?} row {y} tile {tile:?}"
913 );
914 }
915 }
916 }
917 }
918
919 #[test]
920 fn contain_is_inside_placed_on_the_background_at_the_position() {
921 for (w, h, format, position) in [
922 (200, 100, PixelFormat::Rgb8),
923 (61, 97, PixelFormat::Rgba8),
924 (30, 90, PixelFormat::Gray16),
925 ]
926 .into_iter()
927 .flat_map(|(w, h, f)| POSITIONS.map(|p| (w, h, f, p)))
928 {
929 let (input, bytes) = ramp(w, h, format);
930 let (inside_desc, inside) =
931 resize_tiled(&with_fit(40, 30, Fit::Inside), &input, &bytes, (1000, 1000));
932 let bpp = format.bytes_per_pixel();
933 let fill = background_pixel(format, [10, 20, 30, 255]);
934 for tile in [(1000, 1000), (7, 5), (3, 30), (40, 2)] {
935 let (desc, contain) = resize_tiled(
936 &with_fit_at(40, 30, Fit::Contain, position),
937 &input,
938 &bytes,
939 tile,
940 );
941 assert_eq!((desc.width, desc.height), (40, 30));
942 let (px, py) = position.offset((40, 30), (inside_desc.width, inside_desc.height));
943 for y in 0..30_u32 {
944 for x in 0..40_u32 {
945 let at = ((y * 40 + x) as usize) * bpp;
946 let got = &contain[at..at + bpp];
947 let inside_x = x.checked_sub(px).filter(|&v| v < inside_desc.width);
948 let inside_y = y.checked_sub(py).filter(|&v| v < inside_desc.height);
949 let want = match (inside_x, inside_y) {
950 (Some(ix), Some(iy)) => {
951 let from = ((iy * inside_desc.width + ix) as usize) * bpp;
952 &inside[from..from + bpp]
953 }
954 _ => &fill[..],
955 };
956 assert_eq!(got, want, "{w}x{h} {format} at ({x}, {y}) tile {tile:?}");
957 }
958 }
959 }
960 }
961 }
962
963 #[test]
964 fn the_background_follows_the_pixel_format() {
965 assert_eq!(
966 background_pixel(PixelFormat::Rgba8, [1, 2, 3, 4]),
967 vec![1, 2, 3, 4]
968 );
969 assert_eq!(
970 background_pixel(PixelFormat::Rgb8, [1, 2, 3, 4]),
971 vec![1, 2, 3]
972 );
973 assert_eq!(
974 background_pixel(PixelFormat::Gray8, [255, 255, 255, 0]),
975 vec![255]
976 );
977 assert_eq!(
978 background_pixel(PixelFormat::GrayA8, [0, 0, 0, 9]),
979 vec![0, 9]
980 );
981 assert_eq!(
982 background_pixel(PixelFormat::Gray16, [255, 0, 0, 255]),
983 (76_u16 * 257).to_ne_bytes().to_vec()
984 );
985 }
986
987 #[test]
988 fn a_one_to_one_resize_returns_the_image_unchanged() {
989 for format in [
990 PixelFormat::Gray8,
991 PixelFormat::Rgb8,
992 PixelFormat::Rgba8,
993 PixelFormat::Gray16,
994 PixelFormat::Rgb16,
995 ] {
996 let (input, bytes) = ramp(31, 23, format);
997 let op = Resize::new(31, 23, ResizeOptions::default()).unwrap();
998 let (out_desc, out) = resize_whole(&op, &input, &bytes).unwrap();
999 assert_eq!(out_desc.width, 31);
1000 assert_eq!(out, bytes, "{format} changed at 1:1");
1001 }
1002 }
1003
1004 #[test]
1005 fn a_flat_image_stays_flat_at_every_scale_and_filter() {
1006 for filter in [
1007 Filter::Nearest,
1008 Filter::Box,
1009 Filter::Bilinear,
1010 Filter::CatmullRom,
1011 Filter::Mitchell,
1012 Filter::Lanczos2,
1013 Filter::Lanczos3,
1014 ] {
1015 for (w, h) in [(10_u32, 10_u32), (200, 150), (37, 91)] {
1016 let input = ImageDescriptor::new(64, 64, PixelFormat::Rgb8).unwrap();
1017 let bytes = vec![137_u8; input.byte_len().unwrap()];
1018 let op = Resize::new(w, h, ResizeOptions::default().with_filter(filter)).unwrap();
1019 let (_, out) = resize_whole(&op, &input, &bytes).unwrap();
1020 assert!(
1021 out.iter().all(|&v| v == 137),
1022 "{} to {w}x{h} did not stay flat",
1023 filter.as_str()
1024 );
1025 }
1026 }
1027 }
1028
1029 #[test]
1030 fn fit_inside_preserves_the_aspect_ratio() {
1031 let input = ImageDescriptor::new(1000, 500, PixelFormat::Rgb8).unwrap();
1032 let op = Resize::new(100, 100, ResizeOptions::default().with_fit(Fit::Inside)).unwrap();
1033 assert_eq!(
1034 op.target(&input),
1035 (100, 50),
1036 "wide image should bind on width"
1037 );
1038
1039 let tall = ImageDescriptor::new(500, 1000, PixelFormat::Rgb8).unwrap();
1040 assert_eq!(
1041 op.target(&tall),
1042 (50, 100),
1043 "tall image should bind on height"
1044 );
1045 }
1046
1047 #[test]
1048 fn fit_fill_ignores_the_aspect_ratio() {
1049 let input = ImageDescriptor::new(1000, 500, PixelFormat::Rgb8).unwrap();
1050 let op = Resize::new(100, 100, ResizeOptions::default()).unwrap();
1051 assert_eq!(op.target(&input), (100, 100));
1052 }
1053
1054 #[test]
1055 fn without_enlargement_leaves_a_small_image_alone() {
1056 let input = ImageDescriptor::new(40, 30, PixelFormat::Rgb8).unwrap();
1057 let options = ResizeOptions::default()
1058 .with_fit(Fit::Inside)
1059 .without_enlargement(true);
1060 let op = Resize::new(1000, 1000, options).unwrap();
1061 assert_eq!(op.target(&input), (40, 30));
1062
1063 let big = ImageDescriptor::new(4000, 3000, PixelFormat::Rgb8).unwrap();
1065 assert_eq!(op.target(&big), (1000, 750));
1066 }
1067
1068 #[test]
1069 fn without_reduction_leaves_a_large_image_alone() {
1070 let big = ImageDescriptor::new(4000, 3000, PixelFormat::Rgb8).unwrap();
1071 let options = ResizeOptions::default()
1072 .with_fit(Fit::Inside)
1073 .without_reduction(true);
1074 let op = Resize::new(1000, 1000, options).unwrap();
1075 assert_eq!(op.target(&big), (4000, 3000));
1076
1077 let small = ImageDescriptor::new(40, 30, PixelFormat::Rgb8).unwrap();
1079 assert_eq!(op.target(&small), (1000, 750));
1080
1081 let neither = Resize::new(1000, 1000, options.without_enlargement(true)).unwrap();
1083 assert_eq!(neither.target(&big), (4000, 3000));
1084 assert_eq!(neither.target(&small), (40, 30));
1085 }
1086
1087 #[test]
1088 fn a_zero_target_is_an_error() {
1089 assert!(Resize::new(0, 10, ResizeOptions::default()).is_err());
1090 assert!(Resize::new(10, 0, ResizeOptions::default()).is_err());
1091 }
1092
1093 #[test]
1094 fn a_target_never_collapses_to_zero() {
1095 let input = ImageDescriptor::new(10_000, 3, PixelFormat::Gray8).unwrap();
1098 let op = Resize::new(50, 50, ResizeOptions::default().with_fit(Fit::Inside)).unwrap();
1099 let (w, h) = op.target(&input);
1100 assert!(w >= 1 && h >= 1, "target collapsed to {w}x{h}");
1101 }
1102
1103 #[test]
1104 fn reusing_an_op_across_two_shapes_is_an_error_not_a_wrong_answer() {
1105 let first = ImageDescriptor::new(100, 100, PixelFormat::Gray8).unwrap();
1109 let second = ImageDescriptor::new(200, 200, PixelFormat::Gray8).unwrap();
1110 let op = Resize::to(50, 50).unwrap();
1111 op.output_descriptor(std::slice::from_ref(&first)).unwrap();
1112 let error = op
1113 .output_descriptor(std::slice::from_ref(&second))
1114 .unwrap_err();
1115 assert!(error.to_string().contains("bound to"), "{error}");
1116 }
1117
1118 #[test]
1119 fn a_clone_can_be_bound_to_a_different_shape() {
1120 let first = ImageDescriptor::new(100, 100, PixelFormat::Gray8).unwrap();
1123 let second = ImageDescriptor::new(200, 200, PixelFormat::Gray8).unwrap();
1124 let op = Resize::to(50, 50).unwrap();
1125 op.output_descriptor(std::slice::from_ref(&first)).unwrap();
1126 let fresh = op.clone();
1127 assert!(
1128 fresh
1129 .output_descriptor(std::slice::from_ref(&second))
1130 .is_ok()
1131 );
1132 }
1133
1134 #[test]
1135 fn demand_never_reaches_outside_the_input() {
1136 let input = ImageDescriptor::new(50, 40, PixelFormat::Rgb8).unwrap();
1139 let op = Resize::new(200, 160, ResizeOptions::default()).unwrap();
1140 let out = op.output_descriptor(std::slice::from_ref(&input)).unwrap();
1141 for y in 0..out.height {
1142 for x in 0..out.width {
1143 let region = Region::new(x, y, 1, 1);
1144 let demand = op
1145 .input_regions(region, std::slice::from_ref(&input))
1146 .unwrap();
1147 let r = demand[0];
1148 assert!(
1149 r.x + r.width <= input.width && r.y + r.height <= input.height,
1150 "demand {r} for output {region} leaves a {}x{} input",
1151 input.width,
1152 input.height
1153 );
1154 }
1155 }
1156 }
1157
1158 #[test]
1159 fn resize_is_deterministic() {
1160 let (input, bytes) = ramp(123, 87, PixelFormat::Rgba8);
1161 let op = Resize::to(61, 43).unwrap();
1162 let (_, first) = resize_whole(&op, &input, &bytes).unwrap();
1163 for _ in 0..5 {
1164 let op = Resize::to(61, 43).unwrap();
1165 let (_, again) = resize_whole(&op, &input, &bytes).unwrap();
1166 assert_eq!(again, first, "resize is not deterministic");
1167 }
1168 }
1169}