1use cssparser::{BasicParseError, Parser};
18use std::ops::Deref;
19
20use crate::error::*;
21use crate::parse_identifiers;
22use crate::parsers::Parse;
23use crate::rect::Rect;
24use crate::transform::{Transform, ValidTransform};
25use crate::viewbox::ViewBox;
26
27#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
28enum FitMode {
29 #[default]
30 Meet,
31 Slice,
32}
33
34#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
35enum Align1D {
36 Min,
37 #[default]
38 Mid,
39 Max,
40}
41
42#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
43struct X(Align1D);
44#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
45struct Y(Align1D);
46
47impl Deref for X {
48 type Target = Align1D;
49
50 fn deref(&self) -> &Align1D {
51 &self.0
52 }
53}
54
55impl Deref for Y {
56 type Target = Align1D;
57
58 fn deref(&self) -> &Align1D {
59 &self.0
60 }
61}
62
63impl Align1D {
64 fn compute(self, dest_pos: f64, dest_size: f64, obj_size: f64) -> f64 {
65 match self {
66 Align1D::Min => dest_pos,
67 Align1D::Mid => dest_pos + (dest_size - obj_size) / 2.0,
68 Align1D::Max => dest_pos + dest_size - obj_size,
69 }
70 }
71}
72
73#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
74struct Align {
75 x: X,
76 y: Y,
77 fit: FitMode,
78}
79
80#[derive(Debug, Copy, Clone, PartialEq, Eq)]
84pub struct AspectRatio {
85 defer: bool,
86 align: Option<Align>,
87}
88
89impl Default for AspectRatio {
90 fn default() -> AspectRatio {
91 AspectRatio {
92 defer: false,
93 align: Some(Align::default()),
94 }
95 }
96}
97
98impl AspectRatio {
99 pub fn none() -> AspectRatio {
101 AspectRatio {
102 defer: false,
103 align: None,
104 }
105 }
106
107 pub fn is_slice(&self) -> bool {
108 matches!(
109 self.align,
110 Some(Align {
111 fit: FitMode::Slice,
112 ..
113 })
114 )
115 }
116
117 pub fn compute(&self, vbox: &ViewBox, viewport: &Rect) -> Rect {
118 match self.align {
119 None => *viewport,
120
121 Some(Align { x, y, fit }) => {
122 let (vb_width, vb_height) = vbox.size();
123 let (vp_width, vp_height) = viewport.size();
124
125 let w_factor = vp_width / vb_width;
126 let h_factor = vp_height / vb_height;
127
128 let factor = match fit {
129 FitMode::Meet => w_factor.min(h_factor),
130 FitMode::Slice => w_factor.max(h_factor),
131 };
132
133 let w = vb_width * factor;
134 let h = vb_height * factor;
135
136 let xpos = x.compute(viewport.x0, vp_width, w);
137 let ypos = y.compute(viewport.y0, vp_height, h);
138
139 Rect::new(xpos, ypos, xpos + w, ypos + h)
140 }
141 }
142 }
143
144 pub fn viewport_to_viewbox_transform(
157 &self,
158 vbox: Option<ViewBox>,
159 viewport: &Rect,
160 ) -> Result<Option<ValidTransform>, InvalidTransform> {
161 if viewport.is_empty() {
168 return Ok(None);
169 }
170
171 let transform = if let Some(vbox) = vbox {
174 if vbox.is_empty() {
175 return Ok(None);
178 } else {
179 let r = self.compute(&vbox, viewport);
180 Transform::new_translate(r.x0, r.y0)
181 .pre_scale(r.width() / vbox.width(), r.height() / vbox.height())
182 .pre_translate(-vbox.x0, -vbox.y0)
183 }
184 } else {
185 Transform::new_translate(viewport.x0, viewport.y0)
186 };
187
188 ValidTransform::try_from(transform).map(Some)
189 }
190}
191
192fn parse_align_xy<'i>(parser: &mut Parser<'i, '_>) -> Result<Option<(X, Y)>, BasicParseError<'i>> {
193 use self::Align1D::*;
194
195 parse_identifiers!(
196 parser,
197
198 "none" => None,
199
200 "xMinYMin" => Some((X(Min), Y(Min))),
201 "xMidYMin" => Some((X(Mid), Y(Min))),
202 "xMaxYMin" => Some((X(Max), Y(Min))),
203
204 "xMinYMid" => Some((X(Min), Y(Mid))),
205 "xMidYMid" => Some((X(Mid), Y(Mid))),
206 "xMaxYMid" => Some((X(Max), Y(Mid))),
207
208 "xMinYMax" => Some((X(Min), Y(Max))),
209 "xMidYMax" => Some((X(Mid), Y(Max))),
210 "xMaxYMax" => Some((X(Max), Y(Max))),
211 )
212}
213
214fn parse_fit_mode<'i>(parser: &mut Parser<'i, '_>) -> Result<FitMode, BasicParseError<'i>> {
215 parse_identifiers!(
216 parser,
217 "meet" => FitMode::Meet,
218 "slice" => FitMode::Slice,
219 )
220}
221
222impl Parse for AspectRatio {
223 fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<AspectRatio, ParseError<'i>> {
224 let defer = parser
225 .try_parse(|p| p.expect_ident_matching("defer"))
226 .is_ok();
227
228 let align_xy = parser.try_parse(parse_align_xy)?;
229 let fit = parser.try_parse(parse_fit_mode).unwrap_or_default();
230 let align = align_xy.map(|(x, y)| Align { x, y, fit });
231
232 Ok(AspectRatio { defer, align })
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 use crate::{assert_approx_eq_cairo, float_eq_cairo::ApproxEqCairo};
241
242 #[test]
243 fn aspect_ratio_none() {
244 assert_eq!(AspectRatio::none(), AspectRatio::parse_str("none").unwrap());
245 }
246
247 #[test]
248 fn parsing_invalid_strings_yields_error() {
249 assert!(AspectRatio::parse_str("").is_err());
250 assert!(AspectRatio::parse_str("defer").is_err());
251 assert!(AspectRatio::parse_str("defer foo").is_err());
252 assert!(AspectRatio::parse_str("defer xMidYMid foo").is_err());
253 assert!(AspectRatio::parse_str("xMidYMid foo").is_err());
254 assert!(AspectRatio::parse_str("defer xMidYMid meet foo").is_err());
255 }
256
257 #[test]
258 fn parses_valid_strings() {
259 assert_eq!(
260 AspectRatio::parse_str("defer none").unwrap(),
261 AspectRatio {
262 defer: true,
263 align: None,
264 }
265 );
266
267 assert_eq!(
268 AspectRatio::parse_str("xMidYMid").unwrap(),
269 AspectRatio {
270 defer: false,
271 align: Some(Align {
272 x: X(Align1D::Mid),
273 y: Y(Align1D::Mid),
274 fit: FitMode::Meet,
275 },),
276 }
277 );
278
279 assert_eq!(
280 AspectRatio::parse_str("defer xMidYMid").unwrap(),
281 AspectRatio {
282 defer: true,
283 align: Some(Align {
284 x: X(Align1D::Mid),
285 y: Y(Align1D::Mid),
286 fit: FitMode::Meet,
287 },),
288 }
289 );
290
291 assert_eq!(
292 AspectRatio::parse_str("defer xMinYMax").unwrap(),
293 AspectRatio {
294 defer: true,
295 align: Some(Align {
296 x: X(Align1D::Min),
297 y: Y(Align1D::Max),
298 fit: FitMode::Meet,
299 },),
300 }
301 );
302
303 assert_eq!(
304 AspectRatio::parse_str("defer xMaxYMid meet").unwrap(),
305 AspectRatio {
306 defer: true,
307 align: Some(Align {
308 x: X(Align1D::Max),
309 y: Y(Align1D::Mid),
310 fit: FitMode::Meet,
311 },),
312 }
313 );
314
315 assert_eq!(
316 AspectRatio::parse_str("defer xMinYMax slice").unwrap(),
317 AspectRatio {
318 defer: true,
319 align: Some(Align {
320 x: X(Align1D::Min),
321 y: Y(Align1D::Max),
322 fit: FitMode::Slice,
323 },),
324 }
325 );
326 }
327
328 fn assert_rect_equal(r1: &Rect, r2: &Rect) {
329 assert_approx_eq_cairo!(r1.x0, r2.x0);
330 assert_approx_eq_cairo!(r1.y0, r2.y0);
331 assert_approx_eq_cairo!(r1.x1, r2.x1);
332 assert_approx_eq_cairo!(r1.y1, r2.y1);
333 }
334
335 #[test]
336 fn aligns() {
337 let viewbox = ViewBox::from(Rect::from_size(1.0, 10.0));
338
339 let foo = AspectRatio::parse_str("xMinYMin meet").unwrap();
340 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
341 assert_rect_equal(&foo, &Rect::from_size(0.1, 1.0));
342
343 let foo = AspectRatio::parse_str("xMinYMin slice").unwrap();
344 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
345 assert_rect_equal(&foo, &Rect::from_size(10.0, 100.0));
346
347 let foo = AspectRatio::parse_str("xMinYMid meet").unwrap();
348 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
349 assert_rect_equal(&foo, &Rect::from_size(0.1, 1.0));
350
351 let foo = AspectRatio::parse_str("xMinYMid slice").unwrap();
352 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
353 assert_rect_equal(&foo, &Rect::new(0.0, -49.5, 10.0, 100.0 - 49.5));
354
355 let foo = AspectRatio::parse_str("xMinYMax meet").unwrap();
356 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
357 assert_rect_equal(&foo, &Rect::from_size(0.1, 1.0));
358
359 let foo = AspectRatio::parse_str("xMinYMax slice").unwrap();
360 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
361 assert_rect_equal(&foo, &Rect::new(0.0, -99.0, 10.0, 1.0));
362
363 let foo = AspectRatio::parse_str("xMidYMin meet").unwrap();
364 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
365 assert_rect_equal(&foo, &Rect::new(4.95, 0.0, 4.95 + 0.1, 1.0));
366
367 let foo = AspectRatio::parse_str("xMidYMin slice").unwrap();
368 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
369 assert_rect_equal(&foo, &Rect::from_size(10.0, 100.0));
370
371 let foo = AspectRatio::parse_str("xMidYMid meet").unwrap();
372 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
373 assert_rect_equal(&foo, &Rect::new(4.95, 0.0, 4.95 + 0.1, 1.0));
374
375 let foo = AspectRatio::parse_str("xMidYMid slice").unwrap();
376 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
377 assert_rect_equal(&foo, &Rect::new(0.0, -49.5, 10.0, 100.0 - 49.5));
378
379 let foo = AspectRatio::parse_str("xMidYMax meet").unwrap();
380 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
381 assert_rect_equal(&foo, &Rect::new(4.95, 0.0, 4.95 + 0.1, 1.0));
382
383 let foo = AspectRatio::parse_str("xMidYMax slice").unwrap();
384 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
385 assert_rect_equal(&foo, &Rect::new(0.0, -99.0, 10.0, 1.0));
386
387 let foo = AspectRatio::parse_str("xMaxYMin meet").unwrap();
388 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
389 assert_rect_equal(&foo, &Rect::new(9.9, 0.0, 10.0, 1.0));
390
391 let foo = AspectRatio::parse_str("xMaxYMin slice").unwrap();
392 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
393 assert_rect_equal(&foo, &Rect::from_size(10.0, 100.0));
394
395 let foo = AspectRatio::parse_str("xMaxYMid meet").unwrap();
396 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
397 assert_rect_equal(&foo, &Rect::new(9.9, 0.0, 10.0, 1.0));
398
399 let foo = AspectRatio::parse_str("xMaxYMid slice").unwrap();
400 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
401 assert_rect_equal(&foo, &Rect::new(0.0, -49.5, 10.0, 100.0 - 49.5));
402
403 let foo = AspectRatio::parse_str("xMaxYMax meet").unwrap();
404 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
405 assert_rect_equal(&foo, &Rect::new(9.9, 0.0, 10.0, 1.0));
406
407 let foo = AspectRatio::parse_str("xMaxYMax slice").unwrap();
408 let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
409 assert_rect_equal(&foo, &Rect::new(0.0, -99.0, 10.0, 1.0));
410 }
411
412 #[test]
413 fn empty_viewport() {
414 let a = AspectRatio::default();
415 let t = a
416 .viewport_to_viewbox_transform(
417 Some(ViewBox::parse_str("10 10 40 40").unwrap()),
418 &Rect::from_size(0.0, 0.0),
419 )
420 .unwrap();
421
422 assert_eq!(t, None);
423 }
424
425 #[test]
426 fn empty_viewbox() {
427 let a = AspectRatio::default();
428 let t = a
429 .viewport_to_viewbox_transform(
430 Some(ViewBox::parse_str("10 10 0 0").unwrap()),
431 &Rect::from_size(10.0, 10.0),
432 )
433 .unwrap();
434
435 assert_eq!(t, None);
436 }
437
438 #[test]
439 fn valid_viewport_and_viewbox() {
440 let a = AspectRatio::default();
441 let t = a
442 .viewport_to_viewbox_transform(
443 Some(ViewBox::parse_str("10 10 40 40").unwrap()),
444 &Rect::new(1.0, 1.0, 2.0, 2.0),
445 )
446 .unwrap();
447
448 assert_eq!(
449 t,
450 Some(
451 ValidTransform::try_from(
452 Transform::identity()
453 .pre_translate(1.0, 1.0)
454 .pre_scale(0.025, 0.025)
455 .pre_translate(-10.0, -10.0)
456 )
457 .unwrap()
458 )
459 );
460 }
461
462 #[test]
463 fn invalid_viewbox() {
464 let a = AspectRatio::default();
465 let t = a.viewport_to_viewbox_transform(
466 Some(ViewBox::parse_str("0 0 6E20 540").unwrap()),
467 &Rect::new(1.0, 1.0, 2.0, 2.0),
468 );
469
470 assert_eq!(t, Err(InvalidTransform));
471 }
472}