use data_url::mime::Mime;
use gdk_pixbuf::{prelude::PixbufLoaderExt, PixbufLoader};
use markup5ever::QualName;
use once_cell::sync::Lazy;
use std::cell::RefCell;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::fmt;
use std::include_str;
use std::rc::Rc;
use std::str::FromStr;
use std::sync::Arc;
use crate::css::{self, Origin, Stylesheet};
use crate::error::{AcquireError, LoadingError, NodeIdError};
use crate::handle::LoadOptions;
use crate::io::{self, BinaryData};
use crate::is_element_of_type;
use crate::limits;
use crate::node::{Node, NodeBorrow, NodeData};
use crate::session::Session;
use crate::surface_utils::shared_surface::SharedImageSurface;
use crate::url_resolver::{AllowedUrl, UrlResolver};
use crate::xml::{xml_load_from_possibly_compressed_stream, Attributes};
static UA_STYLESHEETS: Lazy<Vec<Stylesheet>> = Lazy::new(|| {
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!")]
});
#[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 Document {
tree: Node,
session: Session,
ids: HashMap<String, Node>,
externs: RefCell<Resources>,
images: RefCell<Images>,
load_options: Arc<LoadOptions>,
stylesheets: Vec<Stylesheet>,
}
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 {
use glib::prelude::*;
let bytes = glib::Bytes::from_static(input);
let stream = gio::MemoryInputStream::from_bytes(&bytes);
Document::load_from_stream(
Session::new_for_test_suite(),
Arc::new(LoadOptions::new(UrlResolver::new(None))),
&stream.upcast(),
None::<&gio::Cancellable>,
)
.unwrap()
}
pub fn root(&self) -> Node {
self.tree.clone()
}
pub fn lookup_node(&self, node_id: &NodeId) -> Option<Node> {
match node_id {
NodeId::Internal(id) => self.lookup_internal_node(id),
NodeId::External(url, id) => self
.externs
.borrow_mut()
.lookup(&self.session, &self.load_options, url, id)
.ok(),
}
}
pub fn lookup_internal_node(&self, id: &str) -> Option<Node> {
self.ids.get(id).map(|n| (*n).clone())
}
pub fn lookup_image(&self, url: &str) -> Result<SharedImageSurface, LoadingError> {
let aurl = self
.load_options
.url_resolver
.resolve_href(url)
.map_err(|_| LoadingError::BadUrl)?;
self.images.borrow_mut().lookup(&self.load_options, &aurl)
}
pub fn cascade(&mut self, extra: &[Stylesheet], session: &Session) {
css::cascade(
&mut self.tree,
&UA_STYLESHEETS,
&self.stylesheets,
extra,
session,
);
}
}
struct Resources {
resources: HashMap<AllowedUrl, Result<Rc<Document>, LoadingError>>,
}
impl Resources {
pub fn new() -> Resources {
Resources {
resources: Default::default(),
}
}
pub fn lookup(
&mut self,
session: &Session,
load_options: &LoadOptions,
url: &str,
id: &str,
) -> Result<Node, LoadingError> {
self.get_extern_document(session, load_options, url)
.and_then(|doc| doc.lookup_internal_node(id).ok_or(LoadingError::BadUrl))
}
fn get_extern_document(
&mut self,
session: &Session,
load_options: &LoadOptions,
href: &str,
) -> Result<Rc<Document>, LoadingError> {
let aurl = load_options
.url_resolver
.resolve_href(href)
.map_err(|_| LoadingError::BadUrl)?;
match self.resources.entry(aurl) {
Entry::Occupied(e) => e.get().clone(),
Entry::Vacant(e) => {
let aurl = e.key();
let doc = io::acquire_stream(aurl, None)
.map_err(LoadingError::from)
.and_then(|stream| {
Document::load_from_stream(
session.clone(),
Arc::new(load_options.copy_with_base_url(aurl)),
&stream,
None,
)
})
.map(Rc::new);
let res = e.insert(doc);
res.clone()
}
}
}
}
struct Images {
images: HashMap<AllowedUrl, Result<SharedImageSurface, LoadingError>>,
}
impl Images {
fn new() -> Images {
Images {
images: Default::default(),
}
}
fn lookup(
&mut self,
load_options: &LoadOptions,
aurl: &AllowedUrl,
) -> Result<SharedImageSurface, LoadingError> {
match self.images.entry(aurl.clone()) {
Entry::Occupied(e) => e.get().clone(),
Entry::Vacant(e) => {
let surface = load_image(load_options, e.key());
let res = e.insert(surface);
res.clone()
}
}
}
}
fn load_image(
load_options: &LoadOptions,
aurl: &AllowedUrl,
) -> Result<SharedImageSurface, LoadingError> {
let BinaryData {
data: bytes,
mime_type,
} = io::acquire_data(aurl, None)?;
if bytes.is_empty() {
return Err(LoadingError::Other(String::from("no image data")));
}
let content_type = content_type_for_gdk_pixbuf(&mime_type);
let loader = if let Some(ref content_type) = content_type {
PixbufLoader::with_mime_type(content_type)?
} else {
PixbufLoader::new()
};
loader.write(&bytes)?;
loader.close()?;
let pixbuf = loader.pixbuf().ok_or_else(|| {
LoadingError::Other(format!("loading image: {}", human_readable_url(aurl)))
})?;
let bytes = if load_options.keep_image_data {
Some(bytes)
} else {
None
};
let surface = SharedImageSurface::from_pixbuf(&pixbuf, content_type.as_deref(), bytes)
.map_err(|e| image_loading_error_from_cairo(e, aurl))?;
Ok(surface)
}
fn content_type_for_gdk_pixbuf(mime_type: &Mime) -> Option<String> {
let unspecified_mime_type = Mime::from_str("text/plain;charset=US-ASCII").unwrap();
if *mime_type == unspecified_mime_type {
None
} else {
Some(format!("{}/{}", mime_type.type_, mime_type.subtype))
}
}
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>>,
}
impl<'i> AcquiredNodes<'i> {
pub fn new(document: &Document) -> AcquiredNodes<'_> {
AcquiredNodes {
document,
num_elements_acquired: 0,
node_stack: Rc::new(RefCell::new(NodeStack::new())),
}
}
pub fn lookup_image(&self, href: &str) -> Result<SharedImageSurface, LoadingError> {
self.document.lookup_image(href)
}
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)
.ok_or_else(|| AcquireError::LinkNotFound(node_id.clone()))?;
if node.borrow_element().is_accessed_by_reference() {
self.acquire_ref(&node)
} else {
Ok(AcquiredNode { stack: None, node })
}
}
pub fn acquire_ref(&self, node: &Node) -> Result<AcquiredNode, AcquireError> {
if self.node_stack.borrow().contains(node) {
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.iter().any(|n| *n == *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() {
self.ids
.entry(id.to_string())
.or_insert_with(|| 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 mut document = Document {
tree: root,
session: session.clone(),
ids,
externs: RefCell::new(Resources::new()),
images: RefCell::new(Images::new()),
load_options,
stylesheets,
};
document.cascade(&[], &session);
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 unspecified_mime_type_yields_no_content_type() {
let mime = Mime::from_str("text/plain;charset=US-ASCII").unwrap();
assert!(content_type_for_gdk_pixbuf(&mime).is_none());
}
#[test]
fn strips_mime_type_parameters() {
let mime = Mime::from_str("image/png;charset=utf-8").unwrap();
assert_eq!(
content_type_for_gdk_pixbuf(&mime),
Some(String::from("image/png"))
);
}
}