1use alloc::{string::String, vec::Vec};
8use azul_css::props::basic::{SvgCubicCurve, SvgPoint, SvgQuadraticCurve};
9
10use crate::svg::{
11 SvgLine, SvgMultiPolygon, SvgPath, SvgPathElement, SvgPathElementVec, SvgPathVec,
12};
13
14const KAPPA: f32 = 0.552_284_8;
16
17const POINT_EPSILON: f32 = 1e-6;
19
20const CLOSEPATH_EPSILON: f32 = 0.001;
22
23const ZERO_LENGTH_EPSILON: f32 = 1e-10;
25
26const ARC_SPLIT_FUDGE: f32 = 0.001;
28
29fn char_at(input: &[u8], pos: usize) -> char {
37 input
38 .get(pos..)
39 .and_then(|rest| core::str::from_utf8(rest).ok())
40 .and_then(|s| s.chars().next())
41 .unwrap_or(char::REPLACEMENT_CHARACTER)
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum SvgPathParseError {
47 EmptyPath,
49 UnexpectedChar { pos: usize, ch: char },
51 ExpectedNumber { pos: usize },
53 InvalidArcFlag { pos: usize },
55}
56
57impl core::fmt::Display for SvgPathParseError {
59 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
60 match self {
61 Self::EmptyPath => write!(f, "empty path"),
62 Self::UnexpectedChar { pos, ch } => {
63 write!(f, "unexpected char '{ch}' at byte {pos}")
64 }
65 Self::ExpectedNumber { pos } => write!(f, "expected number at byte {pos}"),
66 Self::InvalidArcFlag { pos } => write!(f, "invalid arc flag at byte {pos}"),
67 }
68 }
69}
70
71struct PathParser<'a> {
73 input: &'a [u8],
74 pos: usize,
75 current: SvgPoint,
76 subpath_start: SvgPoint,
77 last_control: Option<SvgPoint>,
78 last_command: u8,
79}
80
81impl<'a> PathParser<'a> {
82 const fn new(input: &'a [u8]) -> Self {
83 Self {
84 input,
85 pos: 0,
86 current: SvgPoint { x: 0.0, y: 0.0 },
87 subpath_start: SvgPoint { x: 0.0, y: 0.0 },
88 last_control: None,
89 last_command: 0,
90 }
91 }
92
93 const fn at_end(&self) -> bool {
94 self.pos >= self.input.len()
95 }
96
97 fn peek(&self) -> Option<u8> {
98 self.input.get(self.pos).copied()
99 }
100
101 fn skip_whitespace_and_commas(&mut self) {
102 while let Some(&b) = self.input.get(self.pos) {
103 if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' || b == b',' {
104 self.pos += 1;
105 } else {
106 break;
107 }
108 }
109 }
110
111 fn skip_whitespace(&mut self) {
112 while let Some(&b) = self.input.get(self.pos) {
113 if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' {
114 self.pos += 1;
115 } else {
116 break;
117 }
118 }
119 }
120
121 fn has_number(&self) -> bool {
123 match self.input.get(self.pos) {
124 Some(b'+' | b'-' | b'.') => true,
125 Some(b) if b.is_ascii_digit() => true,
126 _ => false,
127 }
128 }
129
130 fn parse_number(&mut self) -> Result<f32, SvgPathParseError> {
131 self.skip_whitespace_and_commas();
132 let start = self.pos;
133
134 if let Some(&b) = self.input.get(self.pos) {
136 if b == b'+' || b == b'-' {
137 self.pos += 1;
138 }
139 }
140
141 let mut has_digits = false;
142
143 while let Some(&b) = self.input.get(self.pos) {
145 if b.is_ascii_digit() {
146 self.pos += 1;
147 has_digits = true;
148 } else {
149 break;
150 }
151 }
152
153 if self.input.get(self.pos) == Some(&b'.') {
155 self.pos += 1;
156 while let Some(&b) = self.input.get(self.pos) {
157 if b.is_ascii_digit() {
158 self.pos += 1;
159 has_digits = true;
160 } else {
161 break;
162 }
163 }
164 }
165
166 if !has_digits {
167 return Err(SvgPathParseError::ExpectedNumber { pos: start });
168 }
169
170 if let Some(&b) = self.input.get(self.pos) {
172 if b == b'e' || b == b'E' {
173 self.pos += 1;
174 if let Some(&b) = self.input.get(self.pos) {
175 if b == b'+' || b == b'-' {
176 self.pos += 1;
177 }
178 }
179 while let Some(&b) = self.input.get(self.pos) {
180 if b.is_ascii_digit() {
181 self.pos += 1;
182 } else {
183 break;
184 }
185 }
186 }
187 }
188
189 let s = core::str::from_utf8(&self.input[start..self.pos])
190 .map_err(|_| SvgPathParseError::ExpectedNumber { pos: start })?;
191 s.parse::<f32>()
192 .map_err(|_| SvgPathParseError::ExpectedNumber { pos: start })
193 }
194
195 fn parse_flag(&mut self) -> Result<bool, SvgPathParseError> {
196 self.skip_whitespace_and_commas();
197 match self.input.get(self.pos) {
198 Some(b'0') => {
199 self.pos += 1;
200 Ok(false)
201 }
202 Some(b'1') => {
203 self.pos += 1;
204 Ok(true)
205 }
206 _ => Err(SvgPathParseError::InvalidArcFlag { pos: self.pos }),
207 }
208 }
209
210 fn parse_coordinate_pair(&mut self) -> Result<(f32, f32), SvgPathParseError> {
211 let x = self.parse_number()?;
212 let y = self.parse_number()?;
213 Ok((x, y))
214 }
215
216 fn make_absolute(&self, x: f32, y: f32, relative: bool) -> SvgPoint {
217 if relative {
218 SvgPoint {
219 x: self.current.x + x,
220 y: self.current.y + y,
221 }
222 } else {
223 SvgPoint { x, y }
224 }
225 }
226
227 fn handle_line_to(
228 &mut self,
229 relative: bool,
230 elements: &mut Vec<SvgPathElement>,
231 ) -> Result<(), SvgPathParseError> {
232 let (x, y) = self.parse_coordinate_pair()?;
233 let end = self.make_absolute(x, y, relative);
234 elements.push(SvgPathElement::Line(SvgLine {
235 start: self.current,
236 end,
237 }));
238 self.current = end;
239 self.last_control = None;
240 Ok(())
241 }
242
243 fn handle_horizontal_to(
244 &mut self,
245 relative: bool,
246 elements: &mut Vec<SvgPathElement>,
247 ) -> Result<(), SvgPathParseError> {
248 let x = self.parse_number()?;
249 let abs_x = if relative { self.current.x + x } else { x };
250 let end = SvgPoint {
251 x: abs_x,
252 y: self.current.y,
253 };
254 elements.push(SvgPathElement::Line(SvgLine {
255 start: self.current,
256 end,
257 }));
258 self.current = end;
259 self.last_control = None;
260 Ok(())
261 }
262
263 fn handle_vertical_to(
264 &mut self,
265 relative: bool,
266 elements: &mut Vec<SvgPathElement>,
267 ) -> Result<(), SvgPathParseError> {
268 let y = self.parse_number()?;
269 let abs_y = if relative { self.current.y + y } else { y };
270 let end = SvgPoint {
271 x: self.current.x,
272 y: abs_y,
273 };
274 elements.push(SvgPathElement::Line(SvgLine {
275 start: self.current,
276 end,
277 }));
278 self.current = end;
279 self.last_control = None;
280 Ok(())
281 }
282
283 #[allow(clippy::similar_names)] fn handle_cubic_to(
285 &mut self,
286 relative: bool,
287 elements: &mut Vec<SvgPathElement>,
288 ) -> Result<(), SvgPathParseError> {
289 let (c1x, c1y) = self.parse_coordinate_pair()?;
290 let (c2x, c2y) = self.parse_coordinate_pair()?;
291 let (ex, ey) = self.parse_coordinate_pair()?;
292 let ctrl_1 = self.make_absolute(c1x, c1y, relative);
293 let ctrl_2 = self.make_absolute(c2x, c2y, relative);
294 let end = self.make_absolute(ex, ey, relative);
295 elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
296 start: self.current,
297 ctrl_1,
298 ctrl_2,
299 end,
300 }));
301 self.last_control = Some(ctrl_2);
302 self.current = end;
303 Ok(())
304 }
305
306 #[allow(clippy::suboptimal_flops)] #[allow(clippy::similar_names)] fn handle_smooth_cubic_to(
309 &mut self,
310 relative: bool,
311 elements: &mut Vec<SvgPathElement>,
312 ) -> Result<(), SvgPathParseError> {
313 let ctrl_1 = match self.last_control {
314 Some(lc) if matches!(self.last_command.to_ascii_uppercase(), b'C' | b'S') => SvgPoint {
315 x: 2.0 * self.current.x - lc.x,
316 y: 2.0 * self.current.y - lc.y,
317 },
318 _ => self.current,
319 };
320 let (c2x, c2y) = self.parse_coordinate_pair()?;
321 let (ex, ey) = self.parse_coordinate_pair()?;
322 let ctrl_2 = self.make_absolute(c2x, c2y, relative);
323 let end = self.make_absolute(ex, ey, relative);
324 elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
325 start: self.current,
326 ctrl_1,
327 ctrl_2,
328 end,
329 }));
330 self.last_control = Some(ctrl_2);
331 self.current = end;
332 Ok(())
333 }
334
335 fn handle_quadratic_to(
336 &mut self,
337 relative: bool,
338 elements: &mut Vec<SvgPathElement>,
339 ) -> Result<(), SvgPathParseError> {
340 let (cx, cy) = self.parse_coordinate_pair()?;
341 let (ex, ey) = self.parse_coordinate_pair()?;
342 let ctrl = self.make_absolute(cx, cy, relative);
343 let end = self.make_absolute(ex, ey, relative);
344 elements.push(SvgPathElement::QuadraticCurve(SvgQuadraticCurve {
345 start: self.current,
346 ctrl,
347 end,
348 }));
349 self.last_control = Some(ctrl);
350 self.current = end;
351 Ok(())
352 }
353
354 #[allow(clippy::suboptimal_flops)] fn handle_smooth_quadratic_to(
356 &mut self,
357 relative: bool,
358 elements: &mut Vec<SvgPathElement>,
359 ) -> Result<(), SvgPathParseError> {
360 let ctrl = match self.last_control {
361 Some(lc) if matches!(self.last_command.to_ascii_uppercase(), b'Q' | b'T') => SvgPoint {
362 x: 2.0 * self.current.x - lc.x,
363 y: 2.0 * self.current.y - lc.y,
364 },
365 _ => self.current,
366 };
367 let (ex, ey) = self.parse_coordinate_pair()?;
368 let end = self.make_absolute(ex, ey, relative);
369 elements.push(SvgPathElement::QuadraticCurve(SvgQuadraticCurve {
370 start: self.current,
371 ctrl,
372 end,
373 }));
374 self.last_control = Some(ctrl);
375 self.current = end;
376 Ok(())
377 }
378
379 fn handle_arc_to(
380 &mut self,
381 relative: bool,
382 elements: &mut Vec<SvgPathElement>,
383 ) -> Result<(), SvgPathParseError> {
384 let rx = self.parse_number()?.abs();
385 let ry = self.parse_number()?.abs();
386 let x_rotation = self.parse_number()?;
387 let large_arc = self.parse_flag()?;
388 let sweep = self.parse_flag()?;
389 let (ex, ey) = self.parse_coordinate_pair()?;
390 let end = self.make_absolute(ex, ey, relative);
391 arc_to_cubics(
392 self.current,
393 end,
394 rx,
395 ry,
396 x_rotation,
397 large_arc,
398 sweep,
399 elements,
400 );
401 self.current = end;
402 self.last_control = None;
403 Ok(())
404 }
405}
406
407#[allow(clippy::suboptimal_flops)] #[allow(clippy::too_many_lines)] pub fn parse_svg_path_d(d: &str) -> Result<SvgMultiPolygon, SvgPathParseError> {
422 let d = d.trim();
423 if d.is_empty() {
424 return Err(SvgPathParseError::EmptyPath);
425 }
426
427 let mut parser = PathParser::new(d.as_bytes());
428 let mut rings: Vec<SvgPath> = Vec::new();
429 let mut current_elements: Vec<SvgPathElement> = Vec::new();
430
431 parser.skip_whitespace();
432
433 while !parser.at_end() {
434 parser.skip_whitespace_and_commas();
435 if parser.at_end() {
436 break;
437 }
438
439 let b = parser.peek().unwrap();
440
441 let cmd = if b.is_ascii_alphabetic() {
443 parser.pos += 1;
444 b
445 } else if parser.last_command != 0 {
446 match parser.last_command {
448 b'M' => b'L',
449 b'm' => b'l',
450 b'Z' | b'z' => {
457 return Err(SvgPathParseError::UnexpectedChar {
458 pos: parser.pos,
459 ch: char_at(parser.input, parser.pos),
460 });
461 }
462 other => other,
463 }
464 } else {
465 return Err(SvgPathParseError::UnexpectedChar {
466 pos: parser.pos,
467 ch: char_at(parser.input, parser.pos),
468 });
469 };
470
471 let relative = cmd.is_ascii_lowercase();
472 let cmd_upper = cmd.to_ascii_uppercase();
473
474 match cmd_upper {
475 b'M' => {
476 if !current_elements.is_empty() {
478 rings.push(SvgPath {
479 items: SvgPathElementVec::from_vec(core::mem::take(&mut current_elements)),
480 });
481 }
482 let (x, y) = parser.parse_coordinate_pair()?;
483 let pt = parser.make_absolute(x, y, relative);
484 parser.current = pt;
485 parser.subpath_start = pt;
486 parser.last_control = None;
487 parser.last_command = cmd;
488 }
489 b'L' => {
490 parser.handle_line_to(relative, &mut current_elements)?;
491 parser.last_command = cmd;
492 }
493 b'H' => {
494 parser.handle_horizontal_to(relative, &mut current_elements)?;
495 parser.last_command = cmd;
496 }
497 b'V' => {
498 parser.handle_vertical_to(relative, &mut current_elements)?;
499 parser.last_command = cmd;
500 }
501 b'C' => {
502 parser.handle_cubic_to(relative, &mut current_elements)?;
503 parser.last_command = cmd;
504 }
505 b'S' => {
506 parser.handle_smooth_cubic_to(relative, &mut current_elements)?;
507 parser.last_command = cmd;
508 }
509 b'Q' => {
510 parser.handle_quadratic_to(relative, &mut current_elements)?;
511 parser.last_command = cmd;
512 }
513 b'T' => {
514 parser.handle_smooth_quadratic_to(relative, &mut current_elements)?;
515 parser.last_command = cmd;
516 }
517 b'A' => {
518 parser.handle_arc_to(relative, &mut current_elements)?;
519 parser.last_command = cmd;
520 }
521 b'Z' => {
522 let dx = parser.current.x - parser.subpath_start.x;
524 let dy = parser.current.y - parser.subpath_start.y;
525 if dx * dx + dy * dy > CLOSEPATH_EPSILON * CLOSEPATH_EPSILON {
526 current_elements.push(SvgPathElement::Line(SvgLine {
527 start: parser.current,
528 end: parser.subpath_start,
529 }));
530 }
531 parser.current = parser.subpath_start;
532 parser.last_control = None;
533 parser.last_command = cmd;
534
535 if !current_elements.is_empty() {
537 rings.push(SvgPath {
538 items: SvgPathElementVec::from_vec(core::mem::take(&mut current_elements)),
539 });
540 }
541 }
542 _ => {
543 return Err(SvgPathParseError::UnexpectedChar {
544 pos: parser.pos - 1,
545 ch: cmd as char,
546 });
547 }
548 }
549
550 if cmd_upper != b'M' && cmd_upper != b'Z' {
553 loop {
554 parser.skip_whitespace_and_commas();
555 if parser.at_end() {
556 break;
557 }
558 let next = parser.peek().unwrap();
559 if next.is_ascii_alphabetic() {
560 break; }
562 if !parser.has_number() {
563 break;
564 }
565
566 match cmd_upper {
568 b'L' => parser.handle_line_to(relative, &mut current_elements)?,
569 b'H' => parser.handle_horizontal_to(relative, &mut current_elements)?,
570 b'V' => parser.handle_vertical_to(relative, &mut current_elements)?,
571 b'C' => parser.handle_cubic_to(relative, &mut current_elements)?,
572 b'S' => parser.handle_smooth_cubic_to(relative, &mut current_elements)?,
573 b'Q' => parser.handle_quadratic_to(relative, &mut current_elements)?,
574 b'T' => parser.handle_smooth_quadratic_to(relative, &mut current_elements)?,
575 b'A' => parser.handle_arc_to(relative, &mut current_elements)?,
576 _ => break,
577 }
578 }
579 }
580 }
581
582 if !current_elements.is_empty() {
584 rings.push(SvgPath {
585 items: SvgPathElementVec::from_vec(current_elements),
586 });
587 }
588
589 if parser.last_command == 0 && rings.is_empty() {
594 return Err(SvgPathParseError::UnexpectedChar {
595 pos: 0,
596 ch: char_at(parser.input, 0),
597 });
598 }
599
600 Ok(SvgMultiPolygon {
601 rings: SvgPathVec::from_vec(rings),
602 })
603}
604
605#[allow(clippy::suboptimal_flops)]
609#[allow(
613 clippy::cast_possible_truncation,
614 clippy::cast_precision_loss,
615 clippy::cast_sign_loss
616)]
617#[allow(clippy::similar_names)] fn arc_to_cubics(
619 start: SvgPoint,
620 end: SvgPoint,
621 mut rx: f32,
622 mut ry: f32,
623 x_rotation_deg: f32,
624 large_arc: bool,
625 sweep: bool,
626 out: &mut Vec<SvgPathElement>,
627) {
628 if (start.x - end.x).abs() < POINT_EPSILON && (start.y - end.y).abs() < POINT_EPSILON {
630 return;
631 }
632 if rx < POINT_EPSILON || ry < POINT_EPSILON {
633 out.push(SvgPathElement::Line(SvgLine { start, end }));
634 return;
635 }
636
637 let phi = x_rotation_deg.to_radians();
638 let cos_phi = phi.cos();
639 let sin_phi = phi.sin();
640
641 let dx = (start.x - end.x) / 2.0;
643 let dy = (start.y - end.y) / 2.0;
644 let x1p = cos_phi * dx + sin_phi * dy;
645 let y1p = -sin_phi * dx + cos_phi * dy;
646
647 let x1p2 = x1p * x1p;
649 let y1p2 = y1p * y1p;
650 let mut rx2 = rx * rx;
651 let mut ry2 = ry * ry;
652
653 let lambda = x1p2 / rx2 + y1p2 / ry2;
654 if lambda > 1.0 {
655 let sqrt_lambda = lambda.sqrt();
656 rx *= sqrt_lambda;
657 ry *= sqrt_lambda;
658 rx2 = rx * rx;
659 ry2 = ry * ry;
660 }
661
662 let num = (rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2).max(0.0);
663 let den = rx2 * y1p2 + ry2 * x1p2;
664 let sq = if den > 0.0 { (num / den).sqrt() } else { 0.0 };
665
666 let sign = if large_arc == sweep { -1.0 } else { 1.0 };
667 let cxp = sign * sq * (rx * y1p / ry);
668 let cyp = sign * sq * -(ry * x1p / rx);
669
670 let mx = f32::midpoint(start.x, end.x);
672 let my = f32::midpoint(start.y, end.y);
673 let cx = cos_phi * cxp - sin_phi * cyp + mx;
674 let cy = sin_phi * cxp + cos_phi * cyp + my;
675
676 let theta1 = angle_between(1.0, 0.0, (x1p - cxp) / rx, (y1p - cyp) / ry);
678 let mut dtheta = angle_between(
679 (x1p - cxp) / rx,
680 (y1p - cyp) / ry,
681 (-x1p - cxp) / rx,
682 (-y1p - cyp) / ry,
683 );
684
685 if !sweep && dtheta > 0.0 {
686 dtheta -= core::f32::consts::TAU;
687 } else if sweep && dtheta < 0.0 {
688 dtheta += core::f32::consts::TAU;
689 }
690
691 let n_segs = (dtheta.abs() / (core::f32::consts::FRAC_PI_2 + ARC_SPLIT_FUDGE)).ceil() as usize;
693 let n_segs = n_segs.max(1);
694 let seg_angle = dtheta / n_segs as f32;
695
696 let mut prev = start;
697 for i in 0..n_segs {
698 let t1 = theta1 + seg_angle * i as f32;
699 let t2 = theta1 + seg_angle * (i + 1) as f32;
700
701 let (c1, c2, ep) = arc_segment_to_cubic(cx, cy, rx, ry, cos_phi, sin_phi, t1, t2);
702
703 let seg_end = if i + 1 == n_segs { end } else { ep };
704 out.push(SvgPathElement::CubicCurve(SvgCubicCurve {
705 start: prev,
706 ctrl_1: c1,
707 ctrl_2: c2,
708 end: seg_end,
709 }));
710 prev = seg_end;
711 }
712}
713
714#[allow(clippy::suboptimal_flops)] fn angle_between(ux: f32, uy: f32, vx: f32, vy: f32) -> f32 {
717 let dot = ux * vx + uy * vy;
718 let len = ((ux * ux + uy * uy) * (vx * vx + vy * vy)).sqrt();
719 if len < ZERO_LENGTH_EPSILON {
720 return 0.0;
721 }
722 let cos_val = (dot / len).clamp(-1.0, 1.0);
723 let angle = cos_val.acos();
724 if ux * vy - uy * vx < 0.0 {
725 -angle
726 } else {
727 angle
728 }
729}
730
731#[allow(clippy::suboptimal_flops)] #[allow(clippy::similar_names)] fn arc_segment_to_cubic(
735 cx: f32,
736 cy: f32,
737 rx: f32,
738 ry: f32,
739 cos_phi: f32,
740 sin_phi: f32,
741 theta1: f32,
742 theta2: f32,
743) -> (SvgPoint, SvgPoint, SvgPoint) {
744 let alpha = 4.0 / 3.0 * ((theta2 - theta1) / 4.0).tan();
745
746 let cos1 = theta1.cos();
747 let sin1 = theta1.sin();
748 let cos2 = theta2.cos();
749 let sin2 = theta2.sin();
750
751 let dx1 = rx * (cos1 - alpha * sin1);
753 let dy1 = ry * (sin1 + alpha * cos1);
754 let dx2 = rx * (cos2 + alpha * sin2);
756 let dy2 = ry * (sin2 - alpha * cos2);
757 let dx3 = rx * cos2;
759 let dy3 = ry * sin2;
760
761 let c1 = SvgPoint {
762 x: cos_phi * dx1 - sin_phi * dy1 + cx,
763 y: sin_phi * dx1 + cos_phi * dy1 + cy,
764 };
765 let c2 = SvgPoint {
766 x: cos_phi * dx2 - sin_phi * dy2 + cx,
767 y: sin_phi * dx2 + cos_phi * dy2 + cy,
768 };
769 let ep = SvgPoint {
770 x: cos_phi * dx3 - sin_phi * dy3 + cx,
771 y: sin_phi * dx3 + cos_phi * dy3 + cy,
772 };
773
774 (c1, c2, ep)
775}
776
777#[must_use]
781pub fn svg_circle_to_paths(cx: f32, cy: f32, r: f32) -> SvgPath {
782 let k = r * KAPPA;
783
784 let elements = vec![
785 SvgPathElement::CubicCurve(SvgCubicCurve {
787 start: SvgPoint { x: cx, y: cy - r },
788 ctrl_1: SvgPoint {
789 x: cx + k,
790 y: cy - r,
791 },
792 ctrl_2: SvgPoint {
793 x: cx + r,
794 y: cy - k,
795 },
796 end: SvgPoint { x: cx + r, y: cy },
797 }),
798 SvgPathElement::CubicCurve(SvgCubicCurve {
800 start: SvgPoint { x: cx + r, y: cy },
801 ctrl_1: SvgPoint {
802 x: cx + r,
803 y: cy + k,
804 },
805 ctrl_2: SvgPoint {
806 x: cx + k,
807 y: cy + r,
808 },
809 end: SvgPoint { x: cx, y: cy + r },
810 }),
811 SvgPathElement::CubicCurve(SvgCubicCurve {
813 start: SvgPoint { x: cx, y: cy + r },
814 ctrl_1: SvgPoint {
815 x: cx - k,
816 y: cy + r,
817 },
818 ctrl_2: SvgPoint {
819 x: cx - r,
820 y: cy + k,
821 },
822 end: SvgPoint { x: cx - r, y: cy },
823 }),
824 SvgPathElement::CubicCurve(SvgCubicCurve {
826 start: SvgPoint { x: cx - r, y: cy },
827 ctrl_1: SvgPoint {
828 x: cx - r,
829 y: cy - k,
830 },
831 ctrl_2: SvgPoint {
832 x: cx - k,
833 y: cy - r,
834 },
835 end: SvgPoint { x: cx, y: cy - r },
836 }),
837 ];
838
839 SvgPath {
840 items: SvgPathElementVec::from_vec(elements),
841 }
842}
843
844#[must_use]
849#[allow(clippy::vec_init_then_push)]
852#[allow(clippy::too_many_lines)] pub fn svg_rect_to_path(x: f32, y: f32, w: f32, h: f32, rx: f32, ry: f32) -> SvgPath {
854 let rx = rx.min(w / 2.0);
855 let ry = ry.min(h / 2.0);
856
857 if rx < CLOSEPATH_EPSILON && ry < CLOSEPATH_EPSILON {
858 let tl = SvgPoint { x, y };
860 let tr = SvgPoint { x: x + w, y };
861 let br = SvgPoint { x: x + w, y: y + h };
862 let bl = SvgPoint { x, y: y + h };
863
864 let elements = vec![
865 SvgPathElement::Line(SvgLine { start: tl, end: tr }),
866 SvgPathElement::Line(SvgLine { start: tr, end: br }),
867 SvgPathElement::Line(SvgLine { start: br, end: bl }),
868 SvgPathElement::Line(SvgLine { start: bl, end: tl }),
869 ];
870
871 return SvgPath {
872 items: SvgPathElementVec::from_vec(elements),
873 };
874 }
875
876 let kx = rx * KAPPA;
878 let ky = ry * KAPPA;
879
880 let mut elements = Vec::with_capacity(8);
881
882 elements.push(SvgPathElement::Line(SvgLine {
884 start: SvgPoint { x: x + rx, y },
885 end: SvgPoint { x: x + w - rx, y },
886 }));
887 elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
889 start: SvgPoint { x: x + w - rx, y },
890 ctrl_1: SvgPoint {
891 x: x + w - rx + kx,
892 y,
893 },
894 ctrl_2: SvgPoint {
895 x: x + w,
896 y: y + ry - ky,
897 },
898 end: SvgPoint {
899 x: x + w,
900 y: y + ry,
901 },
902 }));
903 elements.push(SvgPathElement::Line(SvgLine {
905 start: SvgPoint {
906 x: x + w,
907 y: y + ry,
908 },
909 end: SvgPoint {
910 x: x + w,
911 y: y + h - ry,
912 },
913 }));
914 elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
916 start: SvgPoint {
917 x: x + w,
918 y: y + h - ry,
919 },
920 ctrl_1: SvgPoint {
921 x: x + w,
922 y: y + h - ry + ky,
923 },
924 ctrl_2: SvgPoint {
925 x: x + w - rx + kx,
926 y: y + h,
927 },
928 end: SvgPoint {
929 x: x + w - rx,
930 y: y + h,
931 },
932 }));
933 elements.push(SvgPathElement::Line(SvgLine {
935 start: SvgPoint {
936 x: x + w - rx,
937 y: y + h,
938 },
939 end: SvgPoint {
940 x: x + rx,
941 y: y + h,
942 },
943 }));
944 elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
946 start: SvgPoint {
947 x: x + rx,
948 y: y + h,
949 },
950 ctrl_1: SvgPoint {
951 x: x + rx - kx,
952 y: y + h,
953 },
954 ctrl_2: SvgPoint {
955 x,
956 y: y + h - ry + ky,
957 },
958 end: SvgPoint { x, y: y + h - ry },
959 }));
960 elements.push(SvgPathElement::Line(SvgLine {
962 start: SvgPoint { x, y: y + h - ry },
963 end: SvgPoint { x, y: y + ry },
964 }));
965 elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
967 start: SvgPoint { x, y: y + ry },
968 ctrl_1: SvgPoint { x, y: y + ry - ky },
969 ctrl_2: SvgPoint { x: x + rx - kx, y },
970 end: SvgPoint { x: x + rx, y },
971 }));
972
973 SvgPath {
974 items: SvgPathElementVec::from_vec(elements),
975 }
976}
977
978#[cfg(test)]
979mod tests {
980 use super::*;
981
982 #[test]
986 fn m0_0z5_does_not_hang() {
987 let err = parse_svg_path_d("M0 0Z5").unwrap_err();
988 match err {
989 SvgPathParseError::UnexpectedChar { ch, .. } => assert_eq!(ch, '5'),
990 other => panic!("expected UnexpectedChar, got {other:?}"),
991 }
992 }
993
994 #[test]
996 fn stray_byte_after_closepath_rejected() {
997 for s in ["M0 0Z9", "m0 0z-", "M0 0Z."] {
998 assert!(
999 matches!(
1000 parse_svg_path_d(s),
1001 Err(SvgPathParseError::UnexpectedChar { .. })
1002 ),
1003 "expected UnexpectedChar for {s:?}"
1004 );
1005 }
1006 }
1007
1008 #[test]
1011 fn error_char_is_unicode_not_byte() {
1012 let err = parse_svg_path_d("ü10 10").unwrap_err();
1014 match err {
1015 SvgPathParseError::UnexpectedChar { ch, pos } => {
1016 assert_eq!(ch, 'ü');
1017 assert_eq!(pos, 0);
1018 }
1019 other => panic!("expected UnexpectedChar, got {other:?}"),
1020 }
1021 }
1022
1023 #[test]
1025 fn valid_closepath_then_command_ok() {
1026 let parsed = parse_svg_path_d("M0 0 L10 0 Z M20 20 L30 20 Z");
1027 assert!(parsed.is_ok(), "valid multi-subpath path should parse");
1028 }
1029}
1030
1031#[cfg(test)]
1032#[path = "path_parser_test.rs"]
1033mod path_parser_test;