rsvg/
layout.rs

1//! Layout tree.
2//!
3//! The idea is to take the DOM tree and produce a layout tree with SVG concepts.
4
5use 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
38/// SVG Stacking context, an inner node in the layout tree.
39///
40/// <https://www.w3.org/TR/SVG2/render.html#EstablishingStackingContex>
41///
42/// This is not strictly speaking an SVG2 stacking context, but a
43/// looser version of it.  For example. the SVG spec mentions that a
44/// an element should establish a stacking context if the `filter`
45/// property applies to the element and is not `none`.  In that case,
46/// the element is rendered as an "isolated group" -
47/// <https://www.w3.org/TR/2015/CR-compositing-1-20150113/#csscompositingrules_SVG>
48///
49/// Here we store all the parameters that may lead to the decision to actually
50/// render an element as an isolated group.
51pub 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    /// Target from an `<a>` element
65    pub link_target: Option<String>,
66}
67
68/// Recursive representation of the `clipPath` element, a union of clipping paths.
69///
70/// The `clipPath` element supports having a list of child elements that define paths; the
71/// final clipping path is the union of them.  In turn, every path can have its own
72/// clipPath.
73pub 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
80/// One item in a [`ClipPath`].
81pub struct ClipPathItem {
82    pub transform: Transform,
83    pub path: CairoPath,
84    pub clip_rule: ClipRule,
85    pub clip_path: Option<Box<ClipPath>>,
86}
87
88/// The item being rendered inside a stacking context.
89pub 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
107/// Used for elements that need to establish a new viewport, like `<svg>`.
108pub struct LayoutViewport {
109    // transform goes in the group's layer's StackingContext
110    /// Position and size of the element, per its x/y/width/height properties.
111    /// For markers, this is markerWidth/markerHeight.
112    pub geometry: Rect,
113
114    /// viewBox attribute
115    pub vbox: Option<ViewBox>,
116
117    /// preserveAspectRatio attribute
118    pub preserve_aspect_ratio: AspectRatio,
119
120    /// overflow property
121    pub overflow: Overflow,
122}
123
124/// Stroke parameters in user-space coordinates.
125pub 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    // https://svgwg.org/svg2-draft/painting.html#non-scaling-stroke
133    pub non_scaling: bool,
134}
135
136/// A path known to be representable by Cairo.
137pub struct Path {
138    pub cairo_path: CairoPath,
139    pub path: Rc<SvgPath>,
140    pub extents: Option<Rect>,
141}
142
143/// Paths and basic shapes resolved to a path.
144pub 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
164/// Image in user-space coordinates.
165pub 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
173/// A single text span in user-space coordinates.
174pub 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
189/// Fully laid-out text in user-space coordinates.
190pub struct Text {
191    pub spans: Vec<TextSpan>,
192    pub extents: Option<Rect>,
193}
194
195/// Font-related properties extracted from `ComputedValues`.
196pub 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
362// FIXME: this will need to use Text2 when we implement it
363fn 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        // FIXME: does the text-rendering property (for text antialiasing) apply to clipping paths?
387
388        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    // FIXME: the following is copied from DrawingCtx::draw_from_use_node().  We need to
402    // refactor this; maybe by making AcquiredNode carry the acquire_ref()'ed <use> node as well.
403
404    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    // width or height set to 0 disables rendering of the element
428    // https://www.w3.org/TR/SVG/struct.html#UseElementWidthAttribute
429
430    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, // fill_paint, not used in clipping
442        None, // stroke_paint, not used in clipping
443    );
444    let child_values = child_cascaded.get();
445
446    if !child_values.is_displayed() || !child_values.is_visible() {
447        // https://www.w3.org/TR/css-masking-1/#ClipPathElement
448        //
449        // "If a child element [of the clipPath element] is made invisible by display or
450        // visibility it does not contribute to the clipping path."
451
452        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(&params, child_values).to_cairo_path(false),
470                ElementData::Polygon(ref e) => {
471                    e.make_path(&params, child_values).to_cairo_path(false)
472                }
473                ElementData::Polyline(ref e) => {
474                    e.make_path(&params, child_values).to_cairo_path(false)
475                }
476                ElementData::Line(ref e) => e.make_path(&params, child_values).to_cairo_path(false),
477                ElementData::Rect(ref e) => e.make_path(&params, child_values).to_cairo_path(false),
478                ElementData::Circle(ref e) => {
479                    e.make_path(&params, child_values).to_cairo_path(false)
480                }
481                ElementData::Ellipse(ref e) => {
482                    e.make_path(&params, 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                    &params,
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
522// https://www.w3.org/TR/css-masking-1/#ClipPathElement
523pub 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        // https://www.w3.org/TR/css-masking-1/#ClipPathElement
546        //
547        // "If a child element [of the clipPath element] is made invisible by display or
548        // visibility it does not contribute to the clipping path."
549
550        return None;
551    }
552
553    if let ElementData::Use(_) = *data {
554        // FIXME: the following implies that we need to return a Result, likely everywhere,
555        // to deal with MaxReferencesExceeded and such.
556        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        // FIXME: practically the only reason we need the DrawingCtx as an argument is so that
668        // the call to layout_clip_path() below can extract the FontOptions from the DrawingCtx, and that
669        // is only so that if a referenced clipPath element has a <text> child, then we'll be able
670        // to layout the text element to use as a clipping path.  Could we carry the FontOptions
671        // somewhere else...?
672
673        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            // "The opacity, filter and display properties do not apply to the mask element"
685            // https://drafts.fxtf.org/css-masking-1/#MaskElement
686            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        // These are the params "outside" the stacking context, and they are used to normalize
698        // lengths inside a clipPath's children (e.g. <clipPath> <rect x="10%"/> </clipPath>).
699        let params = NormalizeParams::new(values, viewport);
700        let clip_path = layout_clip_path(
701            &session,
702            element,
703            &font_options,
704            acquired_nodes,
705            &params,
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        // Note that the clip_rect=Some(...) argument is only used by the markers code,
772        // hence it is None here.  Something to refactor later.
773        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    /// Gets the extents of a layer in its local coordinate system.
804    ///
805    /// Each object or layer is able to compute its own extents, in its local coordinate
806    /// system.  When the parent group layer wants to take the union of the extents of its
807    /// children, that parent group will need to convert the children's extents using each
808    /// child layer's transform.
809    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    /// Collects font properties from a `ComputedValues`.
850    ///
851    /// The `writing-mode` property is passed separately, as it must come from the `<text>` element,
852    /// not the `<tspan>` whose computed values are being passed.
853    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            &params,
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            &params,
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            &params,
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            &params,
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            &params,
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            &params,
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            &params,
1197            &viewport,
1198        )
1199        .expect("find a clipPath node");
1200
1201        assert!(clip_path.paths.is_empty());
1202    }
1203}