//! Toplevel handle for a loaded SVG document.
//!
//! This module provides the primitives on which the public APIs are implemented.
use std::sync::Arc;
use crate::accept_language::UserLanguage;
use crate::bbox::BoundingBox;
use crate::css::{Origin, Stylesheet};
use crate::document::{AcquiredNodes, Document, NodeId};
use crate::dpi::Dpi;
use crate::drawing_ctx::{draw_tree, with_saved_cr, DrawingMode, Viewport};
use crate::error::{DefsLookupErrorKind, LoadingError, RenderingError};
use crate::length::*;
use crate::node::{CascadedValues, Node, NodeBorrow};
use crate::rect::Rect;
use crate::session::Session;
use crate::structure::IntrinsicDimensions;
use crate::url_resolver::{AllowedUrl, UrlResolver};
/// Loading options for SVG documents.
pub struct LoadOptions {
/// Load url resolver; all references will be resolved with respect to this.
pub url_resolver: UrlResolver,
/// Whether to turn off size limits in libxml2.
pub unlimited_size: bool,
/// Whether to keep original (undecoded) image data to embed in Cairo PDF surfaces.
pub keep_image_data: bool,
}
impl LoadOptions {
/// Creates a `LoadOptions` with defaults, and sets the `url resolver`.
pub fn new(url_resolver: UrlResolver) -> Self {
LoadOptions {
url_resolver,
unlimited_size: false,
keep_image_data: false,
}
}
/// Sets whether libxml2's limits on memory usage should be turned off.
///
/// This should only be done for trusted data.
pub fn with_unlimited_size(mut self, unlimited: bool) -> Self {
self.unlimited_size = unlimited;
self
}
/// Sets whether to keep the original compressed image data from referenced JPEG/PNG images.
///
/// This is only useful for rendering to Cairo PDF
/// surfaces, which can embed the original, compressed image data instead of uncompressed
/// RGB buffers.
pub fn keep_image_data(mut self, keep: bool) -> Self {
self.keep_image_data = keep;
self
}
/// Creates a new `LoadOptions` with a different `url resolver`.
///
/// This is used when loading a referenced file that may in turn cause other files
/// to be loaded, for example ``
pub fn copy_with_base_url(&self, base_url: &AllowedUrl) -> Self {
let mut url_resolver = self.url_resolver.clone();
url_resolver.base_url = Some((**base_url).clone());
LoadOptions {
url_resolver,
unlimited_size: self.unlimited_size,
keep_image_data: self.keep_image_data,
}
}
}
/// Main handle to an SVG document.
///
/// This is the main object in librsvg. It gets created with the [`from_stream`] method
/// and then provides access to all the primitives needed to implement the public APIs.
///
/// [`from_stream`]: #method.from_stream
pub struct Handle {
session: Session,
document: Document,
}
impl Handle {
/// Loads an SVG document into a `Handle`.
pub fn from_stream(
session: Session,
load_options: Arc,
stream: &gio::InputStream,
cancellable: Option<&gio::Cancellable>,
) -> Result {
Ok(Handle {
session: session.clone(),
document: Document::load_from_stream(session, load_options, stream, cancellable)?,
})
}
/// Queries whether a document has a certain element `#foo`.
///
/// The `id` must be an URL fragment identifier, i.e. something
/// like `#element_id`.
pub fn has_sub(&self, id: &str) -> Result {
match self.lookup_node(id) {
Ok(_) => Ok(true),
Err(DefsLookupErrorKind::NotFound) => Ok(false),
Err(e) => Err(e.into()),
}
}
/// Normalizes the svg's width/height properties with a 0-sized viewport
///
/// This assumes that if one of the properties is in percentage units, then
/// its corresponding value will not be used. E.g. if width=100%, the caller
/// will ignore the resulting width value.
pub fn width_height_to_user(&self, dpi: Dpi) -> (f64, f64) {
let dimensions = self.get_intrinsic_dimensions();
let width = dimensions.width;
let height = dimensions.height;
let view_params = Viewport::new(dpi, 0.0, 0.0);
let root = self.document.root();
let cascaded = CascadedValues::new_from_node(&root);
let values = cascaded.get();
let params = NormalizeParams::new(values, &view_params);
(width.to_user(¶ms), height.to_user(¶ms))
}
fn get_node_or_root(&self, id: Option<&str>) -> Result {
if let Some(id) = id {
Ok(self.lookup_node(id)?)
} else {
Ok(self.document.root())
}
}
fn geometry_for_layer(
&self,
node: Node,
viewport: Rect,
user_language: &UserLanguage,
dpi: Dpi,
is_testing: bool,
) -> Result<(Rect, Rect), RenderingError> {
let root = self.document.root();
let target = cairo::ImageSurface::create(cairo::Format::Rgb24, 1, 1)?;
let cr = cairo::Context::new(&target)?;
let bbox = draw_tree(
self.session.clone(),
DrawingMode::LimitToStack { node, root },
&cr,
viewport,
user_language,
dpi,
true,
is_testing,
&mut AcquiredNodes::new(&self.document),
)?;
let ink_rect = bbox.ink_rect.unwrap_or_default();
let logical_rect = bbox.rect.unwrap_or_default();
Ok((ink_rect, logical_rect))
}
pub fn get_geometry_for_layer(
&self,
id: Option<&str>,
viewport: &cairo::Rectangle,
user_language: &UserLanguage,
dpi: Dpi,
is_testing: bool,
) -> Result<(cairo::Rectangle, cairo::Rectangle), RenderingError> {
let viewport = Rect::from(*viewport);
let node = self.get_node_or_root(id)?;
let (ink_rect, logical_rect) =
self.geometry_for_layer(node, viewport, user_language, dpi, is_testing)?;
Ok((
cairo::Rectangle::from(ink_rect),
cairo::Rectangle::from(logical_rect),
))
}
fn lookup_node(&self, id: &str) -> Result {
let node_id = NodeId::parse(id).map_err(|_| DefsLookupErrorKind::InvalidId)?;
// The public APIs to get geometries of individual elements, or to render
// them, should only allow referencing elements within the main handle's
// SVG file; that is, only plain "#foo" fragment IDs are allowed here.
// Otherwise, a calling program could request "another-file#foo" and cause
// another-file to be loaded, even if it is not part of the set of
// resources that the main SVG actually references. In the future we may
// relax this requirement to allow lookups within that set, but not to
// other random files.
match node_id {
NodeId::Internal(id) => self
.document
.lookup_internal_node(&id)
.ok_or(DefsLookupErrorKind::NotFound),
NodeId::External(_, _) => {
rsvg_log!(
self.session,
"the public API is not allowed to look up external references: {}",
node_id
);
Err(DefsLookupErrorKind::CannotLookupExternalReferences)
}
}
}
pub fn render_document(
&self,
cr: &cairo::Context,
viewport: &cairo::Rectangle,
user_language: &UserLanguage,
dpi: Dpi,
is_testing: bool,
) -> Result<(), RenderingError> {
self.render_layer(cr, None, viewport, user_language, dpi, is_testing)
}
pub fn render_layer(
&self,
cr: &cairo::Context,
id: Option<&str>,
viewport: &cairo::Rectangle,
user_language: &UserLanguage,
dpi: Dpi,
is_testing: bool,
) -> Result<(), RenderingError> {
cr.status()?;
let node = self.get_node_or_root(id)?;
let root = self.document.root();
let viewport = Rect::from(*viewport);
with_saved_cr(cr, || {
draw_tree(
self.session.clone(),
DrawingMode::LimitToStack { node, root },
cr,
viewport,
user_language,
dpi,
false,
is_testing,
&mut AcquiredNodes::new(&self.document),
)
.map(|_bbox| ())
})
}
fn get_bbox_for_element(
&self,
node: &Node,
user_language: &UserLanguage,
dpi: Dpi,
is_testing: bool,
) -> Result {
let target = cairo::ImageSurface::create(cairo::Format::Rgb24, 1, 1)?;
let cr = cairo::Context::new(&target)?;
let node = node.clone();
draw_tree(
self.session.clone(),
DrawingMode::OnlyNode(node),
&cr,
unit_rectangle(),
user_language,
dpi,
true,
is_testing,
&mut AcquiredNodes::new(&self.document),
)
}
/// Returns (ink_rect, logical_rect)
pub fn get_geometry_for_element(
&self,
id: Option<&str>,
user_language: &UserLanguage,
dpi: Dpi,
is_testing: bool,
) -> Result<(cairo::Rectangle, cairo::Rectangle), RenderingError> {
let node = self.get_node_or_root(id)?;
let bbox = self.get_bbox_for_element(&node, user_language, dpi, is_testing)?;
let ink_rect = bbox.ink_rect.unwrap_or_default();
let logical_rect = bbox.rect.unwrap_or_default();
// Translate so ink_rect is always at offset (0, 0)
let ofs = (-ink_rect.x0, -ink_rect.y0);
Ok((
cairo::Rectangle::from(ink_rect.translate(ofs)),
cairo::Rectangle::from(logical_rect.translate(ofs)),
))
}
pub fn render_element(
&self,
cr: &cairo::Context,
id: Option<&str>,
element_viewport: &cairo::Rectangle,
user_language: &UserLanguage,
dpi: Dpi,
is_testing: bool,
) -> Result<(), RenderingError> {
cr.status()?;
let node = self.get_node_or_root(id)?;
let bbox = self.get_bbox_for_element(&node, user_language, dpi, is_testing)?;
if bbox.ink_rect.is_none() || bbox.rect.is_none() {
// Nothing to draw
return Ok(());
}
let ink_r = bbox.ink_rect.unwrap_or_default();
if ink_r.is_empty() {
return Ok(());
}
// Render, transforming so element is at the new viewport's origin
with_saved_cr(cr, || {
let factor = (element_viewport.width() / ink_r.width())
.min(element_viewport.height() / ink_r.height());
cr.translate(element_viewport.x(), element_viewport.y());
cr.scale(factor, factor);
cr.translate(-ink_r.x0, -ink_r.y0);
draw_tree(
self.session.clone(),
DrawingMode::OnlyNode(node),
cr,
unit_rectangle(),
user_language,
dpi,
false,
is_testing,
&mut AcquiredNodes::new(&self.document),
)
.map(|_bbox| ())
})
}
pub fn get_intrinsic_dimensions(&self) -> IntrinsicDimensions {
let root = self.document.root();
let cascaded = CascadedValues::new_from_node(&root);
let values = cascaded.get();
borrow_element_as!(self.document.root(), Svg).get_intrinsic_dimensions(values)
}
pub fn set_stylesheet(&mut self, css: &str) -> Result<(), LoadingError> {
let stylesheet = Stylesheet::from_data(
css,
&UrlResolver::new(None),
Origin::User,
self.session.clone(),
)?;
self.document.cascade(&[stylesheet], &self.session);
Ok(())
}
}
fn unit_rectangle() -> Rect {
Rect::from_size(1.0, 1.0)
}