/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Specified types for SVG Path.
usecrate::parser::{Parse, ParserContext}; usecrate::values::animated::{lists, Animate, Procedure}; usecrate::values::distance::{ComputeSquaredDistance, SquaredDistance}; usecrate::values::generics::basic_shape::GenericShapeCommand; usecrate::values::generics::basic_shape::{ArcSize, ArcSweep, ByTo, CoordinatePair}; usecrate::values::CSSFloat; use cssparser::Parser; use std::fmt::{self, Write}; use std::iter::{Cloned, Peekable}; use std::ops; use std::slice; use style_traits::values::SequenceWriter; use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
/// Whether to allow empty string in the parser. #[derive(Clone, Debug, Eq, PartialEq)] #[allow(missing_docs)] pubenum AllowEmpty {
Yes,
No,
}
/// The SVG path data. /// /// https://www.w3.org/TR/SVG11/paths.html#PathData #[derive(
Clone,
Debug,
Deserialize,
MallocSizeOf,
PartialEq,
Serialize,
SpecifiedValueInfo,
ToAnimatedZero,
ToComputedValue,
ToResolvedValue,
ToShmem,
)] #[repr(C)] pubstruct SVGPathData( // TODO(emilio): Should probably measure this somehow only from the // specified values. #[ignore_malloc_size_of = "Arc"] pubcrate::ArcSlice<PathCommand>,
);
impl SVGPathData { /// Get the array of PathCommand. #[inline] pubfn commands(&self) -> &[PathCommand] {
&self.0
}
/// Create a normalized copy of this path by converting each relative /// command to an absolute command. pubfn normalize(&self, reduce: bool) -> Self { letmut state = PathTraversalState {
subpath_start: CoordPair::new(0.0, 0.0),
pos: CoordPair::new(0.0, 0.0),
last_command: PathCommand::Close,
last_control: CoordPair::new(0.0, 0.0),
}; let iter = self.0.iter().map(|seg| seg.normalize(&mut state, reduce));
SVGPathData(crate::ArcSlice::from_iter(iter))
}
/// Parse this SVG path string with the argument that indicates whether we should allow the /// empty string. // We cannot use cssparser::Parser to parse a SVG path string because the spec wants to make // the SVG path string as compact as possible. (i.e. The whitespaces may be dropped.) // e.g. "M100 200L100 200" is a valid SVG path string. If we use tokenizer, the first ident // is "M100", instead of "M", and this is not correct. Therefore, we use a Peekable // str::Char iterator to check each character. // // css-shapes-1 says a path data string that does conform but defines an empty path is // invalid and causes the entire path() to be invalid, so we use allow_empty to decide // whether we should allow it. // https://drafts.csswg.org/css-shapes-1/#typedef-basic-shape pubfn parse<'i, 't>(
input: &mut Parser<'i, 't>,
allow_empty: AllowEmpty,
) -> Result<Self, ParseError<'i>> { let location = input.current_source_location(); let path_string = input.expect_string()?.as_ref(); let (path, ok) = Self::parse_bytes(path_string.as_bytes()); if !ok || (allow_empty == AllowEmpty::No && path.0.is_empty()) { return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
} return Ok(path);
}
/// As above, but just parsing the raw byte stream. /// /// Returns the (potentially empty or partial) path, and whether the parsing was ok or we found /// an error. The API is a bit weird because some SVG callers require "parse until first error" /// behavior. pubfn parse_bytes(input: &[u8]) -> (Self, bool) { // Parse the svg path string as multiple sub-paths. letmut ok = true; letmut path_parser = PathParser::new(input);
while skip_wsp(&mut path_parser.chars) { if path_parser.parse_subpath().is_err() {
ok = false; break;
}
}
let path = Self(crate::ArcSlice::from_iter(path_parser.path.into_iter()));
(path, ok)
}
/// Serializes to the path string, potentially including quotes. pubfn to_css<W>(&self, dest: &mut CssWriter<W>, quote: bool) -> fmt::Result where
W: fmt::Write,
{ if quote {
dest.write_char('"')?;
} letmut writer = SequenceWriter::new(dest, " "); for command inself.commands() {
writer.write_item(|inner| command.to_css_for_svg(inner))?;
} if quote {
dest.write_char('"')?;
}
Ok(())
}
}
impl Parse for SVGPathData { fn parse<'i, 't>(
_context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> { // Note that the EBNF allows the path data string in the d property to be empty, so we // don't reject empty SVG path data. // https://svgwg.org/svg2-draft/single-page.html#paths-PathDataBNF
SVGPathData::parse(input, AllowEmpty::Yes)
}
}
// FIXME(emilio): This allocates three copies of the path, that's not // great! Specially, once we're normalized once, we don't need to // re-normalize again. let left = self.normalize(false); let right = other.normalize(false);
impl ComputeSquaredDistance for SVGPathData { fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { ifself.0.len() != other.0.len() { return Err(());
} let left = self.normalize(false); let right = other.normalize(false);
lists::by_computed_value::squared_distance(&left.0, &right.e='color: green'>0)
}
}
/// The SVG path command. /// The fields of these commands are self-explanatory, so we skip the documents. /// Note: the index of the control points, e.g. control1, control2, are mapping to the control /// points of the Bézier curve in the spec. /// /// https://www.w3.org/TR/SVG11/paths.html#PathData pubtype PathCommand = GenericShapeCommand<CSSFloat, CSSFloat>;
// End of string or the next character is a possible new command. if !skip_wsp(&mut $parser.chars) ||
$parser.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) { break;
}
skip_comma_wsp(&mut $parser.chars);
}
Ok(())
}
}
}
/// Parse a sub-path. fn parse_subpath(&mutself) -> Result<(), ()> { // Handle "moveto" Command first. If there is no "moveto", this is not a valid sub-path // (i.e. not a valid moveto-drawto-command-group). self.parse_moveto()?;
// Handle other commands. loop {
skip_wsp(&mutself.chars); ifself.chars.peek().map_or(true, |&m| m == b'M' || m == b'm') { break;
}
let command = self.chars.next().unwrap(); let by_to = if command.is_ascii_uppercase() {
ByTo::To
} else {
ByTo::By
};
/// Parse "moveto" command. fn parse_moveto(&mutself) -> Result<(), ()> { let command = matchself.chars.next() {
Some(c) if c == b'M' || c == b'm' => c,
_ => return Err(()),
};
skip_wsp(&mutself.chars); let point = parse_coord(&mutself.chars)?; let by_to = if command == b'M' { ByTo::To } else { ByTo::By }; self.path.push(PathCommand::Move { by_to, point });
// End of string or the next character is a possible new command. if !skip_wsp(&mutself.chars) || self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic())
{ return Ok(());
}
skip_comma_wsp(&mutself.chars);
// If a moveto is followed by multiple pairs of coordinates, the subsequent // pairs are treated as implicit lineto commands. self.parse_lineto(by_to)
}
/// Parse elliptical arc curve command. fn parse_elliptical_arc(&mutself, by_to: ByTo) -> Result<(), ()> { // Parse a flag whose value is '0' or '1'; otherwise, return Err(()). let parse_arc_size = |iter: &mut Peekable<Cloned<slice::Iter<u8>>>| match iter.next() {
Some(c) if c == b'1' => Ok(ArcSize::Large),
Some(c) if c == b'0' => Ok(ArcSize::Small),
_ => Err(()),
}; let parse_arc_sweep = |iter: &mut Peekable<Cloned<slice::Iter<u8>>>| match iter.next() {
Some(c) if c == b'1' => Ok(ArcSweep::Cw),
Some(c) if c == b'0' => Ok(ArcSweep::Ccw),
_ => Err(()),
};
parse_arguments!(self, by_to, Arc, [
radii => parse_coord,
rotate => parse_number,
arc_size => parse_arc_size,
arc_sweep => parse_arc_sweep,
point => parse_coord
])
}
}
/// Parse a pair of numbers into CoordPair. fn parse_coord(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<CoordPair, ()> { let x = parse_number(iter)?;
skip_comma_wsp(iter); let y = parse_number(iter)?;
Ok(CoordPair::new(x, y))
}
/// This is a special version which parses the number for SVG Path. e.g. "M 0.6.5" should be parsed /// as MoveTo with a coordinate of ("0.6", ".5"), instead of treating 0.6.5 as a non-valid floating /// point number. In other words, the logic here is similar with that of /// tokenizer::consume_numeric, which also consumes the number as many as possible, but here the /// input is a Peekable and we only accept an integer of a floating point number. /// /// The "number" syntax in https://www.w3.org/TR/SVG/paths.html#PathDataBNF fn parse_number(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<CSSFloat, ()> { // 1. Check optional sign. let sign = if iter
.peek()
.map_or(false, |&sign| sign == b'+' || sign == b'-')
{ if iter.next().unwrap() == b'-' {
-1.
} else { 1.
}
} else { 1.
};
// 2. Check integer part. letmut integral_part: f64 = 0.; let got_dot = if !iter.peek().map_or(false, |&n| n == b'.') { // If the first digit in integer part is neither a dot nor a digit, this is not a number. if iter.peek().map_or(true, |n| !n.is_ascii_digit()) { return Err(());
}
while iter.peek().map_or(false, |n| n.is_ascii_digit()) {
integral_part = integral_part * 10. + (iter.next().unwrap() - b'0') as f64;
}
iter.peek().map_or(false, |&n| n == b'.')
} else { true
};
// 3. Check fractional part. letmut fractional_part: f64 = 0.; if got_dot { // Consume '.'.
iter.next(); // If the first digit in fractional part is not a digit, this is not a number. if iter.peek().map_or(true, |n| !n.is_ascii_digit()) { return Err(());
}
letmut value = sign * (integral_part + fractional_part);
// 4. Check exp part. The segment name of SVG Path doesn't include 'E' or 'e', so it's ok to // treat the numbers after 'E' or 'e' are in the exponential part. if iter.peek().map_or(false, |&exp| exp == b'E' || exp == b'e') { // Consume 'E' or 'e'.
iter.next(); let exp_sign = if iter
.peek()
.map_or(false, |&sign| sign == b'+' || sign == b'-')
{ if iter.next().unwrap() == b'-' {
-1.
} else { 1.
}
} else { 1.
};
if value.is_finite() {
Ok(value.min(f32::MAX as f64).max(f32::MIN as f64) as CSSFloat)
} else {
Err(())
}
}
/// Skip all svg whitespaces, and return true if |iter| hasn't finished. #[inline] fn skip_wsp(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> bool { // Note: SVG 1.1 defines the whitespaces as \u{9}, \u{20}, \u{A}, \u{D}. // However, SVG 2 has one extra whitespace: \u{C}. // Therefore, we follow the newest spec for the definition of whitespace, // i.e. \u{9}, \u{20}, \u{A}, \u{C}, \u{D}. while iter.peek().map_or(false, |c| c.is_ascii_whitespace()) {
iter.next();
}
iter.peek().is_some()
}
/// Skip all svg whitespaces and one comma, and return true if |iter| hasn't finished. #[inline] fn skip_comma_wsp(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> bool { if !skip_wsp(iter) { returnfalse;
}
if *iter.peek().unwrap() != b',' { returntrue;
}
iter.next();
skip_wsp(iter)
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.27 Sekunden
(vorverarbeitet am 2026-06-20)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.