| 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 | |