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