Lines
97.41 %
Functions
36.11 %
Branches
54.6 %
//! Layout tree.
//!
//! The idea is to take the DOM tree and produce a layout tree with SVG concepts.
use std::rc::Rc;
use cssparser::Color;
use float_cmp::approx_eq;
use crate::aspect_ratio::AspectRatio;
use crate::bbox::BoundingBox;
use crate::cairo_path::CairoPath;
use crate::coord_units::CoordUnits;
use crate::dasharray::Dasharray;
use crate::document::AcquiredNodes;
use crate::element::{Element, ElementData};
use crate::filter::FilterValueList;
use crate::length::*;
use crate::node::*;
use crate::paint_server::{PaintSource, UserSpacePaintSource};
use crate::path_builder::Path as SvgPath;
use crate::properties::{
self, ClipRule, ComputedValues, Direction, FillRule, FontFamily, FontStretch, FontStyle,
FontVariant, FontWeight, ImageRendering, Isolation, MixBlendMode, Opacity, Overflow,
PaintOrder, ShapeRendering, StrokeDasharray, StrokeLinecap, StrokeLinejoin, StrokeMiterlimit,
TextDecoration, TextRendering, UnicodeBidi, VectorEffect, XmlLang,
};
use crate::rect::Rect;
use crate::rsvg_log;
use crate::session::Session;
use crate::surface_utils::shared_surface::SharedImageSurface;
use crate::transform::Transform;
use crate::unit_interval::UnitInterval;
use crate::viewbox::ViewBox;
use crate::{borrow_element_as, is_element_of_type};
/// SVG Stacking context, an inner node in the layout tree.
///
/// <https://www.w3.org/TR/SVG2/render.html#EstablishingStackingContex>
/// This is not strictly speaking an SVG2 stacking context, but a
/// looser version of it. For example. the SVG spec mentions that a
/// an element should establish a stacking context if the `filter`
/// property applies to the element and is not `none`. In that case,
/// the element is rendered as an "isolated group" -
/// <https://www.w3.org/TR/2015/CR-compositing-1-20150113/#csscompositingrules_SVG>
/// Here we store all the parameters that may lead to the decision to actually
/// render an element as an isolated group.
pub struct StackingContext {
pub element_name: String,
pub transform: Transform,
pub opacity: Opacity,
pub filter: Option<Filter>,
pub clip_rect: Option<Rect>,
pub clip_in_user_space: Option<Node>,
pub clip_in_object_space: Option<Node>,
pub mask: Option<Node>,
pub mix_blend_mode: MixBlendMode,
pub isolation: Isolation,
/// Target from an `<a>` element
pub link_target: Option<String>,
}
/// The item being rendered inside a stacking context.
pub struct Layer {
pub kind: LayerKind,
pub stacking_ctx: StackingContext,
pub enum LayerKind {
Shape(Box<Shape>),
Text(Box<Text>),
Image(Box<Image>),
Group(Box<Group>),
pub struct Group {
pub children: Vec<Layer>,
pub is_visible: bool, // FIXME: move to Layer? All of them have this...
pub establish_viewport: Option<LayoutViewport>,
/// Used for elements that need to establish a new viewport, like `<svg>`.
pub struct LayoutViewport {
// transform goes in the group's layer's StackingContext
/// Position and size of the element, per its x/y/width/height properties.
/// For markers, this is markerWidth/markerHeight.
pub geometry: Rect,
/// viewBox attribute
pub vbox: Option<ViewBox>,
/// preserveAspectRatio attribute
pub preserve_aspect_ratio: AspectRatio,
/// overflow property
pub overflow: Overflow,
/// Stroke parameters in user-space coordinates.
pub struct Stroke {
pub width: f64,
pub miter_limit: StrokeMiterlimit,
pub line_cap: StrokeLinecap,
pub line_join: StrokeLinejoin,
pub dash_offset: f64,
pub dashes: Box<[f64]>,
// https://svgwg.org/svg2-draft/painting.html#non-scaling-stroke
pub non_scaling: bool,
/// A path that has been validated for being suitable for Cairo.
/// As of 2024/Sep/25, Cairo converts path coordinates to fixed point, but it has several problems:
/// * For coordinates that are outside of the representable range in
/// fixed point, Cairo just clamps them. It is not able to return
/// this condition as an error to the caller.
/// * Then, it has multiple cases of possible arithmetic overflow
/// while processing the paths for rendering. Fixing this is an
/// ongoing project.
/// While Cairo gets better in these respects, librsvg will try to do
/// some mitigations, mainly about catching problematic coordinates
/// early and not passing them on to Cairo.
pub enum Path {
/// Path that has been checked for being suitable for Cairo.
/// Note that this also keeps a reference to the original [SvgPath], in addition to
/// the lowered [CairoPath]. This is because the markers code still needs the former.
Validated {
cairo_path: CairoPath,
path: Rc<SvgPath>,
extents: Option<Rect>,
stroke_paint: UserSpacePaintSource,
fill_paint: UserSpacePaintSource,
},
/// Reason why the path was determined to be not suitable for Cairo. This
/// is just used for logging purposes.
Invalid(String),
/// Paths and basic shapes resolved to a path.
pub struct Shape {
pub path: Path,
pub is_visible: bool,
pub paint_order: PaintOrder,
pub stroke: Stroke,
pub fill_rule: FillRule,
pub clip_rule: ClipRule,
pub shape_rendering: ShapeRendering,
pub marker_start: Marker,
pub marker_mid: Marker,
pub marker_end: Marker,
pub struct Marker {
pub node_ref: Option<Node>,
pub context_stroke: Rc<PaintSource>,
pub context_fill: Rc<PaintSource>,
/// Image in user-space coordinates.
pub struct Image {
pub surface: SharedImageSurface,
pub rect: Rect,
pub aspect: AspectRatio,
pub image_rendering: ImageRendering,
/// A single text span in user-space coordinates.
pub struct TextSpan {
pub layout: pango::Layout,
pub gravity: pango::Gravity,
pub bbox: Option<BoundingBox>,
pub x: f64,
pub y: f64,
pub stroke_paint: UserSpacePaintSource,
pub fill_paint: UserSpacePaintSource,
pub text_rendering: TextRendering,
/// Fully laid-out text in user-space coordinates.
pub struct Text {
pub spans: Vec<TextSpan>,
/// Font-related properties extracted from `ComputedValues`.
pub struct FontProperties {
pub xml_lang: XmlLang,
pub unicode_bidi: UnicodeBidi,
pub direction: Direction,
pub font_family: FontFamily,
pub font_style: FontStyle,
pub font_variant: FontVariant,
pub font_weight: FontWeight,
pub font_stretch: FontStretch,
pub font_size: f64,
pub letter_spacing: f64,
pub text_decoration: TextDecoration,
pub struct Filter {
pub filter_list: FilterValueList,
pub current_color: Color,
pub stroke_paint_source: Rc<PaintSource>,
pub fill_paint_source: Rc<PaintSource>,
pub normalize_values: NormalizeValues,
fn get_filter(
values: &ComputedValues,
acquired_nodes: &mut AcquiredNodes<'_>,
session: &Session,
) -> Option<Filter> {
match values.filter() {
properties::Filter::None => None,
properties::Filter::List(filter_list) => Some(get_filter_from_filter_list(
filter_list,
acquired_nodes,
values,
session,
)),
fn get_filter_from_filter_list(
filter_list: FilterValueList,
) -> Filter {
let current_color = values.color().0;
let stroke_paint_source = values.stroke().0.resolve(
values.stroke_opacity().0,
current_color,
None,
);
let fill_paint_source = values.fill().0.resolve(
values.fill_opacity().0,
let normalize_values = NormalizeValues::new(values);
Filter {
stroke_paint_source,
fill_paint_source,
normalize_values,
impl StackingContext {
pub fn new(
element: &Element,
transform: Transform,
clip_rect: Option<Rect>,
) -> StackingContext {
let element_name = format!("{element}");
let opacity;
let filter;
match element.element_data {
// "The opacity, filter and display properties do not apply to the mask element"
// https://drafts.fxtf.org/css-masking-1/#MaskElement
ElementData::Mask(_) => {
opacity = Opacity(UnitInterval::clamp(1.0));
filter = None;
_ => {
opacity = values.opacity();
filter = get_filter(values, acquired_nodes, session);
let clip_path = values.clip_path();
let clip_uri = clip_path.0.get();
let (clip_in_user_space, clip_in_object_space) = clip_uri
.and_then(|node_id| {
acquired_nodes
.acquire(node_id)
.ok()
.filter(|a| is_element_of_type!(*a.get(), ClipPath))
})
.map(|acquired| {
let clip_node = acquired.get().clone();
let units = borrow_element_as!(clip_node, ClipPath).get_units();
match units {
CoordUnits::UserSpaceOnUse => (Some(clip_node), None),
CoordUnits::ObjectBoundingBox => (None, Some(clip_node)),
.unwrap_or((None, None));
let mask = values.mask().0.get().and_then(|mask_id| {
if let Ok(acquired) = acquired_nodes.acquire(mask_id) {
let node = acquired.get();
match *node.borrow_element_data() {
ElementData::Mask(_) => Some(node.clone()),
rsvg_log!(
"element {} references \"{}\" which is not a mask",
element,
mask_id
None
} else {
"element {} references nonexistent mask \"{}\"",
});
let mix_blend_mode = values.mix_blend_mode();
let isolation = values.isolation();
StackingContext {
element_name,
transform,
opacity,
filter,
clip_rect,
clip_in_user_space,
clip_in_object_space,
mask,
mix_blend_mode,
isolation,
link_target: None,
pub fn new_with_link(
link_target: Option<String>,
// Note that the clip_rect=Some(...) argument is only used by the markers code,
// hence it is None here. Something to refactor later.
let mut ctx = Self::new(session, acquired_nodes, element, transform, None, values);
ctx.link_target = link_target;
ctx
pub fn should_isolate(&self) -> bool {
let Opacity(UnitInterval(opacity)) = self.opacity;
match self.isolation {
Isolation::Auto => {
let is_opaque = approx_eq!(f64, opacity, 1.0);
!(is_opaque
&& self.filter.is_none()
&& self.mask.is_none()
&& self.mix_blend_mode == MixBlendMode::Normal
&& self.clip_in_object_space.is_none())
Isolation::Isolate => true,
impl Stroke {
pub fn new(values: &ComputedValues, params: &NormalizeParams) -> Stroke {
let width = values.stroke_width().0.to_user(params);
let miter_limit = values.stroke_miterlimit();
let line_cap = values.stroke_line_cap();
let line_join = values.stroke_line_join();
let dash_offset = values.stroke_dashoffset().0.to_user(params);
let non_scaling = values.vector_effect() == VectorEffect::NonScalingStroke;
let dashes = match values.stroke_dasharray() {
StrokeDasharray(Dasharray::None) => Box::new([]),
StrokeDasharray(Dasharray::Array(dashes)) => dashes
.iter()
.map(|l| l.to_user(params))
.collect::<Box<[f64]>>(),
Stroke {
width,
miter_limit,
line_cap,
line_join,
dash_offset,
dashes,
non_scaling,
impl FontProperties {
/// Collects font properties from a `ComputedValues`.
/// The `writing-mode` property is passed separately, as it must come from the `<text>` element,
/// not the `<tspan>` whose computed values are being passed.
pub fn new(values: &ComputedValues, params: &NormalizeParams) -> FontProperties {
FontProperties {
xml_lang: values.xml_lang(),
unicode_bidi: values.unicode_bidi(),
direction: values.direction(),
font_family: values.font_family(),
font_style: values.font_style(),
font_variant: values.font_variant(),
font_weight: values.font_weight(),
font_stretch: values.font_stretch(),
font_size: values.font_size().to_user(params),
letter_spacing: values.letter_spacing().to_user(params),
text_decoration: values.text_decoration(),