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
1003058
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
28
enum FitMode {
29
    #[default]
30
501526
    Meet,
31
    Slice,
32
}
33

            
34
2005972
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
35
enum Align1D {
36
    Min,
37
    #[default]
38
1002980
    Mid,
39
    Max,
40
}
41

            
42
1003006
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
43
501503
struct X(Align1D);
44
1003016
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
45
501508
struct Y(Align1D);
46

            
47
impl Deref for X {
48
    type Target = Align1D;
49

            
50
1429
    fn deref(&self) -> &Align1D {
51
        &self.0
52
1429
    }
53
}
54

            
55
impl Deref for Y {
56
    type Target = Align1D;
57

            
58
1431
    fn deref(&self) -> &Align1D {
59
        &self.0
60
1431
    }
61
}
62

            
63
impl Align1D {
64
2862
    fn compute(self, dest_pos: f64, dest_size: f64, obj_size: f64) -> f64 {
65
2862
        match self {
66
70
            Align1D::Min => dest_pos,
67
2740
            Align1D::Mid => dest_pos + (dest_size - obj_size) / 2.0,
68
52
            Align1D::Max => dest_pos + dest_size - obj_size,
69
        }
70
2862
    }
71
}
72

            
73
1003004
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
74
struct Align {
75
501502
    x: X,
76
501502
    y: Y,
77
501502
    fit: FitMode,
78
}
79

            
80
14
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
81
pub struct AspectRatio {
82
7
    defer: bool,
83
7
    align: Option<Align>,
84
}
85

            
86
impl Default for AspectRatio {
87
501476
    fn default() -> AspectRatio {
88
501476
        AspectRatio {
89
            defer: false,
90
501476
            align: Some(Align::default()),
91
        }
92
501476
    }
93
}
94

            
95
impl AspectRatio {
96
    /// Produces the equivalent of `preserveAspectRatio="none"`.
97
15
    pub fn none() -> AspectRatio {
98
15
        AspectRatio {
99
            defer: false,
100
15
            align: None,
101
        }
102
15
    }
103

            
104
108
    pub fn is_slice(&self) -> bool {
105
108
        matches!(
106
108
            self.align,
107
            Some(Align {
108
                fit: FitMode::Slice,
109
                ..
110
            })
111
        )
112
108
    }
113

            
114
1458
    pub fn compute(&self, vbox: &ViewBox, viewport: &Rect) -> Rect {
115
1458
        match self.align {
116
25
            None => *viewport,
117

            
118
1433
            Some(Align { x, y, fit }) => {
119
1433
                let (vb_width, vb_height) = vbox.size();
120
1433
                let (vp_width, vp_height) = viewport.size();
121

            
122
1433
                let w_factor = vp_width / vb_width;
123
1433
                let h_factor = vp_height / vb_height;
124

            
125
1433
                let factor = match fit {
126
1394
                    FitMode::Meet => w_factor.min(h_factor),
127
39
                    FitMode::Slice => w_factor.max(h_factor),
128
                };
129

            
130
1433
                let w = vb_width * factor;
131
1433
                let h = vb_height * factor;
132

            
133
1433
                let xpos = x.compute(viewport.x0, vp_width, w);
134
1433
                let ypos = y.compute(viewport.y0, vp_height, h);
135

            
136
1433
                Rect::new(xpos, ypos, xpos + w, ypos + h)
137
            }
138
        }
139
1458
    }
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
1243
    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
1243
        if viewport.is_empty() {
165
1
            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
1242
        let transform = if let Some(vbox) = vbox {
171
1215
            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
1
                return Ok(None);
175
            } else {
176
1214
                let r = self.compute(&vbox, viewport);
177
3642
                Transform::new_translate(r.x0, r.y0)
178
1214
                    .pre_scale(r.width() / vbox.width(), r.height() / vbox.height())
179
1214
                    .pre_translate(-vbox.x0, -vbox.y0)
180
            }
181
        } else {
182
27
            Transform::new_translate(viewport.x0, viewport.y0)
183
        };
184

            
185
1241
        ValidTransform::try_from(transform).map(Some)
186
1243
    }
