canvas: Use wrapped kurbo::BezPath for path everywhere (#37967)

This PR removes existing path(segment) abstractions in favor of
`kurbo::BezPath`, well actually wrapped `kurbo::BezPath`, to ensure
building of valid paths. This allows us better Path2D building in script
and doing all validation and segmentation there and also allows us
remove blocking is_point_in_path on Path2D as we can now do this in
script. Current path is still done on canvas thread side as it will be
harder to move to script (will be done as a follow up), but it now uses
this new path abstraction.

Using kurbo also allows us to ditch our manual svgpath parser with the
one provided by kurbo.

Same code is stolen from: https://github.com/servo/servo/pull/36821.

Testing: Existing WPT tests
Fixes: #37904

wpt run: https://github.com/sagudev/servo/actions/runs/16172191716

---------

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
This commit is contained in:
sagudev 2025-07-12 12:37:47 +02:00 committed by GitHub
parent d4528e84b9
commit 9b5b26386c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 571 additions and 1502 deletions

View file

@ -81,6 +81,7 @@ itertools = { workspace = true }
js = { workspace = true }
jstraceable_derive = { path = "../jstraceable_derive" }
keyboard-types = { workspace = true }
kurbo = { workspace = true }
layout_api = { workspace = true }
libc = { workspace = true }
log = { workspace = true }

View file

@ -9,7 +9,7 @@ use std::sync::Arc;
use canvas_traits::canvas::{
Canvas2dMsg, CanvasId, CanvasMsg, CompositionOrBlending, Direction, FillOrStrokeStyle,
FillRule, LineCapStyle, LineJoinStyle, LinearGradientStyle, PathSegment, RadialGradientStyle,
FillRule, LineCapStyle, LineJoinStyle, LinearGradientStyle, Path, RadialGradientStyle,
RepetitionStyle, TextAlign, TextBaseline, TextMetrics as CanvasTextMetrics,
};
use constellation_traits::ScriptToConstellationMessage;
@ -1799,7 +1799,7 @@ impl CanvasState {
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-fill
pub(crate) fn fill_(&self, path: Vec<PathSegment>, _fill_rule: CanvasFillRule) {
pub(crate) fn fill_(&self, path: Path, _fill_rule: CanvasFillRule) {
// TODO: Process fill rule
let style = self.state.borrow().fill_style.to_fill_or_stroke_style();
self.send_canvas_2d_msg(Canvas2dMsg::FillPath(style, path));
@ -1811,7 +1811,7 @@ impl CanvasState {
self.send_canvas_2d_msg(Canvas2dMsg::Stroke(style));
}
pub(crate) fn stroke_(&self, path: Vec<PathSegment>) {
pub(crate) fn stroke_(&self, path: Path) {
let style = self.state.borrow().stroke_style.to_fill_or_stroke_style();
self.send_canvas_2d_msg(Canvas2dMsg::StrokePath(style, path));
}
@ -1823,7 +1823,7 @@ impl CanvasState {
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-clip
pub(crate) fn clip_(&self, path: Vec<PathSegment>, _fill_rule: CanvasFillRule) {
pub(crate) fn clip_(&self, path: Path, _fill_rule: CanvasFillRule) {
// TODO: Process fill rule
self.send_canvas_2d_msg(Canvas2dMsg::ClipPath(path));
}
@ -1853,24 +1853,17 @@ impl CanvasState {
// https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath
pub(crate) fn is_point_in_path_(
&self,
global: &GlobalScope,
path: Vec<PathSegment>,
_global: &GlobalScope,
path: Path,
x: f64,
y: f64,
fill_rule: CanvasFillRule,
) -> bool {
if !(x.is_finite() && y.is_finite()) {
return false;
}
let fill_rule = match fill_rule {
CanvasFillRule::Nonzero => FillRule::Nonzero,
CanvasFillRule::Evenodd => FillRule::Evenodd,
};
let (sender, receiver) =
profiled_ipc::channel::<bool>(global.time_profiler_chan().clone()).unwrap();
self.send_canvas_2d_msg(Canvas2dMsg::IsPointInPath(path, x, y, fill_rule, sender));
receiver.recv().unwrap()
path.is_point_in_path(x, y, fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-scale

View file

@ -4,7 +4,7 @@
use std::cell::RefCell;
use canvas_traits::canvas::PathSegment;
use canvas_traits::canvas::Path;
use dom_struct::dom_struct;
use js::rust::HandleObject;
use script_bindings::str::DOMString;
@ -15,20 +15,20 @@ use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
use crate::dom::bindings::root::DomRoot;
use crate::dom::globalscope::GlobalScope;
use crate::script_runtime::CanGc;
use crate::svgpath::PathParser;
#[dom_struct]
pub(crate) struct Path2D {
reflector_: Reflector,
#[ignore_malloc_size_of = "Defined in kurbo."]
#[no_trace]
path: RefCell<Vec<PathSegment>>,
path: RefCell<Path>,
}
impl Path2D {
pub(crate) fn new() -> Path2D {
Self {
reflector_: Reflector::new(),
path: RefCell::new(vec![]),
path: RefCell::new(Path::new()),
}
}
pub(crate) fn new_with_path(other: &Path2D) -> Path2D {
@ -37,26 +37,15 @@ impl Path2D {
path: other.path.clone(),
}
}
pub(crate) fn new_with_str(path: &str) -> Path2D {
let mut path_segments = Vec::new();
for segment in PathParser::new(path) {
if let Ok(segment) = segment {
path_segments.push(segment);
} else {
break;
}
}
Self {
reflector_: Reflector::new(),
path: RefCell::new(path_segments),
path: RefCell::new(Path::from_svg(path)),
}
}
pub(crate) fn push(&self, seg: PathSegment) {
self.path.borrow_mut().push(seg);
}
pub(crate) fn segments(&self) -> Vec<PathSegment> {
pub(crate) fn segments(&self) -> Path {
self.path.borrow().clone()
}
}
@ -64,143 +53,49 @@ impl Path2D {
impl Path2DMethods<crate::DomTypeHolder> for Path2D {
/// <https://html.spec.whatwg.org/multipage/#dom-path2d-addpath>
fn AddPath(&self, other: &Path2D) {
let other = other.segments();
// Step 7. Add all the subpaths in c to a.
if std::ptr::eq(&self.path, &other.path) {
// Note: this is not part of the spec, but it is a workaround to
// avoids borrow conflict when path is same as other.path
self.path.borrow_mut().extend_from_within(..);
} else {
let mut dest = self.path.borrow_mut();
dest.extend(other.path.borrow().iter().copied());
}
self.path.borrow_mut().0.extend(other.0);
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-closepath>
fn ClosePath(&self) {
self.push(PathSegment::ClosePath);
self.path.borrow_mut().close_path();
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-moveto>
fn MoveTo(&self, x: f64, y: f64) {
// Step 1. If either of the arguments are infinite or NaN, then return.
if !(x.is_finite() && y.is_finite()) {
return;
}
// Step 2. Create a new subpath with the specified point as its first (and only) point.
self.push(PathSegment::MoveTo {
x: x as f32,
y: y as f32,
});
self.path.borrow_mut().move_to(x, y);
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-lineto>
fn LineTo(&self, x: f64, y: f64) {
// Step 1. If either of the arguments are infinite or NaN, then return.
if !(x.is_finite() && y.is_finite()) {
return;
}
self.push(PathSegment::LineTo {
x: x as f32,
y: y as f32,
});
self.path.borrow_mut().line_to(x, y);
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-quadraticcurveto>
fn QuadraticCurveTo(&self, cpx: f64, cpy: f64, x: f64, y: f64) {
// Step 1. If any of the arguments are infinite or NaN, then return.
if !(cpx.is_finite() && cpy.is_finite() && x.is_finite() && y.is_finite()) {
return;
}
self.push(PathSegment::Quadratic {
cpx: cpx as f32,
cpy: cpy as f32,
x: x as f32,
y: y as f32,
});
self.path.borrow_mut().quadratic_curve_to(cpx, cpy, x, y);
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-beziercurveto>
fn BezierCurveTo(&self, cp1x: f64, cp1y: f64, cp2x: f64, cp2y: f64, x: f64, y: f64) {
// Step 1. If any of the arguments are infinite or NaN, then return.
if !(cp1x.is_finite() &&
cp1y.is_finite() &&
cp2x.is_finite() &&
cp2y.is_finite() &&
x.is_finite() &&
y.is_finite())
{
return;
}
self.push(PathSegment::Bezier {
cp1x: cp1x as f32,
cp1y: cp1y as f32,
cp2x: cp2x as f32,
cp2y: cp2y as f32,
x: x as f32,
y: y as f32,
});
self.path
.borrow_mut()
.bezier_curve_to(cp1x, cp1y, cp2x, cp2y, x, y);
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-arcto>
fn ArcTo(&self, x1: f64, y1: f64, x2: f64, y2: f64, r: f64) -> Fallible<()> {
// Step 1. If any of the arguments are infinite or NaN, then return.
if !(x1.is_finite() && y1.is_finite() && x2.is_finite() && y2.is_finite() && r.is_finite())
{
return Ok(());
}
// Step 3. If radius is negative, then throw an "IndexSizeError" DOMException.
if r < 0.0 {
return Err(Error::IndexSize);
}
self.push(PathSegment::ArcTo {
cp1x: x1 as f32,
cp1y: y1 as f32,
cp2x: x2 as f32,
cp2y: y2 as f32,
radius: r as f32,
});
Ok(())
fn ArcTo(&self, x1: f64, y1: f64, x2: f64, y2: f64, radius: f64) -> Fallible<()> {
self.path
.borrow_mut()
.arc_to(x1, y1, x2, y2, radius)
.map_err(|_| Error::IndexSize)
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-rect>
fn Rect(&self, x: f64, y: f64, w: f64, h: f64) {
// Step 1. If any of the arguments are infinite or NaN, then return.
if !(x.is_finite() && y.is_finite() && w.is_finite() && h.is_finite()) {
return;
}
// Step 2. Create a new subpath containing just the four points
// (x, y), (x+w, y), (x+w, y+h), (x, y+h), in that order,
// with those four points connected by straight lines.
self.push(PathSegment::MoveTo {
x: x as f32,
y: y as f32,
});
self.push(PathSegment::LineTo {
x: (x + w) as f32,
y: y as f32,
});
self.push(PathSegment::LineTo {
x: (x + w) as f32,
y: (y + h) as f32,
});
self.push(PathSegment::LineTo {
x: x as f32,
y: (y + h) as f32,
});
// Step 3. Mark the subpath as closed.
self.push(PathSegment::ClosePath);
// Step 4. Create a new subpath with the point (x, y) as the only point in the subpath.
self.push(PathSegment::MoveTo {
x: x as f32,
y: y as f32,
});
self.path.borrow_mut().rect(x, y, w, h);
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-arc>
@ -208,37 +103,15 @@ impl Path2DMethods<crate::DomTypeHolder> for Path2D {
&self,
x: f64,
y: f64,
r: f64,
start: f64,
end: f64,
anticlockwise: bool,
radius: f64,
start_angle: f64,
end_angle: f64,
counterclockwise: bool,
) -> Fallible<()> {
// Step 1. If any of the arguments are infinite or NaN, then return.
if !(x.is_finite() &&
y.is_finite() &&
r.is_finite() &&
start.is_finite() &&
end.is_finite())
{
return Ok(());
}
// Step 2. If either radiusX or radiusY are negative, then throw an "IndexSizeError" DOMException.
if r < 0.0 {
return Err(Error::IndexSize);
}
self.push(PathSegment::Ellipse {
x: x as f32,
y: y as f32,
radius_x: r as f32,
radius_y: r as f32,
rotation: 0.,
start_angle: start as f32,
end_angle: end as f32,
anticlockwise,
});
Ok(())
self.path
.borrow_mut()
.arc(x, y, radius, start_angle, end_angle, counterclockwise)
.map_err(|_| Error::IndexSize)
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-ellipse>
@ -246,41 +119,26 @@ impl Path2DMethods<crate::DomTypeHolder> for Path2D {
&self,
x: f64,
y: f64,
rx: f64,
ry: f64,
rotation: f64,
start: f64,
end: f64,
anticlockwise: bool,
radius_x: f64,
radius_y: f64,
rotation_angle: f64,
start_angle: f64,
end_angle: f64,
counterclockwise: bool,
) -> Fallible<()> {
// Step 1. If any of the arguments are infinite or NaN, then return.
if !(x.is_finite() &&
y.is_finite() &&
rx.is_finite() &&
ry.is_finite() &&
rotation.is_finite() &&
start.is_finite() &&
end.is_finite())
{
return Ok(());
}
// Step 2. If either radiusX or radiusY are negative, then throw an "IndexSizeError" DOMException.
if rx < 0.0 || ry < 0.0 {
return Err(Error::IndexSize);
}
self.push(PathSegment::Ellipse {
x: x as f32,
y: y as f32,
radius_x: rx as f32,
radius_y: ry as f32,
rotation: rotation as f32,
start_angle: start as f32,
end_angle: end as f32,
anticlockwise,
});
Ok(())
self.path
.borrow_mut()
.ellipse(
x,
y,
radius_x,
radius_y,
rotation_angle,
start_angle,
end_angle,
counterclockwise,
)
.map_err(|_| Error::IndexSize)
}
/// <https://html.spec.whatwg.org/multipage/#dom-path2d-dev>

View file

@ -74,8 +74,6 @@ mod drag_data_store;
mod links;
mod xpath;
mod svgpath;
pub use init::init;
pub(crate) use script_bindings::DomTypes;
pub use script_runtime::JSEngineSetup;

View file

@ -1,13 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
mod number;
mod path;
mod stream;
#[derive(Debug, Default, Eq, PartialEq)]
pub struct Error;
pub(crate) use path::PathParser;
pub(crate) use stream::Stream;

View file

@ -1,198 +0,0 @@
// Copyright 2018 the SVG Types Authors
// Copyright 2025 the Servo Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use std::str::FromStr;
use crate::svgpath::{Error, Stream};
/// An [SVG number](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumber).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Number(pub f32);
impl std::str::FromStr for Number {
type Err = Error;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let mut s = Stream::from(text);
let n = s.parse_number()?;
s.skip_spaces();
if !s.at_end() {
return Err(Error);
}
Ok(Self(n))
}
}
impl Stream<'_> {
/// Parses number from the stream.
///
/// This method will detect a number length and then
/// will pass a substring to the `f64::from_str` method.
///
/// <https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumber>
pub fn parse_number(&mut self) -> Result<f32, Error> {
// Strip off leading whitespaces.
self.skip_spaces();
if self.at_end() {
return Err(Error);
}
self.parse_number_impl().map_err(|_| Error)
}
fn parse_number_impl(&mut self) -> Result<f32, Error> {
let start = self.pos();
let mut c = self.curr_byte()?;
// Consume sign.
if matches!(c, b'+' | b'-') {
self.advance(1);
c = self.curr_byte()?;
}
// Consume integer.
match c {
b'0'..=b'9' => self.skip_digits(),
b'.' => {},
_ => return Err(Error),
}
// Consume fraction.
if let Ok(b'.') = self.curr_byte() {
self.advance(1);
self.skip_digits();
}
if let Ok(c) = self.curr_byte() {
if matches!(c, b'e' | b'E') {
let c2 = self.next_byte()?;
// Check for `em`/`ex`.
if c2 != b'm' && c2 != b'x' {
self.advance(1);
match self.curr_byte()? {
b'+' | b'-' => {
self.advance(1);
self.skip_digits();
},
b'0'..=b'9' => self.skip_digits(),
_ => {
return Err(Error);
},
}
}
}
}
let s = self.slice_back(start);
// Use the default f32 parser now.
if let Ok(n) = f32::from_str(s) {
// inf, nan, etc. are an error.
if n.is_finite() {
return Ok(n);
}
}
Err(Error)
}
/// Parses number from a list of numbers.
pub fn parse_list_number(&mut self) -> Result<f32, Error> {
if self.at_end() {
return Err(Error);
}
let n = self.parse_number()?;
self.skip_spaces();
self.parse_list_separator();
Ok(n)
}
}
/// A pull-based [`<list-of-numbers>`] parser.
///
/// # Examples
///
/// ```
/// use svgtypes::NumberListParser;
///
/// let mut p = NumberListParser::from("10, 20 -50");
/// assert_eq!(p.next().unwrap().unwrap(), 10.0);
/// assert_eq!(p.next().unwrap().unwrap(), 20.0);
/// assert_eq!(p.next().unwrap().unwrap(), -50.0);
/// assert_eq!(p.next().is_none(), true);
/// ```
///
/// [`<list-of-numbers>`]: https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumberList
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct NumberListParser<'a>(Stream<'a>);
impl<'a> From<&'a str> for NumberListParser<'a> {
#[inline]
fn from(v: &'a str) -> Self {
NumberListParser(Stream::from(v))
}
}
impl Iterator for NumberListParser<'_> {
type Item = Result<f32, Error>;
fn next(&mut self) -> Option<Self::Item> {
if self.0.at_end() {
None
} else {
let v = self.0.parse_list_number();
if v.is_err() {
self.0.jump_to_end();
}
Some(v)
}
}
}
#[rustfmt::skip]
#[cfg(test)]
mod tests {
use crate::svgpath::Stream;
macro_rules! test_p {
($name:ident, $text:expr, $result:expr) => (
#[test]
fn $name() {
let mut s = Stream::from($text);
assert_eq!(s.parse_number().unwrap(), $result);
}
)
}
test_p!(parse_1, "0", 0.0);
test_p!(parse_2, "1", 1.0);
test_p!(parse_3, "-1", -1.0);
test_p!(parse_4, " -1 ", -1.0);
test_p!(parse_5, " 1 ", 1.0);
test_p!(parse_6, ".4", 0.4);
test_p!(parse_7, "-.4", -0.4);
test_p!(parse_8, "-.4text", -0.4);
test_p!(parse_9, "-.01 text", -0.01);
test_p!(parse_10, "-.01 4", -0.01);
test_p!(parse_11, ".0000000000008", 0.0000000000008);
test_p!(parse_12, "1000000000000", 1000000000000.0);
test_p!(parse_13, "123456.123456", 123456.123456);
test_p!(parse_14, "+10", 10.0);
test_p!(parse_15, "1e2", 100.0);
test_p!(parse_16, "1e+2", 100.0);
test_p!(parse_17, "1E2", 100.0);
test_p!(parse_18, "1e-2", 0.01);
test_p!(parse_19, "1ex", 1.0);
test_p!(parse_20, "1em", 1.0);
test_p!(parse_21, "12345678901234567890", 12345678901234567000.0);
test_p!(parse_22, "0.", 0.0);
test_p!(parse_23, "1.3e-2", 0.013);
// test_number!(parse_24, "1e", 1.0); // TODO: this
}

View file

@ -1,393 +0,0 @@
// Copyright 2021 the SVG Types Authors
// Copyright 2025 the Servo Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use canvas_traits::canvas::PathSegment;
use crate::svgpath::{Error, Stream};
pub struct PathParser<'a> {
stream: Stream<'a>,
state: State,
last_cmd: u8,
}
impl<'a> PathParser<'a> {
pub fn new(string: &'a str) -> Self {
Self {
stream: Stream::from(string),
state: State::default(),
last_cmd: b' ',
}
}
}
impl Iterator for PathParser<'_> {
type Item = Result<PathSegment, Error>;
fn next(&mut self) -> Option<Self::Item> {
self.stream.skip_spaces();
let Ok(curr_byte) = self.stream.curr_byte() else {
return None;
};
let cmd = if self.last_cmd == b' ' {
if let move_to @ (b'm' | b'M') = curr_byte {
self.stream.advance(1);
move_to
} else {
return Some(Err(Error));
}
} else if curr_byte.is_ascii_alphabetic() {
self.stream.advance(1);
curr_byte
} else {
match self.last_cmd {
b'm' => b'l',
b'M' => b'L',
b'z' | b'Z' => return Some(Err(Error)),
cmd => cmd,
}
};
self.last_cmd = cmd;
Some(to_point(&mut self.stream, cmd, &mut self.state))
}
}
#[derive(Default)]
pub struct State {
start: (f32, f32),
pos: (f32, f32),
quad: (f32, f32),
cubic: (f32, f32),
}
pub fn to_point(s: &mut Stream<'_>, cmd: u8, state: &mut State) -> Result<PathSegment, Error> {
let abs = cmd.is_ascii_uppercase();
let cmd = cmd.to_ascii_lowercase();
let (dx, dy) = if abs { (0., 0.) } else { state.pos };
let seg = match cmd {
b'm' => PathSegment::MoveTo {
x: s.parse_list_number()? + dx,
y: s.parse_list_number()? + dy,
},
b'l' => PathSegment::LineTo {
x: s.parse_list_number()? + dx,
y: s.parse_list_number()? + dy,
},
b'h' => PathSegment::LineTo {
x: s.parse_list_number()? + dx,
y: state.pos.1,
},
b'v' => PathSegment::LineTo {
x: state.pos.0,
y: s.parse_list_number()? + dy,
},
b'c' => PathSegment::Bezier {
cp1x: s.parse_list_number()? + dx,
cp1y: s.parse_list_number()? + dy,
cp2x: s.parse_list_number()? + dx,
cp2y: s.parse_list_number()? + dy,
x: s.parse_list_number()? + dx,
y: s.parse_list_number()? + dy,
},
b's' => PathSegment::Bezier {
cp1x: state.cubic.0,
cp1y: state.cubic.1,
cp2x: s.parse_list_number()? + dx,
cp2y: s.parse_list_number()? + dy,
x: s.parse_list_number()? + dx,
y: s.parse_list_number()? + dy,
},
b'q' => PathSegment::Quadratic {
cpx: s.parse_list_number()? + dx,
cpy: s.parse_list_number()? + dy,
x: s.parse_list_number()? + dx,
y: s.parse_list_number()? + dy,
},
b't' => PathSegment::Quadratic {
cpx: state.quad.0,
cpy: state.quad.1,
x: s.parse_list_number()? + dx,
y: s.parse_list_number()? + dy,
},
b'a' => PathSegment::SvgArc {
radius_x: s.parse_list_number()?,
radius_y: s.parse_list_number()?,
rotation: s.parse_list_number()?,
large_arc: s.parse_flag()?,
sweep: s.parse_flag()?,
x: s.parse_list_number()? + dx,
y: s.parse_list_number()? + dy,
},
b'z' => PathSegment::ClosePath,
_ => return Err(crate::svgpath::Error),
};
match seg {
PathSegment::MoveTo { x, y } => {
state.start = (x, y);
state.pos = (x, y);
state.quad = (x, y);
state.cubic = (x, y);
},
PathSegment::LineTo { x, y } | PathSegment::SvgArc { x, y, .. } => {
state.pos = (x, y);
state.quad = (x, y);
state.cubic = (x, y);
},
PathSegment::Bezier {
cp2x, cp2y, x, y, ..
} => {
state.pos = (x, y);
state.quad = (x, y);
state.cubic = (x * 2.0 - cp2x, y * 2.0 - cp2y);
},
PathSegment::Quadratic { cpx, cpy, x, y, .. } => {
state.pos = (x, y);
state.quad = (x * 2.0 - cpx, y * 2.0 - cpy);
state.cubic = (x, y);
},
PathSegment::ClosePath => {
state.pos = state.start;
state.quad = state.start;
state.cubic = state.start;
},
_ => {},
}
Ok(seg)
}
#[rustfmt::skip]
#[cfg(test)]
mod tests {
use super::*;
macro_rules! test {
($name:ident, $text:expr, $( $seg:expr ),*) => (
#[test]
fn $name() {
let mut s = PathParser::new($text);
$(
assert_eq!(s.next().unwrap().unwrap(), $seg);
)*
if let Some(res) = s.next() {
assert!(res.is_err());
}
}
)
}
test!(null, "", );
test!(not_a_path, "q", );
test!(not_a_move_to, "L 20 30", );
test!(stop_on_err_1, "M 10 20 L 30 40 L 50",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::LineTo { x: 30.0, y: 40.0 }
);
test!(move_to_1, "M 10 20", PathSegment::MoveTo { x: 10.0, y: 20.0 });
test!(move_to_2, "m 10 20", PathSegment::MoveTo { x: 10.0, y: 20.0 });
test!(move_to_3, "M 10 20 30 40 50 60",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::LineTo { x: 30.0, y: 40.0 },
PathSegment::LineTo { x: 50.0, y: 60.0 }
);
test!(move_to_4, "M 10 20 30 40 50 60 M 70 80 90 100 110 120",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::LineTo { x: 30.0, y: 40.0 },
PathSegment::LineTo { x: 50.0, y: 60.0 },
PathSegment::MoveTo { x: 70.0, y: 80.0 },
PathSegment::LineTo { x: 90.0, y: 100.0 },
PathSegment::LineTo { x: 110.0, y: 120.0 }
);
test!(arc_to_1, "M 10 20 A 5 5 30 1 1 20 20",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::SvgArc {
radius_x: 5.0, radius_y: 5.0,
rotation: 30.0,
large_arc: true, sweep: true,
x: 20.0, y: 20.0
}
);
test!(arc_to_2, "M 10 20 a 5 5 30 0 0 20 20",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::SvgArc {
radius_x: 5.0, radius_y: 5.0,
rotation: 30.0,
large_arc: false, sweep: false,
x: 30.0, y: 40.0
}
);
test!(arc_to_10, "M10-20A5.5.3-4 010-.1",
PathSegment::MoveTo { x: 10.0, y: -20.0 },
PathSegment::SvgArc {
radius_x: 5.5, radius_y: 0.3,
rotation: -4.0,
large_arc: false, sweep: true,
x: 0.0, y: -0.1
}
);
test!(separator_1, "M 10 20 L 5 15 C 10 20 30 40 50 60",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::LineTo { x: 5.0, y: 15.0 },
PathSegment::Bezier {
cp1x: 10.0, cp1y: 20.0,
cp2x: 30.0, cp2y: 40.0,
x: 50.0, y: 60.0,
}
);
test!(separator_2, "M 10, 20 L 5, 15 C 10, 20 30, 40 50, 60",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::LineTo { x: 5.0, y: 15.0 },
PathSegment::Bezier {
cp1x: 10.0, cp1y: 20.0,
cp2x: 30.0, cp2y: 40.0,
x: 50.0, y: 60.0,
}
);
test!(separator_3, "M 10,20 L 5,15 C 10,20 30,40 50,60",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::LineTo { x: 5.0, y: 15.0 },
PathSegment::Bezier {
cp1x: 10.0, cp1y: 20.0,
cp2x: 30.0, cp2y: 40.0,
x: 50.0, y: 60.0,
}
);
test!(separator_4, "M10, 20 L5, 15 C10, 20 30 40 50 60",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::LineTo { x: 5.0, y: 15.0 },
PathSegment::Bezier {
cp1x: 10.0, cp1y: 20.0,
cp2x: 30.0, cp2y: 40.0,
x: 50.0, y: 60.0,
}
);
test!(separator_5, "M10 20V30H40V50H60Z",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::LineTo { x: 10.0, y: 30.0 },
PathSegment::LineTo { x: 40.0, y: 30.0 },
PathSegment::LineTo { x: 40.0, y: 50.0 },
PathSegment::LineTo { x: 60.0, y: 50.0 },
PathSegment::ClosePath
);
test!(all_segments_1, "M 10 20 L 30 40 H 50 V 60 C 70 80 90 100 110 120 S 130 140 150 160
Q 170 180 190 200 T 210 220 A 50 50 30 1 1 230 240 Z",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::LineTo { x: 30.0, y: 40.0 },
PathSegment::LineTo { x: 50.0, y: 40.0 },
PathSegment::LineTo { x: 50.0, y: 60.0 },
PathSegment::Bezier {
cp1x: 70.0, cp1y: 80.0,
cp2x: 90.0, cp2y: 100.0,
x: 110.0, y: 120.0,
},
PathSegment::Bezier {
cp1x: 130.0, cp1y: 140.0,
cp2x: 130.0, cp2y: 140.0,
x: 150.0, y: 160.0,
},
PathSegment::Quadratic {
cpx: 170.0, cpy: 180.0,
x: 190.0, y: 200.0,
},
PathSegment::Quadratic {
cpx: 210.0, cpy: 220.0,
x: 210.0, y: 220.0,
},
PathSegment::SvgArc {
radius_x: 50.0, radius_y: 50.0,
rotation: 30.0,
large_arc: true, sweep: true,
x: 230.0, y: 240.0
},
PathSegment::ClosePath
);
test!(all_segments_2, "m 10 20 l 30 40 h 50 v 60 c 70 80 90 100 110 120 s 130 140 150 160
q 170 180 190 200 t 210 220 a 50 50 30 1 1 230 240 z",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::LineTo { x: 40.0, y: 60.0 },
PathSegment::LineTo { x: 90.0, y: 60.0 },
PathSegment::LineTo { x: 90.0, y: 120.0 },
PathSegment::Bezier {
cp1x: 160.0, cp1y: 200.0,
cp2x: 180.0, cp2y: 220.0,
x: 200.0, y: 240.0,
},
PathSegment::Bezier {
cp1x: 220.0, cp1y: 260.0, //?
cp2x: 330.0, cp2y: 380.0,
x: 350.0, y: 400.0,
},
PathSegment::Quadratic {
cpx: 520.0, cpy: 580.0,
x: 540.0, y: 600.0,
},
PathSegment::Quadratic {
cpx: 560.0, cpy: 620.0, //?
x: 750.0, y: 820.0
},
PathSegment::SvgArc {
radius_x: 50.0, radius_y: 50.0,
rotation: 30.0,
large_arc: true, sweep: true,
x: 980.0, y: 1060.0
},
PathSegment::ClosePath
);
test!(close_path_1, "M10 20 L 30 40 ZM 100 200 L 300 400",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::LineTo { x: 30.0, y: 40.0 },
PathSegment::ClosePath,
PathSegment::MoveTo { x: 100.0, y: 200.0 },
PathSegment::LineTo { x: 300.0, y: 400.0 }
);
test!(close_path_2, "M10 20 L 30 40 zM 100 200 L 300 400",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::LineTo { x: 30.0, y: 40.0 },
PathSegment::ClosePath,
PathSegment::MoveTo { x: 100.0, y: 200.0 },
PathSegment::LineTo { x: 300.0, y: 400.0 }
);
test!(close_path_3, "M10 20 L 30 40 Z Z Z",
PathSegment::MoveTo { x: 10.0, y: 20.0 },
PathSegment::LineTo { x: 30.0, y: 40.0 },
PathSegment::ClosePath,
PathSegment::ClosePath,
PathSegment::ClosePath
);
// first token should be EndOfStream
test!(invalid_1, "M\t.", );
// ClosePath can't be followed by a number
test!(invalid_2, "M 0 0 Z 2",
PathSegment::MoveTo { x: 0.0, y: 0.0 },
PathSegment::ClosePath
);
// ClosePath can be followed by any command
test!(invalid_3, "M 0 0 Z H 10",
PathSegment::MoveTo { x: 0.0, y: 0.0 },
PathSegment::ClosePath,
PathSegment::LineTo { x: 10.0, y: 0.0 }
);
}

View file

@ -1,162 +0,0 @@
// Copyright 2018 the SVG Types Authors
// Copyright 2025 the Servo Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::svgpath::Error;
/// A streaming text parsing interface.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Stream<'a> {
text: &'a str,
pos: usize,
}
impl<'a> From<&'a str> for Stream<'a> {
#[inline]
fn from(text: &'a str) -> Self {
Stream { text, pos: 0 }
}
}
impl<'a> Stream<'a> {
/// Returns the current position in bytes.
#[inline]
pub fn pos(&self) -> usize {
self.pos
}
/// Sets current position equal to the end.
///
/// Used to indicate end of parsing on error.
#[inline]
pub fn jump_to_end(&mut self) {
self.pos = self.text.len();
}
/// Checks if the stream is reached the end.
///
/// Any [`pos()`] value larger than original text length indicates stream end.
///
/// Accessing stream after reaching end via safe methods will produce
/// an error.
///
/// Accessing stream after reaching end via *_unchecked methods will produce
/// a Rust's bound checking error.
///
/// [`pos()`]: #method.pos
#[inline]
pub fn at_end(&self) -> bool {
self.pos >= self.text.len()
}
/// Returns a byte from a current stream position.
#[inline]
pub fn curr_byte(&self) -> Result<u8, Error> {
if self.at_end() {
return Err(Error);
}
Ok(self.curr_byte_unchecked())
}
/// Returns a byte from a current stream position.
///
/// # Panics
///
/// - if the current position is after the end of the data
#[inline]
pub fn curr_byte_unchecked(&self) -> u8 {
self.text.as_bytes()[self.pos]
}
/// Checks that current byte is equal to provided.
///
/// Returns `false` if no bytes left.
#[inline]
pub fn is_curr_byte_eq(&self, c: u8) -> bool {
if !self.at_end() {
self.curr_byte_unchecked() == c
} else {
false
}
}
/// Returns a next byte from a current stream position.
#[inline]
pub fn next_byte(&self) -> Result<u8, Error> {
if self.pos + 1 >= self.text.len() {
return Err(Error);
}
Ok(self.text.as_bytes()[self.pos + 1])
}
/// Advances by `n` bytes.
#[inline]
pub fn advance(&mut self, n: usize) {
debug_assert!(self.pos + n <= self.text.len());
self.pos += n;
}
/// Skips whitespaces.
///
/// Accepted values: `' ' \n \r \t`.
pub fn skip_spaces(&mut self) {
while !self.at_end() && matches!(self.curr_byte_unchecked(), b' ' | b'\t' | b'\n' | b'\r') {
self.advance(1);
}
}
/// Consumes bytes by the predicate.
pub fn skip_bytes<F>(&mut self, f: F)
where
F: Fn(&Stream<'_>, u8) -> bool,
{
while !self.at_end() {
let c = self.curr_byte_unchecked();
if f(self, c) {
self.advance(1);
} else {
break;
}
}
}
/// Slices data from `pos` to the current position.
#[inline]
pub fn slice_back(&self, pos: usize) -> &'a str {
&self.text[pos..self.pos]
}
/// Skips digits.
pub fn skip_digits(&mut self) {
self.skip_bytes(|_, c| c.is_ascii_digit());
}
#[inline]
pub(crate) fn parse_list_separator(&mut self) {
if self.is_curr_byte_eq(b',') {
self.advance(1);
}
}
// By the SVG spec 'large-arc' and 'sweep' must contain only one char
// and can be written without any separators, e.g.: 10 20 30 01 10 20.
pub(crate) fn parse_flag(&mut self) -> Result<bool, Error> {
self.skip_spaces();
let c = self.curr_byte()?;
match c {
b'0' | b'1' => {
self.advance(1);
if self.is_curr_byte_eq(b',') {
self.advance(1);
}
self.skip_spaces();
Ok(c == b'1')
},
_ => Err(Error),
}
}
}