Skip to main content

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            let value: &str = n.attribute(AId::Fill)?;
66            convert_paint(
67                node,
68                value,
69                AId::Fill,
70                has_bbox,
71                state,
72                &mut sub_opacity,
73                cache,
74            )?
75        } else {
76            (Paint::Color(Color::black()), None)
77        };
78
79    let fill_opacity = node
80        .find_attribute::<Opacity>(AId::FillOpacity)
81        .unwrap_or(Opacity::ONE);
82
83    Some(Fill {
84        paint,
85        opacity: sub_opacity * fill_opacity,
86        rule: node.find_attribute(AId::FillRule).unwrap_or_default(),
87        context_element,
88    })
89}
90
91pub(crate) fn resolve_stroke(
92    node: SvgNode,
93    has_bbox: bool,
94    state: &converter::State,
95    cache: &mut converter::Cache,
96) -> Option<Stroke> {
97    if state.parent_clip_path.is_some() {
98        // A `clipPath` child cannot be stroked.
99        return None;
100    }
101
102    let mut sub_opacity = Opacity::ONE;
103    let (paint, context_element) =
104        if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::Stroke)) {
105            let value: &str = n.attribute(AId::Stroke)?;
106
107            convert_paint(
108                node,
109                value,
110                AId::Stroke,
111                has_bbox,
112                state,
113                &mut sub_opacity,
114                cache,
115            )?
116        } else {
117            return None;
118        };
119
120    let width = node.resolve_valid_length(AId::StrokeWidth, state, 1.0)?;
121
122    // Must be bigger than 1.
123    let miterlimit = node.find_attribute(AId::StrokeMiterlimit).unwrap_or(4.0);
124    let miterlimit = if miterlimit < 1.0 { 1.0 } else { miterlimit };
125    let miterlimit = StrokeMiterlimit::new(miterlimit);
126
127    let stroke_opacity = node
128        .find_attribute::<Opacity>(AId::StrokeOpacity)
129        .unwrap_or(Opacity::ONE);
130
131    let stroke = Stroke {
132        paint,
133        dasharray: conv_dasharray(node, state),
134        dashoffset: node.resolve_length(AId::StrokeDashoffset, state, 0.0),
135        miterlimit,
136        opacity: sub_opacity * stroke_opacity,
137        width,
138        linecap: node.find_attribute(AId::StrokeLinecap).unwrap_or_default(),
139        linejoin: node.find_attribute(AId::StrokeLinejoin).unwrap_or_default(),
140        context_element,
141    };
142
143    Some(stroke)
144}
145
146fn convert_paint(
147    node: SvgNode,
148    value: &str,
149    aid: AId,
150    has_bbox: bool,
151    state: &converter::State,
152    opacity: &mut Opacity,
153    cache: &mut converter::Cache,
154) -> Option<(Paint, Option<ContextElement>)> {
155    let paint = match svgtypes::Paint::from_str(value) {
156        Ok(v) => v,
157        Err(_) => {
158            if aid == AId::Fill {
159                log::warn!(
160                    "Failed to parse fill value: '{}'. Fallback to black.",
161                    value
162                );
163                svgtypes::Paint::Color(svgtypes::Color::black())
164            } else if aid == AId::Stroke {
165                log::warn!(
166                    "Failed to parse stroke value: '{}'. Fallback to no stroke.",
167                    value
168                );
169                return None;
170            } else {
171                return None;
172            }
173        }
174    };
175
176    match paint {
177        svgtypes::Paint::None => None,
178        svgtypes::Paint::Inherit => None, // already resolved by svgtree
179        svgtypes::Paint::ContextFill => state
180            .context_element
181            .clone()
182            .and_then(|(f, _)| f)
183            .map(|f| (f.paint, f.context_element)),
184        svgtypes::Paint::ContextStroke => state
185            .context_element
186            .clone()
187            .and_then(|(_, s)| s)
188            .map(|s| (s.paint, s.context_element)),
189        svgtypes::Paint::CurrentColor => {
190            let svg_color: svgtypes::Color = node
191                .find_attribute(AId::Color)
192                .unwrap_or_else(svgtypes::Color::black);
193            let (color, alpha) = svg_color.split_alpha();
194            *opacity = alpha;
195            Some((Paint::Color(color), None))
196        }
197        svgtypes::Paint::Color(svg_color) => {
198            let (color, alpha) = svg_color.split_alpha();
199            *opacity = alpha;
200            Some((Paint::Color(color), None))
201        }
202        svgtypes::Paint::FuncIRI(func_iri, fallback) => {
203            if let Some(link) = node.document().element_by_id(func_iri) {
204                let tag_name = link.tag_name().unwrap();
205                if tag_name.is_paint_server() {
206                    match paint_server::convert(link, state, cache) {
207                        Some(paint_server::ServerOrColor::Server(paint)) => {
208                            // We can use a paint server node with ObjectBoundingBox units
209                            // for painting only when the shape itself has a bbox.
210                            //
211                            // See SVG spec 7.11 for details.
212
213                            if !has_bbox && paint.units() == Units::ObjectBoundingBox {
214                                from_fallback(node, fallback, opacity).map(|p| (p, None))
215                            } else {
216                                Some((paint, None))
217                            }
218                        }
219                        Some(paint_server::ServerOrColor::Color { color, opacity: so }) => {
220                            *opacity = so;
221                            Some((Paint::Color(color), None))
222                        }
223                        None => from_fallback(node, fallback, opacity).map(|p| (p, None)),
224                    }
225                } else {
226                    log::warn!("'{}' cannot be used to {} a shape.", tag_name, aid);
227                    None
228                }
229            } else {
230                from_fallback(node, fallback, opacity).map(|p| (p, None))
231            }
232        }
233    }
234}
235
236fn from_fallback(
237    node: SvgNode,
238    fallback: Option<svgtypes::PaintFallback>,
239    opacity: &mut Opacity,
240) -> Option<Paint> {
241    match fallback? {
242        svgtypes::PaintFallback::None => None,
243        svgtypes::PaintFallback::CurrentColor => {
244            let svg_color: svgtypes::Color = node
245                .find_attribute(AId::Color)
246                .unwrap_or_else(svgtypes::Color::black);
247            let (color, alpha) = svg_color.split_alpha();
248            *opacity = alpha;
249            Some(Paint::Color(color))
250        }
251        svgtypes::PaintFallback::Color(svg_color) => {
252            let (color, alpha) = svg_color.split_alpha();
253            *opacity = alpha;
254            Some(Paint::Color(color))
255        }
256    }
257}
258
259// Prepare the 'stroke-dasharray' according to:
260// https://www.w3.org/TR/SVG11/painting.html#StrokeDasharrayProperty
261fn conv_dasharray(node: SvgNode, state: &converter::State) -> Option<Vec<f32>> {
262    let node = node
263        .ancestors()
264        .find(|n| n.has_attribute(AId::StrokeDasharray))?;
265    let list = super::units::convert_list(node, AId::StrokeDasharray, state)?;
266
267    // `A negative value is an error`
268    if list.iter().any(|n| n.is_sign_negative()) {
269        return None;
270    }
271
272    // `If the sum of the values is zero, then the stroke is rendered
273    // as if a value of none were specified.`
274    {
275        // no Iter::sum(), because of f64
276
277        let mut sum: f32 = 0.0;
278        for n in list.iter() {
279            sum += *n;
280        }
281
282        if sum.approx_eq_ulps(&0.0, 4) {
283            return None;
284        }
285    }
286
287    // `If an odd number of values is provided, then the list of values
288    // is repeated to yield an even number of values.`
289    if list.len() % 2 != 0 {
290        let mut tmp_list = list.clone();
291        tmp_list.extend_from_slice(&list);
292        return Some(tmp_list);
293    }
294
295    Some(list)
296}