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::AcquiredNodes;
15use crate::element::{Element, ElementData};
16use crate::filter::FilterValueList;
17use crate::length::*;
18use crate::node::*;
19use crate::paint_server::{PaintSource, UserSpacePaintSource};
20use crate::path_builder::Path as SvgPath;
21use crate::properties::{
22    self, ClipRule, ComputedValues, Direction, FillRule, FontFamily, FontStretch, FontStyle,
23    FontVariant, FontWeight, ImageRendering, Isolation, MixBlendMode, Opacity, Overflow,
24    PaintOrder, ShapeRendering, StrokeDasharray, StrokeLinecap, StrokeLinejoin, StrokeMiterlimit,
25    TextDecoration, TextRendering, UnicodeBidi, VectorEffect, XmlLang,
26};
27use crate::rect::Rect;
28use crate::rsvg_log;
29use crate::session::Session;
30use crate::surface_utils::shared_surface::SharedImageSurface;
31use crate::transform::Transform;
32use crate::unit_interval::UnitInterval;
33use crate::viewbox::ViewBox;
34use crate::{borrow_element_as, is_element_of_type};
35
36/// SVG Stacking context, an inner node in the layout tree.
37///
38/// <https://www.w3.org/TR/SVG2/render.html#EstablishingStackingContex>
39///
40/// This is not strictly speaking an SVG2 stacking context, but a
41/// looser version of it.  For example. the SVG spec mentions that a
42/// an element should establish a stacking context if the `filter`
43/// property applies to the element and is not `none`.  In that case,
44/// the element is rendered as an "isolated group" -
45/// <https://www.w3.org/TR/2015/CR-compositing-1-20150113/#csscompositingrules_SVG>
46///
47/// Here we store all the parameters that may lead to the decision to actually
48/// render an element as an isolated group.
49pub struct StackingContext {
50    pub element_name: String,
51    pub transform: Transform,
52    pub is_visible: bool,
53    pub opacity: Opacity,
54    pub filter: Option<Filter>,
55    pub clip_rect: Option<Rect>,
56    pub clip_in_user_space: Option<Node>,
57    pub clip_in_object_space: Option<Node>,
58    pub mask: Option<Node>,
59    pub mix_blend_mode: MixBlendMode,
60    pub isolation: Isolation,
61
62    /// Target from an `<a>` element
63    pub link_target: Option<String>,
64}
65
66/// The item being rendered inside a stacking context.
67pub struct Layer {
68    pub kind: LayerKind,
69    pub stacking_ctx: StackingContext,
70}
71
72pub enum LayerKind {
73    Shape(Box<Shape>),
74    Text(Box<Text>),
75    Image(Box<Image>),
76    Group(Box<Group>),
77}
78
79pub struct Group {
80    pub children: Vec<Layer>,
81    pub establish_viewport: Option<LayoutViewport>,
82    pub extents: Option<Rect>,
83}
84
85/// Used for elements that need to establish a new viewport, like `<svg>`.
86pub struct LayoutViewport {
87    // transform goes in the group's layer's StackingContext
88    /// Position and size of the element, per its x/y/width/height properties.
89    /// For markers, this is markerWidth/markerHeight.
90    pub geometry: Rect,
91
92    /// viewBox attribute
93    pub vbox: Option<ViewBox>,
94
95    /// preserveAspectRatio attribute
96    pub preserve_aspect_ratio: AspectRatio,
97
98    /// overflow property
99    pub overflow: Overflow,
100}
101
102/// Stroke parameters in user-space coordinates.
103pub struct Stroke {
104    pub width: f64,
105    pub miter_limit: StrokeMiterlimit,
106    pub line_cap: StrokeLinecap,
107    pub line_join: StrokeLinejoin,
108    pub dash_offset: f64,
109    pub dashes: Box<[f64]>,
110    // https://svgwg.org/svg2-draft/painting.html#non-scaling-stroke
111    pub non_scaling: bool,
112}
113
114/// A path known to be representable by Cairo.
115pub struct Path {
116    pub cairo_path: CairoPath,
117    pub path: Rc<SvgPath>,
118    pub extents: Option<Rect>,
119}
120
121/// Paths and basic shapes resolved to a path.
122pub struct Shape {
123    pub path: Path,
124    pub paint_order: PaintOrder,
125    pub stroke_paint: UserSpacePaintSource,
126    pub fill_paint: UserSpacePaintSource,
127    pub stroke: Stroke,
128    pub fill_rule: FillRule,
129    pub clip_rule: ClipRule,
130    pub shape_rendering: ShapeRendering,
131    pub marker_start: Marker,
132    pub marker_mid: Marker,
133    pub marker_end: Marker,
134}
135
136pub struct Marker {
137    pub node_ref: Option<Node>,
138    pub context_stroke: Rc<PaintSource>,
139    pub context_fill: Rc<PaintSource>,
140}
141
142/// Image in user-space coordinates.
143pub struct Image {
144    pub surface: SharedImageSurface,
145    pub rect: Rect,
146    pub aspect: AspectRatio,
147    pub overflow: Overflow,
148    pub image_rendering: ImageRendering,
149}
150
151/// A single text span in user-space coordinates.
152pub struct TextSpan {
153    pub layout: pango::Layout,
154    pub gravity: pango::Gravity,
155    pub extents: Option<Rect>,
156    pub is_visible: bool,
157    pub x: f64,
158    pub y: f64,
159    pub paint_order: PaintOrder,
160    pub stroke: Stroke,
161    pub stroke_paint: UserSpacePaintSource,
162    pub fill_paint: UserSpacePaintSource,
163    pub text_rendering: TextRendering,
164    pub link_target: Option<String>,
165}
166
167/// Fully laid-out text in user-space coordinates.
168pub struct Text {
169    pub spans: Vec<TextSpan>,
170    pub extents: Option<Rect>,
171}
172
173/// Font-related properties extracted from `ComputedValues`.
174pub struct FontProperties {
175    pub xml_lang: XmlLang,
176    pub unicode_bidi: UnicodeBidi,
177    pub direction: Direction,
178    pub font_family: FontFamily,
179    pub font_style: FontStyle,
180    pub font_variant: FontVariant,
181    pub font_weight: FontWeight,
182    pub font_stretch: FontStretch,
183    pub font_size: f64,
184    pub letter_spacing: f64,
185    pub text_decoration: TextDecoration,
186}
187
188pub struct Filter {
189    pub filter_list: FilterValueList,
190    pub current_color: Color,
191    pub stroke_paint_source: Rc<PaintSource>,
192    pub fill_paint_source: Rc<PaintSource>,
193    pub normalize_values: NormalizeValues,
194}
195
196fn get_filter(
197    values: &ComputedValues,
198    acquired_nodes: &mut AcquiredNodes<'_>,
199    session: &Session,
200) -> Option<Filter> {
201    match values.filter() {
202        properties::Filter::None => None,
203
204        properties::Filter::List(filter_list) => Some(get_filter_from_filter_list(
205            filter_list,
206            acquired_nodes,
207            values,
208            session,
209        )),
210    }
211}
212
213fn get_filter_from_filter_list(
214    filter_list: FilterValueList,
215    acquired_nodes: &mut AcquiredNodes<'_>,
216    values: &ComputedValues,
217    session: &Session,
218) -> Filter {
219    let current_color = values.color().0;
220
221    let stroke_paint_source = values.stroke().0.resolve(
222        acquired_nodes,
223        values.stroke_opacity().0,
224        current_color,
225        None,
226        None,
227        session,
228    );
229
230    let fill_paint_source = values.fill().0.resolve(
231        acquired_nodes,
232        values.fill_opacity().0,
233        current_color,
234        None,
235        None,
236        session,
237    );
238
239    let normalize_values = NormalizeValues::new(values);
240
241    Filter {
242        filter_list,
243        current_color,
244        stroke_paint_source,
245        fill_paint_source,
246        normalize_values,
247    }
248}
249
250impl StackingContext {
251    pub fn new(
252        session: &Session,
253        acquired_nodes: &mut AcquiredNodes<'_>,
254        element: &Element,
255        transform: Transform,
256        clip_rect: Option<Rect>,
257        values: &ComputedValues,
258    ) -> StackingContext {
259        let element_name = format!("{element}");
260
261        let is_visible = values.is_visible();
262
263        let opacity;
264        let filter;
265
266        match element.element_data {
267            // "The opacity, filter and display properties do not apply to the mask element"
268            // https://drafts.fxtf.org/css-masking-1/#MaskElement
269            ElementData::Mask(_) => {
270                opacity = Opacity(UnitInterval::clamp(1.0));
271                filter = None;
272            }
273
274            _ => {
275                opacity = values.opacity();
276                filter = get_filter(values, acquired_nodes, session);
277            }
278        }
279
280        let clip_path = values.clip_path();
281        let clip_uri = clip_path.0.get();
282        let (clip_in_user_space, clip_in_object_space) = clip_uri
283            .and_then(|node_id| {
284                acquired_nodes
285                    .acquire(node_id)
286                    .ok()
287                    .filter(|a| is_element_of_type!(*a.get(), ClipPath))
288            })
289            .map(|acquired| {
290                let clip_node = acquired.get().clone();
291
292                let units = borrow_element_as!(clip_node, ClipPath).get_units();
293
294                match units {
295                    CoordUnits::UserSpaceOnUse => (Some(clip_node), None),
296                    CoordUnits::ObjectBoundingBox => (None, Some(clip_node)),
297                }
298            })
299            .unwrap_or((None, None));
300
301        let mask = values.mask().0.get().and_then(|mask_id| {
302            if let Ok(acquired) = acquired_nodes.acquire(mask_id) {
303                let node = acquired.get();
304                match *node.borrow_element_data() {
305                    ElementData::Mask(_) => Some(node.clone()),
306
307                    _ => {
308                        rsvg_log!(
309                            session,
310                            "element {} references \"{}\" which is not a mask",
311                            element,
312                            mask_id
313                        );
314
315                        None
316                    }
317                }
318            } else {
319                rsvg_log!(
320                    session,
321                    "element {} references nonexistent mask \"{}\"",
322                    element,
323                    mask_id
324                );
325
326                None
327            }
328        });
329
330        let mix_blend_mode = values.mix_blend_mode();
331        let isolation = values.isolation();
332
333        StackingContext {
334            element_name,
335            transform,
336            is_visible,
337            opacity,
338            filter,
339            clip_rect,
340            clip_in_user_space,
341            clip_in_object_space,
342            mask,
343            mix_blend_mode,
344            isolation,
345            link_target: None,
346        }
347    }
348
349    pub fn new_with_link(
350        session: &Session,
351        acquired_nodes: &mut AcquiredNodes<'_>,
352        element: &Element,
353        transform: Transform,
354        values: &ComputedValues,
355        link_target: Option<String>,
356    ) -> StackingContext {
357        // Note that the clip_rect=Some(...) argument is only used by the markers code,
358        // hence it is None here.  Something to refactor later.
359        let mut ctx = Self::new(session, acquired_nodes, element, transform, None, values);
360        ctx.link_target = link_target;
361        ctx
362    }
363
364    pub fn should_isolate(&self) -> bool {
365        let Opacity(UnitInterval(opacity)) = self.opacity;
366        match self.isolation {
367            Isolation::Auto => {
368                let is_opaque = approx_eq!(f64, opacity, 1.0);
369                !(is_opaque
370                    && self.filter.is_none()
371                    && self.mask.is_none()
372                    && self.mix_blend_mode == MixBlendMode::Normal
373                    && self.clip_in_object_space.is_none())
374            }
375            Isolation::Isolate => true,
376        }
377    }
378}
379
380impl LayerKind {
381    /// Gets the extents of a layer in its local coordinate system.
382    ///
383    /// Each object or layer is able to compute its own extents, in its local coordinate
384    /// system.  When the parent group layer wants to take the union of the extents of its
385    /// children, that parent group will need to convert the children's extents using each
386    /// child layer's transform.
387    pub fn local_extents(&self) -> Option<Rect> {
388        match *self {
389            LayerKind::Shape(ref shape) => shape.path.extents,
390            LayerKind::Text(ref text) => text.extents,
391            LayerKind::Image(ref image) => Some(image.rect),
392            LayerKind::Group(ref group) => group.extents,
393        }
394    }
395}
396
397impl Stroke {
398    pub fn new(values: &ComputedValues, params: &NormalizeParams) -> Stroke {
399        let width = values.stroke_width().0.to_user(params);
400        let miter_limit = values.stroke_miterlimit();
401        let line_cap = values.stroke_line_cap();
402        let line_join = values.stroke_line_join();
403        let dash_offset = values.stroke_dashoffset().0.to_user(params);
404        let non_scaling = values.vector_effect() == VectorEffect::NonScalingStroke;
405
406        let dashes = match values.stroke_dasharray() {
407            StrokeDasharray(Dasharray::None) => Box::new([]),
408            StrokeDasharray(Dasharray::Array(dashes)) => dashes
409                .iter()
410                .map(|l| l.to_user(params))
411                .collect::<Box<[f64]>>(),
412        };
413
414        Stroke {
415            width,
416            miter_limit,
417            line_cap,
418            line_join,
419            dash_offset,
420            dashes,
421            non_scaling,
422        }
423    }
424}
425
426impl FontProperties {
427    /// Collects font properties from a `ComputedValues`.
428    ///
429    /// The `writing-mode` property is passed separately, as it must come from the `<text>` element,
430    /// not the `<tspan>` whose computed values are being passed.
431    pub fn new(values: &ComputedValues, params: &NormalizeParams) -> FontProperties {
432        FontProperties {
433            xml_lang: values.xml_lang(),
434            unicode_bidi: values.unicode_bidi(),
435            direction: values.direction(),
436            font_family: values.font_family(),
437            font_style: values.font_style(),
438            font_variant: values.font_variant(),
439            font_weight: values.font_weight(),
440            font_stretch: values.font_stretch(),
441            font_size: values.font_size().to_user(params),
442            letter_spacing: values.letter_spacing().to_user(params),
443            text_decoration: values.text_decoration(),
444        }
445    }
446}