aspect_ratio/
lib.rs

1#![warn(missing_docs)]
2
3pub use safe_arithmetic as arithmetic;
4pub use safe_arithmetic::Error;
5
6use safe_arithmetic::Cast;
7
8#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
9pub struct NonUniformScalingFactor {
10    pub x: f64,
11    pub y: f64,
12}
13
14impl Default for NonUniformScalingFactor {
15    fn default() -> Self {
16        Self { x: 1.0, y: 1.0 }
17    }
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, strum::EnumIter)]
21pub enum ScalingFactor {
22    NonUniform(NonUniformScalingFactor),
23    Uniform(f64),
24}
25
26impl ScalingFactor {
27    #[must_use]
28    pub fn is_uniform(&self) -> bool {
29        matches!(self, Self::Uniform(_))
30    }
31
32    #[must_use]
33    pub fn as_uniform(&self) -> Option<f64> {
34        match self {
35            Self::NonUniform { .. } => None,
36            Self::Uniform(x) => Some(*x),
37        }
38    }
39
40    #[must_use]
41    pub fn as_non_uniform(&self) -> Option<f64> {
42        match self {
43            Self::NonUniform { .. } => None,
44            Self::Uniform(x) => Some(*x),
45        }
46    }
47
48    #[must_use]
49    pub fn x(&self) -> f64 {
50        match self {
51            Self::NonUniform(NonUniformScalingFactor { x, .. }) | Self::Uniform(x) => *x,
52        }
53    }
54
55    #[must_use]
56    pub fn y(&self) -> f64 {
57        match self {
58            Self::NonUniform(NonUniformScalingFactor { y, .. }) | Self::Uniform(y) => *y,
59        }
60    }
61}
62
63#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash, strum::EnumIter)]
64pub enum ScalingMode {
65    /// Scale to an exact size.
66    ///
67    /// If both dimensions are given, aspect ratio is ignored.
68    /// If at most a single dimension is given, aspect ratio is kept.
69    ///
70    /// # Example
71    /// ```
72    /// use aspect_ratio::{Size, Bounds};
73    /// assert_eq!(
74    ///     Size::new(200, 200).scale(Bounds::exact().w(300).h(600)).unwrap(),
75    ///     Size::new(300, 600)
76    /// );
77    ///
78    /// assert_eq!(
79    ///     Size::new(200, 100).scale(Bounds::exact().h(150)).unwrap(),
80    ///     Size::new(200, 150)
81    /// );
82    /// ```
83    Exact,
84
85    /// Fit to wxh while keeping aspect ratio, scaling up _or_ down as required.
86    ///
87    /// If at most one dimension is given, the larger image dimension is scaled to
88    /// fit into ``min(w, h)``.
89    ///
90    /// # Example
91    /// ```
92    /// use aspect_ratio::{Size, Bounds};
93    /// assert_eq!(
94    ///     Size::new(200, 200).scale(Bounds::fit().w(400).h(100)).unwrap(),
95    ///     Size::new(100, 100)
96    /// );
97    /// ```
98    Fit,
99
100    /// Fit to cover `(w, h)` while keeping aspect ratio.
101    ///
102    /// If at most one dimension is given, the smallest dimension is scaled up to
103    /// cover ``min(w, h)``.
104    ///
105    /// # Example
106    /// ```
107    /// use aspect_ratio::{Size, Bounds};
108    /// assert_eq!(
109    ///     Size::new(200, 200).scale(Bounds::cover().w(400).h(100)).unwrap(),
110    ///     Size::new(400, 400)
111    /// );
112    /// ```
113    Cover,
114
115    /// Scale to be contained while keeping aspect ratio, **only** scaling down.
116    ///
117    ///
118    /// # Example
119    /// ```
120    /// use aspect_ratio::{Size, Bounds};
121    /// assert_eq!(
122    ///     Size::new(200, 200).scale(Bounds::contain().w(400).h(400)).unwrap(),
123    ///     Size::new(200, 200)
124    /// );
125    /// ```
126    Contain,
127}
128
129impl Default for ScalingMode {
130    fn default() -> Self {
131        Self::Contain
132    }
133}
134
135impl ScalingMode {
136    #[must_use]
137    pub fn iter() -> <Self as strum::IntoEnumIterator>::Iterator {
138        <Self as strum::IntoEnumIterator>::iter()
139    }
140}
141
142#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
143pub struct Bounds {
144    /// width of the image
145    pub width: Option<u32>,
146    /// height of the image
147    pub height: Option<u32>,
148    /// mode of scaling
149    pub mode: Option<ScalingMode>,
150}
151
152impl Bounds {
153    #[must_use]
154    pub fn new() -> Self {
155        Self::default()
156    }
157
158    #[must_use]
159    pub fn fit() -> Self {
160        Self::default().mode(ScalingMode::Fit)
161    }
162
163    #[must_use]
164    pub fn cover() -> Self {
165        Self::default().mode(ScalingMode::Cover)
166    }
167
168    #[must_use]
169    pub fn contain() -> Self {
170        Self::default().mode(ScalingMode::Contain)
171    }
172
173    #[must_use]
174    pub fn exact() -> Self {
175        Self::default().mode(ScalingMode::Exact)
176    }
177
178    #[must_use]
179    pub fn mode(mut self, mode: impl Into<Option<ScalingMode>>) -> Self {
180        self.mode = mode.into();
181        self
182    }
183
184    #[must_use]
185    pub fn w(mut self, width: impl Into<Option<u32>>) -> Self {
186        self.width = width.into();
187        self
188    }
189
190    #[must_use]
191    pub fn h(mut self, height: impl Into<Option<u32>>) -> Self {
192        self.height = height.into();
193        self
194    }
195
196    #[must_use]
197    pub fn max_width(self, width: impl Into<Option<u32>>) -> Self {
198        self.w(width)
199    }
200
201    #[must_use]
202    pub fn max_height(self, height: impl Into<Option<u32>>) -> Self {
203        self.h(height)
204    }
205
206    #[must_use]
207    pub fn max_dim(self, dim: impl Into<Option<u32>>) -> Self {
208        self.max_dimension(dim)
209    }
210
211    #[must_use]
212    pub fn max_dimension(mut self, dim: impl Into<Option<u32>>) -> Self {
213        let dim = dim.into();
214        self = self.w(dim);
215        self = self.h(dim);
216        self
217    }
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
221pub struct Size {
222    /// width
223    pub width: u32,
224    /// height
225    pub height: u32,
226}
227
228impl Size {
229    #[must_use]
230    pub fn new(width: u32, height: u32) -> Self {
231        Self { width, height }
232    }
233
234    #[must_use]
235    pub fn w(mut self, width: u32) -> Self {
236        self.width = width;
237        self
238    }
239
240    #[must_use]
241    pub fn h(mut self, height: u32) -> Self {
242        self.height = height;
243        self
244    }
245
246    #[must_use]
247    pub fn width(self, width: u32) -> Self {
248        self.w(width)
249    }
250
251    #[must_use]
252    pub fn height(self, height: u32) -> Self {
253        self.h(height)
254    }
255}
256
257impl std::fmt::Display for Size {
258    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
259        write!(f, "{}x{}", self.width, self.height)
260    }
261}
262
263impl Size {
264    /// Scale size to bounds.
265    ///
266    /// # Errors
267    /// If an arithmetic error (e.g. division by zero) is encountered.
268    #[inline]
269    pub fn scale(self, bounds: Bounds) -> Result<Self, Error> {
270        let mode = bounds.mode.unwrap_or_default();
271        match bounds {
272            // unbounded
273            Bounds {
274                width: None,
275                height: None,
276                ..
277            } => Ok(self),
278            // single dimension is bounded
279            Bounds {
280                width: None,
281                height: Some(height),
282                ..
283            } => self.scale_to(
284                Size {
285                    width: self.width,
286                    height,
287                },
288                mode,
289            ),
290            Bounds {
291                width: Some(width),
292                height: None,
293                ..
294            } => self.scale_to(
295                Size {
296                    width,
297                    height: self.height,
298                },
299                mode,
300            ),
301            // all dimensions bounded
302            Bounds {
303                width: Some(width),
304                height: Some(height),
305                ..
306            } => self.scale_to(Size { width, height }, mode),
307        }
308    }
309
310    /// Maximum dimension.
311    ///
312    /// The maximum dimension is computed as the maximum of the width and height.
313    #[inline]
314    #[must_use]
315    pub fn max_dim(&self) -> u32 {
316        self.width.max(self.height)
317    }
318
319    /// Minimum dimension
320    ///
321    /// The minimum dimension is computed as the minimum of the width and height.
322    #[inline]
323    #[must_use]
324    pub fn min_dim(&self) -> u32 {
325        self.width.min(self.height)
326    }
327
328    /// Compute aspect-ratio of the size.
329    ///
330    /// # Errors
331    /// If an arithmetic error (e.g. division by zero) is encountered.
332    #[inline]
333    pub fn aspect_ratio(&self) -> Result<f64, Error> {
334        let width = f64::from(self.width);
335        let height = f64::from(self.height);
336        let ratio = safe_arithmetic::ops::CheckedDiv::checked_div(width, height)?;
337        Ok(ratio)
338    }
339
340    /// Compute the scaling factor to scale `self` to the given size.
341    ///
342    /// # Errors
343    /// If an arithmetic error (e.g. division by zero) is encountered.
344    #[inline]
345    pub fn scaling_factor(
346        &self,
347        size: impl Into<Size>,
348        mode: ScalingMode,
349    ) -> Result<ScalingFactor, Error> {
350        let target = size.into();
351        let target_width = f64::from(target.width);
352        let width = f64::from(self.width);
353        let target_height = f64::from(target.height);
354        let height = f64::from(self.height);
355
356        let width_ratio = safe_arithmetic::ops::CheckedDiv::checked_div(target_width, width)?;
357        let height_ratio = safe_arithmetic::ops::CheckedDiv::checked_div(target_height, height)?;
358
359        let factor = match mode {
360            ScalingMode::Exact => ScalingFactor::NonUniform(NonUniformScalingFactor {
361                x: width_ratio,
362                y: height_ratio,
363            }),
364            ScalingMode::Cover => ScalingFactor::Uniform(f64::max(width_ratio, height_ratio)),
365            ScalingMode::Fit => ScalingFactor::Uniform(f64::min(width_ratio, height_ratio)),
366            ScalingMode::Contain => {
367                ScalingFactor::Uniform(f64::min(f64::min(width_ratio, height_ratio), 1.0))
368            }
369        };
370        Ok(factor)
371    }
372
373    /// Scale width and height of `self` by a scaling factor.
374    ///
375    /// # Errors
376    /// If an arithmetic error (e.g. division by zero) is encountered.
377    #[inline]
378    pub fn scale_by<F, R>(self, sx: F, sy: F) -> Result<Self, Error>
379    where
380        R: safe_arithmetic::RoundingMode,
381        F: safe_arithmetic::Cast + safe_arithmetic::Type,
382    {
383        let sx: f64 = sx.cast()?;
384        let sy: f64 = sy.cast()?;
385        let width: f64 = self.width.cast()?;
386        let height: f64 = self.height.cast()?;
387        let width = safe_arithmetic::ops::CheckedMul::checked_mul(width, sx)?;
388        let height = safe_arithmetic::ops::CheckedMul::checked_mul(height, sy)?;
389        // todo: should we allow the size to go zero here?
390        let width: u32 = R::round(width).cast()?;
391        let height: u32 = R::round(height).cast()?;
392        Ok(Self { width, height })
393    }
394
395    /// Scale `self` to the given size.
396    ///
397    /// # Errors
398    /// If an arithmetic error (e.g. division by zero) is encountered.
399    #[inline]
400    pub fn scale_to(self, size: impl Into<Size>, mode: ScalingMode) -> Result<Self, Error> {
401        let target = size.into();
402        if mode == ScalingMode::Exact {
403            return Ok(target);
404        }
405        let factor = self.scaling_factor(target, mode)?;
406        let scaled = self.scale_by::<_, safe_arithmetic::Ceil>(factor.x(), factor.y())?;
407        Ok(scaled)
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::{Bounds, ScalingMode, Size};
414    use color_eyre::eyre;
415    use similar_asserts::assert_eq as sim_assert_eq;
416
417    static INIT: std::sync::Once = std::sync::Once::new();
418
419    /// Initialize test
420    ///
421    /// This ensures `color_eyre` is setup once.
422    pub fn init() {
423        INIT.call_once(|| {
424            color_eyre::install().ok();
425        });
426    }
427
428    #[test]
429    fn scale_unbounded() -> eyre::Result<()> {
430        crate::tests::init();
431        for mode in ScalingMode::iter().map(Some).chain([None]) {
432            sim_assert_eq!(
433                Size::new(200, 200).scale(Bounds::new().mode(mode))?,
434                Size::new(200, 200),
435            );
436        }
437        Ok(())
438    }
439
440    // #[test]
441    // fn scale_bounded_fill() -> eyre::Result<()> {
442    //     crate::tests::init();
443    //
444    //     // fill 200 x 200 to 300 x * -> 300 x 300
445    //     sim_assert_eq!(
446    //         Size::new(200, 200).scale(Bounds::new().w(300).mode(ScalingMode::Fill))?,
447    //         Size::new(300, 300),
448    //     );
449    //
450    //     // fill 400 x 600 to 300 x 300 -> 300 x 300
451    //     sim_assert_eq!(
452    //         Size::new(400, 600).scale(Bounds::new().w(300).h(300).mode(ScalingMode::Fill))?,
453    //         Size::new(300, 450),
454    //     );
455    //     Ok(())
456    // }
457
458    // #[test]
459    // fn scale_bounded_fit() -> eyre::Result<()> {
460    //     crate::tests::init();
461    //
462    //     // fit 200 x 200 to 300 x * -> 300 x 300
463    //     sim_assert_eq!(
464    //         Size::new(200, 200).scale(Bounds::new().w(300).mode(ScalingMode::Fit))?,
465    //         Size::new(300, 300),
466    //     );
467    //
468    //     // fit 400 x 600 to 300 x 300 -> 300 x 300
469    //     sim_assert_eq!(
470    //         Size::new(400, 600).scale(Bounds::new().w(300).h(300).mode(ScalingMode::Fit))?,
471    //         Size::new(200, 300),
472    //     );
473    //     Ok(())
474    // }
475
476    #[test]
477    fn scale_bounded_contain() -> eyre::Result<()> {
478        crate::tests::init();
479
480        // contain 200 x 200 to 300 x 300 -> 200 x 200
481        sim_assert_eq!(
482            Size::new(200, 200).scale(Bounds::new().w(300).h(300).mode(ScalingMode::Contain))?,
483            Size::new(200, 200)
484        );
485
486        // contain 200 x 200 to 200 x * -> 200 x 200
487        sim_assert_eq!(
488            Size::new(200, 200).scale(Bounds::new().w(200).mode(ScalingMode::Contain))?,
489            Size::new(200, 200)
490        );
491
492        // contain 200 x 200 to 100 x * -> 100 x 100
493        sim_assert_eq!(
494            Size::new(200, 200).scale(Bounds::new().w(100).mode(ScalingMode::Contain))?,
495            Size::new(100, 100)
496        );
497
498        // contain 200 x 200 to 100 x * -> 100 x 100
499        sim_assert_eq!(
500            Size::new(200, 200).scale(Bounds::new().w(100).mode(ScalingMode::Contain))?,
501            Size::new(100, 100)
502        );
503
504        // contain 200 x 400 to * x 500 -> 200 x 400
505        sim_assert_eq!(
506            Size::new(200, 400).scale(Bounds::new().h(500).mode(ScalingMode::Contain))?,
507            Size::new(200, 400)
508        );
509
510        // contain 200 x 400 to * x 200 -> 100 x 200
511        sim_assert_eq!(
512            Size::new(200, 400).scale(Bounds::new().h(200).mode(ScalingMode::Contain))?,
513            Size::new(100, 200)
514        );
515        Ok(())
516    }
517}