1use thiserror::Error;
16
17use crate::geometry::{Point, Rect};
18
19const MAX_FLATTEN_DEPTH: u32 = 12;
21const FLATTEN_TOLERANCE: f32 = 0.05;
23const ARC_MAX_ANGLE_STEP: f32 = std::f32::consts::PI / 16.0;
25const SUBSAMPLES: usize = 4;
27
28#[derive(Debug, Clone, PartialEq, Eq, Error)]
30pub enum SvgPathError {
31 #[error("unexpected byte {byte:?} at offset {offset}")]
32 UnexpectedByte { byte: char, offset: usize },
33 #[error("expected a number at offset {offset}")]
34 ExpectedNumber { offset: usize },
35 #[error("expected an arc flag (0 or 1) at offset {offset}")]
36 ExpectedFlag { offset: usize },
37 #[error("path data must start with a moveto (M/m) command")]
38 MissingMoveTo,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum PathFillRule {
44 #[default]
46 NonZero,
47 EvenOdd,
49}
50
51#[derive(Debug, Clone)]
53pub struct VectorPath {
54 subpaths: Vec<Vec<Point>>,
55 fill_rule: PathFillRule,
56 bounds: Rect,
57}
58
59impl VectorPath {
60 pub fn parse(d: &str) -> Result<Self, SvgPathError> {
62 let subpaths = parse_path_data(d)?;
63 Ok(Self::from_subpaths(subpaths, PathFillRule::NonZero))
64 }
65
66 pub fn parse_with_fill_rule(d: &str, fill_rule: PathFillRule) -> Result<Self, SvgPathError> {
68 let subpaths = parse_path_data(d)?;
69 Ok(Self::from_subpaths(subpaths, fill_rule))
70 }
71
72 fn from_subpaths(subpaths: Vec<Vec<Point>>, fill_rule: PathFillRule) -> Self {
73 let mut min = Point::new(f32::INFINITY, f32::INFINITY);
74 let mut max = Point::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
75 for point in subpaths.iter().flatten() {
76 min.x = min.x.min(point.x);
77 min.y = min.y.min(point.y);
78 max.x = max.x.max(point.x);
79 max.y = max.y.max(point.y);
80 }
81 let bounds = if min.x.is_finite() {
82 Rect {
83 x: min.x,
84 y: min.y,
85 width: (max.x - min.x).max(0.0),
86 height: (max.y - min.y).max(0.0),
87 }
88 } else {
89 Rect {
90 x: 0.0,
91 y: 0.0,
92 width: 0.0,
93 height: 0.0,
94 }
95 };
96 Self {
97 subpaths,
98 fill_rule,
99 bounds,
100 }
101 }
102
103 pub fn scaled(&self, factor: f32) -> Self {
106 let subpaths = self
107 .subpaths
108 .iter()
109 .map(|subpath| {
110 subpath
111 .iter()
112 .map(|point| Point::new(point.x * factor, point.y * factor))
113 .collect()
114 })
115 .collect();
116 Self::from_subpaths(subpaths, self.fill_rule)
117 }
118
119 pub fn translated(&self, dx: f32, dy: f32) -> Self {
121 let subpaths = self
122 .subpaths
123 .iter()
124 .map(|subpath| {
125 subpath
126 .iter()
127 .map(|point| Point::new(point.x + dx, point.y + dy))
128 .collect()
129 })
130 .collect();
131 Self::from_subpaths(subpaths, self.fill_rule)
132 }
133
134 pub fn fill_rule(&self) -> PathFillRule {
136 self.fill_rule
137 }
138
139 pub fn bounds(&self) -> Rect {
141 self.bounds
142 }
143
144 pub fn is_empty(&self) -> bool {
146 !self.subpaths.iter().any(|subpath| subpath.len() >= 3)
147 }
148
149 pub fn subpaths(&self) -> &[Vec<Point>] {
151 &self.subpaths
152 }
153
154 pub fn coverage_mask(&self, width: usize, height: usize, origin: Point, scale: f32) -> Vec<u8> {
158 let mut mask = vec![0u8; width * height];
159 if width == 0 || height == 0 || scale <= 0.0 {
160 return mask;
161 }
162
163 struct Edge {
164 top: Point,
165 bottom: Point,
166 winding: i32,
167 }
168 let mut edges = Vec::new();
169 for subpath in &self.subpaths {
170 if subpath.len() < 3 {
171 continue;
172 }
173 let map = |p: &Point| Point::new((p.x - origin.x) * scale, (p.y - origin.y) * scale);
174 for i in 0..subpath.len() {
175 let a = map(&subpath[i]);
176 let b = map(&subpath[(i + 1) % subpath.len()]);
177 if a.y == b.y {
178 continue;
179 }
180 if a.y < b.y {
181 edges.push(Edge {
182 top: a,
183 bottom: b,
184 winding: 1,
185 });
186 } else {
187 edges.push(Edge {
188 top: b,
189 bottom: a,
190 winding: -1,
191 });
192 }
193 }
194 }
195 if edges.is_empty() {
196 return mask;
197 }
198
199 let mut crossings: Vec<(f32, i32)> = Vec::new();
200 let mut row_coverage = vec![0.0f32; width];
201 let subsample_weight = 1.0 / SUBSAMPLES as f32;
202
203 for row in 0..height {
204 row_coverage.fill(0.0);
205 let mut row_touched = false;
206
207 for sub in 0..SUBSAMPLES {
208 let sample_y = row as f32 + (sub as f32 + 0.5) * subsample_weight;
209
210 crossings.clear();
211 for edge in &edges {
212 if edge.top.y <= sample_y && sample_y < edge.bottom.y {
213 let t = (sample_y - edge.top.y) / (edge.bottom.y - edge.top.y);
214 let x = edge.top.x + t * (edge.bottom.x - edge.top.x);
215 crossings.push((x, edge.winding));
216 }
217 }
218 if crossings.len() < 2 {
219 continue;
220 }
221 crossings.sort_by(|a, b| a.0.total_cmp(&b.0));
222
223 let mut winding = 0i32;
224 let mut span_start = 0.0f32;
225 for &(x, direction) in crossings.iter() {
226 let was_inside = match self.fill_rule {
227 PathFillRule::NonZero => winding != 0,
228 PathFillRule::EvenOdd => winding % 2 != 0,
229 };
230 winding += match self.fill_rule {
231 PathFillRule::NonZero => direction,
232 PathFillRule::EvenOdd => 1,
233 };
234 let is_inside = match self.fill_rule {
235 PathFillRule::NonZero => winding != 0,
236 PathFillRule::EvenOdd => winding % 2 != 0,
237 };
238 if !was_inside && is_inside {
239 span_start = x;
240 } else if was_inside && !is_inside {
241 row_touched |= accumulate_span(
242 &mut row_coverage,
243 span_start,
244 x,
245 subsample_weight,
246 width,
247 );
248 }
249 }
250 }
251
252 if row_touched {
253 let mask_row = &mut mask[row * width..(row + 1) * width];
254 for (dst, coverage) in mask_row.iter_mut().zip(row_coverage.iter()) {
255 let existing = *dst as f32 / 255.0;
256 let combined = (existing + coverage).min(1.0);
257 *dst = (combined * 255.0 + 0.5) as u8;
258 }
259 }
260 }
261
262 mask
263 }
264}
265
266fn accumulate_span(row_coverage: &mut [f32], x0: f32, x1: f32, weight: f32, width: usize) -> bool {
270 let x0 = x0.max(0.0);
271 let x1 = x1.min(width as f32);
272 if x1 <= x0 {
273 return false;
274 }
275
276 let first = x0.floor() as usize;
277 let last = (x1.ceil() as usize).min(width);
278 for (pixel, coverage) in row_coverage.iter_mut().enumerate().take(last).skip(first) {
279 let pixel_start = pixel as f32;
280 let pixel_end = pixel_start + 1.0;
281 let covered = (x1.min(pixel_end) - x0.max(pixel_start)).max(0.0);
282 *coverage += covered * weight;
283 }
284 true
285}
286
287struct PathLexer<'a> {
288 bytes: &'a [u8],
289 pos: usize,
290}
291
292impl<'a> PathLexer<'a> {
293 fn new(d: &'a str) -> Self {
294 Self {
295 bytes: d.as_bytes(),
296 pos: 0,
297 }
298 }
299
300 fn skip_separators(&mut self) {
301 while self.pos < self.bytes.len() {
302 match self.bytes[self.pos] {
303 b' ' | b'\t' | b'\r' | b'\n' | b',' => self.pos += 1,
304 _ => break,
305 }
306 }
307 }
308
309 fn peek(&mut self) -> Option<u8> {
310 self.skip_separators();
311 self.bytes.get(self.pos).copied()
312 }
313
314 fn at_number(&mut self) -> bool {
315 matches!(self.peek(), Some(b'0'..=b'9' | b'.' | b'-' | b'+'))
316 }
317
318 fn next_command(&mut self) -> Option<u8> {
319 let byte = self.peek()?;
320 if byte.is_ascii_alphabetic() {
321 self.pos += 1;
322 Some(byte)
323 } else {
324 None
325 }
326 }
327
328 fn next_number(&mut self) -> Result<f32, SvgPathError> {
329 self.skip_separators();
330 let start = self.pos;
331 let bytes = self.bytes;
332 let mut pos = self.pos;
333
334 if pos < bytes.len() && (bytes[pos] == b'+' || bytes[pos] == b'-') {
335 pos += 1;
336 }
337 let int_digits = Self::eat_digits(bytes, &mut pos);
338 let mut frac_digits = 0;
339 if pos < bytes.len() && bytes[pos] == b'.' {
340 pos += 1;
341 frac_digits = Self::eat_digits(bytes, &mut pos);
342 }
343 if int_digits == 0 && frac_digits == 0 {
344 return Err(SvgPathError::ExpectedNumber { offset: start });
345 }
346 if pos < bytes.len() && (bytes[pos] == b'e' || bytes[pos] == b'E') {
347 let mut exp_pos = pos + 1;
348 if exp_pos < bytes.len() && (bytes[exp_pos] == b'+' || bytes[exp_pos] == b'-') {
349 exp_pos += 1;
350 }
351 if Self::eat_digits(bytes, &mut exp_pos) > 0 {
352 pos = exp_pos;
353 }
354 }
355
356 let text = std::str::from_utf8(&bytes[start..pos])
357 .map_err(|_| SvgPathError::ExpectedNumber { offset: start })?;
358 let value = text
359 .parse::<f32>()
360 .map_err(|_| SvgPathError::ExpectedNumber { offset: start })?;
361 self.pos = pos;
362 Ok(value)
363 }
364
365 fn eat_digits(bytes: &[u8], pos: &mut usize) -> usize {
366 let start = *pos;
367 while *pos < bytes.len() && bytes[*pos].is_ascii_digit() {
368 *pos += 1;
369 }
370 *pos - start
371 }
372
373 fn next_flag(&mut self) -> Result<bool, SvgPathError> {
374 self.skip_separators();
375 match self.bytes.get(self.pos) {
376 Some(b'0') => {
377 self.pos += 1;
378 Ok(false)
379 }
380 Some(b'1') => {
381 self.pos += 1;
382 Ok(true)
383 }
384 _ => Err(SvgPathError::ExpectedFlag { offset: self.pos }),
385 }
386 }
387
388 fn at_end(&mut self) -> bool {
389 self.peek().is_none()
390 }
391}
392
393struct PathBuilder {
394 subpaths: Vec<Vec<Point>>,
395 current: Vec<Point>,
396 position: Point,
397 subpath_start: Point,
398 last_cubic_control: Option<Point>,
399 last_quad_control: Option<Point>,
400}
401
402impl PathBuilder {
403 fn new() -> Self {
404 Self {
405 subpaths: Vec::new(),
406 current: Vec::new(),
407 position: Point::ZERO,
408 subpath_start: Point::ZERO,
409 last_cubic_control: None,
410 last_quad_control: None,
411 }
412 }
413
414 fn flush_subpath(&mut self) {
415 if self.current.len() >= 2 {
416 self.subpaths.push(std::mem::take(&mut self.current));
417 } else {
418 self.current.clear();
419 }
420 }
421
422 fn move_to(&mut self, point: Point) {
423 self.flush_subpath();
424 self.position = point;
425 self.subpath_start = point;
426 self.current.push(point);
427 }
428
429 fn line_to(&mut self, point: Point) {
430 if self.current.is_empty() {
431 self.current.push(self.position);
432 }
433 self.current.push(point);
434 self.position = point;
435 }
436
437 fn close(&mut self) {
438 self.position = self.subpath_start;
439 self.flush_subpath();
440 self.current.push(self.subpath_start);
441 }
442
443 fn finish(mut self) -> Vec<Vec<Point>> {
444 self.flush_subpath();
445 self.subpaths
446 }
447}
448
449fn parse_path_data(d: &str) -> Result<Vec<Vec<Point>>, SvgPathError> {
450 let mut lexer = PathLexer::new(d);
451 let mut builder = PathBuilder::new();
452 let mut command: Option<u8> = None;
453 let mut seen_moveto = false;
454
455 loop {
456 if lexer.at_end() {
457 break;
458 }
459
460 if let Some(next) = lexer.next_command() {
461 command = Some(next);
462 } else if command.is_none() || !lexer.at_number() {
463 let offset = lexer.pos;
464 let byte = lexer.bytes.get(offset).copied().unwrap_or(b'?') as char;
465 return Err(SvgPathError::UnexpectedByte { byte, offset });
466 }
467
468 let Some(cmd) = command else {
469 return Err(SvgPathError::MissingMoveTo);
470 };
471 if !seen_moveto && !matches!(cmd, b'M' | b'm') {
472 return Err(SvgPathError::MissingMoveTo);
473 }
474 let relative = cmd.is_ascii_lowercase();
475 let pos = builder.position;
476 let rel = |value: Point| {
477 if relative {
478 Point::new(pos.x + value.x, pos.y + value.y)
479 } else {
480 value
481 }
482 };
483
484 match cmd.to_ascii_uppercase() {
485 b'M' => {
486 let point = rel(read_point(&mut lexer)?);
487 builder.move_to(point);
488 seen_moveto = true;
489 builder.last_cubic_control = None;
490 builder.last_quad_control = None;
491 command = Some(if relative { b'l' } else { b'L' });
492 }
493 b'L' => {
494 let point = rel(read_point(&mut lexer)?);
495 builder.line_to(point);
496 builder.last_cubic_control = None;
497 builder.last_quad_control = None;
498 }
499 b'H' => {
500 let x = lexer.next_number()?;
501 let x = if relative { pos.x + x } else { x };
502 builder.line_to(Point::new(x, pos.y));
503 builder.last_cubic_control = None;
504 builder.last_quad_control = None;
505 }
506 b'V' => {
507 let y = lexer.next_number()?;
508 let y = if relative { pos.y + y } else { y };
509 builder.line_to(Point::new(pos.x, y));
510 builder.last_cubic_control = None;
511 builder.last_quad_control = None;
512 }
513 b'C' => {
514 let c1 = rel(read_point(&mut lexer)?);
515 let c2 = rel(read_point(&mut lexer)?);
516 let end = rel(read_point(&mut lexer)?);
517 emit_cubic(&mut builder, c1, c2, end);
518 }
519 b'S' => {
520 let c1 = match builder.last_cubic_control {
521 Some(control) => reflect(pos, control),
522 None => pos,
523 };
524 let c2 = rel(read_point(&mut lexer)?);
525 let end = rel(read_point(&mut lexer)?);
526 emit_cubic(&mut builder, c1, c2, end);
527 }
528 b'Q' => {
529 let control = rel(read_point(&mut lexer)?);
530 let end = rel(read_point(&mut lexer)?);
531 emit_quad(&mut builder, control, end);
532 }
533 b'T' => {
534 let control = match builder.last_quad_control {
535 Some(control) => reflect(pos, control),
536 None => pos,
537 };
538 let end = rel(read_point(&mut lexer)?);
539 emit_quad(&mut builder, control, end);
540 }
541 b'A' => {
542 let rx = lexer.next_number()?;
543 let ry = lexer.next_number()?;
544 let x_rotation_deg = lexer.next_number()?;
545 let large_arc = lexer.next_flag()?;
546 let sweep = lexer.next_flag()?;
547 let end = rel(read_point(&mut lexer)?);
548 emit_arc(&mut builder, rx, ry, x_rotation_deg, large_arc, sweep, end);
549 builder.last_cubic_control = None;
550 builder.last_quad_control = None;
551 }
552 b'Z' => {
553 builder.close();
554 builder.last_cubic_control = None;
555 builder.last_quad_control = None;
556 command = None;
557 }
558 other => {
559 return Err(SvgPathError::UnexpectedByte {
560 byte: other as char,
561 offset: lexer.pos.saturating_sub(1),
562 });
563 }
564 }
565 }
566
567 if !seen_moveto {
568 return Err(SvgPathError::MissingMoveTo);
569 }
570 Ok(builder.finish())
571}
572
573fn read_point(lexer: &mut PathLexer<'_>) -> Result<Point, SvgPathError> {
574 let x = lexer.next_number()?;
575 let y = lexer.next_number()?;
576 Ok(Point::new(x, y))
577}
578
579fn reflect(origin: Point, point: Point) -> Point {
580 Point::new(2.0 * origin.x - point.x, 2.0 * origin.y - point.y)
581}
582
583fn emit_cubic(builder: &mut PathBuilder, c1: Point, c2: Point, end: Point) {
584 let start = builder.position;
585 flatten_cubic(builder, start, c1, c2, end, 0);
586 builder.position = end;
587 builder.last_cubic_control = Some(c2);
588 builder.last_quad_control = None;
589}
590
591fn emit_quad(builder: &mut PathBuilder, control: Point, end: Point) {
592 let start = builder.position;
593 let c1 = Point::new(
594 start.x + 2.0 / 3.0 * (control.x - start.x),
595 start.y + 2.0 / 3.0 * (control.y - start.y),
596 );
597 let c2 = Point::new(
598 end.x + 2.0 / 3.0 * (control.x - end.x),
599 end.y + 2.0 / 3.0 * (control.y - end.y),
600 );
601 flatten_cubic(builder, start, c1, c2, end, 0);
602 builder.position = end;
603 builder.last_quad_control = Some(control);
604 builder.last_cubic_control = None;
605}
606
607fn flatten_cubic(
608 builder: &mut PathBuilder,
609 p0: Point,
610 p1: Point,
611 p2: Point,
612 p3: Point,
613 depth: u32,
614) {
615 if depth >= MAX_FLATTEN_DEPTH || cubic_is_flat(p0, p1, p2, p3) {
616 builder.line_to(p3);
617 return;
618 }
619
620 let mid = |a: Point, b: Point| Point::new((a.x + b.x) * 0.5, (a.y + b.y) * 0.5);
621 let p01 = mid(p0, p1);
622 let p12 = mid(p1, p2);
623 let p23 = mid(p2, p3);
624 let p012 = mid(p01, p12);
625 let p123 = mid(p12, p23);
626 let p0123 = mid(p012, p123);
627
628 flatten_cubic(builder, p0, p01, p012, p0123, depth + 1);
629 flatten_cubic(builder, p0123, p123, p23, p3, depth + 1);
630}
631
632fn cubic_is_flat(p0: Point, p1: Point, p2: Point, p3: Point) -> bool {
634 let d1 = point_to_chord_distance_squared(p1, p0, p3);
635 let d2 = point_to_chord_distance_squared(p2, p0, p3);
636 let tolerance = FLATTEN_TOLERANCE * FLATTEN_TOLERANCE;
637 d1 <= tolerance && d2 <= tolerance
638}
639
640fn point_to_chord_distance_squared(point: Point, a: Point, b: Point) -> f32 {
641 let ab = Point::new(b.x - a.x, b.y - a.y);
642 let ap = Point::new(point.x - a.x, point.y - a.y);
643 let ab_len_sq = ab.x * ab.x + ab.y * ab.y;
644 if ab_len_sq <= f32::EPSILON {
645 return ap.x * ap.x + ap.y * ap.y;
646 }
647 let cross = ab.x * ap.y - ab.y * ap.x;
648 cross * cross / ab_len_sq
649}
650
651fn emit_arc(
654 builder: &mut PathBuilder,
655 rx: f32,
656 ry: f32,
657 x_rotation_deg: f32,
658 large_arc: bool,
659 sweep: bool,
660 end: Point,
661) {
662 let start = builder.position;
663 if (start.x - end.x).abs() <= f32::EPSILON && (start.y - end.y).abs() <= f32::EPSILON {
664 return;
665 }
666 let mut rx = rx.abs();
667 let mut ry = ry.abs();
668 if rx <= f32::EPSILON || ry <= f32::EPSILON {
669 builder.line_to(end);
670 return;
671 }
672
673 let phi = x_rotation_deg.to_radians();
674 let (sin_phi, cos_phi) = phi.sin_cos();
675
676 let dx2 = (start.x - end.x) * 0.5;
677 let dy2 = (start.y - end.y) * 0.5;
678 let x1p = cos_phi * dx2 + sin_phi * dy2;
679 let y1p = -sin_phi * dx2 + cos_phi * dy2;
680
681 let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
682 if lambda > 1.0 {
683 let scale = lambda.sqrt();
684 rx *= scale;
685 ry *= scale;
686 }
687
688 let rx_sq = rx * rx;
689 let ry_sq = ry * ry;
690 let numerator = (rx_sq * ry_sq - rx_sq * y1p * y1p - ry_sq * x1p * x1p).max(0.0);
691 let denominator = rx_sq * y1p * y1p + ry_sq * x1p * x1p;
692 let mut coefficient = if denominator <= f32::EPSILON {
693 0.0
694 } else {
695 (numerator / denominator).sqrt()
696 };
697 if large_arc == sweep {
698 coefficient = -coefficient;
699 }
700 let cxp = coefficient * rx * y1p / ry;
701 let cyp = -coefficient * ry * x1p / rx;
702
703 let cx = cos_phi * cxp - sin_phi * cyp + (start.x + end.x) * 0.5;
704 let cy = sin_phi * cxp + cos_phi * cyp + (start.y + end.y) * 0.5;
705
706 let angle_of = |x: f32, y: f32| y.atan2(x);
707 let theta1 = angle_of((x1p - cxp) / rx, (y1p - cyp) / ry);
708 let theta2 = angle_of((-x1p - cxp) / rx, (-y1p - cyp) / ry);
709 let two_pi = std::f32::consts::TAU;
710 let mut delta = theta2 - theta1;
711 if sweep {
712 if delta < 0.0 {
713 delta += two_pi;
714 }
715 } else if delta > 0.0 {
716 delta -= two_pi;
717 }
718
719 let segments = ((delta.abs() / ARC_MAX_ANGLE_STEP).ceil() as usize).max(2);
720 for i in 1..=segments {
721 let theta = theta1 + delta * (i as f32 / segments as f32);
722 let (sin_theta, cos_theta) = theta.sin_cos();
723 let x = cos_phi * rx * cos_theta - sin_phi * ry * sin_theta + cx;
724 let y = sin_phi * rx * cos_theta + cos_phi * ry * sin_theta + cy;
725 builder.line_to(Point::new(x, y));
726 }
727 builder.line_to(end);
728 builder.position = end;
729}
730
731#[cfg(test)]
732mod tests {
733 use super::*;
734
735 fn mask_at(mask: &[u8], width: usize, x: usize, y: usize) -> u8 {
736 mask[y * width + x]
737 }
738
739 #[test]
740 fn parses_absolute_triangle() {
741 let path = VectorPath::parse("M 0 0 L 10 0 L 10 10 Z").expect("valid path");
742 assert_eq!(path.subpaths().len(), 1);
743 assert_eq!(
744 path.subpaths()[0],
745 vec![
746 Point::new(0.0, 0.0),
747 Point::new(10.0, 0.0),
748 Point::new(10.0, 10.0)
749 ]
750 );
751 let bounds = path.bounds();
752 assert_eq!((bounds.x, bounds.y), (0.0, 0.0));
753 assert_eq!((bounds.width, bounds.height), (10.0, 10.0));
754 }
755
756 #[test]
757 fn parses_relative_commands_and_h_v() {
758 let path = VectorPath::parse("m 5 5 l 10 0 v 10 h -10 z").expect("valid path");
759 assert_eq!(
760 path.subpaths()[0],
761 vec![
762 Point::new(5.0, 5.0),
763 Point::new(15.0, 5.0),
764 Point::new(15.0, 15.0),
765 Point::new(5.0, 15.0)
766 ]
767 );
768 }
769
770 #[test]
771 fn parses_packed_numbers_and_negative_shorthand() {
772 let path = VectorPath::parse("M10-5L.5.5Z").expect("valid path");
773 assert_eq!(
774 path.subpaths()[0],
775 vec![Point::new(10.0, -5.0), Point::new(0.5, 0.5)]
776 );
777 }
778
779 #[test]
780 fn implicit_lineto_after_moveto() {
781 let path = VectorPath::parse("M 0 0 10 0 10 10").expect("valid path");
782 assert_eq!(path.subpaths()[0].len(), 3);
783 assert_eq!(path.subpaths()[0][2], Point::new(10.0, 10.0));
784 }
785
786 #[test]
787 fn cubic_flattening_hits_endpoints() {
788 let path = VectorPath::parse("M 0 0 C 0 10 10 10 10 0").expect("valid path");
789 let points = &path.subpaths()[0];
790 assert_eq!(points[0], Point::new(0.0, 0.0));
791 assert_eq!(*points.last().unwrap(), Point::new(10.0, 0.0));
792 assert!(points.len() > 4, "curve must be subdivided");
793 let mid = points
794 .iter()
795 .min_by(|a, b| (a.x - 5.0).abs().total_cmp(&(b.x - 5.0).abs()))
796 .unwrap();
797 assert!(
798 (mid.y - 7.5).abs() < 0.2,
799 "flattened curve must pass near the true midpoint, got {mid:?}"
800 );
801 }
802
803 #[test]
804 fn smooth_cubic_reflects_control_point() {
805 let path = VectorPath::parse("M 0 0 C 0 5 2 5 5 5 S 10 5 10 10").expect("valid path");
806 let points = &path.subpaths()[0];
807 assert_eq!(*points.last().unwrap(), Point::new(10.0, 10.0));
808 assert!(
809 points
810 .iter()
811 .any(|p| (p.x - 5.0).abs() < 0.1 && (p.y - 5.0).abs() < 0.1)
812 );
813 }
814
815 #[test]
816 fn quadratic_and_smooth_quadratic() {
817 let path = VectorPath::parse("M 0 0 Q 5 10 10 0 T 20 0").expect("valid path");
818 let points = &path.subpaths()[0];
819 assert_eq!(*points.last().unwrap(), Point::new(20.0, 0.0));
820 assert!(
821 points
822 .iter()
823 .any(|p| (p.x - 5.0).abs() < 0.3 && (p.y - 5.0).abs() < 0.3)
824 );
825 assert!(
826 points
827 .iter()
828 .any(|p| (p.x - 15.0).abs() < 0.3 && (p.y + 5.0).abs() < 0.3)
829 );
830 }
831
832 #[test]
833 fn arc_travels_through_expected_quadrant() {
834 let path = VectorPath::parse("M 0 0 A 5 5 0 0 1 10 0").expect("valid path");
835 let points = &path.subpaths()[0];
836 assert_eq!(*points.last().unwrap(), Point::new(10.0, 0.0));
837 let lowest = points.iter().fold(0.0f32, |acc, p| acc.min(p.y));
838 assert!(
839 (lowest + 5.0).abs() < 0.1,
840 "sweep=1 arc must pass through (5,-5), lowest y = {lowest}"
841 );
842
843 let path = VectorPath::parse("M 0 0 A 5 5 0 0 0 10 0").expect("valid path");
844 let highest = path.subpaths()[0]
845 .iter()
846 .fold(0.0f32, |acc, p| acc.max(p.y));
847 assert!(
848 (highest - 5.0).abs() < 0.1,
849 "sweep=0 arc must pass through (5,5), highest y = {highest}"
850 );
851 }
852
853 #[test]
854 fn arc_flags_may_be_packed() {
855 let spaced = VectorPath::parse("M 0 0 A 5 5 0 0 1 10 0").expect("valid path");
856 let packed = VectorPath::parse("M0 0A5 5 0 0110 0").expect("valid path");
857 assert_eq!(
858 spaced.subpaths()[0].len(),
859 packed.subpaths()[0].len(),
860 "packed arc flags must parse identically"
861 );
862 }
863
864 #[test]
865 fn multiple_subpaths() {
866 let path =
867 VectorPath::parse("M 0 0 h 4 v 4 h -4 Z M 10 10 h 4 v 4 h -4 Z").expect("valid path");
868 assert_eq!(path.subpaths().len(), 2);
869 }
870
871 #[test]
872 fn rejects_garbage() {
873 assert!(VectorPath::parse("this is not a path").is_err());
874 assert!(
875 VectorPath::parse("L 10 10").is_err(),
876 "must start with moveto"
877 );
878 assert!(VectorPath::parse("M 10").is_err(), "missing y coordinate");
879 assert!(
880 VectorPath::parse("M 0 0 A 5 5 0 2 1 10 0").is_err(),
881 "bad flag"
882 );
883 assert_eq!(
884 VectorPath::parse("").unwrap_err(),
885 SvgPathError::MissingMoveTo
886 );
887 }
888
889 #[test]
890 fn fills_axis_aligned_rectangle() {
891 let path = VectorPath::parse("M 2 2 H 8 V 8 H 2 Z").expect("valid path");
892 let mask = path.coverage_mask(10, 10, Point::ZERO, 1.0);
893
894 assert_eq!(mask_at(&mask, 10, 5, 5), 255, "interior must be opaque");
895 assert_eq!(mask_at(&mask, 10, 4, 2), 255, "top edge row is inside");
896 assert_eq!(mask_at(&mask, 10, 0, 0), 0, "outside must stay empty");
897 assert_eq!(mask_at(&mask, 10, 9, 9), 0, "outside must stay empty");
898 }
899
900 #[test]
901 fn triangle_edge_is_antialiased() {
902 let path = VectorPath::parse("M 0 0 L 8 0 L 0 8 Z").expect("valid path");
903 let mask = path.coverage_mask(8, 8, Point::ZERO, 1.0);
904
905 assert_eq!(mask_at(&mask, 8, 1, 1), 255, "deep interior is opaque");
906 assert_eq!(mask_at(&mask, 8, 7, 7), 0, "far corner is empty");
907 let diagonal = mask_at(&mask, 8, 4, 3);
908 assert!(
909 diagonal > 30 && diagonal < 225,
910 "diagonal pixel should be partially covered, got {diagonal}"
911 );
912 }
913
914 #[test]
915 fn even_odd_ring_has_a_hole() {
916 let d = "M 0 0 H 12 V 12 H 0 Z M 4 4 H 8 V 8 H 4 Z";
917 let even_odd =
918 VectorPath::parse_with_fill_rule(d, PathFillRule::EvenOdd).expect("valid path");
919 let non_zero = VectorPath::parse(d).expect("valid path");
920
921 let even_odd_mask = even_odd.coverage_mask(12, 12, Point::ZERO, 1.0);
922 let non_zero_mask = non_zero.coverage_mask(12, 12, Point::ZERO, 1.0);
923
924 assert_eq!(mask_at(&even_odd_mask, 12, 6, 6), 0, "even-odd hole");
925 assert_eq!(mask_at(&even_odd_mask, 12, 2, 6), 255, "even-odd ring");
926 assert_eq!(mask_at(&non_zero_mask, 12, 6, 6), 255, "non-zero solid");
927 }
928
929 #[test]
930 fn non_zero_ring_with_reversed_inner_winding_has_a_hole() {
931 let d = "M 0 0 H 12 V 12 H 0 Z M 4 4 V 8 H 8 V 4 Z";
932 let path = VectorPath::parse(d).expect("valid path");
933 let mask = path.coverage_mask(12, 12, Point::ZERO, 1.0);
934 assert_eq!(mask_at(&mask, 12, 6, 6), 0, "reversed winding hole");
935 assert_eq!(mask_at(&mask, 12, 2, 6), 255, "ring stays filled");
936 }
937
938 #[test]
939 fn circle_from_arcs_fills_center_and_respects_radius() {
940 let path =
941 VectorPath::parse("M 0 8 A 8 8 0 1 1 16 8 A 8 8 0 1 1 0 8 Z").expect("valid path");
942 let mask = path.coverage_mask(16, 16, Point::ZERO, 1.0);
943
944 assert_eq!(mask_at(&mask, 16, 8, 8), 255, "circle center is opaque");
945 assert_eq!(mask_at(&mask, 16, 0, 0), 0, "circle corner is empty");
946 assert_eq!(mask_at(&mask, 16, 15, 0), 0, "circle corner is empty");
947 let area: f32 = mask.iter().map(|&value| value as f32 / 255.0).sum();
948 let expected = std::f32::consts::PI * 8.0 * 8.0;
949 assert!(
950 (area - expected).abs() / expected < 0.05,
951 "filled area {area} should be close to {expected}"
952 );
953 }
954
955 #[test]
956 fn scale_and_origin_map_path_units_to_pixels() {
957 let path = VectorPath::parse("M 10 10 H 14 V 14 H 10 Z").expect("valid path");
958 let mask = path.coverage_mask(8, 8, Point::new(10.0, 10.0), 2.0);
959 assert_eq!(mask_at(&mask, 8, 4, 4), 255, "scaled interior");
960 let full: usize = mask.iter().filter(|&&value| value == 255).count();
961 assert_eq!(full, 64, "the 8x8 pixel mask must be fully covered");
962 }
963
964 #[test]
965 fn empty_and_degenerate_paths_produce_empty_masks() {
966 let path = VectorPath::parse("M 5 5 L 6 6").expect("valid path");
967 assert!(path.is_empty());
968 let mask = path.coverage_mask(8, 8, Point::ZERO, 1.0);
969 assert!(mask.iter().all(|&value| value == 0));
970 }
971}