187
}
188

            
189
111
fn parse_align_xy<'i>(parser: &mut Parser<'i, '_>) -> Result<Option<(X, Y)>, BasicParseError<'i>> {
190
    use self::Align1D::*;
191

            
192
111
    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
111
}
210

            
211
108
fn parse_fit_mode<'i>(parser: &mut Parser<'i, '_>) -> Result<FitMode, BasicParseError<'i>> {
212
108
    parse_identifiers!(
213
        parser,
214
        "meet" => FitMode::Meet,
215
        "slice" => FitMode::Slice,
216
    )
217
108
}
218

            
219
impl Parse for AspectRatio {
220
111
    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<AspectRatio, ParseError<'i>> {
221
111
        let defer = parser
222
110
            .try_parse(|p| p.expect_ident_matching("defer"))
223
111
            .is_ok();
224

            
225
111
        let align_xy = parser.try_parse(parse_align_xy)?;
226
108
        let fit = parser.try_parse(parse_fit_mode).unwrap_or_default();
227
204
        let align = align_xy.map(|(x, y)| Align { x, y, fit });
228

            
229
108
        Ok(AspectRatio { defer, align })
230
111
    }
231
}
232

            
233
#[cfg(test)]
234
mod tests {
235
    use super::*;
236

            
237
    use crate::{assert_approx_eq_cairo, float_eq_cairo::ApproxEqCairo};
238

            
239
    #[test]
240
2
    fn aspect_ratio_none() {
241
1
        assert_eq!(AspectRatio::none(), AspectRatio::parse_str("none").unwrap());
242
2
    }
243

            
244
    #[test]
245
2
    fn parsing_invalid_strings_yields_error() {
246
1
        assert!(AspectRatio::parse_str("").is_err());
247
1
        assert!(AspectRatio::parse_str("defer").is_err());
248
1
        assert!(AspectRatio::parse_str("defer foo").is_err());
249
1
        assert!(AspectRatio::parse_str("defer xMidYMid foo").is_err());
250
1
        assert!(AspectRatio::parse_str("xMidYMid foo").is_err());
251
1
        assert!(AspectRatio::parse_str("defer xMidYMid meet foo").is_err());
252
2
    }
253

            
254
    #[test]
255
2
    fn parses_valid_strings() {
256
1
        assert_eq!(
257
1
            AspectRatio::parse_str("defer none").unwrap(),
258
            AspectRatio {
259
                defer: true,
260
                align: None,
261
            }
262
        );
263

            
264
1
        assert_eq!(
265
1
            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
1
        assert_eq!(
277
1
            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
1
        assert_eq!(
289
1
            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
1
        assert_eq!(
301
1
            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
1
        assert_eq!(
313
1
            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
2
    }
324

            
325
18
    fn assert_rect_equal(r1: &Rect, r2: &Rect) {
326
18
        assert_approx_eq_cairo!(r1.x0, r2.x0);
327
18
        assert_approx_eq_cairo!(r1.y0, r2.y0);
328
18
        assert_approx_eq_cairo!(r1.x1, r2.x1);
329
18
        assert_approx_eq_cairo!(r1.y1, r2.y1);
330
18
    }
331

            
332
    #[test]
333
2
    fn aligns() {
334
1
        let viewbox = ViewBox::from(Rect::from_size(1.0, 10.0));
335

            
336
1
        let foo = AspectRatio::parse_str("xMinYMin meet").unwrap();
337
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
338
1
        assert_rect_equal(&foo, &Rect::from_size(0.1, 1.0));
339

            
340
1
        let foo = AspectRatio::parse_str("xMinYMin slice").unwrap();
341
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
342
1
        assert_rect_equal(&foo, &Rect::from_size(10.0, 100.0));
343

            
344
1
        let foo = AspectRatio::parse_str("xMinYMid meet").unwrap();
345
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
346
1
        assert_rect_equal(&foo, &Rect::from_size(0.1, 1.0));
347

            
348
1
        let foo = AspectRatio::parse_str("xMinYMid slice").unwrap();
349
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
350
1
        assert_rect_equal(&foo, &Rect::new(0.0, -49.5, 10.0, 100.0 - 49.5));
351

            
352
1
        let foo = AspectRatio::parse_str("xMinYMax meet").unwrap();
353
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
354
1
        assert_rect_equal(&foo, &Rect::from_size(0.1, 1.0));
355

            
356
1
        let foo = AspectRatio::parse_str("xMinYMax slice").unwrap();
357
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
358
1
        assert_rect_equal(&foo, &Rect::new(0.0, -99.0, 10.0, 1.0));
359

            
360
1
        let foo = AspectRatio::parse_str("xMidYMin meet").unwrap();
361
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
362
1
        assert_rect_equal(&foo, &Rect::new(4.95, 0.0, 4.95 + 0.1, 1.0));
363

            
364
1
        let foo = AspectRatio::parse_str("xMidYMin slice").unwrap();
365
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
366
1
        assert_rect_equal(&foo, &Rect::from_size(10.0, 100.0));
367

            
368
1
        let foo = AspectRatio::parse_str("xMidYMid meet").unwrap();
369
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
370
1
        assert_rect_equal(&foo, &Rect::new(4.95, 0.0, 4.95 + 0.1, 1.0));
371

            
372
1
        let foo = AspectRatio::parse_str("xMidYMid slice").unwrap();
373
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
374
1
        assert_rect_equal(&foo, &Rect::new(0.0, -49.5, 10.0, 100.0 - 49.5));
375

            
376
1
        let foo = AspectRatio::parse_str("xMidYMax meet").unwrap();
377
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
378
1
        assert_rect_equal(&foo, &Rect::new(4.95, 0.0, 4.95 + 0.1, 1.0));
379

            
380
1
        let foo = AspectRatio::parse_str("xMidYMax slice").unwrap();
381
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
382
1
        assert_rect_equal(&foo, &Rect::new(0.0, -99.0, 10.0, 1.0));
383

            
384
1
        let foo = AspectRatio::parse_str("xMaxYMin meet").unwrap();
385
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
386
1
        assert_rect_equal(&foo, &Rect::new(9.9, 0.0, 10.0, 1.0));
387

            
388
1
        let foo = AspectRatio::parse_str("xMaxYMin slice").unwrap();
389
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
390
1
        assert_rect_equal(&foo, &Rect::from_size(10.0, 100.0));
391

            
392
1
        let foo = AspectRatio::parse_str("xMaxYMid meet").unwrap();
393
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
394
1
        assert_rect_equal(&foo, &Rect::new(9.9, 0.0, 10.0, 1.0));
395

            
396
1
        let foo = AspectRatio::parse_str("xMaxYMid slice").unwrap();
397
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
398
1
        assert_rect_equal(&foo, &Rect::new(0.0, -49.5, 10.0, 100.0 - 49.5));
399

            
400
1
        let foo = AspectRatio::parse_str("xMaxYMax meet").unwrap();
401
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
402
1
        assert_rect_equal(&foo, &Rect::new(9.9, 0.0, 10.0, 1.0));
403

            
404
1
        let foo = AspectRatio::parse_str("xMaxYMax slice").unwrap();
405
1
        let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0));
406
1
        assert_rect_equal(&foo, &Rect::new(0.0, -99.0, 10.0, 1.0));
407
2
    }
408

            
409
    #[test]
410
2
    fn empty_viewport() {
411
1
        let a = AspectRatio::default();
412
1
        let t = a
413
            .viewport_to_viewbox_transform(
414
1
                Some(ViewBox::parse_str("10 10 40 40").unwrap()),
415
1
                &Rect::from_size(0.0, 0.0),
416
            )
417
            .unwrap();
418

            
419
1
        assert_eq!(t, None);
420
2
    }
421

            
422
    #[test]
423
2
    fn empty_viewbox() {
424
1
        let a = AspectRatio::default();
425
1
        let t = a
426
            .viewport_to_viewbox_transform(
427
1
                Some(ViewBox::parse_str("10 10 0 0").unwrap()),
428
1
                &Rect::from_size(10.0, 10.0),
429
            )
430
            .unwrap();
431

            
432
1
        assert_eq!(t, None);
433
2
    }
434

            
435
    #[test]
436
2
    fn valid_viewport_and_viewbox() {
437
1
        let a = AspectRatio::default();
438
1
        let t = a
439
            .viewport_to_viewbox_transform(
440
1
                Some(ViewBox::parse_str("10 10 40 40").unwrap()),
441
1
                &Rect::new(1.0, 1.0, 2.0, 2.0),
442
            )
443
            .unwrap();
444

            
445
1
        assert_eq!(
446
            t,
447
1
            Some(
448
1
                ValidTransform::try_from(
449
1
                    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
2
    }
458

            
459
    #[test]
460
2
    fn invalid_viewbox() {
461
1
        let a = AspectRatio::default();
462
1
        let t = a.viewport_to_viewbox_transform(
463
1
            Some(ViewBox::parse_str("0 0 6E20 540").unwrap()),
464
1
            &Rect::new(1.0, 1.0, 2.0, 2.0),
465
        );
466

            
467
1
        assert_eq!(t, Err(InvalidTransform));
468
2
    }
469
}