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