Skip to main content

style/values/specified/
svg_path.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Specified types for SVG Path.
6
7use crate::derives::*;
8use crate::parser::{Parse, ParserContext};
9use crate::values::CSSFloat;
10use crate::values::animated::{Animate, Procedure, lists};
11use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
12use crate::values::generics::basic_shape::GenericShapeCommand;
13use crate::values::generics::basic_shape::{
14    ArcRadii, ArcSize, ArcSweep, AxisEndPoint, CommandEndPoint, ControlPoint, ControlReference,
15    CoordinatePair, RelativeControlPoint,
16};
17use crate::values::generics::position::GenericPosition;
18use cssparser::Parser;
19use std::fmt::{self, Write};
20use std::iter::{Cloned, Peekable};
21use std::ops;
22use std::slice;
23use style_traits::values::SequenceWriter;
24use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
25
26/// Whether to allow empty string in the parser.
27#[derive(Clone, Debug, Eq, PartialEq)]
28#[allow(missing_docs)]
29pub enum AllowEmpty {
30    Yes,
31    No,
32}
33
34/// The SVG path data.
35///
36/// https://www.w3.org/TR/SVG11/paths.html#PathData
37#[derive(
38    Clone,
39    Debug,
40    Deserialize,
41    MallocSizeOf,
42    PartialEq,
43    Serialize,
44    SpecifiedValueInfo,
45    ToAnimatedZero,
46    ToComputedValue,
47    ToResolvedValue,
48    ToShmem,
49)]
50#[repr(C)]
51pub struct SVGPathData(
52    // TODO(emilio): Should probably measure this somehow only from the
53    // specified values.
54    #[ignore_malloc_size_of = "Arc"] pub crate::ArcSlice<PathCommand>,
55);
56
57impl SVGPathData {
58    /// Get the array of PathCommand.
59    #[inline]
60    pub fn commands(&self) -> &[PathCommand] {
61        &self.0
62    }
63
64    /// Create a normalized copy of this path by converting each relative
65    /// command to an absolute command.
66    pub fn normalize(&self, reduce: bool) -> Self {
67        let mut state = PathTraversalState {
68            subpath_start: CoordPair::new(0.0, 0.0),
69            pos: CoordPair::new(0.0, 0.0),
70            last_command: PathCommand::Close,
71            last_control: CoordPair::new(0.0, 0.0),
72        };
73        let iter = self.0.iter().map(|seg| seg.normalize(&mut state, reduce));
74        SVGPathData(crate::ArcSlice::from_iter(iter))
75    }
76
77    /// Parse this SVG path string with the argument that indicates whether we should allow the
78    /// empty string.
79    // We cannot use cssparser::Parser to parse a SVG path string because the spec wants to make
80    // the SVG path string as compact as possible. (i.e. The whitespaces may be dropped.)
81    // e.g. "M100 200L100 200" is a valid SVG path string. If we use tokenizer, the first ident
82    // is "M100", instead of "M", and this is not correct. Therefore, we use a Peekable
83    // str::Char iterator to check each character.
84    //
85    // css-shapes-1 says a path data string that does conform but defines an empty path is
86    // invalid and causes the entire path() to be invalid, so we use allow_empty to decide
87    // whether we should allow it.
88    // https://drafts.csswg.org/css-shapes-1/#typedef-basic-shape
89    pub fn parse(input: &mut Parser, allow_empty: AllowEmpty) -> Result<Self, ParseError> {
90        let path_string = input.expect_string()?.as_ref();
91        let (path, ok) = Self::parse_bytes(path_string.as_bytes());
92        if !ok || (allow_empty == AllowEmpty::No && path.0.is_empty()) {
93            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
94        }
95        Ok(path)
96    }
97
98    /// As above, but just parsing the raw byte stream.
99    ///
100    /// Returns the (potentially empty or partial) path, and whether the parsing was ok or we found
101    /// an error. The API is a bit weird because some SVG callers require "parse until first error"
102    /// behavior.
103    pub fn parse_bytes(input: &[u8]) -> (Self, bool) {
104        // Parse the svg path string as multiple sub-paths.
105        let mut ok = true;
106        let mut path_parser = PathParser::new(input);
107
108        while skip_wsp(&mut path_parser.chars) {
109            if path_parser.parse_subpath().is_err() {
110                ok = false;
111                break;
112            }
113        }
114
115        let path = Self(crate::ArcSlice::from_iter(path_parser.path.into_iter()));
116        (path, ok)
117    }
118
119    /// Serializes to the path string, potentially including quotes.
120    pub fn to_css<W>(&self, dest: &mut CssWriter<W>, quote: bool) -> fmt::Result
121    where
122        W: fmt::Write,
123    {
124        if quote {
125            dest.write_char('"')?;
126        }
127        let mut writer = SequenceWriter::new(dest, " ");
128        for command in self.commands() {
129            writer.write_item(|inner| command.to_css_for_svg(inner))?;
130        }
131        if quote {
132            dest.write_char('"')?;
133        }
134        Ok(())
135    }
136}
137
138impl ToCss for SVGPathData {
139    #[inline]
140    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
141    where
142        W: fmt::Write,
143    {
144        self.to_css(dest, /* quote = */ true)
145    }
146}
147
148impl Parse for SVGPathData {
149    fn parse(_context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
150        // Note that the EBNF allows the path data string in the d property to be empty, so we
151        // don't reject empty SVG path data.
152        // https://svgwg.org/svg2-draft/single-page.html#paths-PathDataBNF
153        SVGPathData::parse(input, AllowEmpty::Yes)
154    }
155}
156
157impl Animate for SVGPathData {
158    fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
159        if self.0.len() != other.0.len() {
160            return Err(());
161        }
162
163        // FIXME(emilio): This allocates three copies of the path, that's not
164        // great! Specially, once we're normalized once, we don't need to
165        // re-normalize again.
166        let left = self.normalize(false);
167        let right = other.normalize(false);
168
169        let items: Vec<_> = lists::by_computed_value::animate(&left.0, &right.0, procedure)?;
170        Ok(SVGPathData(crate::ArcSlice::from_iter(items.into_iter())))
171    }
172}
173
174impl ComputeSquaredDistance for SVGPathData {
175    fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
176        if self.0.len() != other.0.len() {
177            return Err(());
178        }
179        let left = self.normalize(false);
180        let right = other.normalize(false);
181        lists::by_computed_value::squared_distance(&left.0, &right.0)
182    }
183}
184
185/// A position type for SVG path coordinates (just a pair of floats).
186pub type SVGPathPosition = GenericPosition<CSSFloat, CSSFloat>;
187
188/// The SVG path command.
189/// The fields of these commands are self-explanatory, so we skip the documents.
190/// Note: the index of the control points, e.g. control1, control2, are mapping to the control
191/// points of the Bézier curve in the spec.
192///
193/// https://www.w3.org/TR/SVG11/paths.html#PathData
194pub type PathCommand = GenericShapeCommand<CSSFloat, CSSFloat, SVGPathPosition, CSSFloat>;
195
196/// For internal SVGPath normalization.
197#[allow(missing_docs)]
198struct PathTraversalState {
199    subpath_start: CoordPair,
200    pos: CoordPair,
201    last_command: PathCommand,
202    last_control: CoordPair,
203}
204
205impl PathCommand {
206    /// Create a normalized copy of this PathCommand. Absolute commands will be copied as-is while
207    /// for relative commands an equivalent absolute command will be returned.
208    ///
209    /// See discussion: https://github.com/w3c/svgwg/issues/321
210    /// If reduce is true then the path will be restricted to
211    /// "M", "L", "C", "A" and "Z" commands.
212    fn normalize(&self, state: &mut PathTraversalState, reduce: bool) -> Self {
213        use crate::values::generics::basic_shape::GenericShapeCommand::*;
214        match *self {
215            Close => {
216                state.pos = state.subpath_start;
217                if reduce {
218                    state.last_command = *self;
219                }
220                Close
221            },
222            Move { mut point } => {
223                point = point.to_abs(state.pos);
224                state.pos = point.into();
225                state.subpath_start = point.into();
226                if reduce {
227                    state.last_command = *self;
228                }
229                Move { point }
230            },
231            Line { mut point } => {
232                point = point.to_abs(state.pos);
233                state.pos = point.into();
234                if reduce {
235                    state.last_command = *self;
236                }
237                Line { point }
238            },
239            HLine { mut x } => {
240                x = x.to_abs(state.pos.x);
241                state.pos.x = x.into();
242                if reduce {
243                    state.last_command = *self;
244                    PathCommand::Line {
245                        point: CommandEndPoint::ToPosition(state.pos.into()),
246                    }
247                } else {
248                    HLine { x }
249                }
250            },
251            VLine { mut y } => {
252                y = y.to_abs(state.pos.y);
253                state.pos.y = y.into();
254                if reduce {
255                    state.last_command = *self;
256                    PathCommand::Line {
257                        point: CommandEndPoint::ToPosition(state.pos.into()),
258                    }
259                } else {
260                    VLine { y }
261                }
262            },
263            CubicCurve {
264                mut point,
265                mut control1,
266                mut control2,
267            } => {
268                control1 = control1.to_abs(state.pos, point);
269                control2 = control2.to_abs(state.pos, point);
270                point = point.to_abs(state.pos);
271                state.pos = point.into();
272                if reduce {
273                    state.last_command = *self;
274                    state.last_control = control2.into();
275                }
276                CubicCurve {
277                    point,
278                    control1,
279                    control2,
280                }
281            },
282            QuadCurve {
283                mut point,
284                mut control1,
285            } => {
286                control1 = control1.to_abs(state.pos, point);
287                point = point.to_abs(state.pos);
288                if reduce {
289                    let c1 = state.pos + 2. * (CoordPair::from(control1) - state.pos) / 3.;
290                    let control2 = CoordPair::from(point)
291                        + 2. * (CoordPair::from(control1) - point.into()) / 3.;
292                    state.pos = point.into();
293                    state.last_command = *self;
294                    state.last_control = control1.into();
295                    CubicCurve {
296                        point,
297                        control1: ControlPoint::Absolute(c1.into()),
298                        control2: ControlPoint::Absolute(control2.into()),
299                    }
300                } else {
301                    state.pos = point.into();
302                    QuadCurve { point, control1 }
303                }
304            },
305            SmoothCubic {
306                mut point,
307                mut control2,
308            } => {
309                control2 = control2.to_abs(state.pos, point);
310                point = point.to_abs(state.pos);
311                if reduce {
312                    let control1 = match state.last_command {
313                        PathCommand::CubicCurve {
314                            point: _,
315                            control1: _,
316                            control2: _,
317                        }
318                        | PathCommand::SmoothCubic {
319                            point: _,
320                            control2: _,
321                        } => state.pos + state.pos - state.last_control,
322                        _ => state.pos,
323                    };
324                    state.pos = point.into();
325                    state.last_control = control2.into();
326                    state.last_command = *self;
327                    CubicCurve {
328                        point,
329                        control1: ControlPoint::Absolute(control1.into()),
330                        control2,
331                    }
332                } else {
333                    state.pos = point.into();
334                    SmoothCubic { point, control2 }
335                }
336            },
337            SmoothQuad { mut point } => {
338                point = point.to_abs(state.pos);
339                if reduce {
340                    let control = match state.last_command {
341                        PathCommand::QuadCurve {
342                            point: _,
343                            control1: _,
344                        }
345                        | PathCommand::SmoothQuad { point: _ } => {
346                            state.pos + state.pos - state.last_control
347                        },
348                        _ => state.pos,
349                    };
350                    let control1 = state.pos + 2. * (control - state.pos) / 3.;
351                    let control2 = CoordPair::from(point) + 2. * (control - point.into()) / 3.;
352                    state.pos = point.into();
353                    state.last_command = *self;
354                    state.last_control = control;
355                    CubicCurve {
356                        point,
357                        control1: ControlPoint::Absolute(control1.into()),
358                        control2: ControlPoint::Absolute(control2.into()),
359                    }
360                } else {
361                    state.pos = point.into();
362                    SmoothQuad { point }
363                }
364            },
365            Arc {
366                mut point,
367                radii,
368                arc_sweep,
369                arc_size,
370                rotate,
371            } => {
372                point = point.to_abs(state.pos);
373                state.pos = point.into();
374                if reduce {
375                    state.last_command = *self;
376                    if radii.rx == 0. && radii.ry.as_ref().is_none_or(|v| *v == 0.) {
377                        let end_point = CoordPair::from(point);
378                        CubicCurve {
379                            point: CommandEndPoint::ToPosition(state.pos.into()),
380                            control1: ControlPoint::Absolute(end_point.into()),
381                            control2: ControlPoint::Absolute(end_point.into()),
382                        }
383                    } else {
384                        Arc {
385                            point,
386                            radii,
387                            arc_sweep,
388                            arc_size,
389                            rotate,
390                        }
391                    }
392                } else {
393                    Arc {
394                        point,
395                        radii,
396                        arc_sweep,
397                        arc_size,
398                        rotate,
399                    }
400                }
401            },
402        }
403    }
404
405    /// The serialization of the svg path.
406    fn to_css_for_svg<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
407    where
408        W: fmt::Write,
409    {
410        use crate::values::generics::basic_shape::GenericShapeCommand::*;
411        match *self {
412            Close => dest.write_char('Z'),
413            Move { point } => {
414                dest.write_char(if point.is_abs() { 'M' } else { 'm' })?;
415                dest.write_char(' ')?;
416                CoordPair::from(point).to_css(dest)
417            },
418            Line { point } => {
419                dest.write_char(if point.is_abs() { 'L' } else { 'l' })?;
420                dest.write_char(' ')?;
421                CoordPair::from(point).to_css(dest)
422            },
423            CubicCurve {
424                point,
425                control1,
426                control2,
427            } => {
428                dest.write_char(if point.is_abs() { 'C' } else { 'c' })?;
429                dest.write_char(' ')?;
430                control1.to_css(dest, point.is_abs())?;
431                dest.write_char(' ')?;
432                control2.to_css(dest, point.is_abs())?;
433                dest.write_char(' ')?;
434                CoordPair::from(point).to_css(dest)
435            },
436            QuadCurve { point, control1 } => {
437                dest.write_char(if point.is_abs() { 'Q' } else { 'q' })?;
438                dest.write_char(' ')?;
439                control1.to_css(dest, point.is_abs())?;
440                dest.write_char(' ')?;
441                CoordPair::from(point).to_css(dest)
442            },
443            Arc {
444                point,
445                radii,
446                arc_sweep,
447                arc_size,
448                rotate,
449            } => {
450                dest.write_char(if point.is_abs() { 'A' } else { 'a' })?;
451                dest.write_char(' ')?;
452                radii.to_css(dest)?;
453                dest.write_char(' ')?;
454                rotate.to_css(dest)?;
455                dest.write_char(' ')?;
456                (arc_size as i32).to_css(dest)?;
457                dest.write_char(' ')?;
458                (arc_sweep as i32).to_css(dest)?;
459                dest.write_char(' ')?;
460                CoordPair::from(point).to_css(dest)
461            },
462            HLine { x } => {
463                dest.write_char(if x.is_abs() { 'H' } else { 'h' })?;
464                dest.write_char(' ')?;
465                CSSFloat::from(x).to_css(dest)
466            },
467            VLine { y } => {
468                dest.write_char(if y.is_abs() { 'V' } else { 'v' })?;
469                dest.write_char(' ')?;
470                CSSFloat::from(y).to_css(dest)
471            },
472            SmoothCubic { point, control2 } => {
473                dest.write_char(if point.is_abs() { 'S' } else { 's' })?;
474                dest.write_char(' ')?;
475                control2.to_css(dest, point.is_abs())?;
476                dest.write_char(' ')?;
477                CoordPair::from(point).to_css(dest)
478            },
479            SmoothQuad { point } => {
480                dest.write_char(if point.is_abs() { 'T' } else { 't' })?;
481                dest.write_char(' ')?;
482                CoordPair::from(point).to_css(dest)
483            },
484        }
485    }
486}
487
488/// The path coord type.
489pub type CoordPair = CoordinatePair<CSSFloat>;
490
491impl ops::Add<CoordPair> for CoordPair {
492    type Output = CoordPair;
493
494    fn add(self, rhs: CoordPair) -> CoordPair {
495        Self {
496            x: self.x + rhs.x,
497            y: self.y + rhs.y,
498        }
499    }
500}
501
502impl ops::Sub<CoordPair> for CoordPair {
503    type Output = CoordPair;
504
505    fn sub(self, rhs: CoordPair) -> CoordPair {
506        Self {
507            x: self.x - rhs.x,
508            y: self.y - rhs.y,
509        }
510    }
511}
512
513impl ops::Mul<CSSFloat> for CoordPair {
514    type Output = CoordPair;
515
516    fn mul(self, f: CSSFloat) -> CoordPair {
517        Self {
518            x: self.x * f,
519            y: self.y * f,
520        }
521    }
522}
523
524impl ops::Mul<CoordPair> for CSSFloat {
525    type Output = CoordPair;
526
527    fn mul(self, rhs: CoordPair) -> CoordPair {
528        rhs * self
529    }
530}
531
532impl ops::Div<CSSFloat> for CoordPair {
533    type Output = CoordPair;
534
535    fn div(self, f: CSSFloat) -> CoordPair {
536        Self {
537            x: self.x / f,
538            y: self.y / f,
539        }
540    }
541}
542
543impl CommandEndPoint<SVGPathPosition, CSSFloat> {
544    /// Converts <command-end-point> into absolutely positioned type.
545    pub fn to_abs(self, state_pos: CoordPair) -> Self {
546        // Consume self value.
547        match self {
548            CommandEndPoint::ToPosition(_) => self,
549            CommandEndPoint::ByCoordinate(coord) => {
550                let pos = GenericPosition {
551                    horizontal: coord.x + state_pos.x,
552                    vertical: coord.y + state_pos.y,
553                };
554                CommandEndPoint::ToPosition(pos)
555            },
556        }
557    }
558}
559
560impl AxisEndPoint<CSSFloat, CSSFloat> {
561    /// Converts possibly relative end point into absolutely positioned type.
562    pub fn to_abs(self, base: CSSFloat) -> AxisEndPoint<CSSFloat, CSSFloat> {
563        // Consume self value.
564        match self {
565            AxisEndPoint::ToPosition(_) => self,
566            AxisEndPoint::ByCoordinate(coord) => AxisEndPoint::ToPosition(coord + base),
567        }
568    }
569}
570
571impl ControlPoint<SVGPathPosition, CSSFloat> {
572    /// Converts <control-point> into absolutely positioned control point type.
573    pub fn to_abs(
574        self,
575        state_pos: CoordPair,
576        end_point: CommandEndPoint<SVGPathPosition, CSSFloat>,
577    ) -> Self {
578        // Consume self value.
579        match self {
580            ControlPoint::Absolute(_) => self,
581            ControlPoint::Relative(point) => {
582                let mut pos = GenericPosition {
583                    horizontal: point.coord.x,
584                    vertical: point.coord.y,
585                };
586
587                match point.reference {
588                    ControlReference::Start => {
589                        pos.horizontal += state_pos.x;
590                        pos.vertical += state_pos.y;
591                    },
592                    ControlReference::End => {
593                        let end = CoordPair::from(end_point);
594                        pos.horizontal += end.x;
595                        pos.vertical += end.y;
596                    },
597                    _ => (),
598                }
599                ControlPoint::Absolute(pos)
600            },
601        }
602    }
603}
604
605impl From<CommandEndPoint<SVGPathPosition, CSSFloat>> for CoordPair {
606    #[inline]
607    fn from(p: CommandEndPoint<SVGPathPosition, CSSFloat>) -> Self {
608        match p {
609            CommandEndPoint::ToPosition(pos) => CoordPair {
610                x: pos.horizontal,
611                y: pos.vertical,
612            },
613            CommandEndPoint::ByCoordinate(coord) => coord,
614        }
615    }
616}
617
618impl From<ControlPoint<SVGPathPosition, CSSFloat>> for CoordPair {
619    #[inline]
620    fn from(point: ControlPoint<SVGPathPosition, CSSFloat>) -> Self {
621        match point {
622            ControlPoint::Absolute(pos) => CoordPair {
623                x: pos.horizontal,
624                y: pos.vertical,
625            },
626            ControlPoint::Relative(_) => {
627                panic!(
628                    "Attempted to convert a relative ControlPoint to CoordPair, which is lossy. \
629                        Consider converting it to absolute type first using `.to_abs()`."
630                )
631            },
632        }
633    }
634}
635
636impl From<CoordPair> for CommandEndPoint<SVGPathPosition, CSSFloat> {
637    #[inline]
638    fn from(coord: CoordPair) -> Self {
639        CommandEndPoint::ByCoordinate(coord)
640    }
641}
642
643impl From<CoordPair> for SVGPathPosition {
644    #[inline]
645    fn from(coord: CoordPair) -> Self {
646        GenericPosition {
647            horizontal: coord.x,
648            vertical: coord.y,
649        }
650    }
651}
652
653impl From<AxisEndPoint<CSSFloat, CSSFloat>> for CSSFloat {
654    #[inline]
655    fn from(p: AxisEndPoint<CSSFloat, CSSFloat>) -> Self {
656        match p {
657            AxisEndPoint::ToPosition(a) | AxisEndPoint::ByCoordinate(a) => a,
658        }
659    }
660}
661
662impl ToCss for SVGPathPosition {
663    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
664    where
665        W: Write,
666    {
667        self.horizontal.to_css(dest)?;
668        dest.write_char(' ')?;
669        self.vertical.to_css(dest)
670    }
671}
672
673/// SVG Path parser.
674struct PathParser<'a> {
675    chars: Peekable<Cloned<slice::Iter<'a, u8>>>,
676    path: Vec<PathCommand>,
677}
678
679macro_rules! parse_arguments {
680    (
681        $parser:ident,
682        $enum:ident,
683        $( $field:ident : $value:expr, )*
684        [ $para:ident => $func:ident $(, $other_para:ident => $other_func:ident)* ]
685    ) => {
686        {
687            loop {
688                let $para = $func(&mut $parser.chars)?;
689                $(
690                    skip_comma_wsp(&mut $parser.chars);
691                    let $other_para = $other_func(&mut $parser.chars)?;
692                )*
693                $parser.path.push(
694                    PathCommand::$enum { $( $field: $value, )* $para $(, $other_para)* }
695                );
696
697                // End of string or the next character is a possible new command.
698                if !skip_wsp(&mut $parser.chars) ||
699                   $parser.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) {
700                    break;
701                }
702                skip_comma_wsp(&mut $parser.chars);
703            }
704            Ok(())
705        }
706    }
707}
708
709impl<'a> PathParser<'a> {
710    /// Return a PathParser.
711    #[inline]
712    fn new(bytes: &'a [u8]) -> Self {
713        PathParser {
714            chars: bytes.iter().cloned().peekable(),
715            // Preallocate space for an estimated number of commands, based on
716            // the length of our byte stream.  For this preallocation, we
717            // heuristically assume that our path's commands will be around
718            // 32 bytes long each, on average. (This estimate doesn't have to
719            // be exact; and to the extent that it's wrong, we'd rather have it
720            // be an over-estimate, so that our reserved capacity will tend to
721            // be an under-estimate, so that we're not greedily wasting space.)
722            path: Vec::with_capacity(bytes.len() / 32),
723        }
724    }
725
726    /// Parse a sub-path.
727    fn parse_subpath(&mut self) -> Result<(), ()> {
728        // Handle "moveto" Command first. If there is no "moveto", this is not a valid sub-path
729        // (i.e. not a valid moveto-drawto-command-group).
730        self.parse_moveto()?;
731
732        // Handle other commands.
733        loop {
734            skip_wsp(&mut self.chars);
735            if self.chars.peek().is_none_or(|&m| m == b'M' || m == b'm') {
736                break;
737            }
738
739            let command = self.chars.next().unwrap();
740
741            skip_wsp(&mut self.chars);
742            match command {
743                b'Z' | b'z' => self.parse_closepath(),
744                b'L' => self.parse_line_abs(),
745                b'l' => self.parse_line_rel(),
746                b'H' => self.parse_h_line_abs(),
747                b'h' => self.parse_h_line_rel(),
748                b'V' => self.parse_v_line_abs(),
749                b'v' => self.parse_v_line_rel(),
750                b'C' => self.parse_curve_abs(),
751                b'c' => self.parse_curve_rel(),
752                b'S' => self.parse_smooth_curve_abs(),
753                b's' => self.parse_smooth_curve_rel(),
754                b'Q' => self.parse_quadratic_bezier_curve_abs(),
755                b'q' => self.parse_quadratic_bezier_curve_rel(),
756                b'T' => self.parse_smooth_quadratic_bezier_curve_abs(),
757                b't' => self.parse_smooth_quadratic_bezier_curve_rel(),
758                b'A' => self.parse_elliptical_arc_abs(),
759                b'a' => self.parse_elliptical_arc_rel(),
760                _ => return Err(()),
761            }?;
762        }
763        Ok(())
764    }
765
766    /// Parse "moveto" command.
767    fn parse_moveto(&mut self) -> Result<(), ()> {
768        let command = match self.chars.next() {
769            Some(c) if c == b'M' || c == b'm' => c,
770            _ => return Err(()),
771        };
772
773        skip_wsp(&mut self.chars);
774        let point = if command == b'M' {
775            parse_command_end_abs(&mut self.chars)
776        } else {
777            parse_command_end_rel(&mut self.chars)
778        }?;
779        self.path.push(PathCommand::Move { point });
780
781        // End of string or the next character is a possible new command.
782        if !skip_wsp(&mut self.chars) || self.chars.peek().is_none_or(|c| c.is_ascii_alphabetic()) {
783            return Ok(());
784        }
785        skip_comma_wsp(&mut self.chars);
786
787        // If a moveto is followed by multiple pairs of coordinates, the subsequent
788        // pairs are treated as implicit lineto commands.
789        if point.is_abs() {
790            self.parse_line_abs()
791        } else {
792            self.parse_line_rel()
793        }
794    }
795
796    /// Parse "closepath" command.
797    fn parse_closepath(&mut self) -> Result<(), ()> {
798        self.path.push(PathCommand::Close);
799        Ok(())
800    }
801
802    /// Parse an absolute "lineto" ("L") command.
803    fn parse_line_abs(&mut self) -> Result<(), ()> {
804        parse_arguments!(self, Line, [ point => parse_command_end_abs ])
805    }
806
807    /// Parse a relative "lineto" ("l") command.
808    fn parse_line_rel(&mut self) -> Result<(), ()> {
809        parse_arguments!(self, Line, [ point => parse_command_end_rel ])
810    }
811
812    /// Parse an absolute horizontal "lineto" ("H") command.
813    fn parse_h_line_abs(&mut self) -> Result<(), ()> {
814        parse_arguments!(self, HLine, [ x => parse_axis_end_abs ])
815    }
816
817    /// Parse a relative horizontal "lineto" ("h") command.
818    fn parse_h_line_rel(&mut self) -> Result<(), ()> {
819        parse_arguments!(self, HLine, [ x => parse_axis_end_rel ])
820    }
821
822    /// Parse an absolute vertical "lineto" ("V") command.
823    fn parse_v_line_abs(&mut self) -> Result<(), ()> {
824        parse_arguments!(self, VLine, [ y => parse_axis_end_abs ])
825    }
826
827    /// Parse a relative vertical "lineto" ("v") command.
828    fn parse_v_line_rel(&mut self) -> Result<(), ()> {
829        parse_arguments!(self, VLine, [ y => parse_axis_end_rel ])
830    }
831
832    /// Parse an absolute cubic Bézier curve ("C") command.
833    fn parse_curve_abs(&mut self) -> Result<(), ()> {
834        parse_arguments!(self, CubicCurve, [
835            control1 => parse_control_point_abs, control2 => parse_control_point_abs, point => parse_command_end_abs
836        ])
837    }
838
839    /// Parse a relative cubic Bézier curve ("c") command.
840    fn parse_curve_rel(&mut self) -> Result<(), ()> {
841        parse_arguments!(self, CubicCurve, [
842            control1 => parse_control_point_rel, control2 => parse_control_point_rel, point => parse_command_end_rel
843        ])
844    }
845
846    /// Parse an absolute smooth "curveto" ("S") command.
847    fn parse_smooth_curve_abs(&mut self) -> Result<(), ()> {
848        parse_arguments!(self, SmoothCubic, [
849            control2 => parse_control_point_abs, point => parse_command_end_abs
850        ])
851    }
852
853    /// Parse a relative smooth "curveto" ("s") command.
854    fn parse_smooth_curve_rel(&mut self) -> Result<(), ()> {
855        parse_arguments!(self, SmoothCubic, [
856            control2 => parse_control_point_rel, point => parse_command_end_rel
857        ])
858    }
859
860    /// Parse an absolute quadratic Bézier curve ("Q") command.
861    fn parse_quadratic_bezier_curve_abs(&mut self) -> Result<(), ()> {
862        parse_arguments!(self, QuadCurve, [
863            control1 => parse_control_point_abs, point => parse_command_end_abs
864        ])
865    }
866
867    /// Parse a relative quadratic Bézier curve ("q") command.
868    fn parse_quadratic_bezier_curve_rel(&mut self) -> Result<(), ()> {
869        parse_arguments!(self, QuadCurve, [
870            control1 => parse_control_point_rel, point => parse_command_end_rel
871        ])
872    }
873
874    /// Parse an absolute smooth quadratic Bézier curveto ("T") command.
875    fn parse_smooth_quadratic_bezier_curve_abs(&mut self) -> Result<(), ()> {
876        parse_arguments!(self, SmoothQuad, [ point => parse_command_end_abs ])
877    }
878
879    /// Parse a relative smooth quadratic Bézier curveto ("t") command.
880    fn parse_smooth_quadratic_bezier_curve_rel(&mut self) -> Result<(), ()> {
881        parse_arguments!(self, SmoothQuad, [ point => parse_command_end_rel ])
882    }
883
884    /// Parse an absolute elliptical arc curve ("A") command.
885    fn parse_elliptical_arc_abs(&mut self) -> Result<(), ()> {
886        let (parse_arc_size, parse_arc_sweep) = Self::arc_flag_parsers();
887        parse_arguments!(self, Arc, [
888            radii => parse_arc_radii,
889            rotate => parse_number,
890            arc_size => parse_arc_size,
891            arc_sweep => parse_arc_sweep,
892            point => parse_command_end_abs
893        ])
894    }
895
896    /// Parse a relative elliptical arc curve ("a") command.
897    fn parse_elliptical_arc_rel(&mut self) -> Result<(), ()> {
898        let (parse_arc_size, parse_arc_sweep) = Self::arc_flag_parsers();
899        parse_arguments!(self, Arc, [
900            radii => parse_arc_radii,
901            rotate => parse_number,
902            arc_size => parse_arc_size,
903            arc_sweep => parse_arc_sweep,
904            point => parse_command_end_rel
905        ])
906    }
907
908    /// Helper that returns parsers for the arc-size and arc-sweep flags.
909    fn arc_flag_parsers() -> (
910        impl Fn(&mut Peekable<Cloned<slice::Iter<'_, u8>>>) -> Result<ArcSize, ()>,
911        impl Fn(&mut Peekable<Cloned<slice::Iter<'_, u8>>>) -> Result<ArcSweep, ()>,
912    ) {
913        // Parse a flag whose value is '0' or '1'; otherwise, return Err(()).
914        let parse_arc_size = |iter: &mut Peekable<Cloned<slice::Iter<u8>>>| match iter.next() {
915            Some(c) if c == b'1' => Ok(ArcSize::Large),
916            Some(c) if c == b'0' => Ok(ArcSize::Small),
917            _ => Err(()),
918        };
919        let parse_arc_sweep = |iter: &mut Peekable<Cloned<slice::Iter<u8>>>| match iter.next() {
920            Some(c) if c == b'1' => Ok(ArcSweep::Cw),
921            Some(c) if c == b'0' => Ok(ArcSweep::Ccw),
922            _ => Err(()),
923        };
924        (parse_arc_size, parse_arc_sweep)
925    }
926}
927
928/// Parse a pair of numbers into CoordPair.
929fn parse_coord(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<CoordPair, ()> {
930    let x = parse_number(iter)?;
931    skip_comma_wsp(iter);
932    let y = parse_number(iter)?;
933    Ok(CoordPair::new(x, y))
934}
935
936/// Parse a pair of numbers that describes the absolutely positioned end point.
937fn parse_command_end_abs(
938    iter: &mut Peekable<Cloned<slice::Iter<u8>>>,
939) -> Result<CommandEndPoint<SVGPathPosition, CSSFloat>, ()> {
940    let coord = parse_coord(iter)?;
941    Ok(CommandEndPoint::ToPosition(coord.into()))
942}
943
944/// Parse a pair of numbers that describes the relatively positioned end point.
945fn parse_command_end_rel(
946    iter: &mut Peekable<Cloned<slice::Iter<u8>>>,
947) -> Result<CommandEndPoint<SVGPathPosition, CSSFloat>, ()> {
948    let coord = parse_coord(iter)?;
949    Ok(CommandEndPoint::ByCoordinate(coord))
950}
951
952/// Parse a pair of values that describe the absolutely positioned curve control point.
953fn parse_control_point_abs(
954    iter: &mut Peekable<Cloned<slice::Iter<u8>>>,
955) -> Result<ControlPoint<SVGPathPosition, CSSFloat>, ()> {
956    let coord = parse_coord(iter)?;
957    Ok(ControlPoint::Relative(RelativeControlPoint {
958        coord,
959        reference: ControlReference::Origin,
960    }))
961}
962
963/// Parse a pair of values that describe the relatively positioned curve control point.
964fn parse_control_point_rel(
965    iter: &mut Peekable<Cloned<slice::Iter<u8>>>,
966) -> Result<ControlPoint<SVGPathPosition, CSSFloat>, ()> {
967    let coord = parse_coord(iter)?;
968    Ok(ControlPoint::Relative(RelativeControlPoint {
969        coord,
970        reference: ControlReference::Start,
971    }))
972}
973
974/// Parse a number that describes the absolutely positioned axis end point.
975fn parse_axis_end_abs(
976    iter: &mut Peekable<Cloned<slice::Iter<u8>>>,
977) -> Result<AxisEndPoint<f32, f32>, ()> {
978    let value = parse_number(iter)?;
979    Ok(AxisEndPoint::ToPosition(value))
980}
981
982/// Parse a number that describes the relatively positioned axis end point.
983fn parse_axis_end_rel(
984    iter: &mut Peekable<Cloned<slice::Iter<u8>>>,
985) -> Result<AxisEndPoint<f32, f32>, ()> {
986    let value = parse_number(iter)?;
987    Ok(AxisEndPoint::ByCoordinate(value))
988}
989
990/// Parse a pair of numbers that describes the size of the ellipse that the arc is taken from.
991fn parse_arc_radii(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<ArcRadii<CSSFloat>, ()> {
992    let coord = parse_coord(iter)?;
993    Ok(ArcRadii {
994        rx: coord.x,
995        ry: Some(coord.y).into(),
996    })
997}
998
999/// This is a special version which parses the number for SVG Path. e.g. "M 0.6.5" should be parsed
1000/// as MoveTo with a coordinate of ("0.6", ".5"), instead of treating 0.6.5 as a non-valid floating
1001/// point number. In other words, the logic here is similar with that of
1002/// tokenizer::consume_numeric, which also consumes the number as many as possible, but here the
1003/// input is a Peekable and we only accept an integer of a floating point number.
1004///
1005/// The "number" syntax in https://www.w3.org/TR/SVG/paths.html#PathDataBNF
1006fn parse_number(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<CSSFloat, ()> {
1007    // 1. Check optional sign.
1008    let sign = if iter
1009        .peek()
1010        .is_some_and(|&sign| sign == b'+' || sign == b'-')
1011    {
1012        if iter.next().unwrap() == b'-' {
1013            -1.
1014        } else {
1015            1.
1016        }
1017    } else {
1018        1.
1019    };
1020
1021    // 2. Check integer part.
1022    let mut integral_part: f64 = 0.;
1023    let got_dot = if iter.peek().is_none_or(|&n| n != b'.') {
1024        // If the first digit in integer part is neither a dot nor a digit, this is not a number.
1025        if iter.peek().is_none_or(|n| !n.is_ascii_digit()) {
1026            return Err(());
1027        }
1028
1029        while iter.peek().is_some_and(|n| n.is_ascii_digit()) {
1030            integral_part = integral_part * 10. + (iter.next().unwrap() - b'0') as f64;
1031        }
1032
1033        iter.peek().is_some_and(|&n| n == b'.')
1034    } else {
1035        true
1036    };
1037
1038    // 3. Check fractional part.
1039    let mut fractional_part: f64 = 0.;
1040    if got_dot {
1041        // Consume '.'.
1042        iter.next();
1043        // If the first digit in fractional part is not a digit, this is not a number.
1044        if iter.peek().is_none_or(|n| !n.is_ascii_digit()) {
1045            return Err(());
1046        }
1047
1048        let mut factor = 0.1;
1049        while iter.peek().is_some_and(|n| n.is_ascii_digit()) {
1050            fractional_part += (iter.next().unwrap() - b'0') as f64 * factor;
1051            factor *= 0.1;
1052        }
1053    }
1054
1055    let mut value = sign * (integral_part + fractional_part);
1056
1057    // 4. Check exp part. The segment name of SVG Path doesn't include 'E' or 'e', so it's ok to
1058    //    treat the numbers after 'E' or 'e' are in the exponential part.
1059    if iter.peek().is_some_and(|&exp| exp == b'E' || exp == b'e') {
1060        // Consume 'E' or 'e'.
1061        iter.next();
1062        let exp_sign = if iter
1063            .peek()
1064            .is_some_and(|&sign| sign == b'+' || sign == b'-')
1065        {
1066            if iter.next().unwrap() == b'-' {
1067                -1.
1068            } else {
1069                1.
1070            }
1071        } else {
1072            1.
1073        };
1074
1075        let mut exp: f64 = 0.;
1076        while iter.peek().is_some_and(|n| n.is_ascii_digit()) {
1077            exp = exp * 10. + (iter.next().unwrap() - b'0') as f64;
1078        }
1079
1080        value *= f64::powf(10., exp * exp_sign);
1081    }
1082
1083    if value.is_finite() {
1084        Ok(value.min(f32::MAX as f64).max(f32::MIN as f64) as CSSFloat)
1085    } else {
1086        Err(())
1087    }
1088}
1089
1090/// Skip all svg whitespaces, and return true if |iter| hasn't finished.
1091#[inline]
1092fn skip_wsp(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> bool {
1093    // Note: SVG 1.1 defines the whitespaces as \u{9}, \u{20}, \u{A}, \u{D}.
1094    //       However, SVG 2 has one extra whitespace: \u{C}.
1095    //       Therefore, we follow the newest spec for the definition of whitespace,
1096    //       i.e. \u{9}, \u{20}, \u{A}, \u{C}, \u{D}.
1097    while iter.peek().is_some_and(|c| c.is_ascii_whitespace()) {
1098        iter.next();
1099    }
1100    iter.peek().is_some()
1101}
1102
1103/// Skip all svg whitespaces and one comma, and return true if |iter| hasn't finished.
1104#[inline]
1105fn skip_comma_wsp(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> bool {
1106    if !skip_wsp(iter) {
1107        return false;
1108    }
1109
1110    if *iter.peek().unwrap() != b',' {
1111        return true;
1112    }
1113    iter.next();
1114
1115    skip_wsp(iter)
1116}