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 strict_num::PositiveF32;
9use svgtypes::{Length, LengthUnit as Unit};
10
11use super::converter::{self, Cache, SvgColorExt};
12use super::svgtree::{AId, EId, SvgNode};
13use super::OptionLog;
14use crate::*;
15
16pub(crate) enum ServerOrColor {
17 Server(Paint),
18 Color { color: Color, opacity: Opacity },
19}
20
21pub(crate) fn convert(
22 node: SvgNode,
23 state: &converter::State,
24 cache: &mut converter::Cache,
25) -> Option<ServerOrColor> {
26 // Check for existing.
27 if let Some(paint: &Paint) = cache.paint.get(node.element_id()) {
28 return Some(ServerOrColor::Server(paint.clone()));
29 }
30
31 // Unwrap is safe, because we already checked for is_paint_server().
32 let paint: Option = match node.tag_name().unwrap() {
33 EId::LinearGradient => convert_linear(node, state),
34 EId::RadialGradient => convert_radial(node, state),
35 EId::Pattern => convert_pattern(node, state, cache),
36 _ => unreachable!(),
37 };
38
39 if let Some(ServerOrColor::Server(ref paint: &Paint)) = paint {
40 cache
41 .paint
42 .insert(k:node.element_id().to_string(), v:paint.clone());
43 }
44
45 paint
46}
47
48#[inline(never)]
49fn convert_linear(node: SvgNode, state: &converter::State) -> Option<ServerOrColor> {
50 let id = NonEmptyString::new(node.element_id().to_string())?;
51
52 let stops = convert_stops(find_gradient_with_stops(node)?);
53 if stops.len() < 2 {
54 return stops_to_color(&stops);
55 }
56
57 let units = convert_units(node, AId::GradientUnits, Units::ObjectBoundingBox);
58 let transform = node.resolve_transform(AId::GradientTransform, state);
59
60 let gradient = LinearGradient {
61 x1: resolve_number(node, AId::X1, units, state, Length::zero()),
62 y1: resolve_number(node, AId::Y1, units, state, Length::zero()),
63 x2: resolve_number(
64 node,
65 AId::X2,
66 units,
67 state,
68 Length::new(100.0, Unit::Percent),
69 ),
70 y2: resolve_number(node, AId::Y2, units, state, Length::zero()),
71 base: BaseGradient {
72 id,
73 units,
74 transform,
75 spread_method: convert_spread_method(node),
76 stops,
77 },
78 };
79
80 Some(ServerOrColor::Server(Paint::LinearGradient(Arc::new(
81 gradient,
82 ))))
83}
84
85#[inline(never)]
86fn convert_radial(node: SvgNode, state: &converter::State) -> Option<ServerOrColor> {
87 let id = NonEmptyString::new(node.element_id().to_string())?;
88
89 let stops = convert_stops(find_gradient_with_stops(node)?);
90 if stops.len() < 2 {
91 return stops_to_color(&stops);
92 }
93
94 let units = convert_units(node, AId::GradientUnits, Units::ObjectBoundingBox);
95 let r = resolve_number(node, AId::R, units, state, Length::new(50.0, Unit::Percent));
96
97 // 'A value of zero will cause the area to be painted as a single color
98 // using the color and opacity of the last gradient stop.'
99 //
100 // https://www.w3.org/TR/SVG11/pservers.html#RadialGradientElementRAttribute
101 if !r.is_valid_length() {
102 let stop = stops.last().unwrap();
103 return Some(ServerOrColor::Color {
104 color: stop.color,
105 opacity: stop.opacity,
106 });
107 }
108
109 let spread_method = convert_spread_method(node);
110 let cx = resolve_number(
111 node,
112 AId::Cx,
113 units,
114 state,
115 Length::new(50.0, Unit::Percent),
116 );
117 let cy = resolve_number(
118 node,
119 AId::Cy,
120 units,
121 state,
122 Length::new(50.0, Unit::Percent),
123 );
124 let fx = resolve_number(node, AId::Fx, units, state, Length::new_number(cx as f64));
125 let fy = resolve_number(node, AId::Fy, units, state, Length::new_number(cy as f64));
126 let transform = node.resolve_transform(AId::GradientTransform, state);
127
128 let gradient = RadialGradient {
129 cx,
130 cy,
131 r: PositiveF32::new(r).unwrap(),
132 fx,
133 fy,
134 base: BaseGradient {
135 id,
136 units,
137 transform,
138 spread_method,
139 stops,
140 },
141 };
142
143 Some(ServerOrColor::Server(Paint::RadialGradient(Arc::new(
144 gradient,
145 ))))
146}
147
148#[inline(never)]
149fn convert_pattern(
150 node: SvgNode,
151 state: &converter::State,
152 cache: &mut converter::Cache,
153) -> Option<ServerOrColor> {
154 let node_with_children = find_pattern_with_children(node)?;
155
156 let id = NonEmptyString::new(node.element_id().to_string())?;
157
158 let view_box = {
159 let n1 = resolve_attr(node, AId::ViewBox);
160 let n2 = resolve_attr(node, AId::PreserveAspectRatio);
161 n1.parse_viewbox().map(|vb| ViewBox {
162 rect: vb,
163 aspect: n2.attribute(AId::PreserveAspectRatio).unwrap_or_default(),
164 })
165 };
166
167 let units = convert_units(node, AId::PatternUnits, Units::ObjectBoundingBox);
168 let content_units = convert_units(node, AId::PatternContentUnits, Units::UserSpaceOnUse);
169
170 let transform = node.resolve_transform(AId::PatternTransform, state);
171
172 let rect = NonZeroRect::from_xywh(
173 resolve_number(node, AId::X, units, state, Length::zero()),
174 resolve_number(node, AId::Y, units, state, Length::zero()),
175 resolve_number(node, AId::Width, units, state, Length::zero()),
176 resolve_number(node, AId::Height, units, state, Length::zero()),
177 );
178 let rect = rect.log_none(|| {
179 log::warn!(
180 "Pattern '{}' has an invalid size. Skipped.",
181 node.element_id()
182 )
183 })?;
184
185 let mut patt = Pattern {
186 id,
187 units,
188 content_units,
189 transform,
190 rect,
191 view_box,
192 root: Group::empty(),
193 };
194
195 converter::convert_children(node_with_children, state, cache, &mut patt.root);
196
197 if !patt.root.has_children() {
198 return None;
199 }
200
201 patt.root.calculate_bounding_boxes();
202
203 Some(ServerOrColor::Server(Paint::Pattern(Arc::new(patt))))
204}
205
206fn convert_spread_method(node: SvgNode) -> SpreadMethod {
207 let node: SvgNode<'_, '_> = resolve_attr(node, name:AId::SpreadMethod);
208 node.attribute(AId::SpreadMethod).unwrap_or_default()
209}
210
211pub(crate) fn convert_units(node: SvgNode, name: AId, def: Units) -> Units {
212 let node: SvgNode<'_, '_> = resolve_attr(node, name);
213 node.attribute(name).unwrap_or(default:def)
214}
215
216fn find_gradient_with_stops<'a, 'input: 'a>(
217 node: SvgNode<'a, 'input>,
218) -> Option<SvgNode<'a, 'input>> {
219 for link: SvgNode<'_, '_> in node.href_iter() {
220 if !link.tag_name().unwrap().is_gradient() {
221 log::warn!(
222 "Gradient '{}' cannot reference '{}' via 'xlink:href'.",
223 node.element_id(),
224 link.tag_name().unwrap()
225 );
226 return None;
227 }
228
229 if link.children().any(|n: SvgNode<'_, '_>| n.tag_name() == Some(EId::Stop)) {
230 return Some(link);
231 }
232 }
233
234 None
235}
236
237fn find_pattern_with_children<'a, 'input: 'a>(
238 node: SvgNode<'a, 'input>,
239) -> Option<SvgNode<'a, 'input>> {
240 for link: SvgNode<'_, '_> in node.href_iter() {
241 if link.tag_name() != Some(EId::Pattern) {
242 log::warn!(
243 "Pattern '{}' cannot reference '{}' via 'xlink:href'.",
244 node.element_id(),
245 link.tag_name().unwrap()
246 );
247 return None;
248 }
249
250 if link.has_children() {
251 return Some(link);
252 }
253 }
254
255 None
256}
257
258fn convert_stops(grad: SvgNode) -> Vec<Stop> {
259 let mut stops = Vec::new();
260
261 {
262 let mut prev_offset = Length::zero();
263 for stop in grad.children() {
264 if stop.tag_name() != Some(EId::Stop) {
265 log::warn!("Invalid gradient child: '{:?}'.", stop.tag_name().unwrap());
266 continue;
267 }
268
269 // `number` can be either a number or a percentage.
270 let offset = stop.attribute(AId::Offset).unwrap_or(prev_offset);
271 let offset = match offset.unit {
272 Unit::None => offset.number,
273 Unit::Percent => offset.number / 100.0,
274 _ => prev_offset.number,
275 };
276 prev_offset = Length::new_number(offset);
277 let offset = crate::f32_bound(0.0, offset as f32, 1.0);
278
279 let (color, opacity) = match stop.attribute(AId::StopColor) {
280 Some("currentColor") => stop
281 .find_attribute(AId::Color)
282 .unwrap_or_else(svgtypes::Color::black),
283 Some(value) => {
284 if let Ok(c) = svgtypes::Color::from_str(value) {
285 c
286 } else {
287 log::warn!("Failed to parse stop-color value: '{}'.", value);
288 svgtypes::Color::black()
289 }
290 }
291 _ => svgtypes::Color::black(),
292 }
293 .split_alpha();
294
295 let stop_opacity = stop
296 .attribute::<Opacity>(AId::StopOpacity)
297 .unwrap_or(Opacity::ONE);
298 stops.push(Stop {
299 offset: StopOffset::new_clamped(offset),
300 color,
301 opacity: opacity * stop_opacity,
302 });
303 }
304 }
305
306 // Remove stops with equal offset.
307 //
308 // Example:
309 // offset="0.5"
310 // offset="0.7"
311 // offset="0.7" <-- this one should be removed
312 // offset="0.7"
313 // offset="0.9"
314 if stops.len() >= 3 {
315 let mut i = 0;
316 while i < stops.len() - 2 {
317 let offset1 = stops[i + 0].offset.get();
318 let offset2 = stops[i + 1].offset.get();
319 let offset3 = stops[i + 2].offset.get();
320
321 if offset1.approx_eq_ulps(&offset2, 4) && offset2.approx_eq_ulps(&offset3, 4) {
322 // Remove offset in the middle.
323 stops.remove(i + 1);
324 } else {
325 i += 1;
326 }
327 }
328 }
329
330 // Remove zeros.
331 //
332 // From:
333 // offset="0.0"
334 // offset="0.0"
335 // offset="0.7"
336 //
337 // To:
338 // offset="0.0"
339 // offset="0.00000001"
340 // offset="0.7"
341 if stops.len() >= 2 {
342 let mut i = 0;
343 while i < stops.len() - 1 {
344 let offset1 = stops[i + 0].offset.get();
345 let offset2 = stops[i + 1].offset.get();
346
347 if offset1.approx_eq_ulps(&0.0, 4) && offset2.approx_eq_ulps(&0.0, 4) {
348 stops[i + 1].offset = StopOffset::new_clamped(offset1 + f32::EPSILON);
349 }
350
351 i += 1;
352 }
353 }
354
355 // Shift equal offsets.
356 //
357 // From:
358 // offset="0.5"
359 // offset="0.7"
360 // offset="0.7"
361 //
362 // To:
363 // offset="0.5"
364 // offset="0.699999999"
365 // offset="0.7"
366 {
367 let mut i = 1;
368 while i < stops.len() {
369 let offset1 = stops[i - 1].offset.get();
370 let offset2 = stops[i - 0].offset.get();
371
372 // Next offset must be smaller then previous.
373 if offset1 > offset2 || offset1.approx_eq_ulps(&offset2, 4) {
374 // Make previous offset a bit smaller.
375 let new_offset = offset1 - f32::EPSILON;
376 stops[i - 1].offset = StopOffset::new_clamped(new_offset);
377 stops[i - 0].offset = StopOffset::new_clamped(offset1);
378 }
379
380 i += 1;
381 }
382 }
383
384 stops
385}
386
387#[inline(never)]
388pub(crate) fn resolve_number(
389 node: SvgNode,
390 name: AId,
391 units: Units,
392 state: &converter::State,
393 def: Length,
394) -> f32 {
395 resolve_attr(node, name).convert_length(aid:name, object_units:units, state, def)
396}
397
398fn resolve_attr<'a, 'input: 'a>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> {
399 if node.has_attribute(aid:name) {
400 return node;
401 }
402
403 match node.tag_name().unwrap() {
404 EId::LinearGradient => resolve_lg_attr(node, name),
405 EId::RadialGradient => resolve_rg_attr(node, name),
406 EId::Pattern => resolve_pattern_attr(node, name),
407 EId::Filter => resolve_filter_attr(node, aid:name),
408 _ => node,
409 }
410}
411
412fn resolve_lg_attr<'a, 'input: 'a>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> {
413 for link in node.href_iter() {
414 let tag_name = match link.tag_name() {
415 Some(v) => v,
416 None => return node,
417 };
418
419 match (name, tag_name) {
420 // Coordinates can be resolved only from
421 // ref element with the same type.
422 (AId::X1, EId::LinearGradient)
423 | (AId::Y1, EId::LinearGradient)
424 | (AId::X2, EId::LinearGradient)
425 | (AId::Y2, EId::LinearGradient)
426 // Other attributes can be resolved
427 // from any kind of gradient.
428 | (AId::GradientUnits, EId::LinearGradient)
429 | (AId::GradientUnits, EId::RadialGradient)
430 | (AId::SpreadMethod, EId::LinearGradient)
431 | (AId::SpreadMethod, EId::RadialGradient)
432 | (AId::GradientTransform, EId::LinearGradient)
433 | (AId::GradientTransform, EId::RadialGradient) => {
434 if link.has_attribute(name) {
435 return link;
436 }
437 }
438 _ => break,
439 }
440 }
441
442 node
443}
444
445fn resolve_rg_attr<'a, 'input>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> {
446 for link in node.href_iter() {
447 let tag_name = match link.tag_name() {
448 Some(v) => v,
449 None => return node,
450 };
451
452 match (name, tag_name) {
453 // Coordinates can be resolved only from
454 // ref element with the same type.
455 (AId::Cx, EId::RadialGradient)
456 | (AId::Cy, EId::RadialGradient)
457 | (AId::R, EId::RadialGradient)
458 | (AId::Fx, EId::RadialGradient)
459 | (AId::Fy, EId::RadialGradient)
460 // Other attributes can be resolved
461 // from any kind of gradient.
462 | (AId::GradientUnits, EId::LinearGradient)
463 | (AId::GradientUnits, EId::RadialGradient)
464 | (AId::SpreadMethod, EId::LinearGradient)
465 | (AId::SpreadMethod, EId::RadialGradient)
466 | (AId::GradientTransform, EId::LinearGradient)
467 | (AId::GradientTransform, EId::RadialGradient) => {
468 if link.has_attribute(name) {
469 return link;
470 }
471 }
472 _ => break,
473 }
474 }
475
476 node
477}
478
479fn resolve_pattern_attr<'a, 'input: 'a>(
480 node: SvgNode<'a, 'input>,
481 name: AId,
482) -> SvgNode<'a, 'input> {
483 for link: SvgNode<'_, '_> in node.href_iter() {
484 let tag_name: EId = match link.tag_name() {
485 Some(v: EId) => v,
486 None => return node,
487 };
488
489 if tag_name != EId::Pattern {
490 break;
491 }
492
493 if link.has_attribute(aid:name) {
494 return link;
495 }
496 }
497
498 node
499}
500
501fn resolve_filter_attr<'a, 'input: 'a>(node: SvgNode<'a, 'input>, aid: AId) -> SvgNode<'a, 'input> {
502 for link: SvgNode<'_, '_> in node.href_iter() {
503 let tag_name: EId = match link.tag_name() {
504 Some(v: EId) => v,
505 None => return node,
506 };
507
508 if tag_name != EId::Filter {
509 break;
510 }
511
512 if link.has_attribute(aid) {
513 return link;
514 }
515 }
516
517 node
518}
519
520fn stops_to_color(stops: &[Stop]) -> Option<ServerOrColor> {
521 if stops.is_empty() {
522 None
523 } else {
524 Some(ServerOrColor::Color {
525 color: stops[0].color,
526 opacity: stops[0].opacity,
527 })
528 }
529}
530
531// Convert object units to user one.
532
533pub fn to_user_coordinates(group: &mut Group, text_bbox: Option<Rect>, cache: &mut Cache) {
534 for child: &mut Node in &mut group.children {
535 node_to_user_coordinates(node:child, text_bbox, cache);
536 }
537}
538
539// When parsing clipPaths, masks and filters we already know group's bounding box.
540// But with gradients and patterns we don't, because we have to know text bounding box
541// before we even parsed it. Which is impossible.
542// Therefore our only choice is to parse gradients and patterns preserving their units
543// and then replace them with `userSpaceOnUse` after the whole tree parsing is finished.
544// So while gradients and patterns do still store their units,
545// they are not exposed in the public API and for the caller they are always `userSpaceOnUse`.
546fn node_to_user_coordinates(node: &mut Node, text_bbox: Option<Rect>, cache: &mut Cache) {
547 match node {
548 Node::Group(ref mut g) => {
549 // No need to check clip paths, because they cannot have paint servers.
550
551 if let Some(ref mut mask) = g.mask {
552 if let Some(ref mut mask) = Arc::get_mut(mask) {
553 to_user_coordinates(&mut mask.root, None, cache);
554
555 if let Some(ref mut sub_mask) = mask.mask {
556 if let Some(ref mut sub_mask) = Arc::get_mut(sub_mask) {
557 to_user_coordinates(&mut sub_mask.root, None, cache);
558 }
559 }
560 }
561 }
562
563 for filter in &mut g.filters {
564 if let Some(ref mut filter) = Arc::get_mut(filter) {
565 for primitive in &mut filter.primitives {
566 if let filter::Kind::Image(ref mut image) = primitive.kind {
567 if let filter::ImageKind::Use(ref mut use_node) = image.data {
568 to_user_coordinates(use_node, None, cache);
569 }
570 }
571 }
572 }
573 }
574
575 to_user_coordinates(g, text_bbox, cache);
576 }
577 Node::Path(ref mut path) => {
578 // Paths inside `Text::flattened` are special and must use text's bounding box
579 // instead of their own.
580 let bbox = text_bbox.unwrap_or(path.bounding_box);
581
582 process_fill(&mut path.fill, bbox, cache);
583 process_stroke(&mut path.stroke, bbox, cache);
584 }
585 Node::Image(ref mut image) => {
586 if let ImageKind::SVG(ref mut tree) = image.kind {
587 to_user_coordinates(&mut tree.root, None, cache);
588 }
589 }
590 Node::Text(ref mut text) => {
591 // By the SVG spec, `tspan` doesn't have a bbox and uses the parent `text` bbox.
592 // Therefore we have to use text's bbox when converting tspan and flatted text
593 // paint servers.
594 let bbox = text.bounding_box;
595
596 for chunk in &mut text.chunks {
597 for span in &mut chunk.spans {
598 process_fill(&mut span.fill, bbox, cache);
599 process_stroke(&mut span.stroke, bbox, cache);
600 process_text_decoration(&mut span.decoration.underline, bbox, cache);
601 process_text_decoration(&mut span.decoration.overline, bbox, cache);
602 process_text_decoration(&mut span.decoration.line_through, bbox, cache);
603 }
604 }
605
606 to_user_coordinates(&mut text.flattened, Some(bbox), cache);
607 }
608 }
609}
610
611fn process_fill(fill: &mut Option<Fill>, bbox: Rect, cache: &mut Cache) {
612 let mut ok: bool = false;
613 if let Some(ref mut fill: &mut Fill) = fill {
614 ok = process_paint(&mut fill.paint, bbox, cache);
615 }
616 if !ok {
617 *fill = None;
618 }
619}
620
621fn process_stroke(stroke: &mut Option<Stroke>, bbox: Rect, cache: &mut Cache) {
622 let mut ok: bool = false;
623 if let Some(ref mut stroke: &mut Stroke) = stroke {
624 ok = process_paint(&mut stroke.paint, bbox, cache);
625 }
626 if !ok {
627 *stroke = None;
628 }
629}
630
631fn process_paint(paint: &mut Paint, bbox: Rect, cache: &mut Cache) -> bool {
632 if paint.units() == Units::ObjectBoundingBox
633 || paint.content_units() == Units::ObjectBoundingBox
634 {
635 if paint.to_user_coordinates(bbox, cache).is_none() {
636 return false;
637 }
638 }
639
640 if let Paint::Pattern(ref mut patt: &mut Arc) = paint {
641 if let Some(ref mut patt: &mut &mut Pattern) = Arc::get_mut(this:patt) {
642 to_user_coordinates(&mut patt.root, text_bbox:None, cache);
643 }
644 }
645
646 true
647}
648
649fn process_text_decoration(style: &mut Option<TextDecorationStyle>, bbox: Rect, cache: &mut Cache) {
650 if let Some(ref mut style: &mut TextDecorationStyle) = style {
651 process_fill(&mut style.fill, bbox, cache);
652 process_stroke(&mut style.stroke, bbox, cache);
653 }
654}
655
656impl Paint {
657 fn to_user_coordinates(&mut self, bbox: Rect, cache: &mut Cache) -> Option<()> {
658 let name = if matches!(self, Paint::Pattern(_)) {
659 "Pattern"
660 } else {
661 "Gradient"
662 };
663 let bbox = bbox
664 .to_non_zero_rect()
665 .log_none(|| log::warn!("{} on zero-sized shapes is not allowed.", name))?;
666
667 // `Arc::get_mut()` allow us to modify some paint servers in-place.
668 // This reduces the amount of cloning and preserves the original ID as well.
669 match self {
670 Paint::Color(_) => {} // unreachable
671 Paint::LinearGradient(ref mut lg) => {
672 let transform = lg.transform.post_concat(Transform::from_bbox(bbox));
673 if let Some(ref mut lg) = Arc::get_mut(lg) {
674 lg.base.transform = transform;
675 lg.base.units = Units::UserSpaceOnUse;
676 } else {
677 *lg = Arc::new(LinearGradient {
678 x1: lg.x1,
679 y1: lg.y1,
680 x2: lg.x2,
681 y2: lg.y2,
682 base: BaseGradient {
683 id: cache.gen_linear_gradient_id(),
684 units: Units::UserSpaceOnUse,
685 transform,
686 spread_method: lg.spread_method,
687 stops: lg.stops.clone(),
688 },
689 });
690 }
691 }
692 Paint::RadialGradient(ref mut rg) => {
693 let transform = rg.transform.post_concat(Transform::from_bbox(bbox));
694 if let Some(ref mut rg) = Arc::get_mut(rg) {
695 rg.base.transform = transform;
696 rg.base.units = Units::UserSpaceOnUse;
697 } else {
698 *rg = Arc::new(RadialGradient {
699 cx: rg.cx,
700 cy: rg.cy,
701 r: rg.r,
702 fx: rg.fx,
703 fy: rg.fy,
704 base: BaseGradient {
705 id: cache.gen_radial_gradient_id(),
706 units: Units::UserSpaceOnUse,
707 transform,
708 spread_method: rg.spread_method,
709 stops: rg.stops.clone(),
710 },
711 });
712 }
713 }
714 Paint::Pattern(ref mut patt) => {
715 let rect = if patt.units == Units::ObjectBoundingBox {
716 patt.rect.bbox_transform(bbox)
717 } else {
718 patt.rect
719 };
720
721 if let Some(ref mut patt) = Arc::get_mut(patt) {
722 patt.rect = rect;
723 patt.units = Units::UserSpaceOnUse;
724
725 if patt.content_units == Units::ObjectBoundingBox && patt.view_box().is_none() {
726 // No need to shift patterns.
727 let transform = Transform::from_scale(bbox.width(), bbox.height());
728
729 let mut g = std::mem::replace(&mut patt.root, Group::empty());
730 g.transform = transform;
731 g.abs_transform = transform;
732
733 patt.root.children.push(Node::Group(Box::new(g)));
734 patt.root.calculate_bounding_boxes();
735 }
736
737 patt.content_units = Units::UserSpaceOnUse;
738 } else {
739 let root = if patt.content_units == Units::ObjectBoundingBox
740 && patt.view_box().is_none()
741 {
742 // No need to shift patterns.
743 let transform = Transform::from_scale(bbox.width(), bbox.height());
744
745 let mut g = patt.root.clone();
746 g.transform = transform;
747 g.abs_transform = transform;
748
749 let mut root = Group::empty();
750 root.children.push(Node::Group(Box::new(g)));
751 root.calculate_bounding_boxes();
752 root
753 } else {
754 patt.root.clone()
755 };
756
757 *patt = Arc::new(Pattern {
758 id: cache.gen_pattern_id(),
759 units: Units::UserSpaceOnUse,
760 content_units: Units::UserSpaceOnUse,
761 transform: patt.transform,
762 rect,
763 view_box: patt.view_box,
764 root,
765 })
766 }
767 }
768 }
769
770 Some(())
771 }
772}
773
774impl Paint {
775 #[inline]
776 pub(crate) fn units(&self) -> Units {
777 match self {
778 Self::Color(_) => Units::UserSpaceOnUse,
779 Self::LinearGradient(ref lg: &Arc) => lg.units,
780 Self::RadialGradient(ref rg: &Arc) => rg.units,
781 Self::Pattern(ref patt: &Arc) => patt.units,
782 }
783 }
784
785 #[inline]
786 pub(crate) fn content_units(&self) -> Units {
787 match self {
788 Self::Pattern(ref patt: &Arc) => patt.content_units,
789 _ => Units::UserSpaceOnUse,
790 }
791 }
792}
793