Skip to main content

rsvg/
paint_server.rs

1//! SVG paint servers.
2
3use std::rc::Rc;
4
5use cssparser::{ParseErrorKind, Parser};
6
7use crate::color::{Color, resolve_color};
8use crate::document::{AcquiredNodes, NodeId};
9use crate::drawing_ctx::Viewport;
10use crate::element::ElementData;
11use crate::error::{AcquireError, NodeIdError, ParseError, ValueErrorKind};
12use crate::gradient::{ResolvedGradient, UserSpaceGradient};
13use crate::length::NormalizeValues;
14use crate::node::NodeBorrow;
15use crate::parsers::Parse;
16use crate::pattern::{ResolvedPattern, UserSpacePattern};
17use crate::rect::Rect;
18use crate::rsvg_log;
19use crate::session::Session;
20use crate::unit_interval::UnitInterval;
21
22/// Unresolved SVG paint server straight from the DOM data.
23///
24/// This is either a solid color (which if `currentColor` needs to be extracted from the
25/// `ComputedValues`), or a paint server like a gradient or pattern which is referenced by
26/// a URL that points to a certain document node.
27///
28/// Use [`PaintServer.resolve`](#method.resolve) to turn this into a [`PaintSource`].
29#[derive(Debug, Clone, PartialEq)]
30pub enum PaintServer {
31    /// For example, `fill="none"`.
32    None,
33
34    /// For example, `fill="url(#some_gradient) fallback_color"`.
35    Iri {
36        iri: Box<NodeId>,
37        alternate: Option<Color>,
38    },
39
40    /// For example, `fill="blue"`.
41    SolidColor(Color),
42
43    /// For example, `fill="context-fill"`
44    ContextFill,
45
46    /// For example, `fill="context-stroke"`
47    ContextStroke,
48}
49
50/// Paint server with resolved references, with unnormalized lengths.
51///
52/// Use [`PaintSource.to_user_space`](#method.to_user_space) to turn this into a
53/// [`UserSpacePaintSource`].
54pub enum PaintSource {
55    None,
56    Gradient(ResolvedGradient, Option<Color>),
57    Pattern(ResolvedPattern, Option<Color>),
58    SolidColor(Color),
59}
60
61/// Fully resolved paint server, in user-space units.
62///
63/// This has everything required for rendering.
64pub enum UserSpacePaintSource {
65    None,
66    Gradient(UserSpaceGradient, Option<Color>),
67    Pattern(UserSpacePattern, Option<Color>),
68    SolidColor(Color),
69}
70
71impl Parse for PaintServer {
72    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<PaintServer, ParseError<'i>> {
73        if parser
74            .try_parse(|i| i.expect_ident_matching("none"))
75            .is_ok()
76        {
77            Ok(PaintServer::None)
78        } else if parser
79            .try_parse(|i| i.expect_ident_matching("context-fill"))
80            .is_ok()
81        {
82            Ok(PaintServer::ContextFill)
83        } else if parser
84            .try_parse(|i| i.expect_ident_matching("context-stroke"))
85            .is_ok()
86        {
87            Ok(PaintServer::ContextStroke)
88        } else if let Ok(url) = parser.try_parse(|i| i.expect_url()) {
89            let loc = parser.current_source_location();
90
91            let alternate = if !parser.is_exhausted() {
92                if parser
93                    .try_parse(|i| i.expect_ident_matching("none"))
94                    .is_ok()
95                {
96                    None
97                } else {
98                    Some(parser.try_parse(Color::parse).map_err(|e| ParseError {
99                        kind: ParseErrorKind::Custom(ValueErrorKind::parse_error(
100                            "Could not parse color",
101                        )),
102                        location: e.location,
103                    })?)
104                }
105            } else {
106                None
107            };
108
109            Ok(PaintServer::Iri {
110                iri: Box::new(
111                    NodeId::parse(&url)
112                        .map_err(|e: NodeIdError| -> ValueErrorKind { e.into() })
113                        .map_err(|e| loc.new_custom_error(e))?,
114                ),
115                alternate,
116            })
117        } else {
118            <Color as Parse>::parse(parser).map(PaintServer::SolidColor)
119        }
120    }
121}
122
123impl PaintServer {
124    /// Resolves colors, plus node references for gradients and patterns.
125    ///
126    /// `opacity` depends on `strokeOpacity` or `fillOpacity` depending on whether
127    /// the paint server is for the `stroke` or `fill` properties.
128    ///
129    /// `current_color` should be the value of `ComputedValues.color()`.
130    ///
131    /// After a paint server is resolved, the resulting [`PaintSource`] can be used in
132    /// many places: for an actual shape, or for the `context-fill` of a marker for that
133    /// shape.  Therefore, this returns an [`Rc`] so that the `PaintSource` may be shared
134    /// easily.
135    pub fn resolve(
136        &self,
137        acquired_nodes: &mut AcquiredNodes<'_>,
138        referencing_element_name: &str,
139        opacity: UnitInterval,
140        current_color: Color,
141        context_fill: Option<Rc<PaintSource>>,
142        context_stroke: Option<Rc<PaintSource>>,
143        session: &Session,
144    ) -> Rc<PaintSource> {
145        match self {
146            PaintServer::Iri { iri, alternate } => acquired_nodes
147                .acquire(referencing_element_name, iri)
148                .and_then(|acquired| {
149                    let node = acquired.get();
150                    assert!(node.is_element());
151
152                    match *node.borrow_element_data() {
153                        ElementData::LinearGradient(ref g) => {
154                            g.resolve(node, acquired_nodes, opacity, session).map(|g| {
155                                Rc::new(PaintSource::Gradient(
156                                    g,
157                                    alternate.map(|c| resolve_color(&c, opacity, &current_color)),
158                                ))
159                            })
160                        }
161                        ElementData::Pattern(ref p) => {
162                            p.resolve(node, acquired_nodes, opacity, session).map(|p| {
163                                Rc::new(PaintSource::Pattern(
164                                    p,
165                                    alternate.map(|c| resolve_color(&c, opacity, &current_color)),
166                                ))
167                            })
168                        }
169                        ElementData::RadialGradient(ref g) => {
170                            g.resolve(node, acquired_nodes, opacity, session).map(|g| {
171                                Rc::new(PaintSource::Gradient(
172                                    g,
173                                    alternate.map(|c| resolve_color(&c, opacity, &current_color)),
174                                ))
175                            })
176                        }
177                        _ => {
178                            rsvg_log!(session, "{node} is not a gradient or pattern, ignoring");
179                            Err(AcquireError::InvalidLinkType(iri.as_ref().clone()))
180                        }
181                    }
182                })
183                .unwrap_or_else(|_| match alternate {
184                    // The following cases catch AcquireError::CircularReference and
185                    // AcquireError::MaxReferencesExceeded.
186                    //
187                    // Circular references mean that there is a pattern or gradient with a
188                    // reference cycle in its "href" attribute.  This is an invalid paint
189                    // server, and per
190                    // https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint we should
191                    // try to fall back to the alternate color.
192                    //
193                    // Exceeding the maximum number of references will get caught again
194                    // later in the drawing code, so it should be fine to translate this
195                    // condition to that for an invalid paint server.
196                    Some(color) => {
197                        rsvg_log!(
198                            session,
199                            "could not resolve paint server \"{}\", using alternate color",
200                            iri
201                        );
202
203                        Rc::new(PaintSource::SolidColor(resolve_color(
204                            color,
205                            opacity,
206                            &current_color,
207                        )))
208                    }
209
210                    None => {
211                        rsvg_log!(
212                            session,
213                            "could not resolve paint server \"{}\", no alternate color specified",
214                            iri
215                        );
216
217                        Rc::new(PaintSource::None)
218                    }
219                }),
220
221            PaintServer::SolidColor(color) => Rc::new(PaintSource::SolidColor(resolve_color(
222                color,
223                opacity,
224                &current_color,
225            ))),
226
227            PaintServer::ContextFill => {
228                if let Some(paint) = context_fill {
229                    paint
230                } else {
231                    Rc::new(PaintSource::None)
232                }
233            }
234
235            PaintServer::ContextStroke => {
236                if let Some(paint) = context_stroke {
237                    paint
238                } else {
239                    Rc::new(PaintSource::None)
240                }
241            }
242
243            PaintServer::None => Rc::new(PaintSource::None),
244        }
245    }
246}
247
248impl PaintSource {
249    /// Converts lengths to user-space.
250    pub fn to_user_space(
251        &self,
252        object_bbox: &Option<Rect>,
253        viewport: &Viewport,
254        values: &NormalizeValues,
255    ) -> UserSpacePaintSource {
256        match *self {
257            PaintSource::None => UserSpacePaintSource::None,
258            PaintSource::SolidColor(c) => UserSpacePaintSource::SolidColor(c),
259
260            PaintSource::Gradient(ref g, c) => {
261                match (g.to_user_space(object_bbox, viewport, values), c) {
262                    (Some(gradient), c) => UserSpacePaintSource::Gradient(gradient, c),
263                    (None, Some(c)) => UserSpacePaintSource::SolidColor(c),
264                    (None, None) => UserSpacePaintSource::None,
265                }
266            }
267
268            PaintSource::Pattern(ref p, c) => {
269                match (p.to_user_space(object_bbox, viewport, values), c) {
270                    (Some(pattern), c) => UserSpacePaintSource::Pattern(pattern, c),
271                    (None, Some(c)) => UserSpacePaintSource::SolidColor(c),
272                    (None, None) => UserSpacePaintSource::None,
273                }
274            }
275        }
276    }
277}
278
279impl std::fmt::Debug for PaintSource {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
281        match *self {
282            PaintSource::None => f.write_str("PaintSource::None"),
283            PaintSource::Gradient(_, _) => f.write_str("PaintSource::Gradient"),
284            PaintSource::Pattern(_, _) => f.write_str("PaintSource::Pattern"),
285            PaintSource::SolidColor(_) => f.write_str("PaintSource::SolidColor"),
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    use crate::color::RGBA;
295
296    #[test]
297    fn catches_invalid_syntax() {
298        assert!(PaintServer::parse_str("").is_err());
299        assert!(PaintServer::parse_str("42").is_err());
300        assert!(PaintServer::parse_str("invalid").is_err());
301    }
302
303    #[test]
304    fn parses_none() {
305        assert_eq!(PaintServer::parse_str("none").unwrap(), PaintServer::None);
306    }
307
308    #[test]
309    fn parses_solid_color() {
310        assert_eq!(
311            PaintServer::parse_str("rgb(255, 128, 64, 0.5)").unwrap(),
312            PaintServer::SolidColor(Color::Rgba(RGBA::new(255, 128, 64, 0.5)))
313        );
314
315        assert_eq!(
316            PaintServer::parse_str("currentColor").unwrap(),
317            PaintServer::SolidColor(Color::CurrentColor)
318        );
319    }
320
321    #[test]
322    fn parses_iri() {
323        assert_eq!(
324            PaintServer::parse_str("url(#link)").unwrap(),
325            PaintServer::Iri {
326                iri: Box::new(NodeId::Internal("link".to_string())),
327                alternate: None,
328            }
329        );
330
331        assert_eq!(
332            PaintServer::parse_str("url(foo#link) none").unwrap(),
333            PaintServer::Iri {
334                iri: Box::new(NodeId::External("foo".to_string(), "link".to_string())),
335                alternate: None,
336            }
337        );
338
339        assert_eq!(
340            PaintServer::parse_str("url(#link) #ff8040").unwrap(),
341            PaintServer::Iri {
342                iri: Box::new(NodeId::Internal("link".to_string())),
343                alternate: Some(Color::Rgba(RGBA::new(255, 128, 64, 1.0))),
344            }
345        );
346
347        assert_eq!(
348            PaintServer::parse_str("url(#link) rgb(255, 128, 64, 0.5)").unwrap(),
349            PaintServer::Iri {
350                iri: Box::new(NodeId::Internal("link".to_string())),
351                alternate: Some(Color::Rgba(RGBA::new(255, 128, 64, 0.5))),
352            }
353        );
354
355        assert_eq!(
356            PaintServer::parse_str("url(#link) currentColor").unwrap(),
357            PaintServer::Iri {
358                iri: Box::new(NodeId::Internal("link".to_string())),
359                alternate: Some(Color::CurrentColor),
360            }
361        );
362
363        assert!(PaintServer::parse_str("url(#link) invalid").is_err());
364    }
365
366    #[test]
367    fn resolves_explicit_color() {
368        assert_eq!(
369            resolve_color(
370                &Color::Rgba(RGBA::new(255, 0, 0, 0.5)),
371                UnitInterval::clamp(0.5),
372                &Color::Rgba(RGBA::new(0, 255, 0, 1.0)),
373            ),
374            Color::Rgba(RGBA::new(255, 0, 0, 0.25)),
375        );
376    }
377
378    #[test]
379    fn resolves_current_color() {
380        assert_eq!(
381            resolve_color(
382                &Color::CurrentColor,
383                UnitInterval::clamp(0.5),
384                &Color::Rgba(RGBA::new(0, 255, 0, 0.5)),
385            ),
386            Color::Rgba(RGBA::new(0, 255, 0, 0.25)),
387        );
388    }
389}