1 | use std::f64; |
2 | |
3 | use 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)] |
10 | pub 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 | |
19 | impl 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 | |
27 | impl 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)] |
37 | pub 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)] |
91 | pub struct TransformListParser<'a> { |
92 | stream: Stream<'a>, |
93 | rotate_ts: Option<(f64, f64)>, |
94 | last_angle: Option<f64>, |
95 | } |
96 | |
97 | impl<'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 | |
107 | impl<'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 | |
137 | impl<'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 | |
223 | impl 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)] |
265 | fn 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)] |
278 | mod 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 | |