Coverage Report

Created: 2026-06-13 06:44

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/librsvg/rsvg/src/aspect_ratio.rs
Line
Count
Source
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
17
use cssparser::{BasicParseError, Parser};
18
use std::ops::Deref;
19
20
use crate::error::*;
21
use crate::parse_identifiers;
22
use crate::parsers::Parse;
23
use crate::rect::Rect;
24
use crate::transform::{Transform, ValidTransform};
25
use crate::viewbox::ViewBox;
26
27
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
28
enum FitMode {
29
    #[default]
30
    Meet,
31
    Slice,
32
}
33
34
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
35
enum Align1D {
36
    Min,
37
    #[default]
38
    Mid,
39
    Max,
40
}
41
42
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
43
struct X(Align1D);
44
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
45
struct Y(Align1D);
46
47
impl Deref for X {
48
    type Target = Align1D;
49
50
88.0k
    fn deref(&self) -> &Align1D {
51
88.0k
        &self.0
52
88.0k
    }
53
}
54
55
impl Deref for Y {
56
    type Target = Align1D;
57
58
88.0k
    fn deref(&self) -> &Align1D {
59
88.0k
        &self.0
60
88.0k
    }
61
}
62
63
impl Align1D {
64
176k
    fn compute(self, dest_pos: f64, dest_size: f64, obj_size: f64) -> f64 {
65
176k
        match self {
66
870
            Align1D::Min => dest_pos,
67
175k
            Align1D::Mid => dest_pos + (dest_size - obj_size) / 2.0,
68
90
            Align1D::Max => dest_pos + dest_size - obj_size,
69
        }
70
176k
    }
71
}
72
73
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
74
struct 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)]
84
pub struct AspectRatio {
85
    defer: bool,
86
    align: Option<Align>,
87
}
88
89
impl Default for AspectRatio {
90
57.1k
    fn default() -> AspectRatio {
91
57.1k
        AspectRatio {
92
57.1k
            defer: false,
93
57.1k
            align: Some(Align::default()),
94
57.1k
        }
95
57.1k
    }
96
}
97
98
impl AspectRatio {
99
    /// Produces the equivalent of `preserveAspectRatio="none"`.
100
0
    pub fn none() -> AspectRatio {
101
0
        AspectRatio {
102
0
            defer: false,
103
0
            align: None,
104
0
        }
105
0
    }
106
107
746
    pub fn is_slice(&self) -> bool {
108
746
        matches!(
109
746
            self.align,
110
            Some(Align {
111
                fit: FitMode::Slice,
112
                ..
113
            })
114
        )
115
746
    }
116
117
88.0k
    pub fn compute(&self, vbox: &ViewBox, viewport: &Rect) -> Rect {
118
88.0k
        match self.align {
119
5
            None => *viewport,
120
121
88.0k
            Some(Align { x, y, fit }) => {
122
88.0k
                let (vb_width, vb_height) = vbox.size();
123
88.0k
                let (vp_width, vp_height) = viewport.size();
124
125
88.0k
                let w_factor = vp_width / vb_width;
126
88.0k
                let h_factor = vp_height / vb_height;
127
128
88.0k
                let factor = match fit {
129
87.6k
                    FitMode::Meet => w_factor.min(h_factor),
130
392
                    FitMode::Slice => w_factor.max(h_factor),
131
                };
132
133
88.0k
                let w = vb_width * factor;
134
88.0k
                let h = vb_height * factor;
135
136
88.0k
                let xpos = x.compute(viewport.x0, vp_width, w);
137
88.0k
                let ypos = y.compute(viewport.y0, vp_height, h);
138
139
88.0k
                Rect::new(xpos, ypos, xpos + w, ypos + h)
140
            }
141
        }
142
88.0k
    }
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
53.4k
    pub fn viewport_to_viewbox_transform(
157
53.4k
        &self,
158
53.4k
        vbox: Option<ViewBox>,
159
53.4k
        viewport: &Rect,
160
53.4k
    ) -> 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
53.4k
        if viewport.is_empty() {
168
0
            return Ok(None);
169
53.4k
        }
170
171
        // the preserveAspectRatio attribute is only used if viewBox is specified
172
        // https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute
173
53.4k
        let transform = if let Some(vbox) = vbox {
174
53.4k
            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
0
                return Ok(None);
178
            } else {
179
53.4k
                let r = self.compute(&vbox, viewport);
180
53.4k
                Transform::new_translate(r.x0, r.y0)
181
53.4k
                    .pre_scale(r.width() / vbox.width(), r.height() / vbox.height())
182
53.4k
                    .pre_translate(-vbox.x0, -vbox.y0)
183
            }
184
        } else {
185
0
            Transform::new_translate(viewport.x0, viewport.y0)
186
        };
187
188
53.4k
        ValidTransform::try_from(transform).map(Some)
189
53.4k
    }
190
}
191
192
3.37k
fn parse_align_xy<'i>(parser: &mut Parser<'i, '_>) -> Result<Option<(X, Y)>, BasicParseError<'i>> {
193
    use self::Align1D::*;
194
195
3.37k
    parse_identifiers!(
196
3.37k
        parser,
197
198
3.21k
        "none" => None,
199
200
3.18k
        "xMinYMin" => Some((X(Min), Y(Min))),
201
2.22k
        "xMidYMin" => Some((X(Mid), Y(Min))),
202
2.22k
        "xMaxYMin" => Some((X(Max), Y(Min))),
203
204
2.15k
        "xMinYMid" => Some((X(Min), Y(Mid))),
205
2.15k
        "xMidYMid" => Some((X(Mid), Y(Mid))),
206
1.79k
        "xMaxYMid" => Some((X(Max), Y(Mid))),
207
208
1.49k
        "xMinYMax" => Some((X(Min), Y(Max))),
209
1.49k
        "xMidYMax" => Some((X(Mid), Y(Max))),
210
1.49k
        "xMaxYMax" => Some((X(Max), Y(Max))),
211
    )
212
3.37k
}
213
214
2.04k
fn parse_fit_mode<'i>(parser: &mut Parser<'i, '_>) -> Result<FitMode, BasicParseError<'i>> {
215
2.04k
    parse_identifiers!(
216
2.04k
        parser,
217
1.64k
        "meet" => FitMode::Meet,
218
861
        "slice" => FitMode::Slice,
219
    )
220
2.04k
}
221
222
impl Parse for AspectRatio {
223
3.37k
    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<AspectRatio, ParseError<'i>> {
224
3.37k
        let defer = parser
225
3.37k
            .try_parse(|p| p.expect_ident_matching("defer"))
226
3.37k
            .is_ok();
227
228
3.37k
        let align_xy = parser.try_parse(parse_align_xy)?;
229
2.04k
        let fit = parser.try_parse(parse_fit_mode).unwrap_or_default();
230
2.04k
        let align = align_xy.map(|(x, y)| Align { x, y, fit });
231
232
2.04k
        Ok(AspectRatio { defer, align })
233
3.37k
    }
234
}
235
236
#[cfg(test)]
237
mod 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
}