| 1 | // Copyright 2018 the Resvg Authors |
| 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
| 3 | |
| 4 | use std::sync::Arc; |
| 5 | |
| 6 | use strict_num::NonZeroPositiveF32; |
| 7 | use svgtypes::Length; |
| 8 | use tiny_skia_path::Point; |
| 9 | |
| 10 | use super::converter; |
| 11 | use super::svgtree::{AId, EId, SvgNode}; |
| 12 | use crate::{ |
| 13 | ApproxEqUlps, ApproxZeroUlps, ClipPath, Fill, Group, Node, NonZeroRect, Path, Size, Transform, |
| 14 | ViewBox, |
| 15 | }; |
| 16 | |
| 17 | // Similar to `tiny_skia_path::PathSegment`, but without the `QuadTo`. |
| 18 | #[derive (Copy, Clone, Debug)] |
| 19 | enum Segment { |
| 20 | MoveTo(Point), |
| 21 | LineTo(Point), |
| 22 | CubicTo(Point, Point, Point), |
| 23 | Close, |
| 24 | } |
| 25 | |
| 26 | pub(crate) fn is_valid(node: SvgNode) -> bool { |
| 27 | // `marker-*` attributes cannot be set on shapes inside a `clipPath`. |
| 28 | if nodeAncestors<'_, '_> |
| 29 | .ancestors() |
| 30 | .any(|n: SvgNode<'_, '_>| n.tag_name() == Some(EId::ClipPath)) |
| 31 | { |
| 32 | return false; |
| 33 | } |
| 34 | |
| 35 | let start: Option> = node.find_attribute::<SvgNode>(AId::MarkerStart); |
| 36 | let mid: Option> = node.find_attribute::<SvgNode>(AId::MarkerMid); |
| 37 | let end: Option> = node.find_attribute::<SvgNode>(AId::MarkerEnd); |
| 38 | start.is_some() || mid.is_some() || end.is_some() |
| 39 | } |
| 40 | |
| 41 | pub(crate) fn convert( |
| 42 | node: SvgNode, |
| 43 | path: &tiny_skia_path::Path, |
| 44 | state: &converter::State, |
| 45 | cache: &mut converter::Cache, |
| 46 | parent: &mut Group, |
| 47 | ) { |
| 48 | let list = [ |
| 49 | (AId::MarkerStart, MarkerKind::Start), |
| 50 | (AId::MarkerMid, MarkerKind::Middle), |
| 51 | (AId::MarkerEnd, MarkerKind::End), |
| 52 | ]; |
| 53 | |
| 54 | for (aid, kind) in &list { |
| 55 | let mut marker = None; |
| 56 | if let Some(link) = node.find_attribute::<SvgNode>(*aid) { |
| 57 | if link.tag_name() == Some(EId::Marker) { |
| 58 | marker = Some(link); |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | if let Some(marker) = marker { |
| 63 | // TODO: move to svgtree |
| 64 | // Check for recursive marker. |
| 65 | if state.parent_markers.contains(&marker) { |
| 66 | log::warn!("Recursive marker detected: {}" , marker.element_id()); |
| 67 | continue; |
| 68 | } |
| 69 | |
| 70 | resolve(node, path, marker, *kind, state, cache, parent); |
| 71 | } |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | #[derive (Clone, Copy)] |
| 76 | enum MarkerKind { |
| 77 | Start, |
| 78 | Middle, |
| 79 | End, |
| 80 | } |
| 81 | |
| 82 | enum MarkerOrientation { |
| 83 | Auto, |
| 84 | AutoStartReverse, |
| 85 | Angle(f32), |
| 86 | } |
| 87 | |
| 88 | fn resolve( |
| 89 | shape_node: SvgNode, |
| 90 | path: &tiny_skia_path::Path, |
| 91 | marker_node: SvgNode, |
| 92 | marker_kind: MarkerKind, |
| 93 | state: &converter::State, |
| 94 | cache: &mut converter::Cache, |
| 95 | parent: &mut Group, |
| 96 | ) -> Option<()> { |
| 97 | let stroke_scale = stroke_scale(shape_node, marker_node, state)?.get(); |
| 98 | |
| 99 | let r = convert_rect(marker_node, state)?; |
| 100 | |
| 101 | let view_box = marker_node.parse_viewbox().map(|vb| ViewBox { |
| 102 | rect: vb, |
| 103 | aspect: marker_node |
| 104 | .attribute(AId::PreserveAspectRatio) |
| 105 | .unwrap_or_default(), |
| 106 | }); |
| 107 | |
| 108 | let has_overflow = { |
| 109 | let overflow = marker_node.attribute(AId::Overflow); |
| 110 | // `overflow` is `hidden` by default. |
| 111 | overflow.is_none() || overflow == Some("hidden" ) || overflow == Some("scroll" ) |
| 112 | }; |
| 113 | |
| 114 | let clip_path = if has_overflow { |
| 115 | let clip_rect = if let Some(vbox) = view_box { |
| 116 | vbox.rect |
| 117 | } else { |
| 118 | r.size().to_non_zero_rect(0.0, 0.0) |
| 119 | }; |
| 120 | |
| 121 | let mut clip_path = ClipPath::empty(cache.gen_clip_path_id()); |
| 122 | |
| 123 | let mut path = Path::new_simple(Arc::new(tiny_skia_path::PathBuilder::from_rect( |
| 124 | clip_rect.to_rect(), |
| 125 | )))?; |
| 126 | path.fill = Some(Fill::default()); |
| 127 | |
| 128 | clip_path.root.children.push(Node::Path(Box::new(path))); |
| 129 | |
| 130 | Some(Arc::new(clip_path)) |
| 131 | } else { |
| 132 | None |
| 133 | }; |
| 134 | |
| 135 | // TODO: avoid allocation |
| 136 | let mut segments: Vec<Segment> = Vec::with_capacity(path.len()); |
| 137 | let mut prev = Point::zero(); |
| 138 | let mut prev_move = Point::zero(); |
| 139 | for seg in path.segments() { |
| 140 | match seg { |
| 141 | tiny_skia_path::PathSegment::MoveTo(p) => { |
| 142 | segments.push(Segment::MoveTo(p)); |
| 143 | prev = p; |
| 144 | prev_move = p; |
| 145 | } |
| 146 | tiny_skia_path::PathSegment::LineTo(p) => { |
| 147 | segments.push(Segment::LineTo(p)); |
| 148 | prev = p; |
| 149 | } |
| 150 | tiny_skia_path::PathSegment::QuadTo(p1, p) => { |
| 151 | let (p1, p2, p) = quad_to_curve(prev, p1, p); |
| 152 | segments.push(Segment::CubicTo(p1, p2, p)); |
| 153 | prev = p; |
| 154 | } |
| 155 | tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => { |
| 156 | segments.push(Segment::CubicTo(p1, p2, p)); |
| 157 | prev = p; |
| 158 | } |
| 159 | tiny_skia_path::PathSegment::Close => { |
| 160 | segments.push(Segment::Close); |
| 161 | prev = prev_move; |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | let draw_marker = |p: tiny_skia_path::Point, idx: usize| { |
| 167 | let mut ts = Transform::from_translate(p.x, p.y); |
| 168 | |
| 169 | let angle = match convert_orientation(marker_node) { |
| 170 | MarkerOrientation::AutoStartReverse if idx == 0 => { |
| 171 | (calc_vertex_angle(&segments, idx) + 180.0) % 360.0 |
| 172 | } |
| 173 | MarkerOrientation::Auto | MarkerOrientation::AutoStartReverse => { |
| 174 | calc_vertex_angle(&segments, idx) |
| 175 | } |
| 176 | MarkerOrientation::Angle(angle) => angle, |
| 177 | }; |
| 178 | |
| 179 | if !angle.approx_zero_ulps(4) { |
| 180 | ts = ts.pre_rotate(angle); |
| 181 | } |
| 182 | |
| 183 | if let Some(vbox) = view_box { |
| 184 | let size = Size::from_wh(r.width() * stroke_scale, r.height() * stroke_scale).unwrap(); |
| 185 | let vbox_ts = vbox.to_transform(size); |
| 186 | let (sx, sy) = vbox_ts.get_scale(); |
| 187 | ts = ts.pre_scale(sx, sy); |
| 188 | } else { |
| 189 | ts = ts.pre_scale(stroke_scale, stroke_scale); |
| 190 | } |
| 191 | |
| 192 | ts = ts.pre_translate(-r.x(), -r.y()); |
| 193 | |
| 194 | // TODO: do not create a group when no clipPath |
| 195 | let mut g = Group { |
| 196 | transform: ts, |
| 197 | abs_transform: parent.abs_transform.pre_concat(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 | |
| 217 | fn 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 | |
| 228 | fn draw_markers<P>(path: &[Segment], kind: MarkerKind, mut draw_marker: P) |
| 229 | where |
| 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 | |
| 276 | fn 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 | |
| 388 | fn 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 | |
| 392 | fn 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 | |
| 413 | fn 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 | |
| 446 | fn 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 | |
| 457 | fn 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 | |
| 466 | fn 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 | |
| 475 | fn 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 | |
| 486 | fn 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 | |