1use alloc::{string::String, vec::Vec};
8use azul_css::props::basic::{SvgCubicCurve, SvgPoint, SvgQuadraticCurve};
9
10use crate::svg::{SvgLine, SvgMultiPolygon, SvgPath, SvgPathElement, SvgPathElementVec, SvgPathVec};
11
12const KAPPA: f32 = 0.552_284_8;
14
15const POINT_EPSILON: f32 = 1e-6;
17
18const CLOSEPATH_EPSILON: f32 = 0.001;
20
21const ZERO_LENGTH_EPSILON: f32 = 1e-10;
23
24const ARC_SPLIT_FUDGE: f32 = 0.001;
26
27fn char_at(input: &[u8], pos: usize) -> char {
35 input
36 .get(pos..)
37 .and_then(|rest| core::str::from_utf8(rest).ok())
38 .and_then(|s| s.chars().next())
39 .unwrap_or(char::REPLACEMENT_CHARACTER)
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum SvgPathParseError {
45 EmptyPath,
47 UnexpectedChar { pos: usize, ch: char },
49 ExpectedNumber { pos: usize },
51 InvalidArcFlag { pos: usize },
53}
54
55impl core::fmt::Display for SvgPathParseError {
57 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
58 match self {
59 Self::EmptyPath => write!(f, "empty path"),
60 Self::UnexpectedChar { pos, ch } => {
61 write!(f, "unexpected char '{ch}' at byte {pos}")
62 }
63 Self::ExpectedNumber { pos } => write!(f, "expected number at byte {pos}"),
64 Self::InvalidArcFlag { pos } => write!(f, "invalid arc flag at byte {pos}"),
65 }
66 }
67}
68
69struct PathParser<'a> {
71 input: &'a [u8],
72 pos: usize,
73 current: SvgPoint,
74 subpath_start: SvgPoint,
75 last_control: Option<SvgPoint>,
76 last_command: u8,
77}
78
79impl<'a> PathParser<'a> {
80 const fn new(input: &'a [u8]) -> Self {
81 Self {
82 input,
83 pos: 0,
84 current: SvgPoint { x: 0.0, y: 0.0 },
85 subpath_start: SvgPoint { x: 0.0, y: 0.0 },
86 last_control: None,
87 last_command: 0,
88 }
89 }
90
91 const fn at_end(&self) -> bool {
92 self.pos >= self.input.len()
93 }
94
95 fn peek(&self) -> Option<u8> {
96 self.input.get(self.pos).copied()
97 }
98
99 fn skip_whitespace_and_commas(&mut self) {
100 while let Some(&b) = self.input.get(self.pos) {
101 if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' || b == b',' {
102 self.pos += 1;
103 } else {
104 break;
105 }
106 }
107 }
108
109 fn skip_whitespace(&mut self) {
110 while let Some(&b) = self.input.get(self.pos) {
111 if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' {
112 self.pos += 1;
113 } else {
114 break;
115 }
116 }
117 }
118
119 fn has_number(&self) -> bool {
121 match self.input.get(self.pos) {
122 Some(b'+' | b'-' | b'.') => true,
123 Some(b) if b.is_ascii_digit() => true,
124 _ => false,
125 }
126 }
127
128 fn parse_number(&mut self) -> Result<f32, SvgPathParseError> {
129 self.skip_whitespace_and_commas();
130 let start = self.pos;
131
132 if let Some(&b) = self.input.get(self.pos) {
134 if b == b'+' || b == b'-' {
135 self.pos += 1;
136 }
137 }
138
139 let mut has_digits = false;
140
141 while let Some(&b) = self.input.get(self.pos) {
143 if b.is_ascii_digit() {
144 self.pos += 1;
145 has_digits = true;
146 } else {
147 break;
148 }
149 }
150
151 if self.input.get(self.pos) == Some(&b'.') {
153 self.pos += 1;
154 while let Some(&b) = self.input.get(self.pos) {
155 if b.is_ascii_digit() {
156 self.pos += 1;
157 has_digits = true;
158 } else {
159 break;
160 }
161 }
162 }
163
164 if !has_digits {
165 return Err(SvgPathParseError::ExpectedNumber { pos: start });
166 }
167
168 if let Some(&b) = self.input.get(self.pos) {
170 if b == b'e' || b == b'E' {
171 self.pos += 1;
172 if let Some(&b) = self.input.get(self.pos) {
173 if b == b'+' || b == b'-' {
174 self.pos += 1;
175 }
176 }
177 while let Some(&b) = self.input.get(self.pos) {
178 if b.is_ascii_digit() {
179 self.pos += 1;
180 } else {
181 break;
182 }
183 }
184 }
185 }
186
187 let s = core::str::from_utf8(&self.input[start..self.pos])
188 .map_err(|_| SvgPathParseError::ExpectedNumber { pos: start })?;
189 s.parse::<f32>()
190 .map_err(|_| SvgPathParseError::ExpectedNumber { pos: start })
191 }
192
193 fn parse_flag(&mut self) -> Result<bool, SvgPathParseError> {
194 self.skip_whitespace_and_commas();
195 match self.input.get(self.pos) {
196 Some(b'0') => {
197 self.pos += 1;
198 Ok(false)
199 }
200 Some(b'1') => {
201 self.pos += 1;
202 Ok(true)
203 }
204 _ => Err(SvgPathParseError::InvalidArcFlag { pos: self.pos }),
205 }
206 }
207
208 fn parse_coordinate_pair(&mut self) -> Result<(f32, f32), SvgPathParseError> {
209 let x = self.parse_number()?;
210 let y = self.parse_number()?;
211 Ok((x, y))
212 }
213
214 fn make_absolute(&self, x: f32, y: f32, relative: bool) -> SvgPoint {
215 if relative {
216 SvgPoint {
217 x: self.current.x + x,
218 y: self.current.y + y,
219 }
220 } else {
221 SvgPoint { x, y }
222 }
223 }
224
225 fn handle_line_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
226 let (x, y) = self.parse_coordinate_pair()?;
227 let end = self.make_absolute(x, y, relative);
228 elements.push(SvgPathElement::Line(SvgLine { start: self.current, end }));
229 self.current = end;
230 self.last_control = None;
231 Ok(())
232 }
233
234 fn handle_horizontal_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
235 let x = self.parse_number()?;
236 let abs_x = if relative { self.current.x + x } else { x };
237 let end = SvgPoint { x: abs_x, y: self.current.y };
238 elements.push(SvgPathElement::Line(SvgLine { start: self.current, end }));
239 self.current = end;
240 self.last_control = None;
241 Ok(())
242 }
243
244 fn handle_vertical_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
245 let y = self.parse_number()?;
246 let abs_y = if relative { self.current.y + y } else { y };
247 let end = SvgPoint { x: self.current.x, y: abs_y };
248 elements.push(SvgPathElement::Line(SvgLine { start: self.current, end }));
249 self.current = end;
250 self.last_control = None;
251 Ok(())
252 }
253
254 #[allow(clippy::similar_names)] fn handle_cubic_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
256 let (c1x, c1y) = self.parse_coordinate_pair()?;
257 let (c2x, c2y) = self.parse_coordinate_pair()?;
258 let (ex, ey) = self.parse_coordinate_pair()?;
259 let ctrl_1 = self.make_absolute(c1x, c1y, relative);
260 let ctrl_2 = self.make_absolute(c2x, c2y, relative);
261 let end = self.make_absolute(ex, ey, relative);
262 elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
263 start: self.current, ctrl_1, ctrl_2, end,
264 }));
265 self.last_control = Some(ctrl_2);
266 self.current = end;
267 Ok(())
268 }
269
270 #[allow(clippy::suboptimal_flops)] #[allow(clippy::similar_names)] fn handle_smooth_cubic_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
273 let ctrl_1 = match self.last_control {
274 Some(lc) if matches!(self.last_command.to_ascii_uppercase(), b'C' | b'S') => {
275 SvgPoint {
276 x: 2.0 * self.current.x - lc.x,
277 y: 2.0 * self.current.y - lc.y,
278 }
279 }
280 _ => self.current,
281 };
282 let (c2x, c2y) = self.parse_coordinate_pair()?;
283 let (ex, ey) = self.parse_coordinate_pair()?;
284 let ctrl_2 = self.make_absolute(c2x, c2y, relative);
285 let end = self.make_absolute(ex, ey, relative);
286 elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
287 start: self.current, ctrl_1, ctrl_2, end,
288 }));
289 self.last_control = Some(ctrl_2);
290 self.current = end;
291 Ok(())
292 }
293
294 fn handle_quadratic_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
295 let (cx, cy) = self.parse_coordinate_pair()?;
296 let (ex, ey) = self.parse_coordinate_pair()?;
297 let ctrl = self.make_absolute(cx, cy, relative);
298 let end = self.make_absolute(ex, ey, relative);
299 elements.push(SvgPathElement::QuadraticCurve(SvgQuadraticCurve {
300 start: self.current, ctrl, end,
301 }));
302 self.last_control = Some(ctrl);
303 self.current = end;
304 Ok(())
305 }
306
307 #[allow(clippy::suboptimal_flops)] fn handle_smooth_quadratic_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
309 let ctrl = match self.last_control {
310 Some(lc) if matches!(self.last_command.to_ascii_uppercase(), b'Q' | b'T') => {
311 SvgPoint {
312 x: 2.0 * self.current.x - lc.x,
313 y: 2.0 * self.current.y - lc.y,
314 }
315 }
316 _ => self.current,
317 };
318 let (ex, ey) = self.parse_coordinate_pair()?;
319 let end = self.make_absolute(ex, ey, relative);
320 elements.push(SvgPathElement::QuadraticCurve(SvgQuadraticCurve {
321 start: self.current, ctrl, end,
322 }));
323 self.last_control = Some(ctrl);
324 self.current = end;
325 Ok(())
326 }
327
328 fn handle_arc_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
329 let rx = self.parse_number()?.abs();
330 let ry = self.parse_number()?.abs();
331 let x_rotation = self.parse_number()?;
332 let large_arc = self.parse_flag()?;
333 let sweep = self.parse_flag()?;
334 let (ex, ey) = self.parse_coordinate_pair()?;
335 let end = self.make_absolute(ex, ey, relative);
336 arc_to_cubics(self.current, end, rx, ry, x_rotation, large_arc, sweep, elements);
337 self.current = end;
338 self.last_control = None;
339 Ok(())
340 }
341}
342
343#[allow(clippy::suboptimal_flops)] #[allow(clippy::too_many_lines)] pub fn parse_svg_path_d(d: &str) -> Result<SvgMultiPolygon, SvgPathParseError> {
358 let d = d.trim();
359 if d.is_empty() {
360 return Err(SvgPathParseError::EmptyPath);
361 }
362
363 let mut parser = PathParser::new(d.as_bytes());
364 let mut rings: Vec<SvgPath> = Vec::new();
365 let mut current_elements: Vec<SvgPathElement> = Vec::new();
366
367 parser.skip_whitespace();
368
369 while !parser.at_end() {
370 parser.skip_whitespace_and_commas();
371 if parser.at_end() {
372 break;
373 }
374
375 let b = parser.peek().unwrap();
376
377 let cmd = if b.is_ascii_alphabetic() {
379 parser.pos += 1;
380 b
381 } else if parser.last_command != 0 {
382 match parser.last_command {
384 b'M' => b'L',
385 b'm' => b'l',
386 b'Z' | b'z' => {
393 return Err(SvgPathParseError::UnexpectedChar {
394 pos: parser.pos,
395 ch: char_at(parser.input, parser.pos),
396 });
397 }
398 other => other,
399 }
400 } else {
401 return Err(SvgPathParseError::UnexpectedChar {
402 pos: parser.pos,
403 ch: char_at(parser.input, parser.pos),
404 });
405 };
406
407 let relative = cmd.is_ascii_lowercase();
408 let cmd_upper = cmd.to_ascii_uppercase();
409
410 match cmd_upper {
411 b'M' => {
412 if !current_elements.is_empty() {
414 rings.push(SvgPath {
415 items: SvgPathElementVec::from_vec(core::mem::take(&mut current_elements)),
416 });
417 }
418 let (x, y) = parser.parse_coordinate_pair()?;
419 let pt = parser.make_absolute(x, y, relative);
420 parser.current = pt;
421 parser.subpath_start = pt;
422 parser.last_control = None;
423 parser.last_command = cmd;
424 }
425 b'L' => {
426 parser.handle_line_to(relative, &mut current_elements)?;
427 parser.last_command = cmd;
428 }
429 b'H' => {
430 parser.handle_horizontal_to(relative, &mut current_elements)?;
431 parser.last_command = cmd;
432 }
433 b'V' => {
434 parser.handle_vertical_to(relative, &mut current_elements)?;
435 parser.last_command = cmd;
436 }
437 b'C' => {
438 parser.handle_cubic_to(relative, &mut current_elements)?;
439 parser.last_command = cmd;
440 }
441 b'S' => {
442 parser.handle_smooth_cubic_to(relative, &mut current_elements)?;
443 parser.last_command = cmd;
444 }
445 b'Q' => {
446 parser.handle_quadratic_to(relative, &mut current_elements)?;
447 parser.last_command = cmd;
448 }
449 b'T' => {
450 parser.handle_smooth_quadratic_to(relative, &mut current_elements)?;
451 parser.last_command = cmd;
452 }
453 b'A' => {
454 parser.handle_arc_to(relative, &mut current_elements)?;
455 parser.last_command = cmd;
456 }
457 b'Z' => {
458 let dx = parser.current.x - parser.subpath_start.x;
460 let dy = parser.current.y - parser.subpath_start.y;
461 if dx * dx + dy * dy > CLOSEPATH_EPSILON * CLOSEPATH_EPSILON {
462 current_elements.push(SvgPathElement::Line(SvgLine {
463 start: parser.current,
464 end: parser.subpath_start,
465 }));
466 }
467 parser.current = parser.subpath_start;
468 parser.last_control = None;
469 parser.last_command = cmd;
470
471 if !current_elements.is_empty() {
473 rings.push(SvgPath {
474 items: SvgPathElementVec::from_vec(core::mem::take(&mut current_elements)),
475 });
476 }
477 }
478 _ => {
479 return Err(SvgPathParseError::UnexpectedChar {
480 pos: parser.pos - 1,
481 ch: cmd as char,
482 });
483 }
484 }
485
486 if cmd_upper != b'M' && cmd_upper != b'Z' {
489 loop {
490 parser.skip_whitespace_and_commas();
491 if parser.at_end() {
492 break;
493 }
494 let next = parser.peek().unwrap();
495 if next.is_ascii_alphabetic() {
496 break; }
498 if !parser.has_number() {
499 break;
500 }
501
502 match cmd_upper {
504 b'L' => parser.handle_line_to(relative, &mut current_elements)?,
505 b'H' => parser.handle_horizontal_to(relative, &mut current_elements)?,
506 b'V' => parser.handle_vertical_to(relative, &mut current_elements)?,
507 b'C' => parser.handle_cubic_to(relative, &mut current_elements)?,
508 b'S' => parser.handle_smooth_cubic_to(relative, &mut current_elements)?,
509 b'Q' => parser.handle_quadratic_to(relative, &mut current_elements)?,
510 b'T' => parser.handle_smooth_quadratic_to(relative, &mut current_elements)?,
511 b'A' => parser.handle_arc_to(relative, &mut current_elements)?,
512 _ => break,
513 }
514 }
515 }
516 }
517
518 if !current_elements.is_empty() {
520 rings.push(SvgPath {
521 items: SvgPathElementVec::from_vec(current_elements),
522 });
523 }
524
525 if parser.last_command == 0 && rings.is_empty() {
530 return Err(SvgPathParseError::UnexpectedChar {
531 pos: 0,
532 ch: char_at(parser.input, 0),
533 });
534 }
535
536 Ok(SvgMultiPolygon {
537 rings: SvgPathVec::from_vec(rings),
538 })
539}
540
541#[allow(clippy::suboptimal_flops)] #[allow(
548 clippy::cast_possible_truncation,
549 clippy::cast_precision_loss,
550 clippy::cast_sign_loss
551)]
552#[allow(clippy::similar_names)] fn arc_to_cubics(
554 start: SvgPoint,
555 end: SvgPoint,
556 mut rx: f32,
557 mut ry: f32,
558 x_rotation_deg: f32,
559 large_arc: bool,
560 sweep: bool,
561 out: &mut Vec<SvgPathElement>,
562) {
563 if (start.x - end.x).abs() < POINT_EPSILON && (start.y - end.y).abs() < POINT_EPSILON {
565 return;
566 }
567 if rx < POINT_EPSILON || ry < POINT_EPSILON {
568 out.push(SvgPathElement::Line(SvgLine { start, end }));
569 return;
570 }
571
572 let phi = x_rotation_deg.to_radians();
573 let cos_phi = phi.cos();
574 let sin_phi = phi.sin();
575
576 let dx = (start.x - end.x) / 2.0;
578 let dy = (start.y - end.y) / 2.0;
579 let x1p = cos_phi * dx + sin_phi * dy;
580 let y1p = -sin_phi * dx + cos_phi * dy;
581
582 let x1p2 = x1p * x1p;
584 let y1p2 = y1p * y1p;
585 let mut rx2 = rx * rx;
586 let mut ry2 = ry * ry;
587
588 let lambda = x1p2 / rx2 + y1p2 / ry2;
589 if lambda > 1.0 {
590 let sqrt_lambda = lambda.sqrt();
591 rx *= sqrt_lambda;
592 ry *= sqrt_lambda;
593 rx2 = rx * rx;
594 ry2 = ry * ry;
595 }
596
597 let num = (rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2).max(0.0);
598 let den = rx2 * y1p2 + ry2 * x1p2;
599 let sq = if den > 0.0 {
600 (num / den).sqrt()
601 } else {
602 0.0
603 };
604
605 let sign = if large_arc == sweep { -1.0 } else { 1.0 };
606 let cxp = sign * sq * (rx * y1p / ry);
607 let cyp = sign * sq * -(ry * x1p / rx);
608
609 let mx = f32::midpoint(start.x, end.x);
611 let my = f32::midpoint(start.y, end.y);
612 let cx = cos_phi * cxp - sin_phi * cyp + mx;
613 let cy = sin_phi * cxp + cos_phi * cyp + my;
614
615 let theta1 = angle_between(1.0, 0.0, (x1p - cxp) / rx, (y1p - cyp) / ry);
617 let mut dtheta = angle_between(
618 (x1p - cxp) / rx,
619 (y1p - cyp) / ry,
620 (-x1p - cxp) / rx,
621 (-y1p - cyp) / ry,
622 );
623
624 if !sweep && dtheta > 0.0 {
625 dtheta -= core::f32::consts::TAU;
626 } else if sweep && dtheta < 0.0 {
627 dtheta += core::f32::consts::TAU;
628 }
629
630 let n_segs = (dtheta.abs() / (core::f32::consts::FRAC_PI_2 + ARC_SPLIT_FUDGE)).ceil() as usize;
632 let n_segs = n_segs.max(1);
633 let seg_angle = dtheta / n_segs as f32;
634
635 let mut prev = start;
636 for i in 0..n_segs {
637 let t1 = theta1 + seg_angle * i as f32;
638 let t2 = theta1 + seg_angle * (i + 1) as f32;
639
640 let (c1, c2, ep) =
641 arc_segment_to_cubic(cx, cy, rx, ry, cos_phi, sin_phi, t1, t2);
642
643 let seg_end = if i + 1 == n_segs { end } else { ep };
644 out.push(SvgPathElement::CubicCurve(SvgCubicCurve {
645 start: prev,
646 ctrl_1: c1,
647 ctrl_2: c2,
648 end: seg_end,
649 }));
650 prev = seg_end;
651 }
652}
653
654#[allow(clippy::suboptimal_flops)] fn angle_between(ux: f32, uy: f32, vx: f32, vy: f32) -> f32 {
657 let dot = ux * vx + uy * vy;
658 let len = ((ux * ux + uy * uy) * (vx * vx + vy * vy)).sqrt();
659 if len < ZERO_LENGTH_EPSILON {
660 return 0.0;
661 }
662 let cos_val = (dot / len).clamp(-1.0, 1.0);
663 let angle = cos_val.acos();
664 if ux * vy - uy * vx < 0.0 {
665 -angle
666 } else {
667 angle
668 }
669}
670
671#[allow(clippy::suboptimal_flops)] #[allow(clippy::similar_names)] fn arc_segment_to_cubic(
675 cx: f32,
676 cy: f32,
677 rx: f32,
678 ry: f32,
679 cos_phi: f32,
680 sin_phi: f32,
681 theta1: f32,
682 theta2: f32,
683) -> (SvgPoint, SvgPoint, SvgPoint) {
684 let alpha = 4.0 / 3.0 * ((theta2 - theta1) / 4.0).tan();
685
686 let cos1 = theta1.cos();
687 let sin1 = theta1.sin();
688 let cos2 = theta2.cos();
689 let sin2 = theta2.sin();
690
691 let dx1 = rx * (cos1 - alpha * sin1);
693 let dy1 = ry * (sin1 + alpha * cos1);
694 let dx2 = rx * (cos2 + alpha * sin2);
696 let dy2 = ry * (sin2 - alpha * cos2);
697 let dx3 = rx * cos2;
699 let dy3 = ry * sin2;
700
701 let c1 = SvgPoint {
702 x: cos_phi * dx1 - sin_phi * dy1 + cx,
703 y: sin_phi * dx1 + cos_phi * dy1 + cy,
704 };
705 let c2 = SvgPoint {
706 x: cos_phi * dx2 - sin_phi * dy2 + cx,
707 y: sin_phi * dx2 + cos_phi * dy2 + cy,
708 };
709 let ep = SvgPoint {
710 x: cos_phi * dx3 - sin_phi * dy3 + cx,
711 y: sin_phi * dx3 + cos_phi * dy3 + cy,
712 };
713
714 (c1, c2, ep)
715}
716
717#[must_use]
721pub fn svg_circle_to_paths(cx: f32, cy: f32, r: f32) -> SvgPath {
722 let k = r * KAPPA;
723
724 let elements = vec![
725 SvgPathElement::CubicCurve(SvgCubicCurve {
727 start: SvgPoint { x: cx, y: cy - r },
728 ctrl_1: SvgPoint {
729 x: cx + k,
730 y: cy - r,
731 },
732 ctrl_2: SvgPoint {
733 x: cx + r,
734 y: cy - k,
735 },
736 end: SvgPoint { x: cx + r, y: cy },
737 }),
738 SvgPathElement::CubicCurve(SvgCubicCurve {
740 start: SvgPoint { x: cx + r, y: cy },
741 ctrl_1: SvgPoint {
742 x: cx + r,
743 y: cy + k,
744 },
745 ctrl_2: SvgPoint {
746 x: cx + k,
747 y: cy + r,
748 },
749 end: SvgPoint { x: cx, y: cy + r },
750 }),
751 SvgPathElement::CubicCurve(SvgCubicCurve {
753 start: SvgPoint { x: cx, y: cy + r },
754 ctrl_1: SvgPoint {
755 x: cx - k,
756 y: cy + r,
757 },
758 ctrl_2: SvgPoint {
759 x: cx - r,
760 y: cy + k,
761 },
762 end: SvgPoint { x: cx - r, y: cy },
763 }),
764 SvgPathElement::CubicCurve(SvgCubicCurve {
766 start: SvgPoint { x: cx - r, y: cy },
767 ctrl_1: SvgPoint {
768 x: cx - r,
769 y: cy - k,
770 },
771 ctrl_2: SvgPoint {
772 x: cx - k,
773 y: cy - r,
774 },
775 end: SvgPoint { x: cx, y: cy - r },
776 }),
777 ];
778
779 SvgPath {
780 items: SvgPathElementVec::from_vec(elements),
781 }
782}
783
784#[must_use]
789#[allow(clippy::vec_init_then_push)]
792#[allow(clippy::too_many_lines)] pub fn svg_rect_to_path(x: f32, y: f32, w: f32, h: f32, rx: f32, ry: f32) -> SvgPath {
794 let rx = rx.min(w / 2.0);
795 let ry = ry.min(h / 2.0);
796
797 if rx < CLOSEPATH_EPSILON && ry < CLOSEPATH_EPSILON {
798 let tl = SvgPoint { x, y };
800 let tr = SvgPoint { x: x + w, y };
801 let br = SvgPoint { x: x + w, y: y + h };
802 let bl = SvgPoint { x, y: y + h };
803
804 let elements = vec![
805 SvgPathElement::Line(SvgLine { start: tl, end: tr }),
806 SvgPathElement::Line(SvgLine { start: tr, end: br }),
807 SvgPathElement::Line(SvgLine {
808 start: br,
809 end: bl,
810 }),
811 SvgPathElement::Line(SvgLine { start: bl, end: tl }),
812 ];
813
814 return SvgPath {
815 items: SvgPathElementVec::from_vec(elements),
816 };
817 }
818
819 let kx = rx * KAPPA;
821 let ky = ry * KAPPA;
822
823 let mut elements = Vec::with_capacity(8);
824
825 elements.push(SvgPathElement::Line(SvgLine {
827 start: SvgPoint { x: x + rx, y },
828 end: SvgPoint { x: x + w - rx, y },
829 }));
830 elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
832 start: SvgPoint { x: x + w - rx, y },
833 ctrl_1: SvgPoint {
834 x: x + w - rx + kx,
835 y,
836 },
837 ctrl_2: SvgPoint {
838 x: x + w,
839 y: y + ry - ky,
840 },
841 end: SvgPoint {
842 x: x + w,
843 y: y + ry,
844 },
845 }));
846 elements.push(SvgPathElement::Line(SvgLine {
848 start: SvgPoint {
849 x: x + w,
850 y: y + ry,
851 },
852 end: SvgPoint {
853 x: x + w,
854 y: y + h - ry,
855 },
856 }));
857 elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
859 start: SvgPoint {
860 x: x + w,
861 y: y + h - ry,
862 },
863 ctrl_1: SvgPoint {
864 x: x + w,
865 y: y + h - ry + ky,
866 },
867 ctrl_2: SvgPoint {
868 x: x + w - rx + kx,
869 y: y + h,
870 },
871 end: SvgPoint {
872 x: x + w - rx,
873 y: y + h,
874 },
875 }));
876 elements.push(SvgPathElement::Line(SvgLine {
878 start: SvgPoint {
879 x: x + w - rx,
880 y: y + h,
881 },
882 end: SvgPoint { x: x + rx, y: y + h },
883 }));
884 elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
886 start: SvgPoint { x: x + rx, y: y + h },
887 ctrl_1: SvgPoint {
888 x: x + rx - kx,
889 y: y + h,
890 },
891 ctrl_2: SvgPoint {
892 x,
893 y: y + h - ry + ky,
894 },
895 end: SvgPoint { x, y: y + h - ry },
896 }));
897 elements.push(SvgPathElement::Line(SvgLine {
899 start: SvgPoint { x, y: y + h - ry },
900 end: SvgPoint { x, y: y + ry },
901 }));
902 elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
904 start: SvgPoint { x, y: y + ry },
905 ctrl_1: SvgPoint {
906 x,
907 y: y + ry - ky,
908 },
909 ctrl_2: SvgPoint {
910 x: x + rx - kx,
911 y,
912 },
913 end: SvgPoint { x: x + rx, y },
914 }));
915
916 SvgPath {
917 items: SvgPathElementVec::from_vec(elements),
918 }
919}
920
921#[cfg(test)]
922mod tests {
923 use super::*;
924
925 #[test]
929 fn m0_0z5_does_not_hang() {
930 let err = parse_svg_path_d("M0 0Z5").unwrap_err();
931 match err {
932 SvgPathParseError::UnexpectedChar { ch, .. } => assert_eq!(ch, '5'),
933 other => panic!("expected UnexpectedChar, got {other:?}"),
934 }
935 }
936
937 #[test]
939 fn stray_byte_after_closepath_rejected() {
940 for s in ["M0 0Z9", "m0 0z-", "M0 0Z."] {
941 assert!(
942 matches!(
943 parse_svg_path_d(s),
944 Err(SvgPathParseError::UnexpectedChar { .. })
945 ),
946 "expected UnexpectedChar for {s:?}"
947 );
948 }
949 }
950
951 #[test]
954 fn error_char_is_unicode_not_byte() {
955 let err = parse_svg_path_d("ü10 10").unwrap_err();
957 match err {
958 SvgPathParseError::UnexpectedChar { ch, pos } => {
959 assert_eq!(ch, 'ü');
960 assert_eq!(pos, 0);
961 }
962 other => panic!("expected UnexpectedChar, got {other:?}"),
963 }
964 }
965
966 #[test]
968 fn valid_closepath_then_command_ok() {
969 let parsed = parse_svg_path_d("M0 0 L10 0 Z M20 20 L30 20 Z");
970 assert!(parsed.is_ok(), "valid multi-subpath path should parse");
971 }
972}
973
974#[cfg(test)]
975#[allow(clippy::float_cmp)] mod autotest_generated {
977 use alloc::format;
978
979 use super::*;
980
981 fn approx(a: f32, b: f32) -> bool {
984 (a - b).abs() < 1e-4
985 }
986
987 fn all_points(path: &SvgPath) -> Vec<SvgPoint> {
989 let mut out = Vec::new();
990 for e in path.items.as_ref() {
991 match e {
992 SvgPathElement::Line(l) => {
993 out.push(l.start);
994 out.push(l.end);
995 }
996 SvgPathElement::QuadraticCurve(q) => {
997 out.push(q.start);
998 out.push(q.ctrl);
999 out.push(q.end);
1000 }
1001 SvgPathElement::CubicCurve(c) => {
1002 out.push(c.start);
1003 out.push(c.ctrl_1);
1004 out.push(c.ctrl_2);
1005 out.push(c.end);
1006 }
1007 }
1008 }
1009 out
1010 }
1011
1012 fn assert_contiguous(items: &[SvgPathElement], what: &str) {
1015 for w in items.windows(2) {
1016 assert_eq!(
1017 w[0].get_end(),
1018 w[1].get_start(),
1019 "{what}: element chain is not contiguous"
1020 );
1021 }
1022 }
1023
1024 #[test]
1027 fn char_at_zero_and_ascii() {
1028 assert_eq!(char_at(b"M0 0", 0), 'M');
1030 assert_eq!(char_at(b"M0 0", 2), ' ');
1031 assert_eq!(char_at(b"M0 0", 3), '0');
1032 }
1033
1034 #[test]
1035 fn char_at_empty_input_is_replacement() {
1036 assert_eq!(char_at(b"", 0), char::REPLACEMENT_CHARACTER);
1037 }
1038
1039 #[test]
1040 fn char_at_past_end_is_replacement_not_panic() {
1041 assert_eq!(char_at(b"abc", 3), char::REPLACEMENT_CHARACTER);
1042 assert_eq!(char_at(b"abc", 4), char::REPLACEMENT_CHARACTER);
1043 }
1044
1045 #[test]
1047 fn char_at_usize_max_is_replacement() {
1048 assert_eq!(char_at(b"abc", usize::MAX), char::REPLACEMENT_CHARACTER);
1049 assert_eq!(char_at(b"", usize::MAX), char::REPLACEMENT_CHARACTER);
1050 }
1051
1052 #[test]
1054 fn char_at_decodes_multibyte_not_latin1() {
1055 assert_eq!(char_at("ü".as_bytes(), 0), 'ü');
1056 assert_eq!(char_at("€".as_bytes(), 0), '€');
1057 assert_eq!(char_at("\u{1F600}".as_bytes(), 0), '\u{1F600}');
1058 }
1059
1060 #[test]
1062 fn char_at_mid_codepoint_offset_is_replacement() {
1063 let bytes = "😀".as_bytes(); for pos in 1..bytes.len() {
1065 assert_eq!(
1066 char_at(bytes, pos),
1067 char::REPLACEMENT_CHARACTER,
1068 "mid-codepoint offset {pos} must not panic"
1069 );
1070 }
1071 }
1072
1073 #[test]
1077 fn char_at_invalid_utf8_tail_is_replacement() {
1078 assert_eq!(char_at(&[b'A', 0xFF], 0), char::REPLACEMENT_CHARACTER);
1079 assert_eq!(char_at(&[0xFF], 0), char::REPLACEMENT_CHARACTER);
1080 assert_eq!(char_at(b"AB", 0), 'A');
1082 }
1083
1084 #[test]
1087 fn error_display_is_non_empty_for_every_variant() {
1088 let variants = [
1089 SvgPathParseError::EmptyPath,
1090 SvgPathParseError::UnexpectedChar { pos: 0, ch: 'x' },
1091 SvgPathParseError::ExpectedNumber { pos: 0 },
1092 SvgPathParseError::InvalidArcFlag { pos: 0 },
1093 ];
1094 for v in variants {
1095 let s = format!("{v}");
1096 assert!(!s.is_empty(), "Display for {v:?} must not be empty");
1097 assert!(!format!("{v:?}").is_empty(), "Debug must not be empty");
1098 }
1099 }
1100
1101 #[test]
1102 fn error_display_edge_values_do_not_panic() {
1103 let s = format!(
1104 "{}",
1105 SvgPathParseError::UnexpectedChar {
1106 pos: usize::MAX,
1107 ch: char::REPLACEMENT_CHARACTER,
1108 }
1109 );
1110 assert!(s.contains(&format!("{}", usize::MAX)));
1111 assert!(s.contains(char::REPLACEMENT_CHARACTER));
1112
1113 for ch in ['\0', '\u{1F600}', '\u{0301}'] {
1115 let s = format!("{}", SvgPathParseError::UnexpectedChar { pos: 0, ch });
1116 assert!(!s.is_empty());
1117 }
1118 assert!(!format!("{}", SvgPathParseError::ExpectedNumber { pos: usize::MAX }).is_empty());
1119 assert!(!format!("{}", SvgPathParseError::InvalidArcFlag { pos: usize::MAX }).is_empty());
1120 }
1121
1122 #[test]
1125 fn parser_new_invariants_hold() {
1126 let p = PathParser::new(b"M0 0");
1127 assert_eq!(p.pos, 0);
1128 assert_eq!(p.current, SvgPoint { x: 0.0, y: 0.0 });
1129 assert_eq!(p.subpath_start, SvgPoint { x: 0.0, y: 0.0 });
1130 assert!(p.last_control.is_none());
1131 assert_eq!(p.last_command, 0);
1132 assert_eq!(p.input.len(), 4);
1133 }
1134
1135 #[test]
1136 fn parser_new_on_empty_input_does_not_panic() {
1137 let p = PathParser::new(b"");
1138 assert!(p.at_end(), "empty input is immediately at_end");
1139 assert_eq!(p.peek(), None);
1140 assert!(!p.has_number());
1141 }
1142
1143 #[test]
1144 fn at_end_and_peek_agree_across_the_whole_input() {
1145 let mut p = PathParser::new(b"ab");
1146 assert!(!p.at_end());
1147 assert_eq!(p.peek(), Some(b'a'));
1148 p.pos = 1;
1149 assert!(!p.at_end());
1150 assert_eq!(p.peek(), Some(b'b'));
1151 p.pos = 2;
1152 assert!(p.at_end());
1153 assert_eq!(p.peek(), None);
1154 }
1155
1156 #[test]
1158 fn peek_and_at_end_at_extreme_positions() {
1159 let mut p = PathParser::new(b"abc");
1160 p.pos = usize::MAX;
1161 assert!(p.at_end());
1162 assert_eq!(p.peek(), None);
1163 assert!(!p.has_number());
1164 }
1165
1166 #[test]
1169 fn skip_whitespace_and_commas_consumes_all_separators() {
1170 let mut p = PathParser::new(b" \t\r\n,,, \tX");
1171 p.skip_whitespace_and_commas();
1172 assert_eq!(p.peek(), Some(b'X'));
1173 }
1174
1175 #[test]
1177 fn skip_whitespace_stops_at_comma() {
1178 let mut p = PathParser::new(b" ,1");
1179 p.skip_whitespace();
1180 assert_eq!(p.peek(), Some(b','));
1181 assert_eq!(p.pos, 2);
1182 }
1183
1184 #[test]
1185 fn skip_on_empty_and_all_separator_input_terminates() {
1186 let mut p = PathParser::new(b"");
1187 p.skip_whitespace();
1188 p.skip_whitespace_and_commas();
1189 assert!(p.at_end());
1190
1191 let all_ws = " \t\r\n,".repeat(20_000);
1192 let mut p = PathParser::new(all_ws.as_bytes());
1193 p.skip_whitespace_and_commas();
1194 assert!(p.at_end(), "a huge run of separators must be fully consumed");
1195 assert_eq!(p.pos, all_ws.len());
1196 }
1197
1198 #[test]
1200 fn skip_is_a_no_op_on_non_separator() {
1201 let mut p = PathParser::new("😀".as_bytes());
1202 p.skip_whitespace_and_commas();
1203 assert_eq!(p.pos, 0);
1204 let mut p = PathParser::new(b"\x0c\x0b1");
1206 p.skip_whitespace();
1207 assert_eq!(p.pos, 0);
1208 }
1209
1210 #[test]
1213 fn has_number_true_for_number_starters() {
1214 for s in ["0", "9", "+", "-", ".", "5.5", "-.5"] {
1215 assert!(
1216 PathParser::new(s.as_bytes()).has_number(),
1217 "{s:?} should look like a number start"
1218 );
1219 }
1220 }
1221
1222 #[test]
1223 fn has_number_false_for_non_number_starters() {
1224 for s in ["", " ", ",", "M", "z", "e", "E", "😀", "\u{0301}"] {
1226 assert!(
1227 !PathParser::new(s.as_bytes()).has_number(),
1228 "{s:?} should not look like a number start"
1229 );
1230 }
1231 }
1232
1233 fn num(s: &str) -> Result<f32, SvgPathParseError> {
1236 PathParser::new(s.as_bytes()).parse_number()
1237 }
1238
1239 #[test]
1240 fn parse_number_valid_minimal() {
1241 assert_eq!(num("0").unwrap(), 0.0);
1242 assert_eq!(num("5").unwrap(), 5.0);
1243 assert_eq!(num("+5").unwrap(), 5.0);
1244 assert_eq!(num("-5").unwrap(), -5.0);
1245 assert!(approx(num("12.34").unwrap(), 12.34));
1246 assert!(approx(num(".5").unwrap(), 0.5));
1247 assert_eq!(num("5.").unwrap(), 5.0);
1248 assert!(approx(num("1e2").unwrap(), 100.0));
1249 assert!(approx(num("1E-2").unwrap(), 0.01));
1250 assert!(approx(num(" ,, 7").unwrap(), 7.0), "leading separators skipped");
1251 }
1252
1253 #[test]
1254 fn parse_number_empty_input_is_err() {
1255 assert_eq!(num(""), Err(SvgPathParseError::ExpectedNumber { pos: 0 }));
1256 }
1257
1258 #[test]
1260 fn parse_number_whitespace_only_is_err() {
1261 assert_eq!(num(" "), Err(SvgPathParseError::ExpectedNumber { pos: 3 }));
1262 assert_eq!(num("\t\n"), Err(SvgPathParseError::ExpectedNumber { pos: 2 }));
1263 assert_eq!(num(" , "), Err(SvgPathParseError::ExpectedNumber { pos: 3 }));
1264 }
1265
1266 #[test]
1267 fn parse_number_garbage_is_err_never_panics() {
1268 for s in ["abc", "@", "#$%", "-", "+", ".", "-.", "+.", "e5", "NaN", "inf", "-inf"] {
1269 assert!(
1270 matches!(num(s), Err(SvgPathParseError::ExpectedNumber { .. })),
1271 "{s:?} must be rejected, got {:?}",
1272 num(s)
1273 );
1274 }
1275 }
1276
1277 #[test]
1279 fn parse_number_dangling_exponent_is_err() {
1280 for s in ["1e", "1E", "1e+", "1e-", "1.5e"] {
1281 assert!(
1282 matches!(num(s), Err(SvgPathParseError::ExpectedNumber { pos: 0 })),
1283 "{s:?} must be rejected"
1284 );
1285 }
1286 }
1287
1288 #[test]
1289 fn parse_number_unicode_does_not_panic() {
1290 for s in ["😀", "\u{0301}", "ü", "€1", "1"] {
1291 assert!(num(s).is_err(), "{s:?} must be rejected");
1292 }
1293 let mut p = PathParser::new("1😀".as_bytes());
1295 assert_eq!(p.parse_number().unwrap(), 1.0);
1296 assert_eq!(p.pos, 1);
1297 }
1298
1299 #[test]
1302 fn parse_number_boundary_values_saturate() {
1303 assert!(num("-0").unwrap().is_sign_negative(), "-0 keeps its sign bit");
1304 assert_eq!(num("-0").unwrap(), -0.0);
1305
1306 assert!(num("1e999").unwrap().is_infinite());
1307 assert!(num("1e999").unwrap().is_sign_positive());
1308 assert!(num("-1e999").unwrap().is_infinite());
1309 assert!(num("-1e999").unwrap().is_sign_negative());
1310
1311 assert_eq!(num("1e-999").unwrap(), 0.0, "underflow flushes to zero");
1312
1313 assert!(num("9223372036854775807").unwrap().is_finite());
1315 assert!(num("340282350000000000000000000000000000000").unwrap().is_finite());
1316 assert!(num("1e39").unwrap().is_infinite());
1318 }
1319
1320 #[test]
1322 fn parse_number_extremely_long_input_terminates() {
1323 let huge = "9".repeat(20_000);
1324 assert!(num(&huge).unwrap().is_infinite());
1325
1326 let long_frac = format!("0.{}", "0".repeat(20_000));
1327 assert_eq!(num(&long_frac).unwrap(), 0.0);
1328
1329 let long_zeros = format!("{}1", "0".repeat(20_000));
1330 assert_eq!(num(&long_zeros).unwrap(), 1.0);
1331 }
1332
1333 #[test]
1336 fn parse_number_stops_at_trailing_junk() {
1337 let mut p = PathParser::new(b"1.2.3");
1338 assert!(approx(p.parse_number().unwrap(), 1.2));
1339 assert_eq!(p.pos, 3, "second '.' must not be consumed");
1340
1341 let mut p = PathParser::new(b"5;garbage");
1342 assert_eq!(p.parse_number().unwrap(), 5.0);
1343 assert_eq!(p.peek(), Some(b';'));
1344 }
1345
1346 #[test]
1348 fn parse_number_never_overruns_the_buffer() {
1349 for s in ["", "-", ".", "1e", "1e+", "1.", "+.e", "1e-", "999"] {
1350 let mut p = PathParser::new(s.as_bytes());
1351 let _ = p.parse_number();
1352 assert!(p.pos <= s.len(), "{s:?}: pos {} > len {}", p.pos, s.len());
1353 }
1354 }
1355
1356 fn flag(s: &str) -> Result<bool, SvgPathParseError> {
1359 PathParser::new(s.as_bytes()).parse_flag()
1360 }
1361
1362 #[test]
1363 fn parse_flag_valid_minimal() {
1364 assert!(!flag("0").unwrap());
1365 assert!(flag("1").unwrap());
1366 assert!(flag(" , 1").unwrap(), "separators are skipped first");
1367 }
1368
1369 #[test]
1371 fn parse_flag_consumes_exactly_one_byte() {
1372 let mut p = PathParser::new(b"10");
1373 assert!(p.parse_flag().unwrap());
1374 assert_eq!(p.pos, 1);
1375 assert!(!p.parse_flag().unwrap());
1376 assert_eq!(p.pos, 2);
1377 }
1378
1379 #[test]
1380 fn parse_flag_empty_and_whitespace_only_are_err() {
1381 assert_eq!(flag(""), Err(SvgPathParseError::InvalidArcFlag { pos: 0 }));
1382 assert_eq!(flag(" "), Err(SvgPathParseError::InvalidArcFlag { pos: 3 }));
1383 }
1384
1385 #[test]
1387 fn parse_flag_garbage_is_err_and_does_not_advance() {
1388 for s in ["2", "9", "-1", "+1", "x", ".", "😀", "0.5"] {
1389 let mut p = PathParser::new(s.as_bytes());
1390 let before = p.pos;
1391 match p.parse_flag() {
1392 Err(SvgPathParseError::InvalidArcFlag { pos }) => {
1393 assert_eq!(pos, p.pos, "{s:?}: reported pos must be the cursor");
1394 assert_eq!(p.pos, before, "{s:?}: rejected flag must not advance");
1395 }
1396 Ok(v) => assert!(s == "0.5" && !v, "{s:?} unexpectedly parsed as {v}"),
1398 other => panic!("{s:?}: unexpected {other:?}"),
1399 }
1400 }
1401 }
1402
1403 #[test]
1405 fn parse_flag_extremely_long_separator_run_terminates() {
1406 let s = " ".repeat(100_000);
1407 assert_eq!(
1408 flag(&s),
1409 Err(SvgPathParseError::InvalidArcFlag { pos: 100_000 })
1410 );
1411 }
1412
1413 fn pair(s: &str) -> Result<(f32, f32), SvgPathParseError> {
1416 PathParser::new(s.as_bytes()).parse_coordinate_pair()
1417 }
1418
1419 #[test]
1420 fn parse_coordinate_pair_valid_minimal() {
1421 assert_eq!(pair("1 2").unwrap(), (1.0, 2.0));
1422 assert_eq!(pair("1,2").unwrap(), (1.0, 2.0));
1423 assert_eq!(pair(" 1 , 2 ").unwrap(), (1.0, 2.0));
1424 assert_eq!(pair("-1-2").unwrap(), (-1.0, -2.0));
1426 }
1427
1428 #[test]
1430 fn parse_coordinate_pair_splits_on_second_dot() {
1431 let (x, y) = pair("1.5.5").unwrap();
1432 assert!(approx(x, 1.5) && approx(y, 0.5), "got ({x}, {y})");
1433 }
1434
1435 #[test]
1436 fn parse_coordinate_pair_empty_and_partial_are_err() {
1437 assert!(pair("").is_err());
1438 assert!(pair(" ").is_err());
1439 assert!(pair("1").is_err(), "a lone x with no y must be rejected");
1440 assert!(pair("1 ").is_err());
1441 assert!(pair("1,").is_err());
1442 }
1443
1444 #[test]
1445 fn parse_coordinate_pair_garbage_and_unicode_are_err() {
1446 for s in ["abc", "1 abc", "😀 1", "1 😀", ";;", "1;2"] {
1447 assert!(pair(s).is_err(), "{s:?} must be rejected");
1448 }
1449 }
1450
1451 #[test]
1452 fn parse_coordinate_pair_boundary_values() {
1453 let (x, y) = pair("1e999 -1e999").unwrap();
1454 assert!(x.is_infinite() && x.is_sign_positive());
1455 assert!(y.is_infinite() && y.is_sign_negative());
1456
1457 let (x, y) = pair("-0 0").unwrap();
1458 assert!(x.is_sign_negative() && y.is_sign_positive());
1459 }
1460
1461 #[test]
1462 fn parse_coordinate_pair_extremely_long_input_terminates() {
1463 let s = format!("{} {}", "9".repeat(10_000), "9".repeat(10_000));
1464 let (x, y) = pair(&s).unwrap();
1465 assert!(x.is_infinite() && y.is_infinite());
1466 }
1467
1468 #[test]
1471 fn make_absolute_zero_and_absolute_mode_is_identity() {
1472 let mut p = PathParser::new(b"");
1473 p.current = SvgPoint { x: 7.0, y: -3.0 };
1474 assert_eq!(p.make_absolute(0.0, 0.0, false), SvgPoint { x: 0.0, y: 0.0 });
1476 assert_eq!(p.make_absolute(1.0, 2.0, false), SvgPoint { x: 1.0, y: 2.0 });
1477 assert_eq!(p.make_absolute(0.0, 0.0, true), SvgPoint { x: 7.0, y: -3.0 });
1479 assert_eq!(p.make_absolute(-7.0, 3.0, true), SvgPoint { x: 0.0, y: 0.0 });
1480 }
1481
1482 #[test]
1483 fn make_absolute_negative_inputs() {
1484 let mut p = PathParser::new(b"");
1485 p.current = SvgPoint { x: -10.0, y: -10.0 };
1486 assert_eq!(p.make_absolute(-5.0, -5.0, true), SvgPoint { x: -15.0, y: -15.0 });
1487 assert_eq!(p.make_absolute(-5.0, -5.0, false), SvgPoint { x: -5.0, y: -5.0 });
1488 }
1489
1490 #[test]
1492 fn make_absolute_overflow_saturates_to_infinity() {
1493 let mut p = PathParser::new(b"");
1494 p.current = SvgPoint {
1495 x: f32::MAX,
1496 y: f32::MIN,
1497 };
1498 let r = p.make_absolute(f32::MAX, f32::MIN, true);
1499 assert!(r.x.is_infinite() && r.x.is_sign_positive());
1500 assert!(r.y.is_infinite() && r.y.is_sign_negative());
1501 }
1502
1503 #[test]
1504 fn make_absolute_nan_and_inf_are_defined_not_panics() {
1505 let mut p = PathParser::new(b"");
1506 p.current = SvgPoint {
1507 x: f32::INFINITY,
1508 y: 0.0,
1509 };
1510 let r = p.make_absolute(f32::NEG_INFINITY, f32::NAN, true);
1512 assert!(r.x.is_nan(), "inf + -inf must be NaN");
1513 assert!(r.y.is_nan());
1514
1515 let r = p.make_absolute(f32::NAN, f32::INFINITY, false);
1517 assert!(r.x.is_nan());
1518 assert!(r.y.is_infinite());
1519 }
1520
1521 fn run_handler<F>(
1526 input: &str,
1527 current: SvgPoint,
1528 last_command: u8,
1529 last_control: Option<SvgPoint>,
1530 f: F,
1531 ) -> (Result<(), SvgPathParseError>, Vec<SvgPathElement>, SvgPoint)
1532 where
1533 F: FnOnce(&mut PathParser<'_>, &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError>,
1534 {
1535 let mut p = PathParser::new(input.as_bytes());
1536 p.current = current;
1537 p.last_command = last_command;
1538 p.last_control = last_control;
1539 let mut els = Vec::new();
1540 let r = f(&mut p, &mut els);
1541 (r, els, p.current)
1542 }
1543
1544 const ORIGIN: SvgPoint = SvgPoint { x: 0.0, y: 0.0 };
1545
1546 #[test]
1547 fn handle_line_to_absolute_and_relative() {
1548 let start = SvgPoint { x: 10.0, y: 10.0 };
1549 let (r, els, cur) = run_handler("5 5", start, b'L', None, |p, e| p.handle_line_to(false, e));
1550 assert!(r.is_ok());
1551 assert_eq!(els.len(), 1);
1552 assert_eq!(els[0].get_start(), start);
1553 assert_eq!(els[0].get_end(), SvgPoint { x: 5.0, y: 5.0 });
1554 assert_eq!(cur, SvgPoint { x: 5.0, y: 5.0 });
1555
1556 let (r, els, cur) = run_handler("5 5", start, b'l', None, |p, e| p.handle_line_to(true, e));
1557 assert!(r.is_ok());
1558 assert_eq!(els[0].get_end(), SvgPoint { x: 15.0, y: 15.0 });
1559 assert_eq!(cur, SvgPoint { x: 15.0, y: 15.0 });
1560 }
1561
1562 #[test]
1564 fn handle_horizontal_and_vertical_preserve_the_other_axis() {
1565 let start = SvgPoint { x: 3.0, y: 4.0 };
1566 let (_, els, _) = run_handler("9", start, b'H', None, |p, e| p.handle_horizontal_to(false, e));
1567 assert_eq!(els[0].get_end(), SvgPoint { x: 9.0, y: 4.0 });
1568
1569 let (_, els, _) = run_handler("9", start, b'V', None, |p, e| p.handle_vertical_to(false, e));
1570 assert_eq!(els[0].get_end(), SvgPoint { x: 3.0, y: 9.0 });
1571
1572 let (_, els, _) = run_handler("1e999", start, b'h', None, |p, e| p.handle_horizontal_to(true, e));
1573 let end = els[0].get_end();
1574 assert!(end.x.is_infinite(), "relative H by +inf saturates");
1575 assert_eq!(end.y, 4.0, "y must be untouched");
1576 }
1577
1578 #[test]
1579 fn handlers_reject_empty_and_garbage_input_without_panicking() {
1580 for input in ["", " ", "abc", "😀", ";", "1"] {
1581 let (r, _, _) = run_handler(input, ORIGIN, 0, None, |p, e| p.handle_line_to(false, e));
1583 assert!(r.is_err(), "line_to({input:?}) must be Err");
1584
1585 let (r, _, _) = run_handler(input, ORIGIN, 0, None, |p, e| p.handle_cubic_to(false, e));
1586 assert!(r.is_err(), "cubic_to({input:?}) must be Err");
1587
1588 let (r, _, _) = run_handler(input, ORIGIN, 0, None, |p, e| p.handle_quadratic_to(false, e));
1589 assert!(r.is_err(), "quadratic_to({input:?}) must be Err");
1590
1591 let (r, _, _) = run_handler(input, ORIGIN, 0, None, |p, e| p.handle_arc_to(false, e));
1592 assert!(r.is_err(), "arc_to({input:?}) must be Err");
1593
1594 if input != "1" {
1595 let (r, _, _) =
1596 run_handler(input, ORIGIN, 0, None, |p, e| p.handle_horizontal_to(false, e));
1597 assert!(r.is_err(), "horizontal_to({input:?}) must be Err");
1598 let (r, _, _) =
1599 run_handler(input, ORIGIN, 0, None, |p, e| p.handle_vertical_to(false, e));
1600 assert!(r.is_err(), "vertical_to({input:?}) must be Err");
1601 }
1602 }
1603 }
1604
1605 #[test]
1606 fn handle_cubic_to_records_second_control_point() {
1607 let (r, els, _) = run_handler("1 1 2 2 3 3", ORIGIN, b'C', None, |p, e| {
1608 p.handle_cubic_to(false, e)
1609 });
1610 assert!(r.is_ok());
1611 match els[0] {
1612 SvgPathElement::CubicCurve(c) => {
1613 assert_eq!(c.start, ORIGIN);
1614 assert_eq!(c.ctrl_1, SvgPoint { x: 1.0, y: 1.0 });
1615 assert_eq!(c.ctrl_2, SvgPoint { x: 2.0, y: 2.0 });
1616 assert_eq!(c.end, SvgPoint { x: 3.0, y: 3.0 });
1617 }
1618 other => panic!("expected CubicCurve, got {other:?}"),
1619 }
1620 }
1621
1622 #[test]
1625 fn handle_smooth_cubic_reflects_only_after_c_or_s() {
1626 let cur = SvgPoint { x: 10.0, y: 10.0 };
1627 let lc = Some(SvgPoint { x: 8.0, y: 6.0 });
1628
1629 let (_, els, _) = run_handler("1 1 2 2", cur, b'C', lc, |p, e| p.handle_smooth_cubic_to(false, e));
1630 match els[0] {
1631 SvgPathElement::CubicCurve(c) => assert_eq!(c.ctrl_1, SvgPoint { x: 12.0, y: 14.0 }),
1633 other => panic!("expected CubicCurve, got {other:?}"),
1634 }
1635
1636 let (_, els, _) = run_handler("1 1 2 2", cur, b'L', lc, |p, e| p.handle_smooth_cubic_to(false, e));
1638 match els[0] {
1639 SvgPathElement::CubicCurve(c) => assert_eq!(c.ctrl_1, cur),
1640 other => panic!("expected CubicCurve, got {other:?}"),
1641 }
1642
1643 let (_, els, _) = run_handler("1 1 2 2", cur, b'S', None, |p, e| p.handle_smooth_cubic_to(false, e));
1645 match els[0] {
1646 SvgPathElement::CubicCurve(c) => assert_eq!(c.ctrl_1, cur),
1647 other => panic!("expected CubicCurve, got {other:?}"),
1648 }
1649 }
1650
1651 #[test]
1652 fn handle_smooth_quadratic_reflects_only_after_q_or_t() {
1653 let cur = SvgPoint { x: 10.0, y: 10.0 };
1654 let lc = Some(SvgPoint { x: 8.0, y: 6.0 });
1655
1656 let (_, els, _) = run_handler("2 2", cur, b'Q', lc, |p, e| p.handle_smooth_quadratic_to(false, e));
1657 match els[0] {
1658 SvgPathElement::QuadraticCurve(q) => assert_eq!(q.ctrl, SvgPoint { x: 12.0, y: 14.0 }),
1659 other => panic!("expected QuadraticCurve, got {other:?}"),
1660 }
1661
1662 let (_, els, _) = run_handler("2 2", cur, b'M', lc, |p, e| p.handle_smooth_quadratic_to(false, e));
1663 match els[0] {
1664 SvgPathElement::QuadraticCurve(q) => assert_eq!(q.ctrl, cur),
1665 other => panic!("expected QuadraticCurve, got {other:?}"),
1666 }
1667 }
1668
1669 #[test]
1671 fn handle_arc_to_takes_abs_of_radii() {
1672 let (r, els, cur) = run_handler("-5 -5 0 0 1 10 0", ORIGIN, b'A', None, |p, e| {
1673 p.handle_arc_to(false, e)
1674 });
1675 assert!(r.is_ok());
1676 assert!(!els.is_empty(), "negative radii must still produce an arc");
1677 assert!(
1678 els.iter().all(|e| matches!(e, SvgPathElement::CubicCurve(_))),
1679 "abs() of the radii keeps this a real arc, not a line fallback"
1680 );
1681 assert_eq!(cur, SvgPoint { x: 10.0, y: 0.0 });
1682 }
1683
1684 #[test]
1686 fn handle_arc_to_zero_radius_degenerates_to_line() {
1687 let (r, els, _) = run_handler("0 0 0 0 1 10 0", ORIGIN, b'A', None, |p, e| {
1688 p.handle_arc_to(false, e)
1689 });
1690 assert!(r.is_ok());
1691 assert_eq!(els.len(), 1);
1692 assert!(matches!(els[0], SvgPathElement::Line(_)));
1693 assert_eq!(els[0].get_end(), SvgPoint { x: 10.0, y: 0.0 });
1694 }
1695
1696 #[test]
1697 fn handle_arc_to_rejects_out_of_range_flags() {
1698 for input in ["5 5 0 2 1 10 0", "5 5 0 1 2 10 0", "5 5 0 x 1 10 0", "5 5 0"] {
1699 let (r, _, _) = run_handler(input, ORIGIN, b'A', None, |p, e| p.handle_arc_to(false, e));
1700 assert!(r.is_err(), "arc flags in {input:?} must be rejected");
1701 }
1702 let (r, _, _) = run_handler("5 5 0 2 1 10 0", ORIGIN, b'A', None, |p, e| {
1703 p.handle_arc_to(false, e)
1704 });
1705 assert!(matches!(r, Err(SvgPathParseError::InvalidArcFlag { .. })));
1706 }
1707
1708 #[test]
1711 fn handle_arc_to_infinite_radii_is_bounded() {
1712 let (r, els, _) = run_handler("1e999 1e999 0 0 1 10 10", ORIGIN, b'A', None, |p, e| {
1713 p.handle_arc_to(false, e)
1714 });
1715 assert!(r.is_ok());
1716 assert!(
1717 els.len() <= 4,
1718 "a single arc must never expand past 4 cubics, got {}",
1719 els.len()
1720 );
1721 }
1722
1723 #[test]
1726 fn parse_path_empty_and_whitespace_only_is_empty_path_err() {
1727 for s in ["", " ", "\t\n", "\r\n \t"] {
1728 assert_eq!(
1729 parse_svg_path_d(s),
1730 Err(SvgPathParseError::EmptyPath),
1731 "{s:?} must be EmptyPath"
1732 );
1733 }
1734 }
1735
1736 #[test]
1737 fn parse_path_valid_minimal() {
1738 let mp = parse_svg_path_d("M10 20 L30 40").unwrap();
1739 let rings = mp.rings.as_ref();
1740 assert_eq!(rings.len(), 1);
1741 let items = rings[0].items.as_ref();
1742 assert_eq!(items.len(), 1);
1743 assert_eq!(items[0].get_start(), SvgPoint { x: 10.0, y: 20.0 });
1744 assert_eq!(items[0].get_end(), SvgPoint { x: 30.0, y: 40.0 });
1745 }
1746
1747 #[test]
1749 fn parse_path_relative_accumulates() {
1750 let mp = parse_svg_path_d("m10 20 l30 40").unwrap();
1751 let items_owner = &mp.rings.as_ref()[0];
1752 let items = items_owner.items.as_ref();
1753 assert_eq!(items[0].get_start(), SvgPoint { x: 10.0, y: 20.0 });
1754 assert_eq!(items[0].get_end(), SvgPoint { x: 40.0, y: 60.0 });
1755 }
1756
1757 #[test]
1759 fn parse_path_moveto_only_yields_no_rings() {
1760 let mp = parse_svg_path_d("M10 10").unwrap();
1761 assert_eq!(mp.rings.as_ref().len(), 0);
1762 }
1763
1764 #[test]
1766 fn parse_path_implicit_lineto_after_moveto() {
1767 let mp = parse_svg_path_d("M0 0 10 0 20 0").unwrap();
1768 let items_owner = &mp.rings.as_ref()[0];
1769 let items = items_owner.items.as_ref();
1770 assert_eq!(items.len(), 2, "two implicit L commands");
1771 assert_eq!(items[0].get_end(), SvgPoint { x: 10.0, y: 0.0 });
1772 assert_eq!(items[1].get_end(), SvgPoint { x: 20.0, y: 0.0 });
1773 }
1774
1775 #[test]
1776 fn parse_path_garbage_is_err_never_panics() {
1777 for s in ["@#$", "hello", "?", "-", ".", ",", "0 0", "5", ";;;", "\u{0}"] {
1778 assert!(parse_svg_path_d(s).is_err(), "{s:?} must be rejected");
1779 }
1780 }
1781
1782 #[test]
1784 fn parse_path_unicode_does_not_panic() {
1785 assert_eq!(
1786 parse_svg_path_d("\u{1F600}"),
1787 Err(SvgPathParseError::UnexpectedChar {
1788 pos: 0,
1789 ch: '\u{1F600}',
1790 })
1791 );
1792 assert_eq!(
1793 parse_svg_path_d("\u{0301}M0 0"),
1794 Err(SvgPathParseError::UnexpectedChar {
1795 pos: 0,
1796 ch: '\u{0301}',
1797 })
1798 );
1799 assert_eq!(
1802 parse_svg_path_d("M0 0L1 1ü"),
1803 Err(SvgPathParseError::ExpectedNumber { pos: 8 })
1804 );
1805 }
1806
1807 #[test]
1809 fn parse_path_trailing_junk_is_rejected() {
1810 assert!(parse_svg_path_d("M0 0 L1 1;garbage").is_err());
1811 assert!(parse_svg_path_d("M0 0 L").is_err(), "command with no args");
1812 assert!(parse_svg_path_d("M0 0 L1").is_err(), "half a coordinate pair");
1813 assert!(parse_svg_path_d("M0 0 X10 10").is_err(), "unknown command letter");
1814 assert!(parse_svg_path_d(" \n M0 0 L1 1 \t ").is_ok());
1816 }
1817
1818 #[test]
1820 fn parse_path_unknown_command_reports_its_offset() {
1821 assert_eq!(
1822 parse_svg_path_d("M0 0 X10 10"),
1823 Err(SvgPathParseError::UnexpectedChar { pos: 5, ch: 'X' })
1824 );
1825 }
1826
1827 #[test]
1829 fn parse_path_closepath_epsilon_boundary() {
1830 let mp = parse_svg_path_d("M0 0 L1 0 Z").unwrap();
1832 assert_eq!(mp.rings.as_ref()[0].items.as_ref().len(), 2);
1833
1834 let mp = parse_svg_path_d("M0 0 L0.001 0 Z").unwrap();
1837 assert_eq!(mp.rings.as_ref()[0].items.as_ref().len(), 1);
1838
1839 let mp = parse_svg_path_d("M0 0 L0.0005 0 Z").unwrap();
1841 assert_eq!(mp.rings.as_ref()[0].items.as_ref().len(), 1);
1842
1843 assert_eq!(parse_svg_path_d("M0 0 Z").unwrap().rings.as_ref().len(), 0);
1845 }
1846
1847 #[test]
1849 fn parse_path_multiple_subpaths_produce_multiple_rings() {
1850 let mp = parse_svg_path_d("M0 0 L10 0 Z M20 20 L30 20 Z M40 40 L50 40").unwrap();
1851 assert_eq!(mp.rings.as_ref().len(), 3);
1852 }
1853
1854 #[test]
1857 fn parse_path_rings_are_contiguous_chains() {
1858 let d = "M0 0 L10 0 H20 V10 C25 15 30 20 35 20 S45 25 50 20 \
1859 Q55 15 60 20 T70 20 A5 5 0 1 1 80 30 Z \
1860 m100 100 l10 0 z";
1861 let mp = parse_svg_path_d(d).unwrap();
1862 assert!(mp.rings.as_ref().len() >= 2);
1863 for (i, ring) in mp.rings.as_ref().iter().enumerate() {
1864 assert_contiguous(ring.items.as_ref(), &format!("ring {i}"));
1865 assert!(!ring.items.as_ref().is_empty(), "ring {i} must not be empty");
1866 }
1867 }
1868
1869 #[test]
1871 fn parse_path_closed_ring_returns_to_subpath_start() {
1872 let mp = parse_svg_path_d("M0 0 L10 0 L10 10 Z").unwrap();
1873 let ring = &mp.rings.as_ref()[0];
1874 let items = ring.items.as_ref();
1875 assert_eq!(items.last().unwrap().get_end(), SvgPoint { x: 0.0, y: 0.0 });
1876 assert_eq!(items.first().unwrap().get_start(), SvgPoint { x: 0.0, y: 0.0 });
1877 }
1878
1879 #[test]
1882 fn parse_path_boundary_numbers_saturate() {
1883 let mp = parse_svg_path_d("M1e999 -1e999 L1e-999 0").unwrap();
1884 let items_owner = &mp.rings.as_ref()[0];
1885 let start = items_owner.items.as_ref()[0].get_start();
1886 assert!(start.x.is_infinite() && start.x.is_sign_positive());
1887 assert!(start.y.is_infinite() && start.y.is_sign_negative());
1888
1889 let mp = parse_svg_path_d("M3.4e38 0 l3.4e38 0").unwrap();
1891 let items_owner = &mp.rings.as_ref()[0];
1892 assert!(items_owner.items.as_ref()[0].get_end().x.is_infinite());
1893
1894 assert!(parse_svg_path_d("M NaN 0").is_err());
1897 assert!(parse_svg_path_d("M inf 0").is_err());
1898 }
1899
1900 #[test]
1903 fn parse_path_extremely_long_input_terminates() {
1904 let mut d = String::from("M0 0");
1905 for _ in 0..5_000 {
1906 d.push_str(" L1 1");
1907 }
1908 let mp = parse_svg_path_d(&d).unwrap();
1909 let items_owner = &mp.rings.as_ref()[0];
1910 assert_eq!(items_owner.items.as_ref().len(), 5_000);
1911
1912 let mut d = String::new();
1914 for _ in 0..5_000 {
1915 d.push_str("M0 0 L1 1 Z ");
1916 }
1917 assert_eq!(parse_svg_path_d(&d).unwrap().rings.as_ref().len(), 5_000);
1918 }
1919
1920 #[test]
1923 fn parse_path_adversarial_fragments_all_terminate() {
1924 let fragments = [
1925 "Z", "z", "ZZZ", "M0 0ZZ", "M0 0Z0", "M0 0zZ5", "M0 0Z Z Z",
1926 "M", "M0", "M0 0 C", "M0 0 A", "M0 0 A1", "M0 0 A1 1 0 0 0 0",
1927 "M0 0 S", "M0 0 T", "M0 0 H", "M0 0 V", "M0 0 Q1",
1928 "M0 0 L1 1 1", "M0 0 L1 1 1 1 1", "M0 0 A0 0 0 0 0 0 0",
1929 "M0 0 l-.5-.5-.5-.5", "M0 0 t1 1 2 2", "M0 0 s1 1 2 2 3 3 4 4",
1930 "M0,0,1,1", "M0 0e", "M0 0 1e1e1", "M.5.5.5.5",
1931 "M0 0 A1 1 0 11 10 10", "M0 0 A1 1 0 1 1 10 10 A1 1 0 0 0 0 0",
1932 "M0 0 h1e999 v1e999 h-1e999", "M-0-0-0-0",
1933 ];
1934 for f in fragments {
1935 let r = parse_svg_path_d(f);
1938 assert!(r.is_ok() || r.is_err(), "{f:?} must return, not panic");
1939 }
1940 }
1941
1942 #[test]
1945 fn parse_path_every_command_produces_its_element_kind() {
1946 let cases: [(&str, usize); 10] = [
1947 ("M0 0 L1 1", 1),
1948 ("M0 0 l1 1", 1),
1949 ("M0 0 H1", 1),
1950 ("M0 0 V1", 1),
1951 ("M0 0 C1 1 2 2 3 3", 1),
1952 ("M0 0 S1 1 2 2", 1),
1953 ("M0 0 Q1 1 2 2", 1),
1954 ("M0 0 T1 1", 1),
1955 ("M0 0 L1 0 Z", 2), ("M0 0 A5 5 0 0 1 10 0", 2), ];
1958 for (d, expected) in cases {
1959 let mp = parse_svg_path_d(d).unwrap_or_else(|e| panic!("{d:?} failed: {e:?}"));
1960 let rings = mp.rings.as_ref();
1961 assert_eq!(rings.len(), 1, "{d:?}");
1962 assert_eq!(rings[0].items.as_ref().len(), expected, "{d:?}");
1963 }
1964 }
1965
1966 #[test]
1969 fn arc_to_cubics_coincident_endpoints_emit_nothing() {
1970 let mut out = Vec::new();
1971 arc_to_cubics(ORIGIN, ORIGIN, 5.0, 5.0, 0.0, true, true, &mut out);
1972 assert!(out.is_empty(), "a zero-length arc is dropped per SVG F.6.2");
1973
1974 let near = SvgPoint { x: 1e-9, y: 1e-9 };
1976 arc_to_cubics(ORIGIN, near, 5.0, 5.0, 0.0, false, false, &mut out);
1977 assert!(out.is_empty());
1978 }
1979
1980 #[test]
1981 fn arc_to_cubics_zero_radius_emits_a_line() {
1982 let end = SvgPoint { x: 10.0, y: 10.0 };
1983 for (rx, ry) in [(0.0, 5.0), (5.0, 0.0), (0.0, 0.0)] {
1984 let mut out = Vec::new();
1985 arc_to_cubics(ORIGIN, end, rx, ry, 0.0, false, true, &mut out);
1986 assert_eq!(out.len(), 1, "rx={rx} ry={ry}");
1987 assert!(matches!(out[0], SvgPathElement::Line(_)));
1988 assert_eq!(out[0].get_start(), ORIGIN);
1989 assert_eq!(out[0].get_end(), end);
1990 }
1991 }
1992
1993 #[test]
1996 fn arc_to_cubics_endpoints_are_exact_for_all_flag_combos() {
1997 let start = SvgPoint { x: 0.0, y: 0.0 };
1998 let end = SvgPoint { x: 10.0, y: 10.0 };
1999 for large_arc in [false, true] {
2000 for sweep in [false, true] {
2001 let mut out = Vec::new();
2002 arc_to_cubics(start, end, 8.0, 6.0, 30.0, large_arc, sweep, &mut out);
2003 assert!(
2004 (1..=4).contains(&out.len()),
2005 "large_arc={large_arc} sweep={sweep}: got {} cubics",
2006 out.len()
2007 );
2008 assert_eq!(out[0].get_start(), start);
2009 assert_eq!(out.last().unwrap().get_end(), end);
2010 assert_contiguous(&out, "arc");
2011 for p in out.iter().flat_map(|e| [e.get_start(), e.get_end()]) {
2012 assert!(p.x.is_finite() && p.y.is_finite(), "arc produced {p:?}");
2013 }
2014 }
2015 }
2016 }
2017
2018 #[test]
2021 fn arc_to_cubics_undersized_radii_are_scaled_up() {
2022 let start = ORIGIN;
2023 let end = SvgPoint { x: 100.0, y: 0.0 };
2024 let mut out = Vec::new();
2025 arc_to_cubics(start, end, 1.0, 1.0, 0.0, false, true, &mut out);
2026 assert!(!out.is_empty());
2027 assert_eq!(out.last().unwrap().get_end(), end);
2028 for p in all_points(&SvgPath {
2029 items: SvgPathElementVec::from_vec(out),
2030 }) {
2031 assert!(p.x.is_finite() && p.y.is_finite(), "scaled arc produced {p:?}");
2032 }
2033 }
2034
2035 #[test]
2038 fn arc_to_cubics_nan_and_inf_inputs_are_bounded() {
2039 let end = SvgPoint { x: 10.0, y: 10.0 };
2040 let bad = [
2041 (f32::NAN, 5.0, 0.0),
2042 (5.0, f32::NAN, 0.0),
2043 (f32::INFINITY, f32::INFINITY, 0.0),
2044 (5.0, 5.0, f32::NAN),
2045 (5.0, 5.0, f32::INFINITY),
2046 (f32::MAX, f32::MAX, 360.0),
2047 ];
2048 for (rx, ry, rot) in bad {
2049 let mut out = Vec::new();
2050 arc_to_cubics(ORIGIN, end, rx, ry, rot, true, false, &mut out);
2051 assert!(
2052 out.len() <= 4,
2053 "rx={rx} ry={ry} rot={rot}: {} elements (segment loop ran away)",
2054 out.len()
2055 );
2056 }
2057 }
2058
2059 #[test]
2061 fn arc_to_cubics_extreme_endpoints_do_not_panic() {
2062 let mut out = Vec::new();
2063 arc_to_cubics(
2064 SvgPoint { x: f32::MIN, y: f32::MIN },
2065 SvgPoint { x: f32::MAX, y: f32::MAX },
2066 f32::MAX,
2067 f32::MAX,
2068 0.0,
2069 true,
2070 true,
2071 &mut out,
2072 );
2073 assert!(out.len() <= 4);
2074 }
2075
2076 #[test]
2079 fn angle_between_known_angles() {
2080 assert!(approx(angle_between(1.0, 0.0, 1.0, 0.0), 0.0));
2081 assert!(approx(
2082 angle_between(1.0, 0.0, 0.0, 1.0),
2083 core::f32::consts::FRAC_PI_2
2084 ));
2085 assert!(approx(
2086 angle_between(1.0, 0.0, 0.0, -1.0),
2087 -core::f32::consts::FRAC_PI_2
2088 ));
2089 assert!(approx(
2090 angle_between(1.0, 0.0, -1.0, 0.0),
2091 core::f32::consts::PI
2092 ));
2093 assert!(approx(
2095 angle_between(100.0, 0.0, 0.0, 0.001),
2096 core::f32::consts::FRAC_PI_2
2097 ));
2098 }
2099
2100 #[test]
2102 fn angle_between_zero_length_vectors_return_zero() {
2103 assert_eq!(angle_between(0.0, 0.0, 1.0, 0.0), 0.0);
2104 assert_eq!(angle_between(1.0, 0.0, 0.0, 0.0), 0.0);
2105 assert_eq!(angle_between(0.0, 0.0, 0.0, 0.0), 0.0);
2106 assert_eq!(angle_between(1e-30, 1e-30, 1e-30, 1e-30), 0.0);
2108 }
2109
2110 #[test]
2113 fn angle_between_is_always_within_pi_for_finite_inputs() {
2114 let vals = [-1e30_f32, -3.0, -1.0, -0.0, 0.0, 1.0, 3.0, 1e30];
2115 for ux in vals {
2116 for uy in vals {
2117 for vx in vals {
2118 for vy in vals {
2119 let a = angle_between(ux, uy, vx, vy);
2120 assert!(
2121 a.is_nan() || a.abs() <= core::f32::consts::PI + 1e-5,
2122 "angle_between({ux},{uy},{vx},{vy}) = {a} is out of range"
2123 );
2124 }
2125 }
2126 }
2127 }
2128 }
2129
2130 #[test]
2133 fn angle_between_clamps_the_acos_domain() {
2134 let a = angle_between(0.1, 0.2, 0.1, 0.2);
2135 assert!(!a.is_nan(), "parallel vectors must not produce NaN, got {a}");
2136 assert!(approx(a, 0.0));
2137 let a = angle_between(0.1, 0.2, -0.1, -0.2);
2138 assert!(!a.is_nan());
2139 assert!(approx(a.abs(), core::f32::consts::PI));
2140 }
2141
2142 #[test]
2144 fn angle_between_nan_and_inf_do_not_panic() {
2145 assert!(angle_between(f32::NAN, 0.0, 1.0, 0.0).is_nan());
2146 assert!(angle_between(1.0, 0.0, f32::NAN, f32::NAN).is_nan());
2147 assert!(angle_between(f32::INFINITY, 0.0, f32::INFINITY, 0.0).is_nan());
2148 assert!(angle_between(f32::INFINITY, 0.0, 1.0, 0.0).is_nan());
2149 }
2150
2151 #[test]
2154 fn arc_segment_to_cubic_quarter_circle() {
2155 let (c1, c2, ep) = arc_segment_to_cubic(
2157 0.0,
2158 0.0,
2159 1.0,
2160 1.0,
2161 1.0,
2162 0.0,
2163 0.0,
2164 core::f32::consts::FRAC_PI_2,
2165 );
2166 assert!(approx(ep.x, 0.0) && approx(ep.y, 1.0), "ep = {ep:?}");
2167 assert!(approx(c1.x, 1.0) && approx(c1.y, KAPPA), "c1 = {c1:?}");
2169 assert!(approx(c2.x, KAPPA) && approx(c2.y, 1.0), "c2 = {c2:?}");
2170 }
2171
2172 #[test]
2174 fn arc_segment_to_cubic_zero_sweep_collapses() {
2175 let (c1, c2, ep) = arc_segment_to_cubic(5.0, 5.0, 2.0, 2.0, 1.0, 0.0, 0.7, 0.7);
2176 assert!(approx(c1.x, c2.x) && approx(c1.y, c2.y));
2177 assert!(approx(c2.x, ep.x) && approx(c2.y, ep.y));
2178 assert!(approx((ep.x - 5.0).hypot(ep.y - 5.0), 2.0));
2180 }
2181
2182 #[test]
2184 fn arc_segment_to_cubic_zero_radius_is_the_center() {
2185 let (c1, c2, ep) = arc_segment_to_cubic(3.0, 4.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0);
2186 for p in [c1, c2, ep] {
2187 assert_eq!(p, SvgPoint { x: 3.0, y: 4.0 });
2188 }
2189 }
2190
2191 #[test]
2193 fn arc_segment_to_cubic_applies_rotation() {
2194 let (_, _, ep) = arc_segment_to_cubic(0.0, 0.0, 2.0, 1.0, 0.0, 1.0, 0.0, 0.0);
2196 assert!(approx(ep.x, 0.0) && approx(ep.y, 2.0), "ep = {ep:?}");
2197 }
2198
2199 #[test]
2200 fn arc_segment_to_cubic_nan_and_inf_do_not_panic() {
2201 let bad = [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN];
2202 for v in bad {
2203 let (c1, c2, ep) = arc_segment_to_cubic(v, v, v, v, v, v, v, v);
2204 for p in [c1, c2, ep] {
2206 assert!(p.x.is_nan() || p.x.is_finite() || p.x.is_infinite());
2207 assert!(p.y.is_nan() || p.y.is_finite() || p.y.is_infinite());
2208 }
2209 }
2210 let (c1, _, _) = arc_segment_to_cubic(
2212 0.0,
2213 0.0,
2214 1.0,
2215 1.0,
2216 1.0,
2217 0.0,
2218 0.0,
2219 core::f32::consts::TAU,
2220 );
2221 assert!(!c1.x.is_nan() || c1.x.is_nan(), "must not panic");
2222 }
2223
2224 #[test]
2227 fn circle_has_four_cubics_and_closes_on_itself() {
2228 let p = svg_circle_to_paths(10.0, 20.0, 5.0);
2229 let items = p.items.as_ref();
2230 assert_eq!(items.len(), 4);
2231 assert!(items.iter().all(|e| matches!(e, SvgPathElement::CubicCurve(_))));
2232 assert_contiguous(items, "circle");
2233 assert_eq!(
2234 items.last().unwrap().get_end(),
2235 items.first().unwrap().get_start(),
2236 "the circle must close exactly"
2237 );
2238 assert_eq!(items[0].get_start(), SvgPoint { x: 10.0, y: 15.0 });
2240 assert_eq!(items[0].get_end(), SvgPoint { x: 15.0, y: 20.0 });
2241 assert_eq!(items[1].get_end(), SvgPoint { x: 10.0, y: 25.0 });
2242 assert_eq!(items[2].get_end(), SvgPoint { x: 5.0, y: 20.0 });
2243 }
2244
2245 #[test]
2247 fn circle_zero_radius_collapses_to_the_center() {
2248 let p = svg_circle_to_paths(3.0, 4.0, 0.0);
2249 let items = p.items.as_ref();
2250 assert_eq!(items.len(), 4);
2251 for pt in all_points(&p) {
2252 assert_eq!(pt, SvgPoint { x: 3.0, y: 4.0 });
2253 }
2254 }
2255
2256 #[test]
2259 fn circle_negative_radius_is_mirrored_not_rejected() {
2260 let p = svg_circle_to_paths(0.0, 0.0, -5.0);
2261 let items = p.items.as_ref();
2262 assert_eq!(items.len(), 4);
2263 assert_contiguous(items, "negative-r circle");
2264 assert_eq!(items[0].get_start(), SvgPoint { x: 0.0, y: 5.0 });
2265 for pt in all_points(&p) {
2266 assert!(pt.x.is_finite() && pt.y.is_finite());
2267 }
2268 }
2269
2270 #[test]
2271 fn circle_nan_inf_and_max_do_not_panic() {
2272 for (cx, cy, r) in [
2273 (f32::NAN, 0.0, 1.0),
2274 (0.0, 0.0, f32::NAN),
2275 (0.0, 0.0, f32::INFINITY),
2276 (f32::MAX, f32::MAX, f32::MAX),
2277 (f32::MIN, f32::MIN, f32::MIN),
2278 ] {
2279 let p = svg_circle_to_paths(cx, cy, r);
2280 assert_eq!(p.items.as_ref().len(), 4, "cx={cx} cy={cy} r={r}");
2281 }
2282 let p = svg_circle_to_paths(f32::MAX, 0.0, f32::MAX);
2285 assert!(all_points(&p).iter().any(|pt| pt.x.is_infinite()));
2286 }
2287
2288 #[test]
2291 fn rect_sharp_corners_are_four_lines() {
2292 let p = svg_rect_to_path(1.0, 2.0, 10.0, 20.0, 0.0, 0.0);
2293 let items = p.items.as_ref();
2294 assert_eq!(items.len(), 4);
2295 assert!(items.iter().all(|e| matches!(e, SvgPathElement::Line(_))));
2296 assert_contiguous(items, "sharp rect");
2297 assert_eq!(items[0].get_start(), SvgPoint { x: 1.0, y: 2.0 });
2298 assert_eq!(items[1].get_start(), SvgPoint { x: 11.0, y: 2.0 });
2299 assert_eq!(items[2].get_start(), SvgPoint { x: 11.0, y: 22.0 });
2300 assert_eq!(items[3].get_start(), SvgPoint { x: 1.0, y: 22.0 });
2301 assert_eq!(
2302 items.last().unwrap().get_end(),
2303 items.first().unwrap().get_start(),
2304 "the rect must close exactly"
2305 );
2306 }
2307
2308 #[test]
2309 fn rect_rounded_is_eight_alternating_segments_and_closes() {
2310 let p = svg_rect_to_path(0.0, 0.0, 100.0, 50.0, 10.0, 5.0);
2311 let items = p.items.as_ref();
2312 assert_eq!(items.len(), 8);
2313 for (i, e) in items.iter().enumerate() {
2314 if i % 2 == 0 {
2315 assert!(matches!(e, SvgPathElement::Line(_)), "item {i} should be an edge");
2316 } else {
2317 assert!(
2318 matches!(e, SvgPathElement::CubicCurve(_)),
2319 "item {i} should be a corner"
2320 );
2321 }
2322 }
2323 assert_contiguous(items, "rounded rect");
2324 assert_eq!(
2325 items.last().unwrap().get_end(),
2326 items.first().unwrap().get_start(),
2327 "the rounded rect must close exactly"
2328 );
2329 }
2330
2331 #[test]
2334 fn rect_oversized_radii_are_clamped_to_half() {
2335 let p = svg_rect_to_path(0.0, 0.0, 10.0, 10.0, 1000.0, 1000.0);
2336 let items = p.items.as_ref();
2337 assert_eq!(items.len(), 8);
2338 match items[0] {
2340 SvgPathElement::Line(l) => {
2341 assert_eq!(l.start, SvgPoint { x: 5.0, y: 0.0 });
2342 assert_eq!(l.end, SvgPoint { x: 5.0, y: 0.0 });
2343 }
2344 other => panic!("expected Line, got {other:?}"),
2345 }
2346 assert_contiguous(items, "clamped rect");
2347 for pt in all_points(&p) {
2348 assert!(pt.x.is_finite() && pt.y.is_finite());
2349 assert!((0.0..=10.0).contains(&pt.x), "x {} escaped the rect", pt.x);
2350 assert!((0.0..=10.0).contains(&pt.y), "y {} escaped the rect", pt.y);
2351 }
2352 }
2353
2354 #[test]
2356 fn rect_single_zero_radius_still_rounds() {
2357 let p = svg_rect_to_path(0.0, 0.0, 100.0, 100.0, 0.0, 10.0);
2358 assert_eq!(p.items.as_ref().len(), 8);
2359 let p = svg_rect_to_path(0.0, 0.0, 100.0, 100.0, 10.0, 0.0);
2360 assert_eq!(p.items.as_ref().len(), 8);
2361 }
2362
2363 #[test]
2366 fn rect_negative_extent_takes_the_sharp_branch() {
2367 let p = svg_rect_to_path(0.0, 0.0, -10.0, -10.0, 4.0, 4.0);
2368 let items = p.items.as_ref();
2369 assert_eq!(items.len(), 4);
2370 assert!(items.iter().all(|e| matches!(e, SvgPathElement::Line(_))));
2371 assert_contiguous(items, "negative rect");
2372 assert_eq!(items[1].get_start(), SvgPoint { x: -10.0, y: 0.0 });
2373 }
2374
2375 #[test]
2378 fn rect_nan_radius_falls_back_to_half_extent() {
2379 let p = svg_rect_to_path(0.0, 0.0, 100.0, 100.0, f32::NAN, f32::NAN);
2380 let items = p.items.as_ref();
2381 assert_eq!(items.len(), 8, "NaN radii clamp to w/2, h/2 -> rounded path");
2382 for pt in all_points(&p) {
2383 assert!(pt.x.is_finite() && pt.y.is_finite(), "NaN leaked into {pt:?}");
2384 }
2385 match items[0] {
2387 SvgPathElement::Line(l) => assert_eq!(l.start, l.end),
2388 other => panic!("expected Line, got {other:?}"),
2389 }
2390 }
2391
2392 #[test]
2395 fn rect_nan_extent_is_deterministic() {
2396 let p = svg_rect_to_path(0.0, 0.0, f32::NAN, f32::NAN, 0.0, 0.0);
2397 assert_eq!(p.items.as_ref().len(), 4);
2399 let p = svg_rect_to_path(f32::NAN, f32::NAN, 10.0, 10.0, 0.0, 0.0);
2400 assert_eq!(p.items.as_ref().len(), 4);
2401 }
2402
2403 #[test]
2404 fn rect_inf_and_max_extents_do_not_panic() {
2405 for (x, y, w, h, rx, ry) in [
2406 (0.0, 0.0, f32::INFINITY, f32::INFINITY, 0.0, 0.0),
2407 (0.0, 0.0, f32::MAX, f32::MAX, 0.0, 0.0),
2408 (f32::MIN, f32::MIN, f32::MAX, f32::MAX, f32::MAX, f32::MAX),
2409 (0.0, 0.0, f32::INFINITY, f32::INFINITY, f32::INFINITY, f32::INFINITY),
2410 ] {
2411 let p = svg_rect_to_path(x, y, w, h, rx, ry);
2412 let n = p.items.as_ref().len();
2413 assert!(n == 4 || n == 8, "w={w} h={h} rx={rx} ry={ry}: got {n} items");
2414 }
2415 }
2416}