Skip to main content

use_resolution/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive resolution helpers.
3//!
4//! These helpers classify common resolutions and perform small scaling
5//! calculations without decoding media.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_resolution::{ResolutionClass, classify_resolution, fit_within, megapixels};
11//!
12//! assert_eq!(classify_resolution(1920, 1080).unwrap(), ResolutionClass::FullHd);
13//! assert!((megapixels(1920, 1080).unwrap() - 2.0736).abs() < 1.0e-12);
14//! assert_eq!(fit_within(3840, 2160, 1920, 1080).unwrap(), (1920, 1080));
15//! ```
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ResolutionClass {
19    Sd,
20    Hd,
21    FullHd,
22    QuadHd,
23    UltraHd4k,
24    UltraHd8k,
25    Custom,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ResolutionError {
30    InvalidWidth,
31    InvalidHeight,
32    InvalidMaxWidth,
33    InvalidMaxHeight,
34    InvalidScale,
35    ScaledOutOfRange,
36}
37
38fn validate_dimensions(width: u32, height: u32) -> Result<(u32, u32), ResolutionError> {
39    if width == 0 {
40        return Err(ResolutionError::InvalidWidth);
41    }
42
43    if height == 0 {
44        return Err(ResolutionError::InvalidHeight);
45    }
46
47    Ok((width, height))
48}
49
50fn validate_max_dimensions(max_width: u32, max_height: u32) -> Result<(u32, u32), ResolutionError> {
51    if max_width == 0 {
52        return Err(ResolutionError::InvalidMaxWidth);
53    }
54
55    if max_height == 0 {
56        return Err(ResolutionError::InvalidMaxHeight);
57    }
58
59    Ok((max_width, max_height))
60}
61
62fn validate_scale(scale: f64) -> Result<f64, ResolutionError> {
63    if !scale.is_finite() || scale <= 0.0 {
64        Err(ResolutionError::InvalidScale)
65    } else {
66        Ok(scale)
67    }
68}
69
70pub fn classify_resolution(width: u32, height: u32) -> Result<ResolutionClass, ResolutionError> {
71    let (width, height) = validate_dimensions(width, height)?;
72
73    Ok(if width >= 7_680 && height >= 4_320 {
74        ResolutionClass::UltraHd8k
75    } else if width >= 3_840 && height >= 2_160 {
76        ResolutionClass::UltraHd4k
77    } else if width >= 2_560 && height >= 1_440 {
78        ResolutionClass::QuadHd
79    } else if width >= 1_920 && height >= 1_080 {
80        ResolutionClass::FullHd
81    } else if width >= 1_280 && height >= 720 {
82        ResolutionClass::Hd
83    } else if width >= 640 && height >= 480 {
84        ResolutionClass::Sd
85    } else {
86        ResolutionClass::Custom
87    })
88}
89
90pub fn megapixels(width: u32, height: u32) -> Result<f64, ResolutionError> {
91    Ok(pixels(width, height)? as f64 / 1_000_000.0)
92}
93
94pub fn pixels(width: u32, height: u32) -> Result<u64, ResolutionError> {
95    let (width, height) = validate_dimensions(width, height)?;
96    Ok(u64::from(width) * u64::from(height))
97}
98
99pub fn scale_dimensions(
100    width: u32,
101    height: u32,
102    scale: f64,
103) -> Result<(u32, u32), ResolutionError> {
104    let (width, height) = validate_dimensions(width, height)?;
105    let scale = validate_scale(scale)?;
106    let scaled_width = (f64::from(width) * scale).round();
107    let scaled_height = (f64::from(height) * scale).round();
108
109    if !(1.0..=f64::from(u32::MAX)).contains(&scaled_width)
110        || !(1.0..=f64::from(u32::MAX)).contains(&scaled_height)
111    {
112        return Err(ResolutionError::ScaledOutOfRange);
113    }
114
115    Ok((scaled_width as u32, scaled_height as u32))
116}
117
118pub fn fit_within(
119    width: u32,
120    height: u32,
121    max_width: u32,
122    max_height: u32,
123) -> Result<(u32, u32), ResolutionError> {
124    let (width, height) = validate_dimensions(width, height)?;
125    let (max_width, max_height) = validate_max_dimensions(max_width, max_height)?;
126
127    if width <= max_width && height <= max_height {
128        return Ok((width, height));
129    }
130
131    let scale =
132        (f64::from(max_width) / f64::from(width)).min(f64::from(max_height) / f64::from(height));
133    scale_dimensions(width, height, scale)
134}
135
136#[cfg(test)]
137mod tests {
138    use super::{
139        ResolutionClass, ResolutionError, classify_resolution, fit_within, megapixels, pixels,
140        scale_dimensions,
141    };
142
143    #[test]
144    fn classifies_common_resolution_bands() {
145        assert_eq!(classify_resolution(640, 480).unwrap(), ResolutionClass::Sd);
146        assert_eq!(classify_resolution(1280, 720).unwrap(), ResolutionClass::Hd);
147        assert_eq!(
148            classify_resolution(1920, 1080).unwrap(),
149            ResolutionClass::FullHd
150        );
151        assert_eq!(
152            classify_resolution(3840, 2160).unwrap(),
153            ResolutionClass::UltraHd4k
154        );
155        assert_eq!(
156            classify_resolution(300, 200).unwrap(),
157            ResolutionClass::Custom
158        );
159    }
160
161    #[test]
162    fn computes_pixel_and_scaling_helpers() {
163        assert_eq!(pixels(1920, 1080).unwrap(), 2_073_600);
164        assert!((megapixels(1920, 1080).unwrap() - 2.0736).abs() < 1.0e-12);
165        assert_eq!(scale_dimensions(1920, 1080, 0.5).unwrap(), (960, 540));
166        assert_eq!(fit_within(3840, 2160, 1920, 1080).unwrap(), (1920, 1080));
167        assert_eq!(fit_within(1920, 1080, 1000, 1000).unwrap(), (1000, 563));
168    }
169
170    #[test]
171    fn rejects_invalid_resolution_inputs() {
172        assert_eq!(
173            classify_resolution(0, 1080),
174            Err(ResolutionError::InvalidWidth)
175        );
176        assert_eq!(
177            scale_dimensions(1920, 1080, 0.0),
178            Err(ResolutionError::InvalidScale)
179        );
180        assert_eq!(
181            fit_within(1920, 1080, 0, 1080),
182            Err(ResolutionError::InvalidMaxWidth)
183        );
184    }
185}