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 std::sync::Arc;
6
7use strict_num::NonZeroPositiveF32;
8use svgtypes::Length;
9use tiny_skia_path::Point;
10
11use super::converter;
12use super::svgtree::{AId, EId, SvgNode};
13use crate::{
14 ApproxEqUlps, ApproxZeroUlps, ClipPath, Fill, Group, Node, NonZeroRect, Path, Size, Transform,
15 ViewBox,
16};
17
18// Similar to `tiny_skia_path::PathSegment`, but without the `QuadTo`.
19#[derive(Copy, Clone, Debug)]
20enum Segment {
21 MoveTo(Point),
22 LineTo(Point),
23 CubicTo(Point, Point, Point),
24 Close,
25}
26
27pub(crate) fn is_valid(node: SvgNode) -> bool {
28 // `marker-*` attributes cannot be set on shapes inside a `clipPath`.
29 if nodeAncestors<'_, '_>
30 .ancestors()
31 .any(|n: SvgNode<'_, '_>| n.tag_name() == Some(EId::ClipPath))
32 {
33 return false;
34 }
35
36 let start: Option> = node.find_attribute::<SvgNode>(AId::MarkerStart);
37 let mid: Option> = node.find_attribute::<SvgNode>(AId::MarkerMid);
38 let end: Option> = node.find_attribute::<SvgNode>(AId::MarkerEnd);
39 start.is_some() || mid.is_some() || end.is_some()
40}
41
42pub(crate) fn convert(
43 node: SvgNode,
44 path: &tiny_skia_path::Path,
45 state: &converter::State,
46 cache: &mut converter::Cache,
47 parent: &mut Group,
48) {
49 let list = [
50 (AId::MarkerStart, MarkerKind::Start),
51 (AId::MarkerMid, MarkerKind::Middle),
52 (AId::MarkerEnd, MarkerKind::End),
53 ];
54
55 for (aid, kind) in &list {
56 let mut marker = None;
57 if let Some(link) = node.find_attribute::<SvgNode>(*aid) {
58 if link.tag_name() == Some(EId::Marker) {
59 marker = Some(link);
60 }
61 }
62
63 if let Some(marker) = marker {
64 // TODO: move to svgtree
65 // Check for recursive marker.
66 if state.parent_markers.contains(&marker) {
67 log::warn!("Recursive marker detected: {}", marker.element_id());
68 continue;
69 }
70
71 resolve(node, path, marker, *kind, state, cache, parent);
72 }
73 }
74}
75
76#[derive(Clone, Copy)]
77enum MarkerKind {
78 Start,
79 Middle,
80 End,
81}
82
83enum MarkerOrientation {
84 Auto,
85 AutoStartReverse,
86 Angle(f32),
87}
88
89fn resolve(
90 shape_node: SvgNode,
91 path: &tiny_skia_path::Path,
92 marker_node: SvgNode,
93 marker_kind: MarkerKind,
94 state: &converter::State,
95 cache: &mut converter::Cache,
96 parent: &mut Group,
97) -> Option<()> {
98 let stroke_scale = stroke_scale(shape_node, marker_node, state)?.get();
99
100 let r = convert_rect(marker_node, state)?;
101
102 let view_box = marker_node.parse_viewbox().map(|vb| ViewBox {
103 rect: vb,
104 aspect: marker_node
105 .attribute(AId::PreserveAspectRatio)
106 .unwrap_or_default(),
107 });
108
109 let has_overflow = {
110 let overflow = marker_node.attribute(AId::Overflow);
111 // `overflow` is `hidden` by default.
112 overflow.is_none() || overflow == Some("hidden") || overflow == Some("scroll")
113 };
114
115 let clip_path = if has_overflow {
116 let clip_rect = if let Some(vbox) = view_box {
117 vbox.rect
118 } else {
119 r.size().to_non_zero_rect(0.0, 0.0)
120 };
121
122 let mut clip_path = ClipPath::empty(cache.gen_clip_path_id());
123
124 let mut path = Path::new_simple(Arc::new(tiny_skia_path::PathBuilder::from_rect(
125 clip_rect.to_rect(),
126 )))?;
127 path.fill = Some(Fill::default());
128
129 clip_path.root.children.push(Node::Path(Box::new(path)));
130
131 Some(Arc::new(clip_path))
132 } else {
133 None
134 };
135
136 // TODO: avoid allocation
137 let mut segments: Vec<Segment> = Vec::with_capacity(path.len());
138 let mut prev = Point::zero();
139 let mut prev_move = Point::zero();
140 for seg in path.segments() {
141 match seg {
142 tiny_skia_path::PathSegment::MoveTo(p) => {
143 segments.push(Segment::MoveTo(p));
144 prev = p;
145 prev_move = p;
146 }
147 tiny_skia_path::PathSegment::LineTo(p) => {
148 segments.push(Segment::LineTo(p));
149 prev = p;
150 }
151 tiny_skia_path::PathSegment::QuadTo(p1, p) => {
152 let (p1, p2, p) = quad_to_curve(prev, p1, p);
153 segments.push(Segment::CubicTo(p1, p2, p));
154 prev = p;
155 }
156 tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => {
157 segments.push(Segment::CubicTo(p1, p2, p));
158 prev = p;
159 }
160 tiny_skia_path::PathSegment::Close => {
161 segments.push(Segment::Close);
162 prev = prev_move;
163 }
164 }
165 }
166
167 let draw_marker = |p: tiny_skia_path::Point, idx: usize| {
168 let mut ts = Transform::from_translate(p.x, p.y);
169
170 let angle = match convert_orientation(marker_node) {
171 MarkerOrientation::AutoStartReverse if idx == 0 => {
172 (calc_vertex_angle(&segments, idx) + 180.0) % 360.0
173 }
174 MarkerOrientation::Auto | MarkerOrientation::AutoStartReverse => {
175 calc_vertex_angle(&segments, idx)
176 }
177 MarkerOrientation::Angle(angle) => angle,
178 };
179
180 if !angle.approx_zero_ulps(4) {
181 ts = ts.pre_rotate(angle);
182 }
183
184 if let Some(vbox) = view_box {
185 let size = Size::from_wh(r.width() * stroke_scale, r.height() * stroke_scale).unwrap();
186 let vbox_ts = vbox.to_transform(size);
187 let (sx, sy) = vbox_ts.get_scale();
188 ts = ts.pre_scale(sx, sy);
189 } else {
190 ts = ts.pre_scale(stroke_scale, stroke_scale);
191 }
192
193 ts = ts.pre_translate(-r.x(), -r.y());
194
195 // TODO: do not create a group when no clipPath
196 let mut g = Group {
197 transform: ts,
198 clip_path: clip_path.clone(),
199 ..Group::empty()
200 };
201
202 let mut marker_state = state.clone();
203 marker_state.parent_markers.push(marker_node);
204 converter::convert_children(marker_node, &marker_state, cache, &mut g);
205 g.calculate_bounding_boxes();
206
207 if g.has_children() {
208 parent.children.push(Node::Group(Box::new(g)));
209 }
210 };
211
212 draw_markers(&segments, marker_kind, draw_marker);
213
214 Some(())
215}
216
217fn stroke_scale(
218 path_node: SvgNode,
219 marker_node: SvgNode,
220 state: &converter::State,
221) -> Option<NonZeroPositiveF32> {
222 match marker_node.attribute(AId::MarkerUnits) {
223 Some("userSpaceOnUse") => NonZeroPositiveF32::new(1.0),
224 _ => path_node.resolve_valid_length(AId::StrokeWidth, state, def:1.0),
225 }
226}
227
228fn draw_markers<P>(path: &[Segment], kind: MarkerKind, mut draw_marker: P)
229where
230 P: FnMut(tiny_skia_path::Point, usize),
231{
232 match kind {
233 MarkerKind::Start => {
234 if let Some(Segment::MoveTo(p)) = path.first().cloned() {
235 draw_marker(p, 0);
236 }
237 }
238 MarkerKind::Middle => {
239 let total = path.len() - 1;
240 let mut i = 1;
241 while i < total {
242 let p = match path[i] {
243 Segment::MoveTo(p) => p,
244 Segment::LineTo(p) => p,
245 Segment::CubicTo(_, _, p) => p,
246 _ => {
247 i += 1;
248 continue;
249 }
250 };
251
252 draw_marker(p, i);
253
254 i += 1;
255 }
256 }
257 MarkerKind::End => {
258 let idx = path.len() - 1;
259 match path.last().cloned() {
260 Some(Segment::LineTo(p)) => {
261 draw_marker(p, idx);
262 }
263 Some(Segment::CubicTo(_, _, p)) => {
264 draw_marker(p, idx);
265 }
266 Some(Segment::Close) => {
267 let p = get_subpath_start(path, idx);
268 draw_marker(p, idx);
269 }
270 _ => {}
271 }
272 }
273 }
274}
275
276fn calc_vertex_angle(path: &[Segment], idx: usize) -> f32 {
277 if idx == 0 {
278 // First segment.
279
280 debug_assert!(path.len() > 1);
281
282 let seg1 = path[0];
283 let seg2 = path[1];
284
285 match (seg1, seg2) {
286 (Segment::MoveTo(pm), Segment::LineTo(p)) => calc_line_angle(pm.x, pm.y, p.x, p.y),
287 (Segment::MoveTo(pm), Segment::CubicTo(p1, _, p)) => {
288 if pm.x.approx_eq_ulps(&p1.x, 4) && pm.y.approx_eq_ulps(&p1.y, 4) {
289 calc_line_angle(pm.x, pm.y, p.x, p.y)
290 } else {
291 calc_line_angle(pm.x, pm.y, p1.x, p1.y)
292 }
293 }
294 _ => 0.0,
295 }
296 } else if idx == path.len() - 1 {
297 // Last segment.
298
299 let seg1 = path[idx - 1];
300 let seg2 = path[idx];
301
302 match (seg1, seg2) {
303 (_, Segment::MoveTo(_)) => 0.0, // unreachable
304 (_, Segment::LineTo(p)) => {
305 let prev = get_prev_vertex(path, idx);
306 calc_line_angle(prev.x, prev.y, p.x, p.y)
307 }
308 (_, Segment::CubicTo(p1, p2, p)) => {
309 if p2.x.approx_eq_ulps(&p.x, 4) && p2.y.approx_eq_ulps(&p.y, 4) {
310 calc_line_angle(p1.x, p1.y, p.x, p.y)
311 } else {
312 calc_line_angle(p2.x, p2.y, p.x, p.y)
313 }
314 }
315 (Segment::LineTo(p), Segment::Close) => {
316 let next = get_subpath_start(path, idx);
317 calc_line_angle(p.x, p.y, next.x, next.y)
318 }
319 (Segment::CubicTo(_, p2, p), Segment::Close) => {
320 let prev = get_prev_vertex(path, idx);
321 let next = get_subpath_start(path, idx);
322 calc_curves_angle(
323 prev.x, prev.y, p2.x, p2.y, p.x, p.y, next.x, next.y, next.x, next.y,
324 )
325 }
326 (_, Segment::Close) => 0.0,
327 }
328 } else {
329 // Middle segments.
330
331 let seg1 = path[idx];
332 let seg2 = path[idx + 1];
333
334 // TODO: Not sure if there is a better way.
335 match (seg1, seg2) {
336 (Segment::MoveTo(pm), Segment::LineTo(p)) => calc_line_angle(pm.x, pm.y, p.x, p.y),
337 (Segment::MoveTo(pm), Segment::CubicTo(p1, _, _)) => {
338 calc_line_angle(pm.x, pm.y, p1.x, p1.y)
339 }
340 (Segment::LineTo(p1), Segment::LineTo(p2)) => {
341 let prev = get_prev_vertex(path, idx);
342 calc_angle(prev.x, prev.y, p1.x, p1.y, p1.x, p1.y, p2.x, p2.y)
343 }
344 (Segment::CubicTo(_, c1_p2, c1_p), Segment::CubicTo(c2_p1, _, c2_p)) => {
345 let prev = get_prev_vertex(path, idx);
346 calc_curves_angle(
347 prev.x, prev.y, c1_p2.x, c1_p2.y, c1_p.x, c1_p.y, c2_p1.x, c2_p1.y, c2_p.x,
348 c2_p.y,
349 )
350 }
351 (Segment::LineTo(pl), Segment::CubicTo(p1, _, p)) => {
352 let prev = get_prev_vertex(path, idx);
353 calc_curves_angle(
354 prev.x, prev.y, prev.x, prev.y, pl.x, pl.y, p1.x, p1.y, p.x, p.y,
355 )
356 }
357 (Segment::CubicTo(_, p2, p), Segment::LineTo(pl)) => {
358 let prev = get_prev_vertex(path, idx);
359 calc_curves_angle(prev.x, prev.y, p2.x, p2.y, p.x, p.y, pl.x, pl.y, pl.x, pl.y)
360 }
361 (Segment::LineTo(p), Segment::MoveTo(_)) => {
362 let prev = get_prev_vertex(path, idx);
363 calc_line_angle(prev.x, prev.y, p.x, p.y)
364 }
365 (Segment::CubicTo(_, p2, p), Segment::MoveTo(_)) => {
366 if p.x.approx_eq_ulps(&p2.x, 4) && p.y.approx_eq_ulps(&p2.y, 4) {
367 let prev = get_prev_vertex(path, idx);
368 calc_line_angle(prev.x, prev.y, p.x, p.y)
369 } else {
370 calc_line_angle(p2.x, p2.y, p.x, p.y)
371 }
372 }
373 (Segment::LineTo(p), Segment::Close) => {
374 let prev = get_prev_vertex(path, idx);
375 let next = get_subpath_start(path, idx);
376 calc_angle(prev.x, prev.y, p.x, p.y, p.x, p.y, next.x, next.y)
377 }
378 (_, Segment::Close) => {
379 let prev = get_prev_vertex(path, idx);
380 let next = get_subpath_start(path, idx);
381 calc_line_angle(prev.x, prev.y, next.x, next.y)
382 }
383 (_, Segment::MoveTo(_)) | (Segment::Close, _) => 0.0,
384 }
385 }
386}
387
388fn calc_line_angle(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
389 calc_angle(x1, y1, x2, y2, x3:x1, y3:y1, x4:x2, y4:y2)
390}
391
392fn calc_curves_angle(
393 px: f32,
394 py: f32, // previous vertex
395 cx1: f32,
396 cy1: f32, // previous control point
397 x: f32,
398 y: f32, // current vertex
399 cx2: f32,
400 cy2: f32, // next control point
401 nx: f32,
402 ny: f32, // next vertex
403) -> f32 {
404 if cx1.approx_eq_ulps(&x, ulps:4) && cy1.approx_eq_ulps(&y, ulps:4) {
405 calc_angle(x1:px, y1:py, x2:x, y2:y, x3:x, y3:y, x4:cx2, y4:cy2)
406 } else if x.approx_eq_ulps(&cx2, ulps:4) && y.approx_eq_ulps(&cy2, ulps:4) {
407 calc_angle(x1:cx1, y1:cy1, x2:x, y2:y, x3:x, y3:y, x4:nx, y4:ny)
408 } else {
409 calc_angle(x1:cx1, y1:cy1, x2:x, y2:y, x3:x, y3:y, x4:cx2, y4:cy2)
410 }
411}
412
413fn calc_angle(x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32, x4: f32, y4: f32) -> f32 {
414 use std::f32::consts::*;
415
416 fn normalize(rad: f32) -> f32 {
417 let v = rad % (PI * 2.0);
418 if v < 0.0 {
419 v + PI * 2.0
420 } else {
421 v
422 }
423 }
424
425 fn vector_angle(vx: f32, vy: f32) -> f32 {
426 let rad = vy.atan2(vx);
427 if rad.is_nan() {
428 0.0
429 } else {
430 normalize(rad)
431 }
432 }
433
434 let in_a = vector_angle(x2 - x1, y2 - y1);
435 let out_a = vector_angle(x4 - x3, y4 - y3);
436 let d = (out_a - in_a) * 0.5;
437
438 let mut angle = in_a + d;
439 if FRAC_PI_2 < d.abs() {
440 angle -= PI;
441 }
442
443 normalize(angle).to_degrees()
444}
445
446fn get_subpath_start(segments: &[Segment], idx: usize) -> tiny_skia_path::Point {
447 let offset: usize = segments.len() - idx;
448 for seg: &Segment in segments.iter().rev().skip(offset) {
449 if let Segment::MoveTo(p: Point) = *seg {
450 return p;
451 }
452 }
453
454 tiny_skia_path::Point::zero()
455}
456
457fn get_prev_vertex(segments: &[Segment], idx: usize) -> tiny_skia_path::Point {
458 match segments[idx - 1] {
459 Segment::MoveTo(p: Point) => p,
460 Segment::LineTo(p: Point) => p,
461 Segment::CubicTo(_, _, p: Point) => p,
462 Segment::Close => get_subpath_start(segments, idx),
463 }
464}
465
466fn convert_rect(node: SvgNode, state: &converter::State) -> Option<NonZeroRect> {
467 NonZeroRect::from_xywh(
468 x:node.convert_user_length(AId::RefX, state, Length::zero()),
469 y:node.convert_user_length(AId::RefY, state, Length::zero()),
470 w:node.convert_user_length(AId::MarkerWidth, state, Length::new_number(3.0)),
471 h:node.convert_user_length(AId::MarkerHeight, state, def:Length::new_number(3.0)),
472 )
473}
474
475fn convert_orientation(node: SvgNode) -> MarkerOrientation {
476 match node.attribute(AId::Orient) {
477 Some("auto") => MarkerOrientation::Auto,
478 Some("auto-start-reverse") => MarkerOrientation::AutoStartReverse,
479 _ => match node.attribute::<svgtypes::Angle>(AId::Orient) {
480 Some(angle: Angle) => MarkerOrientation::Angle(angle.to_degrees() as f32),
481 None => MarkerOrientation::Angle(0.0),
482 },
483 }
484}
485
486fn quad_to_curve(prev: Point, p1: Point, p: Point) -> (Point, Point, Point) {
487 #[inline]
488 fn calc(n1: f32, n2: f32) -> f32 {
489 (n1 + n2 * 2.0) / 3.0
490 }
491
492 (
493 Point::from_xy(x:calc(prev.x, p1.x), y:calc(n1:prev.y, n2:p1.y)),
494 Point::from_xy(x:calc(p.x, p1.x), y:calc(n1:p.y, n2:p1.y)),
495 p,
496 )
497}
498