1use std::fmt;
4use std::str::FromStr;
5
6use crate::utilities::{FormatError, SpanStringTokenizer};
7use crate::{Size, Vector};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
13pub struct PixelSize {
14 pub width: i32,
16 pub height: i32,
18}
19
20impl PixelSize {
21 pub const EMPTY: PixelSize = PixelSize::new(0, 0);
23
24 const FROM_SIZE_CEILING_EPSILON: f64 = 1e-6;
25
26 #[inline]
28 pub const fn new(width: i32, height: i32) -> Self {
29 Self { width, height }
30 }
31
32 #[inline]
34 pub fn aspect_ratio(&self) -> f64 {
35 self.width as f64 / self.height as f64
36 }
37
38 pub fn parse(s: &str) -> Result<Self, FormatError> {
40 match Self::try_parse(s) {
41 Some(result) => Ok(result),
42 None => Err(FormatError::new("Invalid PixelSize.")),
43 }
44 }
45
46 pub fn try_parse(source: &str) -> Option<Self> {
50 if source.is_empty() {
51 return None;
52 }
53
54 SpanStringTokenizer::with_separator(source, ',', Some("Invalid PixelSize."))
55 .scope(|t| {
56 if let Some(w) = t.try_read_int32()? {
57 if let Some(h) = t.try_read_int32()? {
58 return Ok(Some(PixelSize::new(w, h)));
59 }
60 }
61
62 Ok(None)
63 })
64 .ok()
65 .flatten()
66 }
67
68 #[inline]
70 pub const fn equals(&self, other: PixelSize) -> bool {
71 self.width == other.width && self.height == other.height
72 }
73
74 #[inline]
76 pub const fn with_width(&self, width: i32) -> PixelSize {
77 PixelSize::new(width, self.height)
78 }
79
80 #[inline]
82 pub const fn with_height(&self, height: i32) -> PixelSize {
83 PixelSize::new(self.width, height)
84 }
85
86 #[inline]
89 pub fn to_size(&self, scale: f64) -> Size {
90 Size::new(self.width as f64 / scale, self.height as f64 / scale)
91 }
92
93 #[inline]
96 pub fn to_size_vector(&self, scale: Vector) -> Size {
97 Size::new(self.width as f64 / scale.x, self.height as f64 / scale.y)
98 }
99
100 #[inline]
103 pub fn to_size_with_dpi(&self, dpi: f64) -> Size {
104 self.to_size(dpi / 96.0)
105 }
106
107 #[inline]
110 pub fn to_size_with_dpi_vector(&self, dpi: Vector) -> Size {
111 self.to_size_vector(Vector::new(dpi.x / 96.0, dpi.y / 96.0))
112 }
113
114 #[inline]
117 pub fn from_size(size: Size, scale: f64) -> PixelSize {
118 PixelSize::new(
119 (size.width * scale).ceil() as i32,
120 (size.height * scale).ceil() as i32,
121 )
122 }
123
124 #[inline]
129 pub fn from_size_ceiling(size: Size, scale: f64) -> PixelSize {
130 PixelSize::new(
131 Self::ceil_with_epsilon(size.width * scale),
132 Self::ceil_with_epsilon(size.height * scale),
133 )
134 }
135
136 #[inline]
137 fn ceil_with_epsilon(value: f64) -> i32 {
138 let rounded = value.round_ties_even();
139 if (value - rounded).abs() < Self::FROM_SIZE_CEILING_EPSILON {
140 return rounded as i32;
141 }
142 value.ceil() as i32
143 }
144
145 #[inline]
148 pub fn from_size_vector(size: Size, scale: Vector) -> PixelSize {
149 PixelSize::new(
150 (size.width * scale.x).ceil() as i32,
151 (size.height * scale.y).ceil() as i32,
152 )
153 }
154
155 #[inline]
157 pub fn from_size_with_dpi(size: Size, dpi: f64) -> PixelSize {
158 PixelSize::from_size(size, dpi / 96.0)
159 }
160
161 #[inline]
163 pub fn from_size_with_dpi_vector(size: Size, dpi: Vector) -> PixelSize {
164 PixelSize::from_size_vector(size, Vector::new(dpi.x / 96.0, dpi.y / 96.0))
165 }
166}
167
168impl FromStr for PixelSize {
169 type Err = FormatError;
170 #[inline]
171 fn from_str(s: &str) -> Result<Self, FormatError> {
172 PixelSize::parse(s)
173 }
174}
175
176impl fmt::Display for PixelSize {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 write!(f, "{}, {}", self.width, self.height)
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn parse() {
188 let cases: [(&str, PixelSize, Option<&str>); 2] = [
190 ("1024,768", PixelSize::new(1024, 768), None),
191 ("1024x768", PixelSize::default(), Some("Invalid PixelSize.")),
192 ];
193
194 for (source, expected, exception) in cases {
195 let mut error = None;
196 let mut result = PixelSize::default();
197 match PixelSize::parse(source) {
198 Ok(value) => result = value,
199 Err(e) => error = Some(e),
200 }
201
202 assert_eq!(exception, error.as_ref().map(|e| e.message()));
203 assert_eq!(expected, result);
204 }
205 }
206
207 #[test]
208 fn try_parse() {
209 let cases: [(&str, PixelSize); 2] = [
210 ("1024,768", PixelSize::new(1024, 768)),
211 ("1024x768", PixelSize::EMPTY),
212 ];
213
214 for (source, expected) in cases {
215 let result = PixelSize::try_parse(source).unwrap_or(PixelSize::EMPTY);
216
217 assert_eq!(expected, result);
218 }
219 }
220
221 #[test]
222 fn from_size_ceiling_computes_expected_pixels() {
223 let cases: [(i32, f64, i32); 9] = [
224 (10, 1.0, 10),
225 (10, 1.25, 13),
226 (10, 1.5, 15),
227 (10, 1.75, 18),
228 (10, 2.0, 20),
229 (10, 1.125, 12),
230 (8, 1.5, 12),
231 (0, 1.5, 0),
232 (1, 2.5, 3),
233 ];
234
235 for (logical, scale, expected) in cases {
236 let pixel =
237 PixelSize::from_size_ceiling(Size::new(logical as f64, logical as f64), scale);
238
239 assert_eq!(expected, pixel.width);
240 assert_eq!(expected, pixel.height);
241 }
242 }
243
244 #[test]
245 fn from_size_ceiling_snaps_when_within_epsilon() {
246 for scale in [1.5, 2.0, 3.0] {
247 const LOGICAL: i32 = 10;
250 let exact = LOGICAL as f64 * scale;
251 let below = exact - 1e-9;
252 let above = exact + 1e-9;
253 let rounded_below = below / scale;
254 let rounded_above = above / scale;
255
256 let p1 = PixelSize::from_size_ceiling(Size::new(rounded_below, rounded_below), scale);
257 let p2 = PixelSize::from_size_ceiling(Size::new(rounded_above, rounded_above), scale);
258
259 assert_eq!(exact as i32, p1.width);
260 assert_eq!(exact as i32, p2.width);
261 }
262 }
263
264 #[test]
267 fn conversions_and_display() {
268 assert_eq!(PixelSize::new(4, 2).aspect_ratio(), 2.0);
269 assert_eq!(PixelSize::new(4, 2).to_string(), "4, 2");
270 assert_eq!(PixelSize::try_parse(""), None);
271 assert_eq!(PixelSize::try_parse("1,2,3"), None);
272 assert_eq!(PixelSize::try_parse("1"), None);
273 assert_eq!(
274 PixelSize::from_size(Size::new(10.1, 10.0), 1.0),
275 PixelSize::new(11, 10)
276 );
277 assert_eq!(
278 PixelSize::from_size_with_dpi(Size::new(10.0, 10.0), 144.0),
279 PixelSize::new(15, 15)
280 );
281 assert_eq!(PixelSize::new(15, 30).to_size(1.5), Size::new(10.0, 20.0));
282 assert_eq!(
283 PixelSize::new(15, 30).to_size_with_dpi(144.0),
284 Size::new(10.0, 20.0)
285 );
286 }
287}