svgtypes/
transform.rs

1// Copyright 2021 the SVG Types Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use std::f64;
5
6use crate::{Error, Stream};
7
8/// Representation of the [`<transform>`] type.
9///
10/// [`<transform>`]: https://www.w3.org/TR/SVG2/coords.html#InterfaceSVGTransform
11#[derive(Clone, Copy, PartialEq, Debug)]
12#[allow(missing_docs)]
13pub struct Transform {
14    pub a: f64,
15    pub b: f64,
16    pub c: f64,
17    pub d: f64,
18    pub e: f64,
19    pub f: f64,
20}
21
22impl Transform {
23    /// Constructs a new transform.
24    #[inline]
25    pub fn new(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self {
26        Transform { a, b, c, d, e, f }
27    }
28}
29
30impl Default for Transform {
31    #[inline]
32    fn default() -> Transform {
33        Transform::new(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
34    }
35}
36
37/// Transform list token.
38#[derive(Clone, Copy, PartialEq, Debug)]
39#[allow(missing_docs)]
40pub enum TransformListToken {
41    Matrix {
42        a: f64,
43        b: f64,
44        c: f64,
45        d: f64,
46        e: f64,
47        f: f64,
48    },
49    Translate {
50        tx: f64,
51        ty: f64,
52    },
53    Scale {
54        sx: f64,
55        sy: f64,
56    },
57    Rotate {
58        angle: f64,
59    },
60    SkewX {
61        angle: f64,
62    },
63    SkewY {
64        angle: f64,
65    },
66}
67
68/// A pull-based [`<transform-list>`] parser.
69///
70/// # Errors
71///
72/// - Most of the `Error` types can occur.
73///
74/// # Notes
75///
76/// - There are no separate `rotate(<rotate-angle> <cx> <cy>)` type.
77///   It will be automatically split into three `Transform` tokens:
78///   `translate(<cx> <cy>) rotate(<rotate-angle>) translate(-<cx> -<cy>)`.
79///   Just like the spec is stated.
80///
81/// # Examples
82///
83/// ```
84/// use svgtypes::{TransformListParser, TransformListToken};
85///
86/// let mut p = TransformListParser::from("scale(2) translate(10, -20)");
87/// assert_eq!(p.next().unwrap().unwrap(), TransformListToken::Scale { sx: 2.0, sy: 2.0 } );
88/// assert_eq!(p.next().unwrap().unwrap(), TransformListToken::Translate { tx: 10.0, ty: -20.0 } );
89/// assert_eq!(p.next().is_none(), true);
90/// ```
91///
92/// [`<transform-list>`]: https://www.w3.org/TR/SVG11/shapes.html#PointsBNF
93#[derive(Clone, Copy, PartialEq, Debug)]
94pub struct TransformListParser<'a> {
95    stream: Stream<'a>,
96    rotate_ts: Option<(f64, f64)>,
97    last_angle: Option<f64>,
98}
99
100impl<'a> From<&'a str> for TransformListParser<'a> {
101    fn from(text: &'a str) -> Self {
102        TransformListParser {
103            stream: Stream::from(text),
104            rotate_ts: None,
105            last_angle: None,
106        }
107    }
108}
109
110impl Iterator for TransformListParser<'_> {
111    type Item = Result<TransformListToken, Error>;
112
113    fn next(&mut self) -> Option<Self::Item> {
114        if let Some(a) = self.last_angle {
115            self.last_angle = None;
116            return Some(Ok(TransformListToken::Rotate { angle: a }));
117        }
118
119        if let Some((x, y)) = self.rotate_ts {
120            self.rotate_ts = None;
121            return Some(Ok(TransformListToken::Translate { tx: -x, ty: -y }));
122        }
123
124        self.stream.skip_spaces();
125
126        if self.stream.at_end() {
127            // empty attribute is still a valid value
128            return None;
129        }
130
131        let res = self.parse_next();
132        if res.is_err() {
133            self.stream.jump_to_end();
134        }
135
136        Some(res)
137    }
138}
139
140impl TransformListParser<'_> {
141    fn parse_next(&mut self) -> Result<TransformListToken, Error> {
142        let s = &mut self.stream;
143
144        let start = s.pos();
145        let name = s.consume_ascii_ident();
146        s.skip_spaces();
147        s.consume_byte(b'(')?;
148
149        let t = match name.as_bytes() {
150            b"matrix" => TransformListToken::Matrix {
151                a: s.parse_list_number()?,
152                b: s.parse_list_number()?,
153                c: s.parse_list_number()?,
154                d: s.parse_list_number()?,
155                e: s.parse_list_number()?,
156                f: s.parse_list_number()?,
157            },
158            b"translate" => {
159                let x = s.parse_list_number()?;
160                s.skip_spaces();
161
162                let y = if s.is_curr_byte_eq(b')') {
163                    // 'If <ty> is not provided, it is assumed to be zero.'
164                    0.0
165                } else {
166                    s.parse_list_number()?
167                };
168
169                TransformListToken::Translate { tx: x, ty: y }
170            }
171            b"scale" => {
172                let x = s.parse_list_number()?;
173                s.skip_spaces();
174
175                let y = if s.is_curr_byte_eq(b')') {
176                    // 'If <sy> is not provided, it is assumed to be equal to <sx>.'
177                    x
178                } else {
179                    s.parse_list_number()?
180                };
181
182                TransformListToken::Scale { sx: x, sy: y }
183            }
184            b"rotate" => {
185                let a = s.parse_list_number()?;
186                s.skip_spaces();
187
188                if !s.is_curr_byte_eq(b')') {
189                    // 'If optional parameters <cx> and <cy> are supplied, the rotate is about the
190                    // point (cx, cy). The operation represents the equivalent of the following
191                    // specification:
192                    // translate(<cx>, <cy>) rotate(<rotate-angle>) translate(-<cx>, -<cy>).'
193                    let cx = s.parse_list_number()?;
194                    let cy = s.parse_list_number()?;
195                    self.rotate_ts = Some((cx, cy));
196                    self.last_angle = Some(a);
197
198                    TransformListToken::Translate { tx: cx, ty: cy }
199                } else {
200                    TransformListToken::Rotate { angle: a }
201                }
202            }
203            b"skewX" => TransformListToken::SkewX {
204                angle: s.parse_list_number()?,
205            },
206            b"skewY" => TransformListToken::SkewY {
207                angle: s.parse_list_number()?,
208            },
209            _ => {
210                return Err(Error::UnexpectedData(s.calc_char_pos_at(start)));
211            }
212        };
213
214        s.skip_spaces();
215        s.consume_byte(b')')?;
216        s.skip_spaces();
217
218        if s.is_curr_byte_eq(b',') {
219            s.advance(1);
220        }
221
222        Ok(t)
223    }
224}
225
226impl std::str::FromStr for Transform {
227    type Err = Error;
228
229    fn from_str(text: &str) -> Result<Self, Error> {
230        let tokens = TransformListParser::from(text);
231        let mut ts = Transform::default();
232
233        for token in tokens {
234            match token? {
235                TransformListToken::Matrix { a, b, c, d, e, f } => {
236                    ts = multiply(&ts, &Transform::new(a, b, c, d, e, f))
237                }
238                TransformListToken::Translate { tx, ty } => {
239                    ts = multiply(&ts, &Transform::new(1.0, 0.0, 0.0, 1.0, tx, ty))
240                }
241                TransformListToken::Scale { sx, sy } => {
242                    ts = multiply(&ts, &Transform::new(sx, 0.0, 0.0, sy, 0.0, 0.0))
243                }
244                TransformListToken::Rotate { angle } => {
245                    let v = angle.to_radians();
246                    let a = v.cos();
247                    let b = v.sin();
248                    let c = -b;
249                    let d = a;
250                    ts = multiply(&ts, &Transform::new(a, b, c, d, 0.0, 0.0))
251                }
252                TransformListToken::SkewX { angle } => {
253                    let c = angle.to_radians().tan();
254                    ts = multiply(&ts, &Transform::new(1.0, 0.0, c, 1.0, 0.0, 0.0))
255                }
256                TransformListToken::SkewY { angle } => {
257                    let b = angle.to_radians().tan();
258                    ts = multiply(&ts, &Transform::new(1.0, b, 0.0, 1.0, 0.0, 0.0))
259                }
260            }
261        }
262
263        Ok(ts)
264    }
265}
266
267#[inline(never)]
268fn multiply(ts1: &Transform, ts2: &Transform) -> Transform {
269    Transform {
270        a: ts1.a * ts2.a + ts1.c * ts2.b,
271        b: ts1.b * ts2.a + ts1.d * ts2.b,
272        c: ts1.a * ts2.c + ts1.c * ts2.d,
273        d: ts1.b * ts2.c + ts1.d * ts2.d,
274        e: ts1.a * ts2.e + ts1.c * ts2.f + ts1.e,
275        f: ts1.b * ts2.e + ts1.d * ts2.f + ts1.f,
276    }
277}
278
279#[rustfmt::skip]
280#[cfg(test)]
281mod tests {
282    use std::str::FromStr;
283    use super::*;
284
285    macro_rules! test {
286        ($name:ident, $text:expr, $result:expr) => (
287            #[test]
288            fn $name() {
289                let ts = Transform::from_str($text).unwrap();
290                let s = format!("matrix({} {} {} {} {} {})", ts.a, ts.b, ts.c, ts.d, ts.e, ts.f);
291                assert_eq!(s, $result);
292            }
293        )
294    }
295
296    test!(parse_1,
297        "matrix(1 0 0 1 10 20)",
298        "matrix(1 0 0 1 10 20)"
299    );
300
301    test!(parse_2,
302        "translate(10 20)",
303        "matrix(1 0 0 1 10 20)"
304    );
305
306    test!(parse_3,
307        "scale(2 3)",
308        "matrix(2 0 0 3 0 0)"
309    );
310
311    test!(parse_4,
312        "rotate(30)",
313        "matrix(0.8660254037844387 0.49999999999999994 -0.49999999999999994 0.8660254037844387 0 0)"
314    );
315
316    test!(parse_5,
317        "rotate(30 10 20)",
318        "matrix(0.8660254037844387 0.49999999999999994 -0.49999999999999994 0.8660254037844387 11.339745962155611 -2.3205080756887746)"
319    );
320
321    test!(parse_6,
322        "translate(10 15) translate(0 5)",
323        "matrix(1 0 0 1 10 20)"
324    );
325
326    test!(parse_7,
327        "translate(10) scale(2)",
328        "matrix(2 0 0 2 10 0)"
329    );
330
331    test!(parse_8,
332        "translate(25 215) scale(2) skewX(45)",
333        "matrix(2 0 1.9999999999999998 2 25 215)"
334    );
335
336    test!(parse_9,
337        "skewX(45)",
338        "matrix(1 0 0.9999999999999999 1 0 0)"
339    );
340
341    macro_rules! test_err {
342        ($name:ident, $text:expr, $result:expr) => (
343            #[test]
344            fn $name() {
345                let ts = Transform::from_str($text);
346                assert_eq!(ts.unwrap_err().to_string(), $result);
347            }
348        )
349    }
350
351    test_err!(parse_err_1, "text", "unexpected end of stream");
352
353    #[test]
354    fn parse_err_2() {
355        let mut ts = TransformListParser::from("scale(2) text");
356        let _ = ts.next().unwrap();
357        assert_eq!(ts.next().unwrap().unwrap_err().to_string(),
358                   "unexpected end of stream");
359    }
360
361    test_err!(parse_err_3, "???G", "expected '(' not '?' at position 1");
362
363    #[test]
364    fn parse_err_4() {
365        let mut ts = TransformListParser::from(" ");
366        assert!(ts.next().is_none());
367    }
368
369    #[test]
370    fn parse_err_5() {
371        let mut ts = TransformListParser::from("\x01");
372        assert!(ts.next().unwrap().is_err());
373    }
374
375    test_err!(parse_err_6, "rect()", "unexpected data at position 1");
376
377    test_err!(parse_err_7, "scale(2) rect()", "unexpected data at position 10");
378}