1 | // Copyright 2021 the SVG Types Authors |
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | |
4 | use std::f64; |
5 | |
6 | use 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)] |
13 | pub 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 | |
22 | impl 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 | |
30 | impl Default for Transform { |
31 | #[inline ] |
32 | fn default() -> Transform { |
33 | Transform::new(a:1.0, b:0.0, c:0.0, d:1.0, e:0.0, f:0.0) |
34 | } |
35 | } |
36 | |
37 | /// Transform list token. |
38 | #[derive (Clone, Copy, PartialEq, Debug)] |
39 | #[allow (missing_docs)] |
40 | pub 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)] |
94 | pub struct TransformListParser<'a> { |
95 | stream: Stream<'a>, |
96 | rotate_ts: Option<(f64, f64)>, |
97 | last_angle: Option<f64>, |
98 | } |
99 | |
100 | impl<'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 | |
110 | impl 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 | |
140 | impl 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 | |
226 | impl 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)] |
268 | fn 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)] |
281 | mod 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 | } |
379 | |