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 Exact,
84
85 Fit,
99
100 Cover,
114
115 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 pub width: Option<u32>,
146 pub height: Option<u32>,
148 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 pub width: u32,
224 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 #[inline]
269 pub fn scale(self, bounds: Bounds) -> Result<Self, Error> {
270 let mode = bounds.mode.unwrap_or_default();
271 match bounds {
272 Bounds {
274 width: None,
275 height: None,
276 ..
277 } => Ok(self),
278 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 Bounds {
303 width: Some(width),
304 height: Some(height),
305 ..
306 } => self.scale_to(Size { width, height }, mode),
307 }
308 }
309
310 #[inline]
314 #[must_use]
315 pub fn max_dim(&self) -> u32 {
316 self.width.max(self.height)
317 }
318
319 #[inline]
323 #[must_use]
324 pub fn min_dim(&self) -> u32 {
325 self.width.min(self.height)
326 }
327
328 #[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 #[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 #[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 let width: u32 = R::round(width).cast()?;
391 let height: u32 = R::round(height).cast()?;
392 Ok(Self { width, height })
393 }
394
395 #[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 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]
477 fn scale_bounded_contain() -> eyre::Result<()> {
478 crate::tests::init();
479
480 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 sim_assert_eq!(
488 Size::new(200, 200).scale(Bounds::new().w(200).mode(ScalingMode::Contain))?,
489 Size::new(200, 200)
490 );
491
492 sim_assert_eq!(
494 Size::new(200, 200).scale(Bounds::new().w(100).mode(ScalingMode::Contain))?,
495 Size::new(100, 100)
496 );
497
498 sim_assert_eq!(
500 Size::new(200, 200).scale(Bounds::new().w(100).mode(ScalingMode::Contain))?,
501 Size::new(100, 100)
502 );
503
504 sim_assert_eq!(
506 Size::new(200, 400).scale(Bounds::new().h(500).mode(ScalingMode::Contain))?,
507 Size::new(200, 400)
508 );
509
510 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}