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