Add Path2D (#35783)

Signed-off-by: Lukas Lihotzki <lukas@lihotzki.de>
This commit is contained in:
Lukas Lihotzki 2025-03-26 13:12:44 +01:00 committed by GitHub
parent f0ea3c6150
commit 251eeb2c2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 1566 additions and 262 deletions

View file

@ -9,7 +9,7 @@ use std::sync::Arc;
use canvas_traits::canvas::{
Canvas2dMsg, CanvasId, CanvasMsg, CompositionOrBlending, Direction, FillOrStrokeStyle,
FillRule, LineCapStyle, LineJoinStyle, LinearGradientStyle, RadialGradientStyle,
FillRule, LineCapStyle, LineJoinStyle, LinearGradientStyle, PathSegment, RadialGradientStyle,
RepetitionStyle, TextAlign, TextBaseline, TextMetrics as CanvasTextMetrics,
};
use cssparser::color::clamp_unit_f32;
@ -1521,18 +1521,36 @@ impl CanvasState {
self.send_canvas_2d_msg(Canvas2dMsg::Fill(style));
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-fill
pub(crate) fn fill_(&self, path: Vec<PathSegment>, _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));
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-stroke
pub(crate) fn stroke(&self) {
let style = self.state.borrow().stroke_style.to_fill_or_stroke_style();
self.send_canvas_2d_msg(Canvas2dMsg::Stroke(style));
}
pub(crate) fn stroke_(&self, path: Vec<PathSegment>) {
let style = self.state.borrow().stroke_style.to_fill_or_stroke_style();
self.send_canvas_2d_msg(Canvas2dMsg::StrokePath(style, path));
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-clip
pub(crate) fn clip(&self, _fill_rule: CanvasFillRule) {
// TODO: Process fill rule
self.send_canvas_2d_msg(Canvas2dMsg::Clip);
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-clip
pub(crate) fn clip_(&self, path: Vec<PathSegment>, _fill_rule: CanvasFillRule) {
// TODO: Process fill rule
self.send_canvas_2d_msg(Canvas2dMsg::ClipPath(path));
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath
pub(crate) fn is_point_in_path(
&self,
@ -1551,7 +1569,30 @@ impl CanvasState {
};
let (sender, receiver) =
profiled_ipc::channel::<bool>(global.time_profiler_chan().clone()).unwrap();
self.send_canvas_2d_msg(Canvas2dMsg::IsPointInPath(x, y, fill_rule, sender));
self.send_canvas_2d_msg(Canvas2dMsg::IsPointInCurrentPath(x, y, fill_rule, sender));
receiver.recv().unwrap()
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath
pub(crate) fn is_point_in_path_(
&self,
global: &GlobalScope,
path: Vec<PathSegment>,
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()
}

View file

@ -32,6 +32,7 @@ use crate::dom::globalscope::GlobalScope;
use crate::dom::htmlcanvaselement::HTMLCanvasElement;
use crate::dom::imagedata::ImageData;
use crate::dom::node::{Node, NodeDamage, NodeTraits};
use crate::dom::path2d::Path2D;
use crate::dom::textmetrics::TextMetrics;
use crate::script_runtime::CanGc;
@ -288,23 +289,46 @@ impl CanvasRenderingContext2DMethods<crate::DomTypeHolder> for CanvasRenderingCo
self.mark_as_dirty();
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-fill
fn Fill_(&self, path: &Path2D, fill_rule: CanvasFillRule) {
self.canvas_state.fill_(path.segments(), fill_rule);
self.mark_as_dirty();
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-stroke
fn Stroke(&self) {
self.canvas_state.stroke();
self.mark_as_dirty();
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-stroke
fn Stroke_(&self, path: &Path2D) {
self.canvas_state.stroke_(path.segments());
self.mark_as_dirty();
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-clip
fn Clip(&self, fill_rule: CanvasFillRule) {
self.canvas_state.clip(fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-clip
fn Clip_(&self, path: &Path2D, fill_rule: CanvasFillRule) {
self.canvas_state.clip_(path.segments(), fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath
fn IsPointInPath(&self, x: f64, y: f64, fill_rule: CanvasFillRule) -> bool {
self.canvas_state
.is_point_in_path(&self.global(), x, y, fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath
fn IsPointInPath_(&self, path: &Path2D, x: f64, y: f64, fill_rule: CanvasFillRule) -> bool {
self.canvas_state
.is_point_in_path_(&self.global(), path.segments(), x, y, fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-filltext
fn FillText(&self, text: DOMString, x: f64, y: f64, max_width: Option<f64>, can_gc: CanGc) {
self.canvas_state

View file

@ -466,6 +466,7 @@ pub(crate) mod paintrenderingcontext2d;
pub(crate) mod paintsize;
pub(crate) mod paintworkletglobalscope;
pub(crate) mod pannernode;
pub(crate) mod path2d;
pub(crate) mod performance;
#[allow(dead_code)]
pub(crate) mod performanceentry;

View file

@ -27,6 +27,7 @@ use crate::dom::dommatrix::DOMMatrix;
use crate::dom::globalscope::GlobalScope;
use crate::dom::imagedata::ImageData;
use crate::dom::offscreencanvas::OffscreenCanvas;
use crate::dom::path2d::Path2D;
use crate::dom::textmetrics::TextMetrics;
use crate::script_runtime::CanGc;
@ -437,21 +438,41 @@ impl OffscreenCanvasRenderingContext2DMethods<crate::DomTypeHolder>
self.context.Fill(fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-fill
fn Fill_(&self, path: &Path2D, fill_rule: CanvasFillRule) {
self.context.Fill_(path, fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-stroke
fn Stroke(&self) {
self.context.Stroke()
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-stroke
fn Stroke_(&self, path: &Path2D) {
self.context.Stroke_(path)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-clip
fn Clip(&self, fill_rule: CanvasFillRule) {
self.context.Clip(fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-clip
fn Clip_(&self, path: &Path2D, fill_rule: CanvasFillRule) {
self.context.Clip_(path, fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath
fn IsPointInPath(&self, x: f64, y: f64, fill_rule: CanvasFillRule) -> bool {
self.context.IsPointInPath(x, y, fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath
fn IsPointInPath_(&self, path: &Path2D, x: f64, y: f64, fill_rule: CanvasFillRule) -> bool {
self.context.IsPointInPath_(path, x, y, fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-scale
fn Scale(&self, x: f64, y: f64) {
self.context.Scale(x, y)

View file

@ -29,6 +29,7 @@ use crate::dom::canvasgradient::CanvasGradient;
use crate::dom::canvaspattern::CanvasPattern;
use crate::dom::dommatrix::DOMMatrix;
use crate::dom::paintworkletglobalscope::PaintWorkletGlobalScope;
use crate::dom::path2d::Path2D;
use crate::script_runtime::CanGc;
#[dom_struct]
@ -192,22 +193,43 @@ impl PaintRenderingContext2DMethods<crate::DomTypeHolder> for PaintRenderingCont
self.canvas_state.fill(fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-fill
fn Fill_(&self, path: &Path2D, fill_rule: CanvasFillRule) {
self.canvas_state.fill_(path.segments(), fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-stroke
fn Stroke(&self) {
self.canvas_state.stroke()
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-stroke
fn Stroke_(&self, path: &Path2D) {
self.canvas_state.stroke_(path.segments())
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-clip
fn Clip(&self, fill_rule: CanvasFillRule) {
self.canvas_state.clip(fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-clip
fn Clip_(&self, path: &Path2D, fill_rule: CanvasFillRule) {
self.canvas_state.clip_(path.segments(), fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath
fn IsPointInPath(&self, x: f64, y: f64, fill_rule: CanvasFillRule) -> bool {
self.canvas_state
.is_point_in_path(&self.global(), x, y, fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath
fn IsPointInPath_(&self, path: &Path2D, x: f64, y: f64, fill_rule: CanvasFillRule) -> bool {
self.canvas_state
.is_point_in_path_(&self.global(), path.segments(), x, y, fill_rule)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage
fn DrawImage(&self, image: CanvasImageSource, dx: f64, dy: f64) -> ErrorResult {
self.canvas_state.draw_image(None, image, dx, dy)

View file

@ -0,0 +1,313 @@
/* 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/. */
use std::cell::RefCell;
use canvas_traits::canvas::PathSegment;
use dom_struct::dom_struct;
use js::rust::HandleObject;
use script_bindings::str::DOMString;
use crate::dom::bindings::codegen::Bindings::CanvasRenderingContext2DBinding::Path2DMethods;
use crate::dom::bindings::error::{Error, Fallible};
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,
#[no_trace]
path: RefCell<Vec<PathSegment>>,
}
impl Path2D {
pub(crate) fn new() -> Path2D {
Self {
reflector_: Reflector::new(),
path: RefCell::new(vec![]),
}
}
pub(crate) fn new_with_path(other: &Path2D) -> Path2D {
Self {
reflector_: Reflector::new(),
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),
}
}
pub(crate) fn push(&self, seg: PathSegment) {
self.path.borrow_mut().push(seg);
}
pub(crate) fn segments(&self) -> Vec<PathSegment> {
self.path.borrow().clone()
}
}
impl Path2DMethods<crate::DomTypeHolder> for Path2D {
/// <https://html.spec.whatwg.org/multipage/#dom-path2d-addpath>
fn AddPath(&self, other: &Path2D) {
// Step 7. Add all the subpaths in c to a.
let mut dest = self.path.borrow_mut();
dest.extend(other.path.borrow().iter().copied());
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-closepath>
fn ClosePath(&self) {
self.push(PathSegment::ClosePath);
}
/// <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,
});
}
/// <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,
});
}
/// <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,
});
}
/// <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,
});
}
/// <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(())
}
/// <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,
});
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-arc>
fn Arc(
&self,
x: f64,
y: f64,
r: f64,
start: f64,
end: f64,
anticlockwise: 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(())
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-ellipse>
fn Ellipse(
&self,
x: f64,
y: f64,
rx: f64,
ry: f64,
rotation: f64,
start: f64,
end: f64,
anticlockwise: 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(())
}
/// <https://html.spec.whatwg.org/multipage/#dom-path2d-dev>
fn Constructor(
global: &GlobalScope,
proto: Option<HandleObject>,
can_gc: CanGc,
) -> DomRoot<Path2D> {
reflect_dom_object_with_proto(Box::new(Self::new()), global, proto, can_gc)
}
/// <https://html.spec.whatwg.org/multipage/#dom-path2d-dev>
fn Constructor_(
global: &GlobalScope,
proto: Option<HandleObject>,
can_gc: CanGc,
other: &Path2D,
) -> DomRoot<Path2D> {
reflect_dom_object_with_proto(Box::new(Self::new_with_path(other)), global, proto, can_gc)
}
/// <https://html.spec.whatwg.org/multipage/#dom-path2d-dev>
fn Constructor__(
global: &GlobalScope,
proto: Option<HandleObject>,
can_gc: CanGc,
path_string: DOMString,
) -> DomRoot<Path2D> {
reflect_dom_object_with_proto(
Box::new(Self::new_with_str(path_string.str())),
global,
proto,
can_gc,
)
}
}

View file

@ -78,6 +78,8 @@ mod drag_data_store;
mod links;
mod xpath;
mod svgpath;
pub use init::init;
pub use script_runtime::JSEngineSetup;
pub use script_thread::ScriptThread;

View file

@ -0,0 +1,13 @@
/* 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

@ -0,0 +1,198 @@
// 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

@ -0,0 +1,393 @@
// 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

@ -0,0 +1,162 @@
// 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),
}
}
}