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#[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    /// Produces the equivalent of `preserveAspectRatio="none"`.
97    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    /// Computes the viewport to viewbox transformation.
142    ///
143    /// Given a viewport, returns a transformation that will create a coordinate
144    /// space inside it.  The `(vbox.x0, vbox.y0)` will be mapped to the viewport's
145    /// upper-left corner, and the `(vbox.x1, vbox.y1)` will be mapped to the viewport's
146    /// lower-right corner.
147    ///
148    /// If the vbox or viewport are empty, returns `Ok(None)`.  Per the SVG spec, either
149    /// of those mean that the corresponding element should not be rendered.
150    ///
151    /// If the vbox would create an invalid transform (say, a vbox with huge numbers that
152    /// leads to a near-zero scaling transform), returns an `Err(())`.
153    pub fn viewport_to_viewbox_transform(
154        &self,
155        vbox: Option<ViewBox>,
156        viewport: &Rect,
157    ) -> Result<Option<ValidTransform>, InvalidTransform> {
158        // width or height set to 0 disables rendering of the element
159        // https://www.w3.org/TR/SVG/struct.html#SVGElementWidthAttribute
160        // https://www.w3.org/TR/SVG/struct.html#UseElementWidthAttribute
161        // https://www.w3.org/TR/SVG/struct.html#ImageElementWidthAttribute
162        // https://www.w3.org/TR/SVG/painting.html#MarkerWidthAttribute
163
164        if viewport.is_empty() {
165            return Ok(None);
166        }
167
168        // the preserveAspectRatio attribute is only used if viewBox is specified
169        // https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute
170        let transform = if let Some(vbox) = vbox {
171            if vbox.is_empty() {
172                // Width or height of 0 for the viewBox disables rendering of the element
173                // https://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute
174                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}