Skip to main content

svgtypes/
transform.rs

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