usvg/parser/
style.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 http://mozilla.org/MPL/2.0/.
4
5use super::converter::{self, SvgColorExt};
6use super::paint_server;
7use super::svgtree::{AId, FromValue, SvgNode};
8use crate::tree::ContextElement;
9use crate::{
10    ApproxEqUlps, Color, Fill, FillRule, LineCap, LineJoin, Opacity, Paint, Stroke,
11    StrokeMiterlimit, Units,
12};
13
14impl<'a, 'input: 'a> FromValue<'a, 'input> for LineCap {
15    fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
16        match value {
17            "butt" => Some(LineCap::Butt),
18            "round" => Some(LineCap::Round),
19            "square" => Some(LineCap::Square),
20            _ => None,
21        }
22    }
23}
24
25impl<'a, 'input: 'a> FromValue<'a, 'input> for LineJoin {
26    fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
27        match value {
28            "miter" => Some(LineJoin::Miter),
29            "miter-clip" => Some(LineJoin::MiterClip),
30            "round" => Some(LineJoin::Round),
31            "bevel" => Some(LineJoin::Bevel),
32            _ => None,
33        }
34    }
35}
36
37impl<'a, 'input: 'a> FromValue<'a, 'input> for FillRule {
38    fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
39        match value {
40            "nonzero" => Some(FillRule::NonZero),
41            "evenodd" => Some(FillRule::EvenOdd),
42            _ => None,
43        }
44    }
45}
46
47pub(crate) fn resolve_fill(
48    node: SvgNode,
49    has_bbox: bool,
50    state: &converter::State,
51    cache: &mut converter::Cache,
52) -> Option<Fill> {
53    if state.parent_clip_path.is_some() {
54        // A `clipPath` child can be filled only with a black color.
55        return Some(Fill {
56            paint: Paint::Color(Color::black()),
57            opacity: Opacity::ONE,
58            rule: node.find_attribute(AId::ClipRule).unwrap_or_default(),
59            context_element: None,
60        });
61    }
62
63    let mut sub_opacity = Opacity::ONE;
64    let (paint, context_element) =
65        if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::Fill)) {
66            convert_paint(n, AId::Fill, has_bbox, state, &mut sub_opacity, cache)?
67        } else {
68            (Paint::Color(Color::black()), None)
69        };
70
71    let fill_opacity = node
72        .find_attribute::<Opacity>(AId::FillOpacity)
73        .unwrap_or(Opacity::ONE);
74
75    Some(Fill {
76        paint,
77        opacity: sub_opacity * fill_opacity,
78        rule: node.find_attribute(AId::FillRule).unwrap_or_default(),
79        context_element,
80    })
81}
82
83pub(crate) fn resolve_stroke(
84    node: SvgNode,
85    has_bbox: bool,
86    state: &converter::State,
87    cache: &mut converter::Cache,
88) -> Option<Stroke> {
89    if state.parent_clip_path.is_some() {
90        // A `clipPath` child cannot be stroked.
91        return None;
92    }
93
94    let mut sub_opacity = Opacity::ONE;
95    let (paint, context_element) =
96        if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::Stroke)) {
97            convert_paint(n, AId::Stroke, has_bbox, state, &mut sub_opacity, cache)?
98        } else {
99            return None;
100        };
101
102    let width = node.resolve_valid_length(AId::StrokeWidth, state, 1.0)?;
103
104    // Must be bigger than 1.
105    let miterlimit = node.find_attribute(AId::StrokeMiterlimit).unwrap_or(4.0);
106    let miterlimit = if miterlimit < 1.0 { 1.0 } else { miterlimit };
107    let miterlimit = StrokeMiterlimit::new(miterlimit);
108
109    let stroke_opacity = node
110        .find_attribute::<Opacity>(AId::StrokeOpacity)
111        .unwrap_or(Opacity::ONE);
112
113    let stroke = Stroke {
114        paint,
115        dasharray: conv_dasharray(node, state),
116        dashoffset: node.resolve_length(AId::StrokeDashoffset, state, 0.0),
117        miterlimit,
118        opacity: sub_opacity * stroke_opacity,
119        width,
120        linecap: node.find_attribute(AId::StrokeLinecap).unwrap_or_default(),
121        linejoin: node.find_attribute(AId::StrokeLinejoin).unwrap_or_default(),
122        context_element,
123    };
124
125    Some(stroke)
126}
127
128fn convert_paint(
129    node: SvgNode,
130    aid: AId,
131    has_bbox: bool,
132    state: &converter::State,
133    opacity: &mut Opacity,
134    cache: &mut converter::Cache,
135) -> Option<(Paint, Option<ContextElement>)> {
136    let value: &str = node.attribute(aid)?;
137    let paint = match svgtypes::Paint::from_str(value) {
138        Ok(v) => v,
139        Err(_) => {
140            if aid == AId::Fill {
141                log::warn!(
142                    "Failed to parse fill value: '{}'. Fallback to black.",
143                    value
144                );
145                svgtypes::Paint::Color(svgtypes::Color::black())
146            } else if aid == AId::Stroke {
147                log::warn!(
148                    "Failed to parse stroke value: '{}'. Fallback to no stroke.",
149                    value
150                );
151                return None;
152            } else {
153                return None;
154            }
155        }
156    };
157
158    match paint {
159        svgtypes::Paint::None => None,
160        svgtypes::Paint::Inherit => None, // already resolved by svgtree
161        svgtypes::Paint::ContextFill => state
162            .context_element
163            .clone()
164            .map(|(f, _)| f)
165            .flatten()
166            .map(|f| (f.paint, f.context_element)),
167        svgtypes::Paint::ContextStroke => state
168            .context_element
169            .clone()
170            .map(|(_, s)| s)
171            .flatten()
172            .map(|s| (s.paint, s.context_element)),
173        svgtypes::Paint::CurrentColor => {
174            let svg_color: svgtypes::Color = node
175                .find_attribute(AId::Color)
176                .unwrap_or_else(svgtypes::Color::black);
177            let (color, alpha) = svg_color.split_alpha();
178            *opacity = alpha;
179            Some((Paint::Color(color), None))
180        }
181        svgtypes::Paint::Color(svg_color) => {
182            let (color, alpha) = svg_color.split_alpha();
183            *opacity = alpha;
184            Some((Paint::Color(color), None))
185        }
186        svgtypes::Paint::FuncIRI(func_iri, fallback) => {
187            if let Some(link) = node.document().element_by_id(func_iri) {
188                let tag_name = link.tag_name().unwrap();
189                if tag_name.is_paint_server() {
190                    match paint_server::convert(link, state, cache) {
191                        Some(paint_server::ServerOrColor::Server(paint)) => {
192                            // We can use a paint server node with ObjectBoundingBox units
193                            // for painting only when the shape itself has a bbox.
194                            //
195                            // See SVG spec 7.11 for details.
196
197                            if !has_bbox && paint.units() == Units::ObjectBoundingBox {
198                                from_fallback(node, fallback, opacity).map(|p| (p, None))
199                            } else {
200                                Some((paint, None))
201                            }
202                        }
203                        Some(paint_server::ServerOrColor::Color { color, opacity: so }) => {
204                            *opacity = so;
205                            Some((Paint::Color(color), None))
206                        }
207                        None => from_fallback(node, fallback, opacity).map(|p| (p, None)),
208                    }
209                } else {
210                    log::warn!("'{}' cannot be used to {} a shape.", tag_name, aid);
211                    None
212                }
213            } else {
214                from_fallback(node, fallback, opacity).map(|p| (p, None))
215            }
216        }
217    }
218}
219
220fn from_fallback(
221    node: SvgNode,
222    fallback: Option<svgtypes::PaintFallback>,
223    opacity: &mut Opacity,
224) -> Option<Paint> {
225    match fallback? {
226        svgtypes::PaintFallback::None => None,
227        svgtypes::PaintFallback::CurrentColor => {
228            let svg_color: svgtypes::Color = node
229                .find_attribute(AId::Color)
230                .unwrap_or_else(svgtypes::Color::black);
231            let (color, alpha) = svg_color.split_alpha();
232            *opacity = alpha;
233            Some(Paint::Color(color))
234        }
235        svgtypes::PaintFallback::Color(svg_color) => {
236            let (color, alpha) = svg_color.split_alpha();
237            *opacity = alpha;
238            Some(Paint::Color(color))
239        }
240    }
241}
242
243// Prepare the 'stroke-dasharray' according to:
244// https://www.w3.org/TR/SVG11/painting.html#StrokeDasharrayProperty
245fn conv_dasharray(node: SvgNode, state: &converter::State) -> Option<Vec<f32>> {
246    let node = node
247        .ancestors()
248        .find(|n| n.has_attribute(AId::StrokeDasharray))?;
249    let list = super::units::convert_list(node, AId::StrokeDasharray, state)?;
250
251    // `A negative value is an error`
252    if list.iter().any(|n| n.is_sign_negative()) {
253        return None;
254    }
255
256    // `If the sum of the values is zero, then the stroke is rendered
257    // as if a value of none were specified.`
258    {
259        // no Iter::sum(), because of f64
260
261        let mut sum: f32 = 0.0;
262        for n in list.iter() {
263            sum += *n;
264        }
265
266        if sum.approx_eq_ulps(&0.0, 4) {
267            return None;
268        }
269    }
270
271    // `If an odd number of values is provided, then the list of values
272    // is repeated to yield an even number of values.`
273    if list.len() % 2 != 0 {
274        let mut tmp_list = list.clone();
275        tmp_list.extend_from_slice(&list);
276        return Some(tmp_list);
277    }
278
279    Some(list)
280}