1use core::ops::Mul;
2use std::f64;
3
4use lightningcss::properties::{
5 svg::{SVGPaint, StrokeDasharray},
6 transform::{Matrix, TransformList},
7};
8use oxvg_ast::{
9 element::Element,
10 get_attribute, get_attribute_mut, get_computed_style, get_computed_style_css, has_attribute,
11 is_attribute,
12 style::{ComputedStyles, Mode},
13 visitor::{Context, PrepareOutcome, Visitor},
14};
15use oxvg_collections::attribute::{
16 core::SVGTransformList,
17 inheritable::Inheritable,
18 presentation::{LengthPercentage, VectorEffect},
19 Attr, AttrId,
20};
21use oxvg_path::{command::Data, convert, Path};
22#[cfg(feature = "serde")]
23use serde::{Deserialize, Serialize};
24
25#[cfg(feature = "wasm")]
26use tsify::Tsify;
27
28use crate::error::JobsError;
29
30#[cfg_attr(feature = "wasm", derive(Tsify))]
31#[cfg_attr(feature = "napi", napi(object))]
32#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
33#[derive(Default, Clone, Debug)]
34#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
35pub struct ApplyTransforms {
57 #[cfg_attr(feature = "wasm", tsify(optional))]
59 pub transform_precision: Option<f64>,
60 #[cfg_attr(feature = "serde", serde(default = "bool::default"))]
62 pub apply_transforms_stroked: bool,
63}
64
65impl<'input, 'arena> Visitor<'input, 'arena> for ApplyTransforms {
66 type Error = JobsError<'input>;
67
68 fn prepare(
69 &self,
70 document: &Element<'input, 'arena>,
71 context: &mut Context<'input, 'arena, '_>,
72 ) -> Result<PrepareOutcome, Self::Error> {
73 context.query_has_stylesheet(document);
74 context.query_has_script(document);
75 Ok(PrepareOutcome::none)
76 }
77
78 fn element(
79 &self,
80 element: &Element<'input, 'arena>,
81 context: &mut Context<'input, 'arena, '_>,
82 ) -> Result<(), Self::Error> {
83 if !has_attribute!(element, D) {
84 log::debug!("run: path has no d");
85 return Ok(());
86 }
87 for mut attr in element.attributes().into_iter_mut() {
88 if is_attribute!(attr, Id | Style) {
89 log::debug!("run: element has id");
90 return Ok(());
91 }
92
93 let mut references_props = false;
94 let mut value = attr.value_mut();
95 value.visit_id(|_| references_props = true);
96 if references_props {
97 log::debug!("run: element id reference");
98 return Ok(());
99 }
100
101 let mut references_url = false;
102 value.visit_url(|url| references_url = references_url || url.starts_with('#'));
103 if references_url {
104 log::debug!("run: element url reference");
105 return Ok(());
106 }
107 }
108
109 let computed_styles = ComputedStyles::default()
110 .with_all(element, &context.query_has_stylesheet_result)
111 .map_err(JobsError::ComputedStylesError)?;
112 let Some(transform_attr) = get_attribute!(element, Transform) else {
113 log::debug!("run: element has no transform");
114 return Ok(());
115 };
116 let Some(transform) = transform_attr.option_ref() else {
117 log::debug!("run: cannot handle inherit transform");
118 return Ok(());
119 };
120 if transform.0.is_empty() {
121 log::debug!("run: cannot handle empty transform");
122 return Ok(());
123 }
124 if let Some((css_transform, Mode::Static)) =
125 get_computed_style_css!(computed_styles, Transform(None))
126 {
127 if (&css_transform)
128 .try_into()
129 .ok()
130 .is_none_or(|css_transform: SVGTransformList| {
131 transform.to_matrix_2d() != css_transform.to_matrix_2d()
132 })
133 {
134 log::debug!("run: another transform is applied to this element");
135 return Ok(());
136 }
137 }
138
139 let stroke = get_computed_style!(computed_styles, Stroke);
140 if matches!(stroke, Some((_, Mode::Dynamic))) {
141 log::debug!("run: cannot handle dynamic stroke");
142 return Ok(());
143 }
144
145 let stroke_width = get_computed_style!(computed_styles, StrokeWidth);
146 if matches!(stroke_width, Some((_, Mode::Dynamic))) {
147 log::debug!("run: cannot handle dynamic stroke_width");
148 return Ok(());
149 }
150 let stroke_width = stroke_width.map(|(stroke_width, _)| stroke_width);
151
152 let css_transform: TransformList = transform.clone().into();
153 let Some(matrix) = css_transform.to_matrix() else {
154 log::debug!("run: cannot get matrix");
155 return Ok(());
156 };
157 let Some(matrix) = matrix.to_matrix2d() else {
158 log::debug!("run: cannot handle matrix");
159 return Ok(());
160 };
161 let matrix = matrix32_to_slice(&matrix);
162
163 drop(transform_attr);
164 if let Some((Inheritable::Defined(stroke), Mode::Static)) = stroke {
165 if self.apply_stroked(&matrix, &stroke, stroke_width, element) {
166 return Ok(());
167 }
168 }
169
170 let Some(mut d) = get_attribute_mut!(element, D) else {
171 unreachable!();
172 };
173 let path = &mut d.0;
174 apply_matrix_to_path_data(path, &matrix);
175 convert::cleanup_unpositioned(path);
176 log::debug!("new d <- {path}");
177 drop(d);
178 element.remove_attribute(&AttrId::Transform);
179 Ok(())
180 }
181}
182
183impl ApplyTransforms {
184 #[allow(clippy::float_cmp, clippy::cast_possible_truncation)]
185 fn apply_stroked(
186 &self,
187 matrix: &[f64; 6],
188 stroke: &SVGPaint,
189 stroke_width: Option<Inheritable<LengthPercentage>>,
190 element: &Element,
191 ) -> bool {
192 if matches!(stroke, SVGPaint::None) {
193 return false;
194 }
195 if self.apply_transforms_stroked {
196 log::debug!("apply_stroked: not applying transformed stroke");
197 return true;
198 }
199 if (matrix[0] != matrix[3] || matrix[1] != -matrix[2])
200 && (matrix[0] != -matrix[3] || matrix[1] != matrix[2])
201 {
202 log::debug!("apply_stroked: stroke cannot be applied with disproportional scale/skew");
203 return true;
204 }
205
206 if let Some(vector_effect) = get_attribute!(element, VectorEffect) {
207 if matches!(&*vector_effect, VectorEffect::NonScalingStroke) {
208 return false;
209 }
210 }
211
212 let mut scale = f64::sqrt((matrix[0] * matrix[0]) + (matrix[1] * matrix[1])); if let Some(transform_precision) = self.transform_precision {
214 scale = f64::round(scale * transform_precision) / transform_precision;
215 }
216 if scale == 1.0 {
217 return false;
218 }
219 let scale = scale as f32;
220
221 let stroke_width = match stroke_width {
222 Some(Inheritable::Defined(value)) => LengthPercentage(value.0.mul(scale)),
223 None | Some(Inheritable::Inherited) => LengthPercentage::px(scale),
225 };
226 element.set_attribute(Attr::StrokeWidth(Inheritable::Defined(stroke_width)));
227
228 if let Some(Inheritable::Defined(LengthPercentage(stroke_dashoffset))) =
229 get_attribute_mut!(element, StrokeDashoffset).as_deref_mut()
230 {
231 *stroke_dashoffset = stroke_dashoffset.clone().mul(scale);
232 }
233 if let Some(Inheritable::Defined(StrokeDasharray::Values(stroke_dasharray))) =
234 get_attribute_mut!(element, StrokeDasharray).as_deref_mut()
235 {
236 stroke_dasharray
237 .iter_mut()
238 .for_each(|dash| *dash = dash.clone().mul(scale));
239 }
240
241 false
242 }
243}
244
245fn matrix32_to_slice(matrix: &Matrix<f32>) -> [f64; 6] {
246 [
247 f64::from(matrix.a),
248 f64::from(matrix.b),
249 f64::from(matrix.c),
250 f64::from(matrix.d),
251 f64::from(matrix.e),
252 f64::from(matrix.f),
253 ]
254}
255
256#[allow(clippy::too_many_lines)]
257fn apply_matrix_to_path_data(path_data: &mut Path, matrix: &[f64; 6]) {
258 log::debug!("applying matrix: {:?}", matrix);
259 let mut start = [0.0; 2];
260 let mut cursor = [0.0; 2];
261 if let Some(data) = path_data.0.get_mut(0) {
262 if let Data::MoveBy(args) = data {
263 *data = Data::MoveTo(*args);
264 }
265 }
266
267 path_data.0.iter_mut().for_each(|data| {
268 if let Data::Implicit(_) = data {
269 *data = data.as_explicit().clone();
270 }
271 match data {
272 Data::HorizontalLineTo(args) => *data = Data::LineTo([args[0], cursor[1]]),
273 Data::HorizontalLineBy(args) => *data = Data::LineBy([args[0], 0.0]),
274 Data::VerticalLineTo(args) => *data = Data::LineTo([cursor[0], args[0]]),
275 Data::VerticalLineBy(args) => *data = Data::LineBy([0.0, args[0]]),
276 _ => {}
277 }
278 match data {
279 Data::MoveTo(args) => {
280 cursor[0] = args[0];
281 cursor[1] = args[1];
282 start[0] = cursor[0];
283 start[1] = cursor[1];
284 *args = transform_absolute_point(matrix, args[0], args[1]);
285 }
286 Data::MoveBy(args) => {
287 cursor[0] += args[0];
288 cursor[1] += args[1];
289 start[0] = cursor[0];
290 start[1] = cursor[1];
291 *args = transform_relative_point(matrix, args[0], args[1]);
292 }
293 Data::LineTo(args) | Data::SmoothQuadraticBezierTo(args) => {
294 cursor[0] = args[0];
295 cursor[1] = args[1];
296 *args = transform_absolute_point(matrix, args[0], args[1]);
297 }
298 Data::LineBy(args) | Data::SmoothQuadraticBezierBy(args) => {
299 cursor[0] += args[0];
300 cursor[1] += args[1];
301 *args = transform_relative_point(matrix, args[0], args[1]);
302 }
303 Data::CubicBezierTo(args) => {
304 cursor[0] = args[4];
305 cursor[1] = args[5];
306 let p1 = transform_absolute_point(matrix, args[0], args[1]);
307 let p2 = transform_absolute_point(matrix, args[2], args[3]);
308 let p = transform_absolute_point(matrix, args[4], args[5]);
309 *args = [p1[0], p1[1], p2[0], p2[1], p[0], p[1]];
310 }
311 Data::CubicBezierBy(args) => {
312 cursor[0] += args[4];
313 cursor[1] += args[5];
314 let p1 = transform_relative_point(matrix, args[0], args[1]);
315 let p2 = transform_relative_point(matrix, args[2], args[3]);
316 let p = transform_relative_point(matrix, args[4], args[5]);
317 *args = [p1[0], p1[1], p2[0], p2[1], p[0], p[1]];
318 }
319 Data::SmoothBezierTo(args) | Data::QuadraticBezierTo(args) => {
320 cursor[0] = args[2];
321 cursor[1] = args[3];
322 let p1 = transform_absolute_point(matrix, args[0], args[1]);
323 let p = transform_absolute_point(matrix, args[2], args[3]);
324 *args = [p1[0], p1[1], p[0], p[1]];
325 }
326 Data::SmoothBezierBy(args) | Data::QuadraticBezierBy(args) => {
327 cursor[0] += args[2];
328 cursor[1] += args[3];
329 let p1 = transform_relative_point(matrix, args[0], args[1]);
330 let p = transform_relative_point(matrix, args[2], args[3]);
331 *args = [p1[0], p1[1], p[0], p[1]];
332 }
333 Data::ArcTo(args) => {
334 transform_arc(cursor, args, matrix);
335 cursor[0] = args[5];
336 cursor[1] = args[6];
337 if f64::abs(args[2]) > 80.0 {
338 args.swap(0, 1);
339 args[2] += if args[2] > 0.0 { -90.0 } else { 90.0 };
340 }
341 let p = transform_absolute_point(matrix, args[5], args[6]);
342 args[5] = p[0];
343 args[6] = p[1];
344 }
345 Data::ArcBy(args) => {
346 transform_arc([0.0; 2], args, matrix);
347 cursor[0] += args[5];
348 cursor[1] += args[6];
349 if f64::abs(args[2]) > 80.0 {
350 args.swap(0, 1);
351 args[2] += if args[2] > 0.0 { -90.0 } else { 90.0 };
352 }
353 let p = transform_relative_point(matrix, args[5], args[6]);
354 args[5] = p[0];
355 args[6] = p[1];
356 }
357 Data::ClosePath => {
358 cursor[0] = start[0];
359 cursor[1] = start[1];
360 }
361 Data::HorizontalLineBy(_)
362 | Data::HorizontalLineTo(_)
363 | Data::VerticalLineBy(_)
364 | Data::VerticalLineTo(_)
365 | Data::Implicit(_) => {
366 unreachable!("Reached destroyed command type")
367 }
368 }
369 });
370}
371
372fn transform_absolute_point(matrix: &[f64; 6], x: f64, y: f64) -> [f64; 2] {
373 [
374 matrix[0] * x + matrix[2] * y + matrix[4],
375 matrix[1] * x + matrix[3] * y + matrix[5],
376 ]
377}
378
379fn transform_relative_point(matrix: &[f64; 6], x: f64, y: f64) -> [f64; 2] {
380 [matrix[0] * x + matrix[2] * y, matrix[1] * x + matrix[3] * y]
381}
382
383fn transform_arc(cursor: [f64; 2], args: &mut [f64; 7], matrix: &[f64; 6]) {
384 let x = args[5] - cursor[0];
385 let y = args[6] - cursor[1];
386 let [a, b, cos, sin] = rotated_ellipse(args, [x, y]);
387
388 let ellipse = [a * cos, a * sin, -b * sin, b * cos, 0.0, 0.0];
389 let new_matrix = multiply_transform_matrices(matrix, ellipse);
390 let last_col = new_matrix[2] * new_matrix[2] + new_matrix[3] * new_matrix[3];
391 let square_sum = new_matrix[0] * new_matrix[0] + new_matrix[1] * new_matrix[1] + last_col;
392 let root = f64::hypot(new_matrix[0] - new_matrix[3], new_matrix[1] + new_matrix[2])
393 * f64::hypot(new_matrix[0] + new_matrix[3], new_matrix[1] - new_matrix[2]);
394
395 if root == 0.0 {
396 args[0] = f64::sqrt(square_sum / 2.0);
397 args[1] = args[0];
398 args[2] = 0.0;
399 } else {
400 let major_axis_square = f64::midpoint(square_sum, root);
401 let minor_axis_square = (square_sum - root) / 2.0;
402 let major = f64::abs(major_axis_square - last_col) > 1e-6;
403 let sub = if major {
404 major_axis_square
405 } else {
406 minor_axis_square
407 } - last_col;
408 let rows_sum = new_matrix[0] * new_matrix[2] + new_matrix[1] * new_matrix[3];
409 let term_1 = new_matrix[0] * sub + new_matrix[2] * rows_sum;
410 let term_2 = new_matrix[1] * sub + new_matrix[3] * rows_sum;
411 let term = if major { term_1 } else { term_2 };
412 args[0] = major_axis_square.sqrt();
413 args[1] = minor_axis_square.sqrt();
414 let term_sign = if (major && term_2 < 0.0) || (!major && term_1 > 0.0) {
415 -1.0
416 } else {
417 1.0
418 };
419 args[2] = (term_sign * f64::acos(term / f64::hypot(term_1, term_2)) * 180.0)
420 / std::f64::consts::PI;
421 }
422
423 if (matrix[0] < 0.0) != (matrix[3] < 0.0) {
424 args[4] = 1.0 - args[4];
425 }
426}
427
428fn rotated_ellipse(args: &mut [f64; 7], point: [f64; 2]) -> [f64; 4] {
429 let rotation = (args[2] * std::f64::consts::PI) / 180.0;
430 let cos = f64::cos(rotation);
431 let sin = f64::sin(rotation);
432
433 let mut a = args[0];
434 let mut b = args[1];
435 if a > 0.0 && b > 0.0 {
436 let h = (point[0] * cos + point[1] * sin).powi(2) / (4.0 * a * a)
437 + (point[1] * cos - point[0] * sin).powi(2) / (4.0 * b * b);
438 if h > 1.0 {
439 let h = h.sqrt();
440 a *= h;
441 b *= h;
442 }
443 }
444 [a, b, cos, sin]
445}
446
447fn multiply_transform_matrices(matrix: &[f64; 6], ellipse: [f64; 6]) -> [f64; 6] {
448 [
449 matrix[0] * ellipse[0] + matrix[2] * ellipse[1],
450 matrix[1] * ellipse[0] + matrix[3] * ellipse[1],
451 matrix[0] * ellipse[2] + matrix[2] * ellipse[3],
452 matrix[1] * ellipse[2] + matrix[3] * ellipse[3],
453 matrix[0] * ellipse[4] + matrix[2] * ellipse[5] + matrix[4],
454 matrix[1] * ellipse[4] + matrix[3] * ellipse[5] + matrix[5],
455 ]
456}
457
458#[test]
459#[allow(clippy::too_many_lines)]
460fn apply_transforms() -> anyhow::Result<()> {
461 use crate::test_config;
462
463 insta::assert_snapshot!(test_config(
464 r#"{ "applyTransforms": {}, "convertPathData": {} }"#,
465 Some(
466 r##"<svg xmlns="http://www.w3.org/2000/svg">
467 <path transform="translate(100,0)" d="M0,0 V100 L 70,50 z M70,50 L140,0 V100 z"/>
468 <path transform="" d="M0,0 V100 L 70,50 z M70,50 L140,0 V100 z"/>
469 <path fill="red" transform="rotate(15) scale(.5) skewX(5) translate(200,100)" d="M100,200 300,400 H100 V300 C100,100 250,100 250,200 S400,300 400,200 Q400,50 600,300 T1000,300 z"/>
470 <path fill="red" stroke="red" transform="rotate(15) scale(.5) skewX(5) translate(200,100)" d="M100,200 300,400 H100 V300 C100,100 250,100 250,200 S400,300 400,200 Q400,50 600,300 T1000,300 z"/>
471 <path fill="red" stroke="red" transform="rotate(15) scale(.5) skewX(5) translate(200,100)" d="M100,200 300,400 H100 V300 C100,100 250,100 250,200 S400,300 400,200 Q400,50 600,300 T1000,300 a150,150 0 1,0 150,-150 z"/>
472 <path fill="red" stroke="red" transform="rotate(15) scale(.5) translate(200,100)" d="M100,200 300,400 H100 V300 C100,100 250,100 250,200 S400,300 400,200 Q400,50 600,300 T1000,300 z"/>
473 <path fill="red" stroke="red" transform="rotate(15) scale(1.5) translate(200,100)" d="M100,200 300,400 H100 V300 C100,100 250,100 250,200 S400,300 400,200 Q400,50 600,300 T1000,300 z"/>
474 <path fill="red" stroke="red" transform="rotate(15) scale(0.33) translate(200,100)" d="M100,200 300,400 H100 V300 C100,100 250,100 250,200 S400,300 400,200 Q400,50 600,300 T1000,300 z"/>
475 <g stroke="red">
476 <path fill="red" transform="rotate(15) scale(.5) translate(200,100)" d="M100,200 300,400 H100 V300 C100,100 250,100 250,200 S400,300 400,200 Q400,50 600,300 T1000,300 z"/>
477 </g>
478 <g stroke="red" stroke-width="2">
479 <path fill="red" transform="rotate(15) scale(.5) translate(200,100)" d="M100,200 300,400 H100 V300 C100,100 250,100 250,200 S400,300 400,200 Q400,50 600,300 T1000,300 z"/>
480 </g>
481 <path transform="scale(10)" id="a" d="M0,0 V100 L 70,50 z M70,50 L140,0 V100 z"/>
482 <path transform="scale(10)" id="a" d="M0,0 V100 L 70,50 z M70,50 L140,0 V100 z" stroke="#000"/>
483 <path transform="scale(10)" id="a" d="M0,0 V100 L 70,50 z M70,50 L140,0 V100 z" stroke="#000" stroke-width=".5"/>
484 <g stroke="#000" stroke-width="5">
485 <path transform="scale(10)" id="a" d="M0,0 V100 L 70,50 z M70,50 L140,0 V100 z"/>
486 </g>
487 <path fill="url(#gradient)" transform="rotate(15) scale(0.33) translate(200,100)" d="M100,200 300,400 H100 V300 C100,100 250,100 250,200 S400,300 400,200 Q400,50 600,300 T1000,300 z"/>
488 <path clip-path="url(#a)" transform="rotate(15) scale(0.33) translate(200,100)" d="M100,200 300,400 H100 V300 C100,100 250,100 250,200 S400,300 400,200 Q400,50 600,300 T1000,300 z"/>
489 <path d="M5 0a10 10 0 1 0 20 0" transform="matrix(1 0 0 1 5 0)"/>
490 <path d="M5 0a10 10 0 1 0 20 0" transform="rotate(15) scale(.8,1.2) "/>
491 <path d="M5 0a10 10 0 1 0 20 0" transform="rotate(45)"/>
492 <path d="M5 0a10 10 0 1 0 20 0" transform="skewX(45)"/>
493 <path d="M0 300a1 2 0 1 0 200 0a1 2 0 1 0 -200 0" transform="rotate(15 100 300) scale(.8 1.2)"/>
494 <path d="M0 300a1 2 0 1 0 200 0a1 2 0 1 0 -200 0" transform="rotate(15 100 300)"/>
495 <path d="M700 300a1 2 0 1 0 200 0a1 2 0 1 0 -200 0" transform="rotate(-75 700 300) scale(.8 1.2)"/>
496 <path d="M12.6 8.6l-3.1-3.2-3.1 3.2-.8-.7 3.9-3.9 3.9 3.9zM9 5h1v10h-1z" transform="rotate(-90 9.5 9.5)"/>
497 <path d="M637.43 482.753a43.516 94.083 0 1 1-87.033 0 43.516 94.083 0 1 1 87.032 0z" transform="matrix(1.081 .234 -.187 .993 -37.573 -235.766)"/>
498 <path d="m-1.26-1.4a6.53 1.8-15.2 1 1 12.55-3.44" transform="translate(0, 0)"/>
499 <path d="M0 0c.07 1.33.14 2.66.21 3.99.07 1.33.14 2.66.21 3.99"/>
500</svg>"##
501 )
502 )?);
503
504 insta::assert_snapshot!(test_config(
505 r#"{ "applyTransforms": {}, "convertPathData": {} }"#,
506 Some(
507 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36">
508 <path d="M32 4a4 4 0 0 0-4-4H8a4 4 0 0 1-4 4v28a4 4 0 0 1 4 4h20a4 4 0 0 0 4-4V4z" fill="#888" transform="matrix(1 0 0 -1 0 36)"/>
509</svg>"##
510 )
511 )?);
512
513 insta::assert_snapshot!(test_config(
514 r#"{ "applyTransforms": {}, "convertPathData": {} }"#,
515 Some(
516 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
517 <path transform="translate(250, 250) scale(1.5, 1.5) translate(-250, -250)" fill="#7ED321" stroke="#000" stroke-width="15" vector-effect="non-scaling-stroke" d="M125 125h250v250h-250v-250z"/>
518</svg>"##
519 )
520 )?);
521
522 insta::assert_snapshot!(test_config(
523 r#"{ "applyTransforms": {}, "convertPathData": {} }"#,
524 Some(
525 r#"<svg width="480" height="360" xmlns="http://www.w3.org/2000/svg">
526 <path transform="scale(1.8)" stroke="black" stroke-width="10" fill="none" stroke-dasharray="none" d=" M 20 20 L 200 20"/>
527 <path transform="scale(1.8)" stroke="black" stroke-width="10" fill="none" stroke-dasharray="0" d=" M 20 40 L 200 40"/>
528 <path transform="scale(1.8)" stroke="black" stroke-width="20" fill="none" stroke-dasharray="5,2,5,5,2,5" d=" M 20 60 L 200 60"/>
529 <path transform="scale(1.8)" stroke="blue" stroke-width="10" fill="none" stroke-dasharray="5,2,5" d=" M 20 60 L 200 60"/>
530 <path transform="scale(1.8)" stroke="black" stroke-width="10" fill="none" stroke-dasharray="2" d=" M 20 80 L 200 80"/>
531 <path transform="scale(1.8)" stroke="blue" stroke-width="10" fill="none" stroke-dasharray="2" stroke-dashoffset="2" d=" M 20 90 L 200 90"/>
532</svg>"#
533 )
534 )?);
535
536 insta::assert_snapshot!(test_config(
537 r#"{ "applyTransforms": {}, "convertPathData": {} }"#,
538 Some(
539 r#"<svg width="200" height="100">
540 <path transform="scale(2)" style="stroke:black;stroke-width:10;" d="M 20 20 H 80" />
541 <path transform="scale(2)" stroke="black" stroke-width="10" d="M 20 20 H 80" />
542</svg>"#
543 )
544 )?);
545
546 insta::assert_snapshot!(test_config(
547 r#"{ "applyTransforms": {}, "convertPathData": {} }"#,
548 Some(
549 r#"<svg width="1200" height="1200">
550 <path transform="translate(100) scale(2)" d="m200 200 h-100 a100 100 0 1 0 100 -100 z"/>
551 <path transform="translate(100) scale(2)" d="M400 200 H300 A100 100 0 1 0 400 100 z"/>
552</svg>"#
553 )
554 )?);
555
556 insta::assert_snapshot!(test_config(
557 r#"{ "applyTransforms": {}, "convertPathData": {} }"#,
558 Some(
559 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 400" fill="#E7DACB">
560 <path
561 d="
562 M 152 65
563 V 158
564 H 49
565 V 65
566 z
567 m -14 75
568 V 83
569 H 67
570 V 141
571 z
572 "
573 transform="translate(-24, -41)"
574 />
575</svg>"##
576 )
577 )?);
578
579 insta::assert_snapshot!(test_config(
580 r#"{ "applyTransforms": {}, "convertPathData": {} }"#,
581 Some(
582 r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 31.6 31.6">
583 <path d="m5.25,2.2H25.13a0,0,0,0,1-.05-.05V14.18Z" transform="translate(0 0)"/>
584</svg>"#
585 )
586 )?);
587
588 Ok(())
589}