/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 | | } |