1
//! Layout tree.
2
//!
3
//! The idea is to take the DOM tree and produce a layout tree with SVG concepts.
4

            
5
use std::rc::Rc;
6

            
7
use cssparser::Color;
8
use float_cmp::approx_eq;
9

            
10
use crate::aspect_ratio::AspectRatio;
11
use crate::bbox::BoundingBox;
12
use crate::cairo_path::CairoPath;
13
use crate::coord_units::CoordUnits;
14
use crate::dasharray::Dasharray;
15
use crate::document::AcquiredNodes;
16
use crate::element::{Element, ElementData};
17
use crate::filter::FilterValueList;
18
use crate::length::*;
19
use crate::node::*;
20
use crate::paint_server::{PaintSource, UserSpacePaintSource};
21
use crate::path_builder::Path as SvgPath;
22
use crate::properties::{
23
    self, ClipRule, ComputedValues, Direction, FillRule, FontFamily, FontStretch, FontStyle,
24
    FontVariant, FontWeight, ImageRendering, Isolation, MixBlendMode, Opacity, Overflow,
25
    PaintOrder, ShapeRendering, StrokeDasharray, StrokeLinecap, StrokeLinejoin, StrokeMiterlimit,
26
    TextDecoration, TextRendering, UnicodeBidi, VectorEffect, XmlLang,
27
};
28
use crate::rect::Rect;
29
use crate::rsvg_log;
30
use crate::session::Session;
31
use crate::surface_utils::shared_surface::SharedImageSurface;
32
use crate::transform::Transform;
33
use crate::unit_interval::UnitInterval;
34
use crate::viewbox::ViewBox;
35
use crate::{borrow_element_as, is_element_of_type};
36

            
37
/// SVG Stacking context, an inner node in the layout tree.
38
///
39
/// <https://www.w3.org/TR/SVG2/render.html#EstablishingStackingContex>
40
///
41
/// This is not strictly speaking an SVG2 stacking context, but a
42
/// looser version of it.  For example. the SVG spec mentions that a
43
/// an element should establish a stacking context if the `filter`
44
/// property applies to the element and is not `none`.  In that case,
45
/// the element is rendered as an "isolated group" -
46
/// <https://www.w3.org/TR/2015/CR-compositing-1-20150113/#csscompositingrules_SVG>
47
///
48
/// Here we store all the parameters that may lead to the decision to actually
49
/// render an element as an isolated group.
50
pub struct StackingContext {
51
    pub element_name: String,
52
    pub transform: Transform,
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.
67
pub struct Layer {
68
    pub kind: LayerKind,
69
    pub stacking_ctx: StackingContext,
70
}
71
pub enum LayerKind {
72
    Shape(Box<Shape>),
73
    Text(Box<Text>),
74
    Image(Box<Image>),
75
    Group(Box<Group>),
76
}
77

            
78
pub struct Group {
79
    pub children: Vec<Layer>,
80
    pub is_visible: bool, // FIXME: move to Layer?  All of them have this...
81
    pub establish_viewport: Option<LayoutViewport>,
82
}
83

            
84
/// Used for elements that need to establish a new viewport, like `<svg>`.
85
pub struct LayoutViewport {
86
    // transform goes in the group's layer's StackingContext
87
    /// Position and size of the element, per its x/y/width/height properties.
88
    /// For markers, this is markerWidth/markerHeight.
89
    pub geometry: Rect,
90

            
91
    /// viewBox attribute
92
    pub vbox: Option<ViewBox>,
93

            
94
    /// preserveAspectRatio attribute
95
    pub preserve_aspect_ratio: AspectRatio,
96

            
97
    /// overflow property
98
    pub overflow: Overflow,
99
}
100

            
101
/// Stroke parameters in user-space coordinates.
102
pub struct Stroke {
103
    pub width: f64,
104
    pub miter_limit: StrokeMiterlimit,
105
    pub line_cap: StrokeLinecap,
106
    pub line_join: StrokeLinejoin,
107
    pub dash_offset: f64,
108
    pub dashes: Box<[f64]>,
109
    // https://svgwg.org/svg2-draft/painting.html#non-scaling-stroke
110
    pub non_scaling: bool,
111
}
112

            
113
/// A path that has been validated for being suitable for Cairo.
114
///
115
/// As of 2024/Sep/25, Cairo converts path coordinates to fixed point, but it has several problems:
116
///
117
/// * For coordinates that are outside of the representable range in
118
///   fixed point, Cairo just clamps them.  It is not able to return
119
///   this condition as an error to the caller.
120
///
121
/// * Then, it has multiple cases of possible arithmetic overflow
122
///   while processing the paths for rendering.  Fixing this is an
123
///   ongoing project.
124
///
125
/// While Cairo gets better in these respects, librsvg will try to do
126
/// some mitigations, mainly about catching problematic coordinates
127
/// early and not passing them on to Cairo.
128
pub enum Path {
129
    /// Path that has been checked for being suitable for Cairo.
130
    ///
131
    /// Note that this also keeps a reference to the original [SvgPath], in addition to
132
    /// the lowered [CairoPath].  This is because the markers code still needs the former.
133
    Validated {
134
        cairo_path: CairoPath,
135
        path: Rc<SvgPath>,
136
        extents: Option<Rect>,
137
        stroke_paint: UserSpacePaintSource,
138
        fill_paint: UserSpacePaintSource,
139
    },
140

            
141
    /// Reason why the path was determined to be not suitable for Cairo.  This
142
    /// is just used for logging purposes.
143
    Invalid(String),
144
}
145

            
146
/// Paths and basic shapes resolved to a path.
147
pub struct Shape {
148
    pub path: Path,
149
    pub is_visible: bool,
150
    pub paint_order: PaintOrder,
151
    pub stroke: Stroke,
152
    pub fill_rule: FillRule,
153
    pub clip_rule: ClipRule,
154
    pub shape_rendering: ShapeRendering,
155
    pub marker_start: Marker,
156
    pub marker_mid: Marker,
157
    pub marker_end: Marker,
158
}
159

            
160
pub struct Marker {
161
    pub node_ref: Option<Node>,
162
    pub context_stroke: Rc<PaintSource>,
163
    pub context_fill: Rc<PaintSource>,
164
}
165

            
166
/// Image in user-space coordinates.
167
pub struct Image {
168
    pub surface: SharedImageSurface,
169
    pub is_visible: bool,
170
    pub rect: Rect,
171
    pub aspect: AspectRatio,
172
    pub overflow: Overflow,
173
    pub image_rendering: ImageRendering,
174
}
175

            
176
/// A single text span in user-space coordinates.
177
pub struct TextSpan {
178
    pub layout: pango::Layout,
179
    pub gravity: pango::Gravity,
180
    pub bbox: Option<BoundingBox>,
181
    pub is_visible: bool,
182
    pub x: f64,
183
    pub y: f64,
184
    pub paint_order: PaintOrder,
185
    pub stroke: Stroke,
186
    pub stroke_paint: UserSpacePaintSource,
187
    pub fill_paint: UserSpacePaintSource,
188
    pub text_rendering: TextRendering,
189
    pub link_target: Option<String>,
190
}
191

            
192
/// Fully laid-out text in user-space coordinates.
193
pub struct Text {
194
    pub spans: Vec<TextSpan>,
195
}
196

            
197
/// Font-related properties extracted from `ComputedValues`.
198
pub struct FontProperties {
199
    pub xml_lang: XmlLang,
200
    pub unicode_bidi: UnicodeBidi,
201
    pub direction: Direction,
202
    pub font_family: FontFamily,
203
    pub font_style: FontStyle,
204
    pub font_variant: FontVariant,
205
    pub font_weight: FontWeight,
206
    pub font_stretch: FontStretch,
207
    pub font_size: f64,
208
    pub letter_spacing: f64,
209
    pub text_decoration: TextDecoration,
210
}
211

            
212
pub struct Filter {
213
    pub filter_list: FilterValueList,
214
    pub current_color: Color,
215
    pub stroke_paint_source: Rc<PaintSource>,
216
    pub fill_paint_source: Rc<PaintSource>,
217
    pub normalize_values: NormalizeValues,
218
}
219

            
220
2005786
fn get_filter(
221
    values: &ComputedValues,
222
    acquired_nodes: &mut AcquiredNodes<'_>,
223
    session: &Session,
224
) -> Option<Filter> {
225
2005786
    match values.filter() {
226
2005483
        properties::Filter::None => None,
227

            
228
303
        properties::Filter::List(filter_list) => Some(get_filter_from_filter_list(
229
            filter_list,
230
            acquired_nodes,
231
            values,
232
            session,
233
        )),
234
    }
235
2005786
}
236

            
237
303
fn get_filter_from_filter_list(
238
    filter_list: FilterValueList,
239
    acquired_nodes: &mut AcquiredNodes<'_>,
240
    values: &ComputedValues,
241
    session: &Session,
242
) -> Filter {
243
303
    let current_color = values.color().0;
244

            
245
606
    let stroke_paint_source = values.stroke().0.resolve(
246
        acquired_nodes,
247
303
        values.stroke_opacity().0,
248
        current_color,
249
303
        None,
250
303
        None,
251
        session,
252
303
    );
253

            
254
606
    let fill_paint_source = values.fill().0.resolve(
255
        acquired_nodes,
256
303
        values.fill_opacity().0,
257
        current_color,
258
303
        None,
259
303
        None,
260
        session,
261
303
    );
262

            
263
303
    let normalize_values = NormalizeValues::new(values);
264

            
265
303
    Filter {
266
303
        filter_list,
267
        current_color,
268
303
        stroke_paint_source,
269
303
        fill_paint_source,
270
        normalize_values,
271
    }
272
303
}
273

            
274
impl StackingContext {
275
2006673
    pub fn new(
276
        session: &Session,
277
        acquired_nodes: &mut AcquiredNodes<'_>,
278
        element: &Element,
279
        transform: Transform,
280
        clip_rect: Option<Rect>,
281
        values: &ComputedValues,
282
    ) -> StackingContext {
283
2006673
        let element_name = format!("{element}");
284

            
285
        let opacity;
286
        let filter;
287

            
288
2006673
        match element.element_data {
289
            // "The opacity, filter and display properties do not apply to the mask element"
290
            // https://drafts.fxtf.org/css-masking-1/#MaskElement
291
59
            ElementData::Mask(_) => {
292
59
                opacity = Opacity(UnitInterval::clamp(1.0));
293
59
                filter = None;
294
            }
295

            
296
2005771
            _ => {
297
2006614
                opacity = values.opacity();
298
2005921
                filter = get_filter(values, acquired_nodes, session);
299
            }
300
        }
301

            
302
2005830
        let clip_path = values.clip_path();
303
2006315
        let clip_uri = clip_path.0.get();
304
2005923
        let (clip_in_user_space, clip_in_object_space) = clip_uri
305
46
            .and_then(|node_id| {
306
46
                acquired_nodes
307
                    .acquire(node_id)
308
                    .ok()
309
45
                    .filter(|a| is_element_of_type!(*a.get(), ClipPath))
310
46
            })
311
45
            .map(|acquired| {
312
45
                let clip_node = acquired.get().clone();
313

            
314
45
                let units = borrow_element_as!(clip_node, ClipPath).get_units();
315

            
316
45
                match units {
317
40
                    CoordUnits::UserSpaceOnUse => (Some(clip_node), None),
318
5
                    CoordUnits::ObjectBoundingBox => (None, Some(clip_node)),
319
                }
320
45
            })
321
2005596
            .unwrap_or((None, None));
322

            
323
2005751
        let mask = values.mask().0.get().and_then(|mask_id| {
324
376
            if let Ok(acquired) = acquired_nodes.acquire(mask_id) {
325
64
                let node = acquired.get();
326
64
                match *node.borrow_element_data() {
327
64
                    ElementData::Mask(_) => Some(node.clone()),
328

            
329
                    _ => {
330
                        rsvg_log!(
331
                            session,
332
                            "element {} references \"{}\" which is not a mask",
333
                            element,
334
                            mask_id
335
                        );
336

            
337
                        None
338
                    }
339
                }
340
64
            } else {
341
                rsvg_log!(
342
156
                    session,
343
                    "element {} references nonexistent mask \"{}\"",
344
                    element,
345
                    mask_id
346
                );
347

            
348
156
                None
349
            }
350
2005451
        });
351

            
352
2004902
        let mix_blend_mode = values.mix_blend_mode();
353
2004769
        let isolation = values.isolation();
354

            
355
2004573
        StackingContext {
356
2004573
            element_name,
357
            transform,
358
2004573
            opacity,
359
2004573
            filter,
360
            clip_rect,
361
2004573
            clip_in_user_space,
362
2004573
            clip_in_object_space,
363
2004573
            mask,
364
            mix_blend_mode,
365
            isolation,
366
2004573
            link_target: None,
367
        }
368
2004573
    }
369

            
370
3
    pub fn new_with_link(
371
        session: &Session,
372
        acquired_nodes: &mut AcquiredNodes<'_>,
373
        element: &Element,
374
        transform: Transform,
375
        values: &ComputedValues,
376
        link_target: Option<String>,
377
    ) -> StackingContext {
378
        // Note that the clip_rect=Some(...) argument is only used by the markers code,
379
        // hence it is None here.  Something to refactor later.
380
3
        let mut ctx = Self::new(session, acquired_nodes, element, transform, None, values);
381
3
        ctx.link_target = link_target;
382
3
        ctx
383
3
    }
384

            
385
2951663
    pub fn should_isolate(&self) -> bool {
386
2951663
        let Opacity(UnitInterval(opacity)) = self.opacity;
387
2951663
        match self.isolation {
388
            Isolation::Auto => {
389
2951660
                let is_opaque = approx_eq!(f64, opacity, 1.0);
390
5902896
                !(is_opaque
391
2951236
                    && self.filter.is_none()
392
2950283
                    && self.mask.is_none()
393
2949929
                    && self.mix_blend_mode == MixBlendMode::Normal
394
2949738
                    && self.clip_in_object_space.is_none())
395
2950838
            }
396
3
            Isolation::Isolate => true,
397
        }
398
2950841
    }
399
}
400

            
401
impl Stroke {
402
948604
    pub fn new(values: &ComputedValues, params: &NormalizeParams) -> Stroke {
403
948604
        let width = values.stroke_width().0.to_user(params);
404
948604
        let miter_limit = values.stroke_miterlimit();
405
948604
        let line_cap = values.stroke_line_cap();
406
948604
        let line_join = values.stroke_line_join();
407
948604
        let dash_offset = values.stroke_dashoffset().0.to_user(params);
408
948604
        let non_scaling = values.vector_effect() == VectorEffect::NonScalingStroke;
409

            
410
948604
        let dashes = match values.stroke_dasharray() {
411
948497
            StrokeDasharray(Dasharray::None) => Box::new([]),
412
107
            StrokeDasharray(Dasharray::Array(dashes)) => dashes
413
                .iter()
414
230
                .map(|l| l.to_user(params))
415
107
                .collect::<Box<[f64]>>(),
416
        };
417

            
418
948604
        Stroke {
419
            width,
420
            miter_limit,
421
            line_cap,
422
            line_join,
423
            dash_offset,
424
948604
            dashes,
425
            non_scaling,
426
        }
427
948604
    }
428
}
429

            
430
impl FontProperties {
431
    /// Collects font properties from a `ComputedValues`.
432
    ///
433
    /// The `writing-mode` property is passed separately, as it must come from the `<text>` element,
434
    /// not the `<tspan>` whose computed values are being passed.
435
1028
    pub fn new(values: &ComputedValues, params: &NormalizeParams) -> FontProperties {
436
1028
        FontProperties {
437
1028
            xml_lang: values.xml_lang(),
438
1028
            unicode_bidi: values.unicode_bidi(),
439
1028
            direction: values.direction(),
440
1028
            font_family: values.font_family(),
441
1028
            font_style: values.font_style(),
442
1028
            font_variant: values.font_variant(),
443
1028
            font_weight: values.font_weight(),
444
1028
            font_stretch: values.font_stretch(),
445
1028
            font_size: values.font_size().to_user(params),
446
1028
            letter_spacing: values.letter_spacing().to_user(params),
447
1028
            text_decoration: values.text_decoration(),
448
        }
449
1028
    }
450
}