diff options
Diffstat (limited to 'rsvg/src/drawing_ctx.rs')
-rw-r--r-- | rsvg/src/drawing_ctx.rs | 2412 |
1 files changed, 2412 insertions, 0 deletions
diff --git a/rsvg/src/drawing_ctx.rs b/rsvg/src/drawing_ctx.rs new file mode 100644 index 00000000..0bf726c3 --- /dev/null +++ b/rsvg/src/drawing_ctx.rs @@ -0,0 +1,2412 @@ +//! The main context structure which drives the drawing process. + +use float_cmp::approx_eq; +use glib::translate::*; +use once_cell::sync::Lazy; +use pango::ffi::PangoMatrix; +use pango::prelude::FontMapExt; +use regex::{Captures, Regex}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::convert::TryFrom; +use std::f64::consts::*; +use std::rc::Rc; +use std::sync::Arc; + +use crate::accept_language::UserLanguage; +use crate::aspect_ratio::AspectRatio; +use crate::bbox::BoundingBox; +use crate::coord_units::CoordUnits; +use crate::document::{AcquiredNodes, NodeId}; +use crate::dpi::Dpi; +use crate::element::{Element, ElementData}; +use crate::error::{AcquireError, ImplementationLimit, RenderingError}; +use crate::filters::{self, FilterSpec}; +use crate::float_eq_cairo::ApproxEqCairo; +use crate::gradient::{GradientVariant, SpreadMethod, UserSpaceGradient}; +use crate::layout::{ + Filter, Image, Layer, LayerKind, Shape, StackingContext, Stroke, Text, TextSpan, +}; +use crate::length::*; +use crate::marker; +use crate::node::{CascadedValues, Node, NodeBorrow, NodeDraw}; +use crate::paint_server::{PaintSource, UserSpacePaintSource}; +use crate::path_builder::*; +use crate::pattern::UserSpacePattern; +use crate::properties::{ + ClipRule, ComputedValues, FillRule, MaskType, MixBlendMode, Opacity, Overflow, PaintTarget, + ShapeRendering, StrokeLinecap, StrokeLinejoin, TextRendering, +}; +use crate::rect::{rect_to_transform, IRect, Rect}; +use crate::session::Session; +use crate::surface_utils::{ + shared_surface::ExclusiveImageSurface, shared_surface::SharedImageSurface, + shared_surface::SurfaceType, +}; +use crate::transform::{Transform, ValidTransform}; +use crate::unit_interval::UnitInterval; +use crate::viewbox::ViewBox; + +/// Opaque font options for a DrawingCtx. +/// +/// This is used for DrawingCtx::create_pango_context. +pub struct FontOptions { + options: cairo::FontOptions, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum ClipMode { + ClipToViewport, + NoClip, +} + +/// Set path on the cairo context, or clear it. +/// This helper object keeps track whether the path has been set already, +/// so that it isn't recalculated every so often. +struct PathHelper<'a> { + cr: &'a cairo::Context, + transform: ValidTransform, + path: &'a Path, + is_square_linecap: bool, + has_path: Option<bool>, +} + +impl<'a> PathHelper<'a> { + pub fn new( + cr: &'a cairo::Context, + transform: ValidTransform, + path: &'a Path, + linecap: StrokeLinecap, + ) -> Self { + PathHelper { + cr, + transform, + path, + is_square_linecap: linecap == StrokeLinecap::Square, + has_path: None, + } + } + + pub fn set(&mut self) -> Result<(), RenderingError> { + match self.has_path { + Some(false) | None => { + self.has_path = Some(true); + self.cr.set_matrix(self.transform.into()); + self.path.to_cairo(self.cr, self.is_square_linecap) + } + Some(true) => Ok(()), + } + } + + pub fn unset(&mut self) { + match self.has_path { + Some(true) | None => { + self.has_path = Some(false); + self.cr.new_path(); + } + Some(false) => {} + } + } +} + +/// Holds the size of the current viewport in the user's coordinate system. +#[derive(Clone)] +pub struct Viewport { + pub dpi: Dpi, + + /// Corners of the current coordinate space. + pub vbox: ViewBox, + + /// The viewport's coordinate system, or "user coordinate system" in SVG terms. + transform: Transform, +} + +impl Viewport { + /// FIXME: this is just used in Handle::with_height_to_user(), and in length.rs's test suite. + /// Find a way to do this without involving a default identity transform. + pub fn new(dpi: Dpi, view_box_width: f64, view_box_height: f64) -> Viewport { + Viewport { + dpi, + vbox: ViewBox::from(Rect::from_size(view_box_width, view_box_height)), + transform: Default::default(), + } + } + + /// Creates a new viewport suitable for a certain kind of units. + /// + /// For `objectBoundingBox`, CSS lengths which are in percentages + /// refer to the size of the current viewport. Librsvg implements + /// that by keeping the same current transformation matrix, and + /// setting a viewport size of (1.0, 1.0). + /// + /// For `userSpaceOnUse`, we just duplicate the current viewport, + /// since that kind of units means to use the current coordinate + /// system unchanged. + pub fn with_units(&self, units: CoordUnits) -> Viewport { + match units { + CoordUnits::ObjectBoundingBox => Viewport { + dpi: self.dpi, + vbox: ViewBox::from(Rect::from_size(1.0, 1.0)), + transform: self.transform, + }, + + CoordUnits::UserSpaceOnUse => Viewport { + dpi: self.dpi, + vbox: self.vbox, + transform: self.transform, + }, + } + } + + /// Returns a viewport with a new size for normalizing `Length` values. + pub fn with_view_box(&self, width: f64, height: f64) -> Viewport { + Viewport { + dpi: self.dpi, + vbox: ViewBox::from(Rect::from_size(width, height)), + transform: self.transform, + } + } +} + +pub struct DrawingCtx { + session: Session, + + initial_viewport: Viewport, + + dpi: Dpi, + + cr_stack: Rc<RefCell<Vec<cairo::Context>>>, + cr: cairo::Context, + + user_language: UserLanguage, + + drawsub_stack: Vec<Node>, + + measuring: bool, + testing: bool, +} + +pub enum DrawingMode { + LimitToStack { node: Node, root: Node }, + + OnlyNode(Node), +} + +/// The toplevel drawing routine. +/// +/// This creates a DrawingCtx internally and starts drawing at the specified `node`. +pub fn draw_tree( + session: Session, + mode: DrawingMode, + cr: &cairo::Context, + viewport_rect: Rect, + user_language: &UserLanguage, + dpi: Dpi, + measuring: bool, + testing: bool, + acquired_nodes: &mut AcquiredNodes<'_>, +) -> Result<BoundingBox, RenderingError> { + let (drawsub_stack, node) = match mode { + DrawingMode::LimitToStack { node, root } => (node.ancestors().collect(), root), + + DrawingMode::OnlyNode(node) => (Vec::new(), node), + }; + + let cascaded = CascadedValues::new_from_node(&node); + + // Preserve the user's transform and use it for the outermost bounding box. All bounds/extents + // will be converted to this transform in the end. + let user_transform = Transform::from(cr.matrix()); + let mut user_bbox = BoundingBox::new().with_transform(user_transform); + + // https://www.w3.org/TR/SVG2/coords.html#InitialCoordinateSystem + // + // "For the outermost svg element, the SVG user agent must + // determine an initial viewport coordinate system and an + // initial user coordinate system such that the two + // coordinates systems are identical. The origin of both + // coordinate systems must be at the origin of the SVG + // viewport." + // + // "... the initial viewport coordinate system (and therefore + // the initial user coordinate system) must have its origin at + // the top/left of the viewport" + + // Translate so (0, 0) is at the viewport's upper-left corner. + let transform = user_transform.pre_translate(viewport_rect.x0, viewport_rect.y0); + + // Here we exit immediately if the transform is not valid, since we are in the + // toplevel drawing function. Downstream cases would simply not render the current + // element and ignore the error. + let valid_transform = ValidTransform::try_from(transform)?; + cr.set_matrix(valid_transform.into()); + + // Per the spec, so the viewport has (0, 0) as upper-left. + let viewport_rect = viewport_rect.translate((-viewport_rect.x0, -viewport_rect.y0)); + let initial_viewport = Viewport { + dpi, + vbox: ViewBox::from(viewport_rect), + transform, + }; + + let mut draw_ctx = DrawingCtx::new( + session, + cr, + &initial_viewport, + user_language.clone(), + dpi, + measuring, + testing, + drawsub_stack, + ); + + let content_bbox = draw_ctx.draw_node_from_stack( + &node, + acquired_nodes, + &cascaded, + &initial_viewport, + false, + )?; + + user_bbox.insert(&content_bbox); + + Ok(user_bbox) +} + +pub fn with_saved_cr<O, F>(cr: &cairo::Context, f: F) -> Result<O, RenderingError> +where + F: FnOnce() -> Result<O, RenderingError>, +{ + cr.save()?; + match f() { + Ok(o) => { + cr.restore()?; + Ok(o) + } + + Err(e) => Err(e), + } +} + +impl Drop for DrawingCtx { + fn drop(&mut self) { + self.cr_stack.borrow_mut().pop(); + } +} + +const CAIRO_TAG_LINK: &str = "Link"; + +impl DrawingCtx { + fn new( + session: Session, + cr: &cairo::Context, + initial_viewport: &Viewport, + user_language: UserLanguage, + dpi: Dpi, + measuring: bool, + testing: bool, + drawsub_stack: Vec<Node>, + ) -> DrawingCtx { + DrawingCtx { + session, + initial_viewport: initial_viewport.clone(), + dpi, + cr_stack: Rc::new(RefCell::new(Vec::new())), + cr: cr.clone(), + user_language, + drawsub_stack, + measuring, + testing, + } + } + + /// Copies a `DrawingCtx` for temporary use on a Cairo surface. + /// + /// `DrawingCtx` maintains state using during the drawing process, and sometimes we + /// would like to use that same state but on a different Cairo surface and context + /// than the ones being used on `self`. This function copies the `self` state into a + /// new `DrawingCtx`, and ties the copied one to the supplied `cr`. + fn nested(&self, cr: cairo::Context) -> DrawingCtx { + let cr_stack = self.cr_stack.clone(); + + cr_stack.borrow_mut().push(self.cr.clone()); + + DrawingCtx { + session: self.session.clone(), + initial_viewport: self.initial_viewport.clone(), + dpi: self.dpi, + cr_stack, + cr, + user_language: self.user_language.clone(), + drawsub_stack: self.drawsub_stack.clone(), + measuring: self.measuring, + testing: self.testing, + } + } + + pub fn session(&self) -> &Session { + &self.session + } + + pub fn user_language(&self) -> &UserLanguage { + &self.user_language + } + + pub fn toplevel_viewport(&self) -> Rect { + *self.initial_viewport.vbox + } + + /// Gets the transform that will be used on the target surface, + /// whether using an isolated stacking context or not. + /// + /// This is only used in the text code, and we should probably try + /// to remove it. + pub fn get_transform_for_stacking_ctx( + &self, + stacking_ctx: &StackingContext, + clipping: bool, + ) -> Result<ValidTransform, RenderingError> { + if stacking_ctx.should_isolate() && !clipping { + let affines = CompositingAffines::new( + *self.get_transform(), + self.initial_viewport.transform, + self.cr_stack.borrow().len(), + ); + + Ok(ValidTransform::try_from(affines.for_temporary_surface)?) + } else { + Ok(self.get_transform()) + } + } + + pub fn is_measuring(&self) -> bool { + self.measuring + } + + pub fn get_transform(&self) -> ValidTransform { + let t = Transform::from(self.cr.matrix()); + ValidTransform::try_from(t) + .expect("Cairo should already have checked that its current transform is valid") + } + + pub fn empty_bbox(&self) -> BoundingBox { + BoundingBox::new().with_transform(*self.get_transform()) + } + + fn size_for_temporary_surface(&self) -> (i32, i32) { + let rect = self.toplevel_viewport(); + + let (viewport_width, viewport_height) = (rect.width(), rect.height()); + + let (width, height) = self + .initial_viewport + .transform + .transform_distance(viewport_width, viewport_height); + + // We need a size in whole pixels, so use ceil() to ensure the whole viewport fits + // into the temporary surface. + (width.ceil() as i32, height.ceil() as i32) + } + + pub fn create_surface_for_toplevel_viewport( + &self, + ) -> Result<cairo::ImageSurface, RenderingError> { + let (w, h) = self.size_for_temporary_surface(); + + Ok(cairo::ImageSurface::create(cairo::Format::ARgb32, w, h)?) + } + + fn create_similar_surface_for_toplevel_viewport( + &self, + surface: &cairo::Surface, + ) -> Result<cairo::Surface, RenderingError> { + let (w, h) = self.size_for_temporary_surface(); + + Ok(cairo::Surface::create_similar( + surface, + cairo::Content::ColorAlpha, + w, + h, + )?) + } + + /// Creates a new coordinate space inside a viewport and sets a clipping rectangle. + /// + /// Note that this actually changes the `draw_ctx.cr`'s transformation to match + /// the new coordinate space, but the old one is not restored after the + /// result's `Viewport` is dropped. Thus, this function must be called + /// inside `with_saved_cr` or `draw_ctx.with_discrete_layer`. + pub fn push_new_viewport( + &self, + current_viewport: &Viewport, + vbox: Option<ViewBox>, + viewport_rect: Rect, + preserve_aspect_ratio: AspectRatio, + clip_mode: ClipMode, + ) -> Option<Viewport> { + if let ClipMode::ClipToViewport = clip_mode { + clip_to_rectangle(&self.cr, &viewport_rect); + } + + preserve_aspect_ratio + .viewport_to_viewbox_transform(vbox, &viewport_rect) + .unwrap_or_else(|_e| { + match vbox { + None => unreachable!( + "viewport_to_viewbox_transform only returns errors when vbox != None" + ), + Some(v) => { + rsvg_log!( + self.session, + "ignoring viewBox ({}, {}, {}, {}) since it is not usable", + v.x0, + v.y0, + v.width(), + v.height() + ); + } + } + None + }) + .map(|t| { + self.cr.transform(t.into()); + + Viewport { + dpi: self.dpi, + vbox: vbox.unwrap_or(current_viewport.vbox), + transform: current_viewport.transform.post_transform(&t), + } + }) + } + + fn clip_to_node( + &mut self, + clip_node: &Option<Node>, + acquired_nodes: &mut AcquiredNodes<'_>, + viewport: &Viewport, + bbox: &BoundingBox, + ) -> Result<(), RenderingError> { + if clip_node.is_none() { + return Ok(()); + } + + let node = clip_node.as_ref().unwrap(); + let units = borrow_element_as!(node, ClipPath).get_units(); + + if let Ok(transform) = rect_to_transform(&bbox.rect, units) { + let cascaded = CascadedValues::new_from_node(node); + let values = cascaded.get(); + + let node_transform = values.transform().post_transform(&transform); + let transform_for_clip = ValidTransform::try_from(node_transform)?; + + let orig_transform = self.get_transform(); + self.cr.transform(transform_for_clip.into()); + + for child in node.children().filter(|c| { + c.is_element() && element_can_be_used_inside_clip_path(&c.borrow_element()) + }) { + child.draw( + acquired_nodes, + &CascadedValues::clone_with_node(&cascaded, &child), + viewport, + self, + true, + )?; + } + + self.cr.clip(); + + self.cr.set_matrix(orig_transform.into()); + } + + Ok(()) + } + + fn generate_cairo_mask( + &mut self, + mask_node: &Node, + viewport: &Viewport, + transform: Transform, + bbox: &BoundingBox, + acquired_nodes: &mut AcquiredNodes<'_>, + ) -> Result<Option<cairo::ImageSurface>, RenderingError> { + if bbox.rect.is_none() { + // The node being masked is empty / doesn't have a + // bounding box, so there's nothing to mask! + return Ok(None); + } + + let _mask_acquired = match acquired_nodes.acquire_ref(mask_node) { + Ok(n) => n, + + Err(AcquireError::CircularReference(_)) => { + rsvg_log!(self.session, "circular reference in element {}", mask_node); + return Ok(None); + } + + _ => unreachable!(), + }; + + let mask = borrow_element_as!(mask_node, Mask); + + let bbox_rect = bbox.rect.as_ref().unwrap(); + + let cascaded = CascadedValues::new_from_node(mask_node); + let values = cascaded.get(); + + let mask_units = mask.get_units(); + + let mask_rect = { + let params = NormalizeParams::new(values, &viewport.with_units(mask_units)); + mask.get_rect(¶ms) + }; + + let mask_element = mask_node.borrow_element(); + + let mask_transform = values.transform().post_transform(&transform); + let transform_for_mask = ValidTransform::try_from(mask_transform)?; + + let mask_content_surface = self.create_surface_for_toplevel_viewport()?; + + // Use a scope because mask_cr needs to release the + // reference to the surface before we access the pixels + { + let mask_cr = cairo::Context::new(&mask_content_surface)?; + mask_cr.set_matrix(transform_for_mask.into()); + + let bbtransform = Transform::new_unchecked( + bbox_rect.width(), + 0.0, + 0.0, + bbox_rect.height(), + bbox_rect.x0, + bbox_rect.y0, + ); + + let clip_rect = if mask_units == CoordUnits::ObjectBoundingBox { + bbtransform.transform_rect(&mask_rect) + } else { + mask_rect + }; + + clip_to_rectangle(&mask_cr, &clip_rect); + + if mask.get_content_units() == CoordUnits::ObjectBoundingBox { + if bbox_rect.is_empty() { + return Ok(None); + } + mask_cr.transform(ValidTransform::try_from(bbtransform)?.into()); + } + + let mask_viewport = viewport.with_units(mask.get_content_units()); + + let mut mask_draw_ctx = self.nested(mask_cr); + + let stacking_ctx = StackingContext::new( + self.session(), + acquired_nodes, + &mask_element, + Transform::identity(), + values, + ); + + let res = mask_draw_ctx.with_discrete_layer( + &stacking_ctx, + acquired_nodes, + &mask_viewport, + false, + None, + &mut |an, dc| mask_node.draw_children(an, &cascaded, &mask_viewport, dc, false), + ); + + res?; + } + + let tmp = SharedImageSurface::wrap(mask_content_surface, SurfaceType::SRgb)?; + + let mask_result = match values.mask_type() { + MaskType::Luminance => tmp.to_luminance_mask()?, + MaskType::Alpha => tmp.extract_alpha(IRect::from_size(tmp.width(), tmp.height()))?, + }; + + let mask = mask_result.into_image_surface()?; + + Ok(Some(mask)) + } + + pub fn with_discrete_layer( + &mut self, + stacking_ctx: &StackingContext, + acquired_nodes: &mut AcquiredNodes<'_>, + viewport: &Viewport, + clipping: bool, + clip_rect: Option<Rect>, + draw_fn: &mut dyn FnMut( + &mut AcquiredNodes<'_>, + &mut DrawingCtx, + ) -> Result<BoundingBox, RenderingError>, + ) -> Result<BoundingBox, RenderingError> { + let stacking_ctx_transform = ValidTransform::try_from(stacking_ctx.transform)?; + + let orig_transform = self.get_transform(); + self.cr.transform(stacking_ctx_transform.into()); + + let res = if clipping { + draw_fn(acquired_nodes, self) + } else { + with_saved_cr(&self.cr.clone(), || { + if let Some(ref link_target) = stacking_ctx.link_target { + self.link_tag_begin(link_target); + } + + let Opacity(UnitInterval(opacity)) = stacking_ctx.opacity; + + let affine_at_start = self.get_transform(); + + if let Some(rect) = clip_rect { + clip_to_rectangle(&self.cr, &rect); + } + + // Here we are clipping in user space, so the bbox doesn't matter + self.clip_to_node( + &stacking_ctx.clip_in_user_space, + acquired_nodes, + viewport, + &self.empty_bbox(), + )?; + + let should_isolate = stacking_ctx.should_isolate(); + + let res = if should_isolate { + // Compute our assortment of affines + + let affines = CompositingAffines::new( + *affine_at_start, + self.initial_viewport.transform, + self.cr_stack.borrow().len(), + ); + + // Create temporary surface and its cr + + let cr = match stacking_ctx.filter { + None => cairo::Context::new( + &self + .create_similar_surface_for_toplevel_viewport(&self.cr.target())?, + )?, + Some(_) => { + cairo::Context::new(self.create_surface_for_toplevel_viewport()?)? + } + }; + + cr.set_matrix(ValidTransform::try_from(affines.for_temporary_surface)?.into()); + + let (source_surface, mut res, bbox) = { + let mut temporary_draw_ctx = self.nested(cr); + + // Draw! + + let res = draw_fn(acquired_nodes, &mut temporary_draw_ctx); + + let bbox = if let Ok(ref bbox) = res { + *bbox + } else { + BoundingBox::new().with_transform(affines.for_temporary_surface) + }; + + if let Some(ref filter) = stacking_ctx.filter { + let surface_to_filter = SharedImageSurface::copy_from_surface( + &cairo::ImageSurface::try_from(temporary_draw_ctx.cr.target()) + .unwrap(), + )?; + + let stroke_paint_source = + Rc::new(filter.stroke_paint_source.to_user_space( + &bbox.rect, + viewport, + &filter.normalize_values, + )); + let fill_paint_source = + Rc::new(filter.fill_paint_source.to_user_space( + &bbox.rect, + viewport, + &filter.normalize_values, + )); + + // Filter functions (like "blend()", not the <filter> element) require + // being resolved in userSpaceonUse units, since that is the default + // for primitive_units. So, get the corresponding NormalizeParams + // here and pass them down. + let user_space_params = NormalizeParams::from_values( + &filter.normalize_values, + &viewport.with_units(CoordUnits::UserSpaceOnUse), + ); + + let filtered_surface = temporary_draw_ctx + .run_filters( + viewport, + surface_to_filter, + filter, + acquired_nodes, + &stacking_ctx.element_name, + &user_space_params, + stroke_paint_source, + fill_paint_source, + bbox, + )? + .into_image_surface()?; + + let generic_surface: &cairo::Surface = &filtered_surface; // deref to Surface + + (generic_surface.clone(), res, bbox) + } else { + (temporary_draw_ctx.cr.target(), res, bbox) + } + }; + + // Set temporary surface as source + + self.cr + .set_matrix(ValidTransform::try_from(affines.compositing)?.into()); + self.cr.set_source_surface(&source_surface, 0.0, 0.0)?; + + // Clip + + self.cr.set_matrix( + ValidTransform::try_from(affines.outside_temporary_surface)?.into(), + ); + self.clip_to_node( + &stacking_ctx.clip_in_object_space, + acquired_nodes, + viewport, + &bbox, + )?; + + // Mask + + if let Some(ref mask_node) = stacking_ctx.mask { + res = res.and_then(|bbox| { + self.generate_cairo_mask( + mask_node, + viewport, + affines.for_temporary_surface, + &bbox, + acquired_nodes, + ) + .and_then(|mask_surf| { + if let Some(surf) = mask_surf { + self.cr.push_group(); + + self.cr.set_matrix( + ValidTransform::try_from(affines.compositing)?.into(), + ); + self.cr.mask_surface(&surf, 0.0, 0.0)?; + + Ok(self.cr.pop_group_to_source()?) + } else { + Ok(()) + } + }) + .map(|_: ()| bbox) + }); + } + + { + // Composite the temporary surface + + self.cr + .set_matrix(ValidTransform::try_from(affines.compositing)?.into()); + self.cr.set_operator(stacking_ctx.mix_blend_mode.into()); + + if opacity < 1.0 { + self.cr.paint_with_alpha(opacity)?; + } else { + self.cr.paint()?; + } + } + + self.cr.set_matrix(affine_at_start.into()); + res + } else { + draw_fn(acquired_nodes, self) + }; + + if stacking_ctx.link_target.is_some() { + self.link_tag_end(); + } + + res + }) + }; + + self.cr.set_matrix(orig_transform.into()); + res + } + + /// Run the drawing function with the specified opacity + fn with_alpha( + &mut self, + opacity: UnitInterval, + draw_fn: &mut dyn FnMut(&mut DrawingCtx) -> Result<BoundingBox, RenderingError>, + ) -> Result<BoundingBox, RenderingError> { + let res; + let UnitInterval(o) = opacity; + if o < 1.0 { + self.cr.push_group(); + res = draw_fn(self); + self.cr.pop_group_to_source()?; + self.cr.paint_with_alpha(o)?; + } else { + res = draw_fn(self); + } + + res + } + + /// Start a Cairo tag for PDF links + fn link_tag_begin(&mut self, link_target: &str) { + let attributes = format!("uri='{}'", escape_link_target(link_target)); + + let cr = self.cr.clone(); + cr.tag_begin(CAIRO_TAG_LINK, &attributes); + } + + /// End a Cairo tag for PDF links + fn link_tag_end(&mut self) { + self.cr.tag_end(CAIRO_TAG_LINK); + } + + fn run_filters( + &mut self, + viewport: &Viewport, + surface_to_filter: SharedImageSurface, + filter: &Filter, + acquired_nodes: &mut AcquiredNodes<'_>, + node_name: &str, + user_space_params: &NormalizeParams, + stroke_paint_source: Rc<UserSpacePaintSource>, + fill_paint_source: Rc<UserSpacePaintSource>, + node_bbox: BoundingBox, + ) -> Result<SharedImageSurface, RenderingError> { + // We try to convert each item in the filter_list to a FilterSpec. + // + // However, the spec mentions, "If the filter references a non-existent object or + // the referenced object is not a filter element, then the whole filter chain is + // ignored." - https://www.w3.org/TR/filter-effects/#FilterProperty + // + // So, run through the filter_list and collect into a Result<Vec<FilterSpec>>. + // This will return an Err if any of the conversions failed. + let filter_specs = filter + .filter_list + .iter() + .map(|filter_value| { + filter_value.to_filter_spec( + acquired_nodes, + user_space_params, + filter.current_color, + viewport, + self, + node_name, + ) + }) + .collect::<Result<Vec<FilterSpec>, _>>(); + + match filter_specs { + Ok(specs) => { + // Start with the surface_to_filter, and apply each filter spec in turn; + // the final result is our return value. + specs.iter().try_fold(surface_to_filter, |surface, spec| { + filters::render( + spec, + stroke_paint_source.clone(), + fill_paint_source.clone(), + surface, + acquired_nodes, + self, + *self.get_transform(), + node_bbox, + ) + }) + } + + Err(e) => { + rsvg_log!( + self.session, + "not rendering filter list on node {} because it was in error: {}", + node_name, + e + ); + // just return the original surface without filtering it + Ok(surface_to_filter) + } + } + } + + fn set_gradient(&mut self, gradient: &UserSpaceGradient) -> Result<(), RenderingError> { + let g = match gradient.variant { + GradientVariant::Linear { x1, y1, x2, y2 } => { + cairo::Gradient::clone(&cairo::LinearGradient::new(x1, y1, x2, y2)) + } + + GradientVariant::Radial { + cx, + cy, + r, + fx, + fy, + fr, + } => cairo::Gradient::clone(&cairo::RadialGradient::new(fx, fy, fr, cx, cy, r)), + }; + + g.set_matrix(ValidTransform::try_from(gradient.transform)?.into()); + g.set_extend(cairo::Extend::from(gradient.spread)); + + for stop in &gradient.stops { + let UnitInterval(stop_offset) = stop.offset; + + g.add_color_stop_rgba( + stop_offset, + f64::from(stop.rgba.red_f32()), + f64::from(stop.rgba.green_f32()), + f64::from(stop.rgba.blue_f32()), + f64::from(stop.rgba.alpha_f32()), + ); + } + + Ok(self.cr.set_source(&g)?) + } + + fn set_pattern( + &mut self, + pattern: &UserSpacePattern, + acquired_nodes: &mut AcquiredNodes<'_>, + ) -> Result<bool, RenderingError> { + // Bail out early if the pattern has zero size, per the spec + if approx_eq!(f64, pattern.width, 0.0) || approx_eq!(f64, pattern.height, 0.0) { + return Ok(false); + } + + // Bail out early if this pattern has a circular reference + let pattern_node_acquired = match pattern.acquire_pattern_node(acquired_nodes) { + Ok(n) => n, + + Err(AcquireError::CircularReference(ref node)) => { + rsvg_log!(self.session, "circular reference in element {}", node); + return Ok(false); + } + + _ => unreachable!(), + }; + + let pattern_node = pattern_node_acquired.get(); + + let taffine = self.get_transform().pre_transform(&pattern.transform); + + let mut scwscale = (taffine.xx.powi(2) + taffine.xy.powi(2)).sqrt(); + let mut schscale = (taffine.yx.powi(2) + taffine.yy.powi(2)).sqrt(); + + let pw: i32 = (pattern.width * scwscale) as i32; + let ph: i32 = (pattern.height * schscale) as i32; + + if pw < 1 || ph < 1 { + return Ok(false); + } + + scwscale = f64::from(pw) / pattern.width; + schscale = f64::from(ph) / pattern.height; + + // Apply the pattern transform + let (affine, caffine) = if scwscale.approx_eq_cairo(1.0) && schscale.approx_eq_cairo(1.0) { + (pattern.coord_transform, pattern.content_transform) + } else { + ( + pattern + .coord_transform + .pre_scale(1.0 / scwscale, 1.0 / schscale), + pattern.content_transform.post_scale(scwscale, schscale), + ) + }; + + // Draw to another surface + let surface = self + .cr + .target() + .create_similar(cairo::Content::ColorAlpha, pw, ph)?; + + let cr_pattern = cairo::Context::new(&surface)?; + + // Set up transformations to be determined by the contents units + + let transform = ValidTransform::try_from(caffine)?; + cr_pattern.set_matrix(transform.into()); + + // Draw everything + + { + let mut pattern_draw_ctx = self.nested(cr_pattern); + + let pattern_viewport = Viewport { + dpi: self.dpi, + vbox: ViewBox::from(Rect::from_size(pattern.width, pattern.height)), + transform: *transform, + }; + + pattern_draw_ctx + .with_alpha(pattern.opacity, &mut |dc| { + let pattern_cascaded = CascadedValues::new_from_node(pattern_node); + let pattern_values = pattern_cascaded.get(); + + let elt = pattern_node.borrow_element(); + + let stacking_ctx = StackingContext::new( + self.session(), + acquired_nodes, + &elt, + Transform::identity(), + pattern_values, + ); + + dc.with_discrete_layer( + &stacking_ctx, + acquired_nodes, + &pattern_viewport, + false, + None, + &mut |an, dc| { + pattern_node.draw_children( + an, + &pattern_cascaded, + &pattern_viewport, + dc, + false, + ) + }, + ) + }) + .map(|_| ())?; + } + + // Set the final surface as a Cairo pattern into the Cairo context + let pattern = cairo::SurfacePattern::create(&surface); + + if let Some(m) = affine.invert() { + pattern.set_matrix(ValidTransform::try_from(m)?.into()); + pattern.set_extend(cairo::Extend::Repeat); + pattern.set_filter(cairo::Filter::Best); + self.cr.set_source(&pattern)?; + } + + Ok(true) + } + + fn set_color(&self, rgba: cssparser::RGBA) { + self.cr.clone().set_source_rgba( + f64::from(rgba.red_f32()), + f64::from(rgba.green_f32()), + f64::from(rgba.blue_f32()), + f64::from(rgba.alpha_f32()), + ); + } + + fn set_paint_source( + &mut self, + paint_source: &UserSpacePaintSource, + acquired_nodes: &mut AcquiredNodes<'_>, + ) -> Result<bool, RenderingError> { + match *paint_source { + UserSpacePaintSource::Gradient(ref gradient, _c) => { + self.set_gradient(gradient)?; + Ok(true) + } + UserSpacePaintSource::Pattern(ref pattern, c) => { + if self.set_pattern(pattern, acquired_nodes)? { + Ok(true) + } else if let Some(c) = c { + self.set_color(c); + Ok(true) + } else { + Ok(false) + } + } + UserSpacePaintSource::SolidColor(c) => { + self.set_color(c); + Ok(true) + } + UserSpacePaintSource::None => Ok(false), + } + } + + /// Computes and returns a surface corresponding to the given paint server. + pub fn get_paint_source_surface( + &mut self, + width: i32, + height: i32, + acquired_nodes: &mut AcquiredNodes<'_>, + paint_source: &UserSpacePaintSource, + ) -> Result<SharedImageSurface, RenderingError> { + let mut surface = ExclusiveImageSurface::new(width, height, SurfaceType::SRgb)?; + + surface.draw(&mut |cr| { + let mut temporary_draw_ctx = self.nested(cr); + + // FIXME: we are ignoring any error + + let had_paint_server = + temporary_draw_ctx.set_paint_source(paint_source, acquired_nodes)?; + if had_paint_server { + temporary_draw_ctx.cr.paint()?; + } + + Ok(()) + })?; + + Ok(surface.share()?) + } + + fn stroke( + &mut self, + cr: &cairo::Context, + acquired_nodes: &mut AcquiredNodes<'_>, + paint_source: &UserSpacePaintSource, + ) -> Result<(), RenderingError> { + let had_paint_server = self.set_paint_source(paint_source, acquired_nodes)?; + if had_paint_server { + cr.stroke_preserve()?; + } + + Ok(()) + } + + fn fill( + &mut self, + cr: &cairo::Context, + acquired_nodes: &mut AcquiredNodes<'_>, + paint_source: &UserSpacePaintSource, + ) -> Result<(), RenderingError> { + let had_paint_server = self.set_paint_source(paint_source, acquired_nodes)?; + if had_paint_server { + cr.fill_preserve()?; + } + + Ok(()) + } + + pub fn compute_path_extents(&self, path: &Path) -> Result<Option<Rect>, RenderingError> { + if path.is_empty() { + return Ok(None); + } + + let surface = cairo::RecordingSurface::create(cairo::Content::ColorAlpha, None)?; + let cr = cairo::Context::new(&surface)?; + + path.to_cairo(&cr, false)?; + let (x0, y0, x1, y1) = cr.path_extents()?; + + Ok(Some(Rect::new(x0, y0, x1, y1))) + } + + pub fn draw_layer( + &mut self, + layer: &Layer, + acquired_nodes: &mut AcquiredNodes<'_>, + clipping: bool, + viewport: &Viewport, + ) -> Result<BoundingBox, RenderingError> { + match &layer.kind { + LayerKind::Shape(shape) => self.draw_shape( + shape, + &layer.stacking_ctx, + acquired_nodes, + clipping, + viewport, + ), + LayerKind::Text(text) => self.draw_text( + text, + &layer.stacking_ctx, + acquired_nodes, + clipping, + viewport, + ), + LayerKind::Image(image) => self.draw_image( + image, + &layer.stacking_ctx, + acquired_nodes, + clipping, + viewport, + ), + } + } + + fn draw_shape( + &mut self, + shape: &Shape, + stacking_ctx: &StackingContext, + acquired_nodes: &mut AcquiredNodes<'_>, + clipping: bool, + viewport: &Viewport, + ) -> Result<BoundingBox, RenderingError> { + if shape.extents.is_none() { + return Ok(self.empty_bbox()); + } + + self.with_discrete_layer( + stacking_ctx, + acquired_nodes, + viewport, + clipping, + None, + &mut |an, dc| { + let cr = dc.cr.clone(); + + let transform = dc.get_transform_for_stacking_ctx(stacking_ctx, clipping)?; + let mut path_helper = + PathHelper::new(&cr, transform, &shape.path, shape.stroke.line_cap); + + if clipping { + if shape.is_visible { + cr.set_fill_rule(cairo::FillRule::from(shape.clip_rule)); + path_helper.set()?; + } + return Ok(dc.empty_bbox()); + } + + cr.set_antialias(cairo::Antialias::from(shape.shape_rendering)); + + setup_cr_for_stroke(&cr, &shape.stroke); + + cr.set_fill_rule(cairo::FillRule::from(shape.fill_rule)); + + path_helper.set()?; + let bbox = compute_stroke_and_fill_box( + &cr, + &shape.stroke, + &shape.stroke_paint, + &dc.initial_viewport, + )?; + + if shape.is_visible { + for &target in &shape.paint_order.targets { + // fill and stroke operations will preserve the path. + // markers operation will clear the path. + match target { + PaintTarget::Fill => { + path_helper.set()?; + dc.fill(&cr, an, &shape.fill_paint)?; + } + + PaintTarget::Stroke => { + path_helper.set()?; + let backup_matrix = if shape.stroke.non_scaling { + let matrix = cr.matrix(); + cr.set_matrix( + ValidTransform::try_from(dc.initial_viewport.transform)? + .into(), + ); + Some(matrix) + } else { + None + }; + dc.stroke(&cr, an, &shape.stroke_paint)?; + if let Some(matrix) = backup_matrix { + cr.set_matrix(matrix); + } + } + + PaintTarget::Markers => { + path_helper.unset(); + marker::render_markers_for_shape( + shape, viewport, dc, an, clipping, + )?; + } + } + } + } + + path_helper.unset(); + Ok(bbox) + }, + ) + } + + fn paint_surface( + &mut self, + surface: &SharedImageSurface, + width: f64, + height: f64, + ) -> Result<(), cairo::Error> { + let cr = self.cr.clone(); + + // We need to set extend appropriately, so can't use cr.set_source_surface(). + // + // If extend is left at its default value (None), then bilinear scaling uses + // transparency outside of the image producing incorrect results. + // For example, in svg1.1/filters-blend-01-b.svgthere's a completely + // opaque 100×1 image of a gradient scaled to 100×98 which ends up + // transparent almost everywhere without this fix (which it shouldn't). + let ptn = surface.to_cairo_pattern(); + ptn.set_extend(cairo::Extend::Pad); + cr.set_source(&ptn)?; + + // Clip is needed due to extend being set to pad. + clip_to_rectangle(&cr, &Rect::from_size(width, height)); + + cr.paint() + } + + fn draw_image( + &mut self, + image: &Image, + stacking_ctx: &StackingContext, + acquired_nodes: &mut AcquiredNodes<'_>, + clipping: bool, + viewport: &Viewport, + ) -> Result<BoundingBox, RenderingError> { + let image_width = image.surface.width(); + let image_height = image.surface.height(); + if clipping || image.rect.is_empty() || image_width == 0 || image_height == 0 { + return Ok(self.empty_bbox()); + } + + let image_width = f64::from(image_width); + let image_height = f64::from(image_height); + let vbox = ViewBox::from(Rect::from_size(image_width, image_height)); + + let clip_mode = if !(image.overflow == Overflow::Auto + || image.overflow == Overflow::Visible) + && image.aspect.is_slice() + { + ClipMode::ClipToViewport + } else { + ClipMode::NoClip + }; + + // The bounding box for <image> is decided by the values of the image's x, y, w, h + // and not by the final computed image bounds. + let bounds = self.empty_bbox().with_rect(image.rect); + + if image.is_visible { + self.with_discrete_layer( + stacking_ctx, + acquired_nodes, + viewport, // FIXME: should this be the push_new_viewport below? + clipping, + None, + &mut |_an, dc| { + with_saved_cr(&dc.cr.clone(), || { + if let Some(_params) = dc.push_new_viewport( + viewport, + Some(vbox), + image.rect, + image.aspect, + clip_mode, + ) { + dc.paint_surface(&image.surface, image_width, image_height)?; + } + + Ok(bounds) + }) + }, + ) + } else { + Ok(bounds) + } + } + + fn draw_text_span( + &mut self, + span: &TextSpan, + acquired_nodes: &mut AcquiredNodes<'_>, + clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + let path = pango_layout_to_path(span.x, span.y, &span.layout, span.gravity)?; + if path.is_empty() { + // Empty strings, or only-whitespace text, get turned into empty paths. + // In that case, we really want to return "no bounds" rather than an + // empty rectangle. + return Ok(self.empty_bbox()); + } + + // #851 - We can't just render all text as paths for PDF; it + // needs the actual text content so text is selectable by PDF + // viewers. + let can_use_text_as_path = self.cr.target().type_() != cairo::SurfaceType::Pdf; + + with_saved_cr(&self.cr.clone(), || { + self.cr + .set_antialias(cairo::Antialias::from(span.text_rendering)); + + setup_cr_for_stroke(&self.cr, &span.stroke); + + if clipping { + path.to_cairo(&self.cr, false)?; + return Ok(self.empty_bbox()); + } + + path.to_cairo(&self.cr, false)?; + let bbox = compute_stroke_and_fill_box( + &self.cr, + &span.stroke, + &span.stroke_paint, + &self.initial_viewport, + )?; + self.cr.new_path(); + + if span.is_visible { + if let Some(ref link_target) = span.link_target { + self.link_tag_begin(link_target); + } + + for &target in &span.paint_order.targets { + match target { + PaintTarget::Fill => { + let had_paint_server = + self.set_paint_source(&span.fill_paint, acquired_nodes)?; + + if had_paint_server { + if can_use_text_as_path { + path.to_cairo(&self.cr, false)?; + self.cr.fill()?; + self.cr.new_path(); + } else { + self.cr.move_to(span.x, span.y); + + let matrix = self.cr.matrix(); + + let rotation_from_gravity = span.gravity.to_rotation(); + if !rotation_from_gravity.approx_eq_cairo(0.0) { + self.cr.rotate(-rotation_from_gravity); + } + + pangocairo::functions::update_layout(&self.cr, &span.layout); + pangocairo::functions::show_layout(&self.cr, &span.layout); + + self.cr.set_matrix(matrix); + } + } + } + + PaintTarget::Stroke => { + let had_paint_server = + self.set_paint_source(&span.stroke_paint, acquired_nodes)?; + + if had_paint_server { + path.to_cairo(&self.cr, false)?; + self.cr.stroke()?; + self.cr.new_path(); + } + } + + PaintTarget::Markers => {} + } + } + + if span.link_target.is_some() { + self.link_tag_end(); + } + } + + Ok(bbox) + }) + } + + fn draw_text( + &mut self, + text: &Text, + stacking_ctx: &StackingContext, + acquired_nodes: &mut AcquiredNodes<'_>, + clipping: bool, + viewport: &Viewport, + ) -> Result<BoundingBox, RenderingError> { + self.with_discrete_layer( + stacking_ctx, + acquired_nodes, + viewport, + clipping, + None, + &mut |an, dc| { + let mut bbox = dc.empty_bbox(); + + for span in &text.spans { + let span_bbox = dc.draw_text_span(span, an, clipping)?; + bbox.insert(&span_bbox); + } + + Ok(bbox) + }, + ) + } + + pub fn get_snapshot( + &self, + width: i32, + height: i32, + ) -> Result<SharedImageSurface, RenderingError> { + // TODO: as far as I can tell this should not render elements past the last (topmost) one + // with enable-background: new (because technically we shouldn't have been caching them). + // Right now there are no enable-background checks whatsoever. + // + // Addendum: SVG 2 has deprecated the enable-background property, and replaced it with an + // "isolation" property from the CSS Compositing and Blending spec. + // + // Deprecation: + // https://www.w3.org/TR/filter-effects-1/#AccessBackgroundImage + // + // BackgroundImage, BackgroundAlpha in the "in" attribute of filter primitives: + // https://www.w3.org/TR/filter-effects-1/#attr-valuedef-in-backgroundimage + // + // CSS Compositing and Blending, "isolation" property: + // https://www.w3.org/TR/compositing-1/#isolation + let mut surface = ExclusiveImageSurface::new(width, height, SurfaceType::SRgb)?; + + surface.draw(&mut |cr| { + // TODO: apparently DrawingCtx.cr_stack is just a way to store pairs of + // (surface, transform). Can we turn it into a DrawingCtx.surface_stack + // instead? See what CSS isolation would like to call that; are the pairs just + // stacking contexts instead, or the result of rendering stacking contexts? + for (depth, draw) in self.cr_stack.borrow().iter().enumerate() { + let affines = CompositingAffines::new( + Transform::from(draw.matrix()), + self.initial_viewport.transform, + depth, + ); + + cr.set_matrix(ValidTransform::try_from(affines.for_snapshot)?.into()); + cr.set_source_surface(&draw.target(), 0.0, 0.0)?; + cr.paint()?; + } + + Ok(()) + })?; + + Ok(surface.share()?) + } + + pub fn draw_node_to_surface( + &mut self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + affine: Transform, + width: i32, + height: i32, + ) -> Result<SharedImageSurface, RenderingError> { + let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, width, height)?; + + let save_cr = self.cr.clone(); + + { + let cr = cairo::Context::new(&surface)?; + cr.set_matrix(ValidTransform::try_from(affine)?.into()); + + self.cr = cr; + let viewport = Viewport { + dpi: self.dpi, + transform: affine, + vbox: ViewBox::from(Rect::from_size(f64::from(width), f64::from(height))), + }; + + let _ = self.draw_node_from_stack(node, acquired_nodes, cascaded, &viewport, false)?; + } + + self.cr = save_cr; + + Ok(SharedImageSurface::wrap(surface, SurfaceType::SRgb)?) + } + + pub fn draw_node_from_stack( + &mut self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + let stack_top = self.drawsub_stack.pop(); + + let draw = if let Some(ref top) = stack_top { + top == node + } else { + true + }; + + let res = if draw { + node.draw(acquired_nodes, cascaded, viewport, self, clipping) + } else { + Ok(self.empty_bbox()) + }; + + if let Some(top) = stack_top { + self.drawsub_stack.push(top); + } + + res + } + + pub fn draw_from_use_node( + &mut self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + values: &ComputedValues, + use_rect: Rect, + link: &NodeId, + clipping: bool, + viewport: &Viewport, + fill_paint: Arc<PaintSource>, + stroke_paint: Arc<PaintSource>, + ) -> Result<BoundingBox, RenderingError> { + // <use> is an element that is used directly, unlike + // <pattern>, which is used through a fill="url(#...)" + // reference. However, <use> will always reference another + // element, potentially itself or an ancestor of itself (or + // another <use> which references the first one, etc.). So, + // we acquire the <use> element itself so that circular + // references can be caught. + let _self_acquired = match acquired_nodes.acquire_ref(node) { + Ok(n) => n, + + Err(AcquireError::CircularReference(_)) => { + rsvg_log!(self.session, "circular reference in element {}", node); + return Ok(self.empty_bbox()); + } + + _ => unreachable!(), + }; + + let acquired = match acquired_nodes.acquire(link) { + Ok(acquired) => acquired, + + Err(AcquireError::CircularReference(node)) => { + rsvg_log!(self.session, "circular reference in element {}", node); + return Ok(self.empty_bbox()); + } + + Err(AcquireError::MaxReferencesExceeded) => { + return Err(RenderingError::LimitExceeded( + ImplementationLimit::TooManyReferencedElements, + )); + } + + Err(AcquireError::InvalidLinkType(_)) => unreachable!(), + + Err(AcquireError::LinkNotFound(node_id)) => { + rsvg_log!( + self.session, + "element {} references nonexistent \"{}\"", + node, + node_id + ); + return Ok(self.empty_bbox()); + } + }; + + // width or height set to 0 disables rendering of the element + // https://www.w3.org/TR/SVG/struct.html#UseElementWidthAttribute + if use_rect.is_empty() { + return Ok(self.empty_bbox()); + } + + let child = acquired.get(); + + if clipping && !element_can_be_used_inside_use_inside_clip_path(&child.borrow_element()) { + return Ok(self.empty_bbox()); + } + + let orig_transform = self.get_transform(); + + self.cr + .transform(ValidTransform::try_from(values.transform())?.into()); + + let use_element = node.borrow_element(); + + let defines_a_viewport = if is_element_of_type!(child, Symbol) { + let symbol = borrow_element_as!(child, Symbol); + Some((symbol.get_viewbox(), symbol.get_preserve_aspect_ratio())) + } else if is_element_of_type!(child, Svg) { + let svg = borrow_element_as!(child, Svg); + Some((svg.get_viewbox(), svg.get_preserve_aspect_ratio())) + } else { + None + }; + + let res = if let Some((viewbox, preserve_aspect_ratio)) = defines_a_viewport { + // <symbol> and <svg> define a viewport, as described in the specification: + // https://www.w3.org/TR/SVG2/struct.html#UseElement + // https://gitlab.gnome.org/GNOME/librsvg/-/issues/875#note_1482705 + + let elt = child.borrow_element(); + + let values = elt.get_computed_values(); + + // FIXME: do we need to look at preserveAspectRatio.slice, like in draw_image()? + let clip_mode = if !values.is_overflow() { + ClipMode::ClipToViewport + } else { + ClipMode::NoClip + }; + + let stacking_ctx = StackingContext::new( + self.session(), + acquired_nodes, + &use_element, + Transform::identity(), + values, + ); + + self.with_discrete_layer( + &stacking_ctx, + acquired_nodes, + viewport, // FIXME: should this be the child_viewport from below? + clipping, + None, + &mut |an, dc| { + if let Some(child_viewport) = dc.push_new_viewport( + viewport, + viewbox, + use_rect, + preserve_aspect_ratio, + clip_mode, + ) { + child.draw_children( + an, + &CascadedValues::new_from_values( + child, + values, + Some(fill_paint.clone()), + Some(stroke_paint.clone()), + ), + &child_viewport, + dc, + clipping, + ) + } else { + Ok(dc.empty_bbox()) + } + }, + ) + } else { + // otherwise the referenced node is not a <symbol>; process it generically + + let stacking_ctx = StackingContext::new( + self.session(), + acquired_nodes, + &use_element, + Transform::new_translate(use_rect.x0, use_rect.y0), + values, + ); + + self.with_discrete_layer( + &stacking_ctx, + acquired_nodes, + viewport, + clipping, + None, + &mut |an, dc| { + child.draw( + an, + &CascadedValues::new_from_values( + child, + values, + Some(fill_paint.clone()), + Some(stroke_paint.clone()), + ), + viewport, + dc, + clipping, + ) + }, + ) + }; + + self.cr.set_matrix(orig_transform.into()); + + if let Ok(bbox) = res { + let mut res_bbox = BoundingBox::new().with_transform(*orig_transform); + res_bbox.insert(&bbox); + Ok(res_bbox) + } else { + res + } + } + + /// Extracts the font options for the current state of the DrawingCtx. + /// + /// You can use the font options later with create_pango_context(). + pub fn get_font_options(&self) -> FontOptions { + let mut options = cairo::FontOptions::new().unwrap(); + if self.testing { + options.set_antialias(cairo::Antialias::Gray); + } + + options.set_hint_style(cairo::HintStyle::None); + options.set_hint_metrics(cairo::HintMetrics::Off); + + FontOptions { options } + } +} + +/// Create a Pango context with a particular configuration. +pub fn create_pango_context(font_options: &FontOptions, transform: &Transform) -> pango::Context { + let font_map = pangocairo::FontMap::default(); + let context = font_map.create_context(); + + context.set_round_glyph_positions(false); + + let pango_matrix = PangoMatrix { + xx: transform.xx, + xy: transform.xy, + yx: transform.yx, + yy: transform.yy, + x0: transform.x0, + y0: transform.y0, + }; + + let pango_matrix_ptr: *const PangoMatrix = &pango_matrix; + + let matrix = unsafe { pango::Matrix::from_glib_none(pango_matrix_ptr) }; + context.set_matrix(Some(&matrix)); + + pangocairo::functions::context_set_font_options(&context, Some(&font_options.options)); + + // Pango says this about pango_cairo_context_set_resolution(): + // + // Sets the resolution for the context. This is a scale factor between + // points specified in a #PangoFontDescription and Cairo units. The + // default value is 96, meaning that a 10 point font will be 13 + // units high. (10 * 96. / 72. = 13.3). + // + // I.e. Pango font sizes in a PangoFontDescription are in *points*, not pixels. + // However, we are normalizing everything to userspace units, which amount to + // pixels. So, we will use 72.0 here to make Pango not apply any further scaling + // to the size values we give it. + // + // An alternative would be to divide our font sizes by (dpi_y / 72) to effectively + // cancel out Pango's scaling, but it's probably better to deal with Pango-isms + // right here, instead of spreading them out through our Length normalization + // code. + pangocairo::functions::context_set_resolution(&context, 72.0); + + context +} + +/// Converts a Pango layout to a Cairo path on the specified cr starting at (x, y). +/// Does not clear the current path first. +fn pango_layout_to_cairo( + x: f64, + y: f64, + layout: &pango::Layout, + gravity: pango::Gravity, + cr: &cairo::Context, +) { + let rotation_from_gravity = gravity.to_rotation(); + let rotation = if !rotation_from_gravity.approx_eq_cairo(0.0) { + Some(-rotation_from_gravity) + } else { + None + }; + + cr.move_to(x, y); + + let matrix = cr.matrix(); + if let Some(rot) = rotation { + cr.rotate(rot); + } + + pangocairo::functions::update_layout(cr, layout); + pangocairo::functions::layout_path(cr, layout); + cr.set_matrix(matrix); +} + +/// Converts a Pango layout to a Path starting at (x, y). +pub fn pango_layout_to_path( + x: f64, + y: f64, + layout: &pango::Layout, + gravity: pango::Gravity, +) -> Result<Path, RenderingError> { + let surface = cairo::RecordingSurface::create(cairo::Content::ColorAlpha, None)?; + let cr = cairo::Context::new(&surface)?; + + pango_layout_to_cairo(x, y, layout, gravity, &cr); + + let cairo_path = cr.copy_path()?; + Ok(Path::from_cairo(cairo_path)) +} + +// https://www.w3.org/TR/css-masking-1/#ClipPathElement +fn element_can_be_used_inside_clip_path(element: &Element) -> bool { + use ElementData::*; + + matches!( + element.element_data, + Circle(_) + | Ellipse(_) + | Line(_) + | Path(_) + | Polygon(_) + | Polyline(_) + | Rect(_) + | Text(_) + | Use(_) + ) +} + +// https://www.w3.org/TR/css-masking-1/#ClipPathElement +fn element_can_be_used_inside_use_inside_clip_path(element: &Element) -> bool { + use ElementData::*; + + matches!( + element.element_data, + Circle(_) | Ellipse(_) | Line(_) | Path(_) | Polygon(_) | Polyline(_) | Rect(_) | Text(_) + ) +} + +#[derive(Debug)] +struct CompositingAffines { + pub outside_temporary_surface: Transform, + #[allow(unused)] + pub initial: Transform, + pub for_temporary_surface: Transform, + pub compositing: Transform, + pub for_snapshot: Transform, +} + +impl CompositingAffines { + fn new(current: Transform, initial: Transform, cr_stack_depth: usize) -> CompositingAffines { + let is_topmost_temporary_surface = cr_stack_depth == 0; + + let initial_inverse = initial.invert().unwrap(); + + let outside_temporary_surface = if is_topmost_temporary_surface { + current + } else { + current.post_transform(&initial_inverse) + }; + + let (scale_x, scale_y) = initial.transform_distance(1.0, 1.0); + + let for_temporary_surface = if is_topmost_temporary_surface { + current + .post_transform(&initial_inverse) + .post_scale(scale_x, scale_y) + } else { + current + }; + + let compositing = if is_topmost_temporary_surface { + initial.pre_scale(1.0 / scale_x, 1.0 / scale_y) + } else { + Transform::identity() + }; + + let for_snapshot = compositing.invert().unwrap(); + + CompositingAffines { + outside_temporary_surface, + initial, + for_temporary_surface, + compositing, + for_snapshot, + } + } +} + +fn compute_stroke_and_fill_extents( + cr: &cairo::Context, + stroke: &Stroke, + stroke_paint_source: &UserSpacePaintSource, + initial_viewport: &Viewport, +) -> Result<PathExtents, RenderingError> { + // Dropping the precision of cairo's bezier subdivision, yielding 2x + // _rendering_ time speedups, are these rather expensive operations + // really needed here? */ + let backup_tolerance = cr.tolerance(); + cr.set_tolerance(1.0); + + // Bounding box for fill + // + // Unlike the case for stroke, for fills we always compute the bounding box. + // In GNOME we have SVGs for symbolic icons where each icon has a bounding + // rectangle with no fill and no stroke, and inside it there are the actual + // paths for the icon's shape. We need to be able to compute the bounding + // rectangle's extents, even when it has no fill nor stroke. + + let (x0, y0, x1, y1) = cr.fill_extents()?; + let fill_extents = Some(Rect::new(x0, y0, x1, y1)); + + // Bounding box for stroke + // + // When presented with a line width of 0, Cairo returns a + // stroke_extents rectangle of (0, 0, 0, 0). This would cause the + // bbox to include a lone point at the origin, which is wrong, as a + // stroke of zero width should not be painted, per + // https://www.w3.org/TR/SVG2/painting.html#StrokeWidth + // + // So, see if the stroke width is 0 and just not include the stroke in the + // bounding box if so. + + let stroke_extents = if !stroke.width.approx_eq_cairo(0.0) + && !matches!(stroke_paint_source, UserSpacePaintSource::None) + { + let backup_matrix = if stroke.non_scaling { + let matrix = cr.matrix(); + cr.set_matrix(ValidTransform::try_from(initial_viewport.transform)?.into()); + Some(matrix) + } else { + None + }; + let (x0, y0, x1, y1) = cr.stroke_extents()?; + if let Some(matrix) = backup_matrix { + cr.set_matrix(matrix); + } + Some(Rect::new(x0, y0, x1, y1)) + } else { + None + }; + + // objectBoundingBox + + let (x0, y0, x1, y1) = cr.path_extents()?; + let path_extents = Some(Rect::new(x0, y0, x1, y1)); + + // restore tolerance + + cr.set_tolerance(backup_tolerance); + + Ok(PathExtents { + path_only: path_extents, + fill: fill_extents, + stroke: stroke_extents, + }) +} + +fn compute_stroke_and_fill_box( + cr: &cairo::Context, + stroke: &Stroke, + stroke_paint_source: &UserSpacePaintSource, + initial_viewport: &Viewport, +) -> Result<BoundingBox, RenderingError> { + let extents = + compute_stroke_and_fill_extents(cr, stroke, stroke_paint_source, initial_viewport)?; + + let ink_rect = match (extents.fill, extents.stroke) { + (None, None) => None, + (Some(f), None) => Some(f), + (None, Some(s)) => Some(s), + (Some(f), Some(s)) => Some(f.union(&s)), + }; + + let mut bbox = BoundingBox::new().with_transform(Transform::from(cr.matrix())); + + if let Some(rect) = extents.path_only { + bbox = bbox.with_rect(rect); + } + + if let Some(ink_rect) = ink_rect { + bbox = bbox.with_ink_rect(ink_rect); + } + + Ok(bbox) +} + +fn setup_cr_for_stroke(cr: &cairo::Context, stroke: &Stroke) { + cr.set_line_width(stroke.width); + cr.set_miter_limit(stroke.miter_limit.0); + cr.set_line_cap(cairo::LineCap::from(stroke.line_cap)); + cr.set_line_join(cairo::LineJoin::from(stroke.line_join)); + + let total_length: f64 = stroke.dashes.iter().sum(); + + if total_length > 0.0 { + cr.set_dash(&stroke.dashes, stroke.dash_offset); + } else { + cr.set_dash(&[], 0.0); + } +} + +/// escape quotes and backslashes with backslash +fn escape_link_target(value: &str) -> Cow<'_, str> { + static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"['\\]").unwrap()); + + REGEX.replace_all(value, |caps: &Captures<'_>| { + match caps.get(0).unwrap().as_str() { + "'" => "\\'".to_owned(), + "\\" => "\\\\".to_owned(), + _ => unreachable!(), + } + }) +} + +fn clip_to_rectangle(cr: &cairo::Context, r: &Rect) { + cr.rectangle(r.x0, r.y0, r.width(), r.height()); + cr.clip(); +} + +impl From<SpreadMethod> for cairo::Extend { + fn from(s: SpreadMethod) -> cairo::Extend { + match s { + SpreadMethod::Pad => cairo::Extend::Pad, + SpreadMethod::Reflect => cairo::Extend::Reflect, + SpreadMethod::Repeat => cairo::Extend::Repeat, + } + } +} + +impl From<StrokeLinejoin> for cairo::LineJoin { + fn from(j: StrokeLinejoin) -> cairo::LineJoin { + match j { + StrokeLinejoin::Miter => cairo::LineJoin::Miter, + StrokeLinejoin::Round => cairo::LineJoin::Round, + StrokeLinejoin::Bevel => cairo::LineJoin::Bevel, + } + } +} + +impl From<StrokeLinecap> for cairo::LineCap { + fn from(j: StrokeLinecap) -> cairo::LineCap { + match j { + StrokeLinecap::Butt => cairo::LineCap::Butt, + StrokeLinecap::Round => cairo::LineCap::Round, + StrokeLinecap::Square => cairo::LineCap::Square, + } + } +} + +impl From<MixBlendMode> for cairo::Operator { + fn from(m: MixBlendMode) -> cairo::Operator { + use cairo::Operator; + + match m { + MixBlendMode::Normal => Operator::Over, + MixBlendMode::Multiply => Operator::Multiply, + MixBlendMode::Screen => Operator::Screen, + MixBlendMode::Overlay => Operator::Overlay, + MixBlendMode::Darken => Operator::Darken, + MixBlendMode::Lighten => Operator::Lighten, + MixBlendMode::ColorDodge => Operator::ColorDodge, + MixBlendMode::ColorBurn => Operator::ColorBurn, + MixBlendMode::HardLight => Operator::HardLight, + MixBlendMode::SoftLight => Operator::SoftLight, + MixBlendMode::Difference => Operator::Difference, + MixBlendMode::Exclusion => Operator::Exclusion, + MixBlendMode::Hue => Operator::HslHue, + MixBlendMode::Saturation => Operator::HslSaturation, + MixBlendMode::Color => Operator::HslColor, + MixBlendMode::Luminosity => Operator::HslLuminosity, + } + } +} + +impl From<ClipRule> for cairo::FillRule { + fn from(c: ClipRule) -> cairo::FillRule { + match c { + ClipRule::NonZero => cairo::FillRule::Winding, + ClipRule::EvenOdd => cairo::FillRule::EvenOdd, + } + } +} + +impl From<FillRule> for cairo::FillRule { + fn from(f: FillRule) -> cairo::FillRule { + match f { + FillRule::NonZero => cairo::FillRule::Winding, + FillRule::EvenOdd => cairo::FillRule::EvenOdd, + } + } +} + +impl From<ShapeRendering> for cairo::Antialias { + fn from(sr: ShapeRendering) -> cairo::Antialias { + match sr { + ShapeRendering::Auto | ShapeRendering::GeometricPrecision => cairo::Antialias::Default, + ShapeRendering::OptimizeSpeed | ShapeRendering::CrispEdges => cairo::Antialias::None, + } + } +} + +impl From<TextRendering> for cairo::Antialias { + fn from(tr: TextRendering) -> cairo::Antialias { + match tr { + TextRendering::Auto + | TextRendering::OptimizeLegibility + | TextRendering::GeometricPrecision => cairo::Antialias::Default, + TextRendering::OptimizeSpeed => cairo::Antialias::None, + } + } +} + +impl From<cairo::Matrix> for Transform { + #[inline] + fn from(m: cairo::Matrix) -> Self { + Self::new_unchecked(m.xx(), m.yx(), m.xy(), m.yy(), m.x0(), m.y0()) + } +} + +impl From<ValidTransform> for cairo::Matrix { + #[inline] + fn from(t: ValidTransform) -> cairo::Matrix { + cairo::Matrix::new(t.xx, t.yx, t.xy, t.yy, t.x0, t.y0) + } +} + +/// Extents for a path in its current coordinate system. +/// +/// Normally you'll want to convert this to a BoundingBox, which has knowledge about just +/// what that coordinate system is. +pub struct PathExtents { + /// Extents of the "plain", unstroked path, or `None` if the path is empty. + pub path_only: Option<Rect>, + + /// Extents of just the fill, or `None` if the path is empty. + pub fill: Option<Rect>, + + /// Extents for the stroked path, or `None` if the path is empty or zero-width. + pub stroke: Option<Rect>, +} + +impl Path { + pub fn to_cairo( + &self, + cr: &cairo::Context, + is_square_linecap: bool, + ) -> Result<(), RenderingError> { + assert!(!self.is_empty()); + + for subpath in self.iter_subpath() { + // If a subpath is empty and the linecap is a square, then draw a square centered on + // the origin of the subpath. See #165. + if is_square_linecap { + let (x, y) = subpath.origin(); + if subpath.is_zero_length() { + let stroke_size = 0.002; + + cr.move_to(x - stroke_size / 2., y); + cr.line_to(x + stroke_size / 2., y); + } + } + + for cmd in subpath.iter_commands() { + cmd.to_cairo(cr); + } + } + + // We check the cr's status right after feeding it a new path for a few reasons: + // + // * Any of the individual path commands may cause the cr to enter an error state, for + // example, if they come with coordinates outside of Cairo's supported range. + // + // * The *next* call to the cr will probably be something that actually checks the status + // (i.e. in cairo-rs), and we don't want to panic there. + + cr.status().map_err(|e| e.into()) + } + + /// Converts a `cairo::Path` to a librsvg `Path`. + fn from_cairo(cairo_path: cairo::Path) -> Path { + let mut builder = PathBuilder::default(); + + // Cairo has the habit of appending a MoveTo to some paths, but we don't want a + // path for empty text to generate that lone point. So, strip out paths composed + // only of MoveTo. + + if !cairo_path_is_only_move_tos(&cairo_path) { + for segment in cairo_path.iter() { + match segment { + cairo::PathSegment::MoveTo((x, y)) => builder.move_to(x, y), + cairo::PathSegment::LineTo((x, y)) => builder.line_to(x, y), + cairo::PathSegment::CurveTo((x2, y2), (x3, y3), (x4, y4)) => { + builder.curve_to(x2, y2, x3, y3, x4, y4) + } + cairo::PathSegment::ClosePath => builder.close_path(), + } + } + } + + builder.into_path() + } +} + +fn cairo_path_is_only_move_tos(path: &cairo::Path) -> bool { + path.iter() + .all(|seg| matches!(seg, cairo::PathSegment::MoveTo((_, _)))) +} + +impl PathCommand { + fn to_cairo(&self, cr: &cairo::Context) { + match *self { + PathCommand::MoveTo(x, y) => cr.move_to(x, y), + PathCommand::LineTo(x, y) => cr.line_to(x, y), + PathCommand::CurveTo(ref curve) => curve.to_cairo(cr), + PathCommand::Arc(ref arc) => arc.to_cairo(cr), + PathCommand::ClosePath => cr.close_path(), + } + } +} + +impl EllipticalArc { + fn to_cairo(&self, cr: &cairo::Context) { + match self.center_parameterization() { + ArcParameterization::CenterParameters { + center, + radii, + theta1, + delta_theta, + } => { + let n_segs = (delta_theta / (PI * 0.5 + 0.001)).abs().ceil() as u32; + let d_theta = delta_theta / f64::from(n_segs); + + let mut theta = theta1; + for _ in 0..n_segs { + arc_segment(center, radii, self.x_axis_rotation, theta, theta + d_theta) + .to_cairo(cr); + theta += d_theta; + } + } + ArcParameterization::LineTo => { + let (x2, y2) = self.to; + cr.line_to(x2, y2); + } + ArcParameterization::Omit => {} + } + } +} + +impl CubicBezierCurve { + fn to_cairo(&self, cr: &cairo::Context) { + let Self { pt1, pt2, to } = *self; + cr.curve_to(pt1.0, pt1.1, pt2.0, pt2.1, to.0, to.1); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rsvg_path_from_cairo_path() { + let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 10, 10).unwrap(); + let cr = cairo::Context::new(&surface).unwrap(); + + cr.move_to(1.0, 2.0); + cr.line_to(3.0, 4.0); + cr.curve_to(5.0, 6.0, 7.0, 8.0, 9.0, 10.0); + cr.close_path(); + + let cairo_path = cr.copy_path().unwrap(); + let path = Path::from_cairo(cairo_path); + + assert_eq!( + path.iter().collect::<Vec<PathCommand>>(), + vec![ + PathCommand::MoveTo(1.0, 2.0), + PathCommand::LineTo(3.0, 4.0), + PathCommand::CurveTo(CubicBezierCurve { + pt1: (5.0, 6.0), + pt2: (7.0, 8.0), + to: (9.0, 10.0), + }), + PathCommand::ClosePath, + PathCommand::MoveTo(1.0, 2.0), // cairo inserts a MoveTo after ClosePath + ], + ); + } +} |