usvg/parser/
style.rs

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