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 svgtypes::{Length, LengthUnit as Unit};
8
9use super::svgtree::{AId, EId, SvgNode};
10use super::{converter, OptionLog};
11use crate::{Group, Mask, MaskType, Node, NonEmptyString, NonZeroRect, Transform, Units};
12
13pub(crate) fn convert(
14 node: SvgNode,
15 state: &converter::State,
16 object_bbox: Option<NonZeroRect>,
17 cache: &mut converter::Cache,
18) -> Option<Arc<Mask>> {
19 // A `mask` attribute must reference a `mask` element.
20 if node.tag_name() != Some(EId::Mask) {
21 return None;
22 }
23
24 let units = node
25 .attribute(AId::MaskUnits)
26 .unwrap_or(Units::ObjectBoundingBox);
27
28 let content_units = node
29 .attribute(AId::MaskContentUnits)
30 .unwrap_or(Units::UserSpaceOnUse);
31
32 // Check if this element was already converted.
33 //
34 // Only `userSpaceOnUse` masks can be shared,
35 // because `objectBoundingBox` one will be converted into user one
36 // and will become node-specific.
37 let cacheable = units == Units::UserSpaceOnUse && content_units == Units::UserSpaceOnUse;
38 if cacheable {
39 if let Some(mask) = cache.masks.get(node.element_id()) {
40 return Some(mask.clone());
41 }
42 }
43
44 let rect = NonZeroRect::from_xywh(
45 node.convert_length(AId::X, units, state, Length::new(-10.0, Unit::Percent)),
46 node.convert_length(AId::Y, units, state, Length::new(-10.0, Unit::Percent)),
47 node.convert_length(AId::Width, units, state, Length::new(120.0, Unit::Percent)),
48 node.convert_length(AId::Height, units, state, Length::new(120.0, Unit::Percent)),
49 );
50 let mut rect =
51 rect.log_none(|| log::warn!("Mask '{}' has an invalid size. Skipped.", node.element_id()))?;
52
53 let mut mask_all = false;
54 if units == Units::ObjectBoundingBox {
55 if let Some(bbox) = object_bbox {
56 rect = rect.bbox_transform(bbox)
57 } else {
58 // When mask units are `objectBoundingBox` and bbox is zero-sized - the whole
59 // element should be masked.
60 // Technically an UB, but this is what Chrome and Firefox do.
61 mask_all = true;
62 }
63 }
64
65 let mut id = NonEmptyString::new(node.element_id().to_string())?;
66 // Generate ID only when we're parsing `objectBoundingBox` mask for the second time.
67 if !cacheable && cache.masks.contains_key(id.get()) {
68 id = cache.gen_mask_id();
69 }
70 let id_copy = id.get().to_string();
71
72 if mask_all {
73 let mask = Arc::new(Mask {
74 id,
75 rect,
76 kind: MaskType::Luminance,
77 mask: None,
78 root: Group::empty(),
79 });
80 cache.masks.insert(id_copy, mask.clone());
81 return Some(mask);
82 }
83
84 // Resolve linked mask.
85 let mut mask = None;
86 if let Some(link) = node.attribute::<SvgNode>(AId::Mask) {
87 mask = convert(link, state, object_bbox, cache);
88
89 // Linked `mask` must be valid.
90 if mask.is_none() {
91 return None;
92 }
93 }
94
95 let kind = if node.attribute(AId::MaskType) == Some("alpha") {
96 MaskType::Alpha
97 } else {
98 MaskType::Luminance
99 };
100
101 let mut mask = Mask {
102 id,
103 rect,
104 kind,
105 mask,
106 root: Group::empty(),
107 };
108
109 // To emulate content `objectBoundingBox` units we have to put
110 // mask children into a group with a transform.
111 let mut subroot = None;
112 if content_units == Units::ObjectBoundingBox {
113 let object_bbox = match object_bbox {
114 Some(v) => v,
115 None => {
116 log::warn!("Masking of zero-sized shapes is not allowed.");
117 return None;
118 }
119 };
120
121 let mut g = Group::empty();
122 g.transform = Transform::from_bbox(object_bbox);
123 // Make sure to set `abs_transform`, because it must propagate to all children.
124 g.abs_transform = g.transform;
125
126 subroot = Some(g);
127 }
128
129 {
130 // Prefer `subroot` to `mask.root`.
131 let real_root = subroot.as_mut().unwrap_or(&mut mask.root);
132 converter::convert_children(node, state, cache, real_root);
133
134 // A mask without children at this point is invalid.
135 // Only masks with zero bbox and `objectBoundingBox` can be empty.
136 if !real_root.has_children() {
137 return None;
138 }
139 }
140
141 if let Some(mut subroot) = subroot {
142 subroot.calculate_bounding_boxes();
143 mask.root.children.push(Node::Group(Box::new(subroot)));
144 }
145
146 mask.root.calculate_bounding_boxes();
147
148 let mask = Arc::new(mask);
149 cache.masks.insert(id_copy, mask.clone());
150 Some(mask)
151}
152