use data_url::mime::Mime;
use glib::prelude::*;
use markup5ever::QualName;
use std::cell::Cell;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::fmt;
use std::include_str;
use std::io::Cursor;
use std::rc::Rc;
use std::str::FromStr;
use std::sync::Arc;
use std::{cell::RefCell, sync::OnceLock};
use crate::accept_language::UserLanguage;
use crate::bbox::BoundingBox;
use crate::borrow_element_as;
use crate::css::{self, Origin, Stylesheet};
use crate::dpi::Dpi;
use crate::drawing_ctx::{
DrawingMode, RenderingConfiguration, SvgNesting, draw_tree, with_saved_cr,
};
use crate::error::{AcquireError, InternalRenderingError, LoadingError, NodeIdError};
use crate::io::{self, BinaryData};
use crate::is_element_of_type;
use crate::limits;
use crate::node::{CascadedValues, Node, NodeBorrow, NodeData};
use crate::rect::Rect;
use crate::rsvg_log;
use crate::session::Session;
use crate::structure::IntrinsicDimensions;
use crate::surface_utils::shared_surface::SharedImageSurface;
use crate::url_resolver::{AllowedUrl, UrlResolver};
use crate::xml::{Attributes, xml_load_from_possibly_compressed_stream};
#[derive(Debug, PartialEq, Clone)]
pub enum NodeId {
Internal(String),
External(String, String),
}
impl NodeId {
pub fn parse(href: &str) -> Result<NodeId, NodeIdError> {
let (url, id) = match href.rfind('#') {
None => (Some(href), None),
Some(0) => (None, Some(&href[1..])),
Some(p) => (Some(&href[..p]), Some(&href[(p + 1)..])),
};
match (url, id) {
(None, Some(id)) if !id.is_empty() => Ok(NodeId::Internal(String::from(id))),
(Some(url), Some(id)) if !id.is_empty() => {
Ok(NodeId::External(String::from(url), String::from(id)))
}
_ => Err(NodeIdError::NodeIdRequired),
}
}
}
impl fmt::Display for NodeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NodeId::Internal(id) => write!(f, "#{id}"),
NodeId::External(url, id) => write!(f, "{url}#{id}"),
}
}
}
pub struct LoadOptions {
pub url_resolver: UrlResolver,
pub unlimited_size: bool,
pub keep_image_data: bool,
}
impl LoadOptions {
pub fn new(url_resolver: UrlResolver) -> Self {
LoadOptions {
url_resolver,
unlimited_size: false,
keep_image_data: false,
}
}
pub fn with_unlimited_size(mut self, unlimited: bool) -> Self {
self.unlimited_size = unlimited;
self
}
pub fn keep_image_data(mut self, keep: bool) -> Self {
self.keep_image_data = keep;
self
}
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,
}
}
}
pub struct RenderingOptions {
pub dpi: Dpi,
pub cancellable: Option<gio::Cancellable>,
pub user_language: UserLanguage,
pub svg_nesting: SvgNesting,
pub testing: bool,
}
impl RenderingOptions {
fn to_rendering_configuration(&self, measuring: bool) -> RenderingConfiguration {
RenderingConfiguration {
dpi: self.dpi,
cancellable: self.cancellable.clone(),
user_language: self.user_language.clone(),
svg_nesting: self.svg_nesting,
testing: self.testing,
measuring,
}
}
}
pub struct Document {
tree: RefCell<Node>,
session: Session,
ids: HashMap<String, Node>,
resources: RefCell<Resources>,
load_options: Arc<LoadOptions>,
stylesheets: Vec<Stylesheet>,
needs_cascade: Cell<bool>,
}
impl Document {
pub fn load_from_stream(
session: Session,
load_options: Arc<LoadOptions>,
stream: &gio::InputStream,
cancellable: Option<&gio::Cancellable>,
) -> Result<Document, LoadingError> {
xml_load_from_possibly_compressed_stream(
session.clone(),
DocumentBuilder::new(session, load_options.clone()),
load_options,
stream,
cancellable,
)
}
#[cfg(test)]
pub fn load_from_bytes(input: &'static [u8]) -> Document {
let bytes = glib::Bytes::from_static(input);
let stream = gio::MemoryInputStream::from_bytes(&bytes);
let session = Session::new_for_test_suite();
let document = Document::load_from_stream(
session.clone(),
Arc::new(LoadOptions::new(UrlResolver::new(None))),
&stream.upcast(),
None::<&gio::Cancellable>,
)
.unwrap();
document.ensure_is_cascaded();
document
}
pub fn root(&self) -> Node {
self.tree.borrow().clone()
}
fn lookup_node(
&self,
node_id: &NodeId,
cancellable: Option<&gio::Cancellable>,
) -> Option<Node> {
match node_id {
NodeId::Internal(id) => self.lookup_internal_node(id),
NodeId::External(url, id) => self
.resources
.borrow_mut()
.lookup_node(&self.session, &self.load_options, url, id, cancellable)
.ok(),
}
}
pub fn lookup_internal_node(&self, id: &str) -> Option<Node> {
self.ids.get(id).map(|n| (*n).clone())
}
fn lookup_resource(
&self,
url: &str,
cancellable: Option<&gio::Cancellable>,
) -> Result<Resource, LoadingError> {
let aurl = self
.load_options
.url_resolver
.resolve_href(url)
.map_err(|_| LoadingError::BadUrl)?;
self.resources.borrow_mut().lookup_resource(
&self.session,
&self.load_options,
&aurl,
cancellable,
)
}
pub fn cascade(&self, extra: &[Stylesheet]) {
self.needs_cascade.set(false);
let stylesheets = {
static UA_STYLESHEETS: OnceLock<Vec<Stylesheet>> = OnceLock::new();
UA_STYLESHEETS.get_or_init(|| {
vec![
Stylesheet::from_data(
include_str!("ua.css"),
&UrlResolver::new(None),
Origin::UserAgent,
Session::default(),
)
.expect("could not parse user agent stylesheet for librsvg, there's a bug!"),
]
})
};
css::cascade(
&mut self.tree.borrow_mut(),
stylesheets,
&self.stylesheets,
extra,
&self.session,
);
}
pub fn get_intrinsic_dimensions(&self) -> IntrinsicDimensions {
self.ensure_is_cascaded();
let root = self.root();
let cascaded = CascadedValues::new_from_node(&root);
let values = cascaded.get();
borrow_element_as!(self.root(), Svg).get_intrinsic_dimensions(values)
}
pub fn render_document(
&self,
cr: &cairo::Context,
viewport: &cairo::Rectangle,
options: &RenderingOptions,
) -> Result<(), InternalRenderingError> {
let root = self.root();
self.render_layer(cr, root, viewport, options)
}
pub fn render_layer(
&self,
cr: &cairo::Context,
node: Node,
viewport: &cairo::Rectangle,
options: &RenderingOptions,
) -> Result<(), InternalRenderingError> {
cr.status()?;
let root = self.root();
let viewport = Rect::from(*viewport);
let config = options.to_rendering_configuration(false);
with_saved_cr(cr, || {
self.draw_tree(
DrawingMode::LimitToStack { node, root },
cr,
viewport,
config,
)
})
.map(|_bbox| ())
.map_err(|err| *err)
}
fn geometry_for_layer(
&self,
node: Node,
viewport: Rect,
options: &RenderingOptions,
) -> Result<(Rect, Rect), Box<InternalRenderingError>> {
let root = self.root();
let target = cairo::ImageSurface::create(cairo::Format::Rgb24, 1, 1)?;
let cr = cairo::Context::new(&target)?;
let config = options.to_rendering_configuration(true);
let bbox = self.draw_tree(
DrawingMode::LimitToStack { node, root },
&cr,
viewport,
config,
)?;
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,
node: Node,
viewport: &cairo::Rectangle,
options: &RenderingOptions,
) -> Result<(cairo::Rectangle, cairo::Rectangle), InternalRenderingError> {
let viewport = Rect::from(*viewport);
let (ink_rect, logical_rect) = self
.geometry_for_layer(node, viewport, options)
.map_err(|err| *err)?;
Ok((
cairo::Rectangle::from(ink_rect),
cairo::Rectangle::from(logical_rect),
))
}
fn get_bbox_for_element(
&self,
node: &Node,
options: &RenderingOptions,
) -> Result<BoundingBox, InternalRenderingError> {
let target = cairo::ImageSurface::create(cairo::Format::Rgb24, 1, 1)?;
let cr = cairo::Context::new(&target)?;
let node = node.clone();
let config = options.to_rendering_configuration(true);
self.draw_tree(DrawingMode::OnlyNode(node), &cr, unit_rectangle(), config)
.map_err(|err| *err)
}
pub fn get_geometry_for_element(
&self,
node: Node,
options: &RenderingOptions,
) -> Result<(cairo::Rectangle, cairo::Rectangle), InternalRenderingError> {
let bbox = self.get_bbox_for_element(&node, options)?;
let ink_rect = bbox.ink_rect.unwrap_or_default();
let logical_rect = bbox.rect.unwrap_or_default();
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,
node: Node,
element_viewport: &cairo::Rectangle,
options: &RenderingOptions,
) -> Result<(), InternalRenderingError> {
cr.status()?;
let bbox = self.get_bbox_for_element(&node, options)?;
if bbox.ink_rect.is_none() || bbox.rect.is_none() {
return Ok(());
}
let ink_r = bbox.ink_rect.unwrap_or_default();
if ink_r.is_empty() {
return Ok(());
}
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);
let config = options.to_rendering_configuration(false);
self.draw_tree(DrawingMode::OnlyNode(node), cr, unit_rectangle(), config)
})
.map(|_bbox| ())
.map_err(|err| *err)
}
fn draw_tree(
&self,
drawing_mode: DrawingMode,
cr: &cairo::Context,
viewport_rect: Rect,
config: RenderingConfiguration,
) -> Result<BoundingBox, Box<InternalRenderingError>> {
self.ensure_is_cascaded();
let cancellable = config.cancellable.clone();
draw_tree(
self.session.clone(),
drawing_mode,
cr,
viewport_rect,
config,
&mut AcquiredNodes::new(self, cancellable),
)
.map(|boxed_bbox| *boxed_bbox)
}
fn ensure_is_cascaded(&self) {
if self.needs_cascade.get() {
self.cascade(&[]);
}
}
}
fn unit_rectangle() -> Rect {
Rect::from_size(1.0, 1.0)
}
#[derive(Clone)]
pub enum Resource {
Document(Rc<Document>),
Image(SharedImageSurface),
}
struct Resources {
resources: HashMap<AllowedUrl, Result<Resource, LoadingError>>,
}
impl Resources {
fn new() -> Resources {
Resources {
resources: Default::default(),
}
}
fn lookup_node(
&mut self,
session: &Session,
load_options: &LoadOptions,
url: &str,
id: &str,
cancellable: Option<&gio::Cancellable>,
) -> Result<Node, LoadingError> {
self.get_extern_document(session, load_options, url, cancellable)
.and_then(|resource| match resource {
Resource::Document(doc) => doc.lookup_internal_node(id).ok_or(LoadingError::BadUrl),
_ => unreachable!("get_extern_document() should already have ensured the document"),
})
}
fn get_extern_document(
&mut self,
session: &Session,
load_options: &LoadOptions,
href: &str,
cancellable: Option<&gio::Cancellable>,
) -> Result<Resource, LoadingError> {
let aurl = load_options
.url_resolver
.resolve_href(href)
.map_err(|_| LoadingError::BadUrl)?;
let resource = self.lookup_resource(session, load_options, &aurl, cancellable)?;
match resource {
Resource::Document(_) => Ok(resource),
_ => Err(LoadingError::Other(format!(
"{href} is not an SVG document"
))),
}
}
fn lookup_resource(
&mut self,
session: &Session,
load_options: &LoadOptions,
aurl: &AllowedUrl,
cancellable: Option<&gio::Cancellable>,
) -> Result<Resource, LoadingError> {
match self.resources.entry(aurl.clone()) {
Entry::Occupied(e) => e.get().clone(),
Entry::Vacant(e) => {
let resource_result = load_resource(session, load_options, aurl, cancellable);
e.insert(resource_result.clone());
resource_result
}
}
}
}
#[derive(Clone, Copy)]
enum ResourceType {
Unknown,
Svg,
Png,
Jpeg,
Gif,
WebP,
Avif,
}
fn is_mime_type(mime: &Mime, type_: &str, subtype: &str) -> bool {
mime.type_ == type_ && mime.subtype == subtype
}
impl ResourceType {
fn from(mime_type: &Option<Mime>) -> ResourceType {
match mime_type {
None => ResourceType::Unknown,
Some(x) if *x == Mime::from_str("text/plain;charset=US-ASCII").unwrap() => {
ResourceType::Unknown
}
Some(x) if is_mime_type(x, "image", "svg+xml") => ResourceType::Svg,
Some(x) if is_mime_type(x, "image", "png") => ResourceType::Png,
Some(x) if is_mime_type(x, "image", "jpeg") => ResourceType::Jpeg,
Some(x) if is_mime_type(x, "image", "gif") => ResourceType::Gif,
Some(x) if is_mime_type(x, "image", "webp") => ResourceType::WebP,
Some(x) if is_mime_type(x, "image", "avif") => ResourceType::Avif,
_ => ResourceType::Unknown,
}
}
fn is_known(&self) -> bool {
!matches!(*self, ResourceType::Unknown)
}
fn to_image_format(self) -> image::ImageFormat {
use ResourceType::*;
match self {
Svg => unreachable!(),
Png => image::ImageFormat::Png,
Jpeg => image::ImageFormat::Jpeg,
Gif => image::ImageFormat::Gif,
WebP => image::ImageFormat::WebP,
Avif => image::ImageFormat::Avif,
_ => unreachable!(),
}
}
}
fn load_resource(
session: &Session,
load_options: &LoadOptions,
aurl: &AllowedUrl,
cancellable: Option<&gio::Cancellable>,
) -> Result<Resource, LoadingError> {
let data = io::acquire_data(aurl, cancellable)?;
let BinaryData {
data: bytes,
mime_type,
} = data;
let resource_type = ResourceType::from(&mime_type);
if resource_type.is_known() {
use ResourceType::*;
match resource_type {
Png | Jpeg | Gif | WebP | Avif => {
let format = resource_type.to_image_format();
load_image_resource_from_bytes(load_options, aurl, bytes, format)
}
Svg => load_svg_resource_from_bytes(session, load_options, aurl, bytes, cancellable),
_ => unreachable!(),
}
} else {
if let Ok(format) = image::guess_format(&bytes) {
load_image_resource_from_bytes(load_options, aurl, bytes, format)
} else {
load_svg_resource_from_bytes(session, load_options, aurl, bytes, cancellable)
}
}
}
fn load_svg_resource_from_bytes(
session: &Session,
load_options: &LoadOptions,
aurl: &AllowedUrl,
input_bytes: Vec<u8>,
cancellable: Option<&gio::Cancellable>,
) -> Result<Resource, LoadingError> {
let bytes = glib::Bytes::from_owned(input_bytes);
let stream = gio::MemoryInputStream::from_bytes(&bytes);
let document = Document::load_from_stream(
session.clone(),
Arc::new(load_options.copy_with_base_url(aurl)),
&stream.upcast(),
cancellable,
)?;
document.ensure_is_cascaded();
Ok(Resource::Document(Rc::new(document)))
}
fn load_image_resource_from_bytes(
load_options: &LoadOptions,
aurl: &AllowedUrl,
bytes: Vec<u8>,
format: image::ImageFormat,
) -> Result<Resource, LoadingError> {
if bytes.is_empty() {
return Err(LoadingError::Other(String::from("no image data")));
}
load_image_with_image_rs(aurl, bytes, format, load_options)
}
fn is_supported_image_format(format: image::ImageFormat) -> bool {
use image::ImageFormat::*;
match format {
Png => true,
Jpeg => true,
Gif => true,
WebP => true,
#[cfg(feature = "avif")]
Avif => true,
_ => false,
}
}
fn image_format_to_content_type(format: image::ImageFormat) -> String {
use image::ImageFormat::*;
let mime_type = match format {
Png => "image/png",
Jpeg => "image/jpeg",
Gif => "image/gif",
WebP => "image/webp",
Avif => "image/avif",
_ => unreachable!("we should have already filtered supported image types"),
};
mime_type.to_string()
}
fn load_image_with_image_rs(
aurl: &AllowedUrl,
bytes: Vec<u8>,
format: image::ImageFormat,
load_options: &LoadOptions,
) -> Result<Resource, LoadingError> {
if is_supported_image_format(format) {
let cursor = Cursor::new(&bytes);
let reader = image::ImageReader::with_format(cursor, format);
let image = reader
.decode()
.map_err(|e| LoadingError::Other(format!("error decoding image: {e}")))?;
let bytes = if load_options.keep_image_data {
Some(bytes)
} else {
None
};
let content_type = image_format_to_content_type(format);
let surface = SharedImageSurface::from_image(&image, Some(&content_type), bytes)
.map_err(|e| image_loading_error_from_cairo(e, aurl))?;
Ok(Resource::Image(surface))
} else {
Err(LoadingError::Other(String::from(
"unsupported image format {format:?}",
)))
}
}
fn human_readable_url(aurl: &AllowedUrl) -> &str {
if aurl.scheme() == "data" {
"data URL"
} else {
aurl.as_ref()
}
}
fn image_loading_error_from_cairo(status: cairo::Error, aurl: &AllowedUrl) -> LoadingError {
let url = human_readable_url(aurl);
match status {
cairo::Error::NoMemory => LoadingError::OutOfMemory(format!("loading image: {url}")),
cairo::Error::InvalidSize => LoadingError::Other(format!("image too big: {url}")),
_ => LoadingError::Other(format!("cairo error: {status}")),
}
}
pub struct AcquiredNode {
stack: Option<Rc<RefCell<NodeStack>>>,
node: Node,
}
impl Drop for AcquiredNode {
fn drop(&mut self) {
if let Some(ref stack) = self.stack {
let mut stack = stack.borrow_mut();
let last = stack.pop().unwrap();
assert!(last == self.node);
}
}
}
impl AcquiredNode {
pub fn get(&self) -> &Node {
&self.node
}
}
pub struct AcquiredNodes<'i> {
document: &'i Document,
num_elements_acquired: usize,
node_stack: Rc<RefCell<NodeStack>>,
nodes_with_cycles: Vec<Node>,
cancellable: Option<gio::Cancellable>,
}
impl<'i> AcquiredNodes<'i> {
pub fn new(document: &Document, cancellable: Option<gio::Cancellable>) -> AcquiredNodes<'_> {
AcquiredNodes {
document,
num_elements_acquired: 0,
node_stack: Rc::new(RefCell::new(NodeStack::new())),
nodes_with_cycles: Vec::new(),
cancellable,
}
}
pub fn lookup_resource(&self, url: &str) -> Result<Resource, LoadingError> {
self.document
.lookup_resource(url, self.cancellable.as_ref())
}
pub fn acquire(&mut self, node_id: &NodeId) -> Result<AcquiredNode, AcquireError> {
self.num_elements_acquired += 1;
if self.num_elements_acquired > limits::MAX_REFERENCED_ELEMENTS {
return Err(AcquireError::MaxReferencesExceeded);
}
let node = self
.document
.lookup_node(node_id, self.cancellable.as_ref())
.ok_or_else(|| AcquireError::LinkNotFound(node_id.clone()))?;
if self.nodes_with_cycles.contains(&node) {
return Err(AcquireError::CircularReference(node.clone()));
}
if node.borrow_element().is_accessed_by_reference() {
self.acquire_ref(&node)
} else {
Ok(AcquiredNode { stack: None, node })
}
}
pub fn acquire_ref(&mut self, node: &Node) -> Result<AcquiredNode, AcquireError> {
if self.nodes_with_cycles.contains(node) {
Err(AcquireError::CircularReference(node.clone()))
} else if self.node_stack.borrow().contains(node) {
self.nodes_with_cycles.push(node.clone());
Err(AcquireError::CircularReference(node.clone()))
} else {
self.node_stack.borrow_mut().push(node);
Ok(AcquiredNode {
stack: Some(self.node_stack.clone()),
node: node.clone(),
})
}
}
}
pub struct NodeStack(Vec<Node>);
impl NodeStack {
pub fn new() -> NodeStack {
NodeStack(Vec::new())
}
pub fn push(&mut self, node: &Node) {
self.0.push(node.clone());
}
pub fn pop(&mut self) -> Option<Node> {
self.0.pop()
}
pub fn contains(&self, node: &Node) -> bool {
self.0.contains(node)
}
}
pub struct DocumentBuilder {
session: Session,
load_options: Arc<LoadOptions>,
tree: Option<Node>,
ids: HashMap<String, Node>,
stylesheets: Vec<Stylesheet>,
}
impl DocumentBuilder {
pub fn new(session: Session, load_options: Arc<LoadOptions>) -> DocumentBuilder {
DocumentBuilder {
session,
load_options,
tree: None,
ids: HashMap::new(),
stylesheets: Vec::new(),
}
}
pub fn append_stylesheet(&mut self, stylesheet: Stylesheet) {
self.stylesheets.push(stylesheet);
}
pub fn append_element(
&mut self,
name: &QualName,
attrs: Attributes,
parent: Option<Node>,
) -> Node {
let node = Node::new(NodeData::new_element(&self.session, name, attrs));
if let Some(id) = node.borrow_element().get_id() {
match self.ids.entry(id.to_string()) {
Entry::Occupied(_) => {
rsvg_log!(self.session, "ignoring duplicate id {id} for {node}");
}
Entry::Vacant(e) => {
e.insert(node.clone());
}
}
}
if let Some(parent) = parent {
parent.append(node.clone());
} else if self.tree.is_none() {
self.tree = Some(node.clone());
} else {
panic!("The tree root has already been set");
}
node
}
pub fn append_characters(&mut self, text: &str, parent: &mut Node) {
if !text.is_empty() {
if let Some(child) = parent.last_child().filter(|c| c.is_chars()) {
child.borrow_chars().append(text);
} else {
parent.append(Node::new(NodeData::new_chars(text)));
};
}
}
pub fn build(self) -> Result<Document, LoadingError> {
let DocumentBuilder {
load_options,
session,
tree,
ids,
stylesheets,
..
} = self;
match tree {
Some(root) if root.is_element() => {
if is_element_of_type!(root, Svg) {
let document = Document {
tree: RefCell::new(root),
session: session.clone(),
ids,
resources: RefCell::new(Resources::new()),
load_options,
stylesheets,
needs_cascade: Cell::new(true),
};
Ok(document)
} else {
Err(LoadingError::NoSvgRoot)
}
}
_ => Err(LoadingError::NoSvgRoot),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_node_id() {
assert_eq!(
NodeId::parse("#foo").unwrap(),
NodeId::Internal("foo".to_string())
);
assert_eq!(
NodeId::parse("uri#foo").unwrap(),
NodeId::External("uri".to_string(), "foo".to_string())
);
assert!(matches!(
NodeId::parse("uri"),
Err(NodeIdError::NodeIdRequired)
));
}
#[test]
fn ignores_stylesheet_with_invalid_utf8() {
let handle = crate::api::Loader::new()
.read_path("tests/fixtures/loading/non-utf8-stylesheet.svg")
.unwrap();
assert!(handle.document.stylesheets.is_empty());
}
}