rsvg/
aspect_ratio.rs

1//! Handling of `preserveAspectRatio` values.
2//!
3//! This module handles `preserveAspectRatio` values [per the SVG specification][spec].
4//! We have an [`AspectRatio`] struct which encapsulates such a value.
5//!
6//! ```
7//! # use rsvg::doctest_only::AspectRatio;
8//! # use rsvg::doctest_only::Parse;
9//! assert_eq!(
10//!     AspectRatio::parse_str("xMidYMid").unwrap(),
11//!     AspectRatio::default()
12//! );
13//! ```
14//!
15//! [spec]: https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute
16
17use 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/// Representation of `preserveAspectRatio` values.
81///
82/// <https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute>
83#[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    /// Produces the equivalent of `preserveAspectRatio="none"`.
100    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    /// Computes the viewport to viewbox transformation.
145    ///
146    /// Given a viewport, returns a transformation that will create a coordinate
147    /// space inside it.  The `(vbox.x0, vbox.y0)` will be mapped to the viewport's
148    /// upper-left corner, and the `(vbox.x1, vbox.y1)` will be mapped to the viewport's
149    /// lower-right corner.
150    ///
151    /// If the vbox or viewport are empty, returns `Ok(None)`.  Per the SVG spec, either
152    /// of those mean that the corresponding element should not be rendered.
153    ///
154    /// If the vbox would create an invalid transform (say, a vbox with huge numbers that
155    /// leads to a near-zero scaling transform), returns an `Err(())`.
156    pub fn viewport_to_viewbox_transform(
157        &self,
158        vbox: Option<ViewBox>,
159        viewport: &Rect,
160    ) -> Result<Option<ValidTransform>, InvalidTransform> {
161        // width or height set to 0 disables rendering of the element
162        // https://www.w3.org/TR/SVG/struct.html#SVGElementWidthAttribute
163        // https://www.w3.org/TR/SVG/struct.html#UseElementWidthAttribute
164        // https://www.w3.org/TR/SVG/struct.html#ImageElementWidthAttribute
165        // https://www.w3.org/TR/SVG/painting.html#MarkerWidthAttribute
166
167        if viewport.is_empty() {
168            return Ok(None);
169        }
170
171        // the preserveAspectRatio attribute is only used if viewBox is specified
172        // https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute
173        let transform = if let Some(vbox) = vbox {
174            if vbox.is_empty() {
175                // Width or height of 0 for the viewBox disables rendering of the element
176                // https://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute
177                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}