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::str::FromStr;
6use std::sync::Arc;
7
8use super::converter;
9use super::svgtree::{AId, EId, SvgNode};
10use crate::{ClipPath, Group, NonEmptyString, NonZeroRect, Transform, Units};
11
12pub(crate) fn convert(
13 node: SvgNode,
14 state: &converter::State,
15 object_bbox: Option<NonZeroRect>,
16 cache: &mut converter::Cache,
17) -> Option<Arc<ClipPath>> {
18 // A `clip-path` attribute must reference a `clipPath` element.
19 if node.tag_name() != Some(EId::ClipPath) {
20 return None;
21 }
22
23 // The whole clip path should be ignored when a transform is invalid.
24 let mut transform = resolve_clip_path_transform(node, state)?;
25
26 let units = node
27 .attribute(AId::ClipPathUnits)
28 .unwrap_or(Units::UserSpaceOnUse);
29
30 // Check if this element was already converted.
31 //
32 // Only `userSpaceOnUse` clipPaths can be shared,
33 // because `objectBoundingBox` one will be converted into user one
34 // and will become node-specific.
35 let cacheable = units == Units::UserSpaceOnUse;
36 if cacheable {
37 if let Some(clip) = cache.clip_paths.get(node.element_id()) {
38 return Some(clip.clone());
39 }
40 }
41
42 if units == Units::ObjectBoundingBox {
43 let object_bbox = match object_bbox {
44 Some(v) => v,
45 None => {
46 log::warn!("Clipping of zero-sized shapes is not allowed.");
47 return None;
48 }
49 };
50
51 let ts = Transform::from_bbox(object_bbox);
52 transform = transform.pre_concat(ts);
53 }
54
55 // Resolve linked clip path.
56 let mut clip_path = None;
57 if let Some(link) = node.attribute::<SvgNode>(AId::ClipPath) {
58 clip_path = convert(link, state, object_bbox, cache);
59
60 // Linked `clipPath` must be valid.
61 if clip_path.is_none() {
62 return None;
63 }
64 }
65
66 let mut id = NonEmptyString::new(node.element_id().to_string())?;
67 // Generate ID only when we're parsing `objectBoundingBox` clip for the second time.
68 if !cacheable && cache.clip_paths.contains_key(id.get()) {
69 id = cache.gen_clip_path_id();
70 }
71 let id_copy = id.get().to_string();
72
73 let mut clip = ClipPath {
74 id,
75 transform,
76 clip_path,
77 root: Group::empty(),
78 };
79
80 let mut clip_state = state.clone();
81 clip_state.parent_clip_path = Some(node);
82 converter::convert_clip_path_elements(node, &clip_state, cache, &mut clip.root);
83
84 if clip.root.has_children() {
85 clip.root.calculate_bounding_boxes();
86 let clip = Arc::new(clip);
87 cache.clip_paths.insert(id_copy, clip.clone());
88 Some(clip)
89 } else {
90 // A clip path without children is invalid.
91 None
92 }
93}
94
95fn resolve_clip_path_transform(node: SvgNode, state: &converter::State) -> Option<Transform> {
96 // Do not use Node::attribute::<Transform>, because it will always
97 // return a valid transform.
98
99 let value: &str = match node.attribute(AId::Transform) {
100 Some(v) => v,
101 None => return Some(Transform::default()),
102 };
103
104 let ts = match svgtypes::Transform::from_str(value) {
105 Ok(v) => v,
106 Err(_) => {
107 log::warn!("Failed to parse {} value: '{}'.", AId::Transform, value);
108 return None;
109 }
110 };
111
112 let ts = Transform::from_row(
113 ts.a as f32,
114 ts.b as f32,
115 ts.c as f32,
116 ts.d as f32,
117 ts.e as f32,
118 ts.f as f32,
119 );
120
121 if ts.is_valid() {
122 Some(node.resolve_transform(AId::Transform, state))
123 } else {
124 None
125 }
126}
127