1use crate::float_math::F64Ext;
22
23#[non_exhaustive]
37#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
38pub enum ConstraintMode {
39 Distort,
41
42 Fit,
46
47 Within,
50
51 FitCrop,
54
55 WithinCrop,
57
58 FitPad,
61
62 WithinPad,
68
69 PadWithin,
78
79 AspectCrop,
81}
82
83#[non_exhaustive]
85#[derive(Copy, Clone, Debug, Default, PartialEq)]
86pub enum Gravity {
87 #[default]
89 Center,
90 Percentage(f32, f32),
92}
93
94#[non_exhaustive]
100#[derive(Copy, Clone, Debug, Default)]
101pub enum CanvasColor {
102 #[default]
105 Transparent,
106 Srgb { r: u8, g: u8, b: u8, a: u8 },
108 Linear { r: f32, g: f32, b: f32, a: f32 },
110}
111
112impl PartialEq for CanvasColor {
113 fn eq(&self, other: &Self) -> bool {
114 match (self, other) {
115 (Self::Transparent, Self::Transparent) => true,
116 (
117 Self::Srgb {
118 r: r1,
119 g: g1,
120 b: b1,
121 a: a1,
122 },
123 Self::Srgb {
124 r: r2,
125 g: g2,
126 b: b2,
127 a: a2,
128 },
129 ) => r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2,
130 (
131 Self::Linear {
132 r: r1,
133 g: g1,
134 b: b1,
135 a: a1,
136 },
137 Self::Linear {
138 r: r2,
139 g: g2,
140 b: b2,
141 a: a2,
142 },
143 ) => {
144 r1.to_bits() == r2.to_bits()
145 && g1.to_bits() == g2.to_bits()
146 && b1.to_bits() == b2.to_bits()
147 && a1.to_bits() == a2.to_bits()
148 }
149 _ => false,
150 }
151 }
152}
153
154impl Eq for CanvasColor {}
155
156impl core::hash::Hash for CanvasColor {
157 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
158 core::mem::discriminant(self).hash(state);
159 match self {
160 Self::Transparent => {}
161 Self::Srgb { r, g, b, a } => {
162 r.hash(state);
163 g.hash(state);
164 b.hash(state);
165 a.hash(state);
166 }
167 Self::Linear { r, g, b, a } => {
168 r.to_bits().hash(state);
169 g.to_bits().hash(state);
170 b.to_bits().hash(state);
171 a.to_bits().hash(state);
172 }
173 }
174 }
175}
176
177impl CanvasColor {
178 pub const fn white() -> Self {
180 Self::Srgb {
181 r: 255,
182 g: 255,
183 b: 255,
184 a: 255,
185 }
186 }
187
188 pub const fn black() -> Self {
190 Self::Srgb {
191 r: 0,
192 g: 0,
193 b: 0,
194 a: 255,
195 }
196 }
197}
198
199#[non_exhaustive]
209#[derive(Copy, Clone, Debug, PartialEq)]
210pub enum SourceCrop {
211 Pixels(Rect),
213 Percent {
217 x: f32,
218 y: f32,
219 width: f32,
220 height: f32,
221 },
222}
223
224impl SourceCrop {
225 pub fn pixels(x: u32, y: u32, width: u32, height: u32) -> Self {
227 Self::Pixels(Rect {
228 x,
229 y,
230 width,
231 height,
232 })
233 }
234
235 pub fn percent(x: f32, y: f32, width: f32, height: f32) -> Self {
240 Self::Percent {
241 x,
242 y,
243 width,
244 height,
245 }
246 }
247
248 pub fn margin_percent(margin: f32) -> Self {
253 Self::Percent {
254 x: margin,
255 y: margin,
256 width: (1.0 - 2.0 * margin).max(0.0),
257 height: (1.0 - 2.0 * margin).max(0.0),
258 }
259 }
260
261 pub fn margins_percent(top: f32, right: f32, bottom: f32, left: f32) -> Self {
265 Self::Percent {
266 x: left,
267 y: top,
268 width: (1.0 - left - right).max(0.0),
269 height: (1.0 - top - bottom).max(0.0),
270 }
271 }
272
273 pub fn resolve(&self, source_w: u32, source_h: u32) -> Rect {
275 match *self {
276 Self::Pixels(r) => r.clamp_to(source_w, source_h),
277 Self::Percent {
278 x,
279 y,
280 width,
281 height,
282 } => {
283 let px = (source_w as f64 * x.clamp(0.0, 1.0) as f64).round_() as u32;
284 let py = (source_h as f64 * y.clamp(0.0, 1.0) as f64).round_() as u32;
285 let pw = (source_w as f64 * width.clamp(0.0, 1.0) as f64).round_() as u32;
286 let ph = (source_h as f64 * height.clamp(0.0, 1.0) as f64).round_() as u32;
287 Rect {
288 x: px,
289 y: py,
290 width: pw,
291 height: ph,
292 }
293 .clamp_to(source_w, source_h)
294 }
295 }
296 }
297}
298
299#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
301pub struct Size {
302 pub width: u32,
304 pub height: u32,
306}
307
308impl Size {
309 pub const fn new(width: u32, height: u32) -> Self {
311 Self { width, height }
312 }
313}
314
315#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
317pub struct Rect {
318 pub x: u32,
320 pub y: u32,
322 pub width: u32,
324 pub height: u32,
326}
327
328impl Rect {
329 pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
331 Self {
332 x,
333 y,
334 width,
335 height,
336 }
337 }
338
339 pub fn clamp_to(self, max_w: u32, max_h: u32) -> Self {
342 let x = self.x.min(max_w.saturating_sub(1));
343 let y = self.y.min(max_h.saturating_sub(1));
344 let w = self.width.min(max_w.saturating_sub(x)).max(1);
345 let h = self.height.min(max_h.saturating_sub(y)).max(1);
346 Self {
347 x,
348 y,
349 width: w,
350 height: h,
351 }
352 }
353
354 pub fn is_full(&self, source_w: u32, source_h: u32) -> bool {
356 self.x == 0 && self.y == 0 && self.width == source_w && self.height == source_h
357 }
358}
359
360#[non_exhaustive]
380#[derive(Clone, Debug, PartialEq)]
381pub struct Constraint {
382 pub mode: ConstraintMode,
384 pub width: Option<u32>,
386 pub height: Option<u32>,
388 pub gravity: Gravity,
390 pub canvas_color: CanvasColor,
392 pub source_crop: Option<SourceCrop>,
394}
395
396impl Constraint {
397 pub fn new(mode: ConstraintMode, width: u32, height: u32) -> Self {
399 Self {
400 mode,
401 width: Some(width),
402 height: Some(height),
403 gravity: Gravity::Center,
404 canvas_color: CanvasColor::Transparent,
405 source_crop: None,
406 }
407 }
408
409 pub fn width_only(mode: ConstraintMode, width: u32) -> Self {
411 Self {
412 mode,
413 width: Some(width),
414 height: None,
415 gravity: Gravity::Center,
416 canvas_color: CanvasColor::Transparent,
417 source_crop: None,
418 }
419 }
420
421 pub fn height_only(mode: ConstraintMode, height: u32) -> Self {
423 Self {
424 mode,
425 width: None,
426 height: Some(height),
427 gravity: Gravity::Center,
428 canvas_color: CanvasColor::Transparent,
429 source_crop: None,
430 }
431 }
432
433 pub fn gravity(mut self, gravity: Gravity) -> Self {
435 self.gravity = gravity;
436 self
437 }
438
439 pub fn canvas_color(mut self, color: CanvasColor) -> Self {
441 self.canvas_color = color;
442 self
443 }
444
445 pub fn source_crop(mut self, crop: SourceCrop) -> Self {
454 self.source_crop = Some(crop);
455 self
456 }
457
458 fn validate_floats(&self) -> Result<(), LayoutError> {
460 if let Gravity::Percentage(x, y) = self.gravity
461 && (!x.is_finite() || !y.is_finite())
462 {
463 return Err(LayoutError::NonFiniteFloat);
464 }
465 if let CanvasColor::Linear { r, g, b, a } = self.canvas_color
466 && (!r.is_finite() || !g.is_finite() || !b.is_finite() || !a.is_finite())
467 {
468 return Err(LayoutError::NonFiniteFloat);
469 }
470 if let Some(SourceCrop::Percent {
471 x,
472 y,
473 width,
474 height,
475 }) = self.source_crop
476 && (!x.is_finite() || !y.is_finite() || !width.is_finite() || !height.is_finite())
477 {
478 return Err(LayoutError::NonFiniteFloat);
479 }
480 Ok(())
481 }
482
483 pub fn compute(&self, source_w: u32, source_h: u32) -> Result<Layout, LayoutError> {
485 if source_w == 0 || source_h == 0 {
486 return Err(LayoutError::ZeroSourceDimension);
487 }
488
489 self.validate_floats()?;
491
492 let (user_crop, sw, sh) = match &self.source_crop {
494 Some(crop) => {
495 let r = crop.resolve(source_w, source_h);
496 (Some(r), r.width, r.height)
497 }
498 None => (None, source_w, source_h),
499 };
500
501 let (tw, th) = self.resolve_target(sw, sh)?;
503
504 use ConstraintMode::*;
512 let single_axis = self.width.is_none() || self.height.is_none();
513 if single_axis {
514 let no_upscale = matches!(self.mode, Within | WithinCrop | WithinPad | PadWithin);
515 let (rw, rh) = if self.mode == AspectCrop {
516 (sw, sh)
519 } else if no_upscale && sw <= tw && sh <= th {
520 (sw, sh)
521 } else {
522 (tw, th)
523 };
524 let (canvas, placement) = match self.mode {
525 FitPad | WithinPad | PadWithin => {
526 let (px, py) = gravity_offset(tw, th, rw, rh, &self.gravity);
527 ((tw, th), (px, py))
528 }
529 _ => ((rw, rh), (0, 0)),
530 };
531 return Ok(Layout {
532 source: Size::new(source_w, source_h),
533 source_crop: user_crop,
534 resize_to: Size::new(rw, rh),
535 canvas: Size::new(canvas.0, canvas.1),
536 placement,
537 canvas_color: self.canvas_color,
538 }
539 .normalize());
540 }
541
542 let layout = match self.mode {
544 Distort => Layout {
545 source: Size::new(source_w, source_h),
546 source_crop: user_crop,
547 resize_to: Size::new(tw, th),
548 canvas: Size::new(tw, th),
549 placement: (0, 0),
550 canvas_color: self.canvas_color,
551 },
552
553 Fit => {
554 let (rw, rh) = fit_inside(sw, sh, tw, th);
555 Layout {
556 source: Size::new(source_w, source_h),
557 source_crop: user_crop,
558 resize_to: Size::new(rw, rh),
559 canvas: Size::new(rw, rh),
560 placement: (0, 0),
561 canvas_color: self.canvas_color,
562 }
563 }
564
565 Within => {
566 let (rw, rh) = if sw <= tw && sh <= th {
567 (sw, sh)
568 } else {
569 fit_inside(sw, sh, tw, th)
570 };
571 Layout {
572 source: Size::new(source_w, source_h),
573 source_crop: user_crop,
574 resize_to: Size::new(rw, rh),
575 canvas: Size::new(rw, rh),
576 placement: (0, 0),
577 canvas_color: self.canvas_color,
578 }
579 }
580
581 FitCrop => {
582 let aspect_crop = crop_to_aspect(sw, sh, tw, th, &self.gravity);
583 let combined = combine_crops(user_crop, aspect_crop);
584 Layout {
585 source: Size::new(source_w, source_h),
586 source_crop: Some(combined),
587 resize_to: Size::new(tw, th),
588 canvas: Size::new(tw, th),
589 placement: (0, 0),
590 canvas_color: self.canvas_color,
591 }
592 }
593
594 WithinCrop => {
595 if sw <= tw && sh <= th {
599 Layout {
601 source: Size::new(source_w, source_h),
602 source_crop: user_crop,
603 resize_to: Size::new(sw, sh),
604 canvas: Size::new(sw, sh),
605 placement: (0, 0),
606 canvas_color: self.canvas_color,
607 }
608 } else if sw >= tw && sh >= th {
609 let aspect_crop = crop_to_aspect(sw, sh, tw, th, &self.gravity);
611 let combined = combine_crops(user_crop, aspect_crop);
612 Layout {
613 source: Size::new(source_w, source_h),
614 source_crop: Some(combined),
615 resize_to: Size::new(tw, th),
616 canvas: Size::new(tw, th),
617 placement: (0, 0),
618 canvas_color: self.canvas_color,
619 }
620 } else {
621 let rw = sw.min(tw);
623 let rh = sh.min(th);
624 let crop = if rw < sw || rh < sh {
625 let x = if rw < sw {
626 gravity_offset_1d(sw - rw, &self.gravity, true)
627 } else {
628 0
629 };
630 let y = if rh < sh {
631 gravity_offset_1d(sh - rh, &self.gravity, false)
632 } else {
633 0
634 };
635 let r = Rect::new(x, y, rw, rh);
636 Some(combine_crops(user_crop, r))
637 } else {
638 user_crop
639 };
640 Layout {
641 source: Size::new(source_w, source_h),
642 source_crop: crop,
643 resize_to: Size::new(rw, rh),
644 canvas: Size::new(rw, rh),
645 placement: (0, 0),
646 canvas_color: self.canvas_color,
647 }
648 }
649 }
650
651 FitPad => {
652 let (rw, rh) = fit_inside(sw, sh, tw, th);
653 let (px, py) = gravity_offset(tw, th, rw, rh, &self.gravity);
654 Layout {
655 source: Size::new(source_w, source_h),
656 source_crop: user_crop,
657 resize_to: Size::new(rw, rh),
658 canvas: Size::new(tw, th),
659 placement: (px, py),
660 canvas_color: self.canvas_color,
661 }
662 }
663
664 WithinPad => {
665 if sw <= tw && sh <= th {
670 Layout {
671 source: Size::new(source_w, source_h),
672 source_crop: user_crop,
673 resize_to: Size::new(sw, sh),
674 canvas: Size::new(sw, sh),
675 placement: (0, 0),
676 canvas_color: self.canvas_color,
677 }
678 } else {
679 let (rw, rh) = fit_inside(sw, sh, tw, th);
680 let (px, py) = gravity_offset(tw, th, rw, rh, &self.gravity);
681 Layout {
682 source: Size::new(source_w, source_h),
683 source_crop: user_crop,
684 resize_to: Size::new(rw, rh),
685 canvas: Size::new(tw, th),
686 placement: (px, py),
687 canvas_color: self.canvas_color,
688 }
689 }
690 }
691
692 PadWithin => {
693 let (rw, rh) = if sw <= tw && sh <= th {
696 (sw, sh) } else {
698 fit_inside(sw, sh, tw, th) };
700 let (px, py) = gravity_offset(tw, th, rw, rh, &self.gravity);
701 Layout {
702 source: Size::new(source_w, source_h),
703 source_crop: user_crop,
704 resize_to: Size::new(rw, rh),
705 canvas: Size::new(tw, th),
706 placement: (px, py),
707 canvas_color: self.canvas_color,
708 }
709 }
710
711 AspectCrop => {
712 let aspect_crop = crop_to_aspect(sw, sh, tw, th, &self.gravity);
713 let combined = combine_crops(user_crop, aspect_crop);
714 Layout {
715 source: Size::new(source_w, source_h),
716 source_crop: Some(combined),
717 resize_to: Size::new(combined.width, combined.height),
718 canvas: Size::new(combined.width, combined.height),
719 placement: (0, 0),
720 canvas_color: self.canvas_color,
721 }
722 }
723 };
724
725 Ok(layout.normalize())
727 }
728
729 fn resolve_target(&self, sw: u32, sh: u32) -> Result<(u32, u32), LayoutError> {
734 match (self.width, self.height) {
735 (Some(w), Some(h)) if w == 0 || h == 0 => Err(LayoutError::ZeroTargetDimension),
736 (Some(w), Some(h)) => Ok((w, h)),
737 (Some(0), None) => Err(LayoutError::ZeroTargetDimension),
738 (Some(w), None) => {
739 let h = (sh as f64 * w as f64 / sw as f64).round_().max(1.0) as u32;
740 Ok((w, h))
741 }
742 (None, Some(0)) => Err(LayoutError::ZeroTargetDimension),
743 (None, Some(h)) => {
744 let w = (sw as f64 * h as f64 / sh as f64).round_().max(1.0) as u32;
745 Ok((w, h))
746 }
747 (None, None) => Ok((sw, sh)),
748 }
749 }
750}
751
752#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
778#[non_exhaustive]
779pub struct Layout {
780 pub source: Size,
782 pub source_crop: Option<Rect>,
784 pub resize_to: Size,
786 pub canvas: Size,
788 pub placement: (i32, i32),
790 pub canvas_color: CanvasColor,
792}
793
794impl Layout {
795 pub fn needs_resize(&self) -> bool {
797 let eff = self.effective_source();
798 self.resize_to != eff
799 }
800
801 pub fn needs_padding(&self) -> bool {
803 self.canvas != self.resize_to
804 }
805
806 pub fn needs_crop(&self) -> bool {
808 self.source_crop.is_some()
809 }
810
811 pub fn effective_source(&self) -> Size {
813 match &self.source_crop {
814 Some(r) => Size::new(r.width, r.height),
815 None => self.source,
816 }
817 }
818
819 fn normalize(mut self) -> Self {
821 if let Some(r) = &self.source_crop
822 && r.is_full(self.source.width, self.source.height)
823 {
824 self.source_crop = None;
825 }
826 self
827 }
828}
829
830#[non_exhaustive]
832#[derive(Copy, Clone, Debug, PartialEq, Eq)]
833pub enum LayoutError {
834 ZeroSourceDimension,
836 ZeroTargetDimension,
838 ZeroRegionDimension,
840 NonFiniteFloat,
842}
843
844impl core::fmt::Display for LayoutError {
845 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
846 match self {
847 Self::ZeroSourceDimension => f.write_str("source image has zero width or height"),
848 Self::ZeroTargetDimension => f.write_str("target width or height is zero"),
849 Self::ZeroRegionDimension => {
850 f.write_str("region viewport has zero or negative width or height")
851 }
852 Self::NonFiniteFloat => {
853 f.write_str("a float parameter contains NaN or infinity")
854 }
855 }
856 }
857}
858
859#[cfg(feature = "std")]
860impl std::error::Error for LayoutError {}
861
862fn fit_inside(sw: u32, sh: u32, tw: u32, th: u32) -> (u32, u32) {
869 let ratio_w = tw as f64 / sw as f64;
870 let ratio_h = th as f64 / sh as f64;
871 if ratio_w <= ratio_h {
872 let h = proportional(sw, sh, tw, true, tw, th);
874 (tw, h)
875 } else {
876 let w = proportional(sw, sh, th, false, tw, th);
878 (w, th)
879 }
880}
881
882fn crop_to_aspect(sw: u32, sh: u32, tw: u32, th: u32, gravity: &Gravity) -> Rect {
884 let cross_s = sw as u64 * th as u64;
886 let cross_t = sh as u64 * tw as u64;
887 if cross_s == cross_t {
888 return Rect {
889 x: 0,
890 y: 0,
891 width: sw,
892 height: sh,
893 };
894 }
895
896 let target_ratio = tw as f64 / th as f64;
897 let source_ratio = sw as f64 / sh as f64;
898
899 if source_ratio > target_ratio {
900 let new_w = proportional(tw, th, sh, false, sw, sh);
904 if new_w >= sw {
905 return Rect {
906 x: 0,
907 y: 0,
908 width: sw,
909 height: sh,
910 };
911 }
912 let x = gravity_offset_1d(sw - new_w, gravity, true);
913 Rect {
914 x,
915 y: 0,
916 width: new_w,
917 height: sh,
918 }
919 } else {
920 let new_h = proportional(tw, th, sw, true, sw, sh);
923 if new_h >= sh {
924 return Rect {
925 x: 0,
926 y: 0,
927 width: sw,
928 height: sh,
929 };
930 }
931 let y = gravity_offset_1d(sh - new_h, gravity, false);
932 Rect {
933 x: 0,
934 y,
935 width: sw,
936 height: new_h,
937 }
938 }
939}
940
941fn combine_crops(user_crop: Option<Rect>, constraint_crop: Rect) -> Rect {
944 match user_crop {
945 None => constraint_crop,
946 Some(uc) => Rect {
947 x: uc.x + constraint_crop.x,
948 y: uc.y + constraint_crop.y,
949 width: constraint_crop
950 .width
951 .min(uc.width.saturating_sub(constraint_crop.x)),
952 height: constraint_crop
953 .height
954 .min(uc.height.saturating_sub(constraint_crop.y)),
955 },
956 }
957}
958
959fn gravity_offset(cw: u32, ch: u32, iw: u32, ih: u32, gravity: &Gravity) -> (i32, i32) {
961 let x = gravity_offset_1d(cw.saturating_sub(iw), gravity, true);
962 let y = gravity_offset_1d(ch.saturating_sub(ih), gravity, false);
963 (x as i32, y as i32)
964}
965
966fn gravity_offset_1d(space: u32, gravity: &Gravity, horizontal: bool) -> u32 {
967 if space == 0 {
968 return 0;
969 }
970 match gravity {
971 Gravity::Center => space / 2,
972 Gravity::Percentage(x, y) => {
973 let pct = if horizontal { *x } else { *y };
974 (space as f64 * pct.clamp(0.0, 1.0) as f64).round_() as u32
975 }
976 }
977}
978
979fn proportional(
990 ratio_w: u32,
991 ratio_h: u32,
992 basis: u32,
993 basis_is_width: bool,
994 target_w: u32,
995 target_h: u32,
996) -> u32 {
997 let ratio = ratio_w as f64 / ratio_h as f64;
998
999 let snap_amount = if basis_is_width {
1001 rounding_loss_height(ratio_w, ratio_h, target_h)
1002 } else {
1003 rounding_loss_width(ratio_w, ratio_h, target_w)
1004 };
1005
1006 let snap_a = if basis_is_width { ratio_h } else { ratio_w };
1008 let snap_b = if basis_is_width { target_h } else { target_w };
1010
1011 let float = if basis_is_width {
1013 basis as f64 / ratio
1014 } else {
1015 ratio * basis as f64
1016 };
1017
1018 let delta_a = (float - snap_a as f64).abs();
1019 let delta_b = (float - snap_b as f64).abs();
1020
1021 let v = if delta_a <= snap_amount && delta_a <= delta_b {
1022 snap_a
1023 } else if delta_b <= snap_amount {
1024 snap_b
1025 } else {
1026 float.round_() as u32
1027 };
1028
1029 if v == 0 { 1 } else { v }
1030}
1031
1032fn rounding_loss_width(ratio_w: u32, ratio_h: u32, target_width: u32) -> f64 {
1035 let ratio = ratio_w as f64 / ratio_h as f64;
1036 let target_x_to_self_x = target_width as f64 / ratio_w as f64;
1037 let recreate_y = ratio_h as f64 * target_x_to_self_x;
1038 let rounded_y = recreate_y.round_();
1039 let recreate_x_from_rounded_y = rounded_y * ratio;
1040 (target_width as f64 - recreate_x_from_rounded_y).abs()
1041}
1042
1043fn rounding_loss_height(ratio_w: u32, ratio_h: u32, target_height: u32) -> f64 {
1046 let ratio = ratio_w as f64 / ratio_h as f64;
1047 let target_y_to_self_y = target_height as f64 / ratio_h as f64;
1048 let recreate_x = ratio_w as f64 * target_y_to_self_y;
1049 let rounded_x = recreate_x.round_();
1050 let recreate_y_from_rounded_x = rounded_x / ratio;
1051 (target_height as f64 - recreate_y_from_rounded_x).abs()
1052}
1053
1054#[cfg(test)]
1055mod tests {
1056 use super::*;
1057
1058 extern crate alloc;
1059 use alloc::format;
1060 use alloc::string::String;
1061 use alloc::vec;
1062 use alloc::vec::Vec;
1063
1064 #[test]
1067 fn fit_inside_landscape_into_landscape() {
1068 assert_eq!(fit_inside(1000, 500, 400, 300), (400, 200));
1070 }
1071
1072 #[test]
1073 fn fit_inside_portrait_into_landscape() {
1074 assert_eq!(fit_inside(500, 1000, 400, 300), (150, 300));
1076 }
1077
1078 #[test]
1079 fn fit_inside_same_aspect() {
1080 assert_eq!(fit_inside(1000, 500, 400, 200), (400, 200));
1082 }
1083
1084 #[test]
1085 fn fit_inside_snap_rounding() {
1086 assert_eq!(fit_inside(1200, 400, 100, 33), (100, 33));
1090 }
1091
1092 #[test]
1093 fn fit_inside_square() {
1094 assert_eq!(fit_inside(1000, 500, 200, 200), (200, 100));
1095 }
1096
1097 #[test]
1100 fn crop_aspect_wider_source() {
1101 let r = crop_to_aspect(1000, 500, 400, 300, &Gravity::Center);
1103 assert_eq!(r.width, 667);
1105 assert_eq!(r.height, 500);
1106 assert_eq!(r.x, 166);
1108 assert_eq!(r.y, 0);
1109 }
1110
1111 #[test]
1112 fn crop_aspect_taller_source() {
1113 let r = crop_to_aspect(500, 1000, 400, 300, &Gravity::Center);
1115 assert_eq!(r.width, 500);
1116 assert_eq!(r.height, 375);
1118 }
1119
1120 #[test]
1121 fn crop_aspect_same_ratio() {
1122 let r = crop_to_aspect(800, 600, 400, 300, &Gravity::Center);
1123 assert_eq!(
1124 r,
1125 Rect {
1126 x: 0,
1127 y: 0,
1128 width: 800,
1129 height: 600
1130 }
1131 );
1132 }
1133
1134 #[test]
1135 fn crop_aspect_gravity_top_left() {
1136 let r = crop_to_aspect(1000, 500, 400, 300, &Gravity::Percentage(0.0, 0.0));
1137 assert_eq!(r.x, 0);
1138 assert_eq!(r.y, 0);
1139 }
1140
1141 #[test]
1142 fn crop_aspect_gravity_bottom_right() {
1143 let r = crop_to_aspect(1000, 500, 400, 300, &Gravity::Percentage(1.0, 1.0));
1144 assert_eq!(r.x, 1000 - r.width);
1145 assert_eq!(r.y, 0); }
1147
1148 #[test]
1151 fn distort_ignores_aspect() {
1152 let l = Constraint::new(ConstraintMode::Distort, 400, 300)
1153 .compute(1000, 500)
1154 .unwrap();
1155 assert_eq!(l.resize_to, Size::new(400, 300));
1156 assert_eq!(l.canvas, Size::new(400, 300));
1157 assert!(l.source_crop.is_none());
1158 }
1159
1160 #[test]
1163 fn fit_downscale() {
1164 let l = Constraint::new(ConstraintMode::Fit, 400, 300)
1165 .compute(1000, 500)
1166 .unwrap();
1167 assert_eq!(l.resize_to, Size::new(400, 200));
1168 assert_eq!(l.canvas, Size::new(400, 200));
1169 assert!(!l.needs_padding());
1170 }
1171
1172 #[test]
1173 fn fit_upscale() {
1174 let l = Constraint::new(ConstraintMode::Fit, 400, 300)
1175 .compute(200, 100)
1176 .unwrap();
1177 assert_eq!(l.resize_to, Size::new(400, 200));
1178 }
1179
1180 #[test]
1183 fn within_no_upscale() {
1184 let l = Constraint::new(ConstraintMode::Within, 400, 300)
1185 .compute(200, 100)
1186 .unwrap();
1187 assert_eq!(l.resize_to, Size::new(200, 100));
1189 assert!(!l.needs_resize());
1190 }
1191
1192 #[test]
1193 fn within_downscale() {
1194 let l = Constraint::new(ConstraintMode::Within, 400, 300)
1195 .compute(1000, 500)
1196 .unwrap();
1197 assert_eq!(l.resize_to, Size::new(400, 200));
1198 }
1199
1200 #[test]
1203 fn fit_crop_exact_dimensions() {
1204 let l = Constraint::new(ConstraintMode::FitCrop, 400, 300)
1205 .compute(1000, 500)
1206 .unwrap();
1207 assert_eq!(l.resize_to, Size::new(400, 300));
1208 assert_eq!(l.canvas, Size::new(400, 300));
1209 assert!(l.source_crop.is_some());
1210 let crop = l.source_crop.unwrap();
1211 assert_eq!(crop.height, 500);
1213 assert!(crop.width > 650 && crop.width < 680);
1215 }
1216
1217 #[test]
1218 fn fit_crop_same_aspect() {
1219 let l = Constraint::new(ConstraintMode::FitCrop, 400, 200)
1220 .compute(1000, 500)
1221 .unwrap();
1222 assert_eq!(l.resize_to, Size::new(400, 200));
1223 assert!(!l.needs_crop());
1225 assert!(l.source_crop.is_none());
1226 }
1227
1228 #[test]
1231 fn within_crop_no_upscale() {
1232 let l = Constraint::new(ConstraintMode::WithinCrop, 400, 300)
1233 .compute(200, 100)
1234 .unwrap();
1235 assert_eq!(l.resize_to, Size::new(200, 100));
1237 assert_eq!(l.canvas, Size::new(200, 100));
1238 assert!(l.source_crop.is_none());
1239 }
1240
1241 #[test]
1244 fn fit_pad_adds_padding() {
1245 let l = Constraint::new(ConstraintMode::FitPad, 400, 300)
1246 .canvas_color(CanvasColor::white())
1247 .compute(1000, 500)
1248 .unwrap();
1249 assert_eq!(l.resize_to, Size::new(400, 200));
1250 assert_eq!(l.canvas, Size::new(400, 300));
1251 assert_eq!(l.placement, (0, 50)); assert!(l.needs_padding());
1253 }
1254
1255 #[test]
1256 fn fit_pad_no_padding_when_aspect_matches() {
1257 let l = Constraint::new(ConstraintMode::FitPad, 400, 200)
1258 .compute(1000, 500)
1259 .unwrap();
1260 assert_eq!(l.resize_to, Size::new(400, 200));
1261 assert_eq!(l.canvas, Size::new(400, 200));
1262 assert!(!l.needs_padding());
1263 }
1264
1265 #[test]
1268 fn within_pad_canvas_expand() {
1269 let l = Constraint::new(ConstraintMode::WithinPad, 400, 300)
1271 .canvas_color(CanvasColor::white())
1272 .compute(200, 100)
1273 .unwrap();
1274 assert_eq!(l.resize_to, Size::new(200, 100));
1275 assert_eq!(l.canvas, Size::new(200, 100));
1276 assert!(!l.needs_resize());
1277 assert!(!l.needs_padding());
1278 }
1279
1280 #[test]
1281 fn within_pad_downscale_and_pad() {
1282 let l = Constraint::new(ConstraintMode::WithinPad, 400, 300)
1283 .compute(1000, 500)
1284 .unwrap();
1285 assert_eq!(l.resize_to, Size::new(400, 200));
1286 assert_eq!(l.canvas, Size::new(400, 300));
1287 assert_eq!(l.placement, (0, 50));
1288 }
1289
1290 #[test]
1293 fn aspect_crop_no_scaling() {
1294 let l = Constraint::new(ConstraintMode::AspectCrop, 400, 300)
1295 .compute(1000, 500)
1296 .unwrap();
1297 let crop = l.source_crop.unwrap();
1298 assert_eq!(l.resize_to, Size::new(crop.width, crop.height));
1300 assert!(!l.needs_resize());
1301 }
1302
1303 #[test]
1306 fn source_crop_pixels() {
1307 let l = Constraint::new(ConstraintMode::Fit, 200, 200)
1308 .source_crop(SourceCrop::pixels(100, 100, 500, 500))
1309 .compute(1000, 1000)
1310 .unwrap();
1311 assert_eq!(
1312 l.source_crop,
1313 Some(Rect {
1314 x: 100,
1315 y: 100,
1316 width: 500,
1317 height: 500
1318 })
1319 );
1320 assert_eq!(l.resize_to, Size::new(200, 200));
1321 }
1322
1323 #[test]
1324 fn source_crop_percent() {
1325 let l = Constraint::new(ConstraintMode::Fit, 200, 200)
1326 .source_crop(SourceCrop::percent(0.25, 0.25, 0.5, 0.5))
1327 .compute(1000, 1000)
1328 .unwrap();
1329 assert_eq!(
1330 l.source_crop,
1331 Some(Rect {
1332 x: 250,
1333 y: 250,
1334 width: 500,
1335 height: 500
1336 })
1337 );
1338 }
1339
1340 #[test]
1341 fn source_crop_combined_with_fit_crop() {
1342 let l = Constraint::new(ConstraintMode::FitCrop, 400, 300)
1344 .source_crop(SourceCrop::percent(0.25, 0.25, 0.5, 0.5))
1345 .compute(1000, 1000)
1346 .unwrap();
1347 let crop = l.source_crop.unwrap();
1348 assert_eq!(crop.width, 500);
1351 assert_eq!(crop.height, 375);
1352 assert_eq!(crop.x, 250);
1354 assert!(crop.y > 250);
1355 }
1356
1357 #[test]
1360 fn margin_percent_symmetric() {
1361 let crop = SourceCrop::margin_percent(0.1);
1362 let r = crop.resolve(1000, 500);
1363 assert_eq!(
1364 r,
1365 Rect {
1366 x: 100,
1367 y: 50,
1368 width: 800,
1369 height: 400
1370 }
1371 );
1372 }
1373
1374 #[test]
1375 fn margins_percent_asymmetric() {
1376 let crop = SourceCrop::margins_percent(0.1, 0.2, 0.1, 0.2);
1378 let r = crop.resolve(1000, 500);
1379 assert_eq!(
1380 r,
1381 Rect {
1382 x: 200,
1383 y: 50,
1384 width: 600,
1385 height: 400
1386 }
1387 );
1388 }
1389
1390 #[test]
1391 fn rect_is_full() {
1392 assert!(Rect::new(0, 0, 100, 100).is_full(100, 100));
1393 assert!(!Rect::new(1, 0, 99, 100).is_full(100, 100));
1394 assert!(!Rect::new(0, 0, 99, 100).is_full(100, 100));
1395 }
1396
1397 #[test]
1400 fn width_only_computes_height() {
1401 let l = Constraint::width_only(ConstraintMode::Fit, 500)
1402 .compute(1000, 600)
1403 .unwrap();
1404 assert_eq!(l.resize_to, Size::new(500, 300));
1405 }
1406
1407 #[test]
1408 fn height_only_computes_width() {
1409 let l = Constraint::height_only(ConstraintMode::Fit, 300)
1410 .compute(1000, 600)
1411 .unwrap();
1412 assert_eq!(l.resize_to, Size::new(500, 300));
1413 }
1414
1415 #[test]
1416 fn width_only_fit_crop_no_crop() {
1417 let l = Constraint::width_only(ConstraintMode::FitCrop, 500)
1419 .compute(1000, 600)
1420 .unwrap();
1421 assert!(!l.needs_crop());
1422 assert!(l.source_crop.is_none());
1423 }
1424
1425 #[test]
1428 fn gravity_top_left_pad() {
1429 let l = Constraint::new(ConstraintMode::FitPad, 400, 400)
1430 .gravity(Gravity::Percentage(0.0, 0.0))
1431 .compute(1000, 500)
1432 .unwrap();
1433 assert_eq!(l.placement, (0, 0));
1434 }
1435
1436 #[test]
1437 fn gravity_bottom_right_pad() {
1438 let l = Constraint::new(ConstraintMode::FitPad, 400, 400)
1439 .gravity(Gravity::Percentage(1.0, 1.0))
1440 .compute(1000, 500)
1441 .unwrap();
1442 assert_eq!(l.resize_to, Size::new(400, 200));
1443 assert_eq!(l.placement, (0, 200));
1444 }
1445
1446 #[test]
1449 fn zero_source_errors() {
1450 assert_eq!(
1451 Constraint::new(ConstraintMode::Fit, 100, 100).compute(0, 100),
1452 Err(LayoutError::ZeroSourceDimension)
1453 );
1454 }
1455
1456 #[test]
1457 fn zero_target_errors() {
1458 assert_eq!(
1459 Constraint::new(ConstraintMode::Fit, 0, 100).compute(100, 100),
1460 Err(LayoutError::ZeroTargetDimension)
1461 );
1462 }
1463
1464 #[test]
1467 fn rect_clamp_oversized() {
1468 let r = Rect {
1469 x: 900,
1470 y: 900,
1471 width: 500,
1472 height: 500,
1473 };
1474 let c = r.clamp_to(1000, 1000);
1475 assert_eq!(
1476 c,
1477 Rect {
1478 x: 900,
1479 y: 900,
1480 width: 100,
1481 height: 100
1482 }
1483 );
1484 }
1485
1486 #[test]
1487 fn rect_clamp_zero_width() {
1488 let r = Rect {
1489 x: 1000,
1490 y: 0,
1491 width: 0,
1492 height: 100,
1493 };
1494 let c = r.clamp_to(1000, 1000);
1495 assert!(c.width >= 1);
1496 assert!(c.x < 1000);
1497 }
1498
1499 #[test]
1502 fn needs_resize_false_for_identity() {
1503 let l = Constraint::new(ConstraintMode::Within, 1000, 1000)
1504 .compute(500, 300)
1505 .unwrap();
1506 assert!(!l.needs_resize());
1507 }
1508
1509 #[test]
1510 fn needs_padding_true_for_pad() {
1511 let l = Constraint::new(ConstraintMode::FitPad, 400, 400)
1512 .compute(1000, 500)
1513 .unwrap();
1514 assert!(l.needs_padding());
1515 }
1516
1517 #[rustfmt::skip]
1522 static SHRINK_WITHIN_TESTS: [(i32, i32, i32, i32); 1185] = [(1399,697, 280, -1),(1399,689, 200, -1),(1399,685, 193, -1),(1399,683, 212, -1),(1399,673, 396, -1),(1399,671, 270, -1),(1399,665, 365, -1),(1399,659, 190, -1),(1399,656, 193, -1),(1399,652, 162, -1),(1399,643, 260, -1),(1399,643, 260, -1),(1399,637, 291, -1),(1399,628, 362, -1),(1399,628, 362, -1),(1399,622, 343, -1),(1399,614, 270, -1),(1399,614, 270, -1),(1399,607, 363, -1),(1399,600, 232, -1),(1399,600, 232, -1),(1399,594, 305, -1),(1399,587, 342, -1),(1399,585, 391, -1),(1399,582, 256, -1),(1399,577, 217, -1),(1399,569, 193, -1),(1399,568, 383, -1),(1399,564, 222, -1),(1399,560, 346, -1),(1399,556, 39, -1),(1399,554, 125, -1),(1399,551, 179, -1),(1399,545, 163, -1),(1399,540, 307, -1),(1399,537, 353, -1),(1399,534, 93, -1),(1399,530, 260, -1),(1399,526, 254, -1),(1399,526, 254, -1),(1399,520, 265, -1),(1399,516, 61, -1),(1399,512, 291, -1),(1399,512, 97, -1),(1399,508, 263, -1),(1399,500, 270, -1),(1399,497, 190, -1),(1399,497, 114, -1),(1399,493, 271, -1),(1399,489, 216, -1),(1399,481, 397, -1),(1399,480, 290, -1),(1399,480, 290, -1),(1399,474, 152, -1),(1399,468, 139, -1),(1399,464, 300, -1),(1399,459, 32, -1),(1399,456, 158, -1),(1399,450, 300, -1),(1399,449, 148, -1),(1399,445, 11, -1),(1399,440, 310, -1),(1399,438, 107, -1),(1399,435, 320, -1),(1399,431, 297, -1),(1399,427, 172, -1),(1399,424, 325, -1),(1399,419, 202, -1),(1399,417, 52, -1),(1399,413, 188, -1),(1399,408, 108, -1),(1399,406, 143, -1),(1399,401, 232, -1),(1399,397, 259, -1),(1399,394, 158, -1),(1399,392, 298, -1),(1399,389, 196, -1),(1399,387, 338, -1),(1399,384, 388, -1),(1399,380, 289, -1),(1399,377, 154, -1),(1399,372, 220, -1),(1399,370, 259, -1),(1399,367, 223, -1),(1399,364, 98, -1),(1399,362, 114, -1),(1399,360, 68, -1),(1399,359, 189, -1),(1399,355, 333, -1),(1399,351, 277, -1),(1399,348, 203, -1),(1399,346, 374, -1),(1399,345, 221, -1),(1399,341, 240, -1),(1399,338, 387, -1),(1399,335, 332, -1),(1399,333, 355, -1),(1399,330, 248, -1),(1399,328, 386, -1),(1399,326, 324, -1),(1399,324, 326, -1),(1399,321, 146, -1),(1399,319, 182, -1),(1399,317, 267, -1),(1399,314, 274, -1),(1399,313, 257, -1),(1399,310, 264, -1),(1399,309, 206, -1),(1399,307, 180, -1),(1399,305, 383, -1),(1399,304, 237, -1),(1399,301, 244, -1),(1399,299, 386, -1),(1399,299, 255, -1),(1399,295, 377, -1),(1399,295, 230, -1),(1399,293, 74, -1),(1399,291, 387, -1),(1399,290, 41, -1),(1399,288, 85, -1),(1399,286, 203, -1),(1399,283, 393, -1),(1399,279, 183, -1),(1399,279, 178, -1),(1399,278, 234, -1),(1399,277, 250, -1),(1399,275, 379, -1),(1399,272, 162, -1),(1399,272, 54, -1),(1399,269, 169, -1),(1399,269, 91, -1),(1399,269, 13, -1),(1399,267, 317, -1),(1399,264, 310, -1),(1399,263, 125, -1),(1399,261, 335, -1),(1399,259, 397, -1),(1399,258, 122, -1),(1399,257, 313, -1),(1399,256, 194, -1),(1399,255, 299, -1),(1399,253, 235, -1),(1399,252, 297, -1),(1399,250, 277, -1),(1399,248, 330, -1),(1399,247, 337, -1),(1399,246, 327, -1),(1399,245, 197, -1),(1399,244, 43, -1),(1399,242, 211, -1),(1399,241, 357, -1),(1399,240, 341, -1),(1399,238, 385, -1),(1399,237, 304, -1),(1399,236, 329, -1),(1399,234, 278, -1),(1399,233, 9, -1),(1399,231, 324, -1),(1399,230, 295, -1),(1399,229, 168, -1),(1399,228, 316, -1),(1399,225, 115, -1),(1399,224, 153, -1),(1399,223, 367, -1),(1399,221, 345, -1),(1399,220, 124, -1),(1399,219, 214, -1),(1399,218, 369, -1),(1399,216, 204, -1),(1399,216, 68, -1),(1399,215, 244, -1),(1399,214, 219, -1),(1399,213, 266, -1),(1399,211, 242, -1),(1399,209, 251, -1),(1399,208, 380, -1),(1399,206, 309, -1),(1399,205, 58, -1),(1399,204, 120, -1),(1399,203, 286, -1),(1399,201, 261, -1),(1399,199, 355, -1),(1399,198, 378, -1),(1399,197, 316, -1),(1399,197, 245, -1),(1399,196, 389, -1),(1399,195, 391, -1),(1399,194, 256, -1),(1399,191, 260, -1),(1399,190, 335, -1),(1399,189, 396, -1),(1399,189, 359, -1),(1399,187, 288, -1),(1399,186, 267, -1),(1399,185, 397, -1),(1399,184, 19, -1),(1399,183, 172, -1),(1399,182, 319, -1),(1399,181, 228, -1),(1399,180, 307, -1),(1399,179, 254, -1),(1399,178, 279, -1),(1399,177, 328, -1),(1399,176, 155, -1),(1399,174, 205, -1),(1399,173, 376, -1),(1399,172, 305, -1),(1399,172, 61, -1),(1399,170, 144, -1),(1399,168, 229, -1),(1399,167, 289, -1),(1399,165, 284, -1),(1399,165, 89, -1),(1399,164, 354, -1),(1399,163, 339, -1),(1399,162, 367, -1),(1399,161, 265, -1),(1399,160, 153, -1),(1399,158, 394, -1),(1399,157, 147, -1),(1399,157, 49, -1),(1399,156, 139, -1),(1399,155, 176, -1),(1399,154, 377, -1),(1399,153, 224, -1),(1399,153, 96, -1),(1399,152, 69, -1),(1399,152, 23, -1),(1399,151, 88, -1),(1399,149, 399, -1),(1399,149, 61, -1),(1399,148, 345, -1),(1399,147, 157, -1),(1399,146, 321, -1),(1399,145, 82, -1),(1399,144, 306, -1),(1399,144, 170, -1),(1399,144, 34, -1),(1399,143, 44, -1),(1399,142, 399, -1),(1399,141, 253, -1),(1399,139, 317, -1),(1399,139, 156, -1),(1399,138, 370, -1),(1399,137, 291, -1),(1399,137, 97, -1),(1399,136, 324, -1),(1399,136, 180, -1),(1399,136, 36, -1),(1399,134, 214, -1),(1399,133, 163, -1),(1399,133, 142, -1),(1399,131, 315, -1),(1399,131, 283, -1),(1399,130, 382, -1),(1399,129, 244, -1),(1399,128, 388, -1),(1399,127, 369, -1),(1399,127, 358, -1),(1399,126, 272, -1),(1399,125, 263, -1),(1399,124, 220, -1),(1399,123, 381, -1),(1399,122, 258, -1),(1399,121, 237, -1),(1399,120, 204, -1),(1399,119, 335, -1),(1399,119, 288, -1),(1399,119, 241, -1),(1399,118, 326, -1),(1399,117, 269, -1),(1399,116, 211, -1),(1399,115, 371, -1),(1399,114, 362, -1),(1399,113, 229, -1),(1399,112, 331, -1),(1399,111, 397, -1),(1399,110, 337, -1),(1399,110, 248, -1),(1399,108, 395, -1),(1399,108, 136, -1),(1399,107, 268, -1),(1399,105, 393, -1),(1399,104, 343, -1),(1399,103, 292, -1),(1399,102, 336, -1),(1399,102, 144, -1),(1399,101, 367, -1),(1399,101, 90, -1),(1399,99, 332, -1),(1399,99, 219, -1),(1399,98, 364, -1),(1399,97, 310, -1),(1399,97, 137, -1),(1399,96, 357, -1),(1399,96, 255, -1),(1399,96, 153, -1),(1399,96, 51, -1),(1399,95, 346, -1),(1399,94, 305, -1),(1399,94, 186, -1),(1399,93, 203, -1),(1399,93, 188, -1),(1399,92, 190, -1),(1399,92, 114, -1),(1399,92, 38, -1),(1399,91, 392, -1),(1399,90, 272, -1),(1399,89, 275, -1),(1399,89, 165, -1),(1399,89, 55, -1),(1399,88, 310, -1),(1399,87, 217, -1),(1399,87, 201, -1),(1399,86, 366, -1),(1399,86, 122, -1),(1399,85, 288, -1),(1399,84, 358, -1),(1399,83, 396, -1),(1399,82, 179, -1),(1399,82, 162, -1),(1399,82, 145, -1),(1399,81, 354, -1),(1399,80, 341, -1),(1399,79, 363, -1),(1399,78, 278, -1),(1399,77, 227, -1),(1399,77, 118, -1),(1399,76, 322, -1),(1399,76, 230, -1),(1399,76, 138, -1),(1399,76, 46, -1),(1399,75, 345, -1),(1399,74, 293, -1),(1399,73, 297, -1),(1399,72, 340, -1),(1399,72, 204, -1),(1399,72, 68, -1),(1399,71, 325, -1),(1399,71, 266, -1),(1399,69, 375, -1),(1399,69, 152, -1),(1399,68, 360, -1),(1399,68, 216, -1),(1399,68, 72, -1),(1399,67, 261, -1),(1399,66, 392, -1),(1399,65, 398, -1),(1399,65, 355, -1),(1399,65, 312, -1),(1399,65, 269, -1),(1399,64, 295, -1),(1399,64, 142, -1),(1399,63, 344, -1),(1399,63, 233, -1),(1399,63, 122, -1),(1399,62, 327, -1),(1399,62, 282, -1),(1399,61, 172, -1),(1399,60, 338, -1),(1399,59, 391, -1),(1399,59, 320, -1),(1399,58, 253, -1),(1399,58, 229, -1),(1399,58, 205, -1),(1399,57, 233, -1),(1399,57, 184, -1),(1399,56, 387, -1),(1399,55, 394, -1),(1399,55, 267, -1),(1399,55, 89, -1),(1399,54, 272, -1),(1399,53, 277, -1),(1399,52, 390, -1),(1399,52, 121, -1),(1399,51, 288, -1),(1399,51, 96, -1),(1399,49, 385, -1),(1399,49, 328, -1),(1399,49, 271, -1),(1399,49, 214, -1),(1399,49, 157, -1),(1399,48, 335, -1),(1399,48, 306, -1),(1399,48, 102, -1),(1399,47, 372, -1),(1399,47, 253, -1),(1399,46, 380, -1),(1399,46, 228, -1),(1399,46, 76, -1),(1399,45, 295, -1),(1399,45, 264, -1),(1399,45, 233, -1),(1399,45, 202, -1),(1399,44, 302, -1),(1399,43, 374, -1),(1399,43, 309, -1),(1399,43, 244, -1),(1399,42, 383, -1),(1399,41, 392, -1),(1399,41, 358, -1),(1399,41, 324, -1),(1399,41, 290, -1),(1399,40, 367, -1),(1399,39, 376, -1),(1399,39, 269, -1),(1399,38, 276, -1),(1399,38, 92, -1),(1399,37, 397, -1),(1399,36, 369, -1),(1399,36, 136, -1),(1399,34, 390, -1),(1399,34, 349, -1),(1399,34, 308, -1),(1399,34, 267, -1),(1399,34, 226, -1),(1399,34, 185, -1),(1399,34, 144, -1),(1399,33, 360, -1),(1399,33, 233, -1),(1399,32, 371, -1),(1399,32, 284, -1),(1399,32, 153, -1),(1399,31, 383, -1),(1399,31, 338, -1),(1399,31, 293, -1),(1399,31, 248, -1),(1399,31, 203, -1),(1399,30, 396, -1),(1399,30, 303, -1),(1399,29, 361, -1),(1399,29, 313, -1),(1399,29, 265, -1),(1399,29, 217, -1),(1399,28, 374, -1),(1399,27, 388, -1),(1399,27, 233, -1),(1399,26, 349, -1),(1399,26, 242, -1),(1399,25, 363, -1),(1399,24, 378, -1),(1399,24, 320, -1),(1399,24, 262, -1),(1399,24, 204, -1),(1399,23, 395, -1),(1399,23, 152, -1),(1399,22, 349, -1),(1399,22, 286, -1),(1399,21, 366, -1),(1399,21, 233, -1),(1399,20, 384, -1),(1399,19, 331, -1),(1399,19, 184, -1),(1399,18, 349, -1),(1399,18, 272, -1),(1399,17, 370, -1),(1399,17, 288, -1),(1399,16, 393, -1),(1399,16, 306, -1),(1399,15, 326, -1),(1399,15, 233, -1),(1399,14, 349, -1),(1399,13, 376, -1),(1399,13, 269, -1),(1399,12, 291, -1),(1399,11, 317, -1),(1399,11, 190, -1),(1399,10, 349, -1),(1399,9, 388, -1),(1399,9, 233, -1),(1399,8, 262, -1),(1399,7, 299, -1),(1399,6, 349, -1),(1399,5, 399, -1),(1398,5, 399, -1),(1397,5, 399, -1),(1396,5, 399, -1),(1395,5, 399, -1),(1394,5, 399, -1),(1393,5, 399, -1),(1392,5, 399, -1),(1391,5, 399, -1),(1390,5, 399, -1),(1389,5, 399, -1),(1388,5, 399, -1),(1387,5, 399, -1),(1386,5, 399, -1),(1385,5, 399, -1),(1384,5, 399, -1),(1383,5, 399, -1),(1382,5, 399, -1),(1381,5, 399, -1),(1380,5, 399, -1),(1379,5, 399, -1),(1378,5, 399, -1),(1377,5, 399, -1),(1376,5, 399, -1),(1375,5, 399, -1),(1374,5, 399, -1),(1373,5, 399, -1),(1372,5, 399, -1),(1371,5, 399, -1),(1370,5, 399, -1),(1369,5, 399, -1),(1368,5, 399, -1),(1367,5, 399, -1),(1366,5, 399, -1),(1365,5, 399, -1),(1364,5, 399, -1),(1363,5, 399, -1),(1362,5, 399, -1),(1361,5, 399, -1),(1360,5, 399, -1),(1359,5, 399, -1),(1358,5, 399, -1),(1357,5, 399, -1),(1356,5, 399, -1),(1355,5, 399, -1),(1354,5, 399, -1),(1353,5, 399, -1),(1352,5, 399, -1),(1351,5, 399, -1),(1350,5, 399, -1),(1349,5, 399, -1),(1348,5, 399, -1),(1347,5, 399, -1),(1346,5, 399, -1),(1345,5, 399, -1),(1344,5, 399, -1),(1343,5, 399, -1),(1342,5, 399, -1),(1341,5, 399, -1),(1340,5, 399, -1),(1339,5, 399, -1),(1338,5, 399, -1),(1337,5, 399, -1),(1336,5, 399, -1),(1335,5, 399, -1),(1334,5, 399, -1),(1333,5, 399, -1),(1332,5, 399, -1),(1331,5, 399, -1),(697,1399, -1, 280),(689,1399, -1, 200),(683,1399, -1, 212),(674,1398, -1, 28),(667,1398, -1, 284),(660,1399, -1, 124),(654,1399, -1, 123),(647,1399, -1, 40),(641,1399, -1, 287),(635,1398, -1, 142),(629,1398, -1, 10),(623,1398, -1, 46),(618,1399, -1, 103),(612,1399, -1, 8),(606,1399, -1, 202),(600,1399, -1, 232),(594,1399, -1, 305),(588,1397, -1, 177),(582,1399, -1, 256),(577,1399, -1, 217),(571,1396, -1, 11),(567,1399, -1, 359),(563,1399, -1, 41),(557,1399, -1, 162),(552,1399, -1, 313),(547,1399, -1, 211),(541,1399, -1, 128),(536,1399, -1, 338),(532,1398, -1, 293),(527,1399, -1, 73),(521,1398, -1, 377),(517,1399, -1, 115),(513,1397, -1, 241),(509,1399, -1, 224),(505,1396, -1, 217),(502,1398, -1, 110),(498,1397, -1, 108),(495,1399, -1, 366),(490,1398, -1, 398),(485,1397, -1, 301),(482,1398, -1, 364),(479,1399, -1, 92),(474,1399, -1, 152),(470,1398, -1, 58),(467,1399, -1, 349),(463,1399, -1, 210),(459,1399, -1, 32),(456,1399, -1, 158),(452,1398, -1, 283),(449,1399, -1, 148),(445,1399, -1, 11),(442,1398, -1, 68),(439,1398, -1, 164),(436,1398, -1, 101),(433,1399, -1, 63),(430,1399, -1, 353),(426,1399, -1, 133),(423,1399, -1, 339),(419,1399, -1, 202),(417,1399, -1, 52),(413,1399, -1, 188),(409,1396, -1, 285),(406,1399, -1, 143),(402,1397, -1, 384),(400,1399, -1, 348),(397,1399, -1, 111),(393,1399, -1, 283),(391,1399, -1, 195),(388,1399, -1, 384),(385,1398, -1, 187),(383,1399, -1, 305),(380,1399, -1, 289),(377,1399, -1, 154),(374,1398, -1, 271),(372,1399, -1, 220),(370,1399, -1, 259),(368,1398, -1, 340),(366,1399, -1, 86),(364,1397, -1, 71),(361,1398, -1, 91),(359,1399, -1, 189),(356,1397, -1, 155),(355,1399, -1, 333),(351,1399, -1, 277),(349,1398, -1, 6),(347,1398, -1, 280),(345,1399, -1, 221),(341,1399, -1, 240),(338,1399, -1, 387),(335,1399, -1, 332),(333,1399, -1, 355),(330,1399, -1, 248),(328,1399, -1, 386),(326,1399, -1, 324),(324,1399, -1, 326),(322,1398, -1, 89),(320,1398, -1, 391),(318,1397, -1, 380),(317,1399, -1, 267),(315,1397, -1, 51),(313,1399, -1, 257),(311,1398, -1, 227),(310,1399, -1, 264),(309,1399, -1, 206),(307,1399, -1, 180),(305,1399, -1, 383),(304,1399, -1, 237),(301,1399, -1, 244),(299,1399, -1, 386),(299,1399, -1, 255),(296,1398, -1, 196),(294,1397, -1, 354),(292,1399, -1, 103),(291,1397, -1, 12),(289,1399, -1, 380),(288,1399, -1, 17),(286,1399, -1, 203),(284,1397, -1, 273),(283,1399, -1, 393),(281,1397, -1, 261),(280,1398, -1, 347),(278,1399, -1, 390),(277,1399, -1, 250),(275,1399, -1, 379),(273,1397, -1, 284),(272,1399, -1, 54),(270,1398, -1, 277),(269,1399, -1, 65),(267,1399, -1, 317),(265,1398, -1, 182),(263,1399, -1, 125),(262,1398, -1, 8),(261,1399, -1, 201),(259,1399, -1, 397),(258,1399, -1, 122),(257,1399, -1, 313),(256,1399, -1, 194),(255,1399, -1, 299),(253,1399, -1, 235),(252,1399, -1, 297),(251,1398, -1, 220),(250,1399, -1, 277),(248,1399, -1, 330),(247,1399, -1, 337),(246,1399, -1, 327),(245,1399, -1, 197),(244,1399, -1, 43),(242,1399, -1, 211),(241,1399, -1, 357),(240,1399, -1, 341),(238,1399, -1, 385),(238,1398, -1, 326),(237,1399, -1, 304),(236,1399, -1, 329),(234,1399, -1, 278),(233,1399, -1, 9),(232,1397, -1, 280),(230,1399, -1, 295),(229,1399, -1, 168),(228,1399, -1, 316),(226,1398, -1, 300),(225,1398, -1, 146),(224,1399, -1, 153),(223,1399, -1, 367),(221,1399, -1, 345),(220,1399, -1, 124),(219,1399, -1, 214),(218,1399, -1, 369),(216,1399, -1, 204),(216,1399, -1, 68),(215,1399, -1, 244),(214,1399, -1, 219),(213,1399, -1, 266),(212,1397, -1, 313),(211,1399, -1, 242),(209,1399, -1, 251),(208,1399, -1, 380),(207,1398, -1, 260),(206,1399, -1, 309),(205,1399, -1, 58),(204,1399, -1, 120),(203,1399, -1, 286),(202,1398, -1, 218),(201,1399, -1, 261),(200,1398, -1, 346),(199,1396, -1, 235),(198,1399, -1, 378),(197,1399, -1, 316),(197,1399, -1, 245),(196,1399, -1, 389),(195,1399, -1, 391),(194,1399, -1, 256),(193,1398, -1, 134),(191,1399, -1, 260),(191,1398, -1, 172),(189,1399, -1, 396),(189,1399, -1, 359),(188,1398, -1, 145),(187,1398, -1, 385),(186,1399, -1, 267),(185,1399, -1, 397),(184,1399, -1, 19),(183,1399, -1, 172),(182,1399, -1, 319),(181,1399, -1, 228),(180,1399, -1, 307),(179,1399, -1, 254),(178,1399, -1, 279),(177,1399, -1, 328),(176,1399, -1, 155),(175,1396, -1, 347),(174,1399, -1, 205),(173,1399, -1, 376),(173,1398, -1, 101),(172,1399, -1, 305),(172,1399, -1, 61),(171,1397, -1, 241),(170,1399, -1, 144),(169,1398, -1, 335),(168,1399, -1, 229),(167,1399, -1, 289),(166,1398, -1, 240),(166,1398, -1, 80),(165,1398, -1, 72),(164,1399, -1, 354),(163,1399, -1, 339),(162,1399, -1, 367),(162,1397, -1, 332),(161,1398, -1, 178),(160,1399, -1, 153),(159,1396, -1, 259),(158,1399, -1, 394),(157,1399, -1, 147),(157,1399, -1, 49),(156,1399, -1, 139),(155,1399, -1, 176),(154,1399, -1, 377),(153,1399, -1, 224),(153,1399, -1, 96),(152,1399, -1, 69),(152,1399, -1, 23),(151,1399, -1, 88),(150,1398, -1, 219),(149,1399, -1, 399),(149,1399, -1, 61),(149,1396, -1, 89),(148,1399, -1, 345),(148,1398, -1, 392),(147,1399, -1, 157),(146,1399, -1, 321),(145,1399, -1, 82),(144,1399, -1, 306),(144,1399, -1, 170),(144,1399, -1, 34),(143,1399, -1, 44),(142,1399, -1, 399),(141,1399, -1, 253),(140,1396, -1, 344),(139,1399, -1, 317),(139,1399, -1, 156),(138,1399, -1, 370),(138,1397, -1, 329),(137,1399, -1, 291),(137,1399, -1, 97),(136,1399, -1, 324),(136,1399, -1, 180),(136,1399, -1, 36),(135,1398, -1, 88),(135,1397, -1, 119),(134,1399, -1, 214),(134,1398, -1, 193),(133,1399, -1, 142),(132,1398, -1, 323),(131,1399, -1, 315),(131,1399, -1, 283),(130,1399, -1, 382),(130,1398, -1, 371),(129,1399, -1, 244),(128,1399, -1, 388),(127,1399, -1, 369),(127,1399, -1, 358),(126,1399, -1, 272),(125,1399, -1, 263),(124,1399, -1, 220),(123,1399, -1, 381),(122,1399, -1, 258),(121,1399, -1, 237),(120,1399, -1, 204),(119,1399, -1, 335),(119,1399, -1, 288),(119,1399, -1, 241),(118,1399, -1, 326),(118,1398, -1, 77),(117,1399, -1, 269),(117,1397, -1, 197),(116,1399, -1, 211),(116,1398, -1, 235),(115,1399, -1, 371),(115,1398, -1, 79),(114,1399, -1, 362),(113,1399, -1, 229),(113,1398, -1, 167),(112,1399, -1, 331),(111,1399, -1, 397),(110,1399, -1, 337),(110,1399, -1, 248),(109,1398, -1, 109),(108,1399, -1, 136),(107,1399, -1, 268),(106,1398, -1, 389),(106,1398, -1, 178),(106,1397, -1, 112),(105,1399, -1, 393),(105,1397, -1, 153),(104,1399, -1, 343),(103,1399, -1, 292),(102,1399, -1, 336),(102,1399, -1, 144),(101,1399, -1, 367),(101,1399, -1, 90),(100,1396, -1, 342),(99,1399, -1, 332),(99,1399, -1, 219),(98,1399, -1, 364),(97,1399, -1, 310),(97,1399, -1, 137),(96,1399, -1, 357),(96,1399, -1, 255),(96,1399, -1, 153),(96,1399, -1, 51),(95,1399, -1, 346),(95,1397, -1, 272),(95,1396, -1, 360),(94,1399, -1, 305),(94,1399, -1, 186),(94,1398, -1, 290),(93,1399, -1, 203),(93,1399, -1, 188),(93,1397, -1, 353),(92,1399, -1, 190),(92,1399, -1, 114),(92,1399, -1, 38),(91,1399, -1, 392),(91,1397, -1, 284),(90,1399, -1, 272),(89,1399, -1, 275),(89,1399, -1, 165),(89,1399, -1, 55),(88,1399, -1, 310),(87,1399, -1, 217),(87,1399, -1, 201),(86,1399, -1, 366),(86,1399, -1, 122),(85,1399, -1, 288),(85,1398, -1, 74),(84,1399, -1, 358),(84,1398, -1, 208),(83,1399, -1, 396),(83,1398, -1, 261),(83,1398, -1, 160),(82,1399, -1, 162),(82,1399, -1, 145),(81,1399, -1, 354),(81,1398, -1, 302),(80,1399, -1, 341),(79,1399, -1, 363),(78,1399, -1, 278),(77,1399, -1, 227),(77,1399, -1, 118),(77,1398, -1, 354),(77,1398, -1, 118),(76,1399, -1, 230),(76,1399, -1, 138),(76,1399, -1, 46),(75,1399, -1, 345),(75,1396, -1, 214),(75,1394, -1, 381),(75,1393, -1, 65),(74,1399, -1, 293),(74,1398, -1, 85),(73,1399, -1, 297),(73,1398, -1, 67),(72,1399, -1, 340),(72,1399, -1, 204),(72,1399, -1, 68),(71,1399, -1, 325),(71,1399, -1, 266),(70,1396, -1, 329),(70,1395, -1, 269),(70,1394, -1, 229),(69,1399, -1, 375),(69,1399, -1, 152),(69,1398, -1, 314),(68,1399, -1, 360),(68,1399, -1, 216),(68,1399, -1, 72),(67,1399, -1, 261),(66,1399, -1, 392),(66,1398, -1, 180),(65,1399, -1, 398),(65,1399, -1, 355),(65,1399, -1, 312),(65,1399, -1, 269),(64,1399, -1, 295),(64,1399, -1, 142),(64,1398, -1, 273),(64,1397, -1, 251),(63,1399, -1, 344),(63,1399, -1, 233),(63,1399, -1, 122),(63,1398, -1, 122),(63,1397, -1, 255),(62,1399, -1, 327),(62,1399, -1, 282),(62,1398, -1, 124),(61,1399, -1, 172),(60,1399, -1, 338),(60,1398, -1, 198),(59,1399, -1, 391),(59,1399, -1, 320),(59,1398, -1, 154),(58,1399, -1, 229),(58,1399, -1, 205),(57,1399, -1, 233),(57,1399, -1, 184),(57,1398, -1, 282),(56,1399, -1, 387),(56,1398, -1, 337),(55,1399, -1, 267),(55,1399, -1, 89),(54,1399, -1, 272),(53,1399, -1, 277),(53,1398, -1, 356),(53,1398, -1, 145),(53,1397, -1, 224),(53,1395, -1, 329),(52,1399, -1, 390),(52,1399, -1, 121),(52,1397, -1, 94),(51,1399, -1, 288),(51,1399, -1, 96),(50,1397, -1, 377),(50,1396, -1, 321),(50,1395, -1, 265),(49,1399, -1, 328),(49,1399, -1, 271),(49,1399, -1, 214),(49,1399, -1, 157),(48,1399, -1, 335),(48,1399, -1, 306),(48,1399, -1, 102),(47,1399, -1, 372),(47,1399, -1, 253),(46,1399, -1, 380),(46,1399, -1, 228),(46,1399, -1, 76),(45,1399, -1, 295),(45,1399, -1, 264),(45,1399, -1, 233),(45,1399, -1, 202),(45,1397, -1, 357),(44,1399, -1, 302),(43,1399, -1, 374),(43,1399, -1, 309),(43,1399, -1, 244),(42,1399, -1, 383),(41,1399, -1, 392),(41,1399, -1, 358),(41,1399, -1, 324),(41,1399, -1, 290),(40,1399, -1, 367),(40,1398, -1, 332),(39,1399, -1, 269),(38,1399, -1, 276),(38,1399, -1, 92),(37,1399, -1, 397),(36,1399, -1, 369),(36,1399, -1, 136),(35,1398, -1, 379),(35,1397, -1, 379),(35,1396, -1, 339),(34,1399, -1, 308),(34,1399, -1, 267),(34,1399, -1, 226),(34,1399, -1, 185),(34,1399, -1, 144),(33,1399, -1, 360),(33,1399, -1, 233),(33,1398, -1, 360),(32,1399, -1, 371),(32,1399, -1, 284),(32,1399, -1, 153),(31,1399, -1, 383),(31,1399, -1, 338),(31,1399, -1, 293),(31,1399, -1, 248),(31,1399, -1, 203),(31,1398, -1, 248),(30,1399, -1, 396),(30,1399, -1, 303),(30,1396, -1, 349),(29,1399, -1, 361),(29,1399, -1, 313),(29,1399, -1, 265),(29,1399, -1, 217),(28,1399, -1, 374),(28,1398, -1, 374),(28,1397, -1, 374),(28,1396, -1, 324),(28,1395, -1, 274),(27,1399, -1, 388),(27,1399, -1, 233),(27,1397, -1, 388),(26,1399, -1, 349),(26,1399, -1, 242),(26,1397, -1, 188),(25,1399, -1, 363),(25,1398, -1, 363),(25,1397, -1, 363),(25,1396, -1, 307),(25,1393, -1, 195),(24,1399, -1, 378),(24,1399, -1, 320),(24,1399, -1, 262),(24,1399, -1, 204),(23,1399, -1, 395),(23,1399, -1, 152),(22,1399, -1, 349),(22,1399, -1, 286),(21,1399, -1, 366),(21,1399, -1, 233),(20,1399, -1, 384),(20,1398, -1, 384),(20,1397, -1, 384),(20,1396, -1, 314),(19,1399, -1, 331),(19,1399, -1, 184),(18,1399, -1, 349),(18,1399, -1, 272),(17,1399, -1, 370),(17,1399, -1, 288),(16,1399, -1, 393),(16,1399, -1, 306),(15,1399, -1, 326),(15,1399, -1, 233),(14,1399, -1, 349),(14,1398, -1, 349),(14,1397, -1, 349),(14,1395, -1, 249),(13,1399, -1, 376),(13,1399, -1, 269),(12,1399, -1, 291),(12,1398, -1, 291),(12,1397, -1, 291),(11,1399, -1, 317),(11,1399, -1, 190),(11,1398, -1, 190),(11,1397, -1, 317),(11,1396, -1, 317),(11,1395, -1, 317),(10,1399, -1, 349),(10,1398, -1, 349),(10,1397, -1, 349),(9,1399, -1, 388),(9,1399, -1, 233),(8,1399, -1, 262),(8,1398, -1, 262),(7,1399, -1, 299),(7,1398, -1, 299),(7,1397, -1, 299),(7,1396, -1, 299),(6,1399, -1, 349),(6,1398, -1, 349),(6,1397, -1, 349),(5,1399, -1, 399),(5,1398, -1, 399),(5,1397, -1, 399),(5,1396, -1, 399),(5,1395, -1, 399),(5,1394, -1, 399),(5,1393, -1, 399),(5,1392, -1, 399),(5,1391, -1, 399),(5,1390, -1, 399),(5,1389, -1, 399),(5,1388, -1, 399),(5,1387, -1, 399),(5,1386, -1, 399),(5,1385, -1, 399),(5,1384, -1, 399),(5,1383, -1, 399),(5,1382, -1, 399),(5,1381, -1, 399),(5,1380, -1, 399),(5,1379, -1, 399),(5,1378, -1, 399),(5,1377, -1, 399),(5,1376, -1, 399),(5,1375, -1, 399),(5,1374, -1, 399),(5,1373, -1, 399),(5,1372, -1, 399),(5,1371, -1, 399),(5,1370, -1, 399),(5,1369, -1, 399),(5,1368, -1, 399),(5,1367, -1, 399),(5,1366, -1, 399),(5,1365, -1, 399),(5,1364, -1, 399),(5,1363, -1, 399),(5,1362, -1, 399),(5,1361, -1, 399),(5,1360, -1, 399),(5,1359, -1, 399),(5,1358, -1, 399),(5,1357, -1, 399),(5,1356, -1, 399),(5,1355, -1, 399),(5,1354, -1, 399),(5,1353, -1, 399),(5,1352, -1, 399),(5,1351, -1, 399),(5,1350, -1, 399),(5,1349, -1, 399),(5,1348, -1, 399),(5,1347, -1, 399),(5,1346, -1, 399),(5,1345, -1, 399),(5,1344, -1, 399),(5,1343, -1, 399),(5,1342, -1, 399),(5,1341, -1, 399),(5,1340, -1, 399),(5,1339, -1, 399),(5,1338, -1, 399),(5,1337, -1, 399),(5,1336, -1, 399),(5,1335, -1, 399),(5,1334, -1, 399),(5,1333, -1, 399),(5,1332, -1, 399),(5,1331, -1, 399)];
1523
1524 #[test]
1525 fn rounding_regression_shrink_within() {
1526 let mut failures = Vec::new();
1527 for (i, &(ow, oh, tw, th)) in SHRINK_WITHIN_TESTS.iter().enumerate() {
1528 let ow = ow as u32;
1529 let oh = oh as u32;
1530 if tw > 0 {
1531 let tw = tw as u32;
1532 let layout = Constraint::width_only(ConstraintMode::Fit, tw)
1533 .compute(ow, oh)
1534 .unwrap();
1535 if layout.resize_to.width != tw {
1536 failures.push(format!(
1537 "case {i}: ({ow}x{oh}, w={tw}) -> resize_to.0={}, expected {tw}",
1538 layout.resize_to.width
1539 ));
1540 }
1541 if layout.source_crop.is_some() {
1542 failures.push(format!(
1543 "case {i}: ({ow}x{oh}, w={tw}) -> unexpected source_crop"
1544 ));
1545 }
1546 } else if th > 0 {
1547 let th = th as u32;
1548 let layout = Constraint::height_only(ConstraintMode::Fit, th)
1549 .compute(ow, oh)
1550 .unwrap();
1551 if layout.resize_to.height != th {
1552 failures.push(format!(
1553 "case {i}: ({ow}x{oh}, h={th}) -> resize_to.1={}, expected {th}",
1554 layout.resize_to.height
1555 ));
1556 }
1557 if layout.source_crop.is_some() {
1558 failures.push(format!(
1559 "case {i}: ({ow}x{oh}, h={th}) -> unexpected source_crop"
1560 ));
1561 }
1562 }
1563 }
1564 assert!(
1565 failures.is_empty(),
1566 "Rounding regression failures ({} of {}):\n{}",
1567 failures.len(),
1568 SHRINK_WITHIN_TESTS.len(),
1569 failures.join("\n")
1570 );
1571 }
1572
1573 const TARGETS: [(u32, u32); 11] = [
1578 (1, 1),
1579 (1, 3),
1580 (3, 1),
1581 (7, 3),
1582 (90, 45),
1583 (10, 10),
1584 (100, 33),
1585 (1621, 883),
1586 (971, 967),
1587 (17, 1871),
1588 (512, 512),
1589 ];
1590
1591 fn gen_source_sizes(tw: u32, th: u32) -> Vec<(u32, u32)> {
1592 fn vary(v: u32) -> Vec<u32> {
1593 let mut vals = vec![v, v.saturating_add(1), v.saturating_sub(1).max(1)];
1594 vals.extend([v * 2, v * 3, v * 10]);
1595 vals.extend([(v / 2).max(1), (v / 3).max(1), (v / 10).max(1)]);
1596 vals.push(v.next_power_of_two());
1597 vals.extend([1, 2, 3, 5, 7, 16, 100, 1000]);
1598 vals.sort_unstable();
1599 vals.dedup();
1600 vals.retain(|&x| x > 0);
1601 vals
1602 }
1603
1604 let w_vals = vary(tw);
1605 let h_vals = vary(th);
1606 let mut sizes: Vec<(u32, u32)> = Vec::new();
1607 for &w in &w_vals {
1608 for &h in &h_vals {
1609 sizes.push((w, h));
1610 }
1611 }
1612
1613 let ratios: [(u64, u64); 6] = [(1, 1), (1, 3), (3, 1), (4, 3), (16, 9), (1200, 400)];
1615 for (rw, rh) in ratios {
1616 let inner_w = (tw as u64).min(th as u64 * rw / rh.max(1)).max(1) as u32;
1617 let inner_h = (th as u64).min(tw as u64 * rh / rw.max(1)).max(1) as u32;
1618 sizes.push((inner_w, inner_h));
1619 let outer_w = (tw as u64).max(th as u64 * rw / rh.max(1)).max(1) as u32;
1620 let outer_h = (th as u64).max(tw as u64 * rh / rw.max(1)).max(1) as u32;
1621 sizes.push((outer_w, outer_h));
1622 }
1623
1624 sizes.sort_unstable();
1625 sizes.dedup();
1626 sizes
1627 }
1628
1629 #[test]
1630 fn parametric_invariants() {
1631 let mut failures = Vec::new();
1632 let mut checked = 0u64;
1633
1634 for &(tw, th) in &TARGETS {
1635 let sources = gen_source_sizes(tw, th);
1636 for &(sw, sh) in &sources {
1637 use ConstraintMode::*;
1638 let modes = [
1639 Distort, Fit, Within, FitCrop, WithinCrop, FitPad, WithinPad, AspectCrop,
1640 ];
1641 for mode in modes {
1642 let c = Constraint::new(mode, tw, th);
1643 let layout = match c.compute(sw, sh) {
1644 Ok(l) => l,
1645 Err(e) => {
1646 failures
1647 .push(format!("{mode:?} ({sw}x{sh} -> {tw}x{th}): error {e:?}"));
1648 continue;
1649 }
1650 };
1651 let Size {
1652 width: rw,
1653 height: rh,
1654 } = layout.resize_to;
1655 let Size {
1656 width: cw,
1657 height: ch,
1658 } = layout.canvas;
1659 let (px, py) = layout.placement;
1660 let tag = format!("{mode:?} ({sw}x{sh} -> {tw}x{th})");
1661
1662 match mode {
1663 Distort => {
1664 if (cw, ch) != (tw, th) {
1665 failures.push(format!(
1666 "{tag}: canvas ({cw},{ch}) != target ({tw},{th})"
1667 ));
1668 }
1669 if (rw, rh) != (tw, th) {
1670 failures.push(format!(
1671 "{tag}: resize_to ({rw},{rh}) != target ({tw},{th})"
1672 ));
1673 }
1674 if layout.source_crop.is_some() {
1675 failures.push(format!("{tag}: unexpected source_crop"));
1676 }
1677 }
1678 Fit => {
1679 if rw > tw || rh > th {
1680 failures.push(format!(
1681 "{tag}: resize_to ({rw},{rh}) exceeds target ({tw},{th})"
1682 ));
1683 }
1684 if rw != tw && rh != th {
1685 failures.push(format!(
1686 "{tag}: doesn't touch either edge: ({rw},{rh}) vs ({tw},{th})"
1687 ));
1688 }
1689 if layout.source_crop.is_some() {
1690 failures.push(format!("{tag}: unexpected source_crop"));
1691 }
1692 if (cw, ch) != (rw, rh) {
1693 failures.push(format!(
1694 "{tag}: canvas ({cw},{ch}) != resize_to ({rw},{rh})"
1695 ));
1696 }
1697 }
1698 Within => {
1699 if rw > tw || rh > th {
1700 failures.push(format!(
1701 "{tag}: resize_to ({rw},{rh}) exceeds target ({tw},{th})"
1702 ));
1703 }
1704 if sw <= tw && sh <= th {
1705 if (rw, rh) != (sw, sh) {
1706 failures.push(format!(
1707 "{tag}: no-upscale: ({rw},{rh}) != source ({sw},{sh})"
1708 ));
1709 }
1710 } else if rw != tw && rh != th {
1711 failures.push(format!(
1712 "{tag}: doesn't touch either edge: ({rw},{rh}) vs ({tw},{th})"
1713 ));
1714 }
1715 if layout.source_crop.is_some() {
1716 failures.push(format!("{tag}: unexpected source_crop"));
1717 }
1718 if (cw, ch) != (rw, rh) {
1719 failures.push(format!(
1720 "{tag}: canvas ({cw},{ch}) != resize_to ({rw},{rh})"
1721 ));
1722 }
1723 }
1724 FitCrop => {
1725 if (cw, ch) != (tw, th) {
1726 failures.push(format!(
1727 "{tag}: canvas ({cw},{ch}) != target ({tw},{th})"
1728 ));
1729 }
1730 if (rw, rh) != (tw, th) {
1731 failures.push(format!(
1732 "{tag}: resize_to ({rw},{rh}) != target ({tw},{th})"
1733 ));
1734 }
1735 if let Some(crop) = &layout.source_crop
1736 && crop.x > 0
1737 && crop.y > 0
1738 && crop.x + crop.width < sw
1739 && crop.y + crop.height < sh
1740 {
1741 failures.push(format!("{tag}: crop on all 4 sides: {crop:?}"));
1742 }
1743 }
1744 WithinCrop => {
1745 if rw > tw || rh > th {
1746 failures.push(format!(
1747 "{tag}: resize_to ({rw},{rh}) exceeds target ({tw},{th})"
1748 ));
1749 }
1750 if let Some(crop) = &layout.source_crop {
1751 if rw > crop.width || rh > crop.height {
1752 failures.push(format!(
1753 "{tag}: upscale: resize ({rw},{rh}) > crop ({},{})",
1754 crop.width, crop.height
1755 ));
1756 }
1757 if crop.x > 0
1758 && crop.y > 0
1759 && crop.x + crop.width < sw
1760 && crop.y + crop.height < sh
1761 {
1762 failures.push(format!("{tag}: crop on all 4 sides: {crop:?}"));
1763 }
1764 }
1765 }
1766 FitPad => {
1767 if (cw, ch) != (tw, th) {
1768 failures.push(format!(
1769 "{tag}: canvas ({cw},{ch}) != target ({tw},{th})"
1770 ));
1771 }
1772 if rw > tw || rh > th {
1773 failures.push(format!(
1774 "{tag}: resize_to ({rw},{rh}) exceeds target ({tw},{th})"
1775 ));
1776 }
1777 if layout.source_crop.is_some() {
1778 failures.push(format!("{tag}: unexpected source_crop"));
1779 }
1780 if px > 0
1781 && py > 0
1782 && px + rw as i32 > 0
1783 && (px as u32) + rw < tw
1784 && (py as u32) + rh < th
1785 {
1786 failures.push(format!("{tag}: padding on all 4 sides"));
1787 }
1788 if px as u32 + rw > tw || py as u32 + rh > th {
1789 failures.push(format!(
1790 "{tag}: placement overflow: ({px},{py})+({rw},{rh})>({tw},{th})"
1791 ));
1792 }
1793 }
1794 WithinPad => {
1795 if sw <= tw && sh <= th {
1796 if (rw, rh) != (sw, sh) {
1798 failures.push(format!(
1799 "{tag}: no-upscale: ({rw},{rh}) != source ({sw},{sh})"
1800 ));
1801 }
1802 if (cw, ch) != (sw, sh) {
1803 failures.push(format!(
1804 "{tag}: identity canvas ({cw},{ch}) != source ({sw},{sh})"
1805 ));
1806 }
1807 } else {
1808 if (cw, ch) != (tw, th) {
1810 failures.push(format!(
1811 "{tag}: canvas ({cw},{ch}) != target ({tw},{th})"
1812 ));
1813 }
1814 if rw > tw || rh > th {
1815 failures.push(format!(
1816 "{tag}: resize_to ({rw},{rh}) exceeds target ({tw},{th})"
1817 ));
1818 }
1819 }
1820 if layout.source_crop.is_some() {
1821 failures.push(format!("{tag}: unexpected source_crop"));
1822 }
1823 if px as u32 + rw > cw || py as u32 + rh > ch {
1824 failures.push(format!(
1825 "{tag}: placement overflow: ({px},{py})+({rw},{rh})>({cw},{ch})"
1826 ));
1827 }
1828 }
1829 PadWithin => {
1830 if (cw, ch) != (tw, th) {
1832 failures.push(format!(
1833 "{tag}: canvas ({cw},{ch}) != target ({tw},{th})"
1834 ));
1835 }
1836 if rw > sw || rh > sh {
1838 failures.push(format!(
1839 "{tag}: upscaled: resize ({rw},{rh}) > source ({sw},{sh})"
1840 ));
1841 }
1842 if rw > tw || rh > th {
1844 failures.push(format!(
1845 "{tag}: resize_to ({rw},{rh}) exceeds target ({tw},{th})"
1846 ));
1847 }
1848 if layout.source_crop.is_some() {
1849 failures.push(format!("{tag}: unexpected source_crop"));
1850 }
1851 if px as u32 + rw > cw || py as u32 + rh > ch {
1852 failures.push(format!(
1853 "{tag}: placement overflow: ({px},{py})+({rw},{rh})>({cw},{ch})"
1854 ));
1855 }
1856 }
1857 AspectCrop => {
1858 if let Some(crop) = &layout.source_crop {
1859 if (rw, rh) != (crop.width, crop.height) {
1860 failures.push(format!(
1861 "{tag}: resize_to ({rw},{rh}) != crop ({},{})",
1862 crop.width, crop.height
1863 ));
1864 }
1865 } else if (rw, rh) != (sw, sh) {
1866 failures.push(format!(
1867 "{tag}: no crop but resize ({rw},{rh}) != source ({sw},{sh})"
1868 ));
1869 }
1870 if (cw, ch) != (rw, rh) {
1871 failures.push(format!(
1872 "{tag}: canvas ({cw},{ch}) != resize_to ({rw},{rh})"
1873 ));
1874 }
1875 }
1876 }
1877 checked += 1;
1878 }
1879 }
1880 }
1881
1882 assert!(
1883 failures.is_empty(),
1884 "Parametric invariant failures ({} of {checked} checked):\n{}",
1885 failures.len(),
1886 failures.join("\n")
1887 );
1888 assert!(
1890 checked > 15_000,
1891 "Only checked {checked} combinations, expected >15,000"
1892 );
1893 }
1894
1895 #[test]
1900 fn rounding_1200x400_to_100x33_fit() {
1901 let l = Constraint::new(ConstraintMode::Fit, 100, 33)
1902 .compute(1200, 400)
1903 .unwrap();
1904 assert_eq!(l.resize_to, Size::new(100, 33));
1905 assert!(l.source_crop.is_none());
1906 }
1907
1908 #[test]
1909 fn rounding_1200x400_to_100x33_fit_crop() {
1910 let l = Constraint::new(ConstraintMode::FitCrop, 100, 33)
1911 .compute(1200, 400)
1912 .unwrap();
1913 assert_eq!(l.resize_to, Size::new(100, 33));
1914 assert_eq!(l.canvas, Size::new(100, 33));
1915 }
1916
1917 #[test]
1918 fn crop_aspect_638x423_to_200x133() {
1919 let l = Constraint::new(ConstraintMode::FitCrop, 200, 133)
1920 .compute(638, 423)
1921 .unwrap();
1922 assert_eq!(l.resize_to, Size::new(200, 133));
1923 assert_eq!(l.canvas, Size::new(200, 133));
1924 }
1925
1926 #[test]
1927 fn fit_2x4_to_1x3() {
1928 let l = Constraint::new(ConstraintMode::Fit, 1, 3)
1929 .compute(2, 4)
1930 .unwrap();
1931 assert!(l.resize_to.width <= 1);
1932 assert!(l.resize_to.height <= 3);
1933 assert!(l.resize_to.width == 1 || l.resize_to.height == 3);
1934 }
1935
1936 #[test]
1937 fn fit_crop_2x4_to_1x3() {
1938 let l = Constraint::new(ConstraintMode::FitCrop, 1, 3)
1939 .compute(2, 4)
1940 .unwrap();
1941 assert_eq!(l.resize_to, Size::new(1, 3));
1942 assert_eq!(l.canvas, Size::new(1, 3));
1943 }
1944
1945 #[test]
1946 fn fit_1399x5_to_width_399() {
1947 let l = Constraint::width_only(ConstraintMode::Fit, 399)
1948 .compute(1399, 5)
1949 .unwrap();
1950 assert_eq!(l.resize_to.width, 399);
1951 assert!(l.source_crop.is_none());
1952 }
1953
1954 #[test]
1955 fn fit_5x1399_to_height_399() {
1956 let l = Constraint::height_only(ConstraintMode::Fit, 399)
1957 .compute(5, 1399)
1958 .unwrap();
1959 assert_eq!(l.resize_to.height, 399);
1960 assert!(l.source_crop.is_none());
1961 }
1962
1963 #[test]
1964 fn fit_1621x883_to_100x33() {
1965 let l = Constraint::new(ConstraintMode::Fit, 100, 33)
1966 .compute(1621, 883)
1967 .unwrap();
1968 assert!(l.resize_to.width <= 100 && l.resize_to.height <= 33);
1969 assert!(l.resize_to.width == 100 || l.resize_to.height == 33);
1970 }
1971
1972 #[test]
1973 fn fit_971x967_to_512x512() {
1974 let l = Constraint::new(ConstraintMode::Fit, 512, 512)
1975 .compute(971, 967)
1976 .unwrap();
1977 assert!(l.resize_to.width <= 512 && l.resize_to.height <= 512);
1978 assert!(l.resize_to.width == 512 || l.resize_to.height == 512);
1979 }
1980
1981 #[test]
1982 fn fit_1000x500_to_1x1() {
1983 let l = Constraint::new(ConstraintMode::Fit, 1, 1)
1984 .compute(1000, 500)
1985 .unwrap();
1986 assert_eq!(l.resize_to, Size::new(1, 1));
1987 }
1988
1989 #[test]
1990 fn fit_crop_1000x500_to_1x1() {
1991 let l = Constraint::new(ConstraintMode::FitCrop, 1, 1)
1992 .compute(1000, 500)
1993 .unwrap();
1994 assert_eq!(l.resize_to, Size::new(1, 1));
1995 assert_eq!(l.canvas, Size::new(1, 1));
1996 }
1997
1998 #[test]
1999 fn fit_pad_1000x500_to_1x1() {
2000 let l = Constraint::new(ConstraintMode::FitPad, 1, 1)
2001 .compute(1000, 500)
2002 .unwrap();
2003 assert_eq!(l.canvas, Size::new(1, 1));
2004 assert!(l.resize_to.width <= 1 && l.resize_to.height <= 1);
2005 }
2006
2007 #[test]
2008 fn fit_100x100_to_100x100() {
2009 let l = Constraint::new(ConstraintMode::Fit, 100, 100)
2010 .compute(100, 100)
2011 .unwrap();
2012 assert_eq!(l.resize_to, Size::new(100, 100));
2013 assert!(!l.needs_resize());
2014 }
2015
2016 #[test]
2017 fn fit_crop_100x100_to_100x100() {
2018 let l = Constraint::new(ConstraintMode::FitCrop, 100, 100)
2019 .compute(100, 100)
2020 .unwrap();
2021 assert_eq!(l.resize_to, Size::new(100, 100));
2022 assert!(l.source_crop.is_none());
2023 }
2024
2025 #[test]
2026 fn within_modes_no_upscale_small_source() {
2027 let modes = [
2028 ConstraintMode::Within,
2029 ConstraintMode::WithinCrop,
2030 ConstraintMode::WithinPad,
2031 ];
2032 for mode in modes {
2033 let l = Constraint::new(mode, 400, 300).compute(50, 30).unwrap();
2034 let tag = format!("{mode:?}");
2035 assert!(
2036 l.resize_to.width <= 50 && l.resize_to.height <= 30,
2037 "{tag}: upscaled to {:?}",
2038 l.resize_to
2039 );
2040 }
2041 }
2042
2043 #[test]
2044 fn width_only_1399x697_to_280() {
2045 let l = Constraint::width_only(ConstraintMode::Fit, 280)
2046 .compute(1399, 697)
2047 .unwrap();
2048 assert_eq!(l.resize_to.width, 280);
2049 assert!(l.source_crop.is_none());
2050 }
2051
2052 #[test]
2053 fn height_only_697x1399_to_280() {
2054 let l = Constraint::height_only(ConstraintMode::Fit, 280)
2055 .compute(697, 1399)
2056 .unwrap();
2057 assert_eq!(l.resize_to.height, 280);
2058 assert!(l.source_crop.is_none());
2059 }
2060
2061 #[test]
2066 fn percent_crop_99_percent_rounds_to_full() {
2067 let l = Constraint::new(ConstraintMode::Fit, 50, 50)
2068 .source_crop(SourceCrop::percent(0.0, 0.0, 0.99, 0.99))
2069 .compute(100, 100)
2070 .unwrap();
2071 let crop = l.source_crop.unwrap();
2073 assert_eq!(crop.width, 99);
2074 assert_eq!(crop.height, 99);
2075 }
2076
2077 #[test]
2078 fn percent_crop_plus_fit_crop_extreme_aspect() {
2079 let l = Constraint::new(ConstraintMode::FitCrop, 100, 10)
2080 .source_crop(SourceCrop::percent(0.0, 0.0, 0.5, 0.5))
2081 .compute(1000, 1000)
2082 .unwrap();
2083 assert_eq!(l.resize_to, Size::new(100, 10));
2084 assert_eq!(l.canvas, Size::new(100, 10));
2085 let crop = l.source_crop.unwrap();
2086 assert!(crop.width <= 500);
2087 assert!(crop.height <= 500);
2088 }
2089
2090 #[test]
2091 fn pixel_crop_to_1x1_with_fit() {
2092 let l = Constraint::new(ConstraintMode::Fit, 200, 200)
2093 .source_crop(SourceCrop::pixels(500, 500, 1, 1))
2094 .compute(1000, 1000)
2095 .unwrap();
2096 assert_eq!(l.resize_to, Size::new(200, 200));
2097 }
2098
2099 #[test]
2100 fn pixel_crop_exceeds_source_clamped() {
2101 let l = Constraint::new(ConstraintMode::Fit, 50, 50)
2102 .source_crop(SourceCrop::pixels(900, 900, 500, 500))
2103 .compute(1000, 1000)
2104 .unwrap();
2105 let crop = l.source_crop.unwrap();
2106 assert!(crop.x + crop.width <= 1000);
2107 assert!(crop.y + crop.height <= 1000);
2108 assert!(crop.width >= 1 && crop.height >= 1);
2109 }
2110
2111 #[test]
2112 fn percent_crop_exceeds_100_clamped() {
2113 let l = Constraint::new(ConstraintMode::Fit, 50, 50)
2114 .source_crop(SourceCrop::percent(0.0, 0.0, 1.5, 1.5))
2115 .compute(100, 100)
2116 .unwrap();
2117 assert!(l.source_crop.is_none());
2119 }
2120
2121 #[test]
2122 fn percent_crop_zero_area() {
2123 let l = Constraint::new(ConstraintMode::Fit, 50, 50)
2124 .source_crop(SourceCrop::percent(0.5, 0.5, 0.0, 0.0))
2125 .compute(100, 100)
2126 .unwrap();
2127 let crop = l.source_crop.unwrap();
2129 assert!(crop.width >= 1 && crop.height >= 1);
2130 }
2131
2132 #[test]
2137 fn gravity_center_odd_padding() {
2138 let l = Constraint::new(ConstraintMode::WithinPad, 103, 103)
2140 .compute(100, 100)
2141 .unwrap();
2142 assert_eq!(l.canvas, Size::new(100, 100));
2143 assert_eq!(l.resize_to, Size::new(100, 100));
2144 assert_eq!(l.placement, (0, 0));
2145
2146 let l = Constraint::new(ConstraintMode::FitPad, 103, 200)
2148 .compute(100, 100)
2149 .unwrap();
2150 assert_eq!(l.canvas, Size::new(103, 200));
2151 assert_eq!(l.resize_to, Size::new(103, 103));
2152 assert_eq!(l.placement, (0, 48));
2154 }
2155
2156 #[test]
2157 fn gravity_percentage_clamp_negative() {
2158 let l = Constraint::new(ConstraintMode::FitPad, 400, 400)
2159 .gravity(Gravity::Percentage(-1.0, -1.0))
2160 .compute(1000, 500)
2161 .unwrap();
2162 assert_eq!(l.placement, (0, 0));
2163 }
2164
2165 #[test]
2166 fn gravity_percentage_clamp_over_1() {
2167 let l = Constraint::new(ConstraintMode::FitPad, 400, 400)
2168 .gravity(Gravity::Percentage(2.0, 2.0))
2169 .compute(1000, 500)
2170 .unwrap();
2171 let max_x = (400 - l.resize_to.width) as i32;
2172 let max_y = (400 - l.resize_to.height) as i32;
2173 assert_eq!(l.placement, (max_x, max_y));
2174 }
2175
2176 #[test]
2177 fn gravity_50_50_equals_center() {
2178 let l_pct = Constraint::new(ConstraintMode::FitPad, 400, 400)
2179 .gravity(Gravity::Percentage(0.5, 0.5))
2180 .compute(1000, 500)
2181 .unwrap();
2182 let l_center = Constraint::new(ConstraintMode::FitPad, 400, 400)
2183 .gravity(Gravity::Center)
2184 .compute(1000, 500)
2185 .unwrap();
2186 assert_eq!(l_pct.placement, l_center.placement);
2187 }
2188
2189 #[test]
2192 fn nan_gravity_rejected() {
2193 let r = Constraint::new(ConstraintMode::FitPad, 400, 300)
2194 .gravity(Gravity::Percentage(f32::NAN, 0.5))
2195 .compute(1000, 500);
2196 assert_eq!(r, Err(LayoutError::NonFiniteFloat));
2197 }
2198
2199 #[test]
2200 fn inf_gravity_rejected() {
2201 let r = Constraint::new(ConstraintMode::FitPad, 400, 300)
2202 .gravity(Gravity::Percentage(f32::INFINITY, 0.5))
2203 .compute(1000, 500);
2204 assert_eq!(r, Err(LayoutError::NonFiniteFloat));
2205 }
2206
2207 #[test]
2208 fn nan_source_crop_rejected() {
2209 let r = Constraint::new(ConstraintMode::Fit, 400, 300)
2210 .source_crop(SourceCrop::percent(f32::NAN, 0.0, 0.5, 0.5))
2211 .compute(1000, 500);
2212 assert_eq!(r, Err(LayoutError::NonFiniteFloat));
2213 }
2214
2215 #[test]
2216 fn nan_canvas_color_rejected() {
2217 let r = Constraint::new(ConstraintMode::FitPad, 400, 300)
2218 .canvas_color(CanvasColor::Linear {
2219 r: f32::NAN,
2220 g: 0.0,
2221 b: 0.0,
2222 a: 1.0,
2223 })
2224 .compute(1000, 500);
2225 assert_eq!(r, Err(LayoutError::NonFiniteFloat));
2226 }
2227
2228 #[allow(dead_code)]
2237 mod oracle {
2238 use alloc::vec;
2239 use alloc::vec::Vec;
2240 use crate::float_math::F64Ext;
2241 use core::cmp::Ordering;
2242
2243 #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
2245 pub struct AR {
2246 pub w: i32,
2247 pub h: i32,
2248 }
2249
2250 impl core::fmt::Debug for AR {
2251 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2252 write!(f, "{}x{}", self.w, self.h)
2253 }
2254 }
2255
2256 impl AR {
2257 pub fn new(w: i32, h: i32) -> Self {
2258 assert!(w >= 1 && h >= 1, "AR dimensions must be >= 1, got {w}x{h}");
2259 AR { w, h }
2260 }
2261
2262 pub fn ratio_f64(&self) -> f64 {
2263 f64::from(self.w) / f64::from(self.h)
2264 }
2265
2266 pub fn aspect_wider_than(&self, other: &AR) -> bool {
2267 other.ratio_f64() > self.ratio_f64()
2268 }
2269
2270 pub fn proportional(
2272 &self,
2273 basis: i32,
2274 basis_is_width: bool,
2275 snap_target: Option<&AR>,
2276 ) -> i32 {
2277 let mut snap_amount = 1f64 - f64::EPSILON;
2278 if let Some(target) = snap_target {
2279 if !basis_is_width {
2280 snap_amount = self.rounding_loss_based_on_target_width(target.w);
2281 } else {
2282 snap_amount = self.rounding_loss_based_on_target_height(target.h);
2283 }
2284 }
2285
2286 let ratio = self.ratio_f64();
2287 let snap_a = if basis_is_width { self.h } else { self.w };
2288 let snap_b = if let Some(target) = snap_target {
2289 if basis_is_width { target.h } else { target.w }
2290 } else {
2291 snap_a
2292 };
2293
2294 let float = if basis_is_width {
2295 f64::from(basis) / ratio
2296 } else {
2297 ratio * f64::from(basis)
2298 };
2299
2300 let delta_a = float - f64::from(snap_a);
2301 let delta_b = float - f64::from(snap_b);
2302
2303 let v = if delta_a.abs() <= snap_amount && delta_a.abs() <= delta_b.abs() {
2304 snap_a
2305 } else if delta_b.abs() <= snap_amount {
2306 snap_b
2307 } else {
2308 float.round_() as i32
2309 };
2310
2311 if v <= 0 { 1 } else { v }
2312 }
2313
2314 fn rounding_loss_based_on_target_width(&self, target_width: i32) -> f64 {
2315 let target_x_to_self_x = target_width as f64 / self.w as f64;
2316 let recreate_y = self.h as f64 * target_x_to_self_x;
2317 let rounded_y = recreate_y.round_();
2318 let recreate_x_from_rounded_y = rounded_y * self.ratio_f64();
2319 (target_width as f64 - recreate_x_from_rounded_y).abs()
2320 }
2321
2322 fn rounding_loss_based_on_target_height(&self, target_height: i32) -> f64 {
2323 let target_y_to_self_y = target_height as f64 / self.h as f64;
2324 let recreate_x = self.w as f64 * target_y_to_self_y;
2325 let rounded_x = recreate_x.round_();
2326 let recreate_y_from_rounded_x = rounded_x / self.ratio_f64();
2327 (target_height as f64 - recreate_y_from_rounded_x).abs()
2328 }
2329
2330 pub fn height_for(&self, w: i32, snap: Option<&AR>) -> i32 {
2331 self.proportional(w, true, snap)
2332 }
2333
2334 pub fn width_for(&self, h: i32, snap: Option<&AR>) -> i32 {
2335 self.proportional(h, false, snap)
2336 }
2337
2338 pub fn box_of(&self, target: &AR, kind: BoxKind) -> AR {
2340 if target.aspect_wider_than(self) == (kind == BoxKind::Inner) {
2341 AR::new(target.w, self.height_for(target.w, Some(target)))
2342 } else {
2343 AR::new(self.width_for(target.h, Some(target)), target.h)
2344 }
2345 }
2346
2347 pub fn exceeds_any(&self, other: &AR) -> bool {
2348 self.w > other.w || self.h > other.h
2349 }
2350
2351 pub fn intersection(&self, other: &AR) -> AR {
2352 AR::new(self.w.min(other.w), self.h.min(other.h))
2353 }
2354
2355 pub fn distort_with(&self, other_old: &AR, other_new: &AR) -> AR {
2356 let new_w =
2357 (i64::from(self.w) * i64::from(other_new.w) / i64::from(other_old.w)) as i32;
2358 let new_h =
2359 (i64::from(self.h) * i64::from(other_new.h) / i64::from(other_old.h)) as i32;
2360 AR::new(new_w.max(1), new_h.max(1))
2361 }
2362
2363 pub fn cmp_size(&self, other: &AR) -> (Ordering, Ordering) {
2364 (self.w.cmp(&other.w), self.h.cmp(&other.h))
2365 }
2366 }
2367
2368 #[derive(Copy, Clone, PartialEq, Debug)]
2369 pub enum BoxKind {
2370 Inner,
2371 Outer,
2372 }
2373
2374 #[derive(Copy, Clone, PartialEq, Debug)]
2376 pub struct Layout {
2377 pub source: AR,
2378 pub target: AR,
2379 pub canvas: AR,
2380 pub image: AR,
2381 }
2382
2383 impl Layout {
2384 pub fn create(source: AR, target: AR) -> Self {
2385 Layout {
2386 source,
2387 target,
2388 canvas: source,
2389 image: source,
2390 }
2391 }
2392
2393 fn scale_canvas(self, target: AR, sizing: BoxKind) -> Self {
2394 let new_canvas = self.canvas.box_of(&target, sizing);
2395 Layout {
2396 image: self.image.distort_with(&self.canvas, &new_canvas),
2397 canvas: new_canvas,
2398 ..self
2399 }
2400 }
2401
2402 fn fill_crop(self, target: AR) -> Self {
2403 let new_source = target.box_of(&self.source, BoxKind::Inner);
2404 Layout {
2405 source: new_source,
2406 image: target,
2407 canvas: target,
2408 ..self
2409 }
2410 }
2411
2412 fn distort_canvas(self, target: AR) -> Self {
2413 Layout {
2414 image: self.image.distort_with(&self.canvas, &target),
2415 canvas: target,
2416 ..self
2417 }
2418 }
2419
2420 fn pad_canvas(self, target: AR) -> Self {
2421 Layout {
2422 canvas: target,
2423 ..self
2424 }
2425 }
2426
2427 fn crop(self, target: AR) -> Self {
2428 let new_image = self.image.intersection(&target);
2429 let new_source = new_image.box_of(&self.source, BoxKind::Inner);
2430 Layout {
2431 source: new_source,
2432 image: new_image,
2433 canvas: target,
2434 ..self
2435 }
2436 }
2437
2438 fn crop_aspect(self) -> Self {
2439 let target_box = self.target.box_of(&self.canvas, BoxKind::Inner);
2440 self.crop(target_box)
2441 }
2442
2443 fn pad_aspect(self) -> Self {
2444 let target_box = self.target.box_of(&self.canvas, BoxKind::Outer);
2445 self.pad_canvas(target_box)
2446 }
2447
2448 fn crop_to_intersection(self) -> Self {
2449 let target = self.image.intersection(&self.target);
2450 self.crop(target)
2451 }
2452
2453 fn evaluate_condition(&self, c: Cond) -> bool {
2454 c.matches(self.canvas.cmp_size(&self.target))
2455 }
2456
2457 pub fn execute_all(self, steps: &[Step]) -> Self {
2458 let mut lay = self;
2459 let mut skipping = false;
2460 for step in steps {
2461 match *step {
2462 Step::SkipIf(c) if lay.evaluate_condition(c) => {
2463 skipping = true;
2464 }
2465 Step::SkipUnless(c) if !lay.evaluate_condition(c) => {
2466 skipping = true;
2467 }
2468 Step::BeginSequence => {
2469 skipping = false;
2470 }
2471 _ => {}
2472 }
2473 if !skipping {
2474 lay = lay.execute_step(*step);
2475 }
2476 }
2477 lay
2478 }
2479
2480 fn execute_step(self, step: Step) -> Self {
2481 match step {
2482 Step::None | Step::BeginSequence | Step::SkipIf(_) | Step::SkipUnless(_) => {
2483 self
2484 }
2485 Step::ScaleToOuter => self.scale_canvas(self.target, BoxKind::Outer),
2486 Step::FillCrop => self.fill_crop(self.target),
2487 Step::ScaleToInner => self.scale_canvas(self.target, BoxKind::Inner),
2488 Step::PadAspect => self.pad_aspect(),
2489 Step::Pad => self.pad_canvas(self.target),
2490 Step::CropAspect => self.crop_aspect(),
2491 Step::Crop => self.crop(self.target),
2492 Step::CropToIntersection => self.crop_to_intersection(),
2493 Step::Distort => self.distort_canvas(self.target),
2494 }
2495 }
2496 }
2497
2498 #[derive(Copy, Clone, PartialEq, Debug)]
2499 pub enum Cond {
2500 Both(Ordering),
2501 Neither(Ordering),
2502 Either(Ordering),
2503 Larger1DSmaller1D,
2504 }
2505
2506 impl Cond {
2507 pub fn matches(&self, cmp: (Ordering, Ordering)) -> bool {
2508 match *self {
2509 Cond::Both(v) => cmp.0 == v && cmp.1 == v,
2510 Cond::Neither(v) => cmp.0 != v && cmp.1 != v,
2511 Cond::Either(v) => cmp.0 == v || cmp.1 == v,
2512 Cond::Larger1DSmaller1D => {
2513 cmp == (Ordering::Greater, Ordering::Less)
2514 || cmp == (Ordering::Less, Ordering::Greater)
2515 }
2516 }
2517 }
2518 }
2519
2520 #[derive(Copy, Clone, PartialEq, Debug)]
2521 pub enum Step {
2522 None,
2523 BeginSequence,
2524 SkipIf(Cond),
2525 SkipUnless(Cond),
2526 ScaleToOuter,
2527 ScaleToInner,
2528 FillCrop,
2529 Distort,
2530 Pad,
2531 PadAspect,
2532 Crop,
2533 CropAspect,
2534 CropToIntersection,
2535 }
2536
2537 pub fn steps_for_mode(mode: super::ConstraintMode) -> Vec<Step> {
2541 use super::ConstraintMode as CM;
2542
2543 match mode {
2544 CM::Distort => vec![Step::Distort],
2546
2547 CM::Within => vec![
2549 Step::SkipUnless(Cond::Either(Ordering::Greater)),
2550 Step::ScaleToInner,
2551 ],
2552
2553 CM::Fit => vec![Step::ScaleToInner],
2555
2556 CM::FitCrop => vec![Step::ScaleToOuter, Step::Crop],
2558
2559 CM::WithinCrop => vec![
2561 Step::SkipIf(Cond::Either(Ordering::Less)),
2562 Step::ScaleToOuter,
2563 Step::Crop,
2564 Step::BeginSequence,
2565 Step::SkipUnless(Cond::Larger1DSmaller1D),
2566 Step::CropToIntersection,
2567 ],
2568
2569 CM::FitPad => vec![Step::ScaleToInner, Step::Pad],
2571
2572 CM::WithinPad => vec![
2574 Step::SkipUnless(Cond::Either(Ordering::Greater)),
2575 Step::ScaleToInner,
2576 Step::Pad,
2577 ],
2578
2579 CM::PadWithin => vec![
2584 Step::SkipUnless(Cond::Either(Ordering::Greater)),
2585 Step::ScaleToInner,
2586 Step::BeginSequence,
2587 Step::Pad,
2588 ],
2589
2590 CM::AspectCrop => vec![Step::CropAspect],
2592 }
2593 }
2594
2595 pub fn gravity_offset(inner: i32, outer: i32) -> i32 {
2597 (outer - inner) / 2
2598 }
2599 }
2600
2601 fn parity_check(mode: ConstraintMode, source_w: u32, source_h: u32, tw: u32, th: u32) {
2603 if source_w == 0 || source_h == 0 || tw == 0 || th == 0 {
2605 return;
2606 }
2607 if source_w > 100_000 || source_h > 100_000 || tw > 100_000 || th > 100_000 {
2609 return;
2610 }
2611
2612 let source = oracle::AR::new(source_w as i32, source_h as i32);
2614 let target = oracle::AR::new(tw as i32, th as i32);
2615 let steps = oracle::steps_for_mode(mode);
2616 let layout = oracle::Layout::create(source, target).execute_all(&steps);
2617
2618 let oracle_image = Size::new(layout.image.w as u32, layout.image.h as u32);
2620 let oracle_canvas = Size::new(layout.canvas.w as u32, layout.canvas.h as u32);
2621 let oracle_crop = (layout.source.w as u32, layout.source.h as u32);
2622
2623 let zl = match Constraint::new(mode, tw, th).compute(source_w, source_h) {
2625 Ok(l) => l,
2626 Err(_) => return, };
2628
2629 let zl_image = zl.resize_to;
2630 let zl_canvas = zl.canvas;
2631 let zl_crop = match zl.source_crop {
2632 Some(r) => (r.width, r.height),
2633 None => (source_w, source_h),
2634 };
2635
2636 assert_eq!(
2638 zl_image, oracle_image,
2639 "IMAGE mismatch for {mode:?} {source_w}x{source_h} → {tw}x{th}: \
2640 zenlayout={zl_image:?} oracle={oracle_image:?}"
2641 );
2642 assert_eq!(
2643 zl_canvas, oracle_canvas,
2644 "CANVAS mismatch for {mode:?} {source_w}x{source_h} → {tw}x{th}: \
2645 zenlayout={zl_canvas:?} oracle={oracle_canvas:?}"
2646 );
2647 assert_eq!(
2648 zl_crop, oracle_crop,
2649 "CROP mismatch for {mode:?} {source_w}x{source_h} → {tw}x{th}: \
2650 zenlayout={zl_crop:?} oracle={oracle_crop:?}"
2651 );
2652 }
2653
2654 fn parity_source_sizes() -> Vec<(u32, u32)> {
2656 let mut sizes = Vec::new();
2657 let fixed = [
2659 (1, 1),
2660 (1, 3),
2661 (3, 1),
2662 (2, 4),
2663 (4, 2),
2664 (100, 100),
2665 (1000, 500),
2666 (500, 1000),
2667 (1200, 400),
2668 (400, 1200),
2669 (1399, 697),
2670 (697, 1399),
2671 (1621, 883),
2672 (883, 1621),
2673 (971, 967),
2674 (967, 971),
2675 (5104, 3380),
2676 (3380, 5104),
2677 (638, 423),
2678 (423, 638),
2679 (768, 433),
2680 (433, 768),
2681 ];
2682 sizes.extend_from_slice(&fixed);
2683
2684 for &(tw, th) in &TARGETS {
2686 sizes.push((tw * 2, th * 2));
2688 sizes.push((tw * 3, th * 2));
2689 sizes.push((tw * 2, th * 3));
2690 sizes.push((tw * 10, th * 10));
2691 if tw > 2 && th > 2 {
2693 sizes.push((tw / 2, th / 2));
2694 sizes.push((tw / 3 + 1, th / 2));
2695 sizes.push((tw / 2, th / 3 + 1));
2696 }
2697 sizes.push((tw * 2, th / 2 + 1));
2699 sizes.push((tw / 2 + 1, th * 2));
2700 sizes.push((tw, th));
2702 sizes.push((tw + 1, th + 1));
2704 if tw > 1 && th > 1 {
2705 sizes.push((tw - 1, th - 1));
2706 }
2707 }
2708
2709 sizes.sort();
2710 sizes.dedup();
2711 sizes.retain(|&(w, h)| w > 0 && h > 0);
2713 sizes
2714 }
2715
2716 #[test]
2717 fn parity_distort() {
2718 let sources = parity_source_sizes();
2719 let mut count = 0u64;
2720 for &(tw, th) in &TARGETS {
2721 for &(sw, sh) in &sources {
2722 parity_check(ConstraintMode::Distort, sw, sh, tw, th);
2723 count += 1;
2724 }
2725 }
2726 assert!(count > 500, "Expected >500 combinations, got {count}");
2727 }
2728
2729 #[test]
2730 fn parity_within() {
2731 let sources = parity_source_sizes();
2732 let mut count = 0u64;
2733 for &(tw, th) in &TARGETS {
2734 for &(sw, sh) in &sources {
2735 parity_check(ConstraintMode::Within, sw, sh, tw, th);
2736 count += 1;
2737 }
2738 }
2739 assert!(count > 500, "Expected >500 combinations, got {count}");
2740 }
2741
2742 #[test]
2743 fn parity_fit() {
2744 let sources = parity_source_sizes();
2745 let mut count = 0u64;
2746 for &(tw, th) in &TARGETS {
2747 for &(sw, sh) in &sources {
2748 parity_check(ConstraintMode::Fit, sw, sh, tw, th);
2749 count += 1;
2750 }
2751 }
2752 assert!(count > 500, "Expected >500 combinations, got {count}");
2753 }
2754
2755 #[test]
2756 fn parity_fit_crop() {
2757 let sources = parity_source_sizes();
2758 let mut count = 0u64;
2759 for &(tw, th) in &TARGETS {
2760 for &(sw, sh) in &sources {
2761 parity_check(ConstraintMode::FitCrop, sw, sh, tw, th);
2762 count += 1;
2763 }
2764 }
2765 assert!(count > 500, "Expected >500 combinations, got {count}");
2766 }
2767
2768 #[test]
2769 fn parity_within_crop() {
2770 let sources = parity_source_sizes();
2771 let mut count = 0u64;
2772 for &(tw, th) in &TARGETS {
2773 for &(sw, sh) in &sources {
2774 parity_check(ConstraintMode::WithinCrop, sw, sh, tw, th);
2775 count += 1;
2776 }
2777 }
2778 assert!(count > 500, "Expected >500 combinations, got {count}");
2779 }
2780
2781 #[test]
2782 fn parity_fit_pad() {
2783 let sources = parity_source_sizes();
2784 let mut count = 0u64;
2785 for &(tw, th) in &TARGETS {
2786 for &(sw, sh) in &sources {
2787 parity_check(ConstraintMode::FitPad, sw, sh, tw, th);
2788 count += 1;
2789 }
2790 }
2791 assert!(count > 500, "Expected >500 combinations, got {count}");
2792 }
2793
2794 #[test]
2795 fn parity_within_pad() {
2796 let sources = parity_source_sizes();
2797 let mut count = 0u64;
2798 for &(tw, th) in &TARGETS {
2799 for &(sw, sh) in &sources {
2800 parity_check(ConstraintMode::WithinPad, sw, sh, tw, th);
2801 count += 1;
2802 }
2803 }
2804 assert!(count > 500, "Expected >500 combinations, got {count}");
2805 }
2806
2807 #[test]
2808 fn parity_pad_within() {
2809 let sources = parity_source_sizes();
2810 let mut count = 0u64;
2811 for &(tw, th) in &TARGETS {
2812 for &(sw, sh) in &sources {
2813 parity_check(ConstraintMode::PadWithin, sw, sh, tw, th);
2814 count += 1;
2815 }
2816 }
2817 assert!(count > 500, "Expected >500 combinations, got {count}");
2818 }
2819
2820 #[test]
2821 fn parity_aspect_crop() {
2822 let sources = parity_source_sizes();
2823 let mut count = 0u64;
2824 for &(tw, th) in &TARGETS {
2825 for &(sw, sh) in &sources {
2826 parity_check(ConstraintMode::AspectCrop, sw, sh, tw, th);
2827 count += 1;
2828 }
2829 }
2830 assert!(count > 500, "Expected >500 combinations, got {count}");
2831 }
2832}