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