1use std::rc::Rc;
6
7use float_cmp::approx_eq;
8
9use crate::aspect_ratio::AspectRatio;
10use crate::cairo_path::CairoPath;
11use crate::color::Color;
12use crate::coord_units::CoordUnits;
13use crate::dasharray::Dasharray;
14use crate::document::{AcquiredNode, AcquiredNodes};
15use crate::drawing_ctx::{DrawingCtx, FontOptions, Viewport, pango_layout_to_cairo_path};
16use crate::element::{Element, ElementData};
17use crate::error::{AcquireError, InternalRenderingError};
18use crate::filter::FilterValueList;
19use crate::length::*;
20use crate::node::*;
21use crate::paint_server::{PaintSource, UserSpacePaintSource};
22use crate::path_builder::Path as SvgPath;
23use crate::properties::{
24 self, ClipRule, ComputedValues, Direction, FillRule, FontFamily, FontStretch, FontStyle,
25 FontVariant, FontWeight, ImageRendering, Isolation, MixBlendMode, Opacity, Overflow,
26 PaintOrder, ShapeRendering, StrokeDasharray, StrokeLinecap, StrokeLinejoin, StrokeMiterlimit,
27 TextDecoration, TextRendering, UnicodeBidi, VectorEffect, XmlLang,
28};
29use crate::rect::Rect;
30use crate::rsvg_log;
31use crate::session::Session;
32use crate::surface_utils::shared_surface::SharedImageSurface;
33use crate::transform::{Transform, ValidTransform};
34use crate::unit_interval::UnitInterval;
35use crate::viewbox::ViewBox;
36use crate::{borrow_element_as, is_element_of_type};
37
38pub struct StackingContext {
52 pub element_name: String,
53 pub transform: Transform,
54 pub is_visible: bool,
55 pub opacity: Opacity,
56 pub filter: Option<Filter>,
57 pub clip_rect: Option<Rect>,
58 pub clip_in_object_space: Option<Node>,
59 pub clip_path: Option<ClipPath>,
60 pub mask: Option<Node>,
61 pub mix_blend_mode: MixBlendMode,
62 pub isolation: Isolation,
63
64 pub link_target: Option<String>,
66}
67
68pub struct ClipPath {
74 pub clip_units: CoordUnits,
75 pub transform: Transform,
76 pub paths: Vec<ClipPathItem>,
77 pub clip_path: Option<Box<ClipPath>>,
78}
79
80pub struct ClipPathItem {
82 pub transform: Transform,
83 pub path: CairoPath,
84 pub clip_rule: ClipRule,
85 pub clip_path: Option<Box<ClipPath>>,
86}
87
88pub struct Layer {
90 pub kind: LayerKind,
91 pub stacking_ctx: StackingContext,
92}
93
94pub enum LayerKind {
95 Shape(Box<Shape>),
96 Text(Box<Text>),
97 Image(Box<Image>),
98 Group(Box<Group>),
99}
100
101pub struct Group {
102 pub children: Vec<Layer>,
103 pub establish_viewport: Option<LayoutViewport>,
104 pub extents: Option<Rect>,
105}
106
107pub struct LayoutViewport {
109 pub geometry: Rect,
113
114 pub vbox: Option<ViewBox>,
116
117 pub preserve_aspect_ratio: AspectRatio,
119
120 pub overflow: Overflow,
122}
123
124pub struct Stroke {
126 pub width: f64,
127 pub miter_limit: StrokeMiterlimit,
128 pub line_cap: StrokeLinecap,
129 pub line_join: StrokeLinejoin,
130 pub dash_offset: f64,
131 pub dashes: Box<[f64]>,
132 pub non_scaling: bool,
134}
135
136pub struct Path {
138 pub cairo_path: CairoPath,
139 pub path: Rc<SvgPath>,
140 pub extents: Option<Rect>,
141}
142
143pub struct Shape {
145 pub path: Path,
146 pub paint_order: PaintOrder,
147 pub stroke_paint: UserSpacePaintSource,
148 pub fill_paint: UserSpacePaintSource,
149 pub stroke: Stroke,
150 pub fill_rule: FillRule,
151 pub clip_rule: ClipRule,
152 pub shape_rendering: ShapeRendering,
153 pub marker_start: Marker,
154 pub marker_mid: Marker,
155 pub marker_end: Marker,
156}
157
158pub struct Marker {
159 pub node_ref: Option<Node>,
160 pub context_stroke: Rc<PaintSource>,
161 pub context_fill: Rc<PaintSource>,
162}
163
164pub struct Image {
166 pub surface: SharedImageSurface,
167 pub rect: Rect,
168 pub aspect: AspectRatio,
169 pub overflow: Overflow,
170 pub image_rendering: ImageRendering,
171}
172
173pub struct TextSpan {
175 pub layout: pango::Layout,
176 pub gravity: pango::Gravity,
177 pub extents: Option<Rect>,
178 pub is_visible: bool,
179 pub x: f64,
180 pub y: f64,
181 pub paint_order: PaintOrder,
182 pub stroke: Stroke,
183 pub stroke_paint: UserSpacePaintSource,
184 pub fill_paint: UserSpacePaintSource,
185 pub text_rendering: TextRendering,
186 pub link_target: Option<String>,
187}
188
189pub struct Text {
191 pub spans: Vec<TextSpan>,
192 pub extents: Option<Rect>,
193}
194
195pub struct FontProperties {
197 pub xml_lang: XmlLang,
198 pub unicode_bidi: UnicodeBidi,
199 pub direction: Direction,
200 pub font_family: FontFamily,
201 pub font_style: FontStyle,
202 pub font_variant: FontVariant,
203 pub font_weight: FontWeight,
204 pub font_stretch: FontStretch,
205 pub font_size: f64,
206 pub letter_spacing: f64,
207 pub text_decoration: TextDecoration,
208}
209
210pub struct Filter {
211 pub filter_list: FilterValueList,
212 pub current_color: Color,
213 pub stroke_paint_source: Rc<PaintSource>,
214 pub fill_paint_source: Rc<PaintSource>,
215 pub normalize_values: NormalizeValues,
216}
217
218fn get_filter(
219 values: &ComputedValues,
220 acquired_nodes: &mut AcquiredNodes<'_>,
221 referencing_element_name: &str,
222 session: &Session,
223) -> Option<Filter> {
224 match values.filter() {
225 properties::Filter::None => None,
226
227 properties::Filter::List(filter_list) => Some(get_filter_from_filter_list(
228 filter_list,
229 acquired_nodes,
230 referencing_element_name,
231 values,
232 session,
233 )),
234 }
235}
236
237fn get_filter_from_filter_list(
238 filter_list: FilterValueList,
239 acquired_nodes: &mut AcquiredNodes<'_>,
240 referencing_element_name: &str,
241 values: &ComputedValues,
242 session: &Session,
243) -> Filter {
244 let current_color = values.color().0;
245
246 let stroke_paint_source = values.stroke().0.resolve(
247 acquired_nodes,
248 referencing_element_name,
249 values.stroke_opacity().0,
250 current_color,
251 None,
252 None,
253 session,
254 );
255
256 let fill_paint_source = values.fill().0.resolve(
257 acquired_nodes,
258 referencing_element_name,
259 values.fill_opacity().0,
260 current_color,
261 None,
262 None,
263 session,
264 );
265
266 let normalize_values = NormalizeValues::new(values);
267
268 Filter {
269 filter_list,
270 current_color,
271 stroke_paint_source,
272 fill_paint_source,
273 normalize_values,
274 }
275}
276
277fn acquire_clip_path(
278 source_element: &Element,
279 acquired_nodes: &mut AcquiredNodes<'_>,
280) -> Result<Option<AcquiredNode>, AcquireError> {
281 let values = source_element.get_computed_values();
282 let clip_path_prop = values.clip_path();
283
284 if let Some(node_id) = clip_path_prop.0.get() {
285 let source_element_name = format!("{source_element}");
286 let acquired = acquired_nodes.acquire(&source_element_name, node_id)?;
287
288 let candidate_clip_path_node = acquired.get().clone();
289
290 if is_element_of_type!(candidate_clip_path_node, ClipPath) {
291 Ok(Some(acquired))
292 } else {
293 Err(AcquireError::InvalidLinkType(node_id.clone()))
294 }
295 } else {
296 Ok(None)
297 }
298}
299
300fn acquire_clip_path_and_log_error(
301 session: &Session,
302 source_element: &Element,
303 acquired_nodes: &mut AcquiredNodes<'_>,
304) -> Option<AcquiredNode> {
305 match acquire_clip_path(source_element, acquired_nodes) {
306 Ok(node) => node,
307 Err(e) => {
308 rsvg_log!(session, "ignoring clip-path for {source_element}: {e}");
309 None
310 }
311 }
312}
313
314fn layout_clip_path(
315 session: &Session,
316 source_element: &Element,
317 font_options: &FontOptions,
318 acquired_nodes: &mut AcquiredNodes<'_>,
319 params: &NormalizeParams,
320 viewport: &Viewport,
321) -> Option<ClipPath> {
322 if let Some(acquired) = acquire_clip_path_and_log_error(session, source_element, acquired_nodes)
323 {
324 let clip_path_node = acquired.get();
325 let clip_path_elt = clip_path_node.borrow_element();
326 let clip_path_data = borrow_element_as!(clip_path_node, ClipPath);
327
328 let values = clip_path_elt.get_computed_values();
329
330 let clip_units = clip_path_data.get_units();
331 let transform = values.transform();
332
333 let paths = layout_paths_for_clip_path(
334 session,
335 clip_path_node,
336 font_options,
337 acquired_nodes,
338 params,
339 viewport,
340 );
341 let recursive_clip_path = layout_clip_path(
342 session,
343 &clip_path_elt,
344 font_options,
345 acquired_nodes,
346 params,
347 viewport,
348 )
349 .map(Box::new);
350
351 Some(ClipPath {
352 clip_units,
353 transform,
354 paths,
355 clip_path: recursive_clip_path,
356 })
357 } else {
358 None
359 }
360}
361
362fn path_from_text(
364 text: &crate::text::Text,
365 node: &Node,
366 cascaded: &CascadedValues<'_>,
367 session: &Session,
368 font_options: &FontOptions,
369 acquired_nodes: &mut AcquiredNodes<'_>,
370 viewport: &Viewport,
371) -> Result<CairoPath, Box<InternalRenderingError>> {
372 let text_layout = text.layout_text_spans(
373 node,
374 acquired_nodes,
375 cascaded,
376 viewport,
377 font_options.clone(),
378 session,
379 );
380
381 let mut result = CairoPath::empty();
382
383 for span in &text_layout.spans {
384 let path = pango_layout_to_cairo_path(span.x, span.y, &span.layout, span.gravity)?;
385
386 result.append(path);
389 }
390
391 Ok(result)
392}
393
394fn path_from_use_referenced_from_clip_path(
395 use_node: &Node,
396 session: &Session,
397 font_options: &FontOptions,
398 acquired_nodes: &mut AcquiredNodes<'_>,
399 viewport: &Viewport,
400) -> Option<ClipPathItem> {
401 let _use_acquired = match acquired_nodes.acquire_ref(use_node) {
405 Ok(n) => n,
406
407 _ => return None,
408 };
409
410 let use_element = use_node.borrow_element();
411 let use_element_name = format!("{use_element}");
412 let use_element_data = borrow_element_as!(use_node, Use);
413
414 let acquired = if let Some(link) = use_element_data.get_link() {
415 match acquired_nodes.acquire(&use_element_name, &link) {
416 Ok(acquired) => acquired,
417
418 _ => return None,
419 }
420 } else {
421 return None;
422 };
423
424 let use_values = use_element.get_computed_values();
425 let use_params = NormalizeParams::new(use_values, viewport);
426
427 let use_rect = use_element_data.get_rect(&use_params);
431 if use_rect.is_empty() {
432 return None;
433 }
434
435 let child = acquired.get();
436 if !element_can_be_used_inside_use_inside_clip_path(&child.borrow_element()) {
437 return None;
438 }
439
440 let child_cascaded = CascadedValues::new_from_values(
441 child, use_values, None, None, );
444 let child_values = child_cascaded.get();
445
446 if !child_values.is_displayed() || !child_values.is_visible() {
447 return None;
453 }
454
455 let use_transform = ValidTransform::try_from(
456 use_values
457 .transform()
458 .pre_translate(use_rect.x0, use_rect.y0)
459 .pre_transform(&child_values.transform()),
460 )
461 .ok()?;
462
463 match viewport.with_composed_transform(use_transform) {
464 Ok(use_viewport) => {
465 let params = NormalizeParams::new(child_values, &use_viewport);
466 let child_data = child.borrow_element_data();
467
468 let path = match *child_data {
469 ElementData::Path(ref e) => e.make_path(¶ms, child_values).to_cairo_path(false),
470 ElementData::Polygon(ref e) => {
471 e.make_path(¶ms, child_values).to_cairo_path(false)
472 }
473 ElementData::Polyline(ref e) => {
474 e.make_path(¶ms, child_values).to_cairo_path(false)
475 }
476 ElementData::Line(ref e) => e.make_path(¶ms, child_values).to_cairo_path(false),
477 ElementData::Rect(ref e) => e.make_path(¶ms, child_values).to_cairo_path(false),
478 ElementData::Circle(ref e) => {
479 e.make_path(¶ms, child_values).to_cairo_path(false)
480 }
481 ElementData::Ellipse(ref e) => {
482 e.make_path(¶ms, child_values).to_cairo_path(false)
483 }
484 ElementData::Text(ref e) => path_from_text(
485 e,
486 child,
487 &child_cascaded,
488 session,
489 font_options,
490 acquired_nodes,
491 viewport,
492 )
493 .ok()?,
494 _ => unreachable!(
495 "we already checked the allowed types of elements in <use> in <clipPath>"
496 ),
497 };
498
499 Some(ClipPathItem {
500 transform: *use_transform,
501 path,
502 clip_rule: child_values.clip_rule(),
503 clip_path: layout_clip_path(
504 session,
505 &child.borrow_element(),
506 font_options,
507 acquired_nodes,
508 ¶ms,
509 &use_viewport,
510 )
511 .map(Box::new),
512 })
513 }
514
515 Err(e) => {
516 rsvg_log!(session, "ignoring {use_node} inside clipPath: {e}");
517 None
518 }
519 }
520}
521
522pub fn element_can_be_used_inside_use_inside_clip_path(element: &Element) -> bool {
524 use ElementData::*;
525
526 matches!(
527 element.element_data,
528 Circle(_) | Ellipse(_) | Line(_) | Path(_) | Polygon(_) | Polyline(_) | Rect(_) | Text(_)
529 )
530}
531
532fn clip_path_item_from_node(
533 session: &Session,
534 node: &Node,
535 font_options: &FontOptions,
536 acquired_nodes: &mut AcquiredNodes<'_>,
537 params: &NormalizeParams,
538 viewport: &Viewport,
539) -> Option<ClipPathItem> {
540 let elt = node.borrow_element();
541 let data = node.borrow_element_data();
542 let values = elt.get_computed_values();
543
544 if !values.is_displayed() || !values.is_visible() {
545 return None;
551 }
552
553 if let ElementData::Use(_) = *data {
554 path_from_use_referenced_from_clip_path(
557 node,
558 session,
559 font_options,
560 acquired_nodes,
561 viewport,
562 )
563 } else {
564 let path = match *data {
565 ElementData::Path(ref e) => e.make_path(params, values).to_cairo_path(false),
566 ElementData::Polygon(ref e) => e.make_path(params, values).to_cairo_path(false),
567 ElementData::Polyline(ref e) => e.make_path(params, values).to_cairo_path(false),
568 ElementData::Line(ref e) => e.make_path(params, values).to_cairo_path(false),
569 ElementData::Rect(ref e) => e.make_path(params, values).to_cairo_path(false),
570 ElementData::Circle(ref e) => e.make_path(params, values).to_cairo_path(false),
571 ElementData::Ellipse(ref e) => e.make_path(params, values).to_cairo_path(false),
572 ElementData::Text(ref e) => {
573 let cascaded = CascadedValues::new_from_node(node);
574 path_from_text(
575 e,
576 node,
577 &cascaded,
578 session,
579 font_options,
580 acquired_nodes,
581 viewport,
582 )
583 .ok()?
584 }
585
586 _ => return None,
587 };
588
589 Some(ClipPathItem {
590 transform: values.transform(),
591 path,
592 clip_rule: values.clip_rule(),
593 clip_path: layout_clip_path(
594 session,
595 &elt,
596 font_options,
597 acquired_nodes,
598 params,
599 viewport,
600 )
601 .map(Box::new),
602 })
603 }
604}
605
606fn layout_paths_for_clip_path(
607 session: &Session,
608 clip_path_node: &Node,
609 font_options: &FontOptions,
610 acquired_nodes: &mut AcquiredNodes<'_>,
611 params: &NormalizeParams,
612 viewport: &Viewport,
613) -> Vec<ClipPathItem> {
614 clip_path_node
615 .children()
616 .filter(|c| c.is_element())
617 .filter_map(|child| {
618 clip_path_item_from_node(
619 session,
620 &child,
621 font_options,
622 acquired_nodes,
623 params,
624 viewport,
625 )
626 })
627 .collect()
628}
629
630fn resolve_object_space_clip_path(
631 values: &ComputedValues,
632 acquired_nodes: &mut AcquiredNodes<'_>,
633 referencing_element_name: &str,
634) -> Option<Node> {
635 let clip_path = values.clip_path();
636 let clip_uri = clip_path.0.get();
637 clip_uri
638 .and_then(|node_id| {
639 acquired_nodes
640 .acquire(referencing_element_name, node_id)
641 .ok()
642 .filter(|a| is_element_of_type!(*a.get(), ClipPath))
643 })
644 .map(|acquired| {
645 let clip_node = acquired.get().clone();
646
647 let units = borrow_element_as!(clip_node, ClipPath).get_units();
648
649 match units {
650 CoordUnits::UserSpaceOnUse => None,
651 CoordUnits::ObjectBoundingBox => Some(clip_node),
652 }
653 })
654 .unwrap_or(None)
655}
656
657impl StackingContext {
658 pub fn new(
659 draw_ctx: &DrawingCtx,
660 acquired_nodes: &mut AcquiredNodes<'_>,
661 element: &Element,
662 transform: Transform,
663 clip_rect: Option<Rect>,
664 values: &ComputedValues,
665 viewport: &Viewport,
666 ) -> StackingContext {
667 let session = draw_ctx.session().clone();
674 let font_options = draw_ctx.get_font_options();
675
676 let element_name = format!("{element}");
677
678 let is_visible = values.is_visible();
679
680 let opacity;
681 let filter;
682
683 match element.element_data {
684 ElementData::Mask(_) => {
687 opacity = Opacity(UnitInterval::clamp(1.0));
688 filter = None;
689 }
690
691 _ => {
692 opacity = values.opacity();
693 filter = get_filter(values, acquired_nodes, &element_name, &session);
694 }
695 }
696
697 let params = NormalizeParams::new(values, viewport);
700 let clip_path = layout_clip_path(
701 &session,
702 element,
703 &font_options,
704 acquired_nodes,
705 ¶ms,
706 viewport,
707 );
708
709 let element_name = format!("{element}");
710
711 let clip_in_object_space =
712 resolve_object_space_clip_path(values, acquired_nodes, &element_name);
713
714 let mask = values.mask().0.get().and_then(|mask_id| {
715 if let Ok(acquired) = acquired_nodes.acquire(&element_name, mask_id) {
716 let node = acquired.get();
717 match *node.borrow_element_data() {
718 ElementData::Mask(_) => Some(node.clone()),
719
720 _ => {
721 rsvg_log!(
722 session,
723 "element {} references \"{}\" which is not a mask",
724 element,
725 mask_id
726 );
727
728 None
729 }
730 }
731 } else {
732 rsvg_log!(
733 session,
734 "element {} references nonexistent mask \"{}\"",
735 element,
736 mask_id
737 );
738
739 None
740 }
741 });
742
743 let mix_blend_mode = values.mix_blend_mode();
744 let isolation = values.isolation();
745
746 StackingContext {
747 element_name,
748 transform,
749 is_visible,
750 opacity,
751 filter,
752 clip_rect,
753 clip_in_object_space,
754 clip_path,
755 mask,
756 mix_blend_mode,
757 isolation,
758 link_target: None,
759 }
760 }
761
762 pub fn new_with_link(
763 draw_ctx: &DrawingCtx,
764 acquired_nodes: &mut AcquiredNodes<'_>,
765 element: &Element,
766 transform: Transform,
767 values: &ComputedValues,
768 viewport: &Viewport,
769 link_target: Option<String>,
770 ) -> StackingContext {
771 let mut ctx = Self::new(
774 draw_ctx,
775 acquired_nodes,
776 element,
777 transform,
778 None,
779 values,
780 viewport,
781 );
782 ctx.link_target = link_target;
783 ctx
784 }
785
786 pub fn should_isolate(&self) -> bool {
787 let Opacity(UnitInterval(opacity)) = self.opacity;
788 match self.isolation {
789 Isolation::Auto => {
790 let is_opaque = approx_eq!(f64, opacity, 1.0);
791 !(is_opaque
792 && self.filter.is_none()
793 && self.mask.is_none()
794 && self.mix_blend_mode == MixBlendMode::Normal
795 && self.clip_in_object_space.is_none())
796 }
797 Isolation::Isolate => true,
798 }
799 }
800}
801
802impl LayerKind {
803 pub fn local_extents(&self) -> Option<Rect> {
810 match *self {
811 LayerKind::Shape(ref shape) => shape.path.extents,
812 LayerKind::Text(ref text) => text.extents,
813 LayerKind::Image(ref image) => Some(image.rect),
814 LayerKind::Group(ref group) => group.extents,
815 }
816 }
817}
818
819impl Stroke {
820 pub fn new(values: &ComputedValues, params: &NormalizeParams) -> Stroke {
821 let width = values.stroke_width().0.to_user(params);
822 let miter_limit = values.stroke_miterlimit();
823 let line_cap = values.stroke_line_cap();
824 let line_join = values.stroke_line_join();
825 let dash_offset = values.stroke_dashoffset().0.to_user(params);
826 let non_scaling = values.vector_effect() == VectorEffect::NonScalingStroke;
827
828 let dashes = match values.stroke_dasharray() {
829 StrokeDasharray(Dasharray::None) => Box::new([]),
830 StrokeDasharray(Dasharray::Array(dashes)) => dashes
831 .iter()
832 .map(|l| l.to_user(params))
833 .collect::<Box<[f64]>>(),
834 };
835
836 Stroke {
837 width,
838 miter_limit,
839 line_cap,
840 line_join,
841 dash_offset,
842 dashes,
843 non_scaling,
844 }
845 }
846}
847
848impl FontProperties {
849 pub fn new(values: &ComputedValues, params: &NormalizeParams) -> FontProperties {
854 FontProperties {
855 xml_lang: values.xml_lang(),
856 unicode_bidi: values.unicode_bidi(),
857 direction: values.direction(),
858 font_family: values.font_family(),
859 font_style: values.font_style(),
860 font_variant: values.font_variant(),
861 font_weight: values.font_weight(),
862 font_stretch: values.font_stretch(),
863 font_size: values.font_size().to_user(params),
864 letter_spacing: values.letter_spacing().to_user(params),
865 text_decoration: values.text_decoration(),
866 }
867 }
868}
869
870#[cfg(test)]
871mod tests {
872 use super::*;
873
874 use crate::document::Document;
875 use crate::dpi::Dpi;
876
877 fn assert_no_clip_path(svg: &'static [u8], element_with_clip_path: &str) {
878 let document = Document::load_from_bytes(svg);
879
880 let node = document
881 .lookup_internal_node(element_with_clip_path)
882 .unwrap();
883 let elt = node.borrow_element();
884
885 let mut acquired = AcquiredNodes::new(&document, None::<gio::Cancellable>);
886 let session = Session::default();
887 let font_options = FontOptions::new(true);
888 let params = NormalizeParams::from_dpi(Dpi::new(96.0, 96.0));
889 let viewport = Viewport::new(Dpi::new(96.0, 96.0), 1.0, 1.0);
890 let clip_path = layout_clip_path(
891 &session,
892 &elt,
893 &font_options,
894 &mut acquired,
895 ¶ms,
896 &viewport,
897 );
898
899 assert!(clip_path.is_none());
900 }
901
902 #[test]
903 fn detects_no_clip_path() {
904 assert_no_clip_path(
905 br#"<?xml version="1.0" encoding="UTF-8"?>
906<svg xmlns="http://www.w3.org/2000/svg">
907 <rect id="foo"/>
908</svg>
909"#,
910 "foo",
911 );
912 }
913
914 #[test]
915 fn detects_invalid_reference_to_clip_path() {
916 assert_no_clip_path(
917 br#"<?xml version="1.0" encoding="UTF-8"?>
918<svg xmlns="http://www.w3.org/2000/svg">
919 <rect id="foo" clip-path="url(#bar)"/>
920</svg>
921"#,
922 "foo",
923 );
924 }
925
926 #[test]
927 fn detects_reference_that_is_not_a_clip_path() {
928 assert_no_clip_path(
929 br#"<?xml version="1.0" encoding="UTF-8"?>
930<svg xmlns="http://www.w3.org/2000/svg">
931 <defs>
932 <rect id="bar"/> <!-- not a clipPath -->
933 </defs>
934 <rect id="foo" clip-path="url(#bar)"/>
935</svg>
936"#,
937 "foo",
938 );
939 }
940
941 #[test]
942 fn decodes_basic_clip_path() {
943 let document = Document::load_from_bytes(
944 br#"<?xml version="1.0" encoding="UTF-8"?>
945<svg xmlns="http://www.w3.org/2000/svg">
946 <defs>
947 <clipPath id="clip1" clipPathUnits="objectBoundingBox" transform="matrix(1.0 2.0 3.0 4.0 5.0 6.0)">
948 <rect x="5" y="5" width="10" height="20" transform="matrix(2.0 3.0 4.0 5.0 6.0 7.0)" clip-rule="evenodd"/>
949 <rect x="1" y="2" width="30" height="40" clip-path="url(#clip2)"/>
950 </clipPath>
951 <clipPath id="clip2">
952 <rect x="6" y="6" width="5" height="6"/>
953 </clipPath>
954 </defs>
955 <rect id="foo" clip-path="url(#clip1)"/>
956</svg>
957"#,
958 );
959
960 let node = document.lookup_internal_node("foo").unwrap();
961 let elt = node.borrow_element();
962
963 let mut acquired = AcquiredNodes::new(&document, None::<gio::Cancellable>);
964 let session = Session::default();
965 let font_options = FontOptions::new(true);
966 let params = NormalizeParams::from_dpi(Dpi::new(96.0, 96.0));
967 let viewport = Viewport::new(Dpi::new(96.0, 96.0), 1.0, 1.0);
968 let clip_path = layout_clip_path(
969 &session,
970 &elt,
971 &font_options,
972 &mut acquired,
973 ¶ms,
974 &viewport,
975 )
976 .expect("find a clipPath node");
977
978 assert_eq!(clip_path.clip_units, CoordUnits::ObjectBoundingBox);
979 assert_eq!(
980 clip_path.transform,
981 Transform::new_unchecked(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)
982 );
983 assert_eq!(clip_path.paths.len(), 2);
984 assert_eq!(
985 clip_path.paths[0].transform,
986 Transform::new_unchecked(2.0, 3.0, 4.0, 5.0, 6.0, 7.0)
987 );
988 assert_eq!(clip_path.paths[0].clip_rule, ClipRule::EvenOdd);
989 assert!(clip_path.paths[0].clip_path.is_none());
990
991 assert_eq!(clip_path.paths[1].clip_rule, ClipRule::NonZero);
992
993 if let Some(ref sub_clip_path) = clip_path.paths[1].clip_path {
994 assert_eq!(sub_clip_path.paths.len(), 1);
995 } else {
996 panic!("clip2 not found");
997 }
998 }
999
1000 #[test]
1001 fn decodes_nested_clip_path() {
1002 let document = Document::load_from_bytes(
1003 br#"<?xml version="1.0" encoding="UTF-8"?>
1004<svg xmlns="http://www.w3.org/2000/svg">
1005 <defs>
1006 <clipPath id="clip1" clip-path="url(#clip2)">
1007 <rect x="1" y="2" width="30" height="40"/>
1008 </clipPath>
1009 <clipPath id="clip2">
1010 <rect x="6" y="6" width="5" height="6"/>
1011 </clipPath>
1012 </defs>
1013 <rect id="foo" clip-path="url(#clip1)"/>
1014</svg>
1015"#,
1016 );
1017
1018 let node = document.lookup_internal_node("foo").unwrap();
1019 let elt = node.borrow_element();
1020
1021 let mut acquired = AcquiredNodes::new(&document, None::<gio::Cancellable>);
1022 let session = Session::default();
1023 let font_options = FontOptions::new(true);
1024 let params = NormalizeParams::from_dpi(Dpi::new(96.0, 96.0));
1025 let viewport = Viewport::new(Dpi::new(96.0, 96.0), 1.0, 1.0);
1026 let clip_path = layout_clip_path(
1027 &session,
1028 &elt,
1029 &font_options,
1030 &mut acquired,
1031 ¶ms,
1032 &viewport,
1033 )
1034 .expect("find a clipPath node");
1035
1036 assert_eq!(clip_path.paths.len(), 1);
1037
1038 if let Some(ref nested_clip_path) = clip_path.clip_path {
1039 assert_eq!(nested_clip_path.paths.len(), 1);
1040 } else {
1041 panic!("clip2 not found");
1042 }
1043 }
1044
1045 #[test]
1046 fn clip_path_does_not_include_invisible_children() {
1047 let document = Document::load_from_bytes(
1048 br#"<?xml version="1.0" encoding="UTF-8"?>
1049<svg xmlns="http://www.w3.org/2000/svg">
1050 <defs>
1051 <clipPath id="clip1">
1052 <rect x="1" y="2" width="30" height="40" visibility="hidden"/>
1053 <rect x="2" y="3" width="40" height="50" display="none"/>
1054 </clipPath>
1055 </defs>
1056 <rect id="foo" clip-path="url(#clip1)"/>
1057</svg>
1058"#,
1059 );
1060
1061 let node = document.lookup_internal_node("foo").unwrap();
1062 let elt = node.borrow_element();
1063
1064 let mut acquired = AcquiredNodes::new(&document, None::<gio::Cancellable>);
1065 let session = Session::default();
1066 let font_options = FontOptions::new(true);
1067 let params = NormalizeParams::from_dpi(Dpi::new(96.0, 96.0));
1068 let viewport = Viewport::new(Dpi::new(96.0, 96.0), 1.0, 1.0);
1069 let clip_path = layout_clip_path(
1070 &session,
1071 &elt,
1072 &font_options,
1073 &mut acquired,
1074 ¶ms,
1075 &viewport,
1076 )
1077 .expect("find a clipPath node");
1078
1079 assert_eq!(clip_path.paths.len(), 0);
1080 }
1081
1082 #[test]
1083 fn clip_path_can_have_use_children() {
1084 let document = Document::load_from_bytes(
1085 br##"<?xml version="1.0" encoding="UTF-8"?>
1086<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
1087 <defs>
1088 <rect id="rect" x="20" y="20" width="60" height="60"/>
1089 <clipPath id="clip1">
1090 <use href="#rect" x="20" y="20"/>/>
1091 </clipPath>
1092 </defs>
1093
1094 <rect width="100%" height="100%" fill="white"/>
1095
1096 <rect id="foo" x="20" y="20" width="60" height="60" fill="#00ff00" clip-path="url(#clip1)"/>
1097</svg>
1098"##,
1099 );
1100
1101 let node = document.lookup_internal_node("foo").unwrap();
1102 let elt = node.borrow_element();
1103
1104 let mut acquired = AcquiredNodes::new(&document, None::<gio::Cancellable>);
1105 let session = Session::default();
1106 let font_options = FontOptions::new(true);
1107 let params = NormalizeParams::from_dpi(Dpi::new(96.0, 96.0));
1108 let viewport = Viewport::new(Dpi::new(96.0, 96.0), 1.0, 1.0);
1109 let clip_path = layout_clip_path(
1110 &session,
1111 &elt,
1112 &font_options,
1113 &mut acquired,
1114 ¶ms,
1115 &viewport,
1116 )
1117 .expect("find a clipPath node");
1118
1119 assert_eq!(clip_path.paths.len(), 1);
1120
1121 let item = &clip_path.paths[0];
1122
1123 assert_eq!(item.transform, Transform::new_translate(20.0, 20.0));
1124 }
1125
1126 #[test]
1127 fn clip_path_handles_use_with_nonexistent_href() {
1128 let document = Document::load_from_bytes(
1129 br##"<?xml version="1.0" encoding="UTF-8"?>
1130<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
1131 <defs>
1132 <clipPath id="clip1">
1133 <use href="#nonexistent"/>
1134 </clipPath>
1135 </defs>
1136
1137 <rect width="100%" height="100%" fill="white"/>
1138
1139 <rect id="foo" x="20" y="20" width="60" height="60" fill="#00ff00" clip-path="url(#clip1)"/>
1140</svg>
1141"##,
1142 );
1143
1144 let node = document.lookup_internal_node("foo").unwrap();
1145 let elt = node.borrow_element();
1146
1147 let mut acquired = AcquiredNodes::new(&document, None::<gio::Cancellable>);
1148 let session = Session::default();
1149 let font_options = FontOptions::new(true);
1150 let params = NormalizeParams::from_dpi(Dpi::new(96.0, 96.0));
1151 let viewport = Viewport::new(Dpi::new(96.0, 96.0), 1.0, 1.0);
1152 let clip_path = layout_clip_path(
1153 &session,
1154 &elt,
1155 &font_options,
1156 &mut acquired,
1157 ¶ms,
1158 &viewport,
1159 )
1160 .expect("find a clipPath node");
1161
1162 assert!(clip_path.paths.is_empty());
1163 }
1164
1165 #[test]
1166 fn clip_path_handles_use_with_no_href() {
1167 let document = Document::load_from_bytes(
1168 br##"<?xml version="1.0" encoding="UTF-8"?>
1169<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
1170 <defs>
1171 <clipPath id="clip1">
1172 <use/>
1173 </clipPath>
1174 </defs>
1175
1176 <rect width="100%" height="100%" fill="white"/>
1177
1178 <rect id="foo" x="20" y="20" width="60" height="60" fill="#00ff00" clip-path="url(#clip1)"/>
1179</svg>
1180"##,
1181 );
1182
1183 let node = document.lookup_internal_node("foo").unwrap();
1184 let elt = node.borrow_element();
1185
1186 let mut acquired = AcquiredNodes::new(&document, None::<gio::Cancellable>);
1187 let session = Session::default();
1188 let font_options = FontOptions::new(true);
1189 let params = NormalizeParams::from_dpi(Dpi::new(96.0, 96.0));
1190 let viewport = Viewport::new(Dpi::new(96.0, 96.0), 1.0, 1.0);
1191 let clip_path = layout_clip_path(
1192 &session,
1193 &elt,
1194 &font_options,
1195 &mut acquired,
1196 ¶ms,
1197 &viewport,
1198 )
1199 .expect("find a clipPath node");
1200
1201 assert!(clip_path.paths.is_empty());
1202 }
1203}