diff options
Diffstat (limited to 'rsvg/src')
84 files changed, 36981 insertions, 0 deletions
diff --git a/rsvg/src/accept_language.rs b/rsvg/src/accept_language.rs new file mode 100644 index 00000000..2abaae7f --- /dev/null +++ b/rsvg/src/accept_language.rs @@ -0,0 +1,458 @@ +//! Parser for an Accept-Language HTTP header. + +use language_tags::{LanguageTag, ParseError}; +use locale_config::{LanguageRange, Locale}; + +use std::error; +use std::fmt; +use std::str::FromStr; + +/// Used to set the language for rendering. +/// +/// SVG documents can use the `<switch>` element, whose children have a `systemLanguage` +/// attribute; only the first child which has a `systemLanguage` that matches the +/// preferred languages will be rendered. +/// +/// This enum, used with `CairoRenderer::with_language`, configures how to obtain the +/// user's prefererred languages. +pub enum Language { + /// Use the Unix environment variables `LANGUAGE`, `LC_ALL`, `LC_MESSAGES` and `LANG` to obtain the + /// user's language. This uses [`g_get_language_names()`][ggln] underneath. + /// + /// [ggln]: https://docs.gtk.org/glib/func.get_language_names.html + FromEnvironment, + AcceptLanguage(AcceptLanguage), +} + +/// `Language` but with the environment's locale converted to something we can use. +#[derive(Clone)] +pub enum UserLanguage { + LanguageTags(LanguageTags), + AcceptLanguage(AcceptLanguage), +} + +#[derive(Clone, Debug, PartialEq)] +struct Weight(Option<f32>); + +impl Weight { + fn numeric(&self) -> f32 { + self.0.unwrap_or(1.0) + } +} + +#[derive(Clone, Debug, PartialEq)] +struct Item { + tag: LanguageTag, + weight: Weight, +} + +/// Stores a parsed version of an HTTP Accept-Language header. +/// +/// RFC 7231: <https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5> +#[derive(Clone, Debug, PartialEq)] +pub struct AcceptLanguage(Box<[Item]>); + +/// Errors when parsing an `AcceptLanguage`. +#[derive(Debug, PartialEq)] +pub enum AcceptLanguageError { + NoElements, + InvalidCharacters, + InvalidLanguageTag(ParseError), + InvalidWeight, +} + +impl error::Error for AcceptLanguageError {} + +impl fmt::Display for AcceptLanguageError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoElements => write!(f, "no language tags in list"), + Self::InvalidCharacters => write!(f, "invalid characters in language list"), + Self::InvalidLanguageTag(e) => write!(f, "invalid language tag: {e}"), + Self::InvalidWeight => write!(f, "invalid q= weight"), + } + } +} + +/// Optional whitespace, Space or Tab, per RFC 7230. +/// +/// RFC 7230: <https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.3> +const OWS: [char; 2] = ['\x20', '\x09']; + +impl AcceptLanguage { + pub fn parse(s: &str) -> Result<AcceptLanguage, AcceptLanguageError> { + if !s.is_ascii() { + return Err(AcceptLanguageError::InvalidCharacters); + } + + let mut items = Vec::new(); + + for val in s.split(',') { + let trimmed = val.trim_matches(&OWS[..]); + if trimmed.is_empty() { + continue; + } + + items.push(Item::parse(trimmed)?); + } + + if items.is_empty() { + Err(AcceptLanguageError::NoElements) + } else { + Ok(AcceptLanguage(items.into_boxed_slice())) + } + } + + pub fn iter(&self) -> impl Iterator<Item = (&LanguageTag, f32)> { + self.0.iter().map(|item| (&item.tag, item.weight.numeric())) + } + + fn any_matches(&self, tag: &LanguageTag) -> bool { + self.iter().any(|(self_tag, _weight)| tag.matches(self_tag)) + } +} + +impl Item { + fn parse(s: &str) -> Result<Item, AcceptLanguageError> { + let semicolon_pos = s.find(';'); + + let (before_semicolon, after_semicolon) = if let Some(semi) = semicolon_pos { + (&s[..semi], Some(&s[semi + 1..])) + } else { + (s, None) + }; + + let tag = LanguageTag::parse(before_semicolon) + .map_err(AcceptLanguageError::InvalidLanguageTag)?; + + let weight = if let Some(quality) = after_semicolon { + let quality = quality.trim_start_matches(&OWS[..]); + + let number = if let Some(qvalue) = quality.strip_prefix("q=") { + if qvalue.starts_with(&['0', '1'][..]) { + let first_digit = qvalue.chars().next().unwrap(); + + if let Some(decimals) = qvalue[1..].strip_prefix('.') { + if (first_digit == '0' + && decimals.len() <= 3 + && decimals.chars().all(|c| c.is_ascii_digit())) + || (first_digit == '1' + && decimals.len() <= 3 + && decimals.chars().all(|c| c == '0')) + { + qvalue + } else { + return Err(AcceptLanguageError::InvalidWeight); + } + } else { + qvalue + } + } else { + return Err(AcceptLanguageError::InvalidWeight); + } + } else { + return Err(AcceptLanguageError::InvalidWeight); + }; + + Weight(Some( + f32::from_str(number).map_err(|_| AcceptLanguageError::InvalidWeight)?, + )) + } else { + Weight(None) + }; + + Ok(Item { tag, weight }) + } +} + +/// A list of BCP47 language tags. +/// +/// RFC 5664: <https://www.rfc-editor.org/info/rfc5664> +#[derive(Debug, Clone, PartialEq)] +pub struct LanguageTags(Vec<LanguageTag>); + +impl LanguageTags { + pub fn empty() -> Self { + LanguageTags(Vec::new()) + } + + /// Converts a `Locale` to a set of language tags. + pub fn from_locale(locale: &Locale) -> Result<LanguageTags, String> { + let mut tags = Vec::new(); + + for locale_range in locale.tags_for("messages") { + if locale_range == LanguageRange::invariant() { + continue; + } + + let str_locale_range = locale_range.as_ref(); + + let locale_tag = LanguageTag::from_str(str_locale_range).map_err(|e| { + format!("invalid language tag \"{str_locale_range}\" in locale: {e}") + })?; + + if !locale_tag.is_language_range() { + return Err(format!( + "language tag \"{locale_tag}\" is not a language range" + )); + } + + tags.push(locale_tag); + } + + Ok(LanguageTags(tags)) + } + + pub fn from(tags: Vec<LanguageTag>) -> LanguageTags { + LanguageTags(tags) + } + + pub fn iter(&self) -> impl Iterator<Item = &LanguageTag> { + self.0.iter() + } + + pub fn any_matches(&self, language_tag: &LanguageTag) -> bool { + self.0.iter().any(|tag| tag.matches(language_tag)) + } +} + +impl UserLanguage { + pub fn any_matches(&self, tags: &LanguageTags) -> bool { + match *self { + UserLanguage::LanguageTags(ref language_tags) => { + tags.iter().any(|tag| language_tags.any_matches(tag)) + } + UserLanguage::AcceptLanguage(ref accept_language) => { + tags.iter().any(|tag| accept_language.any_matches(tag)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_accept_language() { + // plain tag + assert_eq!( + AcceptLanguage::parse("es-MX").unwrap(), + AcceptLanguage( + vec![Item { + tag: LanguageTag::parse("es-MX").unwrap(), + weight: Weight(None) + }] + .into_boxed_slice() + ) + ); + + // with quality + assert_eq!( + AcceptLanguage::parse("es-MX;q=1").unwrap(), + AcceptLanguage( + vec![Item { + tag: LanguageTag::parse("es-MX").unwrap(), + weight: Weight(Some(1.0)) + }] + .into_boxed_slice() + ) + ); + + // with quality + assert_eq!( + AcceptLanguage::parse("es-MX;q=0").unwrap(), + AcceptLanguage( + vec![Item { + tag: LanguageTag::parse("es-MX").unwrap(), + weight: Weight(Some(0.0)) + }] + .into_boxed_slice() + ) + ); + + // zero decimals are allowed + assert_eq!( + AcceptLanguage::parse("es-MX;q=0.").unwrap(), + AcceptLanguage( + vec![Item { + tag: LanguageTag::parse("es-MX").unwrap(), + weight: Weight(Some(0.0)) + }] + .into_boxed_slice() + ) + ); + + // zero decimals are allowed + assert_eq!( + AcceptLanguage::parse("es-MX;q=1.").unwrap(), + AcceptLanguage( + vec![Item { + tag: LanguageTag::parse("es-MX").unwrap(), + weight: Weight(Some(1.0)) + }] + .into_boxed_slice() + ) + ); + + // one decimal + assert_eq!( + AcceptLanguage::parse("es-MX;q=1.0").unwrap(), + AcceptLanguage( + vec![Item { + tag: LanguageTag::parse("es-MX").unwrap(), + weight: Weight(Some(1.0)) + }] + .into_boxed_slice() + ) + ); + + // two decimals + assert_eq!( + AcceptLanguage::parse("es-MX;q=1.00").unwrap(), + AcceptLanguage( + vec![Item { + tag: LanguageTag::parse("es-MX").unwrap(), + weight: Weight(Some(1.0)) + }] + .into_boxed_slice() + ) + ); + + // three decimals + assert_eq!( + AcceptLanguage::parse("es-MX;q=1.000").unwrap(), + AcceptLanguage( + vec![Item { + tag: LanguageTag::parse("es-MX").unwrap(), + weight: Weight(Some(1.0)) + }] + .into_boxed_slice() + ) + ); + + // multiple elements + assert_eq!( + AcceptLanguage::parse("es-MX, en; q=0.5").unwrap(), + AcceptLanguage( + vec![ + Item { + tag: LanguageTag::parse("es-MX").unwrap(), + weight: Weight(None) + }, + Item { + tag: LanguageTag::parse("en").unwrap(), + weight: Weight(Some(0.5)) + }, + ] + .into_boxed_slice() + ) + ); + + // superfluous whitespace + assert_eq!( + AcceptLanguage::parse(",es-MX;q=1.000 , en; q=0.125 , ,").unwrap(), + AcceptLanguage( + vec![ + Item { + tag: LanguageTag::parse("es-MX").unwrap(), + weight: Weight(Some(1.0)) + }, + Item { + tag: LanguageTag::parse("en").unwrap(), + weight: Weight(Some(0.125)) + }, + ] + .into_boxed_slice() + ) + ); + } + + #[test] + fn empty_lists() { + assert!(matches!( + AcceptLanguage::parse(""), + Err(AcceptLanguageError::NoElements) + )); + + assert!(matches!( + AcceptLanguage::parse(","), + Err(AcceptLanguageError::NoElements) + )); + + assert!(matches!( + AcceptLanguage::parse(", , ,,,"), + Err(AcceptLanguageError::NoElements) + )); + } + + #[test] + fn ascii_only() { + assert!(matches!( + AcceptLanguage::parse("ës"), + Err(AcceptLanguageError::InvalidCharacters) + )); + } + + #[test] + fn invalid_tag() { + assert!(matches!( + AcceptLanguage::parse("no_underscores"), + Err(AcceptLanguageError::InvalidLanguageTag(_)) + )); + } + + #[test] + fn invalid_weight() { + assert!(matches!( + AcceptLanguage::parse("es;"), + Err(AcceptLanguageError::InvalidWeight) + )); + assert!(matches!( + AcceptLanguage::parse("es;q"), + Err(AcceptLanguageError::InvalidWeight) + )); + assert!(matches!( + AcceptLanguage::parse("es;q="), + Err(AcceptLanguageError::InvalidWeight) + )); + assert!(matches!( + AcceptLanguage::parse("es;q=2"), + Err(AcceptLanguageError::InvalidWeight) + )); + assert!(matches!( + AcceptLanguage::parse("es;q=1.1"), + Err(AcceptLanguageError::InvalidWeight) + )); + assert!(matches!( + AcceptLanguage::parse("es;q=1.12"), + Err(AcceptLanguageError::InvalidWeight) + )); + assert!(matches!( + AcceptLanguage::parse("es;q=1.123"), + Err(AcceptLanguageError::InvalidWeight) + )); + + // Up to three decimals allowed per RFC 7231 + assert!(matches!( + AcceptLanguage::parse("es;q=0.1234"), + Err(AcceptLanguageError::InvalidWeight) + )); + } + + #[test] + fn iter() { + let accept_language = AcceptLanguage::parse("es-MX, en; q=0.5").unwrap(); + let mut iter = accept_language.iter(); + + let (tag, weight) = iter.next().unwrap(); + assert_eq!(*tag, LanguageTag::parse("es-MX").unwrap()); + assert_eq!(weight, 1.0); + + let (tag, weight) = iter.next().unwrap(); + assert_eq!(*tag, LanguageTag::parse("en").unwrap()); + assert_eq!(weight, 0.5); + + assert!(iter.next().is_none()); + } +} diff --git a/rsvg/src/angle.rs b/rsvg/src/angle.rs new file mode 100644 index 00000000..aa5a1bef --- /dev/null +++ b/rsvg/src/angle.rs @@ -0,0 +1,187 @@ +//! CSS angle values. + +use std::f64::consts::*; + +use cssparser::{Parser, Token}; +use float_cmp::approx_eq; + +use crate::error::*; +use crate::parsers::{finite_f32, Parse}; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Angle(f64); + +impl Angle { + pub fn new(rad: f64) -> Angle { + Angle(Angle::normalize(rad)) + } + + pub fn from_degrees(deg: f64) -> Angle { + Angle(Angle::normalize(deg.to_radians())) + } + + pub fn from_vector(vx: f64, vy: f64) -> Angle { + let rad = vy.atan2(vx); + + if rad.is_nan() { + Angle(0.0) + } else { + Angle(Angle::normalize(rad)) + } + } + + pub fn radians(self) -> f64 { + self.0 + } + + pub fn bisect(self, other: Angle) -> Angle { + let half_delta = (other.0 - self.0) * 0.5; + + if FRAC_PI_2 < half_delta.abs() { + Angle(Angle::normalize(self.0 + half_delta - PI)) + } else { + Angle(Angle::normalize(self.0 + half_delta)) + } + } + + //Flips an angle to be 180deg or PI radians rotated + pub fn flip(self) -> Angle { + Angle::new(self.radians() + PI) + } + + // Normalizes an angle to [0.0, 2*PI) + fn normalize(rad: f64) -> f64 { + let res = rad % (PI * 2.0); + if approx_eq!(f64, res, 0.0) { + 0.0 + } else if res < 0.0 { + res + PI * 2.0 + } else { + res + } + } +} + +// angle: +// https://www.w3.org/TR/SVG/types.html#DataTypeAngle +// +// angle ::= number ("deg" | "grad" | "rad")? +// +impl Parse for Angle { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Angle, ParseError<'i>> { + let angle = { + let loc = parser.current_source_location(); + + let token = parser.next()?; + + match *token { + Token::Number { value, .. } => { + let degrees = finite_f32(value).map_err(|e| loc.new_custom_error(e))?; + Angle::from_degrees(f64::from(degrees)) + } + + Token::Dimension { + value, ref unit, .. + } => { + let value = f64::from(finite_f32(value).map_err(|e| loc.new_custom_error(e))?); + + match unit.as_ref() { + "deg" => Angle::from_degrees(value), + "grad" => Angle::from_degrees(value * 360.0 / 400.0), + "rad" => Angle::new(value), + "turn" => Angle::from_degrees(value * 360.0), + _ => { + return Err(loc.new_unexpected_token_error(token.clone())); + } + } + } + + _ => return Err(loc.new_unexpected_token_error(token.clone())), + } + }; + + Ok(angle) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_angle() { + assert_eq!(Angle::parse_str("0").unwrap(), Angle::new(0.0)); + assert_eq!(Angle::parse_str("15").unwrap(), Angle::from_degrees(15.0)); + assert_eq!( + Angle::parse_str("180.5deg").unwrap(), + Angle::from_degrees(180.5) + ); + assert_eq!(Angle::parse_str("1rad").unwrap(), Angle::new(1.0)); + assert_eq!( + Angle::parse_str("-400grad").unwrap(), + Angle::from_degrees(-360.0) + ); + assert_eq!( + Angle::parse_str("0.25turn").unwrap(), + Angle::from_degrees(90.0) + ); + + assert!(Angle::parse_str("").is_err()); + assert!(Angle::parse_str("foo").is_err()); + assert!(Angle::parse_str("300foo").is_err()); + } + + fn test_bisection_angle( + expected: f64, + incoming_vx: f64, + incoming_vy: f64, + outgoing_vx: f64, + outgoing_vy: f64, + ) { + let i = Angle::from_vector(incoming_vx, incoming_vy); + let o = Angle::from_vector(outgoing_vx, outgoing_vy); + let bisected = i.bisect(o); + assert!(approx_eq!(f64, expected, bisected.radians())); + } + + #[test] + fn bisection_angle_is_correct_from_incoming_counterclockwise_to_outgoing() { + // 1st quadrant + test_bisection_angle(FRAC_PI_4, 1.0, 0.0, 0.0, 1.0); + + // 2nd quadrant + test_bisection_angle(FRAC_PI_2 + FRAC_PI_4, 0.0, 1.0, -1.0, 0.0); + + // 3rd quadrant + test_bisection_angle(PI + FRAC_PI_4, -1.0, 0.0, 0.0, -1.0); + + // 4th quadrant + test_bisection_angle(PI + FRAC_PI_2 + FRAC_PI_4, 0.0, -1.0, 1.0, 0.0); + } + + #[test] + fn bisection_angle_is_correct_from_incoming_clockwise_to_outgoing() { + // 1st quadrant + test_bisection_angle(FRAC_PI_4, 0.0, 1.0, 1.0, 0.0); + + // 2nd quadrant + test_bisection_angle(FRAC_PI_2 + FRAC_PI_4, -1.0, 0.0, 0.0, 1.0); + + // 3rd quadrant + test_bisection_angle(PI + FRAC_PI_4, 0.0, -1.0, -1.0, 0.0); + + // 4th quadrant + test_bisection_angle(PI + FRAC_PI_2 + FRAC_PI_4, 1.0, 0.0, 0.0, -1.0); + } + + #[test] + fn bisection_angle_is_correct_for_more_than_quarter_turn_angle() { + test_bisection_angle(0.0, 0.1, -1.0, 0.1, 1.0); + + test_bisection_angle(FRAC_PI_2, 1.0, 0.1, -1.0, 0.1); + + test_bisection_angle(PI, -0.1, 1.0, -0.1, -1.0); + + test_bisection_angle(PI + FRAC_PI_2, -1.0, -0.1, 1.0, -0.1); + } +} diff --git a/rsvg/src/api.rs b/rsvg/src/api.rs new file mode 100644 index 00000000..34945afa --- /dev/null +++ b/rsvg/src/api.rs @@ -0,0 +1,638 @@ +//! Public Rust API for librsvg. +//! +//! This gets re-exported from the toplevel `lib.rs`. + +#![warn(missing_docs)] + +pub use crate::{ + accept_language::{AcceptLanguage, Language}, + dpi::Dpi, + error::{ImplementationLimit, LoadingError, RenderingError}, + length::{LengthUnit, RsvgLength as Length}, +}; + +use url::Url; + +use std::path::Path; +use std::sync::Arc; + +use gio::prelude::*; // Re-exposes glib's prelude as well +use gio::Cancellable; + +use locale_config::{LanguageRange, Locale}; + +use crate::{ + accept_language::{LanguageTags, UserLanguage}, + handle::{Handle, LoadOptions}, + session::Session, + url_resolver::UrlResolver, +}; + +/// Builder for loading an [`SvgHandle`]. +/// +/// This is the starting point for using librsvg. This struct +/// implements a builder pattern for configuring an [`SvgHandle`]'s +/// options, and then loading the SVG data. You can call the methods +/// of `Loader` in sequence to configure how SVG data should be +/// loaded, and finally use one of the loading functions to load an +/// [`SvgHandle`]. +pub struct Loader { + unlimited_size: bool, + keep_image_data: bool, + session: Session, +} + +impl Loader { + /// Creates a `Loader` with the default flags. + /// + /// * [`unlimited_size`](#method.with_unlimited_size) defaults to `false`, as malicious + /// SVG documents could cause the XML parser to consume very large amounts of memory. + /// + /// * [`keep_image_data`](#method.keep_image_data) defaults to + /// `false`. You may only need this if rendering to Cairo + /// surfaces that support including image data in compressed + /// formats, like PDF. + /// + /// # Example: + /// + /// ``` + /// use rsvg; + /// + /// let svg_handle = rsvg::Loader::new() + /// .read_path("example.svg") + /// .unwrap(); + /// ``` + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + unlimited_size: false, + keep_image_data: false, + session: Session::default(), + } + } + + /// Creates a `Loader` from a pre-created [`Session`]. + /// + /// This is useful when a `Loader` must be created by the C API, which should already + /// have created a session for logging. + #[cfg(feature = "c-api")] + pub fn new_with_session(session: Session) -> Self { + Self { + unlimited_size: false, + keep_image_data: false, + session, + } + } + + /// Controls safety limits used in the XML parser. + /// + /// Internally, librsvg uses libxml2, which has set limits for things like the + /// maximum length of XML element names, the size of accumulated buffers + /// using during parsing of deeply-nested XML files, and the maximum size + /// of embedded XML entities. + /// + /// Set this to `true` only if loading a trusted SVG fails due to size limits. + /// + /// # Example: + /// ``` + /// use rsvg; + /// + /// let svg_handle = rsvg::Loader::new() + /// .with_unlimited_size(true) + /// .read_path("example.svg") // presumably a trusted huge file + /// .unwrap(); + /// ``` + pub fn with_unlimited_size(mut self, unlimited: bool) -> Self { + self.unlimited_size = unlimited; + self + } + + /// Controls embedding of compressed image data into the renderer. + /// + /// Normally, Cairo expects one to pass it uncompressed (decoded) + /// images as surfaces. However, when using a PDF rendering + /// context to render SVG documents that reference raster images + /// (e.g. those which include a bitmap as part of the SVG image), + /// it may be more efficient to embed the original, compressed raster + /// images into the PDF. + /// + /// Set this to `true` if you are using a Cairo PDF context, or any other type + /// of context which allows embedding compressed images. + /// + /// # Example: + /// + /// ``` + /// # use std::env; + /// let svg_handle = rsvg::Loader::new() + /// .keep_image_data(true) + /// .read_path("example.svg") + /// .unwrap(); + /// + /// let mut output = env::temp_dir(); + /// output.push("output.pdf"); + /// let surface = cairo::PdfSurface::new(640.0, 480.0, output)?; + /// let cr = cairo::Context::new(&surface).expect("Failed to create a cairo context"); + /// + /// let renderer = rsvg::CairoRenderer::new(&svg_handle); + /// renderer.render_document( + /// &cr, + /// &cairo::Rectangle::new(0.0, 0.0, 640.0, 480.0), + /// )?; + /// # Ok::<(), rsvg::RenderingError>(()) + /// ``` + pub fn keep_image_data(mut self, keep: bool) -> Self { + self.keep_image_data = keep; + self + } + + /// Reads an SVG document from `path`. + /// + /// # Example: + /// + /// ``` + /// let svg_handle = rsvg::Loader::new() + /// .read_path("example.svg") + /// .unwrap(); + /// ``` + pub fn read_path<P: AsRef<Path>>(self, path: P) -> Result<SvgHandle, LoadingError> { + let file = gio::File::for_path(path); + self.read_file(&file, None::<&Cancellable>) + } + + /// Reads an SVG document from a `gio::File`. + /// + /// The `cancellable` can be used to cancel loading from another thread. + /// + /// # Example: + /// ``` + /// let svg_handle = rsvg::Loader::new() + /// .read_file(&gio::File::for_path("example.svg"), None::<&gio::Cancellable>) + /// .unwrap(); + /// ``` + pub fn read_file<F: IsA<gio::File>, P: IsA<Cancellable>>( + self, + file: &F, + cancellable: Option<&P>, + ) -> Result<SvgHandle, LoadingError> { + let stream = file.read(cancellable)?; + self.read_stream(&stream, Some(file), cancellable) + } + + /// Reads an SVG stream from a `gio::InputStream`. + /// + /// The `base_file`, if it is not `None`, is used to extract the + /// [base URL][crate#the-base-file-and-resolving-references-to-external-files] for this stream. + /// + /// Reading an SVG document may involve resolving relative URLs if the + /// SVG references things like raster images, or other SVG files. + /// In this case, pass the `base_file` that correspondds to the + /// URL where this SVG got loaded from. + /// + /// The `cancellable` can be used to cancel loading from another thread. + /// + /// # Example + /// + /// ``` + /// use gio::prelude::*; + /// + /// let file = gio::File::for_path("example.svg"); + /// + /// let stream = file.read(None::<&gio::Cancellable>).unwrap(); + /// + /// let svg_handle = rsvg::Loader::new() + /// .read_stream(&stream, Some(&file), None::<&gio::Cancellable>) + /// .unwrap(); + /// ``` + pub fn read_stream<S: IsA<gio::InputStream>, F: IsA<gio::File>, P: IsA<Cancellable>>( + self, + stream: &S, + base_file: Option<&F>, + cancellable: Option<&P>, + ) -> Result<SvgHandle, LoadingError> { + let base_file = base_file.map(|f| f.as_ref()); + + let base_url = if let Some(base_file) = base_file { + Some(url_from_file(base_file)?) + } else { + None + }; + + let load_options = LoadOptions::new(UrlResolver::new(base_url)) + .with_unlimited_size(self.unlimited_size) + .keep_image_data(self.keep_image_data); + + Ok(SvgHandle { + handle: Handle::from_stream( + self.session.clone(), + Arc::new(load_options), + stream.as_ref(), + cancellable.map(|c| c.as_ref()), + )?, + session: self.session, + }) + } +} + +fn url_from_file(file: &gio::File) -> Result<Url, LoadingError> { + Url::parse(&file.uri()).map_err(|_| LoadingError::BadUrl) +} + +/// Handle used to hold SVG data in memory. +/// +/// You can create this from one of the `read` methods in +/// [`Loader`]. +pub struct SvgHandle { + session: Session, + pub(crate) handle: Handle, +} + +impl SvgHandle { + /// Checks if the SVG has an element with the specified `id`. + /// + /// Note that the `id` must be a plain fragment identifier like `#foo`, with + /// a leading `#` character. + /// + /// The purpose of the `Err()` case in the return value is to indicate an + /// incorrectly-formatted `id` argument. + pub fn has_element_with_id(&self, id: &str) -> Result<bool, RenderingError> { + self.handle.has_sub(id) + } + + /// Sets a CSS stylesheet to use for an SVG document. + /// + /// During the CSS cascade, the specified stylesheet will be used + /// with a "User" [origin]. + /// + /// Note that `@import` rules will not be resolved, except for `data:` URLs. + /// + /// [origin]: https://drafts.csswg.org/css-cascade-3/#cascading-origins + pub fn set_stylesheet(&mut self, css: &str) -> Result<(), LoadingError> { + self.handle.set_stylesheet(css) + } +} + +/// Can render an `SvgHandle` to a Cairo context. +pub struct CairoRenderer<'a> { + pub(crate) handle: &'a SvgHandle, + pub(crate) dpi: Dpi, + user_language: UserLanguage, + is_testing: bool, +} + +// Note that these are different than the C API's default, which is 90. +const DEFAULT_DPI_X: f64 = 96.0; +const DEFAULT_DPI_Y: f64 = 96.0; + +#[derive(Debug, Copy, Clone, PartialEq)] +/// Contains the computed values of the `<svg>` element's `width`, `height`, and `viewBox`. +/// +/// An SVG document has a toplevel `<svg>` element, with optional attributes `width`, +/// `height`, and `viewBox`. This structure contains the values for those attributes; you +/// can obtain the struct from [`CairoRenderer::intrinsic_dimensions`]. +/// +/// Since librsvg 2.54.0, there is support for [geometry +/// properties](https://www.w3.org/TR/SVG2/geometry.html) from SVG2. This means that +/// `width` and `height` are no longer attributes; they are instead CSS properties that +/// default to `auto`. The computed value for `auto` is `100%`, so for a `<svg>` that +/// does not have these attributes/properties, the `width`/`height` fields will be +/// returned as a [`Length`] of 100%. +/// +/// As an example, the following SVG element has a `width` of 100 pixels +/// and a `height` of 400 pixels, but no `viewBox`. +/// +/// ```xml +/// <svg xmlns="http://www.w3.org/2000/svg" width="100" height="400"> +/// ``` +/// +/// In this case, the length fields will be set to the corresponding +/// values with [`LengthUnit::Px`] units, and the `vbox` field will be +/// set to to `None`. +pub struct IntrinsicDimensions { + /// Computed value of the `width` property of the `<svg>`. + pub width: Length, + + /// Computed value of the `height` property of the `<svg>`. + pub height: Length, + + /// `viewBox` attribute of the `<svg>`, if present. + pub vbox: Option<cairo::Rectangle>, +} + +/// Gets the user's preferred locale from the environment and +/// translates it to a `Locale` with `LanguageRange` fallbacks. +/// +/// The `Locale::current()` call only contemplates a single language, +/// but glib is smarter, and `g_get_langauge_names()` can provide +/// fallbacks, for example, when LC_MESSAGES="en_US.UTF-8:de" (USA +/// English and German). This function converts the output of +/// `g_get_language_names()` into a `Locale` with appropriate +/// fallbacks. +fn locale_from_environment() -> Locale { + let mut locale = Locale::invariant(); + + for name in glib::language_names() { + let name = name.as_str(); + if let Ok(range) = LanguageRange::from_unix(name) { + locale.add(&range); + } + } + + locale +} + +impl UserLanguage { + fn new(language: &Language, session: &Session) -> UserLanguage { + match *language { + Language::FromEnvironment => UserLanguage::LanguageTags( + LanguageTags::from_locale(&locale_from_environment()) + .map_err(|s| { + rsvg_log!(session, "could not convert locale to language tags: {}", s); + }) + .unwrap_or_else(|_| LanguageTags::empty()), + ), + + Language::AcceptLanguage(ref a) => UserLanguage::AcceptLanguage(a.clone()), + } + } +} + +impl<'a> CairoRenderer<'a> { + /// Creates a `CairoRenderer` for the specified `SvgHandle`. + /// + /// The default dots-per-inch (DPI) value is set to 96; you can change it + /// with the [`with_dpi`] method. + /// + /// [`with_dpi`]: #method.with_dpi + pub fn new(handle: &'a SvgHandle) -> Self { + let session = &handle.session; + + CairoRenderer { + handle, + dpi: Dpi::new(DEFAULT_DPI_X, DEFAULT_DPI_Y), + user_language: UserLanguage::new(&Language::FromEnvironment, session), + is_testing: false, + } + } + + /// Configures the dots-per-inch for resolving physical lengths. + /// + /// If an SVG document has physical units like `5cm`, they must be resolved + /// to pixel-based values. The default pixel density is 96 DPI in + /// both dimensions. + pub fn with_dpi(self, dpi_x: f64, dpi_y: f64) -> Self { + assert!(dpi_x > 0.0); + assert!(dpi_y > 0.0); + + CairoRenderer { + dpi: Dpi::new(dpi_x, dpi_y), + ..self + } + } + + /// Configures the set of languages used for rendering. + /// + /// SVG documents can use the `<switch>` element, whose children have a + /// `systemLanguage` attribute; only the first child which has a `systemLanguage` that + /// matches the preferred languages will be rendered. + /// + /// This function sets the preferred languages. The default is + /// `Language::FromEnvironment`, which means that the set of preferred languages will + /// be obtained from the program's environment. To set an explicit list of languages, + /// you can use `Language::AcceptLanguage` instead. + pub fn with_language(self, language: &Language) -> Self { + let user_language = UserLanguage::new(language, &self.handle.session); + + CairoRenderer { + user_language, + ..self + } + } + + /// Queries the `width`, `height`, and `viewBox` attributes in an SVG document. + /// + /// If you are calling this function to compute a scaling factor to render the SVG, + /// consider simply using [`render_document`] instead; it will do the scaling + /// computations automatically. + /// + /// See also [`intrinsic_size_in_pixels`], which does the conversion to pixels if + /// possible. + /// + /// [`render_document`]: #method.render_document + /// [`intrinsic_size_in_pixels`]: #method.intrinsic_size_in_pixels + pub fn intrinsic_dimensions(&self) -> IntrinsicDimensions { + let d = self.handle.handle.get_intrinsic_dimensions(); + + IntrinsicDimensions { + width: Into::into(d.width), + height: Into::into(d.height), + vbox: d.vbox.map(|v| cairo::Rectangle::from(*v)), + } + } + + /// Converts the SVG document's intrinsic dimensions to pixels, if possible. + /// + /// Returns `Some(width, height)` in pixel units if the SVG document has `width` and + /// `height` attributes with physical dimensions (CSS pixels, cm, in, etc.) or + /// font-based dimensions (em, ex). + /// + /// Note that the dimensions are floating-point numbers, so your application can know + /// the exact size of an SVG document. To get integer dimensions, you should use + /// [`f64::ceil()`] to round up to the nearest integer (just using [`f64::round()`], + /// may may chop off pixels with fractional coverage). + /// + /// If the SVG document has percentage-based `width` and `height` attributes, or if + /// either of those attributes are not present, returns `None`. Dimensions of that + /// kind require more information to be resolved to pixels; for example, the calling + /// application can use a viewport size to scale percentage-based dimensions. + pub fn intrinsic_size_in_pixels(&self) -> Option<(f64, f64)> { + let dim = self.intrinsic_dimensions(); + let width = dim.width; + let height = dim.height; + + if width.unit == LengthUnit::Percent || height.unit == LengthUnit::Percent { + return None; + } + + Some(self.handle.handle.width_height_to_user(self.dpi)) + } + + /// Renders the whole SVG document fitted to a viewport + /// + /// The `viewport` gives the position and size at which the whole SVG + /// document will be rendered. + /// + /// The `cr` must be in a `cairo::Status::Success` state, or this function + /// will not render anything, and instead will return + /// `RenderingError::Cairo` with the `cr`'s current error state. + pub fn render_document( + &self, + cr: &cairo::Context, + viewport: &cairo::Rectangle, + ) -> Result<(), RenderingError> { + self.handle.handle.render_document( + cr, + viewport, + &self.user_language, + self.dpi, + self.is_testing, + ) + } + + /// Computes the (ink_rect, logical_rect) of an SVG element, as if + /// the SVG were rendered to a specific viewport. + /// + /// Element IDs should look like an URL fragment identifier; for + /// example, pass `Some("#foo")` to get the geometry of the + /// element that has an `id="foo"` attribute. + /// + /// The "ink rectangle" is the bounding box that would be painted + /// for fully- stroked and filled elements. + /// + /// The "logical rectangle" just takes into account the unstroked + /// paths and text outlines. + /// + /// Note that these bounds are not minimum bounds; for example, + /// clipping paths are not taken into account. + /// + /// You can pass `None` for the `id` if you want to measure all + /// the elements in the SVG, i.e. to measure everything from the + /// root element. + /// + /// This operation is not constant-time, as it involves going through all + /// the child elements. + /// + /// FIXME: example + pub fn geometry_for_layer( + &self, + id: Option<&str>, + viewport: &cairo::Rectangle, + ) -> Result<(cairo::Rectangle, cairo::Rectangle), RenderingError> { + self.handle + .handle + .get_geometry_for_layer(id, viewport, &self.user_language, self.dpi, self.is_testing) + .map(|(i, l)| (i, l)) + } + + /// Renders a single SVG element in the same place as for a whole SVG document + /// + /// This is equivalent to `render_document`, but renders only a single element and its + /// children, as if they composed an individual layer in the SVG. The element is + /// rendered with the same transformation matrix as it has within the whole SVG + /// document. Applications can use this to re-render a single element and repaint it + /// on top of a previously-rendered document, for example. + /// + /// Note that the `id` must be a plain fragment identifier like `#foo`, with + /// a leading `#` character. + /// + /// The `viewport` gives the position and size at which the whole SVG + /// document would be rendered. This function will effectively place the + /// whole SVG within that viewport, but only render the element given by + /// `id`. + /// + /// The `cr` must be in a `cairo::Status::Success` state, or this function + /// will not render anything, and instead will return + /// `RenderingError::Cairo` with the `cr`'s current error state. + pub fn render_layer( + &self, + cr: &cairo::Context, + id: Option<&str>, + viewport: &cairo::Rectangle, + ) -> Result<(), RenderingError> { + self.handle.handle.render_layer( + cr, + id, + viewport, + &self.user_language, + self.dpi, + self.is_testing, + ) + } + + /// Computes the (ink_rect, logical_rect) of a single SVG element + /// + /// While `geometry_for_layer` computes the geometry of an SVG element subtree with + /// its transformation matrix, this other function will compute the element's geometry + /// as if it were being rendered under an identity transformation by itself. That is, + /// the resulting geometry is as if the element got extracted by itself from the SVG. + /// + /// This function is the counterpart to `render_element`. + /// + /// Element IDs should look like an URL fragment identifier; for + /// example, pass `Some("#foo")` to get the geometry of the + /// element that has an `id="foo"` attribute. + /// + /// The "ink rectangle" is the bounding box that would be painted + /// for fully- stroked and filled elements. + /// + /// The "logical rectangle" just takes into account the unstroked + /// paths and text outlines. + /// + /// Note that these bounds are not minimum bounds; for example, + /// clipping paths are not taken into account. + /// + /// You can pass `None` for the `id` if you want to measure all + /// the elements in the SVG, i.e. to measure everything from the + /// root element. + /// + /// This operation is not constant-time, as it involves going through all + /// the child elements. + /// + /// FIXME: example + pub fn geometry_for_element( + &self, + id: Option<&str>, + ) -> Result<(cairo::Rectangle, cairo::Rectangle), RenderingError> { + self.handle + .handle + .get_geometry_for_element(id, &self.user_language, self.dpi, self.is_testing) + .map(|(i, l)| (i, l)) + } + + /// Renders a single SVG element to a given viewport + /// + /// This function can be used to extract individual element subtrees and render them, + /// scaled to a given `element_viewport`. This is useful for applications which have + /// reusable objects in an SVG and want to render them individually; for example, an + /// SVG full of icons that are meant to be be rendered independently of each other. + /// + /// Note that the `id` must be a plain fragment identifier like `#foo`, with + /// a leading `#` character. + /// + /// The `element_viewport` gives the position and size at which the named element will + /// be rendered. FIXME: mention proportional scaling. + /// + /// The `cr` must be in a `cairo::Status::Success` state, or this function + /// will not render anything, and instead will return + /// `RenderingError::Cairo` with the `cr`'s current error state. + pub fn render_element( + &self, + cr: &cairo::Context, + id: Option<&str>, + element_viewport: &cairo::Rectangle, + ) -> Result<(), RenderingError> { + self.handle.handle.render_element( + cr, + id, + element_viewport, + &self.user_language, + self.dpi, + self.is_testing, + ) + } + + /// Returns DPI TODO + pub fn dpi(&self) -> Dpi { + self.dpi + } + + /// Questionable Special function TODO + #[cfg(feature = "c-api")] + pub fn handle(&self) -> &Handle { + &self.handle.handle + } + + /// Turns on test mode. Do not use this function; it is for librsvg's test suite only. + pub fn test_mode(self, is_testing: bool) -> Self { + CairoRenderer { is_testing, ..self } + } +} diff --git a/rsvg/src/aspect_ratio.rs b/rsvg/src/aspect_ratio.rs new file mode 100644 index 00000000..3b2cc830 --- /dev/null +++ b/rsvg/src/aspect_ratio.rs @@ -0,0 +1,456 @@ +//! Handling of `preserveAspectRatio` values. +//! +//! This module handles `preserveAspectRatio` values [per the SVG specification][spec]. +//! We have an [`AspectRatio`] struct which encapsulates such a value. +//! +//! ``` +//! # use rsvg::doctest_only::AspectRatio; +//! # use rsvg::doctest_only::Parse; +//! assert_eq!( +//! AspectRatio::parse_str("xMidYMid").unwrap(), +//! AspectRatio::default() +//! ); +//! ``` +//! +//! [spec]: https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute + +use cssparser::{BasicParseError, Parser}; +use std::ops::Deref; + +use crate::error::*; +use crate::parsers::Parse; +use crate::rect::Rect; +use crate::transform::{Transform, ValidTransform}; +use crate::viewbox::ViewBox; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum FitMode { + Meet, + Slice, +} + +enum_default!(FitMode, FitMode::Meet); + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum Align1D { + Min, + Mid, + Max, +} + +enum_default!(Align1D, Align1D::Mid); + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] +struct X(Align1D); +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] +struct Y(Align1D); + +impl Deref for X { + type Target = Align1D; + + fn deref(&self) -> &Align1D { + &self.0 + } +} + +impl Deref for Y { + type Target = Align1D; + + fn deref(&self) -> &Align1D { + &self.0 + } +} + +impl Align1D { + fn compute(self, dest_pos: f64, dest_size: f64, obj_size: f64) -> f64 { + match self { + Align1D::Min => dest_pos, + Align1D::Mid => dest_pos + (dest_size - obj_size) / 2.0, + Align1D::Max => dest_pos + dest_size - obj_size, + } + } +} + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] +struct Align { + x: X, + y: Y, + fit: FitMode, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct AspectRatio { + defer: bool, + align: Option<Align>, +} + +impl Default for AspectRatio { + fn default() -> AspectRatio { + AspectRatio { + defer: false, + align: Some(Align::default()), + } + } +} + +impl AspectRatio { + pub fn is_slice(&self) -> bool { + matches!( + self.align, + Some(Align { + fit: FitMode::Slice, + .. + }) + ) + } + + pub fn compute(&self, vbox: &ViewBox, viewport: &Rect) -> Rect { + match self.align { + None => *viewport, + + Some(Align { x, y, fit }) => { + let (vb_width, vb_height) = vbox.size(); + let (vp_width, vp_height) = viewport.size(); + + let w_factor = vp_width / vb_width; + let h_factor = vp_height / vb_height; + + let factor = match fit { + FitMode::Meet => w_factor.min(h_factor), + FitMode::Slice => w_factor.max(h_factor), + }; + + let w = vb_width * factor; + let h = vb_height * factor; + + let xpos = x.compute(viewport.x0, vp_width, w); + let ypos = y.compute(viewport.y0, vp_height, h); + + Rect::new(xpos, ypos, xpos + w, ypos + h) + } + } + } + + /// Computes the viewport to viewbox transformation. + /// + /// Given a viewport, returns a transformation that will create a coordinate + /// space inside it. The `(vbox.x0, vbox.y0)` will be mapped to the viewport's + /// upper-left corner, and the `(vbox.x1, vbox.y1)` will be mapped to the viewport's + /// lower-right corner. + /// + /// If the vbox or viewport are empty, returns `Ok(None)`. Per the SVG spec, either + /// of those mean that the corresponding element should not be rendered. + /// + /// If the vbox would create an invalid transform (say, a vbox with huge numbers that + /// leads to a near-zero scaling transform), returns an `Err(())`. + pub fn viewport_to_viewbox_transform( + &self, + vbox: Option<ViewBox>, + viewport: &Rect, + ) -> Result<Option<ValidTransform>, InvalidTransform> { + // width or height set to 0 disables rendering of the element + // https://www.w3.org/TR/SVG/struct.html#SVGElementWidthAttribute + // https://www.w3.org/TR/SVG/struct.html#UseElementWidthAttribute + // https://www.w3.org/TR/SVG/struct.html#ImageElementWidthAttribute + // https://www.w3.org/TR/SVG/painting.html#MarkerWidthAttribute + + if viewport.is_empty() { + return Ok(None); + } + + // the preserveAspectRatio attribute is only used if viewBox is specified + // https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute + let transform = if let Some(vbox) = vbox { + if vbox.is_empty() { + // Width or height of 0 for the viewBox disables rendering of the element + // https://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute + return Ok(None); + } else { + let r = self.compute(&vbox, viewport); + Transform::new_translate(r.x0, r.y0) + .pre_scale(r.width() / vbox.width(), r.height() / vbox.height()) + .pre_translate(-vbox.x0, -vbox.y0) + } + } else { + Transform::new_translate(viewport.x0, viewport.y0) + }; + + ValidTransform::try_from(transform).map(Some) + } +} + +fn parse_align_xy<'i>(parser: &mut Parser<'i, '_>) -> Result<Option<(X, Y)>, BasicParseError<'i>> { + use self::Align1D::*; + + parse_identifiers!( + parser, + + "none" => None, + + "xMinYMin" => Some((X(Min), Y(Min))), + "xMidYMin" => Some((X(Mid), Y(Min))), + "xMaxYMin" => Some((X(Max), Y(Min))), + + "xMinYMid" => Some((X(Min), Y(Mid))), + "xMidYMid" => Some((X(Mid), Y(Mid))), + "xMaxYMid" => Some((X(Max), Y(Mid))), + + "xMinYMax" => Some((X(Min), Y(Max))), + "xMidYMax" => Some((X(Mid), Y(Max))), + "xMaxYMax" => Some((X(Max), Y(Max))), + ) +} + +fn parse_fit_mode<'i>(parser: &mut Parser<'i, '_>) -> Result<FitMode, BasicParseError<'i>> { + parse_identifiers!( + parser, + "meet" => FitMode::Meet, + "slice" => FitMode::Slice, + ) +} + +impl Parse for AspectRatio { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<AspectRatio, ParseError<'i>> { + let defer = parser + .try_parse(|p| p.expect_ident_matching("defer")) + .is_ok(); + + let align_xy = parser.try_parse(parse_align_xy)?; + let fit = parser.try_parse(parse_fit_mode).unwrap_or_default(); + let align = align_xy.map(|(x, y)| Align { x, y, fit }); + + Ok(AspectRatio { defer, align }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::float_eq_cairo::ApproxEqCairo; + + #[test] + fn parsing_invalid_strings_yields_error() { + assert!(AspectRatio::parse_str("").is_err()); + assert!(AspectRatio::parse_str("defer").is_err()); + assert!(AspectRatio::parse_str("defer foo").is_err()); + assert!(AspectRatio::parse_str("defer xMidYMid foo").is_err()); + assert!(AspectRatio::parse_str("xMidYMid foo").is_err()); + assert!(AspectRatio::parse_str("defer xMidYMid meet foo").is_err()); + } + + #[test] + fn parses_valid_strings() { + assert_eq!( + AspectRatio::parse_str("defer none").unwrap(), + AspectRatio { + defer: true, + align: None, + } + ); + + assert_eq!( + AspectRatio::parse_str("xMidYMid").unwrap(), + AspectRatio { + defer: false, + align: Some(Align { + x: X(Align1D::Mid), + y: Y(Align1D::Mid), + fit: FitMode::Meet, + },), + } + ); + + assert_eq!( + AspectRatio::parse_str("defer xMidYMid").unwrap(), + AspectRatio { + defer: true, + align: Some(Align { + x: X(Align1D::Mid), + y: Y(Align1D::Mid), + fit: FitMode::Meet, + },), + } + ); + + assert_eq!( + AspectRatio::parse_str("defer xMinYMax").unwrap(), + AspectRatio { + defer: true, + align: Some(Align { + x: X(Align1D::Min), + y: Y(Align1D::Max), + fit: FitMode::Meet, + },), + } + ); + + assert_eq!( + AspectRatio::parse_str("defer xMaxYMid meet").unwrap(), + AspectRatio { + defer: true, + align: Some(Align { + x: X(Align1D::Max), + y: Y(Align1D::Mid), + fit: FitMode::Meet, + },), + } + ); + + assert_eq!( + AspectRatio::parse_str("defer xMinYMax slice").unwrap(), + AspectRatio { + defer: true, + align: Some(Align { + x: X(Align1D::Min), + y: Y(Align1D::Max), + fit: FitMode::Slice, + },), + } + ); + } + + fn assert_rect_equal(r1: &Rect, r2: &Rect) { + assert_approx_eq_cairo!(r1.x0, r2.x0); + assert_approx_eq_cairo!(r1.y0, r2.y0); + assert_approx_eq_cairo!(r1.x1, r2.x1); + assert_approx_eq_cairo!(r1.y1, r2.y1); + } + + #[test] + fn aligns() { + let viewbox = ViewBox::from(Rect::from_size(1.0, 10.0)); + + let foo = AspectRatio::parse_str("xMinYMin meet").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::from_size(0.1, 1.0)); + + let foo = AspectRatio::parse_str("xMinYMin slice").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::from_size(10.0, 100.0)); + + let foo = AspectRatio::parse_str("xMinYMid meet").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::from_size(0.1, 1.0)); + + let foo = AspectRatio::parse_str("xMinYMid slice").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::new(0.0, -49.5, 10.0, 100.0 - 49.5)); + + let foo = AspectRatio::parse_str("xMinYMax meet").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::from_size(0.1, 1.0)); + + let foo = AspectRatio::parse_str("xMinYMax slice").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::new(0.0, -99.0, 10.0, 1.0)); + + let foo = AspectRatio::parse_str("xMidYMin meet").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::new(4.95, 0.0, 4.95 + 0.1, 1.0)); + + let foo = AspectRatio::parse_str("xMidYMin slice").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::from_size(10.0, 100.0)); + + let foo = AspectRatio::parse_str("xMidYMid meet").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::new(4.95, 0.0, 4.95 + 0.1, 1.0)); + + let foo = AspectRatio::parse_str("xMidYMid slice").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::new(0.0, -49.5, 10.0, 100.0 - 49.5)); + + let foo = AspectRatio::parse_str("xMidYMax meet").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::new(4.95, 0.0, 4.95 + 0.1, 1.0)); + + let foo = AspectRatio::parse_str("xMidYMax slice").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::new(0.0, -99.0, 10.0, 1.0)); + + let foo = AspectRatio::parse_str("xMaxYMin meet").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::new(9.9, 0.0, 10.0, 1.0)); + + let foo = AspectRatio::parse_str("xMaxYMin slice").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::from_size(10.0, 100.0)); + + let foo = AspectRatio::parse_str("xMaxYMid meet").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::new(9.9, 0.0, 10.0, 1.0)); + + let foo = AspectRatio::parse_str("xMaxYMid slice").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::new(0.0, -49.5, 10.0, 100.0 - 49.5)); + + let foo = AspectRatio::parse_str("xMaxYMax meet").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::new(9.9, 0.0, 10.0, 1.0)); + + let foo = AspectRatio::parse_str("xMaxYMax slice").unwrap(); + let foo = foo.compute(&viewbox, &Rect::from_size(10.0, 1.0)); + assert_rect_equal(&foo, &Rect::new(0.0, -99.0, 10.0, 1.0)); + } + + #[test] + fn empty_viewport() { + let a = AspectRatio::default(); + let t = a + .viewport_to_viewbox_transform( + Some(ViewBox::parse_str("10 10 40 40").unwrap()), + &Rect::from_size(0.0, 0.0), + ) + .unwrap(); + + assert_eq!(t, None); + } + + #[test] + fn empty_viewbox() { + let a = AspectRatio::default(); + let t = a + .viewport_to_viewbox_transform( + Some(ViewBox::parse_str("10 10 0 0").unwrap()), + &Rect::from_size(10.0, 10.0), + ) + .unwrap(); + + assert_eq!(t, None); + } + + #[test] + fn valid_viewport_and_viewbox() { + let a = AspectRatio::default(); + let t = a + .viewport_to_viewbox_transform( + Some(ViewBox::parse_str("10 10 40 40").unwrap()), + &Rect::new(1.0, 1.0, 2.0, 2.0), + ) + .unwrap(); + + assert_eq!( + t, + Some( + ValidTransform::try_from( + Transform::identity() + .pre_translate(1.0, 1.0) + .pre_scale(0.025, 0.025) + .pre_translate(-10.0, -10.0) + ) + .unwrap() + ) + ); + } + + #[test] + fn invalid_viewbox() { + let a = AspectRatio::default(); + let t = a.viewport_to_viewbox_transform( + Some(ViewBox::parse_str("0 0 6E20 540").unwrap()), + &Rect::new(1.0, 1.0, 2.0, 2.0), + ); + + assert_eq!(t, Err(InvalidTransform)); + } +} diff --git a/rsvg/src/bbox.rs b/rsvg/src/bbox.rs new file mode 100644 index 00000000..c851c473 --- /dev/null +++ b/rsvg/src/bbox.rs @@ -0,0 +1,121 @@ +//! Bounding boxes that know their coordinate space. + +use crate::rect::Rect; +use crate::transform::Transform; + +#[derive(Debug, Default, Copy, Clone)] +pub struct BoundingBox { + transform: Transform, + pub rect: Option<Rect>, // without stroke + pub ink_rect: Option<Rect>, // with stroke +} + +impl BoundingBox { + pub fn new() -> BoundingBox { + Default::default() + } + + pub fn with_transform(self, transform: Transform) -> BoundingBox { + BoundingBox { transform, ..self } + } + + pub fn with_rect(self, rect: Rect) -> BoundingBox { + BoundingBox { + rect: Some(rect), + ..self + } + } + + pub fn with_ink_rect(self, ink_rect: Rect) -> BoundingBox { + BoundingBox { + ink_rect: Some(ink_rect), + ..self + } + } + + pub fn clear(mut self) { + self.rect = None; + self.ink_rect = None; + } + + fn combine(&mut self, src: &BoundingBox, clip: bool) { + if src.rect.is_none() && src.ink_rect.is_none() { + return; + } + + // this will panic!() if it's not invertible... should we check on our own? + let transform = self + .transform + .invert() + .unwrap() + .pre_transform(&src.transform); + + self.rect = combine_rects(self.rect, src.rect, &transform, clip); + self.ink_rect = combine_rects(self.ink_rect, src.ink_rect, &transform, clip); + } + + pub fn insert(&mut self, src: &BoundingBox) { + self.combine(src, false); + } + + pub fn clip(&mut self, src: &BoundingBox) { + self.combine(src, true); + } +} + +fn combine_rects( + r1: Option<Rect>, + r2: Option<Rect>, + transform: &Transform, + clip: bool, +) -> Option<Rect> { + match (r1, r2, clip) { + (r1, None, _) => r1, + (None, Some(r2), _) => Some(transform.transform_rect(&r2)), + (Some(r1), Some(r2), true) => transform + .transform_rect(&r2) + .intersection(&r1) + .or_else(|| Some(Rect::default())), + (Some(r1), Some(r2), false) => Some(transform.transform_rect(&r2).union(&r1)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn combine() { + let r1 = Rect::new(1.0, 2.0, 3.0, 4.0); + let r2 = Rect::new(1.5, 2.5, 3.5, 4.5); + let r3 = Rect::new(10.0, 11.0, 12.0, 13.0); + let t = Transform::new_unchecked(1.0, 0.0, 0.0, 1.0, 0.5, 0.5); + + let res = combine_rects(None, None, &t, true); + assert_eq!(res, None); + + let res = combine_rects(None, None, &t, false); + assert_eq!(res, None); + + let res = combine_rects(Some(r1), None, &t, true); + assert_eq!(res, Some(r1)); + + let res = combine_rects(Some(r1), None, &t, false); + assert_eq!(res, Some(r1)); + + let res = combine_rects(None, Some(r2), &t, true); + assert_eq!(res, Some(Rect::new(2.0, 3.0, 4.0, 5.0))); + + let res = combine_rects(None, Some(r2), &t, false); + assert_eq!(res, Some(Rect::new(2.0, 3.0, 4.0, 5.0))); + + let res = combine_rects(Some(r1), Some(r2), &t, true); + assert_eq!(res, Some(Rect::new(2.0, 3.0, 3.0, 4.0))); + + let res = combine_rects(Some(r1), Some(r3), &t, true); + assert_eq!(res, Some(Rect::default())); + + let res = combine_rects(Some(r1), Some(r2), &t, false); + assert_eq!(res, Some(Rect::new(1.0, 2.0, 4.0, 5.0))); + } +} diff --git a/rsvg/src/color.rs b/rsvg/src/color.rs new file mode 100644 index 00000000..b83b76f4 --- /dev/null +++ b/rsvg/src/color.rs @@ -0,0 +1,27 @@ +//! CSS color values. + +use cssparser::Parser; + +use crate::error::*; +use crate::parsers::Parse; + +pub use cssparser::Color; + +impl Parse for cssparser::Color { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<cssparser::Color, ParseError<'i>> { + Ok(cssparser::Color::parse(parser)?) + } +} + +impl Parse for cssparser::RGBA { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<cssparser::RGBA, ParseError<'i>> { + let loc = parser.current_source_location(); + + match cssparser::Color::parse(parser)? { + cssparser::Color::RGBA(rgba) => Ok(rgba), + cssparser::Color::CurrentColor => Err(loc.new_custom_error(ValueErrorKind::Value( + "currentColor is not allowed here".to_string(), + ))), + } + } +} diff --git a/rsvg/src/cond.rs b/rsvg/src/cond.rs new file mode 100644 index 00000000..57e4817c --- /dev/null +++ b/rsvg/src/cond.rs @@ -0,0 +1,243 @@ +//! Conditional processing attributes: `requiredExtensions`, `requiredFeatures`, `systemLanguage`. + +#[allow(unused_imports, deprecated)] +use std::ascii::AsciiExt; + +use std::str::FromStr; + +use language_tags::LanguageTag; + +use crate::accept_language::{LanguageTags, UserLanguage}; +use crate::error::*; +use crate::session::Session; + +// No extensions at the moment. +static IMPLEMENTED_EXTENSIONS: &[&str] = &[]; + +#[derive(Debug, PartialEq)] +pub struct RequiredExtensions(pub bool); + +impl RequiredExtensions { + /// Parse a requiredExtensions attribute. + /// + /// <http://www.w3.org/TR/SVG/struct.html#RequiredExtensionsAttribute> + pub fn from_attribute(s: &str) -> RequiredExtensions { + RequiredExtensions( + s.split_whitespace() + .all(|f| IMPLEMENTED_EXTENSIONS.binary_search(&f).is_ok()), + ) + } + + /// Evaluate a requiredExtensions value for conditional processing. + pub fn eval(&self) -> bool { + self.0 + } +} + +// Keep these sorted alphabetically for binary_search. +static IMPLEMENTED_FEATURES: &[&str] = &[ + "http://www.w3.org/TR/SVG11/feature#BasicFilter", + "http://www.w3.org/TR/SVG11/feature#BasicGraphicsAttribute", + "http://www.w3.org/TR/SVG11/feature#BasicPaintAttribute", + "http://www.w3.org/TR/SVG11/feature#BasicStructure", + "http://www.w3.org/TR/SVG11/feature#BasicText", + "http://www.w3.org/TR/SVG11/feature#ConditionalProcessing", + "http://www.w3.org/TR/SVG11/feature#ContainerAttribute", + "http://www.w3.org/TR/SVG11/feature#Filter", + "http://www.w3.org/TR/SVG11/feature#Gradient", + "http://www.w3.org/TR/SVG11/feature#Image", + "http://www.w3.org/TR/SVG11/feature#Marker", + "http://www.w3.org/TR/SVG11/feature#Mask", + "http://www.w3.org/TR/SVG11/feature#OpacityAttribute", + "http://www.w3.org/TR/SVG11/feature#Pattern", + "http://www.w3.org/TR/SVG11/feature#SVG", + "http://www.w3.org/TR/SVG11/feature#SVG-static", + "http://www.w3.org/TR/SVG11/feature#Shape", + "http://www.w3.org/TR/SVG11/feature#Structure", + "http://www.w3.org/TR/SVG11/feature#Style", + "http://www.w3.org/TR/SVG11/feature#View", + "org.w3c.svg.static", // deprecated SVG 1.0 feature string +]; + +#[derive(Debug, PartialEq)] +pub struct RequiredFeatures(pub bool); + +impl RequiredFeatures { + // Parse a requiredFeatures attribute + // http://www.w3.org/TR/SVG/struct.html#RequiredFeaturesAttribute + pub fn from_attribute(s: &str) -> RequiredFeatures { + RequiredFeatures( + s.split_whitespace() + .all(|f| IMPLEMENTED_FEATURES.binary_search(&f).is_ok()), + ) + } + + /// Evaluate a requiredFeatures value for conditional processing. + pub fn eval(&self) -> bool { + self.0 + } +} + +/// The systemLanguage attribute inside `<cond>` element's children. +/// +/// Parsing the value of a `systemLanguage` attribute may fail if the document supplies +/// invalid BCP47 language tags. In that case, we store an `Invalid` variant. +/// +/// That variant is used later, during [`SystemLanguage::eval`], to see whether the +/// `<cond>` should match or not. +#[derive(Debug, PartialEq)] +pub enum SystemLanguage { + Valid(LanguageTags), + Invalid, +} + +impl SystemLanguage { + /// Parse a `systemLanguage` attribute and match it against a given `Locale` + /// + /// The [`systemLanguage`] conditional attribute is a + /// comma-separated list of [BCP47] Language Tags. This function + /// parses the attribute and matches the result against a given + /// `locale`. If there is a match, i.e. if the given locale + /// supports one of the languages listed in the `systemLanguage` + /// attribute, then the `SystemLanguage.0` will be `true`; + /// otherwise it will be `false`. + /// + /// Normally, calling code will pass `&Locale::current()` for the + /// `locale` attribute; this is the user's current locale. + /// + /// [`systemLanguage`]: https://www.w3.org/TR/SVG/struct.html#ConditionalProcessingSystemLanguageAttribute + /// [BCP47]: http://www.ietf.org/rfc/bcp/bcp47.txt + pub fn from_attribute(s: &str, session: &Session) -> SystemLanguage { + let attribute_tags = s + .split(',') + .map(str::trim) + .map(|s| { + LanguageTag::from_str(s).map_err(|e| { + ValueErrorKind::parse_error(&format!("invalid language tag \"{s}\": {e}")) + }) + }) + .collect::<Result<Vec<LanguageTag>, _>>(); + + match attribute_tags { + Ok(tags) => SystemLanguage::Valid(LanguageTags::from(tags)), + + Err(e) => { + rsvg_log!( + session, + "ignoring systemLanguage attribute with invalid value: {}", + e + ); + SystemLanguage::Invalid + } + } + } + + /// Evaluate a systemLanguage value for conditional processing. + pub fn eval(&self, user_language: &UserLanguage) -> bool { + match *self { + SystemLanguage::Valid(ref tags) => user_language.any_matches(tags), + SystemLanguage::Invalid => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use locale_config::Locale; + + #[test] + fn required_extensions() { + assert_eq!( + RequiredExtensions::from_attribute("http://test.org/NotExisting/1.0"), + RequiredExtensions(false) + ); + } + + #[test] + fn required_features() { + assert_eq!( + RequiredFeatures::from_attribute("http://www.w3.org/TR/SVG11/feature#NotExisting"), + RequiredFeatures(false) + ); + + assert_eq!( + RequiredFeatures::from_attribute("http://www.w3.org/TR/SVG11/feature#BasicFilter"), + RequiredFeatures(true) + ); + + assert_eq!( + RequiredFeatures::from_attribute( + "http://www.w3.org/TR/SVG11/feature#BasicFilter \ + http://www.w3.org/TR/SVG11/feature#NotExisting", + ), + RequiredFeatures(false) + ); + + assert_eq!( + RequiredFeatures::from_attribute( + "http://www.w3.org/TR/SVG11/feature#BasicFilter \ + http://www.w3.org/TR/SVG11/feature#BasicText", + ), + RequiredFeatures(true) + ); + } + + #[test] + fn system_language() { + let session = Session::new_for_test_suite(); + + let locale = Locale::new("de,en-US").unwrap(); + let user_language = UserLanguage::LanguageTags(LanguageTags::from_locale(&locale).unwrap()); + + assert!(matches!( + SystemLanguage::from_attribute("", &session), + SystemLanguage::Invalid + )); + + assert!(matches!( + SystemLanguage::from_attribute("12345", &session), + SystemLanguage::Invalid + )); + + assert_eq!( + SystemLanguage::from_attribute("fr", &session).eval(&user_language), + false + ); + + assert_eq!( + SystemLanguage::from_attribute("en", &session).eval(&user_language), + false + ); + + assert_eq!( + SystemLanguage::from_attribute("de", &session).eval(&user_language), + true + ); + + assert_eq!( + SystemLanguage::from_attribute("en-US", &session).eval(&user_language), + true + ); + + assert_eq!( + SystemLanguage::from_attribute("en-GB", &session).eval(&user_language), + false + ); + + assert_eq!( + SystemLanguage::from_attribute("DE", &session).eval(&user_language), + true + ); + + assert_eq!( + SystemLanguage::from_attribute("de-LU", &session).eval(&user_language), + true + ); + + assert_eq!( + SystemLanguage::from_attribute("fr, de", &session).eval(&user_language), + true + ); + } +} diff --git a/rsvg/src/coord_units.rs b/rsvg/src/coord_units.rs new file mode 100644 index 00000000..b7d1a1c8 --- /dev/null +++ b/rsvg/src/coord_units.rs @@ -0,0 +1,99 @@ +//! `userSpaceOnUse` or `objectBoundingBox` values. + +use cssparser::Parser; + +use crate::error::*; +use crate::parsers::Parse; + +/// Defines the units to be used for things that can consider a +/// coordinate system in terms of the current transformation, or in +/// terms of the current object's bounding box. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CoordUnits { + UserSpaceOnUse, + ObjectBoundingBox, +} + +impl Parse for CoordUnits { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + Ok(parse_identifiers!( + parser, + "userSpaceOnUse" => CoordUnits::UserSpaceOnUse, + "objectBoundingBox" => CoordUnits::ObjectBoundingBox, + )?) + } +} + +/// Creates a newtype around `CoordUnits`, with a default value. +/// +/// SVG attributes that can take `userSpaceOnUse` or +/// `objectBoundingBox` values often have different default values +/// depending on the type of SVG element. We use this macro to create +/// a newtype for each SVG element and attribute that requires values +/// of this type. The newtype provides an `impl Default` with the +/// specified `$default` value. +#[macro_export] +macro_rules! coord_units { + ($name:ident, $default:expr) => { + #[derive(Debug, Copy, Clone, PartialEq, Eq)] + pub struct $name(pub CoordUnits); + + impl Default for $name { + fn default() -> Self { + $name($default) + } + } + + impl From<$name> for CoordUnits { + fn from(u: $name) -> Self { + u.0 + } + } + + impl $crate::parsers::Parse for $name { + fn parse<'i>( + parser: &mut ::cssparser::Parser<'i, '_>, + ) -> Result<Self, $crate::error::ParseError<'i>> { + Ok($name($crate::coord_units::CoordUnits::parse(parser)?)) + } + } + }; +} + +#[cfg(test)] +mod tests { + use super::*; + + coord_units!(MyUnits, CoordUnits::ObjectBoundingBox); + + #[test] + fn parsing_invalid_strings_yields_error() { + assert!(MyUnits::parse_str("").is_err()); + assert!(MyUnits::parse_str("foo").is_err()); + } + + #[test] + fn parses_paint_server_units() { + assert_eq!( + MyUnits::parse_str("userSpaceOnUse").unwrap(), + MyUnits(CoordUnits::UserSpaceOnUse) + ); + assert_eq!( + MyUnits::parse_str("objectBoundingBox").unwrap(), + MyUnits(CoordUnits::ObjectBoundingBox) + ); + } + + #[test] + fn has_correct_default() { + assert_eq!(MyUnits::default(), MyUnits(CoordUnits::ObjectBoundingBox)); + } + + #[test] + fn converts_to_coord_units() { + assert_eq!( + CoordUnits::from(MyUnits(CoordUnits::ObjectBoundingBox)), + CoordUnits::ObjectBoundingBox + ); + } +} diff --git a/rsvg/src/css.rs b/rsvg/src/css.rs new file mode 100644 index 00000000..14d51bd6 --- /dev/null +++ b/rsvg/src/css.rs @@ -0,0 +1,1110 @@ +//! Representation of CSS types, and the CSS parsing and matching engine. +//! +//! # Terminology +//! +//! Consider a CSS **stylesheet** like this: +//! +//! ```css +//! @import url("another.css"); +//! +//! foo, .bar { +//! fill: red; +//! stroke: green; +//! } +//! +//! #baz { stroke-width: 42; } +//! ``` +//! The example contains three **rules**, the first one is an **at-rule*, +//! the other two are **qualified rules**. +//! +//! Each rule is made of two parts, a **prelude** and an optional **block** +//! The prelude is the part until the first `{` or until `;`, depending on +//! whether a block is present. The block is the part between curly braces. +//! +//! Let's look at each rule: +//! +//! `@import` is an **at-rule**. This rule has a prelude, but no block. +//! There are other at-rules like `@media` and some of them may have a block, +//! but librsvg doesn't support those yet. +//! +//! The prelude of the following rule is `foo, .bar`. +//! It is a **selector list** with two **selectors**, one for +//! `foo` elements and one for elements that have the `bar` class. +//! +//! The content of the block between `{}` for a qualified rule is a +//! **declaration list**. The block of the first qualified rule contains two +//! **declarations**, one for the `fill` **property** and one for the +//! `stroke` property. +//! +//! After the first qualified rule, we have a second qualified rule with +//! a single selector for the `#baz` id, with a single declaration for the +//! `stroke-width` property. +//! +//! # Helper crates we use +//! +//! * `cssparser` crate as a CSS tokenizer, and some utilities to +//! parse CSS rules and declarations. +//! +//! * `selectors` crate for the representation of selectors and +//! selector lists, and for the matching engine. +//! +//! Both crates provide very generic implementations of their concepts, +//! and expect the caller to provide implementations of various traits, +//! and to provide types that represent certain things. +//! +//! For example, `cssparser` expects one to provide representations of +//! the following types: +//! +//! * A parsed CSS rule. For `fill: blue;` we have +//! `ParsedProperty::Fill(...)`. +//! +//! * A parsed selector list; we use `SelectorList` from the +//! `selectors` crate. +//! +//! In turn, the `selectors` crate needs a way to navigate and examine +//! one's implementation of an element tree. We provide `impl +//! selectors::Element for RsvgElement` for this. This implementation +//! has methods like "does this element have the id `#foo`", or "give +//! me the next sibling element". +//! +//! Finally, the matching engine ties all of this together with +//! `matches_selector()`. This takes an opaque representation of an +//! element, plus a selector, and returns a bool. We iterate through +//! the rules in the stylesheets and gather the matches; then sort the +//! matches by specificity and apply the result to each element. + +use cssparser::{ + self, match_ignore_ascii_case, parse_important, AtRuleParser, BasicParseErrorKind, CowRcStr, + DeclarationListParser, DeclarationParser, Parser, ParserInput, ParserState, + QualifiedRuleParser, RuleListParser, SourceLocation, ToCss, _cssparser_internal_to_lowercase, +}; +use data_url::mime::Mime; +use language_tags::LanguageTag; +use markup5ever::{self, namespace_url, ns, Namespace, QualName}; +use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; +use selectors::matching::{ElementSelectorFlags, MatchingContext, MatchingMode, QuirksMode}; +use selectors::{OpaqueElement, SelectorImpl, SelectorList}; +use std::cmp::Ordering; +use std::fmt; +use std::str; +use std::str::FromStr; + +use crate::error::*; +use crate::io::{self, BinaryData}; +use crate::node::{Node, NodeBorrow, NodeCascade}; +use crate::properties::{parse_value, ComputedValues, ParseAs, ParsedProperty}; +use crate::session::Session; +use crate::url_resolver::{AllowedUrl, UrlResolver}; + +/// A parsed CSS declaration +/// +/// For example, in the declaration `fill: green !important`, the +/// `prop_name` would be `fill`, the `property` would be +/// `ParsedProperty::Fill(...)` with the green value, and `important` +/// would be `true`. +pub struct Declaration { + pub prop_name: QualName, + pub property: ParsedProperty, + pub important: bool, +} + +/// Dummy struct required to use `cssparser::DeclarationListParser` +/// +/// It implements `cssparser::DeclarationParser`, which knows how to parse +/// the property/value pairs from a CSS declaration. +pub struct DeclParser; + +impl<'i> DeclarationParser<'i> for DeclParser { + type Declaration = Declaration; + type Error = ValueErrorKind; + + /// Parses a CSS declaration like `name: input_value [!important]` + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<Declaration, ParseError<'i>> { + let prop_name = QualName::new(None, ns!(), markup5ever::LocalName::from(name.as_ref())); + let property = parse_value(&prop_name, input, ParseAs::Property)?; + + let important = input.try_parse(parse_important).is_ok(); + + Ok(Declaration { + prop_name, + property, + important, + }) + } +} + +// cssparser's DeclarationListParser requires this; we just use the dummy +// implementations from cssparser itself. We may want to provide a real +// implementation in the future, although this may require keeping track of the +// CSS parsing state like Servo does. +impl<'i> AtRuleParser<'i> for DeclParser { + type Prelude = (); + type AtRule = Declaration; + type Error = ValueErrorKind; +} + +/// Struct to implement cssparser::QualifiedRuleParser and cssparser::AtRuleParser +pub struct RuleParser { + session: Session, +} + +/// Errors from the CSS parsing process +#[derive(Debug)] +pub enum ParseErrorKind<'i> { + Selector(selectors::parser::SelectorParseErrorKind<'i>), +} + +impl<'i> From<selectors::parser::SelectorParseErrorKind<'i>> for ParseErrorKind<'i> { + fn from(e: selectors::parser::SelectorParseErrorKind<'_>) -> ParseErrorKind<'_> { + ParseErrorKind::Selector(e) + } +} + +/// A CSS qualified rule (or ruleset) +pub struct QualifiedRule { + selectors: SelectorList<Selector>, + declarations: Vec<Declaration>, +} + +/// Prelude of at-rule used in the AtRuleParser. +pub enum AtRulePrelude { + Import(String), +} + +/// A CSS at-rule (or ruleset) +pub enum AtRule { + Import(String), +} + +/// A CSS rule (or ruleset) +pub enum Rule { + AtRule(AtRule), + QualifiedRule(QualifiedRule), +} + +// Required to implement the `Prelude` associated type in `cssparser::QualifiedRuleParser` +impl<'i> selectors::Parser<'i> for RuleParser { + type Impl = Selector; + type Error = ParseErrorKind<'i>; + + fn default_namespace(&self) -> Option<<Self::Impl as SelectorImpl>::NamespaceUrl> { + Some(ns!(svg)) + } + + fn namespace_for_prefix( + &self, + _prefix: &<Self::Impl as SelectorImpl>::NamespacePrefix, + ) -> Option<<Self::Impl as SelectorImpl>::NamespaceUrl> { + // FIXME: Do we need to keep a lookup table extracted from libxml2's + // XML namespaces? + // + // Or are CSS namespaces completely different, declared elsewhere? + None + } + fn parse_non_ts_pseudo_class( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result<NonTSPseudoClass, cssparser::ParseError<'i, Self::Error>> { + match &*name { + "link" => Ok(NonTSPseudoClass::Link), + "visited" => Ok(NonTSPseudoClass::Visited), + _ => Err(location.new_custom_error( + selectors::parser::SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name), + )), + } + } + fn parse_non_ts_functional_pseudo_class( + &self, + name: CowRcStr<'i>, + arguments: &mut Parser<'i, '_>, + ) -> Result<NonTSPseudoClass, cssparser::ParseError<'i, Self::Error>> { + match &*name { + "lang" => { + // Comma-separated lists of languages are a Selectors 4 feature, + // but a pretty stable one that hasn't changed in a long time. + let tags = arguments.parse_comma_separated(|arg| { + let language_tag = arg.expect_ident_or_string()?.clone(); + LanguageTag::from_str(&language_tag).map_err(|_| { + arg.new_custom_error(selectors::parser::SelectorParseErrorKind::UnsupportedPseudoClassOrElement(language_tag)) + }) + })?; + arguments.expect_exhausted()?; + Ok(NonTSPseudoClass::Lang(tags)) + } + _ => Err(arguments.new_custom_error( + selectors::parser::SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name), + )), + } + } +} + +// `cssparser::RuleListParser` is a struct which requires that we +// provide a type that implements `cssparser::QualifiedRuleParser`. +// +// In turn, `cssparser::QualifiedRuleParser` requires that we +// implement a way to parse the `Prelude` of a ruleset or rule. For +// example, in this ruleset: +// +// ```css +// foo, .bar { fill: red; stroke: green; } +// ``` +// +// The prelude is the selector list with the `foo` and `.bar` selectors. +// +// The `parse_prelude` method just uses `selectors::SelectorList`. This +// is what requires the `impl selectors::Parser for RuleParser`. +// +// Next, the `parse_block` method takes an already-parsed prelude (a selector list), +// and tries to parse the block between braces. It creates a `Rule` out of +// the selector list and the declaration list. +impl<'i> QualifiedRuleParser<'i> for RuleParser { + type Prelude = SelectorList<Selector>; + type QualifiedRule = Rule; + type Error = ParseErrorKind<'i>; + + fn parse_prelude<'t>( + &mut self, + input: &mut Parser<'i, 't>, + ) -> Result<Self::Prelude, cssparser::ParseError<'i, Self::Error>> { + SelectorList::parse(self, input) + } + + fn parse_block<'t>( + &mut self, + prelude: Self::Prelude, + _start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<Self::QualifiedRule, cssparser::ParseError<'i, Self::Error>> { + let declarations = DeclarationListParser::new(input, DeclParser) + .filter_map(|r| match r { + Ok(decl) => Some(decl), + Err(e) => { + rsvg_log!(self.session, "Invalid declaration; ignoring: {:?}", e); + None + } + }) + .collect(); + + Ok(Rule::QualifiedRule(QualifiedRule { + selectors: prelude, + declarations, + })) + } +} + +// Required by `cssparser::RuleListParser`. +// +// This only handles the `@import` at-rule. +impl<'i> AtRuleParser<'i> for RuleParser { + type Prelude = AtRulePrelude; + type AtRule = Rule; + type Error = ParseErrorKind<'i>; + + #[allow(clippy::type_complexity)] + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<Self::Prelude, cssparser::ParseError<'i, Self::Error>> { + match_ignore_ascii_case! { + &name, + + // FIXME: at the moment we ignore media queries + + "import" => { + let url = input.expect_url_or_string()?.as_ref().to_owned(); + Ok(AtRulePrelude::Import(url)) + }, + + _ => Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name))), + } + } + + fn rule_without_block( + &mut self, + prelude: Self::Prelude, + _start: &ParserState, + ) -> Result<Self::AtRule, ()> { + let AtRulePrelude::Import(url) = prelude; + Ok(Rule::AtRule(AtRule::Import(url))) + } + + // When we implement at-rules with blocks, implement the trait's parse_block() method here. +} + +/// Dummy type required by the SelectorImpl trait. +#[allow(clippy::upper_case_acronyms)] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum NonTSPseudoClass { + Link, + Visited, + Lang(Vec<LanguageTag>), +} + +impl ToCss for NonTSPseudoClass { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + match self { + NonTSPseudoClass::Link => write!(dest, "link"), + NonTSPseudoClass::Visited => write!(dest, "visited"), + NonTSPseudoClass::Lang(lang) => write!( + dest, + "lang(\"{}\")", + lang.iter() + .map(ToString::to_string) + .collect::<Vec<_>>() + .join("\",\"") + ), + } + } +} + +impl selectors::parser::NonTSPseudoClass for NonTSPseudoClass { + type Impl = Selector; + + fn is_active_or_hover(&self) -> bool { + false + } + + fn is_user_action_state(&self) -> bool { + false + } +} + +/// Dummy type required by the SelectorImpl trait +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PseudoElement; + +impl ToCss for PseudoElement { + fn to_css<W>(&self, _dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + Ok(()) + } +} + +impl selectors::parser::PseudoElement for PseudoElement { + type Impl = Selector; +} + +/// Holds all the types for the SelectorImpl trait +#[derive(Debug, Clone)] +pub struct Selector; + +/// Wrapper for attribute values. +/// +/// We use a newtype because the associated type Selector::AttrValue +/// must implement `From<&str>` and `ToCss`, which are foreign traits. +/// +/// The `derive` requirements come from the `selectors` crate. +#[derive(Clone, PartialEq, Eq)] +pub struct AttributeValue(String); + +impl From<&str> for AttributeValue { + fn from(s: &str) -> AttributeValue { + AttributeValue(s.to_owned()) + } +} + +impl ToCss for AttributeValue { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + use std::fmt::Write; + + write!(cssparser::CssStringWriter::new(dest), "{}", &self.0) + } +} + +impl AsRef<str> for AttributeValue { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +/// Wrapper for identifier values. +/// +/// Used to implement `ToCss` on the `LocalName` foreign type. +#[derive(Clone, PartialEq, Eq)] +pub struct Identifier(markup5ever::LocalName); + +impl From<&str> for Identifier { + fn from(s: &str) -> Identifier { + Identifier(markup5ever::LocalName::from(s)) + } +} + +impl ToCss for Identifier { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + cssparser::serialize_identifier(&self.0, dest) + } +} + +/// Wrapper for local names. +/// +/// Used to implement `ToCss` on the `LocalName` foreign type. +#[derive(Clone, PartialEq, Eq)] +pub struct LocalName(markup5ever::LocalName); + +impl From<&str> for LocalName { + fn from(s: &str) -> LocalName { + LocalName(markup5ever::LocalName::from(s)) + } +} + +impl ToCss for LocalName { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + cssparser::serialize_identifier(&self.0, dest) + } +} + +/// Wrapper for namespace prefixes. +/// +/// Used to implement `ToCss` on the `markup5ever::Prefix` foreign type. +#[derive(Clone, Default, PartialEq, Eq)] +pub struct NamespacePrefix(markup5ever::Prefix); + +impl From<&str> for NamespacePrefix { + fn from(s: &str) -> NamespacePrefix { + NamespacePrefix(markup5ever::Prefix::from(s)) + } +} + +impl ToCss for NamespacePrefix { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + cssparser::serialize_identifier(&self.0, dest) + } +} + +impl SelectorImpl for Selector { + type ExtraMatchingData = (); + type AttrValue = AttributeValue; + type Identifier = Identifier; + type LocalName = LocalName; + type NamespaceUrl = Namespace; + type NamespacePrefix = NamespacePrefix; + type BorrowedNamespaceUrl = Namespace; + type BorrowedLocalName = LocalName; + type NonTSPseudoClass = NonTSPseudoClass; + type PseudoElement = PseudoElement; +} + +/// Newtype wrapper around `Node` so we can implement [`selectors::Element`] for it. +/// +/// `Node` is an alias for [`rctree::Node`], so we can't implement +/// `selectors::Element` directly on it. We implement it on the +/// `RsvgElement` wrapper instead. +#[derive(Clone, PartialEq)] +pub struct RsvgElement(Node); + +impl From<Node> for RsvgElement { + fn from(n: Node) -> RsvgElement { + RsvgElement(n) + } +} + +impl fmt::Debug for RsvgElement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.borrow()) + } +} + +// The selectors crate uses this to examine our tree of elements. +impl selectors::Element for RsvgElement { + type Impl = Selector; + + /// Converts self into an opaque representation. + fn opaque(&self) -> OpaqueElement { + OpaqueElement::new(&self.0.borrow()) + } + + fn parent_element(&self) -> Option<Self> { + self.0.parent().map(|n| n.into()) + } + + /// Whether the parent node of this element is a shadow root. + fn parent_node_is_shadow_root(&self) -> bool { + // unsupported + false + } + + /// The host of the containing shadow root, if any. + fn containing_shadow_host(&self) -> Option<Self> { + // unsupported + None + } + + /// Whether we're matching on a pseudo-element. + fn is_pseudo_element(&self) -> bool { + // unsupported + false + } + + /// Skips non-element nodes + fn prev_sibling_element(&self) -> Option<Self> { + let mut sibling = self.0.previous_sibling(); + + while let Some(ref sib) = sibling { + if sib.is_element() { + return sibling.map(|n| n.into()); + } + + sibling = sib.previous_sibling(); + } + + None + } + + /// Skips non-element nodes + fn next_sibling_element(&self) -> Option<Self> { + let mut sibling = self.0.next_sibling(); + + while let Some(ref sib) = sibling { + if sib.is_element() { + return sibling.map(|n| n.into()); + } + + sibling = sib.next_sibling(); + } + + None + } + + fn is_html_element_in_html_document(&self) -> bool { + false + } + + fn has_local_name(&self, local_name: &LocalName) -> bool { + self.0.borrow_element().element_name().local == local_name.0 + } + + /// Empty string for no namespace + fn has_namespace(&self, ns: &Namespace) -> bool { + self.0.borrow_element().element_name().ns == *ns + } + + /// Whether this element and the `other` element have the same local name and namespace. + fn is_same_type(&self, other: &Self) -> bool { + self.0.borrow_element().element_name() == other.0.borrow_element().element_name() + } + + fn attr_matches( + &self, + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&AttributeValue>, + ) -> bool { + self.0 + .borrow_element() + .get_attributes() + .iter() + .find(|(attr, _)| { + // do we have an attribute that matches the namespace and local_name? + match *ns { + NamespaceConstraint::Any => local_name.0 == attr.local, + NamespaceConstraint::Specific(ns) => { + QualName::new(None, ns.clone(), local_name.0.clone()) == *attr + } + } + }) + .map(|(_, value)| { + // we have one; does the attribute's value match the expected operation? + operation.eval_str(value) + }) + .unwrap_or(false) + } + + fn match_non_ts_pseudo_class<F>( + &self, + pc: &<Self::Impl as SelectorImpl>::NonTSPseudoClass, + _context: &mut MatchingContext<'_, Self::Impl>, + _flags_setter: &mut F, + ) -> bool + where + F: FnMut(&Self, ElementSelectorFlags), + { + match pc { + NonTSPseudoClass::Link => self.is_link(), + NonTSPseudoClass::Visited => false, + NonTSPseudoClass::Lang(css_lang) => self + .0 + .borrow_element() + .get_computed_values() + .xml_lang() + .0 + .as_ref() + .map_or(false, |e_lang| css_lang.iter().any(|l| l.matches(e_lang))), + } + } + + fn match_pseudo_element( + &self, + _pe: &<Self::Impl as SelectorImpl>::PseudoElement, + _context: &mut MatchingContext<'_, Self::Impl>, + ) -> bool { + // unsupported + false + } + + /// Whether this element is a `link`. + fn is_link(&self) -> bool { + // Style as link only if href is specified at all. + // + // The SVG and CSS specifications do not seem to clearly + // say what happens when you have an `<svg:a>` tag with no + // `(xlink:|svg:)href` attribute. However, both Firefox and Chromium + // consider a bare `<svg:a>` element with no href to be NOT + // a link, so to avoid nasty surprises, we do the same. + // Empty href's, however, ARE considered links. + self.0.is_element() + && match *self.0.borrow_element_data() { + crate::element::ElementData::Link(ref link) => link.link.is_some(), + _ => false, + } + } + + /// Returns whether the element is an HTML `<slot>` element. + fn is_html_slot_element(&self) -> bool { + false + } + + fn has_id(&self, id: &Identifier, case_sensitivity: CaseSensitivity) -> bool { + self.0 + .borrow_element() + .get_id() + .map(|self_id| case_sensitivity.eq(self_id.as_bytes(), id.0.as_bytes())) + .unwrap_or(false) + } + + fn has_class(&self, name: &Identifier, case_sensitivity: CaseSensitivity) -> bool { + self.0 + .borrow_element() + .get_class() + .map(|classes| { + classes + .split_whitespace() + .any(|class| case_sensitivity.eq(class.as_bytes(), name.0.as_bytes())) + }) + .unwrap_or(false) + } + + fn imported_part(&self, _name: &Identifier) -> Option<Identifier> { + // unsupported + None + } + + fn is_part(&self, _name: &Identifier) -> bool { + // unsupported + false + } + + /// Returns whether this element matches `:empty`. + /// + /// That is, whether it does not contain any child element or any non-zero-length text node. + /// See <http://dev.w3.org/csswg/selectors-3/#empty-pseudo>. + fn is_empty(&self) -> bool { + // .all() returns true for the empty iterator + self.0 + .children() + .all(|child| child.is_chars() && child.borrow_chars().is_empty()) + } + + /// Returns whether this element matches `:root`, + /// i.e. whether it is the root element of a document. + /// + /// Note: this can be false even if `.parent_element()` is `None` + /// if the parent node is a `DocumentFragment`. + fn is_root(&self) -> bool { + self.0.parent().is_none() + } +} + +/// Origin for a stylesheet, per CSS 2.2. +/// +/// This is used when sorting selector matches according to their origin and specificity. +/// +/// CSS2.2: <https://www.w3.org/TR/CSS22/cascade.html#cascading-order> +#[derive(Copy, Clone, Eq, Ord, PartialEq, PartialOrd)] +pub enum Origin { + UserAgent, + User, + Author, +} + +/// A parsed CSS stylesheet. +pub struct Stylesheet { + origin: Origin, + qualified_rules: Vec<QualifiedRule>, +} + +/// A match during the selector matching process +/// +/// This struct comes from [`Stylesheet::get_matches`], and represents +/// that a certain node matched a CSS rule which has a selector with a +/// certain `specificity`. The stylesheet's `origin` is also given here. +/// +/// This type implements [`Ord`] so a list of `Match` can be sorted. +/// That implementation does ordering based on origin and specificity +/// as per <https://www.w3.org/TR/CSS22/cascade.html#cascading-order>. +struct Match<'a> { + specificity: u32, + origin: Origin, + declaration: &'a Declaration, +} + +impl<'a> Ord for Match<'a> { + fn cmp(&self, other: &Self) -> Ordering { + match self.origin.cmp(&other.origin) { + Ordering::Equal => self.specificity.cmp(&other.specificity), + o => o, + } + } +} + +impl<'a> PartialOrd for Match<'a> { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl<'a> PartialEq for Match<'a> { + fn eq(&self, other: &Self) -> bool { + self.origin == other.origin && self.specificity == other.specificity + } +} + +impl<'a> Eq for Match<'a> {} + +impl Stylesheet { + fn empty(origin: Origin) -> Stylesheet { + Stylesheet { + origin, + qualified_rules: Vec::new(), + } + } + + /// Parses a new stylesheet from CSS data in a string. + /// + /// The `url_resolver_url` is required for `@import` rules, so that librsvg can determine if + /// the requested path is allowed. + pub fn from_data( + buf: &str, + url_resolver: &UrlResolver, + origin: Origin, + session: Session, + ) -> Result<Self, LoadingError> { + let mut stylesheet = Stylesheet::empty(origin); + stylesheet.add_rules_from_string(buf, url_resolver, session)?; + Ok(stylesheet) + } + + /// Parses a new stylesheet by loading CSS data from a URL. + pub fn from_href( + aurl: &AllowedUrl, + origin: Origin, + session: Session, + ) -> Result<Self, LoadingError> { + let mut stylesheet = Stylesheet::empty(origin); + stylesheet.load(aurl, session)?; + Ok(stylesheet) + } + + /// Parses the CSS rules in `buf` and appends them to the stylesheet. + /// + /// The `url_resolver_url` is required for `@import` rules, so that librsvg can determine if + /// the requested path is allowed. + /// + /// If there is an `@import` rule, its rules will be recursively added into the + /// stylesheet, in the order in which they appear. + fn add_rules_from_string( + &mut self, + buf: &str, + url_resolver: &UrlResolver, + session: Session, + ) -> Result<(), LoadingError> { + let mut input = ParserInput::new(buf); + let mut parser = Parser::new(&mut input); + let rule_parser = RuleParser { + session: session.clone(), + }; + + RuleListParser::new_for_stylesheet(&mut parser, rule_parser) + .filter_map(|r| match r { + Ok(rule) => Some(rule), + Err(e) => { + rsvg_log!(session, "Invalid rule; ignoring: {:?}", e); + None + } + }) + .for_each(|rule| match rule { + Rule::AtRule(AtRule::Import(url)) => match url_resolver.resolve_href(&url) { + Ok(aurl) => { + // ignore invalid imports + let _ = self.load(&aurl, session.clone()); + } + + Err(e) => { + rsvg_log!(session, "Not loading stylesheet from \"{}\": {}", url, e); + } + }, + + Rule::QualifiedRule(qr) => self.qualified_rules.push(qr), + }); + + Ok(()) + } + + /// Parses a stylesheet referenced by an URL + fn load(&mut self, aurl: &AllowedUrl, session: Session) -> Result<(), LoadingError> { + io::acquire_data(aurl, None) + .map_err(LoadingError::from) + .and_then(|data| { + let BinaryData { + data: bytes, + mime_type, + } = data; + + if is_text_css(&mime_type) { + Ok(bytes) + } else { + rsvg_log!(session, "\"{}\" is not of type text/css; ignoring", aurl); + Err(LoadingError::BadCss) + } + }) + .and_then(|bytes| { + String::from_utf8(bytes).map_err(|_| { + rsvg_log!( + session, + "\"{}\" does not contain valid UTF-8 CSS data; ignoring", + aurl + ); + LoadingError::BadCss + }) + }) + .and_then(|utf8| { + let url = (**aurl).clone(); + self.add_rules_from_string(&utf8, &UrlResolver::new(Some(url)), session) + }) + } + + /// Appends the style declarations that match a specified node to a given vector + fn get_matches<'a>( + &'a self, + node: &Node, + match_ctx: &mut MatchingContext<'_, Selector>, + acc: &mut Vec<Match<'a>>, + ) { + for rule in &self.qualified_rules { + for selector in &rule.selectors.0 { + // This magic call is stolen from selectors::matching::matches_selector_list() + let matches = selectors::matching::matches_selector( + selector, + 0, + None, + &RsvgElement(node.clone()), + match_ctx, + &mut |_, _| {}, + ); + + if matches { + for decl in rule.declarations.iter() { + acc.push(Match { + declaration: decl, + specificity: selector.specificity(), + origin: self.origin, + }); + } + } + } + } + } +} + +fn is_text_css(mime_type: &Mime) -> bool { + mime_type.type_ == "text" && mime_type.subtype == "css" +} + +/// Runs the CSS cascade on the specified tree from all the stylesheets +pub fn cascade( + root: &mut Node, + ua_stylesheets: &[Stylesheet], + author_stylesheets: &[Stylesheet], + user_stylesheets: &[Stylesheet], + session: &Session, +) { + for mut node in root.descendants().filter(|n| n.is_element()) { + let mut matches = Vec::new(); + + // xml:lang needs to be inherited before selector matching, so it + // can't be done in the usual SpecifiedValues::to_computed_values, + // which is called by cascade() and runs after matching. + let parent = node.parent().clone(); + node.borrow_element_mut().inherit_xml_lang(parent); + + let mut match_ctx = MatchingContext::new( + MatchingMode::Normal, + // FIXME: how the fuck does one set up a bloom filter here? + None, + // n_index_cache, + None, + QuirksMode::NoQuirks, + ); + + for s in ua_stylesheets + .iter() + .chain(author_stylesheets) + .chain(user_stylesheets) + { + s.get_matches(&node, &mut match_ctx, &mut matches); + } + + matches.as_mut_slice().sort(); + + for m in matches { + node.borrow_element_mut() + .apply_style_declaration(m.declaration, m.origin); + } + + node.borrow_element_mut().set_style_attribute(session); + } + + let values = ComputedValues::default(); + root.cascade(&values); +} + +#[cfg(test)] +mod tests { + use super::*; + use selectors::Element; + + use crate::document::Document; + + #[test] + fn xml_lang() { + let document = Document::load_from_bytes( + br#"<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xml:lang="zh"> + <text id="a" x="10" y="10" width="30" height="30"></text> + <text id="b" x="10" y="20" width="30" height="30" xml:lang="en"></text> +</svg> +"#, + ); + let a = document.lookup_internal_node("a").unwrap(); + assert_eq!( + a.borrow_element() + .get_computed_values() + .xml_lang() + .0 + .unwrap() + .as_str(), + "zh" + ); + let b = document.lookup_internal_node("b").unwrap(); + assert_eq!( + b.borrow_element() + .get_computed_values() + .xml_lang() + .0 + .unwrap() + .as_str(), + "en" + ); + } + + #[test] + fn impl_element() { + let document = Document::load_from_bytes( + br#"<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" id="a"> + <rect id="b" x="10" y="10" width="30" height="30"/> + <circle id="c" cx="10" cy="10" r="10"/> + <rect id="d" class="foo bar"/> +</svg> +"#, + ); + + let a = document.lookup_internal_node("a").unwrap(); + let b = document.lookup_internal_node("b").unwrap(); + let c = document.lookup_internal_node("c").unwrap(); + let d = document.lookup_internal_node("d").unwrap(); + + // Node types + assert!(is_element_of_type!(a, Svg)); + assert!(is_element_of_type!(b, Rect)); + assert!(is_element_of_type!(c, Circle)); + assert!(is_element_of_type!(d, Rect)); + + let a = RsvgElement(a); + let b = RsvgElement(b); + let c = RsvgElement(c); + let d = RsvgElement(d); + + // Tree navigation + + assert_eq!(a.parent_element(), None); + assert_eq!(b.parent_element(), Some(a.clone())); + assert_eq!(c.parent_element(), Some(a.clone())); + assert_eq!(d.parent_element(), Some(a.clone())); + + assert_eq!(b.next_sibling_element(), Some(c.clone())); + assert_eq!(c.next_sibling_element(), Some(d.clone())); + assert_eq!(d.next_sibling_element(), None); + + assert_eq!(b.prev_sibling_element(), None); + assert_eq!(c.prev_sibling_element(), Some(b.clone())); + assert_eq!(d.prev_sibling_element(), Some(c.clone())); + + // Other operations + + assert!(a.has_local_name(&LocalName::from("svg"))); + + assert!(a.has_namespace(&ns!(svg))); + + assert!(!a.is_same_type(&b)); + assert!(b.is_same_type(&d)); + + assert!(a.has_id( + &Identifier::from("a"), + CaseSensitivity::AsciiCaseInsensitive + )); + assert!(!b.has_id( + &Identifier::from("foo"), + CaseSensitivity::AsciiCaseInsensitive + )); + + assert!(d.has_class( + &Identifier::from("foo"), + CaseSensitivity::AsciiCaseInsensitive + )); + assert!(d.has_class( + &Identifier::from("bar"), + CaseSensitivity::AsciiCaseInsensitive + )); + + assert!(!a.has_class( + &Identifier::from("foo"), + CaseSensitivity::AsciiCaseInsensitive + )); + + assert!(d.is_empty()); + assert!(!a.is_empty()); + } +} diff --git a/rsvg/src/dasharray.rs b/rsvg/src/dasharray.rs new file mode 100644 index 00000000..ae0e9488 --- /dev/null +++ b/rsvg/src/dasharray.rs @@ -0,0 +1,114 @@ +//! Parser for the `stroke-dasharray` property. + +use cssparser::Parser; + +use crate::error::*; +use crate::length::*; +use crate::parsers::{optional_comma, Parse}; + +#[derive(Debug, PartialEq, Clone)] +pub enum Dasharray { + None, + Array(Box<[ULength<Both>]>), +} + +enum_default!(Dasharray, Dasharray::None); + +impl Parse for Dasharray { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Dasharray, ParseError<'i>> { + if parser + .try_parse(|p| p.expect_ident_matching("none")) + .is_ok() + { + return Ok(Dasharray::None); + } + + let mut dasharray = Vec::new(); + + loop { + let d = ULength::<Both>::parse(parser)?; + dasharray.push(d); + + if parser.is_exhausted() { + break; + } + + optional_comma(parser); + } + + Ok(Dasharray::Array(dasharray.into_boxed_slice())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn dasharray(l: &[ULength<Both>]) -> Dasharray { + Dasharray::Array( + l.iter() + .cloned() + .collect::<Vec<ULength<Both>>>() + .into_boxed_slice(), + ) + } + + #[test] + fn parses_dash_array() { + // helper to cut down boilderplate + let length_parse = |s| ULength::<Both>::parse_str(s).unwrap(); + + let expected = dasharray(&[ + length_parse("1"), + length_parse("2in"), + length_parse("3"), + length_parse("4%"), + ]); + + let sample_1 = dasharray(&[length_parse("10"), length_parse("6")]); + + let sample_2 = dasharray(&[length_parse("5"), length_parse("5"), length_parse("20")]); + + let sample_3 = dasharray(&[ + length_parse("10px"), + length_parse("20px"), + length_parse("20px"), + ]); + + let sample_4 = dasharray(&[ + length_parse("25"), + length_parse("5"), + length_parse("5"), + length_parse("5"), + ]); + + let sample_5 = dasharray(&[length_parse("3.1415926"), length_parse("8")]); + let sample_6 = dasharray(&[length_parse("5"), length_parse("3.14")]); + let sample_7 = dasharray(&[length_parse("2")]); + + assert_eq!(Dasharray::parse_str("none").unwrap(), Dasharray::None); + assert_eq!(Dasharray::parse_str("1 2in,3 4%").unwrap(), expected); + assert_eq!(Dasharray::parse_str("10,6").unwrap(), sample_1); + assert_eq!(Dasharray::parse_str("5,5,20").unwrap(), sample_2); + assert_eq!(Dasharray::parse_str("10px 20px 20px").unwrap(), sample_3); + assert_eq!(Dasharray::parse_str("25 5 , 5 5").unwrap(), sample_4); + assert_eq!(Dasharray::parse_str("3.1415926,8").unwrap(), sample_5); + assert_eq!(Dasharray::parse_str("5, 3.14").unwrap(), sample_6); + assert_eq!(Dasharray::parse_str("2").unwrap(), sample_7); + + // Negative numbers + assert!(Dasharray::parse_str("20,40,-20").is_err()); + + // Empty dash_array + assert!(Dasharray::parse_str("").is_err()); + assert!(Dasharray::parse_str("\t \n ").is_err()); + assert!(Dasharray::parse_str(",,,").is_err()); + assert!(Dasharray::parse_str("10, \t, 20 \n").is_err()); + + // No trailing commas allowed, parse error + assert!(Dasharray::parse_str("10,").is_err()); + + // A comma should be followed by a number + assert!(Dasharray::parse_str("20,,10").is_err()); + } +} diff --git a/rsvg/src/document.rs b/rsvg/src/document.rs new file mode 100644 index 00000000..0327e769 --- /dev/null +++ b/rsvg/src/document.rs @@ -0,0 +1,667 @@ +//! Main SVG document structure. + +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::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!")] +}); + +/// Identifier of a node +#[derive(Debug, PartialEq, Clone)] +pub enum NodeId { + /// element id + Internal(String), + /// url, element id + External(String, String), +} + +impl NodeId { + pub fn parse(href: &str) -> Result<NodeId, NodeIdError> { + let (url, id) = match href.rfind('#') { + None => (Some(href), None), + Some(p) if p == 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}"), + } + } +} + +/// A loaded SVG file and its derived data. +pub struct Document { + /// Tree of nodes; the root is guaranteed to be an `<svg>` element. + tree: Node, + + /// Metadata about the SVG handle. + session: Session, + + /// Mapping from `id` attributes to nodes. + ids: HashMap<String, Node>, + + // The following two require interior mutability because we load the extern + // resources all over the place. Eventually we'll be able to do this + // once, at loading time, and keep this immutable. + /// SVG documents referenced from this document. + externs: RefCell<Resources>, + + /// Image resources referenced from this document. + images: RefCell<Images>, + + /// Used to load referenced resources. + load_options: Arc<LoadOptions>, + + /// Stylesheets defined in the document. + stylesheets: Vec<Stylesheet>, +} + +impl Document { + /// Constructs a `Document` by loading it from a stream. + 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, + ) + } + + /// Utility function to load a document from a static string in tests. + #[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() + } + + /// Gets the root node. This is guaranteed to be an `<svg>` element. + pub fn root(&self) -> Node { + self.tree.clone() + } + + /// Looks up a node in this document or one of its resources by its `id` attribute. + 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(), + } + } + + /// Looks up a node in this document by its `id` attribute. + pub fn lookup_internal_node(&self, id: &str) -> Option<Node> { + self.ids.get(id).map(|n| (*n).clone()) + } + + /// Loads an image by URL, or returns a pre-loaded one. + 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) + } + + /// Runs the CSS cascade on the document tree + /// + /// This uses the default UserAgent stylesheet, the document's internal stylesheets, + /// plus an extra set of stylesheets supplied by the caller. + 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(); + // FIXME: pass a cancellable to these + 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> { + // See issue #548 - data: URLs without a MIME-type automatically + // fall back to "text/plain;charset=US-ASCII". Some (old?) versions of + // Adobe Illustrator generate data: URLs without MIME-type for image + // data. We'll catch this and fall back to sniffing by unsetting the + // content_type. + 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" { + // avoid printing a huge data: URL for image 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 + } +} + +/// Detects circular references between nodes, and enforces referencing limits. +/// +/// Consider this fragment of SVG: +/// +/// ```xml +/// <pattern id="foo"> +/// <rect width="1" height="1" fill="url(#foo)"/> +/// </pattern> +/// ``` +/// +/// The pattern has a child element that references the pattern itself. This kind of circular +/// reference is invalid. The `AcquiredNodes` struct is passed around +/// wherever it may be necessary to resolve references to nodes, or to access nodes +/// "elsewhere" in the DOM that is not the current subtree. +/// +/// Also, such constructs that reference other elements can be maliciously arranged like +/// in the [billion laughs attack][lol], to cause huge amounts of CPU to be consumed through +/// creating an exponential number of references. `AcquiredNodes` imposes a hard limit on +/// the number of references that can be resolved for typical, well-behaved SVG documents. +/// +/// The [`Self::acquire()`] and [`Self::acquire_ref()`] methods return an [`AcquiredNode`], which +/// acts like a smart pointer for a [`Node`]. Once a node has been acquired, it cannot be +/// acquired again until its [`AcquiredNode`] is dropped. In the example above, a graphic element +/// would acquire the `pattern`, which would then acquire its `rect` child, which then would fail +/// to re-acquired the `pattern` — thus signaling a circular reference. +/// +/// Those methods return an [`AcquireError`] to signal circular references. Also, they +/// can return [`AcquireError::MaxReferencesExceeded`] if the aforementioned referencing limit +/// is exceeded. +/// +/// [lol]: https://bitbucket.org/tiran/defusedxml +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) + } + + /// Acquires a node by its id. + /// + /// This is typically used during an "early resolution" stage, when XML `id`s are being + /// resolved to node references. + pub fn acquire(&mut self, node_id: &NodeId) -> Result<AcquiredNode, AcquireError> { + self.num_elements_acquired += 1; + + // This is a mitigation for SVG files that try to instance a huge number of + // elements via <use>, recursive patterns, etc. See limits.rs for details. + if self.num_elements_acquired > limits::MAX_REFERENCED_ELEMENTS { + return Err(AcquireError::MaxReferencesExceeded); + } + + // FIXME: callers shouldn't have to know that get_node() can initiate a file load. + // Maybe we should have the following stages: + // - load main SVG XML + // + // - load secondary SVG XML and other files like images; all document::Resources and + // document::Images loaded + // + // - Now that all files are loaded, resolve URL references + let node = self + .document + .lookup_node(node_id) + .ok_or_else(|| AcquireError::LinkNotFound(node_id.clone()))?; + + if !node.is_element() { + return Err(AcquireError::InvalidLinkType(node_id.clone())); + } + + if node.borrow_element().is_accessed_by_reference() { + self.acquire_ref(&node) + } else { + Ok(AcquiredNode { stack: None, node }) + } + } + + /// Acquires a node whose reference is already known. + /// + /// This is useful for cases where a node is initially referenced by its id with + /// [`Self::acquire`] and kept around for later use. During the later use, the node + /// needs to be re-acquired with this method. For example: + /// + /// * At an "early resolution" stage, `acquire()` a pattern by its id, and keep around its + /// [`Node`] reference. + /// + /// * At the drawing stage, `acquire_ref()` the pattern node that we already had, so that + /// its child elements that reference other paint servers will be able to detect circular + /// references to the pattern. + 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(), + }) + } + } +} + +/// Keeps a stack of nodes and can check if a certain node is contained in the stack +/// +/// Sometimes parts of the code cannot plainly use the implicit stack of acquired +/// nodes as maintained by DrawingCtx::acquire_node(), and they must keep their +/// own stack of nodes to test for reference cycles. NodeStack can be used to do that. +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) + } +} + +/// Used to build a tree of SVG nodes while an XML document is being read. +/// +/// This struct holds the document-related state while loading an SVG document from XML: +/// the loading options, the partially-built tree of nodes, the CSS stylesheets that +/// appear while loading the document. +/// +/// The XML loader asks a `DocumentBuilder` to +/// [`append_element`][DocumentBuilder::append_element], +/// [`append_characters`][DocumentBuilder::append_characters], etc. When all the XML has +/// been consumed, the caller can use [`build`][DocumentBuilder::build] to get a +/// fully-loaded [`Document`]. +pub struct DocumentBuilder { + /// Metadata for the document's lifetime. + session: Session, + + /// Loading options; mainly the URL resolver. + load_options: Arc<LoadOptions>, + + /// Root node of the tree. + tree: Option<Node>, + + /// Mapping from `id` attributes to nodes. + ids: HashMap<String, Node>, + + /// Stylesheets defined in the document. + 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(), + } + } + + /// Adds a stylesheet in order to the document. + /// + /// Stylesheets will later be matched in the order in which they were added. + pub fn append_stylesheet(&mut self, stylesheet: Stylesheet) { + self.stylesheets.push(stylesheet); + } + + /// Creates an element of the specified `name` as a child of `parent`. + /// + /// This is the main function to create new SVG elements while parsing XML. + /// + /// `name` is the XML element's name, for example `rect`. + /// + /// `attrs` has the XML element's attributes, e.g. cx/cy/r for `<circle cx="0" cy="0" + /// r="5">`. + /// + /// If `parent` is `None` it means that we are creating the root node in the tree of + /// elements. The code will later validate that this is indeed an `<svg>` element. + 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() { + // This is so we don't overwrite an existing 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 + } + + /// Creates a node for an XML text element as a child of `parent`. + pub fn append_characters(&mut self, text: &str, parent: &mut Node) { + if !text.is_empty() { + // When the last child is a Chars node we can coalesce + // the text and avoid screwing up the Pango layouts + 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))); + }; + } + } + + /// Does the final validation on the `Document` being read, and returns it. + 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() { + // Issue #548 + 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() { + // Issue #699 + let mime = Mime::from_str("image/png;charset=utf-8").unwrap(); + assert_eq!( + content_type_for_gdk_pixbuf(&mime), + Some(String::from("image/png")) + ); + } +} diff --git a/rsvg/src/dpi.rs b/rsvg/src/dpi.rs new file mode 100644 index 00000000..e205ec75 --- /dev/null +++ b/rsvg/src/dpi.rs @@ -0,0 +1,13 @@ +//! Resolution for rendering (dots per inch = DPI). + +#[derive(Debug, Copy, Clone)] +pub struct Dpi { + pub x: f64, + pub y: f64, +} + +impl Dpi { + pub fn new(x: f64, y: f64) -> Dpi { + Dpi { x, y } + } +} 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 + ], + ); + } +} diff --git a/rsvg/src/element.rs b/rsvg/src/element.rs new file mode 100644 index 00000000..77e5539c --- /dev/null +++ b/rsvg/src/element.rs @@ -0,0 +1,651 @@ +//! SVG Elements. + +use markup5ever::{expanded_name, local_name, namespace_url, ns, QualName}; +use once_cell::sync::Lazy; +use std::collections::{HashMap, HashSet}; +use std::fmt; + +use crate::accept_language::UserLanguage; +use crate::bbox::BoundingBox; +use crate::cond::{RequiredExtensions, RequiredFeatures, SystemLanguage}; +use crate::css::{Declaration, Origin}; +use crate::document::AcquiredNodes; +use crate::drawing_ctx::{DrawingCtx, Viewport}; +use crate::error::*; +use crate::filter::Filter; +use crate::filters::{ + blend::FeBlend, + color_matrix::FeColorMatrix, + component_transfer::{FeComponentTransfer, FeFuncA, FeFuncB, FeFuncG, FeFuncR}, + composite::FeComposite, + convolve_matrix::FeConvolveMatrix, + displacement_map::FeDisplacementMap, + drop_shadow::FeDropShadow, + flood::FeFlood, + gaussian_blur::FeGaussianBlur, + image::FeImage, + lighting::{FeDiffuseLighting, FeDistantLight, FePointLight, FeSpecularLighting, FeSpotLight}, + merge::{FeMerge, FeMergeNode}, + morphology::FeMorphology, + offset::FeOffset, + tile::FeTile, + turbulence::FeTurbulence, + FilterEffect, +}; +use crate::gradient::{LinearGradient, RadialGradient, Stop}; +use crate::image::Image; +use crate::marker::Marker; +use crate::node::*; +use crate::pattern::Pattern; +use crate::properties::{ComputedValues, SpecifiedValues}; +use crate::session::Session; +use crate::shapes::{Circle, Ellipse, Line, Path, Polygon, Polyline, Rect}; +use crate::structure::{ClipPath, Group, Link, Mask, NonRendering, Svg, Switch, Symbol, Use}; +use crate::style::Style; +use crate::text::{TRef, TSpan, Text}; +use crate::xml::Attributes; + +pub trait ElementTrait { + /// Sets per-element attributes. + /// + /// Each element is supposed to iterate the `attributes`, and parse any ones it needs. + /// SVG specifies that unknown attributes should be ignored, and known attributes with invalid + /// values should be ignored so that the attribute ends up with its "initial value". + /// + /// You can use the [`set_attribute`] function to do that. + fn set_attributes(&mut self, _attributes: &Attributes, _session: &Session) {} + + /// Draw an element. + /// + /// Each element is supposed to draw itself as needed. + fn draw( + &self, + _node: &Node, + _acquired_nodes: &mut AcquiredNodes<'_>, + _cascaded: &CascadedValues<'_>, + _viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + _clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + // by default elements don't draw themselves + Ok(draw_ctx.empty_bbox()) + } +} + +/// Sets `dest` if `parse_result` is `Ok()`, otherwise just logs the error. +/// +/// Implementations of the [`ElementTrait`] trait generally scan a list of attributes +/// for the ones they can handle, and parse their string values. Per the SVG spec, an attribute +/// with an invalid value should be ignored, and it should fall back to the default value. +/// +/// In librsvg, those default values are set in each element's implementation of the [`Default`] trait: +/// at element creation time, each element gets initialized to its `Default`, and then each attribute +/// gets parsed. This function will set that attribute's value only if parsing was successful. +/// +/// In case the `parse_result` is an error, this function will log an appropriate notice +/// via the [`Session`]. +pub fn set_attribute<T>(dest: &mut T, parse_result: Result<T, ElementError>, session: &Session) { + match parse_result { + Ok(v) => *dest = v, + Err(e) => { + // FIXME: this does not provide a clue of what was the problematic element. + // We need tracking of the current parsing position to do that. + rsvg_log!(session, "ignoring attribute with invalid value: {}", e); + } + } +} + +pub struct Element { + element_name: QualName, + attributes: Attributes, + specified_values: SpecifiedValues, + important_styles: HashSet<QualName>, + values: ComputedValues, + required_extensions: Option<RequiredExtensions>, + required_features: Option<RequiredFeatures>, + system_language: Option<SystemLanguage>, + pub element_data: ElementData, +} + +impl fmt::Display for Element { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.element_name().local)?; + write!(f, " id={}", self.get_id().unwrap_or("None"))?; + Ok(()) + } +} + +/// Parsed contents of an element node in the DOM. +/// +/// This enum uses `Box<Foo>` in order to make each variant the size of +/// a pointer. +pub enum ElementData { + Circle(Box<Circle>), + ClipPath(Box<ClipPath>), + Ellipse(Box<Ellipse>), + Filter(Box<Filter>), + Group(Box<Group>), + Image(Box<Image>), + Line(Box<Line>), + LinearGradient(Box<LinearGradient>), + Link(Box<Link>), + Marker(Box<Marker>), + Mask(Box<Mask>), + NonRendering(Box<NonRendering>), + Path(Box<Path>), + Pattern(Box<Pattern>), + Polygon(Box<Polygon>), + Polyline(Box<Polyline>), + RadialGradient(Box<RadialGradient>), + Rect(Box<Rect>), + Stop(Box<Stop>), + Style(Box<Style>), + Svg(Box<Svg>), + Switch(Box<Switch>), + Symbol(Box<Symbol>), + Text(Box<Text>), + TRef(Box<TRef>), + TSpan(Box<TSpan>), + Use(Box<Use>), + + // Filter primitives, these start with "Fe" as element names are e.g. "feBlend" + FeBlend(Box<FeBlend>), + FeColorMatrix(Box<FeColorMatrix>), + FeComponentTransfer(Box<FeComponentTransfer>), + FeComposite(Box<FeComposite>), + FeConvolveMatrix(Box<FeConvolveMatrix>), + FeDiffuseLighting(Box<FeDiffuseLighting>), + FeDisplacementMap(Box<FeDisplacementMap>), + FeDistantLight(Box<FeDistantLight>), + FeDropShadow(Box<FeDropShadow>), + FeFlood(Box<FeFlood>), + FeFuncA(Box<FeFuncA>), + FeFuncB(Box<FeFuncB>), + FeFuncG(Box<FeFuncG>), + FeFuncR(Box<FeFuncR>), + FeGaussianBlur(Box<FeGaussianBlur>), + FeImage(Box<FeImage>), + FeMerge(Box<FeMerge>), + FeMergeNode(Box<FeMergeNode>), + FeMorphology(Box<FeMorphology>), + FeOffset(Box<FeOffset>), + FePointLight(Box<FePointLight>), + FeSpecularLighting(Box<FeSpecularLighting>), + FeSpotLight(Box<FeSpotLight>), + FeTile(Box<FeTile>), + FeTurbulence(Box<FeTurbulence>), +} + +impl Element { + /// Takes an XML element name and consumes a list of attribute/value pairs to create an [`Element`]. + /// + /// This operation does not fail. Unknown element names simply produce a [`NonRendering`] + /// element. + pub fn new(session: &Session, name: &QualName, mut attributes: Attributes) -> Element { + let (create_fn, flags): (ElementDataCreateFn, ElementCreateFlags) = if name.ns == ns!(svg) { + match ELEMENT_CREATORS.get(name.local.as_ref()) { + // hack in the SVG namespace for supported element names + Some(&(create_fn, flags)) => (create_fn, flags), + + // Whenever we encounter a element name we don't understand, represent it as a + // non-rendering element. This is like a group, but it doesn't do any rendering + // of children. The effect is that we will ignore all children of unknown elements. + None => (create_non_rendering, ElementCreateFlags::Default), + } + } else { + (create_non_rendering, ElementCreateFlags::Default) + }; + + if flags == ElementCreateFlags::IgnoreClass { + attributes.clear_class(); + }; + + let element_data = create_fn(session, &attributes); + + let mut e = Self { + element_name: name.clone(), + attributes, + specified_values: Default::default(), + important_styles: Default::default(), + values: Default::default(), + required_extensions: Default::default(), + required_features: Default::default(), + system_language: Default::default(), + element_data, + }; + + e.set_conditional_processing_attributes(session); + e.set_presentation_attributes(session); + + e + } + + pub fn element_name(&self) -> &QualName { + &self.element_name + } + + pub fn get_attributes(&self) -> &Attributes { + &self.attributes + } + + pub fn get_id(&self) -> Option<&str> { + self.attributes.get_id() + } + + pub fn get_class(&self) -> Option<&str> { + self.attributes.get_class() + } + + pub fn inherit_xml_lang(&mut self, parent: Option<Node>) { + self.specified_values + .inherit_xml_lang(&mut self.values, parent); + } + + pub fn get_specified_values(&self) -> &SpecifiedValues { + &self.specified_values + } + + pub fn get_computed_values(&self) -> &ComputedValues { + &self.values + } + + pub fn set_computed_values(&mut self, values: &ComputedValues) { + self.values = values.clone(); + } + + pub fn get_cond(&self, user_language: &UserLanguage) -> bool { + self.required_extensions + .as_ref() + .map(|v| v.eval()) + .unwrap_or(true) + && self + .required_features + .as_ref() + .map(|v| v.eval()) + .unwrap_or(true) + && self + .system_language + .as_ref() + .map(|v| v.eval(user_language)) + .unwrap_or(true) + } + + fn set_conditional_processing_attributes(&mut self, session: &Session) { + for (attr, value) in self.attributes.iter() { + match attr.expanded() { + expanded_name!("", "requiredExtensions") => { + self.required_extensions = Some(RequiredExtensions::from_attribute(value)); + } + + expanded_name!("", "requiredFeatures") => { + self.required_features = Some(RequiredFeatures::from_attribute(value)); + } + + expanded_name!("", "systemLanguage") => { + self.system_language = Some(SystemLanguage::from_attribute(value, session)); + } + + _ => {} + } + } + } + + /// Hands the `attrs` to the node's state, to apply the presentation attributes. + fn set_presentation_attributes(&mut self, session: &Session) { + self.specified_values + .parse_presentation_attributes(session, &self.attributes); + } + + // Applies a style declaration to the node's specified_values + pub fn apply_style_declaration(&mut self, declaration: &Declaration, origin: Origin) { + self.specified_values.set_property_from_declaration( + declaration, + origin, + &mut self.important_styles, + ); + } + + /// Applies CSS styles from the "style" attribute + pub fn set_style_attribute(&mut self, session: &Session) { + let style = self + .attributes + .iter() + .find(|(attr, _)| attr.expanded() == expanded_name!("", "style")) + .map(|(_, value)| value); + + if let Some(style) = style { + self.specified_values.parse_style_declarations( + style, + Origin::Author, + &mut self.important_styles, + session, + ); + } + } + + #[rustfmt::skip] + pub fn as_filter_effect(&self) -> Option<&dyn FilterEffect> { + use ElementData::*; + + match &self.element_data { + FeBlend(fe) => Some(&**fe), + FeColorMatrix(fe) => Some(&**fe), + FeComponentTransfer(fe) => Some(&**fe), + FeComposite(fe) => Some(&**fe), + FeConvolveMatrix(fe) => Some(&**fe), + FeDiffuseLighting(fe) => Some(&**fe), + FeDisplacementMap(fe) => Some(&**fe), + FeDropShadow(fe) => Some(&**fe), + FeFlood(fe) => Some(&**fe), + FeGaussianBlur(fe) => Some(&**fe), + FeImage(fe) => Some(&**fe), + FeMerge(fe) => Some(&**fe), + FeMorphology(fe) => Some(&**fe), + FeOffset(fe) => Some(&**fe), + FeSpecularLighting(fe) => Some(&**fe), + FeTile(fe) => Some(&**fe), + FeTurbulence(fe) => Some(&**fe), + _ => None, + } + } + + /// Returns whether an element of a particular type is only accessed by reference + // from other elements' attributes. The element could in turn cause other nodes + // to get referenced, potentially causing reference cycles. + pub fn is_accessed_by_reference(&self) -> bool { + use ElementData::*; + + matches!( + self.element_data, + ClipPath(_) + | Filter(_) + | LinearGradient(_) + | Marker(_) + | Mask(_) + | Pattern(_) + | RadialGradient(_) + ) + } + + /// The main drawing function for elements. + pub fn draw( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + let values = cascaded.get(); + if values.is_displayed() { + self.element_data + .draw(node, acquired_nodes, cascaded, viewport, draw_ctx, clipping) + } else { + Ok(draw_ctx.empty_bbox()) + } + } +} + +impl ElementData { + /// Dispatcher for the draw method of concrete element implementations. + #[rustfmt::skip] + fn draw( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + use ElementData::*; + + let data: &dyn ElementTrait = match self { + Circle(d) => &**d, + ClipPath(d) => &**d, + Ellipse(d) => &**d, + Filter(d) => &**d, + Group(d) => &**d, + Image(d) => &**d, + Line(d) => &**d, + LinearGradient(d) => &**d, + Link(d) => &**d, + Marker(d) => &**d, + Mask(d) => &**d, + NonRendering(d) => &**d, + Path(d) => &**d, + Pattern(d) => &**d, + Polygon(d) => &**d, + Polyline(d) => &**d, + RadialGradient(d) => &**d, + Rect(d) => &**d, + Stop(d) => &**d, + Style(d) => &**d, + Svg(d) => &**d, + Switch(d) => &**d, + Symbol(d) => &**d, + Text(d) => &**d, + TRef(d) => &**d, + TSpan(d) => &**d, + Use(d) => &**d, + + FeBlend(d) => &**d, + FeColorMatrix(d) => &**d, + FeComponentTransfer(d) => &**d, + FeComposite(d) => &**d, + FeConvolveMatrix(d) => &**d, + FeDiffuseLighting(d) => &**d, + FeDisplacementMap(d) => &**d, + FeDistantLight(d) => &**d, + FeDropShadow(d) => &**d, + FeFlood(d) => &**d, + FeFuncA(d) => &**d, + FeFuncB(d) => &**d, + FeFuncG(d) => &**d, + FeFuncR(d) => &**d, + FeGaussianBlur(d) => &**d, + FeImage(d) => &**d, + FeMerge(d) => &**d, + FeMergeNode(d) => &**d, + FeMorphology(d) => &**d, + FeOffset(d) => &**d, + FePointLight(d) => &**d, + FeSpecularLighting(d) => &**d, + FeSpotLight(d) => &**d, + FeTile(d) => &**d, + FeTurbulence(d) => &**d, + }; + + data.draw(node, acquired_nodes, cascaded, viewport, draw_ctx, clipping) + } +} + +macro_rules! e { + ($name:ident, $element_type:ident) => { + pub fn $name(session: &Session, attributes: &Attributes) -> ElementData { + let mut payload = Box::<$element_type>::default(); + payload.set_attributes(attributes, session); + + ElementData::$element_type(payload) + } + }; +} + +#[rustfmt::skip] +mod creators { + use super::*; + + e!(create_circle, Circle); + e!(create_clip_path, ClipPath); + e!(create_defs, NonRendering); + e!(create_ellipse, Ellipse); + e!(create_fe_blend, FeBlend); + e!(create_fe_color_matrix, FeColorMatrix); + e!(create_fe_component_transfer, FeComponentTransfer); + e!(create_fe_func_a, FeFuncA); + e!(create_fe_func_b, FeFuncB); + e!(create_fe_func_g, FeFuncG); + e!(create_fe_func_r, FeFuncR); + e!(create_fe_composite, FeComposite); + e!(create_fe_convolve_matrix, FeConvolveMatrix); + e!(create_fe_diffuse_lighting, FeDiffuseLighting); + e!(create_fe_displacement_map, FeDisplacementMap); + e!(create_fe_distant_light, FeDistantLight); + e!(create_fe_drop_shadow, FeDropShadow); + e!(create_fe_flood, FeFlood); + e!(create_fe_gaussian_blur, FeGaussianBlur); + e!(create_fe_image, FeImage); + e!(create_fe_merge, FeMerge); + e!(create_fe_merge_node, FeMergeNode); + e!(create_fe_morphology, FeMorphology); + e!(create_fe_offset, FeOffset); + e!(create_fe_point_light, FePointLight); + e!(create_fe_specular_lighting, FeSpecularLighting); + e!(create_fe_spot_light, FeSpotLight); + e!(create_fe_tile, FeTile); + e!(create_fe_turbulence, FeTurbulence); + e!(create_filter, Filter); + e!(create_group, Group); + e!(create_image, Image); + e!(create_line, Line); + e!(create_linear_gradient, LinearGradient); + e!(create_link, Link); + e!(create_marker, Marker); + e!(create_mask, Mask); + e!(create_non_rendering, NonRendering); + e!(create_path, Path); + e!(create_pattern, Pattern); + e!(create_polygon, Polygon); + e!(create_polyline, Polyline); + e!(create_radial_gradient, RadialGradient); + e!(create_rect, Rect); + e!(create_stop, Stop); + e!(create_style, Style); + e!(create_svg, Svg); + e!(create_switch, Switch); + e!(create_symbol, Symbol); + e!(create_text, Text); + e!(create_tref, TRef); + e!(create_tspan, TSpan); + e!(create_use, Use); + + /* Hack to make multiImage sort-of work + * + * disabled for now, as markup5ever doesn't have local names for + * multiImage, subImage, subImageRef. Maybe we can just... create them ourselves? + * + * Is multiImage even in SVG2? + */ + /* + e!(create_multi_image, Switch); + e!(create_sub_image, Group); + e!(create_sub_image_ref, Image); + */ +} + +use creators::*; + +type ElementDataCreateFn = fn(session: &Session, attributes: &Attributes) -> ElementData; + +#[derive(Copy, Clone, PartialEq)] +enum ElementCreateFlags { + Default, + IgnoreClass, +} + +// Lines in comments are elements that we don't support. +#[rustfmt::skip] +static ELEMENT_CREATORS: Lazy<HashMap<&'static str, (ElementDataCreateFn, ElementCreateFlags)>> = Lazy::new(|| { + use ElementCreateFlags::*; + + let creators_table: Vec<(&str, ElementDataCreateFn, ElementCreateFlags)> = vec![ + // name, supports_class, create_fn + ("a", create_link, Default), + /* ("altGlyph", ), */ + /* ("altGlyphDef", ), */ + /* ("altGlyphItem", ), */ + /* ("animate", ), */ + /* ("animateColor", ), */ + /* ("animateMotion", ), */ + /* ("animateTransform", ), */ + ("circle", create_circle, Default), + ("clipPath", create_clip_path, Default), + /* ("color-profile", ), */ + /* ("cursor", ), */ + ("defs", create_defs, Default), + /* ("desc", ), */ + ("ellipse", create_ellipse, Default), + ("feBlend", create_fe_blend, Default), + ("feColorMatrix", create_fe_color_matrix, Default), + ("feComponentTransfer", create_fe_component_transfer, Default), + ("feComposite", create_fe_composite, Default), + ("feConvolveMatrix", create_fe_convolve_matrix, Default), + ("feDiffuseLighting", create_fe_diffuse_lighting, Default), + ("feDisplacementMap", create_fe_displacement_map, Default), + ("feDistantLight", create_fe_distant_light, IgnoreClass), + ("feDropShadow", create_fe_drop_shadow, Default), + ("feFuncA", create_fe_func_a, IgnoreClass), + ("feFuncB", create_fe_func_b, IgnoreClass), + ("feFuncG", create_fe_func_g, IgnoreClass), + ("feFuncR", create_fe_func_r, IgnoreClass), + ("feFlood", create_fe_flood, Default), + ("feGaussianBlur", create_fe_gaussian_blur, Default), + ("feImage", create_fe_image, Default), + ("feMerge", create_fe_merge, Default), + ("feMergeNode", create_fe_merge_node, IgnoreClass), + ("feMorphology", create_fe_morphology, Default), + ("feOffset", create_fe_offset, Default), + ("fePointLight", create_fe_point_light, IgnoreClass), + ("feSpecularLighting", create_fe_specular_lighting, Default), + ("feSpotLight", create_fe_spot_light, IgnoreClass), + ("feTile", create_fe_tile, Default), + ("feTurbulence", create_fe_turbulence, Default), + ("filter", create_filter, Default), + /* ("font", ), */ + /* ("font-face", ), */ + /* ("font-face-format", ), */ + /* ("font-face-name", ), */ + /* ("font-face-src", ), */ + /* ("font-face-uri", ), */ + /* ("foreignObject", ), */ + ("g", create_group, Default), + /* ("glyph", ), */ + /* ("glyphRef", ), */ + /* ("hkern", ), */ + ("image", create_image, Default), + ("line", create_line, Default), + ("linearGradient", create_linear_gradient, Default), + ("marker", create_marker, Default), + ("mask", create_mask, Default), + /* ("metadata", ), */ + /* ("missing-glyph", ), */ + /* ("mpath", ), */ + /* ("multiImage", ), */ + ("path", create_path, Default), + ("pattern", create_pattern, Default), + ("polygon", create_polygon, Default), + ("polyline", create_polyline, Default), + ("radialGradient", create_radial_gradient, Default), + ("rect", create_rect, Default), + /* ("script", ), */ + /* ("set", ), */ + ("stop", create_stop, Default), + ("style", create_style, IgnoreClass), + /* ("subImage", ), */ + /* ("subImageRef", ), */ + ("svg", create_svg, Default), + ("switch", create_switch, Default), + ("symbol", create_symbol, Default), + ("text", create_text, Default), + /* ("textPath", ), */ + /* ("title", ), */ + ("tref", create_tref, Default), + ("tspan", create_tspan, Default), + ("use", create_use, Default), + /* ("view", ), */ + /* ("vkern", ), */ + ]; + + creators_table.into_iter().map(|(n, c, f)| (n, (c, f))).collect() +}); diff --git a/rsvg/src/error.rs b/rsvg/src/error.rs new file mode 100644 index 00000000..60f53551 --- /dev/null +++ b/rsvg/src/error.rs @@ -0,0 +1,526 @@ +//! Error types. + +use std::error; +use std::fmt; + +use cssparser::{BasicParseError, BasicParseErrorKind, ParseErrorKind, ToCss}; +use markup5ever::QualName; + +use crate::document::NodeId; +use crate::io::IoError; +use crate::limits; +use crate::node::Node; + +/// A short-lived error. +/// +/// The lifetime of the error is the same as the `cssparser::ParserInput` that +/// was used to create a `cssparser::Parser`. That is, it is the lifetime of +/// the string data that is being parsed. +/// +/// The code flow will sometimes require preserving this error as a long-lived struct; +/// see the `impl<'i, O> AttributeResultExt<O> for Result<O, ParseError<'i>>` for that +/// purpose. +pub type ParseError<'i> = cssparser::ParseError<'i, ValueErrorKind>; + +/// A simple error which refers to an attribute's value +#[derive(Debug, Clone)] +pub enum ValueErrorKind { + /// A property with the specified name was not found + UnknownProperty, + + /// The value could not be parsed + Parse(String), + + // The value could be parsed, but is invalid + Value(String), +} + +impl ValueErrorKind { + pub fn parse_error(s: &str) -> ValueErrorKind { + ValueErrorKind::Parse(s.to_string()) + } + + pub fn value_error(s: &str) -> ValueErrorKind { + ValueErrorKind::Value(s.to_string()) + } +} + +impl fmt::Display for ValueErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + ValueErrorKind::UnknownProperty => write!(f, "unknown property name"), + + ValueErrorKind::Parse(ref s) => write!(f, "parse error: {s}"), + + ValueErrorKind::Value(ref s) => write!(f, "invalid value: {s}"), + } + } +} + +impl<'a> From<BasicParseError<'a>> for ValueErrorKind { + fn from(e: BasicParseError<'_>) -> ValueErrorKind { + let BasicParseError { kind, .. } = e; + + let msg = match kind { + BasicParseErrorKind::UnexpectedToken(_) => "unexpected token", + BasicParseErrorKind::EndOfInput => "unexpected end of input", + BasicParseErrorKind::AtRuleInvalid(_) => "invalid @-rule", + BasicParseErrorKind::AtRuleBodyInvalid => "invalid @-rule body", + BasicParseErrorKind::QualifiedRuleInvalid => "invalid qualified rule", + }; + + ValueErrorKind::parse_error(msg) + } +} + +/// A complete error for an attribute and its erroneous value +#[derive(Debug, Clone)] +pub struct ElementError { + pub attr: QualName, + pub err: ValueErrorKind, +} + +impl fmt::Display for ElementError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}: {}", self.attr.expanded(), self.err) + } +} + +/// Errors returned when looking up a resource by URL reference. +#[derive(Debug, Clone)] +pub enum DefsLookupErrorKind { + /// Error when parsing the id to lookup. + InvalidId, + + /// Used when the public API tries to look up an external URL, which is not allowed. + /// + /// This catches the case where a public API wants to be misused to access an external + /// resource. For example, `SvgHandle.has_sub("https://evil.com/phone_home#element_id")` will + /// fail with this error. + CannotLookupExternalReferences, + + /// For internal use only. + /// + // FIXME: this is returned internally from Handle.lookup_node(), and gets translated + // to Ok(false). Don't expose this internal code in the public API. + NotFound, +} + +impl fmt::Display for DefsLookupErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + DefsLookupErrorKind::InvalidId => write!(f, "invalid id"), + DefsLookupErrorKind::CannotLookupExternalReferences => { + write!(f, "cannot lookup references to elements in external files") + } + DefsLookupErrorKind::NotFound => write!(f, "not found"), + } + } +} + +/// Errors that can happen while rendering or measuring an SVG document. +#[non_exhaustive] +#[derive(Debug, Clone)] +pub enum RenderingError { + /// An error from the rendering backend. + Rendering(String), + + /// A particular implementation-defined limit was exceeded. + LimitExceeded(ImplementationLimit), + + /// A non-invertible transform was generated. + /// + /// This should not be a fatal error; we should catch it and just not render + /// the problematic element. + InvalidTransform, + + /// Tried to reference an SVG element that does not exist. + IdNotFound, + + /// Tried to reference an SVG element from a fragment identifier that is incorrect. + InvalidId(String), + + /// Not enough memory was available for rendering. + OutOfMemory(String), +} + +impl From<DefsLookupErrorKind> for RenderingError { + fn from(e: DefsLookupErrorKind) -> RenderingError { + match e { + DefsLookupErrorKind::NotFound => RenderingError::IdNotFound, + _ => RenderingError::InvalidId(format!("{e}")), + } + } +} + +impl From<InvalidTransform> for RenderingError { + fn from(_: InvalidTransform) -> RenderingError { + RenderingError::InvalidTransform + } +} + +impl error::Error for RenderingError {} + +impl fmt::Display for RenderingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + RenderingError::Rendering(ref s) => write!(f, "rendering error: {s}"), + RenderingError::LimitExceeded(ref l) => write!(f, "{l}"), + RenderingError::InvalidTransform => write!(f, "invalid transform"), + RenderingError::IdNotFound => write!(f, "element id not found"), + RenderingError::InvalidId(ref s) => write!(f, "invalid id: {s:?}"), + RenderingError::OutOfMemory(ref s) => write!(f, "out of memory: {s}"), + } + } +} + +impl From<cairo::Error> for RenderingError { + fn from(e: cairo::Error) -> RenderingError { + RenderingError::Rendering(format!("{e:?}")) + } +} + +/// Indicates that a transform is not invertible. +/// +/// This generally represents an error from [`crate::transform::ValidTransform::try_from`], which is what we use +/// to check affine transforms for validity. +#[derive(Debug, PartialEq)] +pub struct InvalidTransform; + +/// Errors from [`crate::document::AcquiredNodes`]. +pub enum AcquireError { + /// An element with the specified id was not found. + LinkNotFound(NodeId), + + InvalidLinkType(NodeId), + + /// A circular reference was detected; non-fatal error. + /// + /// Callers are expected to treat the offending element as invalid, for example + /// if a graphic element uses a pattern fill, but the pattern in turn includes + /// another graphic element that references the same pattern. + /// + /// ```xml + /// <pattern id="foo"> + /// <rect width="1" height="1" fill="url(#foo)"/> + /// </pattern> + /// ``` + CircularReference(Node), + + /// Too many referenced objects were resolved; fatal error. + /// + /// Callers are expected to exit as early as possible and return an error to + /// the public API. See [`ImplementationLimit::TooManyReferencedElements`] for details. + MaxReferencesExceeded, +} + +impl fmt::Display for AcquireError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + AcquireError::LinkNotFound(ref frag) => write!(f, "link not found: {frag}"), + + AcquireError::InvalidLinkType(ref frag) => { + write!(f, "link \"{frag}\" is to object of invalid type") + } + + AcquireError::CircularReference(ref node) => { + write!(f, "circular reference in node {node}") + } + + AcquireError::MaxReferencesExceeded => { + write!(f, "maximum number of references exceeded") + } + } + } +} + +/// Helper for converting `Result<O, E>` into `Result<O, ElementError>` +/// +/// A `ElementError` requires a `QualName` that corresponds to the attribute to which the +/// error refers, plus the actual `ValueErrorKind` that describes the error. However, +/// parsing functions for attribute value types will want to return their own kind of +/// error, instead of `ValueErrorKind`. If that particular error type has an `impl +/// From<FooError> for ValueErrorKind`, then this trait helps assign attribute values in +/// `set_atts()` methods as follows: +/// +/// ``` +/// # use rsvg::doctest_only::AttributeResultExt; +/// # use rsvg::doctest_only::ValueErrorKind; +/// # use rsvg::doctest_only::ElementError; +/// # use markup5ever::{QualName, Prefix, Namespace, LocalName}; +/// # type FooError = ValueErrorKind; +/// fn parse_foo(value: &str) -> Result<(), FooError> +/// # { Err(ValueErrorKind::value_error("test")) } +/// +/// // It is assumed that there is an impl From<FooError> for ValueErrorKind +/// # let attr = QualName::new( +/// # Some(Prefix::from("")), +/// # Namespace::from(""), +/// # LocalName::from(""), +/// # ); +/// let result = parse_foo("value").attribute(attr); +/// assert!(result.is_err()); +/// # Ok::<(), ElementError>(()) +/// ``` +/// +/// The call to `.attribute(attr)` converts the `Result` from `parse_foo()` into a full +/// `ElementError` with the provided `attr`. +pub trait AttributeResultExt<O> { + fn attribute(self, attr: QualName) -> Result<O, ElementError>; +} + +impl<O, E: Into<ValueErrorKind>> AttributeResultExt<O> for Result<O, E> { + fn attribute(self, attr: QualName) -> Result<O, ElementError> { + self.map_err(|e| e.into()) + .map_err(|err| ElementError { attr, err }) + } +} + +/// Turns a short-lived `ParseError` into a long-lived `ElementError` +impl<'i, O> AttributeResultExt<O> for Result<O, ParseError<'i>> { + fn attribute(self, attr: QualName) -> Result<O, ElementError> { + self.map_err(|e| { + // FIXME: eventually, here we'll want to preserve the location information + + let ParseError { + kind, + location: _location, + } = e; + + match kind { + ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(tok)) => { + let mut s = String::from("unexpected token '"); + tok.to_css(&mut s).unwrap(); // FIXME: what do we do with a fmt::Error? + s.push('\''); + + ElementError { + attr, + err: ValueErrorKind::Parse(s), + } + } + + ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput) => ElementError { + attr, + err: ValueErrorKind::parse_error("unexpected end of input"), + }, + + ParseErrorKind::Basic(_) => { + unreachable!("attribute parsers should not return errors for CSS rules") + } + + ParseErrorKind::Custom(err) => ElementError { attr, err }, + } + }) + } +} + +/// Errors returned when resolving an URL +#[derive(Debug, Clone)] +pub enum AllowedUrlError { + /// parsing error from `Url::parse()` + UrlParseError(url::ParseError), + + /// A base file/uri was not set + BaseRequired, + + /// Cannot reference a file with a different URI scheme from the base file + DifferentUriSchemes, + + /// Some scheme we don't allow loading + DisallowedScheme, + + /// The requested file is not in the same directory as the base file, + /// or in one directory below the base file. + NotSiblingOrChildOfBaseFile, + + /// Error when obtaining the file path or the base file path + InvalidPath, + + /// The base file cannot be the root of the file system + BaseIsRoot, + + /// Error when canonicalizing either the file path or the base file path + CanonicalizationError, +} + +impl fmt::Display for AllowedUrlError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + AllowedUrlError::UrlParseError(e) => write!(f, "URL parse error: {e}"), + AllowedUrlError::BaseRequired => write!(f, "base required"), + AllowedUrlError::DifferentUriSchemes => write!(f, "different URI schemes"), + AllowedUrlError::DisallowedScheme => write!(f, "disallowed scheme"), + AllowedUrlError::NotSiblingOrChildOfBaseFile => { + write!(f, "not sibling or child of base file") + } + AllowedUrlError::InvalidPath => write!(f, "invalid path"), + AllowedUrlError::BaseIsRoot => write!(f, "base is root"), + AllowedUrlError::CanonicalizationError => write!(f, "canonicalization error"), + } + } +} + +/// Errors returned when creating a `NodeId` out of a string +#[derive(Debug, Clone)] +pub enum NodeIdError { + NodeIdRequired, +} + +impl From<NodeIdError> for ValueErrorKind { + fn from(e: NodeIdError) -> ValueErrorKind { + match e { + NodeIdError::NodeIdRequired => { + ValueErrorKind::value_error("fragment identifier required") + } + } + } +} + +/// Errors that can happen while loading an SVG document. +/// +/// All of these codes are for unrecoverable errors that keep an SVG document from being +/// fully loaded and parsed. Note that SVG is very lenient with respect to document +/// structure and the syntax of CSS property values; most errors there will not lead to a +/// `LoadingError`. To see those errors, you may want to set the `RSVG_LOG=1` environment +/// variable. +/// +/// I/O errors get reported in the `Glib` variant, since librsvg uses GIO internally for +/// all input/output. +#[non_exhaustive] +#[derive(Debug, Clone)] +pub enum LoadingError { + /// XML syntax error. + XmlParseError(String), + + /// Not enough memory to load the document. + OutOfMemory(String), + + /// A malformed or disallowed URL was used. + BadUrl, + + /// An invalid stylesheet was used. + BadCss, + + /// There is no `<svg>` root element in the XML. + NoSvgRoot, + + /// I/O error. + Io(String), + + /// A particular implementation-defined limit was exceeded. + LimitExceeded(ImplementationLimit), + + /// Catch-all for loading errors. + Other(String), +} + +/// Errors for implementation-defined limits, to mitigate malicious SVG documents. +/// +/// These get emitted as `LoadingError::LimitExceeded` or `RenderingError::LimitExceeded`. +/// The limits are present to mitigate malicious SVG documents which may try to exhaust +/// all available memory, or which would use large amounts of CPU time. +#[non_exhaustive] +#[derive(Debug, Copy, Clone)] +pub enum ImplementationLimit { + /// Document exceeded the maximum number of times that elements + /// can be referenced through URL fragments. + /// + /// This is a mitigation for malicious documents that attempt to + /// consume exponential amounts of CPU time by creating millions + /// of references to SVG elements. For example, the `<use>` and + /// `<pattern>` elements allow referencing other elements, which + /// can in turn reference other elements. This can be used to + /// create documents which would require exponential amounts of + /// CPU time to be rendered. + /// + /// Librsvg deals with both cases by placing a limit on how many + /// references will be resolved during the SVG rendering process, + /// that is, how many `url(#foo)` will be resolved. + /// + /// These malicious documents are similar to the XML + /// [billion laughs attack], but done with SVG's referencing features. + /// + /// See issues + /// [#323](https://gitlab.gnome.org/GNOME/librsvg/issues/323) and + /// [#515](https://gitlab.gnome.org/GNOME/librsvg/issues/515) for + /// examples for the `<use>` and `<pattern>` elements, + /// respectively. + /// + /// [billion laughs attack]: https://bitbucket.org/tiran/defusedxml + TooManyReferencedElements, + + /// Document exceeded the maximum number of elements that can be loaded. + /// + /// This is a mitigation for SVG files which create millions of + /// elements in an attempt to exhaust memory. Librsvg does not't + /// allow loading more than a certain number of elements during + /// the initial loading process. + TooManyLoadedElements, + + /// Document exceeded the number of attributes that can be attached to + /// an element. + /// + /// This is here because librsvg uses u16 to address attributes. It should + /// be essentially impossible to actually hit this limit, because the + /// number of attributes that the SVG standard ascribes meaning to are + /// lower than this limit. + TooManyAttributes, +} + +impl error::Error for LoadingError {} + +impl fmt::Display for LoadingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + LoadingError::XmlParseError(ref s) => write!(f, "XML parse error: {s}"), + LoadingError::OutOfMemory(ref s) => write!(f, "out of memory: {s}"), + LoadingError::BadUrl => write!(f, "invalid URL"), + LoadingError::BadCss => write!(f, "invalid CSS"), + LoadingError::NoSvgRoot => write!(f, "XML does not have <svg> root"), + LoadingError::Io(ref s) => write!(f, "I/O error: {s}"), + LoadingError::LimitExceeded(ref l) => write!(f, "{l}"), + LoadingError::Other(ref s) => write!(f, "{s}"), + } + } +} + +impl From<glib::Error> for LoadingError { + fn from(e: glib::Error) -> LoadingError { + // FIXME: this is somewhat fishy; not all GError are I/O errors, but in librsvg + // most GError do come from gio. Some come from GdkPixbufLoader, though. + LoadingError::Io(format!("{e}")) + } +} + +impl From<IoError> for LoadingError { + fn from(e: IoError) -> LoadingError { + match e { + IoError::BadDataUrl => LoadingError::BadUrl, + IoError::Glib(e) => LoadingError::Io(format!("{e}")), + } + } +} + +impl fmt::Display for ImplementationLimit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + ImplementationLimit::TooManyReferencedElements => write!( + f, + "exceeded more than {} referenced elements", + limits::MAX_REFERENCED_ELEMENTS + ), + + ImplementationLimit::TooManyLoadedElements => write!( + f, + "cannot load more than {} XML elements", + limits::MAX_LOADED_ELEMENTS + ), + + ImplementationLimit::TooManyAttributes => write!( + f, + "cannot load more than {} XML attributes", + limits::MAX_LOADED_ATTRIBUTES + ), + } + } +} diff --git a/rsvg/src/filter.rs b/rsvg/src/filter.rs new file mode 100644 index 00000000..2e591b1e --- /dev/null +++ b/rsvg/src/filter.rs @@ -0,0 +1,332 @@ +//! The `filter` element. + +use cssparser::{Parser, RGBA}; +use markup5ever::{expanded_name, local_name, namespace_url, ns}; +use std::slice::Iter; + +use crate::coord_units::CoordUnits; +use crate::document::{AcquiredNodes, NodeId}; +use crate::drawing_ctx::{DrawingCtx, Viewport}; +use crate::element::{set_attribute, ElementData, ElementTrait}; +use crate::error::ValueErrorKind; +use crate::filter_func::FilterFunction; +use crate::filters::{FilterResolveError, FilterSpec}; +use crate::length::*; +use crate::node::{Node, NodeBorrow}; +use crate::parsers::{Parse, ParseValue}; +use crate::rect::Rect; +use crate::session::Session; +use crate::xml::Attributes; + +/// The `<filter>` node. +pub struct Filter { + x: Length<Horizontal>, + y: Length<Vertical>, + width: ULength<Horizontal>, + height: ULength<Vertical>, + filter_units: CoordUnits, + primitive_units: CoordUnits, +} + +/// A `<filter>` element definition in user-space coordinates. +pub struct UserSpaceFilter { + pub rect: Rect, + pub filter_units: CoordUnits, + pub primitive_units: CoordUnits, +} + +impl Default for Filter { + /// Constructs a new `Filter` with default properties. + fn default() -> Self { + Self { + x: Length::<Horizontal>::parse_str("-10%").unwrap(), + y: Length::<Vertical>::parse_str("-10%").unwrap(), + width: ULength::<Horizontal>::parse_str("120%").unwrap(), + height: ULength::<Vertical>::parse_str("120%").unwrap(), + filter_units: CoordUnits::ObjectBoundingBox, + primitive_units: CoordUnits::UserSpaceOnUse, + } + } +} + +impl Filter { + pub fn get_filter_units(&self) -> CoordUnits { + self.filter_units + } + + pub fn to_user_space(&self, params: &NormalizeParams) -> UserSpaceFilter { + let x = self.x.to_user(params); + let y = self.y.to_user(params); + let w = self.width.to_user(params); + let h = self.height.to_user(params); + + let rect = Rect::new(x, y, x + w, y + h); + + UserSpaceFilter { + rect, + filter_units: self.filter_units, + primitive_units: self.primitive_units, + } + } +} + +impl ElementTrait for Filter { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "filterUnits") => { + set_attribute(&mut self.filter_units, attr.parse(value), session) + } + expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), + expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), + expanded_name!("", "width") => { + set_attribute(&mut self.width, attr.parse(value), session) + } + expanded_name!("", "height") => { + set_attribute(&mut self.height, attr.parse(value), session) + } + expanded_name!("", "primitiveUnits") => { + set_attribute(&mut self.primitive_units, attr.parse(value), session) + } + _ => (), + } + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum FilterValue { + Url(NodeId), + Function(FilterFunction), +} + +impl FilterValue { + pub fn to_filter_spec( + &self, + acquired_nodes: &mut AcquiredNodes<'_>, + user_space_params: &NormalizeParams, + current_color: RGBA, + viewport: &Viewport, + draw_ctx: &DrawingCtx, + node_being_filtered_name: &str, + ) -> Result<FilterSpec, FilterResolveError> { + match *self { + FilterValue::Url(ref node_id) => filter_spec_from_filter_node( + acquired_nodes, + viewport, + draw_ctx, + node_id, + node_being_filtered_name, + ), + + FilterValue::Function(ref func) => { + Ok(func.to_filter_spec(user_space_params, current_color)) + } + } + } +} + +/// Holds the viewport parameters for both objectBoundingBox and userSpaceOnUse units. +/// +/// When collecting a set of filter primitives (`feFoo`) into a [`FilterSpec`], which is +/// in user space, we need to convert each primitive's units into user space units. So, +/// pre-compute both cases and pass them around. +/// +/// This struct needs a better name; I didn't want to make it seem specific to filters by +/// calling `FiltersViewport` or `FilterCollectionProcessViewport`. Maybe the +/// original [`Viewport`] should be this struct, with both cases included... +struct ViewportGen { + object_bounding_box: Viewport, + user_space_on_use: Viewport, +} + +impl ViewportGen { + pub fn new(viewport: &Viewport) -> Self { + ViewportGen { + object_bounding_box: viewport.with_units(CoordUnits::ObjectBoundingBox), + user_space_on_use: viewport.with_units(CoordUnits::UserSpaceOnUse), + } + } + + fn get(&self, units: CoordUnits) -> &Viewport { + match units { + CoordUnits::ObjectBoundingBox => &self.object_bounding_box, + CoordUnits::UserSpaceOnUse => &self.user_space_on_use, + } + } +} + +fn extract_filter_from_filter_node( + filter_node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + session: &Session, + filter_view_params: &ViewportGen, +) -> Result<FilterSpec, FilterResolveError> { + assert!(is_element_of_type!(filter_node, Filter)); + + let filter_element = filter_node.borrow_element(); + + let user_space_filter = { + let filter_values = filter_element.get_computed_values(); + + let filter = borrow_element_as!(filter_node, Filter); + + filter.to_user_space(&NormalizeParams::new( + filter_values, + filter_view_params.get(filter.get_filter_units()), + )) + }; + + let primitive_view_params = filter_view_params.get(user_space_filter.primitive_units); + + let primitive_nodes = filter_node + .children() + .filter(|c| c.is_element()) + // Keep only filter primitives (those that implement the Filter trait) + .filter(|c| c.borrow_element().as_filter_effect().is_some()); + + let mut user_space_primitives = Vec::new(); + + for primitive_node in primitive_nodes { + let elt = primitive_node.borrow_element(); + let effect = elt.as_filter_effect().unwrap(); + + let primitive_name = format!("{primitive_node}"); + + let primitive_values = elt.get_computed_values(); + let params = NormalizeParams::new(primitive_values, primitive_view_params); + + let primitives = match effect.resolve(acquired_nodes, &primitive_node) { + Ok(primitives) => primitives, + Err(e) => { + rsvg_log!( + session, + "(filter primitive {} returned an error: {})", + primitive_name, + e + ); + return Err(e); + } + }; + + for p in primitives { + user_space_primitives.push(p.into_user_space(¶ms)); + } + } + + Ok(FilterSpec { + user_space_filter, + primitives: user_space_primitives, + }) +} + +fn filter_spec_from_filter_node( + acquired_nodes: &mut AcquiredNodes<'_>, + viewport: &Viewport, + draw_ctx: &DrawingCtx, + node_id: &NodeId, + node_being_filtered_name: &str, +) -> Result<FilterSpec, FilterResolveError> { + let session = draw_ctx.session().clone(); + + let filter_view_params = ViewportGen::new(viewport); + + acquired_nodes + .acquire(node_id) + .map_err(|e| { + rsvg_log!( + session, + "element {} will not be filtered with \"{}\": {}", + node_being_filtered_name, + node_id, + e + ); + FilterResolveError::ReferenceToNonFilterElement + }) + .and_then(|acquired| { + let node = acquired.get(); + + match *node.borrow_element_data() { + ElementData::Filter(_) => extract_filter_from_filter_node( + node, + acquired_nodes, + &session, + &filter_view_params, + ), + + _ => { + rsvg_log!( + session, + "element {} will not be filtered since \"{}\" is not a filter", + node_being_filtered_name, + node_id, + ); + Err(FilterResolveError::ReferenceToNonFilterElement) + } + } + }) +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct FilterValueList(Vec<FilterValue>); + +impl FilterValueList { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn iter(&self) -> Iter<'_, FilterValue> { + self.0.iter() + } +} + +impl Parse for FilterValueList { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, crate::error::ParseError<'i>> { + let mut result = FilterValueList::default(); + + loop { + let loc = parser.current_source_location(); + + let filter_value = if let Ok(func) = parser.try_parse(|p| FilterFunction::parse(p)) { + FilterValue::Function(func) + } else { + let url = parser.expect_url()?; + let node_id = NodeId::parse(&url) + .map_err(|e| loc.new_custom_error(ValueErrorKind::from(e)))?; + + FilterValue::Url(node_id) + }; + + result.0.push(filter_value); + + if parser.is_exhausted() { + break; + } + } + + Ok(result) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_filter_value_list() { + let n1 = NodeId::External("foo.svg".to_string(), "bar".to_string()); + let n2 = NodeId::External("test.svg".to_string(), "baz".to_string()); + assert_eq!( + FilterValueList::parse_str("url(foo.svg#bar) url(test.svg#baz)").unwrap(), + FilterValueList(vec![FilterValue::Url(n1), FilterValue::Url(n2)]) + ); + } + + #[test] + fn detects_invalid_filter_value_list() { + assert!(FilterValueList::parse_str("none").is_err()); + assert!(FilterValueList::parse_str("").is_err()); + assert!(FilterValueList::parse_str("fail").is_err()); + assert!(FilterValueList::parse_str("url(#test) none").is_err()); + } +} diff --git a/rsvg/src/filter_func.rs b/rsvg/src/filter_func.rs new file mode 100644 index 00000000..a6611bf8 --- /dev/null +++ b/rsvg/src/filter_func.rs @@ -0,0 +1,959 @@ +//! SVG2 filter function shortcuts - `blur()`, `brightness()`, etc. +//! +//! The `<filter>` element from SVG1.1 (also present in SVG2) uses some verbose XML to +//! define chains of filter primitives. In SVG2, there is a shortcut form of the `filter` +//! attribute and property, where one can simply say `filter="blur(5)"` and get the +//! equivalent of writing a full `<filter>` with a `<feGaussianBlur>` element. +//! +//! This module has a type for each of the filter functions in SVG2 with the function's +//! parameters, for example [`Blur`] stores the blur's standard deviation parameter. +//! +//! Those types get aggregated in the [`FilterFunction`] enum. A [`FilterFunction`] can +//! then convert itself into a [`FilterSpec`], which is ready to be rendered on a surface. + +use cssparser::{Color, Parser, RGBA}; + +use crate::angle::Angle; +use crate::error::*; +use crate::filter::Filter; +use crate::filters::{ + color_matrix::ColorMatrix, + component_transfer::{self, FeFuncA, FeFuncB, FeFuncCommon, FeFuncG, FeFuncR}, + composite::{Composite, Operator}, + flood::Flood, + gaussian_blur::GaussianBlur, + merge::{Merge, MergeNode}, + offset::Offset, + FilterSpec, Input, Primitive, PrimitiveParams, ResolvedPrimitive, +}; +use crate::length::*; +use crate::paint_server::resolve_color; +use crate::parsers::{CustomIdent, NumberOptionalNumber, NumberOrPercentage, Parse}; +use crate::unit_interval::UnitInterval; + +/// CSS Filter functions from the Filter Effects Module Level 1 +/// +/// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#filter-functions> +#[derive(Debug, Clone, PartialEq)] +pub enum FilterFunction { + Blur(Blur), + Brightness(Brightness), + Contrast(Contrast), + DropShadow(DropShadow), + Grayscale(Grayscale), + HueRotate(HueRotate), + Invert(Invert), + Opacity(Opacity), + Saturate(Saturate), + Sepia(Sepia), +} + +/// Parameters for the `blur()` filter function +/// +/// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#funcdef-filter-blur> +#[derive(Debug, Clone, PartialEq)] +pub struct Blur { + std_deviation: Option<Length<Both>>, +} + +/// Parameters for the `brightness()` filter function +/// +/// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#funcdef-filter-brightness> +#[derive(Debug, Clone, PartialEq)] +pub struct Brightness { + proportion: Option<f64>, +} + +/// Parameters for the `contrast()` filter function +/// +/// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#funcdef-filter-contrast> +#[derive(Debug, Clone, PartialEq)] +pub struct Contrast { + proportion: Option<f64>, +} + +/// Parameters for the `drop-shadow()` filter function +/// +/// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#funcdef-filter-drop-shadow> +#[derive(Debug, Clone, PartialEq)] +pub struct DropShadow { + color: Option<Color>, + dx: Option<Length<Horizontal>>, + dy: Option<Length<Vertical>>, + std_deviation: Option<ULength<Both>>, +} + +/// Parameters for the `grayscale()` filter function +/// +/// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#funcdef-filter-grayscale> +#[derive(Debug, Clone, PartialEq)] +pub struct Grayscale { + proportion: Option<f64>, +} + +/// Parameters for the `hue-rotate()` filter function +/// +/// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#funcdef-filter-huerotate> +#[derive(Debug, Clone, PartialEq)] +pub struct HueRotate { + angle: Option<Angle>, +} + +/// Parameters for the `invert()` filter function +/// +/// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#funcdef-filter-invert> +#[derive(Debug, Clone, PartialEq)] +pub struct Invert { + proportion: Option<f64>, +} + +/// Parameters for the `opacity()` filter function +/// +/// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#funcdef-filter-opacity> +#[derive(Debug, Clone, PartialEq)] +pub struct Opacity { + proportion: Option<f64>, +} + +/// Parameters for the `saturate()` filter function +/// +/// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#funcdef-filter-saturate> +#[derive(Debug, Clone, PartialEq)] +pub struct Saturate { + proportion: Option<f64>, +} + +/// Parameters for the `sepia()` filter function +/// +/// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#funcdef-filter-sepia> +#[derive(Debug, Clone, PartialEq)] +pub struct Sepia { + proportion: Option<f64>, +} + +/// Reads an optional number or percentage from the parser. +/// Negative numbers are not allowed. +fn parse_num_or_percentage(parser: &mut Parser<'_, '_>) -> Option<f64> { + match parser.try_parse(|p| NumberOrPercentage::parse(p)) { + Ok(NumberOrPercentage { value }) if value < 0.0 => None, + Ok(NumberOrPercentage { value }) => Some(value), + Err(_) => None, + } +} + +/// Reads an optional number or percentage from the parser, returning a value clamped to [0, 1]. +/// Negative numbers are not allowed. +fn parse_num_or_percentage_clamped(parser: &mut Parser<'_, '_>) -> Option<f64> { + parse_num_or_percentage(parser).map(|value| value.clamp(0.0, 1.0)) +} + +fn parse_function<'i, F>( + parser: &mut Parser<'i, '_>, + name: &str, + f: F, +) -> Result<FilterFunction, ParseError<'i>> +where + F: for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<FilterFunction, ParseError<'i>>, +{ + parser.expect_function_matching(name)?; + parser.parse_nested_block(f) +} + +// This function doesn't fail, but returns a Result like the other parsers, so tell Clippy +// about that. +#[allow(clippy::unnecessary_wraps)] +fn parse_blur<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, ParseError<'i>> { + let length = parser.try_parse(|p| Length::parse(p)).ok(); + + Ok(FilterFunction::Blur(Blur { + std_deviation: length, + })) +} + +#[allow(clippy::unnecessary_wraps)] +fn parse_brightness<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, ParseError<'i>> { + let proportion = parse_num_or_percentage(parser); + + Ok(FilterFunction::Brightness(Brightness { proportion })) +} + +#[allow(clippy::unnecessary_wraps)] +fn parse_contrast<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, ParseError<'i>> { + let proportion = parse_num_or_percentage(parser); + + Ok(FilterFunction::Contrast(Contrast { proportion })) +} +#[allow(clippy::unnecessary_wraps)] +fn parse_dropshadow<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, ParseError<'i>> { + let mut result = DropShadow { + color: None, + dx: None, + dy: None, + std_deviation: None, + }; + + result.color = parser.try_parse(Color::parse).ok(); + + // if dx is provided, dy must follow and an optional std_dev must follow that. + if let Ok(dx) = parser.try_parse(Length::parse) { + result.dx = Some(dx); + result.dy = Some(parser.try_parse(Length::parse)?); + result.std_deviation = parser.try_parse(ULength::parse).ok(); + } + + let loc = parser.current_source_location(); + + // because the color and length arguments can be provided in either order, + // check again after potentially parsing lengths if the color is now provided. + // if a color is provided both before and after, that is an error. + if let Ok(c) = parser.try_parse(Color::parse) { + if result.color.is_some() { + return Err( + loc.new_custom_error(ValueErrorKind::Value("color already specified".to_string())) + ); + } else { + result.color = Some(c); + } + } + + Ok(FilterFunction::DropShadow(result)) +} + +#[allow(clippy::unnecessary_wraps)] +fn parse_grayscale<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, ParseError<'i>> { + let proportion = parse_num_or_percentage_clamped(parser); + + Ok(FilterFunction::Grayscale(Grayscale { proportion })) +} + +#[allow(clippy::unnecessary_wraps)] +fn parse_huerotate<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, ParseError<'i>> { + let angle = parser.try_parse(|p| Angle::parse(p)).ok(); + + Ok(FilterFunction::HueRotate(HueRotate { angle })) +} + +#[allow(clippy::unnecessary_wraps)] +fn parse_invert<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, ParseError<'i>> { + let proportion = parse_num_or_percentage_clamped(parser); + + Ok(FilterFunction::Invert(Invert { proportion })) +} + +#[allow(clippy::unnecessary_wraps)] +fn parse_opacity<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, ParseError<'i>> { + let proportion = parse_num_or_percentage_clamped(parser); + + Ok(FilterFunction::Opacity(Opacity { proportion })) +} + +#[allow(clippy::unnecessary_wraps)] +fn parse_saturate<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, ParseError<'i>> { + let proportion = parse_num_or_percentage(parser); + + Ok(FilterFunction::Saturate(Saturate { proportion })) +} + +#[allow(clippy::unnecessary_wraps)] +fn parse_sepia<'i>(parser: &mut Parser<'i, '_>) -> Result<FilterFunction, ParseError<'i>> { + let proportion = parse_num_or_percentage_clamped(parser); + + Ok(FilterFunction::Sepia(Sepia { proportion })) +} + +impl Blur { + fn to_filter_spec(&self, params: &NormalizeParams) -> FilterSpec { + // The 0.0 default is from the spec + let std_dev = self.std_deviation.map(|l| l.to_user(params)).unwrap_or(0.0); + + let user_space_filter = Filter::default().to_user_space(params); + + let gaussian_blur = ResolvedPrimitive { + primitive: Primitive::default(), + params: PrimitiveParams::GaussianBlur(GaussianBlur { + std_deviation: NumberOptionalNumber(std_dev, std_dev), + ..GaussianBlur::default() + }), + } + .into_user_space(params); + + FilterSpec { + user_space_filter, + primitives: vec![gaussian_blur], + } + } +} + +impl Brightness { + fn to_filter_spec(&self, params: &NormalizeParams) -> FilterSpec { + let user_space_filter = Filter::default().to_user_space(params); + let slope = self.proportion.unwrap_or(1.0); + + let brightness = ResolvedPrimitive { + primitive: Primitive::default(), + params: PrimitiveParams::ComponentTransfer(component_transfer::ComponentTransfer { + functions: component_transfer::Functions { + r: FeFuncR(FeFuncCommon { + function_type: component_transfer::FunctionType::Linear, + slope, + ..FeFuncCommon::default() + }), + g: FeFuncG(FeFuncCommon { + function_type: component_transfer::FunctionType::Linear, + slope, + ..FeFuncCommon::default() + }), + b: FeFuncB(FeFuncCommon { + function_type: component_transfer::FunctionType::Linear, + slope, + ..FeFuncCommon::default() + }), + a: FeFuncA::default(), + }, + ..component_transfer::ComponentTransfer::default() + }), + } + .into_user_space(params); + + FilterSpec { + user_space_filter, + primitives: vec![brightness], + } + } +} + +impl Contrast { + fn to_filter_spec(&self, params: &NormalizeParams) -> FilterSpec { + let user_space_filter = Filter::default().to_user_space(params); + let slope = self.proportion.unwrap_or(1.0); + let intercept = -(0.5 * slope) + 0.5; + + let contrast = ResolvedPrimitive { + primitive: Primitive::default(), + params: PrimitiveParams::ComponentTransfer(component_transfer::ComponentTransfer { + functions: component_transfer::Functions { + r: FeFuncR(FeFuncCommon { + function_type: component_transfer::FunctionType::Linear, + slope, + intercept, + ..FeFuncCommon::default() + }), + g: FeFuncG(FeFuncCommon { + function_type: component_transfer::FunctionType::Linear, + slope, + intercept, + ..FeFuncCommon::default() + }), + b: FeFuncB(FeFuncCommon { + function_type: component_transfer::FunctionType::Linear, + slope, + intercept, + ..FeFuncCommon::default() + }), + a: FeFuncA::default(), + }, + ..component_transfer::ComponentTransfer::default() + }), + } + .into_user_space(params); + + FilterSpec { + user_space_filter, + primitives: vec![contrast], + } + } +} + +/// Creates the filter primitives required for a `feDropShadow` effect. +/// +/// Both the `drop-shadow()` filter function and the `feDropShadow` element need to create +/// a sequence of filter primitives (blur, offset, etc.) to build the drop shadow. This +/// function builds that sequence. +pub fn drop_shadow_primitives( + dx: f64, + dy: f64, + std_deviation: NumberOptionalNumber<f64>, + color: RGBA, +) -> Vec<ResolvedPrimitive> { + let offsetblur = CustomIdent("offsetblur".to_string()); + + let gaussian_blur = ResolvedPrimitive { + primitive: Primitive::default(), + params: PrimitiveParams::GaussianBlur(GaussianBlur { + in1: Input::SourceAlpha, + std_deviation, + ..GaussianBlur::default() + }), + }; + + let offset = ResolvedPrimitive { + primitive: Primitive { + result: Some(offsetblur.clone()), + ..Primitive::default() + }, + params: PrimitiveParams::Offset(Offset { + in1: Input::default(), + dx, + dy, + }), + }; + + let flood = ResolvedPrimitive { + primitive: Primitive::default(), + params: PrimitiveParams::Flood(Flood { color }), + }; + + let composite = ResolvedPrimitive { + primitive: Primitive::default(), + params: PrimitiveParams::Composite(Composite { + in2: Input::FilterOutput(offsetblur), + operator: Operator::In, + ..Composite::default() + }), + }; + + let merge = ResolvedPrimitive { + primitive: Primitive::default(), + params: PrimitiveParams::Merge(Merge { + merge_nodes: vec![ + MergeNode::default(), + MergeNode { + in1: Input::SourceGraphic, + ..MergeNode::default() + }, + ], + }), + }; + + vec![gaussian_blur, offset, flood, composite, merge] +} + +impl DropShadow { + /// Converts a DropShadow into the set of filter element primitives. + /// + /// See <https://www.w3.org/TR/filter-effects/#dropshadowEquivalent>. + fn to_filter_spec(&self, params: &NormalizeParams, default_color: RGBA) -> FilterSpec { + let user_space_filter = Filter::default().to_user_space(params); + let dx = self.dx.map(|l| l.to_user(params)).unwrap_or(0.0); + let dy = self.dy.map(|l| l.to_user(params)).unwrap_or(0.0); + let std_dev = self.std_deviation.map(|l| l.to_user(params)).unwrap_or(0.0); + let std_deviation = NumberOptionalNumber(std_dev, std_dev); + let color = self + .color + .as_ref() + .map(|c| resolve_color(c, UnitInterval::clamp(1.0), default_color)) + .unwrap_or(default_color); + + let resolved_primitives = drop_shadow_primitives(dx, dy, std_deviation, color); + + let primitives = resolved_primitives + .into_iter() + .map(|p| p.into_user_space(params)) + .collect(); + + FilterSpec { + user_space_filter, + primitives, + } + } +} + +impl Grayscale { + fn to_filter_spec(&self, params: &NormalizeParams) -> FilterSpec { + // grayscale is implemented as the inverse of a saturate operation, + // with the input clamped to the range [0, 1] by the parser. + let p = 1.0 - self.proportion.unwrap_or(1.0); + let saturate = Saturate { + proportion: Some(p), + }; + + saturate.to_filter_spec(params) + } +} + +impl HueRotate { + fn to_filter_spec(&self, params: &NormalizeParams) -> FilterSpec { + let rads = self.angle.map(|a| a.radians()).unwrap_or(0.0); + let user_space_filter = Filter::default().to_user_space(params); + + let huerotate = ResolvedPrimitive { + primitive: Primitive::default(), + params: PrimitiveParams::ColorMatrix(ColorMatrix { + matrix: ColorMatrix::hue_rotate_matrix(rads), + ..ColorMatrix::default() + }), + } + .into_user_space(params); + + FilterSpec { + user_space_filter, + primitives: vec![huerotate], + } + } +} + +impl Invert { + fn to_filter_spec(&self, params: &NormalizeParams) -> FilterSpec { + let p = self.proportion.unwrap_or(1.0); + let user_space_filter = Filter::default().to_user_space(params); + + let invert = ResolvedPrimitive { + primitive: Primitive::default(), + params: PrimitiveParams::ComponentTransfer(component_transfer::ComponentTransfer { + functions: component_transfer::Functions { + r: FeFuncR(FeFuncCommon { + function_type: component_transfer::FunctionType::Table, + table_values: vec![p, 1.0 - p], + ..FeFuncCommon::default() + }), + g: FeFuncG(FeFuncCommon { + function_type: component_transfer::FunctionType::Table, + table_values: vec![p, 1.0 - p], + ..FeFuncCommon::default() + }), + b: FeFuncB(FeFuncCommon { + function_type: component_transfer::FunctionType::Table, + table_values: vec![p, 1.0 - p], + ..FeFuncCommon::default() + }), + a: FeFuncA::default(), + }, + ..component_transfer::ComponentTransfer::default() + }), + } + .into_user_space(params); + + FilterSpec { + user_space_filter, + primitives: vec![invert], + } + } +} + +impl Opacity { + fn to_filter_spec(&self, params: &NormalizeParams) -> FilterSpec { + let p = self.proportion.unwrap_or(1.0); + let user_space_filter = Filter::default().to_user_space(params); + + let opacity = ResolvedPrimitive { + primitive: Primitive::default(), + params: PrimitiveParams::ComponentTransfer(component_transfer::ComponentTransfer { + functions: component_transfer::Functions { + a: FeFuncA(FeFuncCommon { + function_type: component_transfer::FunctionType::Table, + table_values: vec![0.0, p], + ..FeFuncCommon::default() + }), + ..component_transfer::Functions::default() + }, + ..component_transfer::ComponentTransfer::default() + }), + } + .into_user_space(params); + + FilterSpec { + user_space_filter, + primitives: vec![opacity], + } + } +} + +impl Saturate { + #[rustfmt::skip] + fn matrix(&self) -> nalgebra::Matrix5<f64> { + let p = self.proportion.unwrap_or(1.0); + + nalgebra::Matrix5::new( + 0.213 + 0.787 * p, 0.715 - 0.715 * p, 0.072 - 0.072 * p, 0.0, 0.0, + 0.213 - 0.213 * p, 0.715 + 0.285 * p, 0.072 - 0.072 * p, 0.0, 0.0, + 0.213 - 0.213 * p, 0.715 - 0.715 * p, 0.072 + 0.928 * p, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0, + ) + } + + fn to_filter_spec(&self, params: &NormalizeParams) -> FilterSpec { + let user_space_filter = Filter::default().to_user_space(params); + + let saturate = ResolvedPrimitive { + primitive: Primitive::default(), + params: PrimitiveParams::ColorMatrix(ColorMatrix { + matrix: self.matrix(), + ..ColorMatrix::default() + }), + } + .into_user_space(params); + + FilterSpec { + user_space_filter, + primitives: vec![saturate], + } + } +} + +impl Sepia { + #[rustfmt::skip] + fn matrix(&self) -> nalgebra::Matrix5<f64> { + let p = self.proportion.unwrap_or(1.0); + + nalgebra::Matrix5::new( + 0.393 + 0.607 * (1.0 - p), 0.769 - 0.769 * (1.0 - p), 0.189 - 0.189 * (1.0 - p), 0.0, 0.0, + 0.349 - 0.349 * (1.0 - p), 0.686 + 0.314 * (1.0 - p), 0.168 - 0.168 * (1.0 - p), 0.0, 0.0, + 0.272 - 0.272 * (1.0 - p), 0.534 - 0.534 * (1.0 - p), 0.131 + 0.869 * (1.0 - p), 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0, + ) + } + + fn to_filter_spec(&self, params: &NormalizeParams) -> FilterSpec { + let user_space_filter = Filter::default().to_user_space(params); + + let sepia = ResolvedPrimitive { + primitive: Primitive::default(), + params: PrimitiveParams::ColorMatrix(ColorMatrix { + matrix: self.matrix(), + ..ColorMatrix::default() + }), + } + .into_user_space(params); + + FilterSpec { + user_space_filter, + primitives: vec![sepia], + } + } +} + +impl Parse for FilterFunction { + #[allow(clippy::type_complexity)] + #[rustfmt::skip] + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, crate::error::ParseError<'i>> { + let loc = parser.current_source_location(); + let fns: Vec<(&str, &dyn Fn(&mut Parser<'i, '_>) -> _)> = vec![ + ("blur", &parse_blur), + ("brightness", &parse_brightness), + ("contrast", &parse_contrast), + ("drop-shadow", &parse_dropshadow), + ("grayscale", &parse_grayscale), + ("hue-rotate", &parse_huerotate), + ("invert", &parse_invert), + ("opacity", &parse_opacity), + ("saturate", &parse_saturate), + ("sepia", &parse_sepia), + ]; + + for (filter_name, parse_fn) in fns { + if let Ok(func) = parser.try_parse(|p| parse_function(p, filter_name, parse_fn)) { + return Ok(func); + } + } + + return Err(loc.new_custom_error(ValueErrorKind::parse_error("expected filter function"))); + } +} + +impl FilterFunction { + // If this function starts actually returning an Err, remove this Clippy exception: + #[allow(clippy::unnecessary_wraps)] + pub fn to_filter_spec(&self, params: &NormalizeParams, current_color: RGBA) -> FilterSpec { + match self { + FilterFunction::Blur(v) => v.to_filter_spec(params), + FilterFunction::Brightness(v) => v.to_filter_spec(params), + FilterFunction::Contrast(v) => v.to_filter_spec(params), + FilterFunction::DropShadow(v) => v.to_filter_spec(params, current_color), + FilterFunction::Grayscale(v) => v.to_filter_spec(params), + FilterFunction::HueRotate(v) => v.to_filter_spec(params), + FilterFunction::Invert(v) => v.to_filter_spec(params), + FilterFunction::Opacity(v) => v.to_filter_spec(params), + FilterFunction::Saturate(v) => v.to_filter_spec(params), + FilterFunction::Sepia(v) => v.to_filter_spec(params), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_blur() { + assert_eq!( + FilterFunction::parse_str("blur()").unwrap(), + FilterFunction::Blur(Blur { + std_deviation: None + }) + ); + + assert_eq!( + FilterFunction::parse_str("blur(5px)").unwrap(), + FilterFunction::Blur(Blur { + std_deviation: Some(Length::new(5.0, LengthUnit::Px)) + }) + ); + } + + #[test] + fn parses_brightness() { + assert_eq!( + FilterFunction::parse_str("brightness()").unwrap(), + FilterFunction::Brightness(Brightness { proportion: None }) + ); + + assert_eq!( + FilterFunction::parse_str("brightness(50%)").unwrap(), + FilterFunction::Brightness(Brightness { + proportion: Some(0.50_f32.into()), + }) + ); + } + + #[test] + fn parses_contrast() { + assert_eq!( + FilterFunction::parse_str("contrast()").unwrap(), + FilterFunction::Contrast(Contrast { proportion: None }) + ); + + assert_eq!( + FilterFunction::parse_str("contrast(50%)").unwrap(), + FilterFunction::Contrast(Contrast { + proportion: Some(0.50_f32.into()), + }) + ); + } + + #[test] + fn parses_dropshadow() { + assert_eq!( + FilterFunction::parse_str("drop-shadow(4px 5px)").unwrap(), + FilterFunction::DropShadow(DropShadow { + color: None, + dx: Some(Length::new(4.0, LengthUnit::Px)), + dy: Some(Length::new(5.0, LengthUnit::Px)), + std_deviation: None, + }) + ); + + assert_eq!( + FilterFunction::parse_str("drop-shadow(#ff0000 4px 5px 32px)").unwrap(), + FilterFunction::DropShadow(DropShadow { + color: Some(Color::RGBA(RGBA { + red: 255, + green: 0, + blue: 0, + alpha: 255 + })), + dx: Some(Length::new(4.0, LengthUnit::Px)), + dy: Some(Length::new(5.0, LengthUnit::Px)), + std_deviation: Some(ULength::new(32.0, LengthUnit::Px)), + }) + ); + + assert_eq!( + FilterFunction::parse_str("drop-shadow(1px 2px blue)").unwrap(), + FilterFunction::DropShadow(DropShadow { + color: Some(Color::RGBA(RGBA { + red: 0, + green: 0, + blue: 255, + alpha: 255 + })), + dx: Some(Length::new(1.0, LengthUnit::Px)), + dy: Some(Length::new(2.0, LengthUnit::Px)), + std_deviation: None, + }) + ); + + assert_eq!( + FilterFunction::parse_str("drop-shadow(1px 2px 3px currentColor)").unwrap(), + FilterFunction::DropShadow(DropShadow { + color: Some(Color::CurrentColor), + dx: Some(Length::new(1.0, LengthUnit::Px)), + dy: Some(Length::new(2.0, LengthUnit::Px)), + std_deviation: Some(ULength::new(3.0, LengthUnit::Px)), + }) + ); + + assert_eq!( + FilterFunction::parse_str("drop-shadow(1 2 3)").unwrap(), + FilterFunction::DropShadow(DropShadow { + color: None, + dx: Some(Length::new(1.0, LengthUnit::Px)), + dy: Some(Length::new(2.0, LengthUnit::Px)), + std_deviation: Some(ULength::new(3.0, LengthUnit::Px)), + }) + ); + } + + #[test] + fn parses_grayscale() { + assert_eq!( + FilterFunction::parse_str("grayscale()").unwrap(), + FilterFunction::Grayscale(Grayscale { proportion: None }) + ); + + assert_eq!( + FilterFunction::parse_str("grayscale(50%)").unwrap(), + FilterFunction::Grayscale(Grayscale { + proportion: Some(0.50_f32.into()), + }) + ); + } + + #[test] + fn parses_huerotate() { + assert_eq!( + FilterFunction::parse_str("hue-rotate()").unwrap(), + FilterFunction::HueRotate(HueRotate { angle: None }) + ); + + assert_eq!( + FilterFunction::parse_str("hue-rotate(0)").unwrap(), + FilterFunction::HueRotate(HueRotate { + angle: Some(Angle::new(0.0)) + }) + ); + + assert_eq!( + FilterFunction::parse_str("hue-rotate(128deg)").unwrap(), + FilterFunction::HueRotate(HueRotate { + angle: Some(Angle::from_degrees(128.0)) + }) + ); + } + + #[test] + fn parses_invert() { + assert_eq!( + FilterFunction::parse_str("invert()").unwrap(), + FilterFunction::Invert(Invert { proportion: None }) + ); + + assert_eq!( + FilterFunction::parse_str("invert(50%)").unwrap(), + FilterFunction::Invert(Invert { + proportion: Some(0.50_f32.into()), + }) + ); + } + + #[test] + fn parses_opacity() { + assert_eq!( + FilterFunction::parse_str("opacity()").unwrap(), + FilterFunction::Opacity(Opacity { proportion: None }) + ); + + assert_eq!( + FilterFunction::parse_str("opacity(50%)").unwrap(), + FilterFunction::Opacity(Opacity { + proportion: Some(0.50_f32.into()), + }) + ); + } + + #[test] + fn parses_saturate() { + assert_eq!( + FilterFunction::parse_str("saturate()").unwrap(), + FilterFunction::Saturate(Saturate { proportion: None }) + ); + + assert_eq!( + FilterFunction::parse_str("saturate(50%)").unwrap(), + FilterFunction::Saturate(Saturate { + proportion: Some(0.50_f32.into()), + }) + ); + } + + #[test] + fn parses_sepia() { + assert_eq!( + FilterFunction::parse_str("sepia()").unwrap(), + FilterFunction::Sepia(Sepia { proportion: None }) + ); + + assert_eq!( + FilterFunction::parse_str("sepia(80%)").unwrap(), + FilterFunction::Sepia(Sepia { + proportion: Some(0.80_f32.into()) + }) + ); + + assert_eq!( + FilterFunction::parse_str("sepia(0.52)").unwrap(), + FilterFunction::Sepia(Sepia { + proportion: Some(0.52_f32.into()) + }) + ); + + // values > 1.0 should be clamped to 1.0 + assert_eq!( + FilterFunction::parse_str("sepia(1.5)").unwrap(), + FilterFunction::Sepia(Sepia { + proportion: Some(1.0) + }) + ); + + // negative numbers are invalid. + assert_eq!( + FilterFunction::parse_str("sepia(-1)").unwrap(), + FilterFunction::Sepia(Sepia { proportion: None }), + ); + } + + #[test] + fn invalid_blur_yields_error() { + assert!(FilterFunction::parse_str("blur(foo)").is_err()); + assert!(FilterFunction::parse_str("blur(42 43)").is_err()); + } + + #[test] + fn invalid_brightness_yields_error() { + assert!(FilterFunction::parse_str("brightness(foo)").is_err()); + } + + #[test] + fn invalid_contrast_yields_error() { + assert!(FilterFunction::parse_str("contrast(foo)").is_err()); + } + + #[test] + fn invalid_dropshadow_yields_error() { + assert!(FilterFunction::parse_str("drop-shadow(blue 5px green)").is_err()); + assert!(FilterFunction::parse_str("drop-shadow(blue 5px 5px green)").is_err()); + assert!(FilterFunction::parse_str("drop-shadow(blue 1px)").is_err()); + assert!(FilterFunction::parse_str("drop-shadow(1 2 3 4 blue)").is_err()); + } + + #[test] + fn invalid_grayscale_yields_error() { + assert!(FilterFunction::parse_str("grayscale(foo)").is_err()); + } + + #[test] + fn invalid_huerotate_yields_error() { + assert!(FilterFunction::parse_str("hue-rotate(foo)").is_err()); + } + + #[test] + fn invalid_invert_yields_error() { + assert!(FilterFunction::parse_str("invert(foo)").is_err()); + } + + #[test] + fn invalid_opacity_yields_error() { + assert!(FilterFunction::parse_str("opacity(foo)").is_err()); + } + + #[test] + fn invalid_saturate_yields_error() { + assert!(FilterFunction::parse_str("saturate(foo)").is_err()); + } + + #[test] + fn invalid_sepia_yields_error() { + assert!(FilterFunction::parse_str("sepia(foo)").is_err()); + } +} diff --git a/rsvg/src/filters/blend.rs b/rsvg/src/filters/blend.rs new file mode 100644 index 00000000..30b0bdf7 --- /dev/null +++ b/rsvg/src/filters/blend.rs @@ -0,0 +1,178 @@ +use cssparser::Parser; +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::{set_attribute, ElementTrait}; +use crate::error::*; +use crate::node::{CascadedValues, Node}; +use crate::parsers::{Parse, ParseValue}; +use crate::properties::ColorInterpolationFilters; +use crate::rect::IRect; +use crate::session::Session; +use crate::surface_utils::shared_surface::Operator; +use crate::xml::Attributes; + +use super::bounds::BoundsBuilder; +use super::context::{FilterContext, FilterOutput}; +use super::{ + FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, + ResolvedPrimitive, +}; + +/// Enumeration of the possible blending modes. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +enum Mode { + Normal, + Multiply, + Screen, + Darken, + Lighten, + Overlay, + ColorDodge, + ColorBurn, + HardLight, + SoftLight, + Difference, + Exclusion, + HslHue, + HslSaturation, + HslColor, + HslLuminosity, +} + +enum_default!(Mode, Mode::Normal); + +/// The `feBlend` filter primitive. +#[derive(Default)] +pub struct FeBlend { + base: Primitive, + params: Blend, +} + +/// Resolved `feBlend` primitive for rendering. +#[derive(Clone, Default)] +pub struct Blend { + in1: Input, + in2: Input, + mode: Mode, + color_interpolation_filters: ColorInterpolationFilters, +} + +impl ElementTrait for FeBlend { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + let (in1, in2) = self.base.parse_two_inputs(attrs, session); + self.params.in1 = in1; + self.params.in2 = in2; + + for (attr, value) in attrs.iter() { + if let expanded_name!("", "mode") = attr.expanded() { + set_attribute(&mut self.params.mode, attr.parse(value), session); + } + } + } +} + +impl Blend { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + let input_1 = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.in1, + self.color_interpolation_filters, + )?; + let input_2 = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.in2, + self.color_interpolation_filters, + )?; + let bounds: IRect = bounds_builder + .add_input(&input_1) + .add_input(&input_2) + .compute(ctx) + .clipped + .into(); + + let surface = input_1 + .surface() + .compose(input_2.surface(), bounds, self.mode.into())?; + + Ok(FilterOutput { surface, bounds }) + } +} + +impl FilterEffect for FeBlend { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + let cascaded = CascadedValues::new_from_node(node); + let values = cascaded.get(); + + let mut params = self.params.clone(); + params.color_interpolation_filters = values.color_interpolation_filters(); + + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::Blend(params), + }]) + } +} + +impl Parse for Mode { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + Ok(parse_identifiers!( + parser, + "normal" => Mode::Normal, + "multiply" => Mode::Multiply, + "screen" => Mode::Screen, + "darken" => Mode::Darken, + "lighten" => Mode::Lighten, + "overlay" => Mode::Overlay, + "color-dodge" => Mode::ColorDodge, + "color-burn" => Mode::ColorBurn, + "hard-light" => Mode::HardLight, + "soft-light" => Mode::SoftLight, + "difference" => Mode::Difference, + "exclusion" => Mode::Exclusion, + "hue" => Mode::HslHue, + "saturation" => Mode::HslSaturation, + "color" => Mode::HslColor, + "luminosity" => Mode::HslLuminosity, + )?) + } +} + +impl From<Mode> for Operator { + #[inline] + fn from(x: Mode) -> Self { + use Mode::*; + + match x { + Normal => Operator::Over, + Multiply => Operator::Multiply, + Screen => Operator::Screen, + Darken => Operator::Darken, + Lighten => Operator::Lighten, + Overlay => Operator::Overlay, + ColorDodge => Operator::ColorDodge, + ColorBurn => Operator::ColorBurn, + HardLight => Operator::HardLight, + SoftLight => Operator::SoftLight, + Difference => Operator::Difference, + Exclusion => Operator::Exclusion, + HslHue => Operator::HslHue, + HslSaturation => Operator::HslSaturation, + HslColor => Operator::HslColor, + HslLuminosity => Operator::HslLuminosity, + } + } +} diff --git a/rsvg/src/filters/bounds.rs b/rsvg/src/filters/bounds.rs new file mode 100644 index 00000000..6a6dd9d2 --- /dev/null +++ b/rsvg/src/filters/bounds.rs @@ -0,0 +1,121 @@ +//! Filter primitive subregion computation. +use crate::rect::Rect; +use crate::transform::Transform; + +use super::context::{FilterContext, FilterInput}; + +/// A helper type for filter primitive subregion computation. +pub struct BoundsBuilder { + /// Filter primitive properties. + x: Option<f64>, + y: Option<f64>, + width: Option<f64>, + height: Option<f64>, + + /// The transform to use when generating the rect + transform: Transform, + + /// The inverse transform used when adding rects + inverse: Transform, + + /// Whether one of the input nodes is standard input. + standard_input_was_referenced: bool, + + /// The current bounding rectangle. + rect: Option<Rect>, +} + +/// A filter primitive's subregion. +pub struct Bounds { + /// Primitive's subregion, clipped to the filter effects region. + pub clipped: Rect, + + /// Primitive's subregion, unclipped. + pub unclipped: Rect, +} + +impl BoundsBuilder { + /// Constructs a new `BoundsBuilder`. + #[inline] + pub fn new( + x: Option<f64>, + y: Option<f64>, + width: Option<f64>, + height: Option<f64>, + transform: Transform, + ) -> Self { + // We panic if transform is not invertible. This is checked in the caller. + Self { + x, + y, + width, + height, + transform, + inverse: transform.invert().unwrap(), + standard_input_was_referenced: false, + rect: None, + } + } + + /// Adds a filter primitive input to the bounding box. + #[inline] + pub fn add_input(mut self, input: &FilterInput) -> Self { + // If a standard input was referenced, the default value is the filter effects region + // regardless of other referenced inputs. This means we can skip computing the bounds. + if self.standard_input_was_referenced { + return self; + } + + match *input { + FilterInput::StandardInput(_) => { + self.standard_input_was_referenced = true; + } + FilterInput::PrimitiveOutput(ref output) => { + let input_rect = self.inverse.transform_rect(&Rect::from(output.bounds)); + self.rect = Some(self.rect.map_or(input_rect, |r| input_rect.union(&r))); + } + } + + self + } + + /// Returns the final exact bounds, both with and without clipping to the effects region. + pub fn compute(self, ctx: &FilterContext) -> Bounds { + let effects_region = ctx.effects_region(); + + // The default value is the filter effects region converted into + // the ptimitive coordinate system. + let mut rect = match self.rect { + Some(r) if !self.standard_input_was_referenced => r, + _ => self.inverse.transform_rect(&effects_region), + }; + + // If any of the properties were specified, we need to respect them. + // These replacements are possible because of the primitive coordinate system. + if self.x.is_some() || self.y.is_some() || self.width.is_some() || self.height.is_some() { + if let Some(x) = self.x { + let w = rect.width(); + rect.x0 = x; + rect.x1 = rect.x0 + w; + } + if let Some(y) = self.y { + let h = rect.height(); + rect.y0 = y; + rect.y1 = rect.y0 + h; + } + if let Some(width) = self.width { + rect.x1 = rect.x0 + width; + } + if let Some(height) = self.height { + rect.y1 = rect.y0 + height; + } + } + + // Convert into the surface coordinate system. + let unclipped = self.transform.transform_rect(&rect); + + let clipped = unclipped.intersection(&effects_region).unwrap_or_default(); + + Bounds { clipped, unclipped } + } +} diff --git a/rsvg/src/filters/color_matrix.rs b/rsvg/src/filters/color_matrix.rs new file mode 100644 index 00000000..88eb6f11 --- /dev/null +++ b/rsvg/src/filters/color_matrix.rs @@ -0,0 +1,342 @@ +use cssparser::Parser; +use markup5ever::{expanded_name, local_name, namespace_url, ns, QualName}; +use nalgebra::{Matrix3, Matrix4x5, Matrix5, Vector5}; + +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::{set_attribute, ElementTrait}; +use crate::error::*; +use crate::node::{CascadedValues, Node}; +use crate::parsers::{NumberList, Parse, ParseValue}; +use crate::properties::ColorInterpolationFilters; +use crate::rect::IRect; +use crate::session::Session; +use crate::surface_utils::{ + iterators::Pixels, shared_surface::ExclusiveImageSurface, ImageSurfaceDataExt, Pixel, +}; +use crate::util::clamp; +use crate::xml::Attributes; + +use super::bounds::BoundsBuilder; +use super::context::{FilterContext, FilterOutput}; +use super::{ + FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, + ResolvedPrimitive, +}; + +/// Color matrix operation types. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum OperationType { + Matrix, + Saturate, + HueRotate, + LuminanceToAlpha, +} + +enum_default!(OperationType, OperationType::Matrix); + +/// The `feColorMatrix` filter primitive. +#[derive(Default)] +pub struct FeColorMatrix { + base: Primitive, + params: ColorMatrix, +} + +/// Resolved `feColorMatrix` primitive for rendering. +#[derive(Clone)] +pub struct ColorMatrix { + pub in1: Input, + pub matrix: Matrix5<f64>, + pub color_interpolation_filters: ColorInterpolationFilters, +} + +impl Default for ColorMatrix { + fn default() -> ColorMatrix { + ColorMatrix { + in1: Default::default(), + color_interpolation_filters: Default::default(), + + // nalgebra's Default for Matrix5 is all zeroes, so we actually need this :( + matrix: Matrix5::identity(), + } + } +} + +impl ElementTrait for FeColorMatrix { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.params.in1 = self.base.parse_one_input(attrs, session); + + // First, determine the operation type. + let mut operation_type = Default::default(); + for (attr, value) in attrs + .iter() + .filter(|(attr, _)| attr.expanded() == expanded_name!("", "type")) + { + set_attribute(&mut operation_type, attr.parse(value), session); + } + + // Now read the matrix correspondingly. + // + // Here we cannot assume that ColorMatrix::default() has provided the correct + // initial value for the matrix itself, since the initial value for the matrix + // (i.e. the value to which it should fall back if the `values` attribute is in + // error) depends on the operation_type. + // + // So, for each operation_type, first initialize the proper default matrix, then + // try to parse the value. + + use OperationType::*; + + self.params.matrix = match operation_type { + Matrix => ColorMatrix::default_matrix(), + Saturate => ColorMatrix::saturate_matrix(1.0), + HueRotate => ColorMatrix::hue_rotate_matrix(0.0), + LuminanceToAlpha => ColorMatrix::luminance_to_alpha_matrix(), + }; + + for (attr, value) in attrs + .iter() + .filter(|(attr, _)| attr.expanded() == expanded_name!("", "values")) + { + match operation_type { + Matrix => parse_matrix(&mut self.params.matrix, attr, value, session), + Saturate => parse_saturate_matrix(&mut self.params.matrix, attr, value, session), + HueRotate => parse_hue_rotate_matrix(&mut self.params.matrix, attr, value, session), + LuminanceToAlpha => { + parse_luminance_to_alpha_matrix(&mut self.params.matrix, attr, value, session) + } + } + } + } +} + +fn parse_matrix(dest: &mut Matrix5<f64>, attr: QualName, value: &str, session: &Session) { + let parsed: Result<NumberList<20, 20>, _> = attr.parse(value); + + match parsed { + Ok(NumberList(v)) => { + let matrix = Matrix4x5::from_row_slice(&v); + let mut matrix = matrix.fixed_resize(0.0); + matrix[(4, 4)] = 1.0; + *dest = matrix; + } + + Err(e) => { + rsvg_log!(session, "element feColorMatrix with type=\"matrix\", expected a values attribute with 20 numbers: {}", e); + } + } +} + +fn parse_saturate_matrix(dest: &mut Matrix5<f64>, attr: QualName, value: &str, session: &Session) { + let parsed: Result<f64, _> = attr.parse(value); + + match parsed { + Ok(s) => { + *dest = ColorMatrix::saturate_matrix(s); + } + + Err(e) => { + rsvg_log!(session, "element feColorMatrix with type=\"saturate\", expected a values attribute with 1 number: {}", e); + } + } +} + +fn parse_hue_rotate_matrix( + dest: &mut Matrix5<f64>, + attr: QualName, + value: &str, + session: &Session, +) { + let parsed: Result<f64, _> = attr.parse(value); + + match parsed { + Ok(degrees) => { + *dest = ColorMatrix::hue_rotate_matrix(degrees.to_radians()); + } + + Err(e) => { + rsvg_log!(session, "element feColorMatrix with type=\"hueRotate\", expected a values attribute with 1 number: {}", e); + } + } +} + +fn parse_luminance_to_alpha_matrix( + _dest: &mut Matrix5<f64>, + _attr: QualName, + _value: &str, + session: &Session, +) { + // There's nothing to parse, since our caller already supplied the default value, + // and type="luminanceToAlpha" does not takes a `values` attribute. So, just warn + // that the value is being ignored. + + rsvg_log!( + session, + "ignoring \"values\" attribute for feColorMatrix with type=\"luminanceToAlpha\"" + ); +} + +impl ColorMatrix { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + let input_1 = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.in1, + self.color_interpolation_filters, + )?; + let bounds: IRect = bounds_builder + .add_input(&input_1) + .compute(ctx) + .clipped + .into(); + + let mut surface = ExclusiveImageSurface::new( + ctx.source_graphic().width(), + ctx.source_graphic().height(), + input_1.surface().surface_type(), + )?; + + surface.modify(&mut |data, stride| { + for (x, y, pixel) in Pixels::within(input_1.surface(), bounds) { + let alpha = f64::from(pixel.a) / 255f64; + + let pixel_vec = if alpha == 0.0 { + Vector5::new(0.0, 0.0, 0.0, 0.0, 1.0) + } else { + Vector5::new( + f64::from(pixel.r) / 255f64 / alpha, + f64::from(pixel.g) / 255f64 / alpha, + f64::from(pixel.b) / 255f64 / alpha, + alpha, + 1.0, + ) + }; + let mut new_pixel_vec = Vector5::zeros(); + self.matrix.mul_to(&pixel_vec, &mut new_pixel_vec); + + let new_alpha = clamp(new_pixel_vec[3], 0.0, 1.0); + + let premultiply = |x: f64| ((clamp(x, 0.0, 1.0) * new_alpha * 255f64) + 0.5) as u8; + + let output_pixel = Pixel { + r: premultiply(new_pixel_vec[0]), + g: premultiply(new_pixel_vec[1]), + b: premultiply(new_pixel_vec[2]), + a: ((new_alpha * 255f64) + 0.5) as u8, + }; + + data.set_pixel(stride, output_pixel, x, y); + } + }); + + Ok(FilterOutput { + surface: surface.share()?, + bounds, + }) + } + + /// Compute a `type="hueRotate"` matrix. + /// + /// <https://drafts.fxtf.org/filter-effects/#element-attrdef-fecolormatrix-values> + #[rustfmt::skip] + pub fn hue_rotate_matrix(radians: f64) -> Matrix5<f64> { + let (sin, cos) = radians.sin_cos(); + + let a = Matrix3::new( + 0.213, 0.715, 0.072, + 0.213, 0.715, 0.072, + 0.213, 0.715, 0.072, + ); + + let b = Matrix3::new( + 0.787, -0.715, -0.072, + -0.213, 0.285, -0.072, + -0.213, -0.715, 0.928, + ); + + let c = Matrix3::new( + -0.213, -0.715, 0.928, + 0.143, 0.140, -0.283, + -0.787, 0.715, 0.072, + ); + + let top_left = a + b * cos + c * sin; + + let mut matrix = top_left.fixed_resize(0.0); + matrix[(3, 3)] = 1.0; + matrix[(4, 4)] = 1.0; + matrix + } + + /// Compute a `type="luminanceToAlpha"` matrix. + /// + /// <https://drafts.fxtf.org/filter-effects/#element-attrdef-fecolormatrix-values> + #[rustfmt::skip] + fn luminance_to_alpha_matrix() -> Matrix5<f64> { + Matrix5::new( + 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, + 0.2126, 0.7152, 0.0722, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0, + ) + } + + /// Compute a `type="saturate"` matrix. + /// + /// <https://drafts.fxtf.org/filter-effects/#element-attrdef-fecolormatrix-values> + #[rustfmt::skip] + fn saturate_matrix(s: f64) -> Matrix5<f64> { + Matrix5::new( + 0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0.0, 0.0, + 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0.0, 0.0, + 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0, + ) + } + + /// Default for `type="matrix"`. + /// + /// <https://drafts.fxtf.org/filter-effects/#element-attrdef-fecolormatrix-values> + fn default_matrix() -> Matrix5<f64> { + Matrix5::identity() + } +} + +impl FilterEffect for FeColorMatrix { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + let cascaded = CascadedValues::new_from_node(node); + let values = cascaded.get(); + + let mut params = self.params.clone(); + params.color_interpolation_filters = values.color_interpolation_filters(); + + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::ColorMatrix(params), + }]) + } +} + +impl Parse for OperationType { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + Ok(parse_identifiers!( + parser, + "matrix" => OperationType::Matrix, + "saturate" => OperationType::Saturate, + "hueRotate" => OperationType::HueRotate, + "luminanceToAlpha" => OperationType::LuminanceToAlpha, + )?) + } +} diff --git a/rsvg/src/filters/component_transfer.rs b/rsvg/src/filters/component_transfer.rs new file mode 100644 index 00000000..6f26f683 --- /dev/null +++ b/rsvg/src/filters/component_transfer.rs @@ -0,0 +1,458 @@ +use std::cmp::min; + +use cssparser::Parser; +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::{set_attribute, ElementData, ElementTrait}; +use crate::error::*; +use crate::node::{CascadedValues, Node, NodeBorrow}; +use crate::parsers::{NumberList, Parse, ParseValue}; +use crate::properties::ColorInterpolationFilters; +use crate::rect::IRect; +use crate::session::Session; +use crate::surface_utils::{ + iterators::Pixels, shared_surface::ExclusiveImageSurface, ImageSurfaceDataExt, Pixel, +}; +use crate::util::clamp; +use crate::xml::Attributes; + +use super::bounds::BoundsBuilder; +use super::context::{FilterContext, FilterOutput}; +use super::{ + FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, + ResolvedPrimitive, +}; + +/// The `feComponentTransfer` filter primitive. +#[derive(Default)] +pub struct FeComponentTransfer { + base: Primitive, + params: ComponentTransfer, +} + +/// Resolved `feComponentTransfer` primitive for rendering. +#[derive(Clone, Default)] +pub struct ComponentTransfer { + pub in1: Input, + pub functions: Functions, + pub color_interpolation_filters: ColorInterpolationFilters, +} + +impl ElementTrait for FeComponentTransfer { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.params.in1 = self.base.parse_one_input(attrs, session); + } +} + +/// Component transfer function types. +#[derive(Clone, Debug, PartialEq)] +pub enum FunctionType { + Identity, + Table, + Discrete, + Linear, + Gamma, +} + +impl Parse for FunctionType { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + Ok(parse_identifiers!( + parser, + "identity" => FunctionType::Identity, + "table" => FunctionType::Table, + "discrete" => FunctionType::Discrete, + "linear" => FunctionType::Linear, + "gamma" => FunctionType::Gamma, + )?) + } +} + +/// The compute function parameters. +struct FunctionParameters { + table_values: Vec<f64>, + slope: f64, + intercept: f64, + amplitude: f64, + exponent: f64, + offset: f64, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Functions { + pub r: FeFuncR, + pub g: FeFuncG, + pub b: FeFuncB, + pub a: FeFuncA, +} + +/// The compute function type. +type Function = fn(&FunctionParameters, f64) -> f64; + +/// The identity component transfer function. +fn identity(_: &FunctionParameters, value: f64) -> f64 { + value +} + +/// The table component transfer function. +fn table(params: &FunctionParameters, value: f64) -> f64 { + let n = params.table_values.len() - 1; + let k = (value * (n as f64)).floor() as usize; + + let k = min(k, n); // Just in case. + + if k == n { + return params.table_values[k]; + } + + let vk = params.table_values[k]; + let vk1 = params.table_values[k + 1]; + let k = k as f64; + let n = n as f64; + + vk + (value - k / n) * n * (vk1 - vk) +} + +/// The discrete component transfer function. +fn discrete(params: &FunctionParameters, value: f64) -> f64 { + let n = params.table_values.len(); + let k = (value * (n as f64)).floor() as usize; + + params.table_values[min(k, n - 1)] +} + +/// The linear component transfer function. +fn linear(params: &FunctionParameters, value: f64) -> f64 { + params.slope * value + params.intercept +} + +/// The gamma component transfer function. +fn gamma(params: &FunctionParameters, value: f64) -> f64 { + params.amplitude * value.powf(params.exponent) + params.offset +} + +/// Common values for `feFuncX` elements +/// +/// The elements `feFuncR`, `feFuncG`, `feFuncB`, `feFuncA` all have the same parameters; this structure +/// contains them. Later we define newtypes on this struct as [`FeFuncR`], etc. +#[derive(Clone, Debug, PartialEq)] +pub struct FeFuncCommon { + pub function_type: FunctionType, + pub table_values: Vec<f64>, + pub slope: f64, + pub intercept: f64, + pub amplitude: f64, + pub exponent: f64, + pub offset: f64, +} + +impl Default for FeFuncCommon { + #[inline] + fn default() -> Self { + Self { + function_type: FunctionType::Identity, + table_values: Vec::new(), + slope: 1.0, + intercept: 0.0, + amplitude: 1.0, + exponent: 1.0, + offset: 0.0, + } + } +} + +// All FeFunc* elements are defined here; they just delegate their attributes +// to the FeFuncCommon inside. +macro_rules! impl_func { + ($(#[$attr:meta])* + $name:ident + ) => { + #[derive(Clone, Debug, Default, PartialEq)] + pub struct $name(pub FeFuncCommon); + + impl ElementTrait for $name { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.0.set_attributes(attrs, session); + } + } + }; +} + +impl_func!( + /// The `feFuncR` element. + FeFuncR +); + +impl_func!( + /// The `feFuncG` element. + FeFuncG +); + +impl_func!( + /// The `feFuncB` element. + FeFuncB +); + +impl_func!( + /// The `feFuncA` element. + FeFuncA +); + +impl FeFuncCommon { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "type") => { + set_attribute(&mut self.function_type, attr.parse(value), session) + } + expanded_name!("", "tableValues") => { + // #691: Limit list to 256 to mitigate malicious SVGs + let mut number_list = NumberList::<0, 256>(Vec::new()); + set_attribute(&mut number_list, attr.parse(value), session); + self.table_values = number_list.0; + } + expanded_name!("", "slope") => { + set_attribute(&mut self.slope, attr.parse(value), session) + } + expanded_name!("", "intercept") => { + set_attribute(&mut self.intercept, attr.parse(value), session) + } + expanded_name!("", "amplitude") => { + set_attribute(&mut self.amplitude, attr.parse(value), session) + } + expanded_name!("", "exponent") => { + set_attribute(&mut self.exponent, attr.parse(value), session) + } + expanded_name!("", "offset") => { + set_attribute(&mut self.offset, attr.parse(value), session) + } + + _ => (), + } + } + + // The table function type with empty table_values is considered + // an identity function. + match self.function_type { + FunctionType::Table | FunctionType::Discrete => { + if self.table_values.is_empty() { + self.function_type = FunctionType::Identity; + } + } + _ => (), + } + } + + fn function_parameters(&self) -> FunctionParameters { + FunctionParameters { + table_values: self.table_values.clone(), + slope: self.slope, + intercept: self.intercept, + amplitude: self.amplitude, + exponent: self.exponent, + offset: self.offset, + } + } + + fn function(&self) -> Function { + match self.function_type { + FunctionType::Identity => identity, + FunctionType::Table => table, + FunctionType::Discrete => discrete, + FunctionType::Linear => linear, + FunctionType::Gamma => gamma, + } + } +} + +macro_rules! func_or_default { + ($func_node:ident, $func_type:ident) => { + match $func_node { + Some(ref f) => match &*f.borrow_element_data() { + ElementData::$func_type(e) => (**e).clone(), + _ => unreachable!(), + }, + _ => $func_type::default(), + } + }; +} + +macro_rules! get_func_x_node { + ($func_node:ident, $func_type:ident) => { + $func_node + .children() + .rev() + .filter(|c| c.is_element()) + .find(|c| matches!(*c.borrow_element_data(), ElementData::$func_type(_))) + }; +} + +impl ComponentTransfer { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + let input_1 = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.in1, + self.color_interpolation_filters, + )?; + let bounds: IRect = bounds_builder + .add_input(&input_1) + .compute(ctx) + .clipped + .into(); + + // Create the output surface. + let mut surface = ExclusiveImageSurface::new( + ctx.source_graphic().width(), + ctx.source_graphic().height(), + input_1.surface().surface_type(), + )?; + + fn compute_func(func: &FeFuncCommon) -> impl Fn(u8, f64, f64) -> u8 { + let compute = func.function(); + let params = func.function_parameters(); + + move |value, alpha, new_alpha| { + let value = f64::from(value) / 255f64; + + let unpremultiplied = if alpha == 0f64 { 0f64 } else { value / alpha }; + + let new_value = compute(¶ms, unpremultiplied); + let new_value = clamp(new_value, 0f64, 1f64); + + ((new_value * new_alpha * 255f64) + 0.5) as u8 + } + } + + let compute_r = compute_func(&self.functions.r.0); + let compute_g = compute_func(&self.functions.g.0); + let compute_b = compute_func(&self.functions.b.0); + + // Alpha gets special handling since everything else depends on it. + let compute_a = self.functions.a.0.function(); + let params_a = self.functions.a.0.function_parameters(); + let compute_a = |alpha| compute_a(¶ms_a, alpha); + + // Do the actual processing. + surface.modify(&mut |data, stride| { + for (x, y, pixel) in Pixels::within(input_1.surface(), bounds) { + let alpha = f64::from(pixel.a) / 255f64; + let new_alpha = compute_a(alpha); + + let output_pixel = Pixel { + r: compute_r(pixel.r, alpha, new_alpha), + g: compute_g(pixel.g, alpha, new_alpha), + b: compute_b(pixel.b, alpha, new_alpha), + a: ((new_alpha * 255f64) + 0.5) as u8, + }; + + data.set_pixel(stride, output_pixel, x, y); + } + }); + + Ok(FilterOutput { + surface: surface.share()?, + bounds, + }) + } +} + +impl FilterEffect for FeComponentTransfer { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + let cascaded = CascadedValues::new_from_node(node); + let values = cascaded.get(); + + let mut params = self.params.clone(); + params.functions = get_functions(node)?; + params.color_interpolation_filters = values.color_interpolation_filters(); + + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::ComponentTransfer(params), + }]) + } +} + +/// Takes a feComponentTransfer and walks its children to produce the feFuncX arguments. +fn get_functions(node: &Node) -> Result<Functions, FilterResolveError> { + let func_r_node = get_func_x_node!(node, FeFuncR); + let func_g_node = get_func_x_node!(node, FeFuncG); + let func_b_node = get_func_x_node!(node, FeFuncB); + let func_a_node = get_func_x_node!(node, FeFuncA); + + let r = func_or_default!(func_r_node, FeFuncR); + let g = func_or_default!(func_g_node, FeFuncG); + let b = func_or_default!(func_b_node, FeFuncB); + let a = func_or_default!(func_a_node, FeFuncA); + + Ok(Functions { r, g, b, a }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::document::Document; + + #[test] + fn extracts_functions() { + let document = Document::load_from_bytes( + br#"<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <filter id="filter"> + <feComponentTransfer id="component_transfer"> + <!-- no feFuncR so it should get the defaults --> + + <feFuncG type="table" tableValues="0.0 1.0 2.0"/> + + <feFuncB type="table"/> + <!-- duplicate this to test that last-one-wins --> + <feFuncB type="discrete" tableValues="0.0, 1.0" slope="1.0" intercept="2.0" amplitude="3.0" exponent="4.0" offset="5.0"/> + + <!-- no feFuncA so it should get the defaults --> + </feComponentTransfer> + </filter> +</svg> +"# + ); + + let component_transfer = document.lookup_internal_node("component_transfer").unwrap(); + let functions = get_functions(&component_transfer).unwrap(); + + assert_eq!( + functions, + Functions { + r: FeFuncR::default(), + + g: FeFuncG(FeFuncCommon { + function_type: FunctionType::Table, + table_values: vec![0.0, 1.0, 2.0], + ..FeFuncCommon::default() + }), + + b: FeFuncB(FeFuncCommon { + function_type: FunctionType::Discrete, + table_values: vec![0.0, 1.0], + slope: 1.0, + intercept: 2.0, + amplitude: 3.0, + exponent: 4.0, + offset: 5.0, + ..FeFuncCommon::default() + }), + + a: FeFuncA::default(), + } + ); + } +} diff --git a/rsvg/src/filters/composite.rs b/rsvg/src/filters/composite.rs new file mode 100644 index 00000000..c5c02af1 --- /dev/null +++ b/rsvg/src/filters/composite.rs @@ -0,0 +1,179 @@ +use cssparser::Parser; +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::{set_attribute, ElementTrait}; +use crate::error::*; +use crate::node::{CascadedValues, Node}; +use crate::parsers::{Parse, ParseValue}; +use crate::properties::ColorInterpolationFilters; +use crate::rect::IRect; +use crate::session::Session; +use crate::surface_utils::shared_surface::Operator as SurfaceOperator; +use crate::xml::Attributes; + +use super::bounds::BoundsBuilder; +use super::context::{FilterContext, FilterOutput}; +use super::{ + FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, + ResolvedPrimitive, +}; + +/// Enumeration of the possible compositing operations. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Operator { + Over, + In, + Out, + Atop, + Xor, + Arithmetic, +} + +enum_default!(Operator, Operator::Over); + +/// The `feComposite` filter primitive. +#[derive(Default)] +pub struct FeComposite { + base: Primitive, + params: Composite, +} + +/// Resolved `feComposite` primitive for rendering. +#[derive(Clone, Default)] +pub struct Composite { + pub in1: Input, + pub in2: Input, + pub operator: Operator, + pub k1: f64, + pub k2: f64, + pub k3: f64, + pub k4: f64, + pub color_interpolation_filters: ColorInterpolationFilters, +} + +impl ElementTrait for FeComposite { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + let (in1, in2) = self.base.parse_two_inputs(attrs, session); + self.params.in1 = in1; + self.params.in2 = in2; + + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "operator") => { + set_attribute(&mut self.params.operator, attr.parse(value), session) + } + expanded_name!("", "k1") => { + set_attribute(&mut self.params.k1, attr.parse(value), session) + } + expanded_name!("", "k2") => { + set_attribute(&mut self.params.k2, attr.parse(value), session) + } + expanded_name!("", "k3") => { + set_attribute(&mut self.params.k3, attr.parse(value), session) + } + expanded_name!("", "k4") => { + set_attribute(&mut self.params.k4, attr.parse(value), session) + } + _ => (), + } + } + } +} + +impl Composite { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + let input_1 = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.in1, + self.color_interpolation_filters, + )?; + let input_2 = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.in2, + self.color_interpolation_filters, + )?; + let bounds: IRect = bounds_builder + .add_input(&input_1) + .add_input(&input_2) + .compute(ctx) + .clipped + .into(); + + let surface = if self.operator == Operator::Arithmetic { + input_1.surface().compose_arithmetic( + input_2.surface(), + bounds, + self.k1, + self.k2, + self.k3, + self.k4, + )? + } else { + input_1 + .surface() + .compose(input_2.surface(), bounds, self.operator.into())? + }; + + Ok(FilterOutput { surface, bounds }) + } +} + +impl FilterEffect for FeComposite { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + let cascaded = CascadedValues::new_from_node(node); + let values = cascaded.get(); + + let mut params = self.params.clone(); + params.color_interpolation_filters = values.color_interpolation_filters(); + + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::Composite(params), + }]) + } +} + +impl Parse for Operator { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + Ok(parse_identifiers!( + parser, + "over" => Operator::Over, + "in" => Operator::In, + "out" => Operator::Out, + "atop" => Operator::Atop, + "xor" => Operator::Xor, + "arithmetic" => Operator::Arithmetic, + )?) + } +} + +impl From<Operator> for SurfaceOperator { + #[inline] + fn from(x: Operator) -> SurfaceOperator { + use Operator::*; + + match x { + Over => SurfaceOperator::Over, + In => SurfaceOperator::In, + Out => SurfaceOperator::Out, + Atop => SurfaceOperator::Atop, + Xor => SurfaceOperator::Xor, + + _ => panic!("can't convert Operator::Arithmetic to a shared_surface::Operator"), + } + } +} diff --git a/rsvg/src/filters/context.rs b/rsvg/src/filters/context.rs new file mode 100644 index 00000000..a09160ab --- /dev/null +++ b/rsvg/src/filters/context.rs @@ -0,0 +1,405 @@ +use once_cell::sync::OnceCell; +use std::collections::HashMap; +use std::rc::Rc; + +use crate::bbox::BoundingBox; +use crate::coord_units::CoordUnits; +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::filter::UserSpaceFilter; +use crate::paint_server::UserSpacePaintSource; +use crate::parsers::CustomIdent; +use crate::properties::ColorInterpolationFilters; +use crate::rect::{IRect, Rect}; +use crate::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; +use crate::transform::Transform; + +use super::error::FilterError; +use super::Input; + +/// A filter primitive output. +#[derive(Debug, Clone)] +pub struct FilterOutput { + /// The surface after the filter primitive was applied. + pub surface: SharedImageSurface, + + /// The filter primitive subregion. + pub bounds: IRect, +} + +/// A filter primitive result. +#[derive(Debug, Clone)] +pub struct FilterResult { + /// The name of this result: the value of the `result` attribute. + pub name: Option<CustomIdent>, + + /// The output. + pub output: FilterOutput, +} + +/// An input to a filter primitive. +#[derive(Debug, Clone)] +pub enum FilterInput { + /// One of the standard inputs. + StandardInput(SharedImageSurface), + /// Output of another filter primitive. + PrimitiveOutput(FilterOutput), +} + +/// The filter rendering context. +pub struct FilterContext { + /// Paint source for primitives which have an input value equal to `StrokePaint`. + stroke_paint: Rc<UserSpacePaintSource>, + /// Paint source for primitives which have an input value equal to `FillPaint`. + fill_paint: Rc<UserSpacePaintSource>, + + /// The source graphic surface. + source_surface: SharedImageSurface, + /// Output of the last filter primitive. + last_result: Option<FilterOutput>, + /// Surfaces of the previous filter primitives by name. + previous_results: HashMap<CustomIdent, FilterOutput>, + + /// Input surface for primitives that require an input of `BackgroundImage` or `BackgroundAlpha`. Computed lazily. + background_surface: OnceCell<Result<SharedImageSurface, FilterError>>, + + // Input surface for primitives that require an input of `StrokePaint`, Computed lazily. + stroke_paint_surface: OnceCell<Result<SharedImageSurface, FilterError>>, + + // Input surface for primitives that require an input of `FillPaint`, Computed lazily. + fill_paint_surface: OnceCell<Result<SharedImageSurface, FilterError>>, + + /// Primtive units + primitive_units: CoordUnits, + /// The filter effects region. + effects_region: Rect, + + /// The filter element affine matrix. + /// + /// If `filterUnits == userSpaceOnUse`, equal to the drawing context matrix, so, for example, + /// if the target node is in a group with `transform="translate(30, 20)"`, this will be equal + /// to a matrix that translates to 30, 20 (and does not scale). Note that the target node + /// bounding box isn't included in the computations in this case. + /// + /// If `filterUnits == objectBoundingBox`, equal to the target node bounding box matrix + /// multiplied by the drawing context matrix, so, for example, if the target node is in a group + /// with `transform="translate(30, 20)"` and also has `x="1", y="1", width="50", height="50"`, + /// this will be equal to a matrix that translates to 31, 21 and scales to 50, 50. + /// + /// This is to be used in conjunction with setting the viewbox size to account for the scaling. + /// For `filterUnits == userSpaceOnUse`, the viewbox will have the actual resolution size, and + /// for `filterUnits == objectBoundingBox`, the viewbox will have the size of 1, 1. + _affine: Transform, + + /// The filter primitive affine matrix. + /// + /// See the comments for `_affine`, they largely apply here. + paffine: Transform, +} + +impl FilterContext { + /// Creates a new `FilterContext`. + pub fn new( + filter: &UserSpaceFilter, + stroke_paint: Rc<UserSpacePaintSource>, + fill_paint: Rc<UserSpacePaintSource>, + source_surface: &SharedImageSurface, + draw_transform: Transform, + node_bbox: BoundingBox, + ) -> Result<Self, FilterError> { + // The rect can be empty (for example, if the filter is applied to an empty group). + // However, with userSpaceOnUse it's still possible to create images with a filter. + let bbox_rect = node_bbox.rect.unwrap_or_default(); + + let affine = match filter.filter_units { + CoordUnits::UserSpaceOnUse => draw_transform, + CoordUnits::ObjectBoundingBox => Transform::new_unchecked( + bbox_rect.width(), + 0.0, + 0.0, + bbox_rect.height(), + bbox_rect.x0, + bbox_rect.y0, + ) + .post_transform(&draw_transform), + }; + + let paffine = match filter.primitive_units { + CoordUnits::UserSpaceOnUse => draw_transform, + CoordUnits::ObjectBoundingBox => Transform::new_unchecked( + bbox_rect.width(), + 0.0, + 0.0, + bbox_rect.height(), + bbox_rect.x0, + bbox_rect.y0, + ) + .post_transform(&draw_transform), + }; + + if !(affine.is_invertible() && paffine.is_invertible()) { + return Err(FilterError::InvalidParameter( + "transform is not invertible".to_string(), + )); + } + + let effects_region = { + let mut bbox = BoundingBox::new(); + let other_bbox = BoundingBox::new() + .with_transform(affine) + .with_rect(filter.rect); + + // At this point all of the previous viewbox and matrix business gets converted to pixel + // coordinates in the final surface, because bbox is created with an identity transform. + bbox.insert(&other_bbox); + + // Finally, clip to the width and height of our surface. + let (width, height) = (source_surface.width(), source_surface.height()); + let rect = Rect::from_size(f64::from(width), f64::from(height)); + let other_bbox = BoundingBox::new().with_rect(rect); + bbox.clip(&other_bbox); + + bbox.rect.unwrap() + }; + + Ok(Self { + stroke_paint, + fill_paint, + source_surface: source_surface.clone(), + last_result: None, + previous_results: HashMap::new(), + background_surface: OnceCell::new(), + stroke_paint_surface: OnceCell::new(), + fill_paint_surface: OnceCell::new(), + primitive_units: filter.primitive_units, + effects_region, + _affine: affine, + paffine, + }) + } + + /// Returns the surface corresponding to the source graphic. + #[inline] + pub fn source_graphic(&self) -> &SharedImageSurface { + &self.source_surface + } + + /// Returns the surface corresponding to the background image snapshot. + fn background_image(&self, draw_ctx: &DrawingCtx) -> Result<SharedImageSurface, FilterError> { + let res = self.background_surface.get_or_init(|| { + draw_ctx + .get_snapshot(self.source_surface.width(), self.source_surface.height()) + .map_err(FilterError::Rendering) + }); + + res.as_ref().map(|s| s.clone()).map_err(|e| e.clone()) + } + + /// Returns a surface filled with the current stroke's paint, for `StrokePaint` inputs in primitives. + /// + /// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#attr-valuedef-in-strokepaint> + fn stroke_paint_image( + &self, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<SharedImageSurface, FilterError> { + let res = self.stroke_paint_surface.get_or_init(|| { + Ok(draw_ctx.get_paint_source_surface( + self.source_surface.width(), + self.source_surface.height(), + acquired_nodes, + &self.stroke_paint, + )?) + }); + + res.as_ref().map(|s| s.clone()).map_err(|e| e.clone()) + } + + /// Returns a surface filled with the current fill's paint, for `FillPaint` inputs in primitives. + /// + /// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#attr-valuedef-in-fillpaint> + fn fill_paint_image( + &self, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<SharedImageSurface, FilterError> { + let res = self.fill_paint_surface.get_or_init(|| { + Ok(draw_ctx.get_paint_source_surface( + self.source_surface.width(), + self.source_surface.height(), + acquired_nodes, + &self.fill_paint, + )?) + }); + + res.as_ref().map(|s| s.clone()).map_err(|e| e.clone()) + } + + /// Converts this `FilterContext` into the surface corresponding to the output of the filter + /// chain. + /// + /// The returned surface is in the sRGB color space. + // TODO: sRGB conversion should probably be done by the caller. + #[inline] + pub fn into_output(self) -> Result<SharedImageSurface, cairo::Error> { + match self.last_result { + Some(FilterOutput { surface, bounds }) => surface.to_srgb(bounds), + None => SharedImageSurface::empty( + self.source_surface.width(), + self.source_surface.height(), + SurfaceType::AlphaOnly, + ), + } + } + + /// Stores a filter primitive result into the context. + #[inline] + pub fn store_result(&mut self, result: FilterResult) { + if let Some(name) = result.name { + self.previous_results.insert(name, result.output.clone()); + } + + self.last_result = Some(result.output); + } + + /// Returns the paffine matrix. + #[inline] + pub fn paffine(&self) -> Transform { + self.paffine + } + + /// Returns the primitive units. + #[inline] + pub fn primitive_units(&self) -> CoordUnits { + self.primitive_units + } + + /// Returns the filter effects region. + #[inline] + pub fn effects_region(&self) -> Rect { + self.effects_region + } + + /// Get a filter primitive's default input as if its `in=\"...\"` were not specified. + /// + /// Per <https://drafts.fxtf.org/filter-effects/#element-attrdef-filter-primitive-in>, + /// "References to non-existent results will be treated as if no result was + /// specified". That is, fall back to the last result in the filter chain, or if this + /// is the first in the chain, just use SourceGraphic. + fn get_unspecified_input(&self) -> FilterInput { + if let Some(output) = self.last_result.as_ref() { + FilterInput::PrimitiveOutput(output.clone()) + } else { + FilterInput::StandardInput(self.source_graphic().clone()) + } + } + + /// Retrieves the filter input surface according to the SVG rules. + fn get_input_raw( + &self, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + in_: &Input, + ) -> Result<FilterInput, FilterError> { + match *in_ { + Input::Unspecified => Ok(self.get_unspecified_input()), + + Input::SourceGraphic => Ok(FilterInput::StandardInput(self.source_graphic().clone())), + + Input::SourceAlpha => self + .source_graphic() + .extract_alpha(self.effects_region().into()) + .map_err(FilterError::CairoError) + .map(FilterInput::StandardInput), + + Input::BackgroundImage => self + .background_image(draw_ctx) + .map(FilterInput::StandardInput), + + Input::BackgroundAlpha => self + .background_image(draw_ctx) + .and_then(|surface| { + surface + .extract_alpha(self.effects_region().into()) + .map_err(FilterError::CairoError) + }) + .map(FilterInput::StandardInput), + + Input::FillPaint => self + .fill_paint_image(acquired_nodes, draw_ctx) + .map(FilterInput::StandardInput), + + Input::StrokePaint => self + .stroke_paint_image(acquired_nodes, draw_ctx) + .map(FilterInput::StandardInput), + + Input::FilterOutput(ref name) => { + let input = match self.previous_results.get(name).cloned() { + Some(filter_output) => { + // Happy path: we found a previous primitive's named output, so pass it on. + FilterInput::PrimitiveOutput(filter_output) + } + + None => { + // Fallback path: we didn't find a primitive's output by the + // specified name, so fall back to using an unspecified output. + // Per the spec, "References to non-existent results will be + // treated as if no result was specified." - + // https://drafts.fxtf.org/filter-effects/#element-attrdef-filter-primitive-in + self.get_unspecified_input() + } + }; + + Ok(input) + } + } + } + + /// Retrieves the filter input surface according to the SVG rules. + /// + /// The surface will be converted to the color space specified by `color_interpolation_filters`. + pub fn get_input( + &self, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + in_: &Input, + color_interpolation_filters: ColorInterpolationFilters, + ) -> Result<FilterInput, FilterError> { + let raw = self.get_input_raw(acquired_nodes, draw_ctx, in_)?; + + // Convert the input surface to the desired format. + let (surface, bounds) = match raw { + FilterInput::StandardInput(ref surface) => (surface, self.effects_region().into()), + FilterInput::PrimitiveOutput(FilterOutput { + ref surface, + ref bounds, + }) => (surface, *bounds), + }; + + let surface = match color_interpolation_filters { + ColorInterpolationFilters::Auto => Ok(surface.clone()), + ColorInterpolationFilters::LinearRgb => surface.to_linear_rgb(bounds), + ColorInterpolationFilters::Srgb => surface.to_srgb(bounds), + }; + + surface + .map_err(FilterError::CairoError) + .map(|surface| match raw { + FilterInput::StandardInput(_) => FilterInput::StandardInput(surface), + FilterInput::PrimitiveOutput(ref output) => { + FilterInput::PrimitiveOutput(FilterOutput { surface, ..*output }) + } + }) + } +} + +impl FilterInput { + /// Retrieves the surface from `FilterInput`. + #[inline] + pub fn surface(&self) -> &SharedImageSurface { + match *self { + FilterInput::StandardInput(ref surface) => surface, + FilterInput::PrimitiveOutput(FilterOutput { ref surface, .. }) => surface, + } + } +} diff --git a/rsvg/src/filters/convolve_matrix.rs b/rsvg/src/filters/convolve_matrix.rs new file mode 100644 index 00000000..096ad043 --- /dev/null +++ b/rsvg/src/filters/convolve_matrix.rs @@ -0,0 +1,354 @@ +use cssparser::Parser; +use markup5ever::{expanded_name, local_name, namespace_url, ns}; +use nalgebra::{DMatrix, Dyn, VecStorage}; + +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::{set_attribute, ElementTrait}; +use crate::error::*; +use crate::node::{CascadedValues, Node}; +use crate::parsers::{NumberList, NumberOptionalNumber, Parse, ParseValue}; +use crate::properties::ColorInterpolationFilters; +use crate::rect::IRect; +use crate::session::Session; +use crate::surface_utils::{ + iterators::{PixelRectangle, Pixels}, + shared_surface::ExclusiveImageSurface, + EdgeMode, ImageSurfaceDataExt, Pixel, +}; +use crate::util::clamp; +use crate::xml::Attributes; + +use super::bounds::BoundsBuilder; +use super::context::{FilterContext, FilterOutput}; +use super::{ + FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, + ResolvedPrimitive, +}; + +/// The `feConvolveMatrix` filter primitive. +#[derive(Default)] +pub struct FeConvolveMatrix { + base: Primitive, + params: ConvolveMatrix, +} + +/// Resolved `feConvolveMatrix` primitive for rendering. +#[derive(Clone)] +pub struct ConvolveMatrix { + in1: Input, + order: NumberOptionalNumber<u32>, + kernel_matrix: NumberList<0, 400>, // #691: Limit list to 400 (20x20) to mitigate malicious SVGs + divisor: f64, + bias: f64, + target_x: Option<u32>, + target_y: Option<u32>, + edge_mode: EdgeMode, + kernel_unit_length: Option<(f64, f64)>, + preserve_alpha: bool, + color_interpolation_filters: ColorInterpolationFilters, +} + +impl Default for ConvolveMatrix { + /// Constructs a new `ConvolveMatrix` with empty properties. + #[inline] + fn default() -> ConvolveMatrix { + ConvolveMatrix { + in1: Default::default(), + order: NumberOptionalNumber(3, 3), + kernel_matrix: NumberList(Vec::new()), + divisor: 0.0, + bias: 0.0, + target_x: None, + target_y: None, + edge_mode: EdgeMode::Duplicate, + kernel_unit_length: None, + preserve_alpha: false, + color_interpolation_filters: Default::default(), + } + } +} + +impl ElementTrait for FeConvolveMatrix { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.params.in1 = self.base.parse_one_input(attrs, session); + + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "order") => { + set_attribute(&mut self.params.order, attr.parse(value), session) + } + expanded_name!("", "kernelMatrix") => { + set_attribute(&mut self.params.kernel_matrix, attr.parse(value), session) + } + expanded_name!("", "divisor") => { + set_attribute(&mut self.params.divisor, attr.parse(value), session) + } + expanded_name!("", "bias") => { + set_attribute(&mut self.params.bias, attr.parse(value), session) + } + expanded_name!("", "targetX") => { + set_attribute(&mut self.params.target_x, attr.parse(value), session) + } + expanded_name!("", "targetY") => { + set_attribute(&mut self.params.target_y, attr.parse(value), session) + } + expanded_name!("", "edgeMode") => { + set_attribute(&mut self.params.edge_mode, attr.parse(value), session) + } + expanded_name!("", "kernelUnitLength") => { + let v: Result<NumberOptionalNumber<f64>, _> = attr.parse(value); + match v { + Ok(NumberOptionalNumber(x, y)) => { + self.params.kernel_unit_length = Some((x, y)); + } + + Err(e) => { + rsvg_log!(session, "ignoring attribute with invalid value: {}", e); + } + } + } + expanded_name!("", "preserveAlpha") => { + set_attribute(&mut self.params.preserve_alpha, attr.parse(value), session); + } + + _ => (), + } + } + } +} + +impl ConvolveMatrix { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + #![allow(clippy::many_single_char_names)] + + let input_1 = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.in1, + self.color_interpolation_filters, + )?; + let mut bounds: IRect = bounds_builder + .add_input(&input_1) + .compute(ctx) + .clipped + .into(); + let original_bounds = bounds; + + let target_x = match self.target_x { + Some(x) if x >= self.order.0 => { + return Err(FilterError::InvalidParameter( + "targetX must be less than orderX".to_string(), + )) + } + Some(x) => x, + None => self.order.0 / 2, + }; + + let target_y = match self.target_y { + Some(y) if y >= self.order.1 => { + return Err(FilterError::InvalidParameter( + "targetY must be less than orderY".to_string(), + )) + } + Some(y) => y, + None => self.order.1 / 2, + }; + + let mut input_surface = if self.preserve_alpha { + // preserve_alpha means we need to premultiply and unpremultiply the values. + input_1.surface().unpremultiply(bounds)? + } else { + input_1.surface().clone() + }; + + let scale = self + .kernel_unit_length + .and_then(|(x, y)| { + if x <= 0.0 || y <= 0.0 { + None + } else { + Some((x, y)) + } + }) + .map(|(dx, dy)| ctx.paffine().transform_distance(dx, dy)); + + if let Some((ox, oy)) = scale { + // Scale the input surface to match kernel_unit_length. + let (new_surface, new_bounds) = input_surface.scale(bounds, 1.0 / ox, 1.0 / oy)?; + + input_surface = new_surface; + bounds = new_bounds; + } + + let cols = self.order.0 as usize; + let rows = self.order.1 as usize; + let number_of_elements = cols * rows; + let numbers = self.kernel_matrix.0.clone(); + + if numbers.len() != number_of_elements && numbers.len() != 400 { + // "If the result of orderX * orderY is not equal to the the number of entries + // in the value list, the filter primitive acts as a pass through filter." + // + // https://drafts.fxtf.org/filter-effects/#element-attrdef-feconvolvematrix-kernelmatrix + rsvg_log!( + draw_ctx.session(), + "feConvolveMatrix got {} elements when it expected {}; ignoring it", + numbers.len(), + number_of_elements + ); + return Ok(FilterOutput { + surface: input_1.surface().clone(), + bounds: original_bounds, + }); + } + + let matrix = DMatrix::from_data(VecStorage::new(Dyn(rows), Dyn(cols), numbers)); + + let divisor = if self.divisor != 0.0 { + self.divisor + } else { + let d = matrix.iter().sum(); + + if d != 0.0 { + d + } else { + 1.0 + } + }; + + let mut surface = ExclusiveImageSurface::new( + input_surface.width(), + input_surface.height(), + input_1.surface().surface_type(), + )?; + + surface.modify(&mut |data, stride| { + for (x, y, pixel) in Pixels::within(&input_surface, bounds) { + // Compute the convolution rectangle bounds. + let kernel_bounds = IRect::new( + x as i32 - target_x as i32, + y as i32 - target_y as i32, + x as i32 - target_x as i32 + self.order.0 as i32, + y as i32 - target_y as i32 + self.order.1 as i32, + ); + + // Do the convolution. + let mut r = 0.0; + let mut g = 0.0; + let mut b = 0.0; + let mut a = 0.0; + + for (x, y, pixel) in + PixelRectangle::within(&input_surface, bounds, kernel_bounds, self.edge_mode) + { + let kernel_x = (kernel_bounds.x1 - x - 1) as usize; + let kernel_y = (kernel_bounds.y1 - y - 1) as usize; + + r += f64::from(pixel.r) / 255.0 * matrix[(kernel_y, kernel_x)]; + g += f64::from(pixel.g) / 255.0 * matrix[(kernel_y, kernel_x)]; + b += f64::from(pixel.b) / 255.0 * matrix[(kernel_y, kernel_x)]; + + if !self.preserve_alpha { + a += f64::from(pixel.a) / 255.0 * matrix[(kernel_y, kernel_x)]; + } + } + + // If preserve_alpha is true, set a to the source alpha value. + if self.preserve_alpha { + a = f64::from(pixel.a) / 255.0; + } else { + a = a / divisor + self.bias; + } + + let clamped_a = clamp(a, 0.0, 1.0); + + let compute = |x| { + let x = x / divisor + self.bias * a; + + let x = if self.preserve_alpha { + // Premultiply the output value. + clamp(x, 0.0, 1.0) * clamped_a + } else { + clamp(x, 0.0, clamped_a) + }; + + ((x * 255.0) + 0.5) as u8 + }; + + let output_pixel = Pixel { + r: compute(r), + g: compute(g), + b: compute(b), + a: ((clamped_a * 255.0) + 0.5) as u8, + }; + + data.set_pixel(stride, output_pixel, x, y); + } + }); + + let mut surface = surface.share()?; + + if let Some((ox, oy)) = scale { + // Scale the output surface back. + surface = surface.scale_to( + ctx.source_graphic().width(), + ctx.source_graphic().height(), + original_bounds, + ox, + oy, + )?; + + bounds = original_bounds; + } + + Ok(FilterOutput { surface, bounds }) + } +} + +impl FilterEffect for FeConvolveMatrix { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + let cascaded = CascadedValues::new_from_node(node); + let values = cascaded.get(); + + let mut params = self.params.clone(); + params.color_interpolation_filters = values.color_interpolation_filters(); + + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::ConvolveMatrix(params), + }]) + } +} + +impl Parse for EdgeMode { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + Ok(parse_identifiers!( + parser, + "duplicate" => EdgeMode::Duplicate, + "wrap" => EdgeMode::Wrap, + "none" => EdgeMode::None, + )?) + } +} + +// Used for the preserveAlpha attribute +impl Parse for bool { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + Ok(parse_identifiers!( + parser, + "false" => false, + "true" => true, + )?) + } +} diff --git a/rsvg/src/filters/displacement_map.rs b/rsvg/src/filters/displacement_map.rs new file mode 100644 index 00000000..f0cead68 --- /dev/null +++ b/rsvg/src/filters/displacement_map.rs @@ -0,0 +1,195 @@ +use cssparser::Parser; +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::{set_attribute, ElementTrait}; +use crate::error::*; +use crate::node::{CascadedValues, Node}; +use crate::parsers::{Parse, ParseValue}; +use crate::properties::ColorInterpolationFilters; +use crate::rect::IRect; +use crate::session::Session; +use crate::surface_utils::{iterators::Pixels, shared_surface::ExclusiveImageSurface}; +use crate::xml::Attributes; + +use super::bounds::BoundsBuilder; +use super::context::{FilterContext, FilterOutput}; +use super::{ + FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, + ResolvedPrimitive, +}; + +/// Enumeration of the color channels the displacement map can source. +#[derive(Clone, Copy)] +enum ColorChannel { + R, + G, + B, + A, +} + +enum_default!(ColorChannel, ColorChannel::A); + +/// The `feDisplacementMap` filter primitive. +#[derive(Default)] +pub struct FeDisplacementMap { + base: Primitive, + params: DisplacementMap, +} + +/// Resolved `feDisplacementMap` primitive for rendering. +#[derive(Clone, Default)] +pub struct DisplacementMap { + in1: Input, + in2: Input, + scale: f64, + x_channel_selector: ColorChannel, + y_channel_selector: ColorChannel, + color_interpolation_filters: ColorInterpolationFilters, +} + +impl ElementTrait for FeDisplacementMap { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + let (in1, in2) = self.base.parse_two_inputs(attrs, session); + self.params.in1 = in1; + self.params.in2 = in2; + + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "scale") => { + set_attribute(&mut self.params.scale, attr.parse(value), session) + } + expanded_name!("", "xChannelSelector") => { + set_attribute( + &mut self.params.x_channel_selector, + attr.parse(value), + session, + ); + } + expanded_name!("", "yChannelSelector") => { + set_attribute( + &mut self.params.y_channel_selector, + attr.parse(value), + session, + ); + } + _ => (), + } + } + } +} + +impl DisplacementMap { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + // https://www.w3.org/TR/filter-effects/#feDisplacementMapElement + // "The color-interpolation-filters property only applies to + // the in2 source image and does not apply to the in source + // image. The in source image must remain in its current color + // space. + + let input_1 = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.in1, + ColorInterpolationFilters::Auto, + )?; + let displacement_input = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.in2, + self.color_interpolation_filters, + )?; + let bounds: IRect = bounds_builder + .add_input(&input_1) + .add_input(&displacement_input) + .compute(ctx) + .clipped + .into(); + + // Displacement map's values need to be non-premultiplied. + let displacement_surface = displacement_input.surface().unpremultiply(bounds)?; + + let (sx, sy) = ctx.paffine().transform_distance(self.scale, self.scale); + + let mut surface = ExclusiveImageSurface::new( + ctx.source_graphic().width(), + ctx.source_graphic().height(), + input_1.surface().surface_type(), + )?; + + surface.draw(&mut |cr| { + for (x, y, displacement_pixel) in Pixels::within(&displacement_surface, bounds) { + let get_value = |channel| match channel { + ColorChannel::R => displacement_pixel.r, + ColorChannel::G => displacement_pixel.g, + ColorChannel::B => displacement_pixel.b, + ColorChannel::A => displacement_pixel.a, + }; + + let process = |x| f64::from(x) / 255.0 - 0.5; + + let dx = process(get_value(self.x_channel_selector)); + let dy = process(get_value(self.y_channel_selector)); + + let x = f64::from(x); + let y = f64::from(y); + let ox = sx * dx; + let oy = sy * dy; + + // Doing this in a loop doesn't look too bad performance wise, and allows not to + // manually implement bilinear or other interpolation. + cr.rectangle(x, y, 1.0, 1.0); + cr.reset_clip(); + cr.clip(); + + input_1.surface().set_as_source_surface(&cr, -ox, -oy)?; + cr.paint()?; + } + + Ok(()) + })?; + + Ok(FilterOutput { + surface: surface.share()?, + bounds, + }) + } +} + +impl FilterEffect for FeDisplacementMap { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + let cascaded = CascadedValues::new_from_node(node); + let values = cascaded.get(); + + let mut params = self.params.clone(); + params.color_interpolation_filters = values.color_interpolation_filters(); + + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::DisplacementMap(params), + }]) + } +} + +impl Parse for ColorChannel { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + Ok(parse_identifiers!( + parser, + "R" => ColorChannel::R, + "G" => ColorChannel::G, + "B" => ColorChannel::B, + "A" => ColorChannel::A, + )?) + } +} diff --git a/rsvg/src/filters/drop_shadow.rs b/rsvg/src/filters/drop_shadow.rs new file mode 100644 index 00000000..a7003f10 --- /dev/null +++ b/rsvg/src/filters/drop_shadow.rs @@ -0,0 +1,88 @@ +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::document::AcquiredNodes; +use crate::element::{set_attribute, ElementTrait}; +use crate::filter_func::drop_shadow_primitives; +use crate::node::{CascadedValues, Node}; +use crate::paint_server::resolve_color; +use crate::parsers::{NumberOptionalNumber, ParseValue}; +use crate::session::Session; +use crate::xml::Attributes; + +use super::{FilterEffect, FilterResolveError, Input, Primitive, ResolvedPrimitive}; + +/// The `feDropShadow` element. +#[derive(Default)] +pub struct FeDropShadow { + base: Primitive, + params: DropShadow, +} + +/// Resolved `feDropShadow` parameters for rendering. +#[derive(Clone)] +pub struct DropShadow { + pub in1: Input, + pub dx: f64, + pub dy: f64, + pub std_deviation: NumberOptionalNumber<f64>, +} + +impl Default for DropShadow { + /// Defaults come from <https://www.w3.org/TR/filter-effects/#feDropShadowElement> + fn default() -> Self { + Self { + in1: Default::default(), + dx: 2.0, + dy: 2.0, + std_deviation: NumberOptionalNumber(2.0, 2.0), + } + } +} + +impl ElementTrait for FeDropShadow { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.params.in1 = self.base.parse_one_input(attrs, session); + + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "dx") => { + set_attribute(&mut self.params.dx, attr.parse(value), session); + } + + expanded_name!("", "dy") => { + set_attribute(&mut self.params.dy, attr.parse(value), session); + } + + expanded_name!("", "stdDeviation") => { + set_attribute(&mut self.params.std_deviation, attr.parse(value), session); + } + + _ => (), + } + } + } +} + +impl FilterEffect for FeDropShadow { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + let cascaded = CascadedValues::new_from_node(node); + let values = cascaded.get(); + + let color = resolve_color( + &values.flood_color().0, + values.flood_opacity().0, + values.color().0, + ); + + Ok(drop_shadow_primitives( + self.params.dx, + self.params.dy, + self.params.std_deviation, + color, + )) + } +} diff --git a/rsvg/src/filters/error.rs b/rsvg/src/filters/error.rs new file mode 100644 index 00000000..1b1f9bc1 --- /dev/null +++ b/rsvg/src/filters/error.rs @@ -0,0 +1,78 @@ +use std::fmt; + +use crate::error::RenderingError; + +/// An enumeration of errors that can occur during filter primitive rendering. +#[derive(Debug, Clone)] +pub enum FilterError { + /// The filter was passed invalid input (the `in` attribute). + InvalidInput, + /// The filter was passed an invalid parameter. + InvalidParameter(String), + /// The filter input surface has an unsuccessful status. + BadInputSurfaceStatus(cairo::Error), + /// A Cairo error. + /// + /// This means that either a failed intermediate surface creation or bad intermediate surface + /// status. + CairoError(cairo::Error), + /// Error from the rendering backend. + Rendering(RenderingError), + /// A lighting filter input surface is too small. + LightingInputTooSmall, +} + +/// Errors that can occur while resolving a `FilterSpec`. +#[derive(Debug)] +pub enum FilterResolveError { + /// An `uri(#foo)` reference does not point to a `<filter>` element. + ReferenceToNonFilterElement, + /// A lighting filter has none or multiple light sources. + InvalidLightSourceCount, + /// Child node was in error. + ChildNodeInError, +} + +impl fmt::Display for FilterError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + FilterError::InvalidInput => write!(f, "invalid value of the `in` attribute"), + FilterError::InvalidParameter(ref s) => write!(f, "invalid parameter value: {s}"), + FilterError::BadInputSurfaceStatus(ref status) => { + write!(f, "invalid status of the input surface: {status}") + } + FilterError::CairoError(ref status) => write!(f, "Cairo error: {status}"), + FilterError::Rendering(ref e) => write!(f, "Rendering error: {e}"), + FilterError::LightingInputTooSmall => write!( + f, + "lighting filter input surface is too small (less than 2×2 pixels)" + ), + } + } +} + +impl fmt::Display for FilterResolveError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + FilterResolveError::ReferenceToNonFilterElement => { + write!(f, "reference to a non-filter element") + } + FilterResolveError::InvalidLightSourceCount => write!(f, "invalid light source count"), + FilterResolveError::ChildNodeInError => write!(f, "child node was in error"), + } + } +} + +impl From<cairo::Error> for FilterError { + #[inline] + fn from(x: cairo::Error) -> Self { + FilterError::CairoError(x) + } +} + +impl From<RenderingError> for FilterError { + #[inline] + fn from(e: RenderingError) -> Self { + FilterError::Rendering(e) + } +} diff --git a/rsvg/src/filters/flood.rs b/rsvg/src/filters/flood.rs new file mode 100644 index 00000000..4ebf0257 --- /dev/null +++ b/rsvg/src/filters/flood.rs @@ -0,0 +1,70 @@ +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::ElementTrait; +use crate::node::{CascadedValues, Node}; +use crate::paint_server::resolve_color; +use crate::rect::IRect; +use crate::session::Session; +use crate::xml::Attributes; + +use super::bounds::BoundsBuilder; +use super::context::{FilterContext, FilterOutput}; +use super::{ + FilterEffect, FilterError, FilterResolveError, Primitive, PrimitiveParams, ResolvedPrimitive, +}; + +/// The `feFlood` filter primitive. +#[derive(Default)] +pub struct FeFlood { + base: Primitive, +} + +/// Resolved `feFlood` primitive for rendering. +pub struct Flood { + pub color: cssparser::RGBA, +} + +impl ElementTrait for FeFlood { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.base.parse_no_inputs(attrs, session); + } +} + +impl Flood { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + _acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + let bounds: IRect = bounds_builder.compute(ctx).clipped.into(); + rsvg_log!(draw_ctx.session(), "(feFlood bounds={:?}", bounds); + + let surface = ctx.source_graphic().flood(bounds, self.color)?; + + Ok(FilterOutput { surface, bounds }) + } +} + +impl FilterEffect for FeFlood { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + let cascaded = CascadedValues::new_from_node(node); + let values = cascaded.get(); + + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::Flood(Flood { + color: resolve_color( + &values.flood_color().0, + values.flood_opacity().0, + values.color().0, + ), + }), + }]) + } +} diff --git a/rsvg/src/filters/gaussian_blur.rs b/rsvg/src/filters/gaussian_blur.rs new file mode 100644 index 00000000..b56fc9ef --- /dev/null +++ b/rsvg/src/filters/gaussian_blur.rs @@ -0,0 +1,282 @@ +use std::cmp::min; +use std::f64; + +use markup5ever::{expanded_name, local_name, namespace_url, ns}; +use nalgebra::{DMatrix, Dyn, VecStorage}; + +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::{set_attribute, ElementTrait}; +use crate::node::{CascadedValues, Node}; +use crate::parsers::{NumberOptionalNumber, ParseValue}; +use crate::properties::ColorInterpolationFilters; +use crate::rect::IRect; +use crate::session::Session; +use crate::surface_utils::{ + shared_surface::{BlurDirection, Horizontal, SharedImageSurface, Vertical}, + EdgeMode, +}; +use crate::xml::Attributes; + +use super::bounds::BoundsBuilder; +use super::context::{FilterContext, FilterOutput}; +use super::{ + FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, + ResolvedPrimitive, +}; + +/// The maximum gaussian blur kernel size. +/// +/// The value of 500 is used in webkit. +const MAXIMUM_KERNEL_SIZE: usize = 500; + +/// The `feGaussianBlur` filter primitive. +#[derive(Default)] +pub struct FeGaussianBlur { + base: Primitive, + params: GaussianBlur, +} + +/// Resolved `feGaussianBlur` primitive for rendering. +#[derive(Clone)] +pub struct GaussianBlur { + pub in1: Input, + pub std_deviation: NumberOptionalNumber<f64>, + pub color_interpolation_filters: ColorInterpolationFilters, +} + +// We need this because NumberOptionalNumber doesn't impl Default +impl Default for GaussianBlur { + fn default() -> GaussianBlur { + GaussianBlur { + in1: Default::default(), + std_deviation: NumberOptionalNumber(0.0, 0.0), + color_interpolation_filters: Default::default(), + } + } +} + +impl ElementTrait for FeGaussianBlur { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.params.in1 = self.base.parse_one_input(attrs, session); + + for (attr, value) in attrs.iter() { + if let expanded_name!("", "stdDeviation") = attr.expanded() { + set_attribute(&mut self.params.std_deviation, attr.parse(value), session); + } + } + } +} + +/// Computes a gaussian kernel line for the given standard deviation. +fn gaussian_kernel(std_deviation: f64) -> Vec<f64> { + assert!(std_deviation > 0.0); + + // Make sure there aren't any infinities. + let maximal_deviation = (MAXIMUM_KERNEL_SIZE / 2) as f64 / 3.0; + + // Values further away than std_deviation * 3 are too small to contribute anything meaningful. + let radius = ((std_deviation.min(maximal_deviation) * 3.0) + 0.5) as usize; + // Clamp the radius rather than diameter because `MAXIMUM_KERNEL_SIZE` might be even and we + // want an odd-sized kernel. + let radius = min(radius, (MAXIMUM_KERNEL_SIZE - 1) / 2); + let diameter = radius * 2 + 1; + + let mut kernel = Vec::with_capacity(diameter); + + let gauss_point = |x: f64| (-x.powi(2) / (2.0 * std_deviation.powi(2))).exp(); + + // Fill the matrix by doing numerical integration approximation from -2*std_dev to 2*std_dev, + // sampling 50 points per pixel. We do the bottom half, mirror it to the top half, then compute + // the center point. Otherwise asymmetric quantization errors will occur. The formula to + // integrate is e^-(x^2/2s^2). + for i in 0..diameter / 2 { + let base_x = (diameter / 2 + 1 - i) as f64 - 0.5; + + let mut sum = 0.0; + for j in 1..=50 { + let r = base_x + 0.02 * f64::from(j); + sum += gauss_point(r); + } + + kernel.push(sum / 50.0); + } + + // We'll compute the middle point later. + kernel.push(0.0); + + // Mirror the bottom half to the top half. + for i in 0..diameter / 2 { + let x = kernel[diameter / 2 - 1 - i]; + kernel.push(x); + } + + // Find center val -- calculate an odd number of quanta to make it symmetric, even if the + // center point is weighted slightly higher than others. + let mut sum = 0.0; + for j in 0..=50 { + let r = -0.5 + 0.02 * f64::from(j); + sum += gauss_point(r); + } + kernel[diameter / 2] = sum / 51.0; + + // Normalize the distribution by scaling the total sum to 1. + let sum = kernel.iter().sum::<f64>(); + kernel.iter_mut().for_each(|x| *x /= sum); + + kernel +} + +/// Returns a size of the box blur kernel to approximate the gaussian blur. +fn box_blur_kernel_size(std_deviation: f64) -> usize { + let d = (std_deviation * 3.0 * (2.0 * f64::consts::PI).sqrt() / 4.0 + 0.5).floor(); + let d = d.min(MAXIMUM_KERNEL_SIZE as f64); + d as usize +} + +/// Applies three box blurs to approximate the gaussian blur. +/// +/// This is intended to be used in two steps, horizontal and vertical. +fn three_box_blurs<B: BlurDirection>( + surface: &SharedImageSurface, + bounds: IRect, + std_deviation: f64, +) -> Result<SharedImageSurface, FilterError> { + let d = box_blur_kernel_size(std_deviation); + if d == 0 { + return Ok(surface.clone()); + } + + let surface = if d % 2 == 1 { + // Odd kernel sizes just get three successive box blurs. + let mut surface = surface.clone(); + + for _ in 0..3 { + surface = surface.box_blur::<B>(bounds, d, d / 2)?; + } + + surface + } else { + // Even kernel sizes have a more interesting scheme. + let surface = surface.box_blur::<B>(bounds, d, d / 2)?; + let surface = surface.box_blur::<B>(bounds, d, d / 2 - 1)?; + + let d = d + 1; + surface.box_blur::<B>(bounds, d, d / 2)? + }; + + Ok(surface) +} + +/// Applies the gaussian blur. +/// +/// This is intended to be used in two steps, horizontal and vertical. +fn gaussian_blur( + input_surface: &SharedImageSurface, + bounds: IRect, + std_deviation: f64, + vertical: bool, +) -> Result<SharedImageSurface, FilterError> { + let kernel = gaussian_kernel(std_deviation); + let (rows, cols) = if vertical { + (kernel.len(), 1) + } else { + (1, kernel.len()) + }; + let kernel = DMatrix::from_data(VecStorage::new(Dyn(rows), Dyn(cols), kernel)); + + Ok(input_surface.convolve( + bounds, + ((cols / 2) as i32, (rows / 2) as i32), + &kernel, + EdgeMode::None, + )?) +} + +impl GaussianBlur { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + let input_1 = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.in1, + self.color_interpolation_filters, + )?; + let bounds: IRect = bounds_builder + .add_input(&input_1) + .compute(ctx) + .clipped + .into(); + + let NumberOptionalNumber(std_x, std_y) = self.std_deviation; + + // "A negative value or a value of zero disables the effect of + // the given filter primitive (i.e., the result is the filter + // input image)." + if std_x <= 0.0 && std_y <= 0.0 { + return Ok(FilterOutput { + surface: input_1.surface().clone(), + bounds, + }); + } + + let (std_x, std_y) = ctx.paffine().transform_distance(std_x, std_y); + + // The deviation can become negative here due to the transform. + let std_x = std_x.abs(); + let std_y = std_y.abs(); + + // Performance TODO: gaussian blur is frequently used for shadows, operating on SourceAlpha + // (so the image is alpha-only). We can use this to not waste time processing the other + // channels. + + // Horizontal convolution. + let horiz_result_surface = if std_x >= 2.0 { + // The spec says for deviation >= 2.0 three box blurs can be used as an optimization. + three_box_blurs::<Horizontal>(input_1.surface(), bounds, std_x)? + } else if std_x != 0.0 { + gaussian_blur(input_1.surface(), bounds, std_x, false)? + } else { + input_1.surface().clone() + }; + + // Vertical convolution. + let output_surface = if std_y >= 2.0 { + // The spec says for deviation >= 2.0 three box blurs can be used as an optimization. + three_box_blurs::<Vertical>(&horiz_result_surface, bounds, std_y)? + } else if std_y != 0.0 { + gaussian_blur(&horiz_result_surface, bounds, std_y, true)? + } else { + horiz_result_surface + }; + + Ok(FilterOutput { + surface: output_surface, + bounds, + }) + } +} + +impl FilterEffect for FeGaussianBlur { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + let cascaded = CascadedValues::new_from_node(node); + let values = cascaded.get(); + + let mut params = self.params.clone(); + params.color_interpolation_filters = values.color_interpolation_filters(); + + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::GaussianBlur(params), + }]) + } +} diff --git a/rsvg/src/filters/image.rs b/rsvg/src/filters/image.rs new file mode 100644 index 00000000..eaeb08f9 --- /dev/null +++ b/rsvg/src/filters/image.rs @@ -0,0 +1,211 @@ +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::aspect_ratio::AspectRatio; +use crate::document::{AcquiredNodes, NodeId}; +use crate::drawing_ctx::DrawingCtx; +use crate::element::{set_attribute, ElementTrait}; +use crate::href::{is_href, set_href}; +use crate::node::{CascadedValues, Node}; +use crate::parsers::ParseValue; +use crate::properties::ComputedValues; +use crate::rect::Rect; +use crate::session::Session; +use crate::surface_utils::shared_surface::SharedImageSurface; +use crate::viewbox::ViewBox; +use crate::xml::Attributes; + +use super::bounds::{Bounds, BoundsBuilder}; +use super::context::{FilterContext, FilterOutput}; +use super::{ + FilterEffect, FilterError, FilterResolveError, Primitive, PrimitiveParams, ResolvedPrimitive, +}; + +/// The `feImage` filter primitive. +#[derive(Default)] +pub struct FeImage { + base: Primitive, + params: ImageParams, +} + +#[derive(Clone, Default)] +struct ImageParams { + aspect: AspectRatio, + href: Option<String>, +} + +/// Resolved `feImage` primitive for rendering. +pub struct Image { + aspect: AspectRatio, + source: Source, + feimage_values: Box<ComputedValues>, +} + +/// What a feImage references for rendering. +enum Source { + /// Nothing is referenced; ignore the filter. + None, + + /// Reference to a node. + Node(Node), + + /// Reference to an external image. This is just a URL. + ExternalImage(String), +} + +impl Image { + /// Renders the filter if the source is an existing node. + fn render_node( + &self, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + bounds: Rect, + referenced_node: &Node, + ) -> Result<SharedImageSurface, FilterError> { + // https://www.w3.org/TR/filter-effects/#feImageElement + // + // The filters spec says, "... otherwise [rendering a referenced object], the + // referenced resource is rendered according to the behavior of the use element." + // I think this means that we use the same cascading mode as <use>, i.e. the + // referenced object inherits its properties from the feImage element. + let cascaded = + CascadedValues::new_from_values(referenced_node, &self.feimage_values, None, None); + + let image = draw_ctx.draw_node_to_surface( + referenced_node, + acquired_nodes, + &cascaded, + ctx.paffine(), + ctx.source_graphic().width(), + ctx.source_graphic().height(), + )?; + + let surface = ctx.source_graphic().paint_image(bounds, &image, None)?; + + Ok(surface) + } + + /// Renders the filter if the source is an external image. + fn render_external_image( + &self, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + _draw_ctx: &DrawingCtx, + bounds: &Bounds, + url: &str, + ) -> Result<SharedImageSurface, FilterError> { + // FIXME: translate the error better here + let image = acquired_nodes + .lookup_image(url) + .map_err(|_| FilterError::InvalidInput)?; + + let rect = self.aspect.compute( + &ViewBox::from(Rect::from_size( + f64::from(image.width()), + f64::from(image.height()), + )), + &bounds.unclipped, + ); + + let surface = ctx + .source_graphic() + .paint_image(bounds.clipped, &image, Some(rect))?; + + Ok(surface) + } +} + +impl ElementTrait for FeImage { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.base.parse_no_inputs(attrs, session); + + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "preserveAspectRatio") => { + set_attribute(&mut self.params.aspect, attr.parse(value), session); + } + + // "path" is used by some older Adobe Illustrator versions + ref a if is_href(a) || *a == expanded_name!("", "path") => { + set_href(a, &mut self.params.href, Some(value.to_string())); + } + + _ => (), + } + } + } +} + +impl Image { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + let bounds = bounds_builder.compute(ctx); + + let surface = match &self.source { + Source::None => return Err(FilterError::InvalidInput), + + Source::Node(node) => { + if let Ok(acquired) = acquired_nodes.acquire_ref(node) { + self.render_node( + ctx, + acquired_nodes, + draw_ctx, + bounds.clipped, + acquired.get(), + )? + } else { + return Err(FilterError::InvalidInput); + } + } + + Source::ExternalImage(ref href) => { + self.render_external_image(ctx, acquired_nodes, draw_ctx, &bounds, href)? + } + }; + + Ok(FilterOutput { + surface, + bounds: bounds.clipped.into(), + }) + } +} + +impl FilterEffect for FeImage { + fn resolve( + &self, + acquired_nodes: &mut AcquiredNodes<'_>, + node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + let cascaded = CascadedValues::new_from_node(node); + let feimage_values = cascaded.get().clone(); + + let source = match self.params.href { + None => Source::None, + + Some(ref s) => { + if let Ok(node_id) = NodeId::parse(s) { + acquired_nodes + .acquire(&node_id) + .map(|acquired| Source::Node(acquired.get().clone())) + .unwrap_or(Source::None) + } else { + Source::ExternalImage(s.to_string()) + } + } + }; + + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::Image(Image { + aspect: self.params.aspect, + source, + feimage_values: Box::new(feimage_values), + }), + }]) + } +} diff --git a/rsvg/src/filters/lighting.rs b/rsvg/src/filters/lighting.rs new file mode 100644 index 00000000..ed39e78b --- /dev/null +++ b/rsvg/src/filters/lighting.rs @@ -0,0 +1,1090 @@ +//! Lighting filters and light nodes. + +use float_cmp::approx_eq; +use markup5ever::{expanded_name, local_name, namespace_url, ns}; +use nalgebra::{Vector2, Vector3}; +use num_traits::identities::Zero; +use rayon::prelude::*; +use std::cmp::max; + +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::{set_attribute, ElementData, ElementTrait}; +use crate::filters::{ + bounds::BoundsBuilder, + context::{FilterContext, FilterOutput}, + FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, + ResolvedPrimitive, +}; +use crate::node::{CascadedValues, Node, NodeBorrow}; +use crate::paint_server::resolve_color; +use crate::parsers::{NonNegative, NumberOptionalNumber, ParseValue}; +use crate::properties::ColorInterpolationFilters; +use crate::rect::IRect; +use crate::session::Session; +use crate::surface_utils::{ + shared_surface::{ExclusiveImageSurface, SharedImageSurface, SurfaceType}, + ImageSurfaceDataExt, Pixel, +}; +use crate::transform::Transform; +use crate::unit_interval::UnitInterval; +use crate::util::clamp; +use crate::xml::Attributes; + +/// The `feDiffuseLighting` filter primitives. +#[derive(Default)] +pub struct FeDiffuseLighting { + base: Primitive, + params: DiffuseLightingParams, +} + +#[derive(Clone)] +pub struct DiffuseLightingParams { + in1: Input, + surface_scale: f64, + kernel_unit_length: Option<(f64, f64)>, + diffuse_constant: NonNegative, +} + +impl Default for DiffuseLightingParams { + fn default() -> Self { + Self { + in1: Default::default(), + surface_scale: 1.0, + kernel_unit_length: None, + diffuse_constant: NonNegative(1.0), + } + } +} + +/// The `feSpecularLighting` filter primitives. +#[derive(Default)] +pub struct FeSpecularLighting { + base: Primitive, + params: SpecularLightingParams, +} + +#[derive(Clone)] +pub struct SpecularLightingParams { + in1: Input, + surface_scale: f64, + kernel_unit_length: Option<(f64, f64)>, + specular_constant: NonNegative, + specular_exponent: f64, +} + +impl Default for SpecularLightingParams { + fn default() -> Self { + Self { + in1: Default::default(), + surface_scale: 1.0, + kernel_unit_length: None, + specular_constant: NonNegative(1.0), + specular_exponent: 1.0, + } + } +} + +/// Resolved `feDiffuseLighting` primitive for rendering. +pub struct DiffuseLighting { + params: DiffuseLightingParams, + light: Light, +} + +/// Resolved `feSpecularLighting` primitive for rendering. +pub struct SpecularLighting { + params: SpecularLightingParams, + light: Light, +} + +/// A light source before applying affine transformations, straight out of the SVG. +#[derive(Debug, PartialEq)] +enum UntransformedLightSource { + Distant(FeDistantLight), + Point(FePointLight), + Spot(FeSpotLight), +} + +/// A light source with affine transformations applied. +enum LightSource { + Distant { + azimuth: f64, + elevation: f64, + }, + Point { + origin: Vector3<f64>, + }, + Spot { + origin: Vector3<f64>, + direction: Vector3<f64>, + specular_exponent: f64, + limiting_cone_angle: Option<f64>, + }, +} + +impl UntransformedLightSource { + fn transform(&self, paffine: Transform) -> LightSource { + match *self { + UntransformedLightSource::Distant(ref l) => l.transform(), + UntransformedLightSource::Point(ref l) => l.transform(paffine), + UntransformedLightSource::Spot(ref l) => l.transform(paffine), + } + } +} + +struct Light { + source: UntransformedLightSource, + lighting_color: cssparser::RGBA, + color_interpolation_filters: ColorInterpolationFilters, +} + +impl Light { + /// Returns the color and unit (or null) vector from the image sample to the light. + #[inline] + pub fn color_and_vector( + &self, + source: &LightSource, + x: f64, + y: f64, + z: f64, + ) -> (cssparser::RGBA, Vector3<f64>) { + let vector = match *source { + LightSource::Distant { azimuth, elevation } => { + let azimuth = azimuth.to_radians(); + let elevation = elevation.to_radians(); + Vector3::new( + azimuth.cos() * elevation.cos(), + azimuth.sin() * elevation.cos(), + elevation.sin(), + ) + } + LightSource::Point { origin } | LightSource::Spot { origin, .. } => { + let mut v = origin - Vector3::new(x, y, z); + let _ = v.try_normalize_mut(0.0); + v + } + }; + + let color = match *source { + LightSource::Spot { + direction, + specular_exponent, + limiting_cone_angle, + .. + } => { + let minus_l_dot_s = -vector.dot(&direction); + match limiting_cone_angle { + _ if minus_l_dot_s <= 0.0 => cssparser::RGBA::transparent(), + Some(a) if minus_l_dot_s < a.to_radians().cos() => { + cssparser::RGBA::transparent() + } + _ => { + let factor = minus_l_dot_s.powf(specular_exponent); + let compute = |x| (clamp(f64::from(x) * factor, 0.0, 255.0) + 0.5) as u8; + + cssparser::RGBA { + red: compute(self.lighting_color.red), + green: compute(self.lighting_color.green), + blue: compute(self.lighting_color.blue), + alpha: 255, + } + } + } + } + _ => self.lighting_color, + }; + + (color, vector) + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct FeDistantLight { + azimuth: f64, + elevation: f64, +} + +impl FeDistantLight { + fn transform(&self) -> LightSource { + LightSource::Distant { + azimuth: self.azimuth, + elevation: self.elevation, + } + } +} + +impl ElementTrait for FeDistantLight { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "azimuth") => { + set_attribute(&mut self.azimuth, attr.parse(value), session) + } + expanded_name!("", "elevation") => { + set_attribute(&mut self.elevation, attr.parse(value), session) + } + _ => (), + } + } + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct FePointLight { + x: f64, + y: f64, + z: f64, +} + +impl FePointLight { + fn transform(&self, paffine: Transform) -> LightSource { + let (x, y) = paffine.transform_point(self.x, self.y); + let z = transform_dist(paffine, self.z); + + LightSource::Point { + origin: Vector3::new(x, y, z), + } + } +} + +impl ElementTrait for FePointLight { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), + expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), + expanded_name!("", "z") => set_attribute(&mut self.z, attr.parse(value), session), + _ => (), + } + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct FeSpotLight { + x: f64, + y: f64, + z: f64, + points_at_x: f64, + points_at_y: f64, + points_at_z: f64, + specular_exponent: f64, + limiting_cone_angle: Option<f64>, +} + +// We need this because, per the spec, the initial values for all fields are 0.0 +// except for specular_exponent, which is 1. +impl Default for FeSpotLight { + fn default() -> FeSpotLight { + FeSpotLight { + x: 0.0, + y: 0.0, + z: 0.0, + points_at_x: 0.0, + points_at_y: 0.0, + points_at_z: 0.0, + specular_exponent: 1.0, + limiting_cone_angle: None, + } + } +} + +impl FeSpotLight { + fn transform(&self, paffine: Transform) -> LightSource { + let (x, y) = paffine.transform_point(self.x, self.y); + let z = transform_dist(paffine, self.z); + let (points_at_x, points_at_y) = + paffine.transform_point(self.points_at_x, self.points_at_y); + let points_at_z = transform_dist(paffine, self.points_at_z); + + let origin = Vector3::new(x, y, z); + let mut direction = Vector3::new(points_at_x, points_at_y, points_at_z) - origin; + let _ = direction.try_normalize_mut(0.0); + + LightSource::Spot { + origin, + direction, + specular_exponent: self.specular_exponent, + limiting_cone_angle: self.limiting_cone_angle, + } + } +} + +impl ElementTrait for FeSpotLight { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), + expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), + expanded_name!("", "z") => set_attribute(&mut self.z, attr.parse(value), session), + expanded_name!("", "pointsAtX") => { + set_attribute(&mut self.points_at_x, attr.parse(value), session) + } + expanded_name!("", "pointsAtY") => { + set_attribute(&mut self.points_at_y, attr.parse(value), session) + } + expanded_name!("", "pointsAtZ") => { + set_attribute(&mut self.points_at_z, attr.parse(value), session) + } + + expanded_name!("", "specularExponent") => { + set_attribute(&mut self.specular_exponent, attr.parse(value), session); + } + + expanded_name!("", "limitingConeAngle") => { + set_attribute(&mut self.limiting_cone_angle, attr.parse(value), session); + } + + _ => (), + } + } + } +} + +/// Applies the `primitiveUnits` coordinate transformation to a non-x or y distance. +#[inline] +fn transform_dist(t: Transform, d: f64) -> f64 { + d * (t.xx.powi(2) + t.yy.powi(2)).sqrt() / std::f64::consts::SQRT_2 +} + +impl ElementTrait for FeDiffuseLighting { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.params.in1 = self.base.parse_one_input(attrs, session); + + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "surfaceScale") => { + set_attribute(&mut self.params.surface_scale, attr.parse(value), session); + } + expanded_name!("", "kernelUnitLength") => { + let v: Result<NumberOptionalNumber<f64>, _> = attr.parse(value); + match v { + Ok(NumberOptionalNumber(x, y)) => { + self.params.kernel_unit_length = Some((x, y)); + } + + Err(e) => { + rsvg_log!(session, "ignoring attribute with invalid value: {}", e); + } + } + } + expanded_name!("", "diffuseConstant") => { + set_attribute( + &mut self.params.diffuse_constant, + attr.parse(value), + session, + ); + } + _ => (), + } + } + } +} + +impl DiffuseLighting { + #[inline] + fn compute_factor(&self, normal: Normal, light_vector: Vector3<f64>) -> f64 { + let k = if normal.normal.is_zero() { + // Common case of (0, 0, 1) normal. + light_vector.z + } else { + let mut n = normal + .normal + .map(|x| f64::from(x) * self.params.surface_scale / 255.); + n.component_mul_assign(&normal.factor); + let normal = Vector3::new(n.x, n.y, 1.0); + + normal.dot(&light_vector) / normal.norm() + }; + + self.params.diffuse_constant.0 * k + } +} + +impl ElementTrait for FeSpecularLighting { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.params.in1 = self.base.parse_one_input(attrs, session); + + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "surfaceScale") => { + set_attribute(&mut self.params.surface_scale, attr.parse(value), session); + } + expanded_name!("", "kernelUnitLength") => { + let v: Result<NumberOptionalNumber<f64>, _> = attr.parse(value); + match v { + Ok(NumberOptionalNumber(x, y)) => { + self.params.kernel_unit_length = Some((x, y)); + } + + Err(e) => { + rsvg_log!(session, "ignoring attribute with invalid value: {}", e); + } + } + } + expanded_name!("", "specularConstant") => { + set_attribute( + &mut self.params.specular_constant, + attr.parse(value), + session, + ); + } + expanded_name!("", "specularExponent") => { + set_attribute( + &mut self.params.specular_exponent, + attr.parse(value), + session, + ); + } + _ => (), + } + } + } +} + +impl SpecularLighting { + #[inline] + fn compute_factor(&self, normal: Normal, light_vector: Vector3<f64>) -> f64 { + let h = light_vector + Vector3::new(0.0, 0.0, 1.0); + let h_norm = h.norm(); + + if h_norm == 0.0 { + return 0.0; + } + + let n_dot_h = if normal.normal.is_zero() { + // Common case of (0, 0, 1) normal. + h.z / h_norm + } else { + let mut n = normal + .normal + .map(|x| f64::from(x) * self.params.surface_scale / 255.); + n.component_mul_assign(&normal.factor); + let normal = Vector3::new(n.x, n.y, 1.0); + normal.dot(&h) / normal.norm() / h_norm + }; + + if approx_eq!(f64, self.params.specular_exponent, 1.0) { + self.params.specular_constant.0 * n_dot_h + } else { + self.params.specular_constant.0 * n_dot_h.powf(self.params.specular_exponent) + } + } +} + +macro_rules! impl_lighting_filter { + ($lighting_type:ty, $params_name:ident, $alpha_func:ident) => { + impl $params_name { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + let input_1 = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.params.in1, + self.light.color_interpolation_filters, + )?; + let mut bounds: IRect = bounds_builder + .add_input(&input_1) + .compute(ctx) + .clipped + .into(); + let original_bounds = bounds; + + let scale = self + .params + .kernel_unit_length + .and_then(|(x, y)| { + if x <= 0.0 || y <= 0.0 { + None + } else { + Some((x, y)) + } + }) + .map(|(dx, dy)| ctx.paffine().transform_distance(dx, dy)); + + let mut input_surface = input_1.surface().clone(); + + if let Some((ox, oy)) = scale { + // Scale the input surface to match kernel_unit_length. + let (new_surface, new_bounds) = + input_surface.scale(bounds, 1.0 / ox, 1.0 / oy)?; + + input_surface = new_surface; + bounds = new_bounds; + } + + let (bounds_w, bounds_h) = bounds.size(); + + // Check if the surface is too small for normal computation. This case is + // unspecified; WebKit doesn't render anything in this case. + if bounds_w < 2 || bounds_h < 2 { + return Err(FilterError::LightingInputTooSmall); + } + + let (ox, oy) = scale.unwrap_or((1.0, 1.0)); + + let source = self.light.source.transform(ctx.paffine()); + + let mut surface = ExclusiveImageSurface::new( + input_surface.width(), + input_surface.height(), + SurfaceType::from(self.light.color_interpolation_filters), + )?; + + { + let output_stride = surface.stride() as usize; + let mut output_data = surface.data(); + let output_slice = &mut *output_data; + + let compute_output_pixel = + |output_slice: &mut [u8], base_y, x, y, normal: Normal| { + let pixel = input_surface.get_pixel(x, y); + + let scaled_x = f64::from(x) * ox; + let scaled_y = f64::from(y) * oy; + let z = f64::from(pixel.a) / 255.0 * self.params.surface_scale; + + let (color, vector) = + self.light.color_and_vector(&source, scaled_x, scaled_y, z); + + // compute the factor just once for the three colors + let factor = self.compute_factor(normal, vector); + let compute = + |x| (clamp(factor * f64::from(x), 0.0, 255.0) + 0.5) as u8; + + let r = compute(color.red); + let g = compute(color.green); + let b = compute(color.blue); + let a = $alpha_func(r, g, b); + + let output_pixel = Pixel { r, g, b, a }; + + output_slice.set_pixel(output_stride, output_pixel, x, y - base_y); + }; + + // Top left. + compute_output_pixel( + output_slice, + 0, + bounds.x0 as u32, + bounds.y0 as u32, + Normal::top_left(&input_surface, bounds), + ); + + // Top right. + compute_output_pixel( + output_slice, + 0, + bounds.x1 as u32 - 1, + bounds.y0 as u32, + Normal::top_right(&input_surface, bounds), + ); + + // Bottom left. + compute_output_pixel( + output_slice, + 0, + bounds.x0 as u32, + bounds.y1 as u32 - 1, + Normal::bottom_left(&input_surface, bounds), + ); + + // Bottom right. + compute_output_pixel( + output_slice, + 0, + bounds.x1 as u32 - 1, + bounds.y1 as u32 - 1, + Normal::bottom_right(&input_surface, bounds), + ); + + if bounds_w >= 3 { + // Top row. + for x in bounds.x0 as u32 + 1..bounds.x1 as u32 - 1 { + compute_output_pixel( + output_slice, + 0, + x, + bounds.y0 as u32, + Normal::top_row(&input_surface, bounds, x), + ); + } + + // Bottom row. + for x in bounds.x0 as u32 + 1..bounds.x1 as u32 - 1 { + compute_output_pixel( + output_slice, + 0, + x, + bounds.y1 as u32 - 1, + Normal::bottom_row(&input_surface, bounds, x), + ); + } + } + + if bounds_h >= 3 { + // Left column. + for y in bounds.y0 as u32 + 1..bounds.y1 as u32 - 1 { + compute_output_pixel( + output_slice, + 0, + bounds.x0 as u32, + y, + Normal::left_column(&input_surface, bounds, y), + ); + } + + // Right column. + for y in bounds.y0 as u32 + 1..bounds.y1 as u32 - 1 { + compute_output_pixel( + output_slice, + 0, + bounds.x1 as u32 - 1, + y, + Normal::right_column(&input_surface, bounds, y), + ); + } + } + + if bounds_w >= 3 && bounds_h >= 3 { + // Interior pixels. + let first_row = bounds.y0 as u32 + 1; + let one_past_last_row = bounds.y1 as u32 - 1; + let first_pixel = (first_row as usize) * output_stride; + let one_past_last_pixel = (one_past_last_row as usize) * output_stride; + + output_slice[first_pixel..one_past_last_pixel] + .par_chunks_mut(output_stride) + .zip(first_row..one_past_last_row) + .for_each(|(slice, y)| { + for x in bounds.x0 as u32 + 1..bounds.x1 as u32 - 1 { + compute_output_pixel( + slice, + y, + x, + y, + Normal::interior(&input_surface, bounds, x, y), + ); + } + }); + } + } + + let mut surface = surface.share()?; + + if let Some((ox, oy)) = scale { + // Scale the output surface back. + surface = surface.scale_to( + ctx.source_graphic().width(), + ctx.source_graphic().height(), + original_bounds, + ox, + oy, + )?; + + bounds = original_bounds; + } + + Ok(FilterOutput { surface, bounds }) + } + } + + impl FilterEffect for $lighting_type { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + let mut sources = node.children().rev().filter(|c| { + c.is_element() + && matches!( + *c.borrow_element_data(), + ElementData::FeDistantLight(_) + | ElementData::FePointLight(_) + | ElementData::FeSpotLight(_) + ) + }); + + let source_node = sources.next(); + if source_node.is_none() || sources.next().is_some() { + return Err(FilterResolveError::InvalidLightSourceCount); + } + + let source_node = source_node.unwrap(); + + let source = match &*source_node.borrow_element_data() { + ElementData::FeDistantLight(l) => { + UntransformedLightSource::Distant((**l).clone()) + } + ElementData::FePointLight(l) => UntransformedLightSource::Point((**l).clone()), + ElementData::FeSpotLight(l) => UntransformedLightSource::Spot((**l).clone()), + _ => unreachable!(), + }; + + let cascaded = CascadedValues::new_from_node(node); + let values = cascaded.get(); + + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::$params_name($params_name { + params: self.params.clone(), + light: Light { + source, + lighting_color: resolve_color( + &values.lighting_color().0, + UnitInterval::clamp(1.0), + values.color().0, + ), + color_interpolation_filters: values.color_interpolation_filters(), + }, + }), + }]) + } + } + }; +} + +const fn diffuse_alpha(_r: u8, _g: u8, _b: u8) -> u8 { + 255 +} + +fn specular_alpha(r: u8, g: u8, b: u8) -> u8 { + max(max(r, g), b) +} + +impl_lighting_filter!(FeDiffuseLighting, DiffuseLighting, diffuse_alpha); + +impl_lighting_filter!(FeSpecularLighting, SpecularLighting, specular_alpha); + +/// 2D normal and factor stored separately. +/// +/// The normal needs to be multiplied by `surface_scale * factor / 255` and +/// normalized with 1 as the z component. +/// pub for the purpose of accessing this from benchmarks. +#[derive(Debug, Clone, Copy)] +pub struct Normal { + pub factor: Vector2<f64>, + pub normal: Vector2<i16>, +} + +impl Normal { + #[inline] + fn new(factor_x: f64, nx: i16, factor_y: f64, ny: i16) -> Normal { + // Negative nx and ny to account for the different coordinate system. + Normal { + factor: Vector2::new(factor_x, factor_y), + normal: Vector2::new(-nx, -ny), + } + } + + /// Computes and returns the normal vector for the top left pixel for light filters. + #[inline] + pub fn top_left(surface: &SharedImageSurface, bounds: IRect) -> Normal { + // Surface needs to be at least 2×2. + assert!(bounds.width() >= 2); + assert!(bounds.height() >= 2); + + let get = |x, y| i16::from(surface.get_pixel(x, y).a); + let (x, y) = (bounds.x0 as u32, bounds.y0 as u32); + + let center = get(x, y); + let right = get(x + 1, y); + let bottom = get(x, y + 1); + let bottom_right = get(x + 1, y + 1); + + Self::new( + 2. / 3., + -2 * center + 2 * right - bottom + bottom_right, + 2. / 3., + -2 * center - right + 2 * bottom + bottom_right, + ) + } + + /// Computes and returns the normal vector for the top row pixels for light filters. + #[inline] + pub fn top_row(surface: &SharedImageSurface, bounds: IRect, x: u32) -> Normal { + assert!(x as i32 > bounds.x0); + assert!((x as i32) + 1 < bounds.x1); + assert!(bounds.height() >= 2); + + let get = |x, y| i16::from(surface.get_pixel(x, y).a); + let y = bounds.y0 as u32; + + let left = get(x - 1, y); + let center = get(x, y); + let right = get(x + 1, y); + let bottom_left = get(x - 1, y + 1); + let bottom = get(x, y + 1); + let bottom_right = get(x + 1, y + 1); + + Self::new( + 1. / 3., + -2 * left + 2 * right - bottom_left + bottom_right, + 1. / 2., + -left - 2 * center - right + bottom_left + 2 * bottom + bottom_right, + ) + } + + /// Computes and returns the normal vector for the top right pixel for light filters. + #[inline] + pub fn top_right(surface: &SharedImageSurface, bounds: IRect) -> Normal { + // Surface needs to be at least 2×2. + assert!(bounds.width() >= 2); + assert!(bounds.height() >= 2); + + let get = |x, y| i16::from(surface.get_pixel(x, y).a); + let (x, y) = (bounds.x1 as u32 - 1, bounds.y0 as u32); + + let left = get(x - 1, y); + let center = get(x, y); + let bottom_left = get(x - 1, y + 1); + let bottom = get(x, y + 1); + + Self::new( + 2. / 3., + -2 * left + 2 * center - bottom_left + bottom, + 2. / 3., + -left - 2 * center + bottom_left + 2 * bottom, + ) + } + + /// Computes and returns the normal vector for the left column pixels for light filters. + #[inline] + pub fn left_column(surface: &SharedImageSurface, bounds: IRect, y: u32) -> Normal { + assert!(y as i32 > bounds.y0); + assert!((y as i32) + 1 < bounds.y1); + assert!(bounds.width() >= 2); + + let get = |x, y| i16::from(surface.get_pixel(x, y).a); + let x = bounds.x0 as u32; + + let top = get(x, y - 1); + let top_right = get(x + 1, y - 1); + let center = get(x, y); + let right = get(x + 1, y); + let bottom = get(x, y + 1); + let bottom_right = get(x + 1, y + 1); + + Self::new( + 1. / 2., + -top + top_right - 2 * center + 2 * right - bottom + bottom_right, + 1. / 3., + -2 * top - top_right + 2 * bottom + bottom_right, + ) + } + + /// Computes and returns the normal vector for the interior pixels for light filters. + #[inline] + pub fn interior(surface: &SharedImageSurface, bounds: IRect, x: u32, y: u32) -> Normal { + assert!(x as i32 > bounds.x0); + assert!((x as i32) + 1 < bounds.x1); + assert!(y as i32 > bounds.y0); + assert!((y as i32) + 1 < bounds.y1); + + let get = |x, y| i16::from(surface.get_pixel(x, y).a); + + let top_left = get(x - 1, y - 1); + let top = get(x, y - 1); + let top_right = get(x + 1, y - 1); + let left = get(x - 1, y); + let right = get(x + 1, y); + let bottom_left = get(x - 1, y + 1); + let bottom = get(x, y + 1); + let bottom_right = get(x + 1, y + 1); + + Self::new( + 1. / 4., + -top_left + top_right - 2 * left + 2 * right - bottom_left + bottom_right, + 1. / 4., + -top_left - 2 * top - top_right + bottom_left + 2 * bottom + bottom_right, + ) + } + + /// Computes and returns the normal vector for the right column pixels for light filters. + #[inline] + pub fn right_column(surface: &SharedImageSurface, bounds: IRect, y: u32) -> Normal { + assert!(y as i32 > bounds.y0); + assert!((y as i32) + 1 < bounds.y1); + assert!(bounds.width() >= 2); + + let get = |x, y| i16::from(surface.get_pixel(x, y).a); + let x = bounds.x1 as u32 - 1; + + let top_left = get(x - 1, y - 1); + let top = get(x, y - 1); + let left = get(x - 1, y); + let center = get(x, y); + let bottom_left = get(x - 1, y + 1); + let bottom = get(x, y + 1); + + Self::new( + 1. / 2., + -top_left + top - 2 * left + 2 * center - bottom_left + bottom, + 1. / 3., + -top_left - 2 * top + bottom_left + 2 * bottom, + ) + } + + /// Computes and returns the normal vector for the bottom left pixel for light filters. + #[inline] + pub fn bottom_left(surface: &SharedImageSurface, bounds: IRect) -> Normal { + // Surface needs to be at least 2×2. + assert!(bounds.width() >= 2); + assert!(bounds.height() >= 2); + + let get = |x, y| i16::from(surface.get_pixel(x, y).a); + let (x, y) = (bounds.x0 as u32, bounds.y1 as u32 - 1); + + let top = get(x, y - 1); + let top_right = get(x + 1, y - 1); + let center = get(x, y); + let right = get(x + 1, y); + + Self::new( + 2. / 3., + -top + top_right - 2 * center + 2 * right, + 2. / 3., + -2 * top - top_right + 2 * center + right, + ) + } + + /// Computes and returns the normal vector for the bottom row pixels for light filters. + #[inline] + pub fn bottom_row(surface: &SharedImageSurface, bounds: IRect, x: u32) -> Normal { + assert!(x as i32 > bounds.x0); + assert!((x as i32) + 1 < bounds.x1); + assert!(bounds.height() >= 2); + + let get = |x, y| i16::from(surface.get_pixel(x, y).a); + let y = bounds.y1 as u32 - 1; + + let top_left = get(x - 1, y - 1); + let top = get(x, y - 1); + let top_right = get(x + 1, y - 1); + let left = get(x - 1, y); + let center = get(x, y); + let right = get(x + 1, y); + + Self::new( + 1. / 3., + -top_left + top_right - 2 * left + 2 * right, + 1. / 2., + -top_left - 2 * top - top_right + left + 2 * center + right, + ) + } + + /// Computes and returns the normal vector for the bottom right pixel for light filters. + #[inline] + pub fn bottom_right(surface: &SharedImageSurface, bounds: IRect) -> Normal { + // Surface needs to be at least 2×2. + assert!(bounds.width() >= 2); + assert!(bounds.height() >= 2); + + let get = |x, y| i16::from(surface.get_pixel(x, y).a); + let (x, y) = (bounds.x1 as u32 - 1, bounds.y1 as u32 - 1); + + let top_left = get(x - 1, y - 1); + let top = get(x, y - 1); + let left = get(x - 1, y); + let center = get(x, y); + + Self::new( + 2. / 3., + -top_left + top - 2 * left + 2 * center, + 2. / 3., + -top_left - 2 * top + left + 2 * center, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::document::Document; + + #[test] + fn extracts_light_source() { + let document = Document::load_from_bytes( + br#"<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <filter id="filter"> + <feDiffuseLighting id="diffuse_distant"> + <feDistantLight azimuth="0.0" elevation="45.0"/> + </feDiffuseLighting> + + <feSpecularLighting id="specular_point"> + <fePointLight x="1.0" y="2.0" z="3.0"/> + </feSpecularLighting> + + <feDiffuseLighting id="diffuse_spot"> + <feSpotLight x="1.0" y="2.0" z="3.0" + pointsAtX="4.0" pointsAtY="5.0" pointsAtZ="6.0" + specularExponent="7.0" limitingConeAngle="8.0"/> + </feDiffuseLighting> + </filter> +</svg> +"#, + ); + let mut acquired_nodes = AcquiredNodes::new(&document); + + let node = document.lookup_internal_node("diffuse_distant").unwrap(); + let lighting = borrow_element_as!(node, FeDiffuseLighting); + let resolved = lighting.resolve(&mut acquired_nodes, &node).unwrap(); + let ResolvedPrimitive { params, .. } = resolved.first().unwrap(); + let diffuse_lighting = match params { + PrimitiveParams::DiffuseLighting(l) => l, + _ => unreachable!(), + }; + assert_eq!( + diffuse_lighting.light.source, + UntransformedLightSource::Distant(FeDistantLight { + azimuth: 0.0, + elevation: 45.0, + }) + ); + + let node = document.lookup_internal_node("specular_point").unwrap(); + let lighting = borrow_element_as!(node, FeSpecularLighting); + let resolved = lighting.resolve(&mut acquired_nodes, &node).unwrap(); + let ResolvedPrimitive { params, .. } = resolved.first().unwrap(); + let specular_lighting = match params { + PrimitiveParams::SpecularLighting(l) => l, + _ => unreachable!(), + }; + assert_eq!( + specular_lighting.light.source, + UntransformedLightSource::Point(FePointLight { + x: 1.0, + y: 2.0, + z: 3.0, + }) + ); + + let node = document.lookup_internal_node("diffuse_spot").unwrap(); + let lighting = borrow_element_as!(node, FeDiffuseLighting); + let resolved = lighting.resolve(&mut acquired_nodes, &node).unwrap(); + let ResolvedPrimitive { params, .. } = resolved.first().unwrap(); + let diffuse_lighting = match params { + PrimitiveParams::DiffuseLighting(l) => l, + _ => unreachable!(), + }; + assert_eq!( + diffuse_lighting.light.source, + UntransformedLightSource::Spot(FeSpotLight { + x: 1.0, + y: 2.0, + z: 3.0, + points_at_x: 4.0, + points_at_y: 5.0, + points_at_z: 6.0, + specular_exponent: 7.0, + limiting_cone_angle: Some(8.0), + }) + ); + } +} diff --git a/rsvg/src/filters/merge.rs b/rsvg/src/filters/merge.rs new file mode 100644 index 00000000..0f762fdd --- /dev/null +++ b/rsvg/src/filters/merge.rs @@ -0,0 +1,217 @@ +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::{set_attribute, ElementData, ElementTrait}; +use crate::node::{CascadedValues, Node, NodeBorrow}; +use crate::parsers::ParseValue; +use crate::properties::ColorInterpolationFilters; +use crate::rect::IRect; +use crate::session::Session; +use crate::surface_utils::shared_surface::{Operator, SharedImageSurface, SurfaceType}; +use crate::xml::Attributes; + +use super::bounds::BoundsBuilder; +use super::context::{FilterContext, FilterOutput}; +use super::{ + FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, + ResolvedPrimitive, +}; + +/// The `feMerge` filter primitive. +pub struct FeMerge { + base: Primitive, +} + +/// The `<feMergeNode>` element. +#[derive(Clone, Default)] +pub struct FeMergeNode { + in1: Input, +} + +/// Resolved `feMerge` primitive for rendering. +pub struct Merge { + pub merge_nodes: Vec<MergeNode>, +} + +/// Resolved `feMergeNode` for rendering. +#[derive(Debug, Default, PartialEq)] +pub struct MergeNode { + pub in1: Input, + pub color_interpolation_filters: ColorInterpolationFilters, +} + +impl Default for FeMerge { + /// Constructs a new `Merge` with empty properties. + #[inline] + fn default() -> FeMerge { + FeMerge { + base: Default::default(), + } + } +} + +impl ElementTrait for FeMerge { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.base.parse_no_inputs(attrs, session); + } +} + +impl ElementTrait for FeMergeNode { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + if let expanded_name!("", "in") = attr.expanded() { + set_attribute(&mut self.in1, attr.parse(value), session); + } + } + } +} + +impl MergeNode { + fn render( + &self, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + bounds: IRect, + output_surface: Option<SharedImageSurface>, + ) -> Result<SharedImageSurface, FilterError> { + let input = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.in1, + self.color_interpolation_filters, + )?; + + if output_surface.is_none() { + return Ok(input.surface().clone()); + } + + input + .surface() + .compose(&output_surface.unwrap(), bounds, Operator::Over) + .map_err(FilterError::CairoError) + } +} + +impl Merge { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + // Compute the filter bounds, taking each feMergeNode's input into account. + let mut bounds_builder = bounds_builder; + for merge_node in &self.merge_nodes { + let input = ctx.get_input( + acquired_nodes, + draw_ctx, + &merge_node.in1, + merge_node.color_interpolation_filters, + )?; + bounds_builder = bounds_builder.add_input(&input); + } + + let bounds: IRect = bounds_builder.compute(ctx).clipped.into(); + + // Now merge them all. + let mut output_surface = None; + for merge_node in &self.merge_nodes { + output_surface = merge_node + .render(ctx, acquired_nodes, draw_ctx, bounds, output_surface) + .ok(); + } + + let surface = match output_surface { + Some(s) => s, + None => SharedImageSurface::empty( + ctx.source_graphic().width(), + ctx.source_graphic().height(), + SurfaceType::AlphaOnly, + )?, + }; + + Ok(FilterOutput { surface, bounds }) + } +} + +impl FilterEffect for FeMerge { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::Merge(Merge { + merge_nodes: resolve_merge_nodes(node)?, + }), + }]) + } +} + +/// Takes a feMerge and walks its children to produce a list of feMergeNode arguments. +fn resolve_merge_nodes(node: &Node) -> Result<Vec<MergeNode>, FilterResolveError> { + let mut merge_nodes = Vec::new(); + + for child in node.children().filter(|c| c.is_element()) { + let cascaded = CascadedValues::new_from_node(&child); + let values = cascaded.get(); + + if let ElementData::FeMergeNode(merge_node) = &*child.borrow_element_data() { + merge_nodes.push(MergeNode { + in1: merge_node.in1.clone(), + color_interpolation_filters: values.color_interpolation_filters(), + }); + } + } + + Ok(merge_nodes) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::document::Document; + + #[test] + fn extracts_parameters() { + let document = Document::load_from_bytes( + br#"<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <filter id="filter"> + <feMerge id="merge"> + <feMergeNode in="SourceGraphic"/> + <feMergeNode in="SourceAlpha" color-interpolation-filters="sRGB"/> + </feMerge> + </filter> +</svg> +"#, + ); + let mut acquired_nodes = AcquiredNodes::new(&document); + + let node = document.lookup_internal_node("merge").unwrap(); + let merge = borrow_element_as!(node, FeMerge); + let resolved = merge.resolve(&mut acquired_nodes, &node).unwrap(); + let ResolvedPrimitive { params, .. } = resolved.first().unwrap(); + let params = match params { + PrimitiveParams::Merge(m) => m, + _ => unreachable!(), + }; + assert_eq!( + ¶ms.merge_nodes[..], + vec![ + MergeNode { + in1: Input::SourceGraphic, + color_interpolation_filters: Default::default(), + }, + MergeNode { + in1: Input::SourceAlpha, + color_interpolation_filters: ColorInterpolationFilters::Srgb, + }, + ] + ); + } +} diff --git a/rsvg/src/filters/mod.rs b/rsvg/src/filters/mod.rs new file mode 100644 index 00000000..f0fee772 --- /dev/null +++ b/rsvg/src/filters/mod.rs @@ -0,0 +1,381 @@ +//! Entry point for the CSS filters infrastructure. + +use cssparser::{BasicParseError, Parser}; +use markup5ever::{expanded_name, local_name, namespace_url, ns}; +use std::rc::Rc; +use std::time::Instant; + +use crate::bbox::BoundingBox; +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::{set_attribute, ElementTrait}; +use crate::error::{ParseError, RenderingError}; +use crate::filter::UserSpaceFilter; +use crate::length::*; +use crate::node::Node; +use crate::paint_server::UserSpacePaintSource; +use crate::parsers::{CustomIdent, Parse, ParseValue}; +use crate::properties::ColorInterpolationFilters; +use crate::session::Session; +use crate::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; +use crate::transform::Transform; +use crate::xml::Attributes; + +mod bounds; +use self::bounds::BoundsBuilder; + +pub mod context; +use self::context::{FilterContext, FilterOutput, FilterResult}; + +mod error; +use self::error::FilterError; +pub use self::error::FilterResolveError; + +/// A filter primitive interface. +pub trait FilterEffect: ElementTrait { + fn resolve( + &self, + acquired_nodes: &mut AcquiredNodes<'_>, + node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError>; +} + +pub mod blend; +pub mod color_matrix; +pub mod component_transfer; +pub mod composite; +pub mod convolve_matrix; +pub mod displacement_map; +pub mod drop_shadow; +pub mod flood; +pub mod gaussian_blur; +pub mod image; +pub mod lighting; +pub mod merge; +pub mod morphology; +pub mod offset; +pub mod tile; +pub mod turbulence; + +pub struct FilterSpec { + pub user_space_filter: UserSpaceFilter, + pub primitives: Vec<UserSpacePrimitive>, +} + +/// Resolved parameters for each filter primitive. +/// +/// These gather all the data that a primitive may need during rendering: +/// the `feFoo` element's attributes, any computed values from its properties, +/// and parameters extracted from the element's children (for example, +/// `feMerge` gathers info from its `feMergNode` children). +pub enum PrimitiveParams { + Blend(blend::Blend), + ColorMatrix(color_matrix::ColorMatrix), + ComponentTransfer(component_transfer::ComponentTransfer), + Composite(composite::Composite), + ConvolveMatrix(convolve_matrix::ConvolveMatrix), + DiffuseLighting(lighting::DiffuseLighting), + DisplacementMap(displacement_map::DisplacementMap), + Flood(flood::Flood), + GaussianBlur(gaussian_blur::GaussianBlur), + Image(image::Image), + Merge(merge::Merge), + Morphology(morphology::Morphology), + Offset(offset::Offset), + SpecularLighting(lighting::SpecularLighting), + Tile(tile::Tile), + Turbulence(turbulence::Turbulence), +} + +impl PrimitiveParams { + /// Returns a human-readable name for a primitive. + #[rustfmt::skip] + fn name(&self) -> &'static str { + use PrimitiveParams::*; + match self { + Blend(..) => "feBlend", + ColorMatrix(..) => "feColorMatrix", + ComponentTransfer(..) => "feComponentTransfer", + Composite(..) => "feComposite", + ConvolveMatrix(..) => "feConvolveMatrix", + DiffuseLighting(..) => "feDiffuseLighting", + DisplacementMap(..) => "feDisplacementMap", + Flood(..) => "feFlood", + GaussianBlur(..) => "feGaussianBlur", + Image(..) => "feImage", + Merge(..) => "feMerge", + Morphology(..) => "feMorphology", + Offset(..) => "feOffset", + SpecularLighting(..) => "feSpecularLighting", + Tile(..) => "feTile", + Turbulence(..) => "feTurbulence", + } + } +} + +/// The base filter primitive node containing common properties. +#[derive(Default, Clone)] +pub struct Primitive { + pub x: Option<Length<Horizontal>>, + pub y: Option<Length<Vertical>>, + pub width: Option<ULength<Horizontal>>, + pub height: Option<ULength<Vertical>>, + pub result: Option<CustomIdent>, +} + +pub struct ResolvedPrimitive { + pub primitive: Primitive, + pub params: PrimitiveParams, +} + +/// A fully resolved filter primitive in user-space coordinates. +pub struct UserSpacePrimitive { + x: Option<f64>, + y: Option<f64>, + width: Option<f64>, + height: Option<f64>, + result: Option<CustomIdent>, + + params: PrimitiveParams, +} + +/// An enumeration of possible inputs for a filter primitive. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Input { + Unspecified, + SourceGraphic, + SourceAlpha, + BackgroundImage, + BackgroundAlpha, + FillPaint, + StrokePaint, + FilterOutput(CustomIdent), +} + +enum_default!(Input, Input::Unspecified); + +impl Parse for Input { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + parser + .try_parse(|p| { + parse_identifiers!( + p, + "SourceGraphic" => Input::SourceGraphic, + "SourceAlpha" => Input::SourceAlpha, + "BackgroundImage" => Input::BackgroundImage, + "BackgroundAlpha" => Input::BackgroundAlpha, + "FillPaint" => Input::FillPaint, + "StrokePaint" => Input::StrokePaint, + ) + }) + .or_else(|_: BasicParseError<'_>| { + let ident = CustomIdent::parse(parser)?; + Ok(Input::FilterOutput(ident)) + }) + } +} + +impl ResolvedPrimitive { + pub fn into_user_space(self, params: &NormalizeParams) -> UserSpacePrimitive { + let x = self.primitive.x.map(|l| l.to_user(params)); + let y = self.primitive.y.map(|l| l.to_user(params)); + let width = self.primitive.width.map(|l| l.to_user(params)); + let height = self.primitive.height.map(|l| l.to_user(params)); + + UserSpacePrimitive { + x, + y, + width, + height, + result: self.primitive.result, + params: self.params, + } + } +} + +impl UserSpacePrimitive { + /// Validates attributes and returns the `BoundsBuilder` for bounds computation. + #[inline] + fn get_bounds(&self, ctx: &FilterContext) -> BoundsBuilder { + BoundsBuilder::new(self.x, self.y, self.width, self.height, ctx.paffine()) + } +} + +impl Primitive { + fn parse_standard_attributes( + &mut self, + attrs: &Attributes, + session: &Session, + ) -> (Input, Input) { + let mut input_1 = Input::Unspecified; + let mut input_2 = Input::Unspecified; + + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), + expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), + expanded_name!("", "width") => { + set_attribute(&mut self.width, attr.parse(value), session) + } + expanded_name!("", "height") => { + set_attribute(&mut self.height, attr.parse(value), session) + } + expanded_name!("", "result") => { + set_attribute(&mut self.result, attr.parse(value), session) + } + expanded_name!("", "in") => set_attribute(&mut input_1, attr.parse(value), session), + expanded_name!("", "in2") => { + set_attribute(&mut input_2, attr.parse(value), session) + } + _ => (), + } + } + + (input_1, input_2) + } + + pub fn parse_no_inputs(&mut self, attrs: &Attributes, session: &Session) { + let (_, _) = self.parse_standard_attributes(attrs, session); + } + + pub fn parse_one_input(&mut self, attrs: &Attributes, session: &Session) -> Input { + let (input_1, _) = self.parse_standard_attributes(attrs, session); + input_1 + } + + pub fn parse_two_inputs(&mut self, attrs: &Attributes, session: &Session) -> (Input, Input) { + self.parse_standard_attributes(attrs, session) + } +} + +/// Applies a filter and returns the resulting surface. +pub fn render( + filter: &FilterSpec, + stroke_paint_source: Rc<UserSpacePaintSource>, + fill_paint_source: Rc<UserSpacePaintSource>, + source_surface: SharedImageSurface, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + transform: Transform, + node_bbox: BoundingBox, +) -> Result<SharedImageSurface, RenderingError> { + let session = draw_ctx.session().clone(); + + FilterContext::new( + &filter.user_space_filter, + stroke_paint_source, + fill_paint_source, + &source_surface, + transform, + node_bbox, + ) + .and_then(|mut filter_ctx| { + // the message has an unclosed parenthesis; we'll close it below. + rsvg_log!( + session, + "(rendering filter with effects_region={:?}", + filter_ctx.effects_region() + ); + for user_space_primitive in &filter.primitives { + let start = Instant::now(); + + match render_primitive(user_space_primitive, &filter_ctx, acquired_nodes, draw_ctx) { + Ok(output) => { + let elapsed = start.elapsed(); + rsvg_log!( + session, + "(rendered filter primitive {} in\n {} seconds)", + user_space_primitive.params.name(), + elapsed.as_secs() as f64 + f64::from(elapsed.subsec_nanos()) / 1e9 + ); + + filter_ctx.store_result(FilterResult { + name: user_space_primitive.result.clone(), + output, + }); + } + + Err(err) => { + rsvg_log!( + session, + "(filter primitive {} returned an error: {})", + user_space_primitive.params.name(), + err + ); + + // close the opening parenthesis from the message at the start of this function + rsvg_log!(session, ")"); + + // Exit early on Cairo errors. Continue rendering otherwise. + if let FilterError::CairoError(status) = err { + return Err(FilterError::CairoError(status)); + } + } + } + } + + // close the opening parenthesis from the message at the start of this function + rsvg_log!(session, ")"); + + Ok(filter_ctx.into_output()?) + }) + .or_else(|err| match err { + FilterError::CairoError(status) => { + // Exit early on Cairo errors + Err(RenderingError::from(status)) + } + + _ => { + // ignore other filter errors and just return an empty surface + Ok(SharedImageSurface::empty( + source_surface.width(), + source_surface.height(), + SurfaceType::AlphaOnly, + )?) + } + }) +} + +#[rustfmt::skip] +fn render_primitive( + primitive: &UserSpacePrimitive, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, +) -> Result<FilterOutput, FilterError> { + use PrimitiveParams::*; + + let bounds_builder = primitive.get_bounds(ctx); + + // Note that feDropShadow is not handled here. When its FilterElement::resolve() is called, + // it returns a series of lower-level primitives (flood, blur, offset, etc.) that make up + // the drop-shadow effect. + + match primitive.params { + Blend(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + ColorMatrix(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + ComponentTransfer(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + Composite(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + ConvolveMatrix(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + DiffuseLighting(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + DisplacementMap(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + Flood(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + GaussianBlur(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + Image(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + Merge(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + Morphology(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + Offset(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + SpecularLighting(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + Tile(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + Turbulence(ref p) => p.render(bounds_builder, ctx, acquired_nodes, draw_ctx), + } +} + +impl From<ColorInterpolationFilters> for SurfaceType { + fn from(c: ColorInterpolationFilters) -> Self { + match c { + ColorInterpolationFilters::LinearRgb => SurfaceType::LinearRgb, + _ => SurfaceType::SRgb, + } + } +} diff --git a/rsvg/src/filters/morphology.rs b/rsvg/src/filters/morphology.rs new file mode 100644 index 00000000..1ff7ddaa --- /dev/null +++ b/rsvg/src/filters/morphology.rs @@ -0,0 +1,200 @@ +use std::cmp::{max, min}; + +use cssparser::Parser; +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::{set_attribute, ElementTrait}; +use crate::error::*; +use crate::node::Node; +use crate::parsers::{NumberOptionalNumber, Parse, ParseValue}; +use crate::properties::ColorInterpolationFilters; +use crate::rect::IRect; +use crate::session::Session; +use crate::surface_utils::{ + iterators::{PixelRectangle, Pixels}, + shared_surface::ExclusiveImageSurface, + EdgeMode, ImageSurfaceDataExt, Pixel, +}; +use crate::xml::Attributes; + +use super::bounds::BoundsBuilder; +use super::context::{FilterContext, FilterOutput}; +use super::{ + FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, + ResolvedPrimitive, +}; + +/// Enumeration of the possible morphology operations. +#[derive(Clone)] +enum Operator { + Erode, + Dilate, +} + +enum_default!(Operator, Operator::Erode); + +/// The `feMorphology` filter primitive. +#[derive(Default)] +pub struct FeMorphology { + base: Primitive, + params: Morphology, +} + +/// Resolved `feMorphology` primitive for rendering. +#[derive(Clone)] +pub struct Morphology { + in1: Input, + operator: Operator, + radius: NumberOptionalNumber<f64>, +} + +// We need this because NumberOptionalNumber doesn't impl Default +impl Default for Morphology { + fn default() -> Morphology { + Morphology { + in1: Default::default(), + operator: Default::default(), + radius: NumberOptionalNumber(0.0, 0.0), + } + } +} + +impl ElementTrait for FeMorphology { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.params.in1 = self.base.parse_one_input(attrs, session); + + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "operator") => { + set_attribute(&mut self.params.operator, attr.parse(value), session); + } + expanded_name!("", "radius") => { + set_attribute(&mut self.params.radius, attr.parse(value), session); + } + _ => (), + } + } + } +} + +impl Morphology { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + // Although https://www.w3.org/TR/filter-effects/#propdef-color-interpolation-filters does not mention + // feMorphology as being one of the primitives that does *not* use that property, + // the SVG1.1 test for filters-morph-01-f.svg fails if we pass the value from the ComputedValues here (that + // document does not specify the color-interpolation-filters property, so it defaults to linearRGB). + // So, we pass Auto, which will get resolved to SRGB, and that makes that test pass. + // + // I suppose erosion/dilation doesn't care about the color space of the source image? + + let input_1 = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.in1, + ColorInterpolationFilters::Auto, + )?; + let bounds: IRect = bounds_builder + .add_input(&input_1) + .compute(ctx) + .clipped + .into(); + + let NumberOptionalNumber(rx, ry) = self.radius; + + if rx <= 0.0 && ry <= 0.0 { + return Ok(FilterOutput { + surface: input_1.surface().clone(), + bounds, + }); + } + + let (rx, ry) = ctx.paffine().transform_distance(rx, ry); + + // The radii can become negative here due to the transform. + // Additionally The radii being excessively large causes cpu hangups + let (rx, ry) = (rx.abs().min(10.0), ry.abs().min(10.0)); + + let mut surface = ExclusiveImageSurface::new( + ctx.source_graphic().width(), + ctx.source_graphic().height(), + input_1.surface().surface_type(), + )?; + + surface.modify(&mut |data, stride| { + for (x, y, _pixel) in Pixels::within(input_1.surface(), bounds) { + // Compute the kernel rectangle bounds. + let kernel_bounds = IRect::new( + (f64::from(x) - rx).floor() as i32, + (f64::from(y) - ry).floor() as i32, + (f64::from(x) + rx).ceil() as i32 + 1, + (f64::from(y) + ry).ceil() as i32 + 1, + ); + + // Compute the new pixel values. + let initial = match self.operator { + Operator::Erode => u8::max_value(), + Operator::Dilate => u8::min_value(), + }; + + let mut output_pixel = Pixel { + r: initial, + g: initial, + b: initial, + a: initial, + }; + + for (_x, _y, pixel) in + PixelRectangle::within(input_1.surface(), bounds, kernel_bounds, EdgeMode::None) + { + let op = match self.operator { + Operator::Erode => min, + Operator::Dilate => max, + }; + + output_pixel.r = op(output_pixel.r, pixel.r); + output_pixel.g = op(output_pixel.g, pixel.g); + output_pixel.b = op(output_pixel.b, pixel.b); + output_pixel.a = op(output_pixel.a, pixel.a); + } + + data.set_pixel(stride, output_pixel, x, y); + } + }); + + Ok(FilterOutput { + surface: surface.share()?, + bounds, + }) + } +} + +impl FilterEffect for FeMorphology { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + _node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::Morphology(self.params.clone()), + }]) + } +} + +impl Parse for Operator { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + Ok(parse_identifiers!( + parser, + "erode" => Operator::Erode, + "dilate" => Operator::Dilate, + )?) + } +} diff --git a/rsvg/src/filters/offset.rs b/rsvg/src/filters/offset.rs new file mode 100644 index 00000000..5b15b583 --- /dev/null +++ b/rsvg/src/filters/offset.rs @@ -0,0 +1,100 @@ +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::{set_attribute, ElementTrait}; +use crate::node::Node; +use crate::parsers::ParseValue; +use crate::properties::ColorInterpolationFilters; +use crate::rect::IRect; +use crate::session::Session; +use crate::xml::Attributes; + +use super::bounds::BoundsBuilder; +use super::context::{FilterContext, FilterOutput}; +use super::{ + FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, + ResolvedPrimitive, +}; + +/// The `feOffset` filter primitive. +#[derive(Default)] +pub struct FeOffset { + base: Primitive, + params: Offset, +} + +/// Resolved `feOffset` primitive for rendering. +#[derive(Clone, Default)] +pub struct Offset { + pub in1: Input, + pub dx: f64, + pub dy: f64, +} + +impl ElementTrait for FeOffset { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.params.in1 = self.base.parse_one_input(attrs, session); + + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "dx") => { + set_attribute(&mut self.params.dx, attr.parse(value), session) + } + expanded_name!("", "dy") => { + set_attribute(&mut self.params.dy, attr.parse(value), session) + } + _ => (), + } + } + } +} + +impl Offset { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + // https://www.w3.org/TR/filter-effects/#ColorInterpolationFiltersProperty + // + // "Note: The color-interpolation-filters property just has an + // effect on filter operations. Therefore, it has no effect on + // filter primitives like feOffset" + // + // This is why we pass Auto here. + let input_1 = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.in1, + ColorInterpolationFilters::Auto, + )?; + let bounds: IRect = bounds_builder + .add_input(&input_1) + .compute(ctx) + .clipped + .into(); + rsvg_log!(draw_ctx.session(), "(feOffset bounds={:?}", bounds); + + let (dx, dy) = ctx.paffine().transform_distance(self.dx, self.dy); + + let surface = input_1.surface().offset(bounds, dx, dy)?; + + Ok(FilterOutput { surface, bounds }) + } +} + +impl FilterEffect for FeOffset { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + _node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::Offset(self.params.clone()), + }]) + } +} diff --git a/rsvg/src/filters/tile.rs b/rsvg/src/filters/tile.rs new file mode 100644 index 00000000..fb50ce81 --- /dev/null +++ b/rsvg/src/filters/tile.rs @@ -0,0 +1,109 @@ +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::ElementTrait; +use crate::node::Node; +use crate::properties::ColorInterpolationFilters; +use crate::rect::IRect; +use crate::session::Session; +use crate::xml::Attributes; + +use super::bounds::BoundsBuilder; +use super::context::{FilterContext, FilterInput, FilterOutput}; +use super::{ + FilterEffect, FilterError, FilterResolveError, Input, Primitive, PrimitiveParams, + ResolvedPrimitive, +}; + +/// The `feTile` filter primitive. +#[derive(Default)] +pub struct FeTile { + base: Primitive, + params: Tile, +} + +/// Resolved `feTile` primitive for rendering. +#[derive(Clone, Default)] +pub struct Tile { + in1: Input, +} + +impl ElementTrait for FeTile { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.params.in1 = self.base.parse_one_input(attrs, session); + } +} + +impl Tile { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + acquired_nodes: &mut AcquiredNodes<'_>, + draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + // https://www.w3.org/TR/filter-effects/#ColorInterpolationFiltersProperty + // + // "Note: The color-interpolation-filters property just has an + // effect on filter operations. Therefore, it has no effect on + // filter primitives like [...], feTile" + // + // This is why we pass Auto here. + let input_1 = ctx.get_input( + acquired_nodes, + draw_ctx, + &self.in1, + ColorInterpolationFilters::Auto, + )?; + + // feTile doesn't consider its inputs in the filter primitive subregion calculation. + let bounds: IRect = bounds_builder.compute(ctx).clipped.into(); + + let surface = match input_1 { + FilterInput::StandardInput(input_surface) => input_surface, + FilterInput::PrimitiveOutput(FilterOutput { + surface: input_surface, + bounds: input_bounds, + }) => { + if input_bounds.is_empty() { + rsvg_log!( + draw_ctx.session(), + "(feTile with empty input_bounds; returning just the input surface)" + ); + + input_surface + } else { + rsvg_log!( + draw_ctx.session(), + "(feTile bounds={:?}, input_bounds={:?})", + bounds, + input_bounds + ); + + let tile_surface = input_surface.tile(input_bounds)?; + + ctx.source_graphic().paint_image_tiled( + bounds, + &tile_surface, + input_bounds.x0, + input_bounds.y0, + )? + } + } + }; + + Ok(FilterOutput { surface, bounds }) + } +} + +impl FilterEffect for FeTile { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + _node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::Tile(self.params.clone()), + }]) + } +} diff --git a/rsvg/src/filters/turbulence.rs b/rsvg/src/filters/turbulence.rs new file mode 100644 index 00000000..9e76a2a6 --- /dev/null +++ b/rsvg/src/filters/turbulence.rs @@ -0,0 +1,484 @@ +use cssparser::Parser; +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::document::AcquiredNodes; +use crate::drawing_ctx::DrawingCtx; +use crate::element::{set_attribute, ElementTrait}; +use crate::error::*; +use crate::node::{CascadedValues, Node}; +use crate::parsers::{NumberOptionalNumber, Parse, ParseValue}; +use crate::properties::ColorInterpolationFilters; +use crate::rect::IRect; +use crate::session::Session; +use crate::surface_utils::{ + shared_surface::{ExclusiveImageSurface, SurfaceType}, + ImageSurfaceDataExt, Pixel, PixelOps, +}; +use crate::util::clamp; +use crate::xml::Attributes; + +use super::bounds::BoundsBuilder; +use super::context::{FilterContext, FilterOutput}; +use super::{ + FilterEffect, FilterError, FilterResolveError, Primitive, PrimitiveParams, ResolvedPrimitive, +}; + +/// Enumeration of the tile stitching modes. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +enum StitchTiles { + Stitch, + NoStitch, +} + +enum_default!(StitchTiles, StitchTiles::NoStitch); + +/// Enumeration of the noise types. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +enum NoiseType { + FractalNoise, + Turbulence, +} + +enum_default!(NoiseType, NoiseType::Turbulence); + +/// The `feTurbulence` filter primitive. +#[derive(Default)] +pub struct FeTurbulence { + base: Primitive, + params: Turbulence, +} + +/// Resolved `feTurbulence` primitive for rendering. +#[derive(Clone)] +pub struct Turbulence { + base_frequency: NumberOptionalNumber<f64>, + num_octaves: i32, + seed: f64, + stitch_tiles: StitchTiles, + type_: NoiseType, + color_interpolation_filters: ColorInterpolationFilters, +} + +impl Default for Turbulence { + /// Constructs a new `Turbulence` with empty properties. + #[inline] + fn default() -> Turbulence { + Turbulence { + base_frequency: NumberOptionalNumber(0.0, 0.0), + num_octaves: 1, + seed: 0.0, + stitch_tiles: Default::default(), + type_: Default::default(), + color_interpolation_filters: Default::default(), + } + } +} + +impl ElementTrait for FeTurbulence { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.base.parse_no_inputs(attrs, session); + + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "baseFrequency") => { + set_attribute(&mut self.params.base_frequency, attr.parse(value), session); + } + expanded_name!("", "numOctaves") => { + set_attribute(&mut self.params.num_octaves, attr.parse(value), session); + } + // Yes, seed needs to be parsed as a number and then truncated. + expanded_name!("", "seed") => { + set_attribute(&mut self.params.seed, attr.parse(value), session); + } + expanded_name!("", "stitchTiles") => { + set_attribute(&mut self.params.stitch_tiles, attr.parse(value), session); + } + expanded_name!("", "type") => { + set_attribute(&mut self.params.type_, attr.parse(value), session) + } + _ => (), + } + } + } +} + +// Produces results in the range [1, 2**31 - 2]. +// Algorithm is: r = (a * r) mod m +// where a = 16807 and m = 2**31 - 1 = 2147483647 +// See [Park & Miller], CACM vol. 31 no. 10 p. 1195, Oct. 1988 +// To test: the algorithm should produce the result 1043618065 +// as the 10,000th generated number if the original seed is 1. +const RAND_M: i32 = 2147483647; // 2**31 - 1 +const RAND_A: i32 = 16807; // 7**5; primitive root of m +const RAND_Q: i32 = 127773; // m / a +const RAND_R: i32 = 2836; // m % a + +fn setup_seed(mut seed: i32) -> i32 { + if seed <= 0 { + seed = -(seed % (RAND_M - 1)) + 1; + } + if seed > RAND_M - 1 { + seed = RAND_M - 1; + } + seed +} + +fn random(seed: i32) -> i32 { + let mut result = RAND_A * (seed % RAND_Q) - RAND_R * (seed / RAND_Q); + if result <= 0 { + result += RAND_M; + } + result +} + +const B_SIZE: usize = 0x100; +const PERLIN_N: i32 = 0x1000; + +#[derive(Clone, Copy)] +struct NoiseGenerator { + base_frequency: (f64, f64), + num_octaves: i32, + stitch_tiles: StitchTiles, + type_: NoiseType, + + tile_width: f64, + tile_height: f64, + + lattice_selector: [usize; B_SIZE + B_SIZE + 2], + gradient: [[[f64; 2]; B_SIZE + B_SIZE + 2]; 4], +} + +#[derive(Clone, Copy)] +struct StitchInfo { + width: usize, // How much to subtract to wrap for stitching. + height: usize, + wrap_x: usize, // Minimum value to wrap. + wrap_y: usize, +} + +impl NoiseGenerator { + fn new( + seed: i32, + base_frequency: (f64, f64), + num_octaves: i32, + type_: NoiseType, + stitch_tiles: StitchTiles, + tile_width: f64, + tile_height: f64, + ) -> Self { + let mut rv = Self { + base_frequency, + num_octaves, + type_, + stitch_tiles, + + tile_width, + tile_height, + + lattice_selector: [0; B_SIZE + B_SIZE + 2], + gradient: [[[0.0; 2]; B_SIZE + B_SIZE + 2]; 4], + }; + + let mut seed = setup_seed(seed); + + for k in 0..4 { + for i in 0..B_SIZE { + rv.lattice_selector[i] = i; + for j in 0..2 { + seed = random(seed); + rv.gradient[k][i][j] = + ((seed % (B_SIZE + B_SIZE) as i32) - B_SIZE as i32) as f64 / B_SIZE as f64; + } + let s = (rv.gradient[k][i][0] * rv.gradient[k][i][0] + + rv.gradient[k][i][1] * rv.gradient[k][i][1]) + .sqrt(); + rv.gradient[k][i][0] /= s; + rv.gradient[k][i][1] /= s; + } + } + for i in (1..B_SIZE).rev() { + let k = rv.lattice_selector[i]; + seed = random(seed); + let j = seed as usize % B_SIZE; + rv.lattice_selector[i] = rv.lattice_selector[j]; + rv.lattice_selector[j] = k; + } + for i in 0..B_SIZE + 2 { + rv.lattice_selector[B_SIZE + i] = rv.lattice_selector[i]; + for k in 0..4 { + for j in 0..2 { + rv.gradient[k][B_SIZE + i][j] = rv.gradient[k][i][j]; + } + } + } + + rv + } + + fn noise2(&self, color_channel: usize, vec: [f64; 2], stitch_info: Option<StitchInfo>) -> f64 { + #![allow(clippy::many_single_char_names)] + + const BM: usize = 0xff; + + let s_curve = |t| t * t * (3. - 2. * t); + let lerp = |t, a, b| a + t * (b - a); + + let t = vec[0] + f64::from(PERLIN_N); + let mut bx0 = t as usize; + let mut bx1 = bx0 + 1; + let rx0 = t.fract(); + let rx1 = rx0 - 1.0; + let t = vec[1] + f64::from(PERLIN_N); + let mut by0 = t as usize; + let mut by1 = by0 + 1; + let ry0 = t.fract(); + let ry1 = ry0 - 1.0; + + // If stitching, adjust lattice points accordingly. + if let Some(stitch_info) = stitch_info { + if bx0 >= stitch_info.wrap_x { + bx0 -= stitch_info.width; + } + if bx1 >= stitch_info.wrap_x { + bx1 -= stitch_info.width; + } + if by0 >= stitch_info.wrap_y { + by0 -= stitch_info.height; + } + if by1 >= stitch_info.wrap_y { + by1 -= stitch_info.height; + } + } + bx0 &= BM; + bx1 &= BM; + by0 &= BM; + by1 &= BM; + let i = self.lattice_selector[bx0]; + let j = self.lattice_selector[bx1]; + let b00 = self.lattice_selector[i + by0]; + let b10 = self.lattice_selector[j + by0]; + let b01 = self.lattice_selector[i + by1]; + let b11 = self.lattice_selector[j + by1]; + let sx = s_curve(rx0); + let sy = s_curve(ry0); + let q = self.gradient[color_channel][b00]; + let u = rx0 * q[0] + ry0 * q[1]; + let q = self.gradient[color_channel][b10]; + let v = rx1 * q[0] + ry0 * q[1]; + let a = lerp(sx, u, v); + let q = self.gradient[color_channel][b01]; + let u = rx0 * q[0] + ry1 * q[1]; + let q = self.gradient[color_channel][b11]; + let v = rx1 * q[0] + ry1 * q[1]; + let b = lerp(sx, u, v); + lerp(sy, a, b) + } + + fn turbulence(&self, color_channel: usize, point: [f64; 2], tile_x: f64, tile_y: f64) -> f64 { + let mut stitch_info = None; + let mut base_frequency = self.base_frequency; + + // Adjust the base frequencies if necessary for stitching. + if self.stitch_tiles == StitchTiles::Stitch { + // When stitching tiled turbulence, the frequencies must be adjusted + // so that the tile borders will be continuous. + if base_frequency.0 != 0.0 { + let freq_lo = (self.tile_width * base_frequency.0).floor() / self.tile_width; + let freq_hi = (self.tile_width * base_frequency.0).ceil() / self.tile_width; + if base_frequency.0 / freq_lo < freq_hi / base_frequency.0 { + base_frequency.0 = freq_lo; + } else { + base_frequency.0 = freq_hi; + } + } + if base_frequency.1 != 0.0 { + let freq_lo = (self.tile_height * base_frequency.1).floor() / self.tile_height; + let freq_hi = (self.tile_height * base_frequency.1).ceil() / self.tile_height; + if base_frequency.1 / freq_lo < freq_hi / base_frequency.1 { + base_frequency.1 = freq_lo; + } else { + base_frequency.1 = freq_hi; + } + } + + // Set up initial stitch values. + let width = (self.tile_width * base_frequency.0 + 0.5) as usize; + let height = (self.tile_height * base_frequency.1 + 0.5) as usize; + stitch_info = Some(StitchInfo { + width, + wrap_x: (tile_x * base_frequency.0) as usize + PERLIN_N as usize + width, + height, + wrap_y: (tile_y * base_frequency.1) as usize + PERLIN_N as usize + height, + }); + } + + let mut sum = 0.0; + let mut vec = [point[0] * base_frequency.0, point[1] * base_frequency.1]; + let mut ratio = 1.0; + for _ in 0..self.num_octaves { + if self.type_ == NoiseType::FractalNoise { + sum += self.noise2(color_channel, vec, stitch_info) / ratio; + } else { + sum += (self.noise2(color_channel, vec, stitch_info)).abs() / ratio; + } + vec[0] *= 2.0; + vec[1] *= 2.0; + ratio *= 2.0; + if let Some(stitch_info) = stitch_info.as_mut() { + // Update stitch values. Subtracting PerlinN before the multiplication and + // adding it afterward simplifies to subtracting it once. + stitch_info.width *= 2; + stitch_info.wrap_x = 2 * stitch_info.wrap_x - PERLIN_N as usize; + stitch_info.height *= 2; + stitch_info.wrap_y = 2 * stitch_info.wrap_y - PERLIN_N as usize; + } + } + sum + } +} + +impl Turbulence { + pub fn render( + &self, + bounds_builder: BoundsBuilder, + ctx: &FilterContext, + _acquired_nodes: &mut AcquiredNodes<'_>, + _draw_ctx: &mut DrawingCtx, + ) -> Result<FilterOutput, FilterError> { + let bounds: IRect = bounds_builder.compute(ctx).clipped.into(); + + let affine = ctx.paffine().invert().unwrap(); + + let seed = clamp( + self.seed.trunc(), // per the spec, round towards zero + f64::from(i32::min_value()), + f64::from(i32::max_value()), + ) as i32; + + // "Negative values are unsupported" -> set to the initial value which is 0.0 + // + // https://drafts.fxtf.org/filter-effects/#element-attrdef-feturbulence-basefrequency + let base_frequency = { + let NumberOptionalNumber(base_freq_x, base_freq_y) = self.base_frequency; + let x = base_freq_x.max(0.0); + let y = base_freq_y.max(0.0); + (x, y) + }; + + let noise_generator = NoiseGenerator::new( + seed, + base_frequency, + self.num_octaves, + self.type_, + self.stitch_tiles, + f64::from(bounds.width()), + f64::from(bounds.height()), + ); + + // The generated color values are in the color space determined by + // color-interpolation-filters. + let surface_type = SurfaceType::from(self.color_interpolation_filters); + + let mut surface = ExclusiveImageSurface::new( + ctx.source_graphic().width(), + ctx.source_graphic().height(), + surface_type, + )?; + + surface.modify(&mut |data, stride| { + for y in bounds.y_range() { + for x in bounds.x_range() { + let point = affine.transform_point(f64::from(x), f64::from(y)); + let point = [point.0, point.1]; + + let generate = |color_channel| { + let v = noise_generator.turbulence( + color_channel, + point, + f64::from(x - bounds.x0), + f64::from(y - bounds.y0), + ); + + let v = match self.type_ { + NoiseType::FractalNoise => (v * 255.0 + 255.0) / 2.0, + NoiseType::Turbulence => v * 255.0, + }; + + (clamp(v, 0.0, 255.0) + 0.5) as u8 + }; + + let pixel = Pixel { + r: generate(0), + g: generate(1), + b: generate(2), + a: generate(3), + } + .premultiply(); + + data.set_pixel(stride, pixel, x as u32, y as u32); + } + } + }); + + Ok(FilterOutput { + surface: surface.share()?, + bounds, + }) + } +} + +impl FilterEffect for FeTurbulence { + fn resolve( + &self, + _acquired_nodes: &mut AcquiredNodes<'_>, + node: &Node, + ) -> Result<Vec<ResolvedPrimitive>, FilterResolveError> { + let cascaded = CascadedValues::new_from_node(node); + let values = cascaded.get(); + + let mut params = self.params.clone(); + params.color_interpolation_filters = values.color_interpolation_filters(); + + Ok(vec![ResolvedPrimitive { + primitive: self.base.clone(), + params: PrimitiveParams::Turbulence(params), + }]) + } +} + +impl Parse for StitchTiles { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + Ok(parse_identifiers!( + parser, + "stitch" => StitchTiles::Stitch, + "noStitch" => StitchTiles::NoStitch, + )?) + } +} + +impl Parse for NoiseType { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + Ok(parse_identifiers!( + parser, + "fractalNoise" => NoiseType::FractalNoise, + "turbulence" => NoiseType::Turbulence, + )?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn turbulence_rng() { + let mut r = 1; + r = setup_seed(r); + + for _ in 0..10_000 { + r = random(r); + } + + assert_eq!(r, 1043618065); + } +} diff --git a/rsvg/src/float_eq_cairo.rs b/rsvg/src/float_eq_cairo.rs new file mode 100644 index 00000000..74a14d9f --- /dev/null +++ b/rsvg/src/float_eq_cairo.rs @@ -0,0 +1,154 @@ +//! Utilities to compare floating-point numbers. + +use float_cmp::ApproxEq; + +// The following are copied from cairo/src/{cairo-fixed-private.h, +// cairo-fixed-type-private.h} + +const CAIRO_FIXED_FRAC_BITS: u64 = 8; +const CAIRO_MAGIC_NUMBER_FIXED: f64 = (1u64 << (52 - CAIRO_FIXED_FRAC_BITS)) as f64 * 1.5; + +fn cairo_magic_double(d: f64) -> f64 { + d + CAIRO_MAGIC_NUMBER_FIXED +} + +fn cairo_fixed_from_double(d: f64) -> i32 { + let bits = cairo_magic_double(d).to_bits(); + let lower = bits & 0xffffffff; + lower as i32 +} + +/// Implements a method to check whether two `f64` numbers would have +/// the same fixed-point representation in Cairo. +/// +/// This generally means that the absolute difference between them, +/// when taken as floating-point numbers, is less than the smallest +/// representable fraction that Cairo can represent in fixed-point. +/// +/// Implementation detail: Cairo fixed-point numbers use 24 bits for +/// the integral part, and 8 bits for the fractional part. That is, +/// the smallest fraction they can represent is 1/256. +pub trait FixedEqCairo { + fn fixed_eq_cairo(&self, other: &Self) -> bool; +} + +impl FixedEqCairo for f64 { + fn fixed_eq_cairo(&self, other: &f64) -> bool { + // FIXME: Here we have the same problem as Cairo itself: we + // don't check for overflow in the conversion of double to + // fixed-point. + cairo_fixed_from_double(*self) == cairo_fixed_from_double(*other) + } +} + +/// Checks whether two floating-point numbers are approximately equal, +/// considering Cairo's limitations on numeric representation. +/// +/// Cairo uses fixed-point numbers internally. We implement this +/// trait for `f64`, so that two numbers can be considered "close +/// enough to equal" if their absolute difference is smaller than the +/// smallest fixed-point fraction that Cairo can represent. +/// +/// Note that this trait is reliable even if the given numbers are +/// outside of the range that Cairo's fixed-point numbers can +/// represent. In that case, we check for the absolute difference, +/// and finally allow a difference of 1 unit-in-the-last-place (ULP) +/// for very large f64 values. +pub trait ApproxEqCairo: ApproxEq { + fn approx_eq_cairo(self, other: Self) -> bool; +} + +impl ApproxEqCairo for f64 { + fn approx_eq_cairo(self, other: f64) -> bool { + let cairo_smallest_fraction = 1.0 / f64::from(1 << CAIRO_FIXED_FRAC_BITS); + self.approx_eq(other, (cairo_smallest_fraction, 1)) + } +} + +// Macro for usage in unit tests +#[macro_export] +macro_rules! assert_approx_eq_cairo { + ($left:expr, $right:expr) => {{ + match ($left, $right) { + (l, r) => { + if !l.approx_eq_cairo(r) { + panic!( + r#"assertion failed: `(left == right)` + left: `{:?}`, + right: `{:?}`"#, + l, r + ) + } + } + } + }}; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn numbers_equal_in_cairo_fixed_point() { + assert!(1.0_f64.fixed_eq_cairo(&1.0_f64)); + + assert!(1.0_f64.fixed_eq_cairo(&1.001953125_f64)); // 1 + 1/512 - cairo rounds to 1 + + assert!(!1.0_f64.fixed_eq_cairo(&1.00390625_f64)); // 1 + 1/256 - cairo can represent it + } + + #[test] + fn numbers_approx_equal() { + // 0 == 1/256 - cairo can represent it, so not equal + assert!(!0.0_f64.approx_eq_cairo(0.00390635_f64)); + + // 1 == 1 + 1/256 - cairo can represent it, so not equal + assert!(!1.0_f64.approx_eq_cairo(1.00390635_f64)); + + // 0 == 1/256 - cairo can represent it, so not equal + assert!(!0.0_f64.approx_eq_cairo(-0.00390635_f64)); + + // 1 == 1 - 1/256 - cairo can represent it, so not equal + assert!(!1.0_f64.approx_eq_cairo(0.99609365_f64)); + + // 0 == 1/512 - cairo approximates to 0, so equal + assert!(0.0_f64.approx_eq_cairo(0.001953125_f64)); + + // 1 == 1 + 1/512 - cairo approximates to 1, so equal + assert!(1.0_f64.approx_eq_cairo(1.001953125_f64)); + + // 0 == -1/512 - cairo approximates to 0, so equal + assert!(0.0_f64.approx_eq_cairo(-0.001953125_f64)); + + // 1 == 1 - 1/512 - cairo approximates to 1, so equal + assert!(1.0_f64.approx_eq_cairo(0.998046875_f64)); + + // This is 2^53 compared to (2^53 + 2). When represented as + // f64, they are 1 unit-in-the-last-place (ULP) away from each + // other, since the mantissa has 53 bits (52 bits plus 1 + // "hidden" bit). The first number is an exact double, and + // the second one is the next biggest double. We consider a + // difference of 1 ULP to mean that numbers are "equal", to + // account for slight imprecision in floating-point + // calculations. Most of the time, for small values, we will + // be using the cairo_smallest_fraction from the + // implementation of approx_eq_cairo() above. For large + // values, we want the ULPs. + // + // In the second assertion, we compare 2^53 with (2^53 + 4). Those are + // 2 ULPs away, and we don't consider them equal. + assert!(9_007_199_254_740_992.0.approx_eq_cairo(9_007_199_254_740_994.0)); + assert!(!9_007_199_254_740_992.0.approx_eq_cairo(9_007_199_254_740_996.0)); + } + + #[test] + fn assert_approx_eq_cairo_should_not_panic() { + assert_approx_eq_cairo!(42_f64, 42_f64); + } + + #[test] + #[should_panic] + fn assert_approx_eq_cairo_should_panic() { + assert_approx_eq_cairo!(3_f64, 42_f64); + } +} diff --git a/rsvg/src/font_props.rs b/rsvg/src/font_props.rs new file mode 100644 index 00000000..e4babe02 --- /dev/null +++ b/rsvg/src/font_props.rs @@ -0,0 +1,878 @@ +//! CSS font properties. +//! +//! Do not import things directly from this module; use the `properties` module instead, +//! which re-exports things from here. + +use cast::{f64, u16}; +use cssparser::{Parser, Token}; + +use crate::error::*; +use crate::length::*; +use crate::parsers::{finite_f32, Parse}; +use crate::properties::ComputedValues; +use crate::property_defs::{FontStretch, FontStyle, FontVariant}; + +/// `font` shorthand property. +/// +/// CSS2: <https://www.w3.org/TR/CSS2/fonts.html#font-shorthand> +/// +/// CSS Fonts 3: <https://www.w3.org/TR/css-fonts-3/#propdef-font> +/// +/// CSS Fonts 4: <https://drafts.csswg.org/css-fonts-4/#font-prop> +/// +/// This is a shorthand, which expands to the longhands `font-family`, `font-size`, etc. +// servo/components/style/properties/shorthands/font.mako.rs is a good reference for this +#[derive(Debug, Clone, PartialEq)] +pub enum Font { + Caption, + Icon, + Menu, + MessageBox, + SmallCaption, + StatusBar, + Spec(FontSpec), +} + +/// Parameters from the `font` shorthand property. +#[derive(Debug, Default, Clone, PartialEq)] +pub struct FontSpec { + pub style: FontStyle, + pub variant: FontVariant, + pub weight: FontWeight, + pub stretch: FontStretch, + pub size: FontSize, + pub line_height: LineHeight, + pub family: FontFamily, +} + +impl Parse for Font { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Font, ParseError<'i>> { + if let Ok(f) = parse_font_spec_identifiers(parser) { + return Ok(f); + } + + // The following is stolen from servo/components/style/properties/shorthands/font.mako.rs + + let mut nb_normals = 0; + let mut style = None; + let mut variant_caps = None; + let mut weight = None; + let mut stretch = None; + let size; + + loop { + // Special-case 'normal' because it is valid in each of + // font-style, font-weight, font-variant and font-stretch. + // Leaves the values to None, 'normal' is the initial value for each of them. + if parser + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + nb_normals += 1; + continue; + } + if style.is_none() { + if let Ok(value) = parser.try_parse(FontStyle::parse) { + style = Some(value); + continue; + } + } + if weight.is_none() { + if let Ok(value) = parser.try_parse(FontWeight::parse) { + weight = Some(value); + continue; + } + } + if variant_caps.is_none() { + if let Ok(value) = parser.try_parse(FontVariant::parse) { + variant_caps = Some(value); + continue; + } + } + if stretch.is_none() { + if let Ok(value) = parser.try_parse(FontStretch::parse) { + stretch = Some(value); + continue; + } + } + size = FontSize::parse(parser)?; + break; + } + + let line_height = if parser.try_parse(|input| input.expect_delim('/')).is_ok() { + Some(LineHeight::parse(parser)?) + } else { + None + }; + + #[inline] + fn count<T>(opt: &Option<T>) -> u8 { + if opt.is_some() { + 1 + } else { + 0 + } + } + + if (count(&style) + count(&weight) + count(&variant_caps) + count(&stretch) + nb_normals) + > 4 + { + return Err(parser.new_custom_error(ValueErrorKind::parse_error( + "invalid syntax for 'font' property", + ))); + } + + let family = FontFamily::parse(parser)?; + + Ok(Font::Spec(FontSpec { + style: style.unwrap_or_default(), + variant: variant_caps.unwrap_or_default(), + weight: weight.unwrap_or_default(), + stretch: stretch.unwrap_or_default(), + size, + line_height: line_height.unwrap_or_default(), + family, + })) + } +} + +impl Font { + pub fn to_font_spec(&self) -> FontSpec { + match *self { + Font::Caption + | Font::Icon + | Font::Menu + | Font::MessageBox + | Font::SmallCaption + | Font::StatusBar => { + // We don't actually pick up the systme fonts, so reduce them to a default. + FontSpec::default() + } + + Font::Spec(ref spec) => spec.clone(), + } + } +} + +/// Parses identifiers used for system fonts. +#[rustfmt::skip] +fn parse_font_spec_identifiers<'i>(parser: &mut Parser<'i, '_>) -> Result<Font, ParseError<'i>> { + Ok(parser.try_parse(|p| { + parse_identifiers! { + p, + "caption" => Font::Caption, + "icon" => Font::Icon, + "menu" => Font::Menu, + "message-box" => Font::MessageBox, + "small-caption" => Font::SmallCaption, + "status-bar" => Font::StatusBar, + } + })?) +} + +/// `font-size` property. +/// +/// SVG1.1: <https://www.w3.org/TR/SVG11/text.html#FontSizeProperty> +/// +/// CSS2: <https://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#propdef-font-size> +/// +/// CSS Fonts 3: <https://www.w3.org/TR/css-fonts-3/#font-size-prop> +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug, Clone, PartialEq)] +pub enum FontSize { + Smaller, + Larger, + XXSmall, + XSmall, + Small, + Medium, + Large, + XLarge, + XXLarge, + Value(Length<Both>), +} + +impl FontSize { + pub fn value(&self) -> Length<Both> { + match self { + FontSize::Value(s) => *s, + _ => unreachable!(), + } + } + + pub fn compute(&self, v: &ComputedValues) -> Self { + let compute_points = |p| 12.0 * 1.2f64.powf(p) / POINTS_PER_INCH; + + let parent = v.font_size().value(); + + // The parent must already have resolved to an absolute unit + assert!( + parent.unit != LengthUnit::Percent + && parent.unit != LengthUnit::Em + && parent.unit != LengthUnit::Ex + ); + + use FontSize::*; + + #[rustfmt::skip] + let new_size = match self { + Smaller => Length::<Both>::new(parent.length / 1.2, parent.unit), + Larger => Length::<Both>::new(parent.length * 1.2, parent.unit), + XXSmall => Length::<Both>::new(compute_points(-3.0), LengthUnit::In), + XSmall => Length::<Both>::new(compute_points(-2.0), LengthUnit::In), + Small => Length::<Both>::new(compute_points(-1.0), LengthUnit::In), + Medium => Length::<Both>::new(compute_points(0.0), LengthUnit::In), + Large => Length::<Both>::new(compute_points(1.0), LengthUnit::In), + XLarge => Length::<Both>::new(compute_points(2.0), LengthUnit::In), + XXLarge => Length::<Both>::new(compute_points(3.0), LengthUnit::In), + + Value(s) if s.unit == LengthUnit::Percent => { + Length::<Both>::new(parent.length * s.length, parent.unit) + } + + Value(s) if s.unit == LengthUnit::Em => { + Length::<Both>::new(parent.length * s.length, parent.unit) + } + + Value(s) if s.unit == LengthUnit::Ex => { + // FIXME: it would be nice to know the actual Ex-height + // of the font. + Length::<Both>::new(parent.length * s.length / 2.0, parent.unit) + } + + Value(s) => *s, + }; + + FontSize::Value(new_size) + } + + pub fn to_user(&self, params: &NormalizeParams) -> f64 { + self.value().to_user(params) + } +} + +impl Parse for FontSize { + #[rustfmt::skip] + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<FontSize, ParseError<'i>> { + parser + .try_parse(|p| Length::<Both>::parse(p)) + .map(FontSize::Value) + .or_else(|_| { + Ok(parse_identifiers!( + parser, + "smaller" => FontSize::Smaller, + "larger" => FontSize::Larger, + "xx-small" => FontSize::XXSmall, + "x-small" => FontSize::XSmall, + "small" => FontSize::Small, + "medium" => FontSize::Medium, + "large" => FontSize::Large, + "x-large" => FontSize::XLarge, + "xx-large" => FontSize::XXLarge, + )?) + }) + } +} + +/// `font-weight` property. +/// +/// CSS Fonts 3: <https://www.w3.org/TR/css-fonts-3/#propdef-font-weight> +/// +/// CSS Fonts 4: <https://drafts.csswg.org/css-fonts-4/#font-weight-prop> +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum FontWeight { + Normal, + Bold, + Bolder, + Lighter, + Weight(u16), +} + +impl Parse for FontWeight { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<FontWeight, ParseError<'i>> { + parser + .try_parse(|p| { + Ok(parse_identifiers!( + p, + "normal" => FontWeight::Normal, + "bold" => FontWeight::Bold, + "bolder" => FontWeight::Bolder, + "lighter" => FontWeight::Lighter, + )?) + }) + .or_else(|_: ParseError<'_>| { + let loc = parser.current_source_location(); + let i = parser.expect_integer()?; + if (1..=1000).contains(&i) { + Ok(FontWeight::Weight(u16(i).unwrap())) + } else { + Err(loc.new_custom_error(ValueErrorKind::value_error( + "value must be between 1 and 1000 inclusive", + ))) + } + }) + } +} + +impl FontWeight { + #[rustfmt::skip] + pub fn compute(&self, v: &Self) -> Self { + use FontWeight::*; + + // Here, note that we assume that Normal=W400 and Bold=W700, per the spec. Also, + // this must match `impl From<FontWeight> for pango::Weight`. + // + // See the table at https://drafts.csswg.org/css-fonts-4/#relative-weights + + match *self { + Bolder => match v.numeric_weight() { + w if ( 1..100).contains(&w) => Weight(400), + w if (100..350).contains(&w) => Weight(400), + w if (350..550).contains(&w) => Weight(700), + w if (550..750).contains(&w) => Weight(900), + w if (750..900).contains(&w) => Weight(900), + w if 900 <= w => Weight(w), + + _ => unreachable!(), + } + + Lighter => match v.numeric_weight() { + w if ( 1..100).contains(&w) => Weight(w), + w if (100..350).contains(&w) => Weight(100), + w if (350..550).contains(&w) => Weight(100), + w if (550..750).contains(&w) => Weight(400), + w if (750..900).contains(&w) => Weight(700), + w if 900 <= w => Weight(700), + + _ => unreachable!(), + } + + _ => *self, + } + } + + // Converts the symbolic weights to numeric weights. Will panic on `Bolder` or `Lighter`. + pub fn numeric_weight(self) -> u16 { + use FontWeight::*; + + // Here, note that we assume that Normal=W400 and Bold=W700, per the spec. Also, + // this must match `impl From<FontWeight> for pango::Weight`. + match self { + Normal => 400, + Bold => 700, + Bolder | Lighter => unreachable!(), + Weight(w) => w, + } + } +} + +/// `letter-spacing` property. +/// +/// SVG1.1: <https://www.w3.org/TR/SVG11/text.html#LetterSpacingProperty> +/// +/// CSS Text 3: <https://www.w3.org/TR/css-text-3/#letter-spacing-property> +#[derive(Debug, Clone, PartialEq)] +pub enum LetterSpacing { + Normal, + Value(Length<Horizontal>), +} + +impl LetterSpacing { + pub fn value(&self) -> Length<Horizontal> { + match self { + LetterSpacing::Value(s) => *s, + _ => unreachable!(), + } + } + + pub fn compute(&self) -> Self { + let spacing = match self { + LetterSpacing::Normal => Length::<Horizontal>::new(0.0, LengthUnit::Px), + LetterSpacing::Value(s) => *s, + }; + + LetterSpacing::Value(spacing) + } + + pub fn to_user(&self, params: &NormalizeParams) -> f64 { + self.value().to_user(params) + } +} + +impl Parse for LetterSpacing { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<LetterSpacing, ParseError<'i>> { + parser + .try_parse(|p| Length::<Horizontal>::parse(p)) + .map(LetterSpacing::Value) + .or_else(|_| { + Ok(parse_identifiers!( + parser, + "normal" => LetterSpacing::Normal, + )?) + }) + } +} + +/// `line-height` property. +/// +/// CSS2: <https://www.w3.org/TR/CSS2/visudet.html#propdef-line-height> +#[derive(Debug, Clone, PartialEq)] +pub enum LineHeight { + Normal, + Number(f32), + Length(Length<Both>), + Percentage(f32), +} + +impl LineHeight { + pub fn value(&self) -> Length<Both> { + match self { + LineHeight::Length(l) => *l, + _ => unreachable!(), + } + } + + pub fn compute(&self, values: &ComputedValues) -> Self { + let font_size = values.font_size().value(); + + match *self { + LineHeight::Normal => LineHeight::Length(font_size), + + LineHeight::Number(f) | LineHeight::Percentage(f) => { + LineHeight::Length(Length::new(font_size.length * f64(f), font_size.unit)) + } + + LineHeight::Length(l) => LineHeight::Length(l), + } + } + + pub fn to_user(&self, params: &NormalizeParams) -> f64 { + self.value().to_user(params) + } +} + +impl Parse for LineHeight { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<LineHeight, ParseError<'i>> { + let state = parser.state(); + let loc = parser.current_source_location(); + + let token = parser.next()?.clone(); + + match token { + Token::Ident(ref cow) => { + if cow.eq_ignore_ascii_case("normal") { + Ok(LineHeight::Normal) + } else { + Err(parser + .new_basic_unexpected_token_error(token.clone()) + .into()) + } + } + + Token::Number { value, .. } => Ok(LineHeight::Number( + finite_f32(value).map_err(|e| loc.new_custom_error(e))?, + )), + + Token::Percentage { unit_value, .. } => Ok(LineHeight::Percentage(unit_value)), + + Token::Dimension { .. } => { + parser.reset(&state); + Ok(LineHeight::Length(Length::<Both>::parse(parser)?)) + } + + _ => Err(parser.new_basic_unexpected_token_error(token).into()), + } + } +} + +/// `font-family` property. +/// +/// SVG1.1: <https://www.w3.org/TR/SVG11/text.html#FontFamilyProperty> +/// +/// CSS 2: <https://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#propdef-font-family> +/// +/// CSS Fonts 3: <https://www.w3.org/TR/css-fonts-3/#font-family-prop> +#[derive(Debug, Clone, PartialEq)] +pub struct FontFamily(pub String); + +impl Parse for FontFamily { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<FontFamily, ParseError<'i>> { + let loc = parser.current_source_location(); + + let fonts = parser.parse_comma_separated(|parser| { + if let Ok(cow) = parser.try_parse(|p| p.expect_string_cloned()) { + if cow == "" { + return Err(loc.new_custom_error(ValueErrorKind::value_error( + "empty string is not a valid font family name", + ))); + } + + return Ok(cow); + } + + let first_ident = parser.expect_ident()?.clone(); + let mut value = first_ident.as_ref().to_owned(); + + while let Ok(cow) = parser.try_parse(|p| p.expect_ident_cloned()) { + value.push(' '); + value.push_str(&cow); + } + Ok(cssparser::CowRcStr::from(value)) + })?; + + Ok(FontFamily(fonts.join(","))) + } +} + +impl FontFamily { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +/// `glyph-orientation-vertical` property. +/// +/// Note that in SVG1.1 this could be `auto` or `<angle>`, but in SVG2/CSS3 it is +/// deprecated, and turned into a shorthand for the `text-orientation` property. Also, +/// now it only takes values `auto`, `0deg`, `90deg`, `0`, `90`. At parsing time, this +/// gets translated to fixed enum values. +/// +/// CSS Writing Modes 3: <https://www.w3.org/TR/css-writing-modes-3/#propdef-glyph-orientation-vertical> +/// +/// Obsolete SVG1.1: <https://www.w3.org/TR/SVG11/text.html#GlyphOrientationVerticalProperty> +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum GlyphOrientationVertical { + Auto, + Angle0, + Angle90, +} + +impl Parse for GlyphOrientationVertical { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<GlyphOrientationVertical, ParseError<'i>> { + let loc = parser.current_source_location(); + + if parser + .try_parse(|p| p.expect_ident_matching("auto")) + .is_ok() + { + return Ok(GlyphOrientationVertical::Auto); + } + + let token = parser.next()?.clone(); + + // Apart from "auto" (handled above), + // https://www.w3.org/TR/css-writing-modes-3/#propdef-glyph-orientation-vertical + // only allows the values "0", "90", "0deg", "90deg". So, we will look at + // individual tokens. We'll reject non-integer numbers or non-integer dimensions. + match token { + Token::Number { + int_value: Some(0), .. + } => Ok(GlyphOrientationVertical::Angle0), + + Token::Number { + int_value: Some(90), + .. + } => Ok(GlyphOrientationVertical::Angle90), + + Token::Dimension { + int_value: Some(0), + unit, + .. + } if unit.eq_ignore_ascii_case("deg") => Ok(GlyphOrientationVertical::Angle0), + + Token::Dimension { + int_value: Some(90), + unit, + .. + } if unit.eq_ignore_ascii_case("deg") => Ok(GlyphOrientationVertical::Angle90), + _ => Err(loc.new_unexpected_token_error(token.clone())), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::properties::{ParsedProperty, SpecifiedValue, SpecifiedValues}; + + #[test] + fn parses_font_shorthand() { + assert_eq!( + Font::parse_str("small-caption").unwrap(), + Font::SmallCaption, + ); + + assert_eq!( + Font::parse_str("italic bold 12px sans").unwrap(), + Font::Spec(FontSpec { + style: FontStyle::Italic, + variant: Default::default(), + weight: FontWeight::Bold, + stretch: Default::default(), + size: FontSize::Value(Length::new(12.0, LengthUnit::Px)), + line_height: Default::default(), + family: FontFamily("sans".to_string()), + }), + ); + + assert_eq!( + Font::parse_str("bold 14cm/2 serif").unwrap(), + Font::Spec(FontSpec { + style: Default::default(), + variant: Default::default(), + weight: FontWeight::Bold, + stretch: Default::default(), + size: FontSize::Value(Length::new(14.0, LengthUnit::Cm)), + line_height: LineHeight::Number(2.0), + family: FontFamily("serif".to_string()), + }), + ); + } + + #[test] + fn detects_invalid_invalid_font_size() { + assert!(FontSize::parse_str("furlong").is_err()); + } + + #[test] + fn computes_parent_relative_font_size() { + let mut specified = SpecifiedValues::default(); + specified.set_parsed_property(&ParsedProperty::FontSize(SpecifiedValue::Specified( + FontSize::parse_str("10px").unwrap(), + ))); + + let mut values = ComputedValues::default(); + specified.to_computed_values(&mut values); + + assert_eq!( + FontSize::parse_str("150%").unwrap().compute(&values), + FontSize::parse_str("15px").unwrap() + ); + + assert_eq!( + FontSize::parse_str("1.5em").unwrap().compute(&values), + FontSize::parse_str("15px").unwrap() + ); + + assert_eq!( + FontSize::parse_str("1ex").unwrap().compute(&values), + FontSize::parse_str("5px").unwrap() + ); + + let smaller = FontSize::parse_str("smaller").unwrap().compute(&values); + if let FontSize::Value(v) = smaller { + assert!(v.length < 10.0); + assert_eq!(v.unit, LengthUnit::Px); + } else { + unreachable!(); + } + + let larger = FontSize::parse_str("larger").unwrap().compute(&values); + if let FontSize::Value(v) = larger { + assert!(v.length > 10.0); + assert_eq!(v.unit, LengthUnit::Px); + } else { + unreachable!(); + } + } + + #[test] + fn parses_font_weight() { + assert_eq!( + <FontWeight as Parse>::parse_str("normal").unwrap(), + FontWeight::Normal + ); + assert_eq!( + <FontWeight as Parse>::parse_str("bold").unwrap(), + FontWeight::Bold + ); + assert_eq!( + <FontWeight as Parse>::parse_str("100").unwrap(), + FontWeight::Weight(100) + ); + } + + #[test] + fn detects_invalid_font_weight() { + assert!(<FontWeight as Parse>::parse_str("").is_err()); + assert!(<FontWeight as Parse>::parse_str("strange").is_err()); + assert!(<FontWeight as Parse>::parse_str("0").is_err()); + assert!(<FontWeight as Parse>::parse_str("-1").is_err()); + assert!(<FontWeight as Parse>::parse_str("1001").is_err()); + assert!(<FontWeight as Parse>::parse_str("3.14").is_err()); + } + + #[test] + fn parses_letter_spacing() { + assert_eq!( + <LetterSpacing as Parse>::parse_str("normal").unwrap(), + LetterSpacing::Normal + ); + assert_eq!( + <LetterSpacing as Parse>::parse_str("10em").unwrap(), + LetterSpacing::Value(Length::<Horizontal>::new(10.0, LengthUnit::Em,)) + ); + } + + #[test] + fn computes_letter_spacing() { + assert_eq!( + <LetterSpacing as Parse>::parse_str("normal") + .map(|s| s.compute()) + .unwrap(), + LetterSpacing::Value(Length::<Horizontal>::new(0.0, LengthUnit::Px,)) + ); + assert_eq!( + <LetterSpacing as Parse>::parse_str("10em") + .map(|s| s.compute()) + .unwrap(), + LetterSpacing::Value(Length::<Horizontal>::new(10.0, LengthUnit::Em,)) + ); + } + + #[test] + fn detects_invalid_invalid_letter_spacing() { + assert!(LetterSpacing::parse_str("furlong").is_err()); + } + + #[test] + fn parses_font_family() { + assert_eq!( + <FontFamily as Parse>::parse_str("'Hello world'").unwrap(), + FontFamily("Hello world".to_owned()) + ); + + assert_eq!( + <FontFamily as Parse>::parse_str("\"Hello world\"").unwrap(), + FontFamily("Hello world".to_owned()) + ); + + assert_eq!( + <FontFamily as Parse>::parse_str("\"Hello world with spaces\"").unwrap(), + FontFamily("Hello world with spaces".to_owned()) + ); + + assert_eq!( + <FontFamily as Parse>::parse_str(" Hello world ").unwrap(), + FontFamily("Hello world".to_owned()) + ); + + assert_eq!( + <FontFamily as Parse>::parse_str("Plonk").unwrap(), + FontFamily("Plonk".to_owned()) + ); + } + + #[test] + fn parses_multiple_font_family() { + assert_eq!( + <FontFamily as Parse>::parse_str("serif,monospace,\"Hello world\", with spaces ") + .unwrap(), + FontFamily("serif,monospace,Hello world,with spaces".to_owned()) + ); + } + + #[test] + fn detects_invalid_font_family() { + assert!(<FontFamily as Parse>::parse_str("").is_err()); + assert!(<FontFamily as Parse>::parse_str("''").is_err()); + assert!(<FontFamily as Parse>::parse_str("42").is_err()); + } + + #[test] + fn parses_line_height() { + assert_eq!( + <LineHeight as Parse>::parse_str("normal").unwrap(), + LineHeight::Normal + ); + + assert_eq!( + <LineHeight as Parse>::parse_str("2").unwrap(), + LineHeight::Number(2.0) + ); + + assert_eq!( + <LineHeight as Parse>::parse_str("2cm").unwrap(), + LineHeight::Length(Length::new(2.0, LengthUnit::Cm)) + ); + + assert_eq!( + <LineHeight as Parse>::parse_str("150%").unwrap(), + LineHeight::Percentage(1.5) + ); + } + + #[test] + fn detects_invalid_line_height() { + assert!(<LineHeight as Parse>::parse_str("").is_err()); + assert!(<LineHeight as Parse>::parse_str("florp").is_err()); + assert!(<LineHeight as Parse>::parse_str("3florp").is_err()); + } + + #[test] + fn computes_line_height() { + let mut specified = SpecifiedValues::default(); + specified.set_parsed_property(&ParsedProperty::FontSize(SpecifiedValue::Specified( + FontSize::parse_str("10px").unwrap(), + ))); + + let mut values = ComputedValues::default(); + specified.to_computed_values(&mut values); + + assert_eq!( + LineHeight::Normal.compute(&values), + LineHeight::Length(Length::new(10.0, LengthUnit::Px)), + ); + + assert_eq!( + LineHeight::Number(2.0).compute(&values), + LineHeight::Length(Length::new(20.0, LengthUnit::Px)), + ); + + assert_eq!( + LineHeight::Length(Length::new(3.0, LengthUnit::Cm)).compute(&values), + LineHeight::Length(Length::new(3.0, LengthUnit::Cm)), + ); + + assert_eq!( + LineHeight::parse_str("150%").unwrap().compute(&values), + LineHeight::Length(Length::new(15.0, LengthUnit::Px)), + ); + } + + #[test] + fn parses_glyph_orientation_vertical() { + assert_eq!( + <GlyphOrientationVertical as Parse>::parse_str("auto").unwrap(), + GlyphOrientationVertical::Auto + ); + assert_eq!( + <GlyphOrientationVertical as Parse>::parse_str("0").unwrap(), + GlyphOrientationVertical::Angle0 + ); + assert_eq!( + <GlyphOrientationVertical as Parse>::parse_str("0deg").unwrap(), + GlyphOrientationVertical::Angle0 + ); + assert_eq!( + <GlyphOrientationVertical as Parse>::parse_str("90").unwrap(), + GlyphOrientationVertical::Angle90 + ); + assert_eq!( + <GlyphOrientationVertical as Parse>::parse_str("90deg").unwrap(), + GlyphOrientationVertical::Angle90 + ); + } + + #[test] + fn detects_invalid_glyph_orientation_vertical() { + assert!(<GlyphOrientationVertical as Parse>::parse_str("").is_err()); + assert!(<GlyphOrientationVertical as Parse>::parse_str("0.0").is_err()); + assert!(<GlyphOrientationVertical as Parse>::parse_str("90.0").is_err()); + assert!(<GlyphOrientationVertical as Parse>::parse_str("0.0deg").is_err()); + assert!(<GlyphOrientationVertical as Parse>::parse_str("90.0deg").is_err()); + assert!(<GlyphOrientationVertical as Parse>::parse_str("0rad").is_err()); + assert!(<GlyphOrientationVertical as Parse>::parse_str("0.0rad").is_err()); + } +} diff --git a/rsvg/src/gradient.rs b/rsvg/src/gradient.rs new file mode 100644 index 00000000..e29d3fcd --- /dev/null +++ b/rsvg/src/gradient.rs @@ -0,0 +1,748 @@ +//! Gradient paint servers; the `linearGradient` and `radialGradient` elements. + +use cssparser::Parser; +use markup5ever::{ + expanded_name, local_name, namespace_url, ns, ExpandedName, LocalName, Namespace, +}; + +use crate::coord_units::CoordUnits; +use crate::document::{AcquiredNodes, NodeId, NodeStack}; +use crate::drawing_ctx::Viewport; +use crate::element::{set_attribute, ElementData, ElementTrait}; +use crate::error::*; +use crate::href::{is_href, set_href}; +use crate::length::*; +use crate::node::{CascadedValues, Node, NodeBorrow}; +use crate::paint_server::resolve_color; +use crate::parsers::{Parse, ParseValue}; +use crate::rect::{rect_to_transform, Rect}; +use crate::session::Session; +use crate::transform::{Transform, TransformAttribute}; +use crate::unit_interval::UnitInterval; +use crate::xml::Attributes; + +/// Contents of a `<stop>` element for gradient color stops +#[derive(Copy, Clone)] +pub struct ColorStop { + /// `<stop offset="..."/>` + pub offset: UnitInterval, + + /// `<stop stop-color="..." stop-opacity="..."/>` + pub rgba: cssparser::RGBA, +} + +// gradientUnits attribute; its default is objectBoundingBox +coord_units!(GradientUnits, CoordUnits::ObjectBoundingBox); + +/// spreadMethod attribute for gradients +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum SpreadMethod { + Pad, + Reflect, + Repeat, +} + +enum_default!(SpreadMethod, SpreadMethod::Pad); + +impl Parse for SpreadMethod { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<SpreadMethod, ParseError<'i>> { + Ok(parse_identifiers!( + parser, + "pad" => SpreadMethod::Pad, + "reflect" => SpreadMethod::Reflect, + "repeat" => SpreadMethod::Repeat, + )?) + } +} + +/// Node for the `<stop>` element +#[derive(Default)] +pub struct Stop { + /// `<stop offset="..."/>` + offset: UnitInterval, + /* stop-color and stop-opacity are not attributes; they are properties, so + * they go into property_defs.rs */ +} + +impl ElementTrait for Stop { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + if attr.expanded() == expanded_name!("", "offset") { + set_attribute(&mut self.offset, attr.parse(value), session); + } + } + } +} + +/// Parameters specific to each gradient type, before being resolved. +/// These will be composed together with UnreseolvedVariant from fallback +/// nodes (referenced with e.g. `<linearGradient xlink:href="#fallback">`) to form +/// a final, resolved Variant. +#[derive(Copy, Clone)] +enum UnresolvedVariant { + Linear { + x1: Option<Length<Horizontal>>, + y1: Option<Length<Vertical>>, + x2: Option<Length<Horizontal>>, + y2: Option<Length<Vertical>>, + }, + + Radial { + cx: Option<Length<Horizontal>>, + cy: Option<Length<Vertical>>, + r: Option<Length<Both>>, + fx: Option<Length<Horizontal>>, + fy: Option<Length<Vertical>>, + fr: Option<Length<Both>>, + }, +} + +/// Parameters specific to each gradient type, after resolving. +#[derive(Clone)] +enum ResolvedGradientVariant { + Linear { + x1: Length<Horizontal>, + y1: Length<Vertical>, + x2: Length<Horizontal>, + y2: Length<Vertical>, + }, + + Radial { + cx: Length<Horizontal>, + cy: Length<Vertical>, + r: Length<Both>, + fx: Length<Horizontal>, + fy: Length<Vertical>, + fr: Length<Both>, + }, +} + +/// Parameters specific to each gradient type, after normalizing to user-space units. +pub enum GradientVariant { + Linear { + x1: f64, + y1: f64, + x2: f64, + y2: f64, + }, + + Radial { + cx: f64, + cy: f64, + r: f64, + fx: f64, + fy: f64, + fr: f64, + }, +} + +impl UnresolvedVariant { + fn into_resolved(self) -> ResolvedGradientVariant { + assert!(self.is_resolved()); + + match self { + UnresolvedVariant::Linear { x1, y1, x2, y2 } => ResolvedGradientVariant::Linear { + x1: x1.unwrap(), + y1: y1.unwrap(), + x2: x2.unwrap(), + y2: y2.unwrap(), + }, + + UnresolvedVariant::Radial { + cx, + cy, + r, + fx, + fy, + fr, + } => ResolvedGradientVariant::Radial { + cx: cx.unwrap(), + cy: cy.unwrap(), + r: r.unwrap(), + fx: fx.unwrap(), + fy: fy.unwrap(), + fr: fr.unwrap(), + }, + } + } + + fn is_resolved(&self) -> bool { + match *self { + UnresolvedVariant::Linear { x1, y1, x2, y2 } => { + x1.is_some() && y1.is_some() && x2.is_some() && y2.is_some() + } + + UnresolvedVariant::Radial { + cx, + cy, + r, + fx, + fy, + fr, + } => { + cx.is_some() + && cy.is_some() + && r.is_some() + && fx.is_some() + && fy.is_some() + && fr.is_some() + } + } + } + + fn resolve_from_fallback(&self, fallback: &UnresolvedVariant) -> UnresolvedVariant { + match (*self, *fallback) { + ( + UnresolvedVariant::Linear { x1, y1, x2, y2 }, + UnresolvedVariant::Linear { + x1: fx1, + y1: fy1, + x2: fx2, + y2: fy2, + }, + ) => UnresolvedVariant::Linear { + x1: x1.or(fx1), + y1: y1.or(fy1), + x2: x2.or(fx2), + y2: y2.or(fy2), + }, + + ( + UnresolvedVariant::Radial { + cx, + cy, + r, + fx, + fy, + fr, + }, + UnresolvedVariant::Radial { + cx: f_cx, + cy: f_cy, + r: f_r, + fx: f_fx, + fy: f_fy, + fr: f_fr, + }, + ) => UnresolvedVariant::Radial { + cx: cx.or(f_cx), + cy: cy.or(f_cy), + r: r.or(f_r), + fx: fx.or(f_fx), + fy: fy.or(f_fy), + fr: fr.or(f_fr), + }, + + _ => *self, // If variants are of different types, then nothing to resolve + } + } + + // https://www.w3.org/TR/SVG/pservers.html#LinearGradients + // https://www.w3.org/TR/SVG/pservers.html#RadialGradients + fn resolve_from_defaults(&self) -> UnresolvedVariant { + match self { + UnresolvedVariant::Linear { x1, y1, x2, y2 } => UnresolvedVariant::Linear { + x1: x1.or_else(|| Some(Length::<Horizontal>::parse_str("0%").unwrap())), + y1: y1.or_else(|| Some(Length::<Vertical>::parse_str("0%").unwrap())), + x2: x2.or_else(|| Some(Length::<Horizontal>::parse_str("100%").unwrap())), + y2: y2.or_else(|| Some(Length::<Vertical>::parse_str("0%").unwrap())), + }, + + UnresolvedVariant::Radial { + cx, + cy, + r, + fx, + fy, + fr, + } => { + let cx = cx.or_else(|| Some(Length::<Horizontal>::parse_str("50%").unwrap())); + let cy = cy.or_else(|| Some(Length::<Vertical>::parse_str("50%").unwrap())); + let r = r.or_else(|| Some(Length::<Both>::parse_str("50%").unwrap())); + + // fx and fy fall back to the presentational value of cx and cy + let fx = fx.or(cx); + let fy = fy.or(cy); + let fr = fr.or_else(|| Some(Length::<Both>::parse_str("0%").unwrap())); + + UnresolvedVariant::Radial { + cx, + cy, + r, + fx, + fy, + fr, + } + } + } + } +} + +/// Fields shared by all gradient nodes +#[derive(Default)] +struct Common { + units: Option<GradientUnits>, + transform: Option<TransformAttribute>, + spread: Option<SpreadMethod>, + + fallback: Option<NodeId>, +} + +/// Node for the `<linearGradient>` element +#[derive(Default)] +pub struct LinearGradient { + common: Common, + + x1: Option<Length<Horizontal>>, + y1: Option<Length<Vertical>>, + x2: Option<Length<Horizontal>>, + y2: Option<Length<Vertical>>, +} + +/// Node for the `<radialGradient>` element +#[derive(Default)] +pub struct RadialGradient { + common: Common, + + cx: Option<Length<Horizontal>>, + cy: Option<Length<Vertical>>, + r: Option<Length<Both>>, + fx: Option<Length<Horizontal>>, + fy: Option<Length<Vertical>>, + fr: Option<Length<Both>>, +} + +/// Main structure used during gradient resolution. For unresolved +/// gradients, we store all fields as `Option<T>` - if `None`, it means +/// that the field is not specified; if `Some(T)`, it means that the +/// field was specified. +struct UnresolvedGradient { + units: Option<GradientUnits>, + transform: Option<TransformAttribute>, + spread: Option<SpreadMethod>, + stops: Option<Vec<ColorStop>>, + + variant: UnresolvedVariant, +} + +/// Resolved gradient; this is memoizable after the initial resolution. +#[derive(Clone)] +pub struct ResolvedGradient { + units: GradientUnits, + transform: TransformAttribute, + spread: SpreadMethod, + stops: Vec<ColorStop>, + + variant: ResolvedGradientVariant, +} + +/// Gradient normalized to user-space units. +pub struct UserSpaceGradient { + pub transform: Transform, + pub spread: SpreadMethod, + pub stops: Vec<ColorStop>, + + pub variant: GradientVariant, +} + +impl UnresolvedGradient { + fn into_resolved(self) -> ResolvedGradient { + assert!(self.is_resolved()); + + let UnresolvedGradient { + units, + transform, + spread, + stops, + variant, + } = self; + + match variant { + UnresolvedVariant::Linear { .. } => ResolvedGradient { + units: units.unwrap(), + transform: transform.unwrap(), + spread: spread.unwrap(), + stops: stops.unwrap(), + + variant: variant.into_resolved(), + }, + + UnresolvedVariant::Radial { .. } => ResolvedGradient { + units: units.unwrap(), + transform: transform.unwrap(), + spread: spread.unwrap(), + stops: stops.unwrap(), + + variant: variant.into_resolved(), + }, + } + } + + /// Helper for add_color_stops_from_node() + fn add_color_stop(&mut self, offset: UnitInterval, rgba: cssparser::RGBA) { + if self.stops.is_none() { + self.stops = Some(Vec::<ColorStop>::new()); + } + + if let Some(ref mut stops) = self.stops { + let last_offset = if !stops.is_empty() { + stops[stops.len() - 1].offset + } else { + UnitInterval(0.0) + }; + + let offset = if offset > last_offset { + offset + } else { + last_offset + }; + + stops.push(ColorStop { offset, rgba }); + } else { + unreachable!(); + } + } + + /// Looks for `<stop>` children inside a linearGradient or radialGradient node, + /// and adds their info to the UnresolvedGradient &self. + fn add_color_stops_from_node(&mut self, node: &Node, opacity: UnitInterval) { + assert!(matches!( + *node.borrow_element_data(), + ElementData::LinearGradient(_) | ElementData::RadialGradient(_) + )); + + for child in node.children().filter(|c| c.is_element()) { + if let ElementData::Stop(ref stop) = &*child.borrow_element_data() { + let cascaded = CascadedValues::new_from_node(&child); + let values = cascaded.get(); + + let UnitInterval(stop_opacity) = values.stop_opacity().0; + let UnitInterval(o) = opacity; + + let composed_opacity = UnitInterval(stop_opacity * o); + + let rgba = + resolve_color(&values.stop_color().0, composed_opacity, values.color().0); + + self.add_color_stop(stop.offset, rgba); + } + } + } + + fn is_resolved(&self) -> bool { + self.units.is_some() + && self.transform.is_some() + && self.spread.is_some() + && self.stops.is_some() + && self.variant.is_resolved() + } + + fn resolve_from_fallback(&self, fallback: &UnresolvedGradient) -> UnresolvedGradient { + let units = self.units.or(fallback.units); + let transform = self.transform.or(fallback.transform); + let spread = self.spread.or(fallback.spread); + let stops = self.stops.clone().or_else(|| fallback.stops.clone()); + let variant = self.variant.resolve_from_fallback(&fallback.variant); + + UnresolvedGradient { + units, + transform, + spread, + stops, + variant, + } + } + + fn resolve_from_defaults(&self) -> UnresolvedGradient { + let units = self.units.or_else(|| Some(GradientUnits::default())); + let transform = self + .transform + .or_else(|| Some(TransformAttribute::default())); + let spread = self.spread.or_else(|| Some(SpreadMethod::default())); + let stops = self.stops.clone().or_else(|| Some(Vec::<ColorStop>::new())); + let variant = self.variant.resolve_from_defaults(); + + UnresolvedGradient { + units, + transform, + spread, + stops, + variant, + } + } +} + +/// State used during the gradient resolution process +/// +/// This is the current node's gradient information, plus the fallback +/// that should be used in case that information is not complete for a +/// resolved gradient yet. +struct Unresolved { + gradient: UnresolvedGradient, + fallback: Option<NodeId>, +} + +impl LinearGradient { + fn get_unresolved_variant(&self) -> UnresolvedVariant { + UnresolvedVariant::Linear { + x1: self.x1, + y1: self.y1, + x2: self.x2, + y2: self.y2, + } + } +} + +impl RadialGradient { + fn get_unresolved_variant(&self) -> UnresolvedVariant { + UnresolvedVariant::Radial { + cx: self.cx, + cy: self.cy, + r: self.r, + fx: self.fx, + fy: self.fy, + fr: self.fr, + } + } +} + +impl Common { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "gradientUnits") => { + set_attribute(&mut self.units, attr.parse(value), session) + } + expanded_name!("", "gradientTransform") => { + set_attribute(&mut self.transform, attr.parse(value), session); + } + expanded_name!("", "spreadMethod") => { + set_attribute(&mut self.spread, attr.parse(value), session) + } + ref a if is_href(a) => { + let mut href = None; + set_attribute( + &mut href, + NodeId::parse(value).map(Some).attribute(attr.clone()), + session, + ); + set_href(a, &mut self.fallback, href); + } + _ => (), + } + } + } +} + +impl ElementTrait for LinearGradient { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.common.set_attributes(attrs, session); + + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "x1") => set_attribute(&mut self.x1, attr.parse(value), session), + expanded_name!("", "y1") => set_attribute(&mut self.y1, attr.parse(value), session), + expanded_name!("", "x2") => set_attribute(&mut self.x2, attr.parse(value), session), + expanded_name!("", "y2") => set_attribute(&mut self.y2, attr.parse(value), session), + + _ => (), + } + } + } +} + +macro_rules! impl_gradient { + ($gradient_type:ident, $other_type:ident) => { + impl $gradient_type { + fn get_unresolved(&self, node: &Node, opacity: UnitInterval) -> Unresolved { + let mut gradient = UnresolvedGradient { + units: self.common.units, + transform: self.common.transform, + spread: self.common.spread, + stops: None, + variant: self.get_unresolved_variant(), + }; + + gradient.add_color_stops_from_node(node, opacity); + + Unresolved { + gradient, + fallback: self.common.fallback.clone(), + } + } + + pub fn resolve( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + opacity: UnitInterval, + ) -> Result<ResolvedGradient, AcquireError> { + let Unresolved { + mut gradient, + mut fallback, + } = self.get_unresolved(node, opacity); + + let mut stack = NodeStack::new(); + + while !gradient.is_resolved() { + if let Some(node_id) = fallback { + let acquired = acquired_nodes.acquire(&node_id)?; + let acquired_node = acquired.get(); + + if stack.contains(acquired_node) { + return Err(AcquireError::CircularReference(acquired_node.clone())); + } + + let unresolved = match *acquired_node.borrow_element_data() { + ElementData::$gradient_type(ref g) => { + g.get_unresolved(&acquired_node, opacity) + } + ElementData::$other_type(ref g) => { + g.get_unresolved(&acquired_node, opacity) + } + _ => return Err(AcquireError::InvalidLinkType(node_id.clone())), + }; + + gradient = gradient.resolve_from_fallback(&unresolved.gradient); + fallback = unresolved.fallback; + + stack.push(acquired_node); + } else { + gradient = gradient.resolve_from_defaults(); + break; + } + } + + Ok(gradient.into_resolved()) + } + } + }; +} + +impl_gradient!(LinearGradient, RadialGradient); +impl_gradient!(RadialGradient, LinearGradient); + +impl ElementTrait for RadialGradient { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + self.common.set_attributes(attrs, session); + + // Create a local expanded name for "fr" because markup5ever doesn't have built-in + let expanded_name_fr = ExpandedName { + ns: &Namespace::from(""), + local: &LocalName::from("fr"), + }; + + for (attr, value) in attrs.iter() { + let attr_expanded = attr.expanded(); + match attr_expanded { + expanded_name!("", "cx") => set_attribute(&mut self.cx, attr.parse(value), session), + expanded_name!("", "cy") => set_attribute(&mut self.cy, attr.parse(value), session), + expanded_name!("", "r") => set_attribute(&mut self.r, attr.parse(value), session), + expanded_name!("", "fx") => set_attribute(&mut self.fx, attr.parse(value), session), + expanded_name!("", "fy") => set_attribute(&mut self.fy, attr.parse(value), session), + a if a == expanded_name_fr => { + set_attribute(&mut self.fr, attr.parse(value), session) + } + + _ => (), + } + } + } +} + +impl ResolvedGradient { + pub fn to_user_space( + &self, + object_bbox: &Option<Rect>, + viewport: &Viewport, + values: &NormalizeValues, + ) -> Option<UserSpaceGradient> { + let units = self.units.0; + let transform = rect_to_transform(object_bbox, units).ok()?; + let view_params = viewport.with_units(units); + let params = NormalizeParams::from_values(values, &view_params); + + let gradient_transform = self.transform.to_transform(); + let transform = transform.pre_transform(&gradient_transform).invert()?; + + let variant = match self.variant { + ResolvedGradientVariant::Linear { x1, y1, x2, y2 } => GradientVariant::Linear { + x1: x1.to_user(¶ms), + y1: y1.to_user(¶ms), + x2: x2.to_user(¶ms), + y2: y2.to_user(¶ms), + }, + + ResolvedGradientVariant::Radial { + cx, + cy, + r, + fx, + fy, + fr, + } => GradientVariant::Radial { + cx: cx.to_user(¶ms), + cy: cy.to_user(¶ms), + r: r.to_user(¶ms), + fx: fx.to_user(¶ms), + fy: fy.to_user(¶ms), + fr: fr.to_user(¶ms), + }, + }; + + Some(UserSpaceGradient { + transform, + spread: self.spread, + stops: self.stops.clone(), + variant, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::node::{Node, NodeData}; + use markup5ever::{namespace_url, ns, QualName}; + + #[test] + fn parses_spread_method() { + assert_eq!(SpreadMethod::parse_str("pad").unwrap(), SpreadMethod::Pad); + assert_eq!( + SpreadMethod::parse_str("reflect").unwrap(), + SpreadMethod::Reflect + ); + assert_eq!( + SpreadMethod::parse_str("repeat").unwrap(), + SpreadMethod::Repeat + ); + assert!(SpreadMethod::parse_str("foobar").is_err()); + } + + #[test] + fn gradient_resolved_from_defaults_is_really_resolved() { + let session = Session::default(); + + let node = Node::new(NodeData::new_element( + &session, + &QualName::new(None, ns!(svg), local_name!("linearGradient")), + Attributes::new(), + )); + + let unresolved = borrow_element_as!(node, LinearGradient) + .get_unresolved(&node, UnitInterval::clamp(1.0)); + let gradient = unresolved.gradient.resolve_from_defaults(); + assert!(gradient.is_resolved()); + + let node = Node::new(NodeData::new_element( + &session, + &QualName::new(None, ns!(svg), local_name!("radialGradient")), + Attributes::new(), + )); + + let unresolved = borrow_element_as!(node, RadialGradient) + .get_unresolved(&node, UnitInterval::clamp(1.0)); + let gradient = unresolved.gradient.resolve_from_defaults(); + assert!(gradient.is_resolved()); + } +} diff --git a/rsvg/src/handle.rs b/rsvg/src/handle.rs new file mode 100644 index 00000000..86428008 --- /dev/null +++ b/rsvg/src/handle.rs @@ -0,0 +1,388 @@ +//! 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 `<image xlink:href="subimage.svg"/>` + 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<LoadOptions>, + stream: &gio::InputStream, + cancellable: Option<&gio::Cancellable>, + ) -> Result<Handle, LoadingError> { + 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<bool, RenderingError> { + 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<Node, RenderingError> { + 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<Node, DefsLookupErrorKind> { + 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<BoundingBox, RenderingError> { + 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) +} diff --git a/rsvg/src/href.rs b/rsvg/src/href.rs new file mode 100644 index 00000000..b2626221 --- /dev/null +++ b/rsvg/src/href.rs @@ -0,0 +1,50 @@ +//! Handling of `xlink:href` and `href` attributes +//! +//! In SVG1.1, links to elements are done with the `xlink:href` attribute. However, SVG2 +//! reduced this to just plain `href` with no namespace: +//! <https://svgwg.org/svg2-draft/linking.html#XLinkRefAttrs> +//! +//! If an element has both `xlink:href` and `href` attributes, the `href` overrides the +//! other. We implement that logic in this module. + +use markup5ever::{expanded_name, local_name, namespace_url, ns, ExpandedName}; + +/// Returns whether the attribute is either of `xlink:href` or `href`. +/// +/// # Example +/// +/// Use with an `if` pattern inside a `match`: +/// +/// ``` +/// # use markup5ever::{expanded_name, local_name, namespace_url, ns, QualName, Prefix, Namespace, LocalName, ExpandedName}; +/// # use rsvg::doctest_only::{is_href,set_href}; +/// +/// let qual_name = QualName::new( +/// Some(Prefix::from("xlink")), +/// Namespace::from("http://www.w3.org/1999/xlink"), +/// LocalName::from("href"), +/// ); +/// +/// let value = "some_url"; +/// let mut my_field: Option<String> = None; +/// +/// match qual_name.expanded() { +/// ref name if is_href(name) => set_href(name, &mut my_field, Some(value.to_string())), +/// _ => unreachable!(), +/// } +/// ``` +pub fn is_href(name: &ExpandedName<'_>) -> bool { + matches!( + *name, + expanded_name!(xlink "href") | expanded_name!("", "href") + ) +} + +/// Sets an `href` attribute in preference over an `xlink:href` one. +/// +/// See [`is_href`] for example usage. +pub fn set_href<T>(name: &ExpandedName<'_>, dest: &mut Option<T>, href: Option<T>) { + if dest.is_none() || *name != expanded_name!(xlink "href") { + *dest = href; + } +} diff --git a/rsvg/src/image.rs b/rsvg/src/image.rs new file mode 100644 index 00000000..ece7f741 --- /dev/null +++ b/rsvg/src/image.rs @@ -0,0 +1,119 @@ +//! The `image` element. + +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::aspect_ratio::AspectRatio; +use crate::bbox::BoundingBox; +use crate::document::AcquiredNodes; +use crate::drawing_ctx::{DrawingCtx, Viewport}; +use crate::element::{set_attribute, ElementTrait}; +use crate::error::*; +use crate::href::{is_href, set_href}; +use crate::layout::{self, Layer, LayerKind, StackingContext}; +use crate::length::*; +use crate::node::{CascadedValues, Node, NodeBorrow}; +use crate::parsers::ParseValue; +use crate::rect::Rect; +use crate::session::Session; +use crate::xml::Attributes; + +/// The `<image>` element. +/// +/// Note that its x/y/width/height are properties in SVG2, so they are +/// defined as part of [the properties machinery](properties.rs). +#[derive(Default)] +pub struct Image { + aspect: AspectRatio, + href: Option<String>, +} + +impl ElementTrait for Image { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "preserveAspectRatio") => { + set_attribute(&mut self.aspect, attr.parse(value), session) + } + + // "path" is used by some older Adobe Illustrator versions + ref a if is_href(a) || *a == expanded_name!("", "path") => { + set_href(a, &mut self.href, Some(value.to_string())) + } + + _ => (), + } + } + } + + fn draw( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + let surface = match self.href { + Some(ref url) => match acquired_nodes.lookup_image(url) { + Ok(surf) => surf, + Err(e) => { + rsvg_log!( + draw_ctx.session(), + "could not load image \"{}\": {}", + url, + e + ); + return Ok(draw_ctx.empty_bbox()); + } + }, + None => return Ok(draw_ctx.empty_bbox()), + }; + + let values = cascaded.get(); + + let params = NormalizeParams::new(values, viewport); + + let x = values.x().0.to_user(¶ms); + let y = values.y().0.to_user(¶ms); + + let w = match values.width().0 { + LengthOrAuto::Length(l) => l.to_user(¶ms), + LengthOrAuto::Auto => surface.width() as f64, + }; + let h = match values.height().0 { + LengthOrAuto::Length(l) => l.to_user(¶ms), + LengthOrAuto::Auto => surface.height() as f64, + }; + + let is_visible = values.is_visible(); + + let rect = Rect::new(x, y, x + w, y + h); + + let overflow = values.overflow(); + + let image = Box::new(layout::Image { + surface, + is_visible, + rect, + aspect: self.aspect, + overflow, + }); + + let elt = node.borrow_element(); + let stacking_ctx = StackingContext::new( + draw_ctx.session(), + acquired_nodes, + &elt, + values.transform(), + values, + ); + + let layer = Layer { + kind: LayerKind::Image(image), + stacking_ctx, + }; + + draw_ctx.draw_layer(&layer, acquired_nodes, clipping, viewport) + } +} diff --git a/rsvg/src/io.rs b/rsvg/src/io.rs new file mode 100644 index 00000000..a5748aa3 --- /dev/null +++ b/rsvg/src/io.rs @@ -0,0 +1,122 @@ +//! Utilities to acquire streams and data from from URLs. + +use data_url::{mime::Mime, DataUrl}; +use gio::{prelude::FileExt, Cancellable, File as GFile, InputStream, MemoryInputStream}; +use glib::{self, Bytes as GBytes, Cast}; +use std::fmt; +use std::str::FromStr; + +use crate::url_resolver::AllowedUrl; + +pub enum IoError { + BadDataUrl, + Glib(glib::Error), +} + +impl From<glib::Error> for IoError { + fn from(e: glib::Error) -> IoError { + IoError::Glib(e) + } +} + +impl fmt::Display for IoError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + IoError::BadDataUrl => write!(f, "invalid data: URL"), + IoError::Glib(ref e) => e.fmt(f), + } + } +} + +pub struct BinaryData { + pub data: Vec<u8>, + pub mime_type: Mime, +} + +fn decode_data_uri(uri: &str) -> Result<BinaryData, IoError> { + let data_url = DataUrl::process(uri).map_err(|_| IoError::BadDataUrl)?; + + let mime = data_url.mime_type(); + + // data_url::mime::Mime doesn't impl Clone, so do it by hand + + let mime_type = Mime { + type_: mime.type_.clone(), + subtype: mime.subtype.clone(), + parameters: mime.parameters.clone(), + }; + + let (bytes, fragment_id) = data_url.decode_to_vec().map_err(|_| IoError::BadDataUrl)?; + + // See issue #377 - per the data: URL spec + // (https://fetch.spec.whatwg.org/#data-urls), those URLs cannot + // have fragment identifiers. So, just return an error if we find + // one. This probably indicates mis-quoted SVG data inside the + // data: URL. + if fragment_id.is_some() { + return Err(IoError::BadDataUrl); + } + + Ok(BinaryData { + data: bytes, + mime_type, + }) +} + +/// Creates a stream for reading. The url can be a data: URL or a plain URI. +pub fn acquire_stream( + aurl: &AllowedUrl, + cancellable: Option<&Cancellable>, +) -> Result<InputStream, IoError> { + let uri = aurl.as_str(); + + if uri.starts_with("data:") { + let BinaryData { data, .. } = decode_data_uri(uri)?; + + // { + // use std::fs::File; + // use std::io::prelude::*; + // + // let mut file = File::create("data.bin").unwrap(); + // file.write_all(&data).unwrap(); + // } + + let stream = MemoryInputStream::from_bytes(&GBytes::from_owned(data)); + Ok(stream.upcast::<InputStream>()) + } else { + let file = GFile::for_uri(uri); + let stream = file.read(cancellable)?; + + Ok(stream.upcast::<InputStream>()) + } +} + +/// Reads the entire contents pointed by an URL. The url can be a data: URL or a plain URI. +pub fn acquire_data( + aurl: &AllowedUrl, + cancellable: Option<&Cancellable>, +) -> Result<BinaryData, IoError> { + let uri = aurl.as_str(); + + if uri.starts_with("data:") { + Ok(decode_data_uri(uri)?) + } else { + let file = GFile::for_uri(uri); + let (contents, _etag) = file.load_contents(cancellable)?; + + let (content_type, _uncertain) = gio::content_type_guess(Some(uri), &contents); + + let mime_type = if let Some(mime_type_str) = gio::content_type_get_mime_type(&content_type) + { + Mime::from_str(&mime_type_str) + .expect("gio::content_type_get_mime_type returned an invalid MIME-type!?") + } else { + Mime::from_str("application/octet-stream").unwrap() + }; + + Ok(BinaryData { + data: contents, + mime_type, + }) + } +} diff --git a/rsvg/src/iri.rs b/rsvg/src/iri.rs new file mode 100644 index 00000000..e3272fe6 --- /dev/null +++ b/rsvg/src/iri.rs @@ -0,0 +1,90 @@ +//! CSS funciri values. + +use cssparser::Parser; + +use crate::document::NodeId; +use crate::error::*; +use crate::parsers::Parse; + +/// Used where style properties take a funciri or "none" +/// +/// This is not to be used for values which don't come from properties. +/// For example, the `xlink:href` attribute in the `<image>` element +/// does not take a funciri value (which looks like `url(...)`), but rather +/// it takes a plain URL. +#[derive(Debug, Clone, PartialEq)] +pub enum Iri { + None, + Resource(Box<NodeId>), +} + +impl Iri { + /// Returns the contents of an `IRI::Resource`, or `None` + pub fn get(&self) -> Option<&NodeId> { + match *self { + Iri::None => None, + Iri::Resource(ref f) => Some(f), + } + } +} + +impl Parse for Iri { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Iri, ParseError<'i>> { + if parser + .try_parse(|i| i.expect_ident_matching("none")) + .is_ok() + { + Ok(Iri::None) + } else { + let loc = parser.current_source_location(); + let url = parser.expect_url()?; + let node_id = + NodeId::parse(&url).map_err(|e| loc.new_custom_error(ValueErrorKind::from(e)))?; + + Ok(Iri::Resource(Box::new(node_id))) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_none() { + assert_eq!(Iri::parse_str("none").unwrap(), Iri::None); + } + + #[test] + fn parses_url() { + assert_eq!( + Iri::parse_str("url(#bar)").unwrap(), + Iri::Resource(Box::new(NodeId::Internal("bar".to_string()))) + ); + + assert_eq!( + Iri::parse_str("url(foo#bar)").unwrap(), + Iri::Resource(Box::new(NodeId::External( + "foo".to_string(), + "bar".to_string() + ))) + ); + + // be permissive if the closing ) is missing + assert_eq!( + Iri::parse_str("url(#bar").unwrap(), + Iri::Resource(Box::new(NodeId::Internal("bar".to_string()))) + ); + assert_eq!( + Iri::parse_str("url(foo#bar").unwrap(), + Iri::Resource(Box::new(NodeId::External( + "foo".to_string(), + "bar".to_string() + ))) + ); + + assert!(Iri::parse_str("").is_err()); + assert!(Iri::parse_str("foo").is_err()); + assert!(Iri::parse_str("url(foo)bar").is_err()); + } +} diff --git a/rsvg/src/layout.rs b/rsvg/src/layout.rs new file mode 100644 index 00000000..ca94d3e2 --- /dev/null +++ b/rsvg/src/layout.rs @@ -0,0 +1,387 @@ +//! Layout tree. +//! +//! The idea is to take the DOM tree and produce a layout tree with SVG concepts. + +use std::rc::Rc; +use std::sync::Arc; + +use cssparser::RGBA; +use float_cmp::approx_eq; + +use crate::aspect_ratio::AspectRatio; +use crate::bbox::BoundingBox; +use crate::coord_units::CoordUnits; +use crate::dasharray::Dasharray; +use crate::document::AcquiredNodes; +use crate::element::{Element, ElementData}; +use crate::filter::FilterValueList; +use crate::length::*; +use crate::node::*; +use crate::paint_server::{PaintSource, UserSpacePaintSource}; +use crate::path_builder::Path; +use crate::properties::{ + self, ClipRule, ComputedValues, Direction, FillRule, FontFamily, FontStretch, FontStyle, + FontVariant, FontWeight, Isolation, MixBlendMode, Opacity, Overflow, PaintOrder, + ShapeRendering, StrokeDasharray, StrokeLinecap, StrokeLinejoin, StrokeMiterlimit, + TextDecoration, TextRendering, UnicodeBidi, VectorEffect, XmlLang, +}; +use crate::rect::Rect; +use crate::session::Session; +use crate::surface_utils::shared_surface::SharedImageSurface; +use crate::transform::Transform; +use crate::unit_interval::UnitInterval; + +/// SVG Stacking context, an inner node in the layout tree. +/// +/// <https://www.w3.org/TR/SVG2/render.html#EstablishingStackingContex> +/// +/// This is not strictly speaking an SVG2 stacking context, but a +/// looser version of it. For example. the SVG spec mentions that a +/// an element should establish a stacking context if the `filter` +/// property applies to the element and is not `none`. In that case, +/// the element is rendered as an "isolated group" - +/// <https://www.w3.org/TR/2015/CR-compositing-1-20150113/#csscompositingrules_SVG> +/// +/// Here we store all the parameters that may lead to the decision to actually +/// render an element as an isolated group. +pub struct StackingContext { + pub element_name: String, + pub transform: Transform, + pub opacity: Opacity, + pub filter: Option<Filter>, + pub clip_in_user_space: Option<Node>, + pub clip_in_object_space: Option<Node>, + pub mask: Option<Node>, + pub mix_blend_mode: MixBlendMode, + pub isolation: Isolation, + + /// Target from an `<a>` element + pub link_target: Option<String>, +} + +/// The item being rendered inside a stacking context. +pub struct Layer { + pub kind: LayerKind, + pub stacking_ctx: StackingContext, +} +pub enum LayerKind { + Shape(Box<Shape>), + Text(Box<Text>), + Image(Box<Image>), +} + +/// Stroke parameters in user-space coordinates. +pub struct Stroke { + pub width: f64, + pub miter_limit: StrokeMiterlimit, + pub line_cap: StrokeLinecap, + pub line_join: StrokeLinejoin, + pub dash_offset: f64, + pub dashes: Box<[f64]>, + // https://svgwg.org/svg2-draft/painting.html#non-scaling-stroke + pub non_scaling: bool, +} + +/// Paths and basic shapes resolved to a path. +pub struct Shape { + pub path: Rc<Path>, + pub extents: Option<Rect>, + pub is_visible: bool, + pub paint_order: PaintOrder, + pub stroke: Stroke, + pub stroke_paint: UserSpacePaintSource, + pub fill_paint: UserSpacePaintSource, + pub fill_rule: FillRule, + pub clip_rule: ClipRule, + pub shape_rendering: ShapeRendering, + pub marker_start: Marker, + pub marker_mid: Marker, + pub marker_end: Marker, +} + +pub struct Marker { + pub node_ref: Option<Node>, + pub context_stroke: Arc<PaintSource>, + pub context_fill: Arc<PaintSource>, +} + +/// Image in user-space coordinates. +pub struct Image { + pub surface: SharedImageSurface, + pub is_visible: bool, + pub rect: Rect, + pub aspect: AspectRatio, + pub overflow: Overflow, +} + +/// A single text span in user-space coordinates. +pub struct TextSpan { + pub layout: pango::Layout, + pub gravity: pango::Gravity, + pub bbox: Option<BoundingBox>, + pub is_visible: bool, + pub x: f64, + pub y: f64, + pub paint_order: PaintOrder, + pub stroke: Stroke, + pub stroke_paint: UserSpacePaintSource, + pub fill_paint: UserSpacePaintSource, + pub text_rendering: TextRendering, + pub link_target: Option<String>, +} + +/// Fully laid-out text in user-space coordinates. +pub struct Text { + pub spans: Vec<TextSpan>, +} + +/// Font-related properties extracted from `ComputedValues`. +pub struct FontProperties { + pub xml_lang: XmlLang, + pub unicode_bidi: UnicodeBidi, + pub direction: Direction, + pub font_family: FontFamily, + pub font_style: FontStyle, + pub font_variant: FontVariant, + pub font_weight: FontWeight, + pub font_stretch: FontStretch, + pub font_size: f64, + pub letter_spacing: f64, + pub text_decoration: TextDecoration, +} + +pub struct Filter { + pub filter_list: FilterValueList, + pub current_color: RGBA, + pub stroke_paint_source: Arc<PaintSource>, + pub fill_paint_source: Arc<PaintSource>, + pub normalize_values: NormalizeValues, +} + +fn get_filter( + values: &ComputedValues, + acquired_nodes: &mut AcquiredNodes<'_>, + session: &Session, +) -> Option<Filter> { + match values.filter() { + properties::Filter::None => None, + + properties::Filter::List(filter_list) => Some(get_filter_from_filter_list( + filter_list, + acquired_nodes, + values, + session, + )), + } +} + +fn get_filter_from_filter_list( + filter_list: FilterValueList, + acquired_nodes: &mut AcquiredNodes<'_>, + values: &ComputedValues, + session: &Session, +) -> Filter { + let current_color = values.color().0; + + let stroke_paint_source = values.stroke().0.resolve( + acquired_nodes, + values.stroke_opacity().0, + current_color, + None, + None, + session, + ); + + let fill_paint_source = values.fill().0.resolve( + acquired_nodes, + values.fill_opacity().0, + current_color, + None, + None, + session, + ); + + let normalize_values = NormalizeValues::new(values); + + Filter { + filter_list, + current_color, + stroke_paint_source, + fill_paint_source, + normalize_values, + } +} + +impl StackingContext { + pub fn new( + session: &Session, + acquired_nodes: &mut AcquiredNodes<'_>, + element: &Element, + transform: Transform, + values: &ComputedValues, + ) -> StackingContext { + let element_name = format!("{element}"); + + let opacity; + let filter; + + match element.element_data { + // "The opacity, filter and display properties do not apply to the mask element" + // https://drafts.fxtf.org/css-masking-1/#MaskElement + ElementData::Mask(_) => { + opacity = Opacity(UnitInterval::clamp(1.0)); + filter = None; + } + + _ => { + opacity = values.opacity(); + filter = get_filter(values, acquired_nodes, session); + } + } + + let clip_path = values.clip_path(); + let clip_uri = clip_path.0.get(); + let (clip_in_user_space, clip_in_object_space) = clip_uri + .and_then(|node_id| { + acquired_nodes + .acquire(node_id) + .ok() + .filter(|a| is_element_of_type!(*a.get(), ClipPath)) + }) + .map(|acquired| { + let clip_node = acquired.get().clone(); + + let units = borrow_element_as!(clip_node, ClipPath).get_units(); + + match units { + CoordUnits::UserSpaceOnUse => (Some(clip_node), None), + CoordUnits::ObjectBoundingBox => (None, Some(clip_node)), + } + }) + .unwrap_or((None, None)); + + let mask = values.mask().0.get().and_then(|mask_id| { + if let Ok(acquired) = acquired_nodes.acquire(mask_id) { + let node = acquired.get(); + match *node.borrow_element_data() { + ElementData::Mask(_) => Some(node.clone()), + + _ => { + rsvg_log!( + session, + "element {} references \"{}\" which is not a mask", + element, + mask_id + ); + + None + } + } + } else { + rsvg_log!( + session, + "element {} references nonexistent mask \"{}\"", + element, + mask_id + ); + + None + } + }); + + let mix_blend_mode = values.mix_blend_mode(); + let isolation = values.isolation(); + + StackingContext { + element_name, + transform, + opacity, + filter, + clip_in_user_space, + clip_in_object_space, + mask, + mix_blend_mode, + isolation, + link_target: None, + } + } + + pub fn new_with_link( + session: &Session, + acquired_nodes: &mut AcquiredNodes<'_>, + element: &Element, + transform: Transform, + values: &ComputedValues, + link_target: Option<String>, + ) -> StackingContext { + let mut ctx = Self::new(session, acquired_nodes, element, transform, values); + ctx.link_target = link_target; + ctx + } + + pub fn should_isolate(&self) -> bool { + let Opacity(UnitInterval(opacity)) = self.opacity; + match self.isolation { + Isolation::Auto => { + let is_opaque = approx_eq!(f64, opacity, 1.0); + !(is_opaque + && self.filter.is_none() + && self.mask.is_none() + && self.mix_blend_mode == MixBlendMode::Normal + && self.clip_in_object_space.is_none()) + } + Isolation::Isolate => true, + } + } +} + +impl Stroke { + pub fn new(values: &ComputedValues, params: &NormalizeParams) -> Stroke { + let width = values.stroke_width().0.to_user(params); + let miter_limit = values.stroke_miterlimit(); + let line_cap = values.stroke_line_cap(); + let line_join = values.stroke_line_join(); + let dash_offset = values.stroke_dashoffset().0.to_user(params); + let non_scaling = values.vector_effect() == VectorEffect::NonScalingStroke; + + let dashes = match values.stroke_dasharray() { + StrokeDasharray(Dasharray::None) => Box::new([]), + StrokeDasharray(Dasharray::Array(dashes)) => dashes + .iter() + .map(|l| l.to_user(params)) + .collect::<Box<[f64]>>(), + }; + + Stroke { + width, + miter_limit, + line_cap, + line_join, + dash_offset, + dashes, + non_scaling, + } + } +} + +impl FontProperties { + /// Collects font properties from a `ComputedValues`. + /// + /// The `writing-mode` property is passed separately, as it must come from the `<text>` element, + /// not the `<tspan>` whose computed values are being passed. + pub fn new(values: &ComputedValues, params: &NormalizeParams) -> FontProperties { + FontProperties { + xml_lang: values.xml_lang(), + unicode_bidi: values.unicode_bidi(), + direction: values.direction(), + font_family: values.font_family(), + font_style: values.font_style(), + font_variant: values.font_variant(), + font_weight: values.font_weight(), + font_stretch: values.font_stretch(), + font_size: values.font_size().to_user(params), + letter_spacing: values.letter_spacing().to_user(params), + text_decoration: values.text_decoration(), + } + } +} diff --git a/rsvg/src/length.rs b/rsvg/src/length.rs new file mode 100644 index 00000000..56156d41 --- /dev/null +++ b/rsvg/src/length.rs @@ -0,0 +1,736 @@ +//! CSS length values. +//! +//! [`CssLength`] is the struct librsvg uses to represent CSS lengths. +//! See its documentation for examples of how to construct it. +//! +//! `CssLength` values need to know whether they will be normalized with respect to the width, +//! height, or both dimensions of the current viewport. `CssLength` values can be signed or +//! unsigned. So, a `CssLength` has two type parameters, [`Normalize`] and [`Validate`]; +//! the full type is `CssLength<N: Normalize, V: Validate>`. We provide [`Horizontal`], +//! [`Vertical`], and [`Both`] implementations of [`Normalize`]; these let length values know +//! how to normalize themselves with respect to the current viewport. We also provide +//! [`Signed`] and [`Unsigned`] implementations of [`Validate`]. +//! +//! For ease of use, we define two type aliases [`Length`] and [`ULength`] corresponding to +//! signed and unsigned. +//! +//! For example, the implementation of [`Circle`][crate::shapes::Circle] defines this +//! structure with fields for the `(center_x, center_y, radius)`: +//! +//! ``` +//! # use rsvg::doctest_only::{Length,ULength,Horizontal,Vertical,Both}; +//! pub struct Circle { +//! cx: Length<Horizontal>, +//! cy: Length<Vertical>, +//! r: ULength<Both>, +//! } +//! ``` +//! +//! This means that: +//! +//! * `cx` and `cy` define the center of the circle, they can be positive or negative, and +//! they will be normalized with respect to the current viewport's width and height, +//! respectively. If the SVG document specified `<circle cx="50%" cy="30%">`, the values +//! would be normalized to be at 50% of the the viewport's width, and 30% of the viewport's +//! height. +//! +//! * `r` is non-negative and needs to be resolved against the [normalized diagonal][diag] +//! of the current viewport. +//! +//! The `N` type parameter of `CssLength<N, I>` is enough to know how to normalize a length +//! value; the [`CssLength::to_user`] method will handle it automatically. +//! +//! [diag]: https://www.w3.org/TR/SVG/coords.html#Units + +use cssparser::{match_ignore_ascii_case, Parser, Token, _cssparser_internal_to_lowercase}; +use std::f64::consts::*; +use std::fmt; +use std::marker::PhantomData; + +use crate::dpi::Dpi; +use crate::drawing_ctx::Viewport; +use crate::error::*; +use crate::parsers::{finite_f32, Parse}; +use crate::properties::{ComputedValues, FontSize}; +use crate::rect::Rect; +use crate::viewbox::ViewBox; + +/// Units for length values. +// This needs to be kept in sync with `rsvg.h:RsvgUnit`. +#[repr(C)] +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum LengthUnit { + /// `1.0` means 100% + Percent, + + /// Pixels, or the CSS default unit + Px, + + /// Size of the current font + Em, + + /// x-height of the current font + Ex, + + /// Inches (25.4 mm) + In, + + /// Centimeters + Cm, + + /// Millimeters + Mm, + + /// Points (1/72 inch) + Pt, + + /// Picas (12 points) + Pc, +} + +/// A CSS length value. +/// +/// This is equivalent to [CSS lengths]. +/// +/// [CSS lengths]: https://www.w3.org/TR/CSS22/syndata.html#length-units +/// +/// It is up to the calling application to convert lengths in non-pixel units (i.e. those +/// where the [`unit`][RsvgLength::unit] field is not [`LengthUnit::Px`]) into something +/// meaningful to the application. For example, if your application knows the +/// dots-per-inch (DPI) it is using, it can convert lengths with [`unit`] in +/// [`LengthUnit::In`] or other physical units. +// Keep this in sync with rsvg.h:RsvgLength +#[repr(C)] +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct RsvgLength { + /// Numeric part of the length + pub length: f64, + + /// Unit part of the length + pub unit: LengthUnit, +} + +impl RsvgLength { + pub fn new(l: f64, unit: LengthUnit) -> RsvgLength { + RsvgLength { length: l, unit } + } +} + +/// Used for the `N` type parameter of `CssLength<N: Normalize, V: Validate>`. +pub trait Normalize { + /// Computes an orientation-based scaling factor. + /// + /// This is used in the [`CssLength::to_user`] method to resolve lengths with percentage + /// units; they need to be resolved with respect to the width, height, or [normalized + /// diagonal][diag] of the current viewport. + /// + /// [diag]: https://www.w3.org/TR/SVG/coords.html#Units + fn normalize(x: f64, y: f64) -> f64; +} + +/// Allows declaring `CssLength<Horizontal>`. +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct Horizontal; + +/// Allows declaring `CssLength<Vertical>`. +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct Vertical; + +/// Allows declaring `CssLength<Both>`. +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct Both; + +impl Normalize for Horizontal { + #[inline] + fn normalize(x: f64, _y: f64) -> f64 { + x + } +} + +impl Normalize for Vertical { + #[inline] + fn normalize(_x: f64, y: f64) -> f64 { + y + } +} + +impl Normalize for Both { + #[inline] + fn normalize(x: f64, y: f64) -> f64 { + viewport_percentage(x, y) + } +} + +/// Used for the `V` type parameter of `CssLength<N: Normalize, V: Validate>`. +pub trait Validate { + /// Checks if the specified value is acceptable + /// + /// This is used when parsing a length value + fn validate(v: f64) -> Result<f64, ValueErrorKind> { + Ok(v) + } +} + +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct Signed; + +impl Validate for Signed {} + +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct Unsigned; + +impl Validate for Unsigned { + fn validate(v: f64) -> Result<f64, ValueErrorKind> { + if v >= 0.0 { + Ok(v) + } else { + Err(ValueErrorKind::Value( + "value must be non-negative".to_string(), + )) + } + } +} + +/// A CSS length value. +/// +/// This is equivalent to [CSS lengths]. +/// +/// [CSS lengths]: https://www.w3.org/TR/CSS22/syndata.html#length-units +/// +/// `CssLength` implements the [`Parse`] trait, so it can be parsed out of a +/// [`cssparser::Parser`]. +/// +/// This type will be normally used through the type aliases [`Length`] and [`ULength`] +/// +/// Examples of construction: +/// +/// ``` +/// # use rsvg::doctest_only::{Length,ULength,LengthUnit,Horizontal,Vertical,Both}; +/// # use rsvg::doctest_only::Parse; +/// // Explicit type +/// let width: Length<Horizontal> = Length::new(42.0, LengthUnit::Cm); +/// +/// // Inferred type +/// let height = Length::<Vertical>::new(42.0, LengthUnit::Cm); +/// +/// // Parsed +/// let radius = ULength::<Both>::parse_str("5px").unwrap(); +/// ``` +/// +/// During the rendering phase, a `CssLength` needs to be converted to user-space +/// coordinates with the [`CssLength::to_user`] method. +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct CssLength<N: Normalize, V: Validate> { + /// Numeric part of the length + pub length: f64, + + /// Unit part of the length + pub unit: LengthUnit, + + /// Dummy; used internally for the type parameter `N` + orientation: PhantomData<N>, + + /// Dummy; used internally for the type parameter `V` + validation: PhantomData<V>, +} + +impl<N: Normalize, V: Validate> From<CssLength<N, V>> for RsvgLength { + fn from(l: CssLength<N, V>) -> RsvgLength { + RsvgLength { + length: l.length, + unit: l.unit, + } + } +} + +impl<N: Normalize, V: Validate> Default for CssLength<N, V> { + fn default() -> Self { + CssLength::new(0.0, LengthUnit::Px) + } +} + +pub const POINTS_PER_INCH: f64 = 72.0; +const CM_PER_INCH: f64 = 2.54; +const MM_PER_INCH: f64 = 25.4; +const PICA_PER_INCH: f64 = 6.0; + +impl<N: Normalize, V: Validate> Parse for CssLength<N, V> { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<CssLength<N, V>, ParseError<'i>> { + let l_value; + let l_unit; + + let token = parser.next()?.clone(); + + match token { + Token::Number { value, .. } => { + l_value = value; + l_unit = LengthUnit::Px; + } + + Token::Percentage { unit_value, .. } => { + l_value = unit_value; + l_unit = LengthUnit::Percent; + } + + Token::Dimension { + value, ref unit, .. + } => { + l_value = value; + + l_unit = match_ignore_ascii_case! {unit.as_ref(), + "px" => LengthUnit::Px, + "em" => LengthUnit::Em, + "ex" => LengthUnit::Ex, + "in" => LengthUnit::In, + "cm" => LengthUnit::Cm, + "mm" => LengthUnit::Mm, + "pt" => LengthUnit::Pt, + "pc" => LengthUnit::Pc, + + _ => return Err(parser.new_unexpected_token_error(token)), + }; + } + + _ => return Err(parser.new_unexpected_token_error(token)), + } + + let l_value = f64::from(finite_f32(l_value).map_err(|e| parser.new_custom_error(e))?); + + <V as Validate>::validate(l_value) + .map_err(|e| parser.new_custom_error(e)) + .map(|l_value| CssLength::new(l_value, l_unit)) + } +} + +/// Parameters for length normalization extractedfrom [`ComputedValues`]. +/// +/// This is a precursor to [`NormalizeParams::from_values`], for cases where it is inconvenient +/// to keep a [`ComputedValues`] around. +pub struct NormalizeValues { + font_size: FontSize, +} + +impl NormalizeValues { + pub fn new(values: &ComputedValues) -> NormalizeValues { + NormalizeValues { + font_size: values.font_size(), + } + } +} + +/// Parameters to normalize [`Length`] values to user-space distances. +pub struct NormalizeParams { + vbox: ViewBox, + font_size: f64, + dpi: Dpi, +} + +impl NormalizeParams { + /// Extracts the information needed to normalize [`Length`] values from a set of + /// [`ComputedValues`] and the viewport size in [`Viewport`]. + pub fn new(values: &ComputedValues, viewport: &Viewport) -> NormalizeParams { + let v = NormalizeValues::new(values); + NormalizeParams::from_values(&v, viewport) + } + + pub fn from_values(v: &NormalizeValues, viewport: &Viewport) -> NormalizeParams { + NormalizeParams { + vbox: viewport.vbox, + font_size: font_size_from_values(v, viewport.dpi), + dpi: viewport.dpi, + } + } + + /// Just used by rsvg-convert, where there is no font size nor viewport. + pub fn from_dpi(dpi: Dpi) -> NormalizeParams { + NormalizeParams { + vbox: ViewBox::from(Rect::default()), + font_size: 1.0, + dpi, + } + } +} + +impl<N: Normalize, V: Validate> CssLength<N, V> { + /// Creates a CssLength. + /// + /// The compiler needs to know the type parameters `N` and `V` which represents the + /// length's orientation and validation. + /// You can specify them explicitly, or call the parametrized method: + /// + /// ``` + /// # use rsvg::doctest_only::{Length,LengthUnit,Horizontal,Vertical}; + /// // Explicit type + /// let width: Length<Horizontal> = Length::new(42.0, LengthUnit::Cm); + /// + /// // Inferred type + /// let height = Length::<Vertical>::new(42.0, LengthUnit::Cm); + /// ``` + pub fn new(l: f64, unit: LengthUnit) -> CssLength<N, V> { + CssLength { + length: l, + unit, + orientation: PhantomData, + validation: PhantomData, + } + } + + /// Convert a Length with units into user-space coordinates. + /// + /// Lengths may come with non-pixel units, and when rendering, they need to be normalized + /// to pixels based on the current viewport (e.g. for lengths with percent units), and + /// based on the current element's set of [`ComputedValues`] (e.g. for lengths with `Em` + /// units that need to be resolved against the current font size). + /// + /// Those parameters can be obtained with [`NormalizeParams::new()`]. + pub fn to_user(&self, params: &NormalizeParams) -> f64 { + match self.unit { + LengthUnit::Px => self.length, + + LengthUnit::Percent => { + self.length * <N as Normalize>::normalize(params.vbox.width(), params.vbox.height()) + } + + LengthUnit::Em => self.length * params.font_size, + + LengthUnit::Ex => self.length * params.font_size / 2.0, + + LengthUnit::In => self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y), + + LengthUnit::Cm => { + self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y) / CM_PER_INCH + } + + LengthUnit::Mm => { + self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y) / MM_PER_INCH + } + + LengthUnit::Pt => { + self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y) + / POINTS_PER_INCH + } + + LengthUnit::Pc => { + self.length * <N as Normalize>::normalize(params.dpi.x, params.dpi.y) + / PICA_PER_INCH + } + } + } + + /// Converts a Length to points. Pixels are taken to be respect with the DPI. + /// + /// # Panics + /// + /// Will panic if the length is in Percent, Em, or Ex units. + pub fn to_points(&self, params: &NormalizeParams) -> f64 { + match self.unit { + LengthUnit::Px => { + self.length / <N as Normalize>::normalize(params.dpi.x, params.dpi.y) * 72.0 + } + + LengthUnit::Percent => { + panic!("Cannot convert a percentage length into an absolute length"); + } + + LengthUnit::Em => { + panic!("Cannot convert an Em length into an absolute length"); + } + + LengthUnit::Ex => { + panic!("Cannot convert an Ex length into an absolute length"); + } + + LengthUnit::In => self.length * POINTS_PER_INCH, + + LengthUnit::Cm => self.length / CM_PER_INCH * POINTS_PER_INCH, + + LengthUnit::Mm => self.length / MM_PER_INCH * POINTS_PER_INCH, + + LengthUnit::Pt => self.length, + + LengthUnit::Pc => self.length / PICA_PER_INCH * POINTS_PER_INCH, + } + } + + pub fn to_inches(&self, params: &NormalizeParams) -> f64 { + self.to_points(params) / POINTS_PER_INCH + } + + pub fn to_cm(&self, params: &NormalizeParams) -> f64 { + self.to_inches(params) * CM_PER_INCH + } + + pub fn to_mm(&self, params: &NormalizeParams) -> f64 { + self.to_inches(params) * MM_PER_INCH + } + + pub fn to_picas(&self, params: &NormalizeParams) -> f64 { + self.to_inches(params) * PICA_PER_INCH + } +} + +fn font_size_from_values(values: &NormalizeValues, dpi: Dpi) -> f64 { + let v = values.font_size.value(); + + match v.unit { + LengthUnit::Percent => unreachable!("ComputedValues can't have a relative font size"), + + LengthUnit::Px => v.length, + + // The following implies that our default font size is 12, which + // matches the default from the FontSize property. + LengthUnit::Em => v.length * 12.0, + LengthUnit::Ex => v.length * 12.0 / 2.0, + + // FontSize always is a Both, per properties.rs + LengthUnit::In => v.length * Both::normalize(dpi.x, dpi.y), + LengthUnit::Cm => v.length * Both::normalize(dpi.x, dpi.y) / CM_PER_INCH, + LengthUnit::Mm => v.length * Both::normalize(dpi.x, dpi.y) / MM_PER_INCH, + LengthUnit::Pt => v.length * Both::normalize(dpi.x, dpi.y) / POINTS_PER_INCH, + LengthUnit::Pc => v.length * Both::normalize(dpi.x, dpi.y) / PICA_PER_INCH, + } +} + +fn viewport_percentage(x: f64, y: f64) -> f64 { + // https://www.w3.org/TR/SVG/coords.html#Units + // "For any other length value expressed as a percentage of the viewport, the + // percentage is calculated as the specified percentage of + // sqrt((actual-width)**2 + (actual-height)**2))/sqrt(2)." + (x * x + y * y).sqrt() / SQRT_2 +} + +/// Alias for `CssLength` types that can have negative values +pub type Length<N> = CssLength<N, Signed>; + +/// Alias for `CssLength` types that are non negative +pub type ULength<N> = CssLength<N, Unsigned>; + +#[derive(Debug, Default, PartialEq, Copy, Clone)] +pub enum LengthOrAuto<N: Normalize> { + #[default] + Auto, + Length(CssLength<N, Unsigned>), +} + +impl<N: Normalize> Parse for LengthOrAuto<N> { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<LengthOrAuto<N>, ParseError<'i>> { + if parser + .try_parse(|i| i.expect_ident_matching("auto")) + .is_ok() + { + Ok(LengthOrAuto::Auto) + } else { + Ok(LengthOrAuto::Length(CssLength::parse(parser)?)) + } + } +} + +impl fmt::Display for LengthUnit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let unit = match &self { + LengthUnit::Percent => "%", + LengthUnit::Px => "px", + LengthUnit::Em => "em", + LengthUnit::Ex => "ex", + LengthUnit::In => "in", + LengthUnit::Cm => "cm", + LengthUnit::Mm => "mm", + LengthUnit::Pt => "pt", + LengthUnit::Pc => "pc", + }; + + write!(f, "{unit}") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::float_eq_cairo::ApproxEqCairo; + + #[test] + fn parses_default() { + assert_eq!( + Length::<Horizontal>::parse_str("42").unwrap(), + Length::<Horizontal>::new(42.0, LengthUnit::Px) + ); + + assert_eq!( + Length::<Horizontal>::parse_str("-42px").unwrap(), + Length::<Horizontal>::new(-42.0, LengthUnit::Px) + ); + } + + #[test] + fn parses_percent() { + assert_eq!( + Length::<Horizontal>::parse_str("50.0%").unwrap(), + Length::<Horizontal>::new(0.5, LengthUnit::Percent) + ); + } + + #[test] + fn parses_font_em() { + assert_eq!( + Length::<Vertical>::parse_str("22.5em").unwrap(), + Length::<Vertical>::new(22.5, LengthUnit::Em) + ); + } + + #[test] + fn parses_font_ex() { + assert_eq!( + Length::<Vertical>::parse_str("22.5ex").unwrap(), + Length::<Vertical>::new(22.5, LengthUnit::Ex) + ); + } + + #[test] + fn parses_physical_units() { + assert_eq!( + Length::<Both>::parse_str("72pt").unwrap(), + Length::<Both>::new(72.0, LengthUnit::Pt) + ); + + assert_eq!( + Length::<Both>::parse_str("-22.5in").unwrap(), + Length::<Both>::new(-22.5, LengthUnit::In) + ); + + assert_eq!( + Length::<Both>::parse_str("-254cm").unwrap(), + Length::<Both>::new(-254.0, LengthUnit::Cm) + ); + + assert_eq!( + Length::<Both>::parse_str("254mm").unwrap(), + Length::<Both>::new(254.0, LengthUnit::Mm) + ); + + assert_eq!( + Length::<Both>::parse_str("60pc").unwrap(), + Length::<Both>::new(60.0, LengthUnit::Pc) + ); + } + + #[test] + fn parses_unsigned() { + assert_eq!( + ULength::<Horizontal>::parse_str("42").unwrap(), + ULength::<Horizontal>::new(42.0, LengthUnit::Px) + ); + + assert_eq!( + ULength::<Both>::parse_str("0pt").unwrap(), + ULength::<Both>::new(0.0, LengthUnit::Pt) + ); + + assert!(ULength::<Horizontal>::parse_str("-42px").is_err()); + } + + #[test] + fn empty_length_yields_error() { + assert!(Length::<Both>::parse_str("").is_err()); + } + + #[test] + fn invalid_unit_yields_error() { + assert!(Length::<Both>::parse_str("8furlong").is_err()); + } + + #[test] + fn normalize_default_works() { + let view_params = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 100.0); + let values = ComputedValues::default(); + let params = NormalizeParams::new(&values, &view_params); + + assert_approx_eq_cairo!( + Length::<Both>::new(10.0, LengthUnit::Px).to_user(¶ms), + 10.0 + ); + } + + #[test] + fn normalize_absolute_units_works() { + let view_params = Viewport::new(Dpi::new(40.0, 50.0), 100.0, 100.0); + let values = ComputedValues::default(); + let params = NormalizeParams::new(&values, &view_params); + + assert_approx_eq_cairo!( + Length::<Horizontal>::new(10.0, LengthUnit::In).to_user(¶ms), + 400.0 + ); + assert_approx_eq_cairo!( + Length::<Vertical>::new(10.0, LengthUnit::In).to_user(¶ms), + 500.0 + ); + + assert_approx_eq_cairo!( + Length::<Horizontal>::new(10.0, LengthUnit::Cm).to_user(¶ms), + 400.0 / CM_PER_INCH + ); + assert_approx_eq_cairo!( + Length::<Horizontal>::new(10.0, LengthUnit::Mm).to_user(¶ms), + 400.0 / MM_PER_INCH + ); + assert_approx_eq_cairo!( + Length::<Horizontal>::new(10.0, LengthUnit::Pt).to_user(¶ms), + 400.0 / POINTS_PER_INCH + ); + assert_approx_eq_cairo!( + Length::<Horizontal>::new(10.0, LengthUnit::Pc).to_user(¶ms), + 400.0 / PICA_PER_INCH + ); + } + + #[test] + fn normalize_percent_works() { + let view_params = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 200.0); + let values = ComputedValues::default(); + let params = NormalizeParams::new(&values, &view_params); + + assert_approx_eq_cairo!( + Length::<Horizontal>::new(0.05, LengthUnit::Percent).to_user(¶ms), + 5.0 + ); + assert_approx_eq_cairo!( + Length::<Vertical>::new(0.05, LengthUnit::Percent).to_user(¶ms), + 10.0 + ); + } + + #[test] + fn normalize_font_em_ex_works() { + let view_params = Viewport::new(Dpi::new(40.0, 40.0), 100.0, 200.0); + let values = ComputedValues::default(); + let params = NormalizeParams::new(&values, &view_params); + + // These correspond to the default size for the font-size + // property and the way we compute Em/Ex from that. + + assert_approx_eq_cairo!( + Length::<Vertical>::new(1.0, LengthUnit::Em).to_user(¶ms), + 12.0 + ); + + assert_approx_eq_cairo!( + Length::<Vertical>::new(1.0, LengthUnit::Ex).to_user(¶ms), + 6.0 + ); + } + + #[test] + fn to_points_works() { + let params = NormalizeParams::from_dpi(Dpi::new(40.0, 96.0)); + + assert_approx_eq_cairo!( + Length::<Horizontal>::new(80.0, LengthUnit::Px).to_points(¶ms), + 2.0 * 72.0 + ); + assert_approx_eq_cairo!( + Length::<Vertical>::new(192.0, LengthUnit::Px).to_points(¶ms), + 2.0 * 72.0 + ); + } +} diff --git a/rsvg/src/lib.rs b/rsvg/src/lib.rs new file mode 100644 index 00000000..6d470d59 --- /dev/null +++ b/rsvg/src/lib.rs @@ -0,0 +1,261 @@ +//! Load and render SVG images into Cairo surfaces. +//! +//! This crate can load SVG images and render them to Cairo surfaces, +//! using a mixture of SVG's [static mode] and [secure static mode]. +//! Librsvg does not do animation nor scripting, and can load +//! references to external data only in some situations; see below. +//! +//! Librsvg supports reading [SVG 1.1] data, and is gradually adding +//! support for features in [SVG 2]. Librsvg also supports SVGZ +//! files, which are just an SVG stream compressed with the GZIP +//! algorithm. +//! +//! # Basic usage +//! +//! * Create a [`Loader`] struct. +//! * Get an [`SvgHandle`] from the [`Loader`]. +//! * Create a [`CairoRenderer`] for the [`SvgHandle`] and render to a Cairo context. +//! +//! You can put the following in your `Cargo.toml`: +//! +//! ```toml +//! [dependencies] +//! rsvg = { git="https://gitlab.gnome.org/GNOME/librsvg" } +//! cairo-rs = "0.8.0" +//! glib = "0.9.0" # only if you need streams +//! gio = { version="0.8.1", features=["v2_50"] } # likewise +//! ``` +//! +//! # Example +//! +//! ``` +//! +//! const WIDTH: i32 = 640; +//! const HEIGHT: i32 = 480; +//! +//! fn main() { +//! // Loading from a file +//! +//! let handle = rsvg::Loader::new().read_path("example.svg").unwrap(); +//! +//! let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, WIDTH, HEIGHT).unwrap(); +//! let cr = cairo::Context::new(&surface).expect("Failed to create a cairo context"); +//! +//! let renderer = rsvg::CairoRenderer::new(&handle); +//! renderer.render_document( +//! &cr, +//! &cairo::Rectangle::new(0.0, 0.0, f64::from(WIDTH), f64::from(HEIGHT)) +//! ).unwrap(); +//! +//! // Loading from a static SVG asset +//! +//! let bytes = glib::Bytes::from_static( +//! br#"<?xml version="1.0" encoding="UTF-8"?> +//! <svg xmlns="http://www.w3.org/2000/svg" width="50" height="50"> +//! <rect id="foo" x="10" y="10" width="30" height="30"/> +//! </svg> +//! "# +//! ); +//! let stream = gio::MemoryInputStream::from_bytes(&bytes); +//! +//! let handle = rsvg::Loader::new().read_stream( +//! &stream, +//! None::<&gio::File>, // no base file as this document has no references +//! None::<&gio::Cancellable>, // no cancellable +//! ).unwrap(); +//! } +//! ``` +//! +//! # The "base file" and resolving references to external files +//! +//! When you load an SVG, librsvg needs to know the location of the "base file" +//! for it. This is so that librsvg can determine the location of referenced +//! entities. For example, say you have an SVG in <filename>/foo/bar/foo.svg</filename> +//! and that it has an image element like this: +//! +//! ```xml +//! <image href="resources/foo.png" .../> +//! ``` +//! +//! In this case, librsvg needs to know the location of the toplevel +//! `/foo/bar/foo.svg` so that it can generate the appropriate +//! reference to `/foo/bar/resources/foo.png`. +//! +//! ## Security and locations of referenced files +//! +//! When processing an SVG, librsvg will only load referenced files if +//! they are in the same directory as the base file, or in a +//! subdirectory of it. That is, if the base file is +//! `/foo/bar/baz.svg`, then librsvg will only try to load referenced +//! files (from SVG's `<image>` element, for example, or from content +//! included through XML entities) if those files are in `/foo/bar/*` +//! or in `/foo/bar/*/.../*`. This is so that malicious SVG documents +//! cannot include files that are in a directory above. +//! +//! The full set of rules for deciding which URLs may be loaded is as follows; +//! they are applied in order. A referenced URL will not be loaded as soon as +//! one of these rules fails: +//! +//! 1. All `data:` URLs may be loaded. These are sometimes used to +//! include raster image data, encoded as base-64, directly in an SVG +//! file. +//! +//! 2. All other URL schemes in references require a base URL. For +//! example, this means that if you load an SVG with [`Loader::read_stream`] +//! without providing a `base_file`, then any referenced files will not +//! be allowed (e.g. raster images to be loaded from other files will +//! not work). +//! +//! 3. If referenced URLs are absolute, rather than relative, then +//! they must have the same scheme as the base URL. For example, if +//! the base URL has a "`file`" scheme, then all URL references inside +//! the SVG must also have the "`file`" scheme, or be relative +//! references which will be resolved against the base URL. +//! +//! 4. If referenced URLs have a "`resource`" scheme, that is, if they +//! are included into your binary program with GLib's resource +//! mechanism, they are allowed to be loaded (provided that the base +//! URL is also a "`resource`", per the previous rule). +//! +//! 5. Otherwise, non-`file` schemes are not allowed. For example, +//! librsvg will not load `http` resources, to keep malicious SVG data +//! from "phoning home". +//! +//! 6. A relative URL must resolve to the same directory as the base +//! URL, or to one of its subdirectories. Librsvg will canonicalize +//! filenames, by removing "`..`" path components and resolving symbolic +//! links, to decide whether files meet these conditions. +//! +//! [static mode]: https://www.w3.org/TR/SVG2/conform.html#static-mode +//! [secure static mode]: https://www.w3.org/TR/SVG2/conform.html#secure-static-mode +//! [SVG 1.1]: https://www.w3.org/TR/SVG11/ +//! [SVG 2]: https://www.w3.org/TR/SVG2/ + +#![doc(html_logo_url = "https://gnome.pages.gitlab.gnome.org/librsvg/Rsvg-2.0/librsvg-r.svg")] +#![allow(rustdoc::private_intra_doc_links)] +#![allow(clippy::clone_on_ref_ptr)] +#![allow(clippy::not_unsafe_ptr_arg_deref)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::derive_partial_eq_without_eq)] +#![warn(nonstandard_style, rust_2018_idioms, unused)] +// Some lints no longer exist +#![warn(renamed_and_removed_lints)] +// Standalone lints +#![warn(trivial_casts, trivial_numeric_casts)] +// The public API is exported here +pub use crate::api::*; + +pub use crate::color::Color; + +pub use crate::rect::{IRect, Rect}; + +#[macro_use] +pub mod log; + +#[macro_use] +mod parsers; + +#[macro_use] +mod coord_units; + +#[macro_use] +mod float_eq_cairo; + +#[macro_use] +mod node; + +#[macro_use] +mod property_macros; + +#[macro_use] +mod util; + +mod accept_language; +mod angle; +mod api; +mod aspect_ratio; +mod bbox; +mod color; +mod cond; +mod css; +mod dasharray; +mod document; +mod dpi; +mod drawing_ctx; +mod element; +mod error; +mod filter; +mod filter_func; +pub mod filters; +mod font_props; +mod gradient; +mod handle; +mod href; +mod image; +mod io; +mod iri; +mod layout; +mod length; +mod limits; +mod marker; +mod paint_server; +mod path_builder; +mod path_parser; +mod pattern; +mod properties; +mod property_defs; +mod rect; +mod session; +mod shapes; +mod space; +mod structure; +mod style; +pub mod surface_utils; +mod text; +mod transform; +mod unit_interval; +mod url_resolver; +mod viewbox; +mod xml; + +#[cfg(feature = "test-utils")] +#[doc(hidden)] +pub mod test_utils; + +#[doc(hidden)] +pub mod bench_only { + pub use crate::path_builder::PathBuilder; + pub use crate::path_parser::Lexer; +} + +#[doc(hidden)] +#[cfg(feature = "c-api")] +pub mod c_api_only { + pub use crate::handle::Handle; + pub use crate::session::Session; +} + +#[doc(hidden)] +pub mod doctest_only { + pub use crate::aspect_ratio::AspectRatio; + pub use crate::error::AttributeResultExt; + pub use crate::error::ElementError; + pub use crate::error::ValueErrorKind; + pub use crate::href::is_href; + pub use crate::href::set_href; + pub use crate::length::{Both, CssLength, Horizontal, Length, LengthUnit, ULength, Vertical}; + pub use crate::parsers::{Parse, ParseValue}; +} + +#[doc(hidden)] +pub mod rsvg_convert_only { + pub use crate::aspect_ratio::AspectRatio; + pub use crate::error::ParseError; + pub use crate::length::{ + CssLength, Horizontal, Length, Normalize, NormalizeParams, Signed, ULength, Unsigned, + Validate, Vertical, + }; + pub use crate::parsers::{Parse, ParseValue}; + pub use crate::rect::Rect; + pub use crate::viewbox::ViewBox; +} diff --git a/rsvg/src/limits.rs b/rsvg/src/limits.rs new file mode 100644 index 00000000..e287767b --- /dev/null +++ b/rsvg/src/limits.rs @@ -0,0 +1,52 @@ +//! Processing limits to mitigate malicious SVGs. + +/// Maximum number of times that elements can be referenced through URL fragments. +/// +/// This is a mitigation for the security-related bugs: +/// <https://gitlab.gnome.org/GNOME/librsvg/issues/323> +/// <https://gitlab.gnome.org/GNOME/librsvg/issues/515> +/// +/// Imagine the XML [billion laughs attack], but done in SVG's terms: +/// +/// - #323 above creates deeply nested groups of `<use>` elements. +/// The first one references the second one ten times, the second one +/// references the third one ten times, and so on. In the file given, +/// this causes 10^17 objects to be rendered. While this does not +/// exhaust memory, it would take a really long time. +/// +/// - #515 has deeply nested references of `<pattern>` elements. Each +/// object inside each pattern has an attribute +/// fill="url(#next_pattern)", so the number of final rendered objects +/// grows exponentially. +/// +/// We deal with both cases by placing a limit on how many references +/// will be resolved during the SVG rendering process, that is, +/// how many `url(#foo)` will be resolved. +/// +/// [billion laughs attack]: https://bitbucket.org/tiran/defusedxml +pub const MAX_REFERENCED_ELEMENTS: usize = 500_000; + +/// Maximum number of elements loadable per document. +/// +/// This is a mitigation for SVG files which create millions of elements +/// in an attempt to exhaust memory. We don't allow loading more than +/// this number of elements during the initial streaming load process. +pub const MAX_LOADED_ELEMENTS: usize = 1_000_000; + +/// Maximum number of attributes loadable per document. +/// +/// This is here because librsvg uses u16 to address attributes. It should +/// be essentially impossible to actually hit this limit, because the number +/// of attributes that the SVG standard ascribes meaning to are lower than +/// this limit. +pub const MAX_LOADED_ATTRIBUTES: usize = u16::MAX as usize; + +/// Maximum level of nesting for XInclude (XML Include) files. +/// +/// See <https://gitlab.gnome.org/GNOME/librsvg/-/issues/942>. With +/// the use of XML like `<xi:include parse="xml" href="foo.xml"/>`, an +/// SVG document can recursively include other XML files. This value +/// defines a maximum level of nesting for XInclude, to prevent cases +/// where the base document is included within itself, or when two +/// documents recursively include each other. +pub const MAX_XINCLUDE_DEPTH: usize = 20; diff --git a/rsvg/src/log.rs b/rsvg/src/log.rs new file mode 100644 index 00000000..36a363e2 --- /dev/null +++ b/rsvg/src/log.rs @@ -0,0 +1,75 @@ +//! Utilities for logging messages from the library. + +#[macro_export] +macro_rules! rsvg_log { + ( + $session:expr, + $($arg:tt)+ + ) => { + if $session.log_enabled() { + println!("{}", format_args!($($arg)+)); + } + }; +} + +/// Captures the basic state of a [`cairo::Context`] for logging purposes. +/// +/// A librsvg "transaction" like rendering a +/// [`crate::api::SvgHandle`], which takes a Cairo context, depends on the state of the +/// context as it was passed in by the caller. For example, librsvg may decide to +/// operate differently depending on the context's target surface type, or its current +/// transformation matrix. This struct captures that sort of information. +#[derive(Copy, Clone, Debug, PartialEq)] +struct CairoContextState { + surface_type: cairo::SurfaceType, + matrix: cairo::Matrix, +} + +impl CairoContextState { + #[cfg(test)] + fn new(cr: &cairo::Context) -> Self { + let surface_type = cr.target().type_(); + let matrix = cr.matrix(); + + Self { + surface_type, + matrix, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn captures_cr_state() { + let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 10, 10).unwrap(); + let cr = cairo::Context::new(&surface).unwrap(); + let state = CairoContextState::new(&cr); + + assert_eq!( + CairoContextState { + surface_type: cairo::SurfaceType::Image, + matrix: cairo::Matrix::identity(), + }, + state, + ); + + let surface = cairo::RecordingSurface::create(cairo::Content::ColorAlpha, None).unwrap(); + let cr = cairo::Context::new(&surface).unwrap(); + cr.scale(2.0, 3.0); + let state = CairoContextState::new(&cr); + + let mut matrix = cairo::Matrix::identity(); + matrix.scale(2.0, 3.0); + + assert_eq!( + CairoContextState { + surface_type: cairo::SurfaceType::Recording, + matrix, + }, + state, + ); + } +} diff --git a/rsvg/src/marker.rs b/rsvg/src/marker.rs new file mode 100644 index 00000000..ba2006c6 --- /dev/null +++ b/rsvg/src/marker.rs @@ -0,0 +1,1215 @@ +//! The `marker` element, and geometry computations for markers. + +use std::f64::consts::*; +use std::ops::Deref; + +use cssparser::Parser; +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::angle::Angle; +use crate::aspect_ratio::*; +use crate::bbox::BoundingBox; +use crate::document::AcquiredNodes; +use crate::drawing_ctx::{DrawingCtx, Viewport}; +use crate::element::{set_attribute, ElementTrait}; +use crate::error::*; +use crate::float_eq_cairo::ApproxEqCairo; +use crate::layout::{self, Shape, StackingContext}; +use crate::length::*; +use crate::node::{CascadedValues, Node, NodeBorrow, NodeDraw}; +use crate::parsers::{Parse, ParseValue}; +use crate::path_builder::{arc_segment, ArcParameterization, CubicBezierCurve, Path, PathCommand}; +use crate::rect::Rect; +use crate::session::Session; +use crate::transform::Transform; +use crate::viewbox::*; +use crate::xml::Attributes; + +// markerUnits attribute: https://www.w3.org/TR/SVG/painting.html#MarkerElement +#[derive(Debug, Copy, Clone, PartialEq)] +enum MarkerUnits { + UserSpaceOnUse, + StrokeWidth, +} + +enum_default!(MarkerUnits, MarkerUnits::StrokeWidth); + +impl Parse for MarkerUnits { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<MarkerUnits, ParseError<'i>> { + Ok(parse_identifiers!( + parser, + "userSpaceOnUse" => MarkerUnits::UserSpaceOnUse, + "strokeWidth" => MarkerUnits::StrokeWidth, + )?) + } +} + +// orient attribute: https://www.w3.org/TR/SVG/painting.html#MarkerElement +#[derive(Debug, Copy, Clone, PartialEq)] +enum MarkerOrient { + Auto, + AutoStartReverse, + Angle(Angle), +} + +enum_default!(MarkerOrient, MarkerOrient::Angle(Angle::new(0.0))); + +impl Parse for MarkerOrient { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<MarkerOrient, ParseError<'i>> { + if parser + .try_parse(|p| p.expect_ident_matching("auto")) + .is_ok() + { + return Ok(MarkerOrient::Auto); + } + + if parser + .try_parse(|p| p.expect_ident_matching("auto-start-reverse")) + .is_ok() + { + Ok(MarkerOrient::AutoStartReverse) + } else { + Angle::parse(parser).map(MarkerOrient::Angle) + } + } +} + +pub struct Marker { + units: MarkerUnits, + ref_x: Length<Horizontal>, + ref_y: Length<Vertical>, + width: ULength<Horizontal>, + height: ULength<Vertical>, + orient: MarkerOrient, + aspect: AspectRatio, + vbox: Option<ViewBox>, +} + +impl Default for Marker { + fn default() -> Marker { + Marker { + units: MarkerUnits::default(), + ref_x: Default::default(), + ref_y: Default::default(), + // the following two are per the spec + width: ULength::<Horizontal>::parse_str("3").unwrap(), + height: ULength::<Vertical>::parse_str("3").unwrap(), + orient: MarkerOrient::default(), + aspect: AspectRatio::default(), + vbox: None, + } + } +} + +impl Marker { + fn render( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + xpos: f64, + ypos: f64, + computed_angle: Angle, + line_width: f64, + clipping: bool, + marker_type: MarkerType, + marker: &layout::Marker, + ) -> Result<BoundingBox, RenderingError> { + let mut cascaded = CascadedValues::new_from_node(node); + cascaded.context_fill = Some(marker.context_fill.clone()); + cascaded.context_stroke = Some(marker.context_stroke.clone()); + + let values = cascaded.get(); + + let params = NormalizeParams::new(values, viewport); + + let marker_width = self.width.to_user(¶ms); + let marker_height = self.height.to_user(¶ms); + + if marker_width.approx_eq_cairo(0.0) || marker_height.approx_eq_cairo(0.0) { + // markerWidth or markerHeight set to 0 disables rendering of the element + // https://www.w3.org/TR/SVG/painting.html#MarkerWidthAttribute + return Ok(draw_ctx.empty_bbox()); + } + + let rotation = match self.orient { + MarkerOrient::Auto => computed_angle, + MarkerOrient::AutoStartReverse => { + if marker_type == MarkerType::Start { + computed_angle.flip() + } else { + computed_angle + } + } + MarkerOrient::Angle(a) => a, + }; + + let mut transform = Transform::new_translate(xpos, ypos).pre_rotate(rotation); + + if self.units == MarkerUnits::StrokeWidth { + transform = transform.pre_scale(line_width, line_width); + } + + let content_viewport = if let Some(vbox) = self.vbox { + if vbox.is_empty() { + return Ok(draw_ctx.empty_bbox()); + } + + let r = self + .aspect + .compute(&vbox, &Rect::from_size(marker_width, marker_height)); + + let (vb_width, vb_height) = vbox.size(); + transform = transform.pre_scale(r.width() / vb_width, r.height() / vb_height); + + viewport.with_view_box(vb_width, vb_height) + } else { + viewport.with_view_box(marker_width, marker_height) + }; + + let content_params = NormalizeParams::new(values, &content_viewport); + + transform = transform.pre_translate( + -self.ref_x.to_user(&content_params), + -self.ref_y.to_user(&content_params), + ); + + let clip = if values.is_overflow() { + None + } else { + Some( + self.vbox + .map_or_else(|| Rect::from_size(marker_width, marker_height), |vb| *vb), + ) + }; + + let elt = node.borrow_element(); + let stacking_ctx = + StackingContext::new(draw_ctx.session(), acquired_nodes, &elt, transform, values); + + // FIXME: use content_viewport + draw_ctx.with_discrete_layer( + &stacking_ctx, + acquired_nodes, + &content_viewport, + clipping, + clip, + &mut |an, dc| node.draw_children(an, &cascaded, &content_viewport, dc, clipping), + ) + } +} + +impl ElementTrait for Marker { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "markerUnits") => { + set_attribute(&mut self.units, attr.parse(value), session) + } + expanded_name!("", "refX") => { + set_attribute(&mut self.ref_x, attr.parse(value), session) + } + expanded_name!("", "refY") => { + set_attribute(&mut self.ref_y, attr.parse(value), session) + } + expanded_name!("", "markerWidth") => { + set_attribute(&mut self.width, attr.parse(value), session) + } + expanded_name!("", "markerHeight") => { + set_attribute(&mut self.height, attr.parse(value), session) + } + expanded_name!("", "orient") => { + set_attribute(&mut self.orient, attr.parse(value), session) + } + expanded_name!("", "preserveAspectRatio") => { + set_attribute(&mut self.aspect, attr.parse(value), session) + } + expanded_name!("", "viewBox") => { + set_attribute(&mut self.vbox, attr.parse(value), session) + } + _ => (), + } + } + } +} + +// Machinery to figure out marker orientations +#[derive(Debug, PartialEq)] +enum Segment { + Degenerate { + // A single lone point + x: f64, + y: f64, + }, + + LineOrCurve { + x1: f64, + y1: f64, + x2: f64, + y2: f64, + x3: f64, + y3: f64, + x4: f64, + y4: f64, + }, +} + +impl Segment { + fn degenerate(x: f64, y: f64) -> Segment { + Segment::Degenerate { x, y } + } + + fn curve(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> Segment { + Segment::LineOrCurve { + x1, + y1, + x2, + y2, + x3, + y3, + x4, + y4, + } + } + + fn line(x1: f64, y1: f64, x2: f64, y2: f64) -> Segment { + Segment::curve(x1, y1, x2, y2, x1, y1, x2, y2) + } + + // If the segment has directionality, returns two vectors (v1x, v1y, v2x, v2y); otherwise, + // returns None. The vectors are the tangents at the beginning and at the end of the segment, + // respectively. A segment does not have directionality if it is degenerate (i.e. a single + // point) or a zero-length segment, i.e. where all four control points are coincident (the first + // and last control points may coincide, but the others may define a loop - thus nonzero length) + fn get_directionalities(&self) -> Option<(f64, f64, f64, f64)> { + match *self { + Segment::Degenerate { .. } => None, + + Segment::LineOrCurve { + x1, + y1, + x2, + y2, + x3, + y3, + x4, + y4, + } => { + let coincide_1_and_2 = points_equal(x1, y1, x2, y2); + let coincide_1_and_3 = points_equal(x1, y1, x3, y3); + let coincide_1_and_4 = points_equal(x1, y1, x4, y4); + let coincide_2_and_3 = points_equal(x2, y2, x3, y3); + let coincide_2_and_4 = points_equal(x2, y2, x4, y4); + let coincide_3_and_4 = points_equal(x3, y3, x4, y4); + + if coincide_1_and_2 && coincide_1_and_3 && coincide_1_and_4 { + None + } else if coincide_1_and_2 && coincide_1_and_3 { + Some((x4 - x1, y4 - y1, x4 - x3, y4 - y3)) + } else if coincide_1_and_2 && coincide_3_and_4 { + Some((x4 - x1, y4 - y1, x4 - x1, y4 - y1)) + } else if coincide_2_and_3 && coincide_2_and_4 { + Some((x2 - x1, y2 - y1, x4 - x1, y4 - y1)) + } else if coincide_1_and_2 { + Some((x3 - x1, y3 - y1, x4 - x3, y4 - y3)) + } else if coincide_3_and_4 { + Some((x2 - x1, y2 - y1, x4 - x2, y4 - y2)) + } else { + Some((x2 - x1, y2 - y1, x4 - x3, y4 - y3)) + } + } + } + } +} + +fn points_equal(x1: f64, y1: f64, x2: f64, y2: f64) -> bool { + x1.approx_eq_cairo(x2) && y1.approx_eq_cairo(y2) +} + +enum SegmentState { + Initial, + NewSubpath, + InSubpath, + ClosedSubpath, +} + +#[derive(Debug, PartialEq)] +struct Segments(Vec<Segment>); + +impl Deref for Segments { + type Target = [Segment]; + + fn deref(&self) -> &[Segment] { + &self.0 + } +} + +// This converts a path builder into a vector of curveto-like segments. +// Each segment can be: +// +// 1. Segment::Degenerate => the segment is actually a single point (x, y) +// +// 2. Segment::LineOrCurve => either a lineto or a curveto (or the effective +// lineto that results from a closepath). +// We have the following points: +// P1 = (x1, y1) +// P2 = (x2, y2) +// P3 = (x3, y3) +// P4 = (x4, y4) +// +// The start and end points are P1 and P4, respectively. +// The tangent at the start point is given by the vector (P2 - P1). +// The tangent at the end point is given by the vector (P4 - P3). +// The tangents also work if the segment refers to a lineto (they will +// both just point in the same direction). +impl From<&Path> for Segments { + fn from(path: &Path) -> Segments { + let mut last_x: f64; + let mut last_y: f64; + + let mut cur_x: f64 = 0.0; + let mut cur_y: f64 = 0.0; + let mut subpath_start_x: f64 = 0.0; + let mut subpath_start_y: f64 = 0.0; + + let mut segments = Vec::new(); + let mut state = SegmentState::Initial; + + for path_command in path.iter() { + last_x = cur_x; + last_y = cur_y; + + match path_command { + PathCommand::MoveTo(x, y) => { + cur_x = x; + cur_y = y; + + subpath_start_x = cur_x; + subpath_start_y = cur_y; + + match state { + SegmentState::Initial | SegmentState::InSubpath => { + // Ignore the very first moveto in a sequence (Initial state), + // or if we were already drawing within a subpath, start + // a new subpath. + state = SegmentState::NewSubpath; + } + + SegmentState::NewSubpath => { + // We had just begun a new subpath (i.e. from a moveto) and we got + // another moveto? Output a stray point for the + // previous moveto. + segments.push(Segment::degenerate(last_x, last_y)); + state = SegmentState::NewSubpath; + } + + SegmentState::ClosedSubpath => { + // Cairo outputs a moveto after every closepath, so that subsequent + // lineto/curveto commands will start at the closed vertex. + // We don't want to actually emit a point (a degenerate segment) in + // that artificial-moveto case. + // + // We'll reset to the Initial state so that a subsequent "real" + // moveto will be handled as the beginning of a new subpath, or a + // degenerate point, as usual. + state = SegmentState::Initial; + } + } + } + + PathCommand::LineTo(x, y) => { + cur_x = x; + cur_y = y; + + segments.push(Segment::line(last_x, last_y, cur_x, cur_y)); + + state = SegmentState::InSubpath; + } + + PathCommand::CurveTo(curve) => { + let CubicBezierCurve { + pt1: (x2, y2), + pt2: (x3, y3), + to, + } = curve; + cur_x = to.0; + cur_y = to.1; + + segments.push(Segment::curve(last_x, last_y, x2, y2, x3, y3, cur_x, cur_y)); + + state = SegmentState::InSubpath; + } + + PathCommand::Arc(arc) => { + cur_x = arc.to.0; + cur_y = arc.to.1; + + match arc.center_parameterization() { + ArcParameterization::CenterParameters { + center, + radii, + theta1, + delta_theta, + } => { + let rot = arc.x_axis_rotation; + let theta2 = 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 segment1 = + arc_segment(center, radii, rot, theta1, theta1 + d_theta); + let segment2 = + arc_segment(center, radii, rot, theta2 - d_theta, theta2); + + let (x2, y2) = segment1.pt1; + let (x3, y3) = segment2.pt2; + segments + .push(Segment::curve(last_x, last_y, x2, y2, x3, y3, cur_x, cur_y)); + + state = SegmentState::InSubpath; + } + ArcParameterization::LineTo => { + segments.push(Segment::line(last_x, last_y, cur_x, cur_y)); + + state = SegmentState::InSubpath; + } + ArcParameterization::Omit => {} + } + } + + PathCommand::ClosePath => { + cur_x = subpath_start_x; + cur_y = subpath_start_y; + + segments.push(Segment::line(last_x, last_y, cur_x, cur_y)); + + state = SegmentState::ClosedSubpath; + } + } + } + + if let SegmentState::NewSubpath = state { + // Output a lone point if we started a subpath with a moveto + // command, but there are no subsequent commands. + segments.push(Segment::degenerate(cur_x, cur_y)); + }; + + Segments(segments) + } +} + +// The SVG spec 1.1 says http://www.w3.org/TR/SVG/implnote.html#PathElementImplementationNotes +// Certain line-capping and line-joining situations and markers +// require that a path segment have directionality at its start and +// end points. Zero-length path segments have no directionality. In +// these cases, the following algorithm is used to establish +// directionality: to determine the directionality of the start +// point of a zero-length path segment, go backwards in the path +// data specification within the current subpath until you find a +// segment which has directionality at its end point (e.g., a path +// segment with non-zero length) and use its ending direction; +// otherwise, temporarily consider the start point to lack +// directionality. Similarly, to determine the directionality of the +// end point of a zero-length path segment, go forwards in the path +// data specification within the current subpath until you find a +// segment which has directionality at its start point (e.g., a path +// segment with non-zero length) and use its starting direction; +// otherwise, temporarily consider the end point to lack +// directionality. If the start point has directionality but the end +// point doesn't, then the end point uses the start point's +// directionality. If the end point has directionality but the start +// point doesn't, then the start point uses the end point's +// directionality. Otherwise, set the directionality for the path +// segment's start and end points to align with the positive x-axis +// in user space. +impl Segments { + fn find_incoming_angle_backwards(&self, start_index: usize) -> Option<Angle> { + // "go backwards ... within the current subpath until ... segment which has directionality + // at its end point" + for segment in self[..=start_index].iter().rev() { + match *segment { + Segment::Degenerate { .. } => { + return None; // reached the beginning of the subpath as we ran into a standalone point + } + + Segment::LineOrCurve { .. } => match segment.get_directionalities() { + Some((_, _, v2x, v2y)) => { + return Some(Angle::from_vector(v2x, v2y)); + } + None => { + continue; + } + }, + } + } + + None + } + + fn find_outgoing_angle_forwards(&self, start_index: usize) -> Option<Angle> { + // "go forwards ... within the current subpath until ... segment which has directionality at + // its start point" + for segment in &self[start_index..] { + match *segment { + Segment::Degenerate { .. } => { + return None; // reached the end of a subpath as we ran into a standalone point + } + + Segment::LineOrCurve { .. } => match segment.get_directionalities() { + Some((v1x, v1y, _, _)) => { + return Some(Angle::from_vector(v1x, v1y)); + } + None => { + continue; + } + }, + } + } + + None + } +} + +// From SVG's marker-start, marker-mid, marker-end properties +#[derive(Debug, Copy, Clone, PartialEq)] +enum MarkerType { + Start, + Middle, + End, +} + +fn emit_marker_by_node( + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + acquired_nodes: &mut AcquiredNodes<'_>, + marker: &layout::Marker, + xpos: f64, + ypos: f64, + computed_angle: Angle, + line_width: f64, + clipping: bool, + marker_type: MarkerType, +) -> Result<BoundingBox, RenderingError> { + match acquired_nodes.acquire_ref(marker.node_ref.as_ref().unwrap()) { + Ok(acquired) => { + let node = acquired.get(); + + let marker_elt = borrow_element_as!(node, Marker); + + marker_elt.render( + node, + acquired_nodes, + viewport, + draw_ctx, + xpos, + ypos, + computed_angle, + line_width, + clipping, + marker_type, + marker, + ) + } + + Err(e) => { + rsvg_log!(draw_ctx.session(), "could not acquire marker: {}", e); + Ok(draw_ctx.empty_bbox()) + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +enum MarkerEndpoint { + Start, + End, +} + +fn emit_marker<E>( + segment: &Segment, + endpoint: MarkerEndpoint, + marker_type: MarkerType, + orient: Angle, + emit_fn: &mut E, +) -> Result<BoundingBox, RenderingError> +where + E: FnMut(MarkerType, f64, f64, Angle) -> Result<BoundingBox, RenderingError>, +{ + let (x, y) = match *segment { + Segment::Degenerate { x, y } => (x, y), + + Segment::LineOrCurve { x1, y1, x4, y4, .. } => match endpoint { + MarkerEndpoint::Start => (x1, y1), + MarkerEndpoint::End => (x4, y4), + }, + }; + + emit_fn(marker_type, x, y, orient) +} + +pub fn render_markers_for_shape( + shape: &Shape, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + acquired_nodes: &mut AcquiredNodes<'_>, + clipping: bool, +) -> Result<BoundingBox, RenderingError> { + if shape.stroke.width.approx_eq_cairo(0.0) { + return Ok(draw_ctx.empty_bbox()); + } + + if shape.marker_start.node_ref.is_none() + && shape.marker_mid.node_ref.is_none() + && shape.marker_end.node_ref.is_none() + { + return Ok(draw_ctx.empty_bbox()); + } + + emit_markers_for_path( + &shape.path, + draw_ctx.empty_bbox(), + &mut |marker_type: MarkerType, x: f64, y: f64, computed_angle: Angle| { + let marker = match marker_type { + MarkerType::Start => &shape.marker_start, + MarkerType::Middle => &shape.marker_mid, + MarkerType::End => &shape.marker_end, + }; + + if marker.node_ref.is_some() { + emit_marker_by_node( + viewport, + draw_ctx, + acquired_nodes, + marker, + x, + y, + computed_angle, + shape.stroke.width, + clipping, + marker_type, + ) + } else { + Ok(draw_ctx.empty_bbox()) + } + }, + ) +} + +fn emit_markers_for_path<E>( + path: &Path, + empty_bbox: BoundingBox, + emit_fn: &mut E, +) -> Result<BoundingBox, RenderingError> +where + E: FnMut(MarkerType, f64, f64, Angle) -> Result<BoundingBox, RenderingError>, +{ + enum SubpathState { + NoSubpath, + InSubpath, + } + + let mut bbox = empty_bbox; + + // Convert the path to a list of segments and bare points + let segments = Segments::from(path); + + let mut subpath_state = SubpathState::NoSubpath; + + for (i, segment) in segments.iter().enumerate() { + match *segment { + Segment::Degenerate { .. } => { + if let SubpathState::InSubpath = subpath_state { + assert!(i > 0); + + // Got a lone point after a subpath; render the subpath's end marker first + let angle = segments + .find_incoming_angle_backwards(i - 1) + .unwrap_or_else(|| Angle::new(0.0)); + let marker_bbox = emit_marker( + &segments[i - 1], + MarkerEndpoint::End, + MarkerType::End, + angle, + emit_fn, + )?; + bbox.insert(&marker_bbox); + } + + // Render marker for the lone point; no directionality + let marker_bbox = emit_marker( + segment, + MarkerEndpoint::Start, + MarkerType::Middle, + Angle::new(0.0), + emit_fn, + )?; + bbox.insert(&marker_bbox); + + subpath_state = SubpathState::NoSubpath; + } + + Segment::LineOrCurve { .. } => { + // Not a degenerate segment + match subpath_state { + SubpathState::NoSubpath => { + let angle = segments + .find_outgoing_angle_forwards(i) + .unwrap_or_else(|| Angle::new(0.0)); + let marker_bbox = emit_marker( + segment, + MarkerEndpoint::Start, + MarkerType::Start, + angle, + emit_fn, + )?; + bbox.insert(&marker_bbox); + + subpath_state = SubpathState::InSubpath; + } + + SubpathState::InSubpath => { + assert!(i > 0); + + let incoming = segments.find_incoming_angle_backwards(i - 1); + let outgoing = segments.find_outgoing_angle_forwards(i); + + let angle = match (incoming, outgoing) { + (Some(incoming), Some(outgoing)) => incoming.bisect(outgoing), + (Some(incoming), _) => incoming, + (_, Some(outgoing)) => outgoing, + _ => Angle::new(0.0), + }; + + let marker_bbox = emit_marker( + segment, + MarkerEndpoint::Start, + MarkerType::Middle, + angle, + emit_fn, + )?; + bbox.insert(&marker_bbox); + } + } + } + } + } + + // Finally, render the last point + if !segments.is_empty() { + let segment = &segments[segments.len() - 1]; + if let Segment::LineOrCurve { .. } = *segment { + let incoming = segments + .find_incoming_angle_backwards(segments.len() - 1) + .unwrap_or_else(|| Angle::new(0.0)); + + let angle = { + if let PathCommand::ClosePath = path.iter().nth(segments.len()).unwrap() { + let outgoing = segments + .find_outgoing_angle_forwards(0) + .unwrap_or_else(|| Angle::new(0.0)); + incoming.bisect(outgoing) + } else { + incoming + } + }; + + let marker_bbox = emit_marker( + segment, + MarkerEndpoint::End, + MarkerType::End, + angle, + emit_fn, + )?; + bbox.insert(&marker_bbox); + } + } + + Ok(bbox) +} + +#[cfg(test)] +mod parser_tests { + use super::*; + + #[test] + fn parsing_invalid_marker_units_yields_error() { + assert!(MarkerUnits::parse_str("").is_err()); + assert!(MarkerUnits::parse_str("foo").is_err()); + } + + #[test] + fn parses_marker_units() { + assert_eq!( + MarkerUnits::parse_str("userSpaceOnUse").unwrap(), + MarkerUnits::UserSpaceOnUse + ); + assert_eq!( + MarkerUnits::parse_str("strokeWidth").unwrap(), + MarkerUnits::StrokeWidth + ); + } + + #[test] + fn parsing_invalid_marker_orient_yields_error() { + assert!(MarkerOrient::parse_str("").is_err()); + assert!(MarkerOrient::parse_str("blah").is_err()); + assert!(MarkerOrient::parse_str("45blah").is_err()); + } + + #[test] + fn parses_marker_orient() { + assert_eq!(MarkerOrient::parse_str("auto").unwrap(), MarkerOrient::Auto); + assert_eq!( + MarkerOrient::parse_str("auto-start-reverse").unwrap(), + MarkerOrient::AutoStartReverse + ); + + assert_eq!( + MarkerOrient::parse_str("0").unwrap(), + MarkerOrient::Angle(Angle::new(0.0)) + ); + assert_eq!( + MarkerOrient::parse_str("180").unwrap(), + MarkerOrient::Angle(Angle::from_degrees(180.0)) + ); + assert_eq!( + MarkerOrient::parse_str("180deg").unwrap(), + MarkerOrient::Angle(Angle::from_degrees(180.0)) + ); + assert_eq!( + MarkerOrient::parse_str("-400grad").unwrap(), + MarkerOrient::Angle(Angle::from_degrees(-360.0)) + ); + assert_eq!( + MarkerOrient::parse_str("1rad").unwrap(), + MarkerOrient::Angle(Angle::new(1.0)) + ); + } +} + +#[cfg(test)] +mod directionality_tests { + use super::*; + use crate::path_builder::PathBuilder; + + // Single open path; the easy case + fn setup_open_path() -> Segments { + let mut builder = PathBuilder::default(); + + builder.move_to(10.0, 10.0); + builder.line_to(20.0, 10.0); + builder.line_to(20.0, 20.0); + + Segments::from(&builder.into_path()) + } + + #[test] + fn path_to_segments_handles_open_path() { + let expected_segments: Segments = Segments(vec![ + Segment::line(10.0, 10.0, 20.0, 10.0), + Segment::line(20.0, 10.0, 20.0, 20.0), + ]); + + assert_eq!(setup_open_path(), expected_segments); + } + + fn setup_multiple_open_subpaths() -> Segments { + let mut builder = PathBuilder::default(); + + builder.move_to(10.0, 10.0); + builder.line_to(20.0, 10.0); + builder.line_to(20.0, 20.0); + + builder.move_to(30.0, 30.0); + builder.line_to(40.0, 30.0); + builder.curve_to(50.0, 35.0, 60.0, 60.0, 70.0, 70.0); + builder.line_to(80.0, 90.0); + + Segments::from(&builder.into_path()) + } + + #[test] + fn path_to_segments_handles_multiple_open_subpaths() { + let expected_segments: Segments = Segments(vec![ + Segment::line(10.0, 10.0, 20.0, 10.0), + Segment::line(20.0, 10.0, 20.0, 20.0), + Segment::line(30.0, 30.0, 40.0, 30.0), + Segment::curve(40.0, 30.0, 50.0, 35.0, 60.0, 60.0, 70.0, 70.0), + Segment::line(70.0, 70.0, 80.0, 90.0), + ]); + + assert_eq!(setup_multiple_open_subpaths(), expected_segments); + } + + // Closed subpath; must have a line segment back to the first point + fn setup_closed_subpath() -> Segments { + let mut builder = PathBuilder::default(); + + builder.move_to(10.0, 10.0); + builder.line_to(20.0, 10.0); + builder.line_to(20.0, 20.0); + builder.close_path(); + + Segments::from(&builder.into_path()) + } + + #[test] + fn path_to_segments_handles_closed_subpath() { + let expected_segments: Segments = Segments(vec![ + Segment::line(10.0, 10.0, 20.0, 10.0), + Segment::line(20.0, 10.0, 20.0, 20.0), + Segment::line(20.0, 20.0, 10.0, 10.0), + ]); + + assert_eq!(setup_closed_subpath(), expected_segments); + } + + // Multiple closed subpaths; each must have a line segment back to their + // initial points, with no degenerate segments between subpaths. + fn setup_multiple_closed_subpaths() -> Segments { + let mut builder = PathBuilder::default(); + + builder.move_to(10.0, 10.0); + builder.line_to(20.0, 10.0); + builder.line_to(20.0, 20.0); + builder.close_path(); + + builder.move_to(30.0, 30.0); + builder.line_to(40.0, 30.0); + builder.curve_to(50.0, 35.0, 60.0, 60.0, 70.0, 70.0); + builder.line_to(80.0, 90.0); + builder.close_path(); + + Segments::from(&builder.into_path()) + } + + #[test] + fn path_to_segments_handles_multiple_closed_subpaths() { + let expected_segments: Segments = Segments(vec![ + Segment::line(10.0, 10.0, 20.0, 10.0), + Segment::line(20.0, 10.0, 20.0, 20.0), + Segment::line(20.0, 20.0, 10.0, 10.0), + Segment::line(30.0, 30.0, 40.0, 30.0), + Segment::curve(40.0, 30.0, 50.0, 35.0, 60.0, 60.0, 70.0, 70.0), + Segment::line(70.0, 70.0, 80.0, 90.0), + Segment::line(80.0, 90.0, 30.0, 30.0), + ]); + + assert_eq!(setup_multiple_closed_subpaths(), expected_segments); + } + + // A lineto follows the first closed subpath, with no moveto to start the second subpath. + // The lineto must start at the first point of the first subpath. + fn setup_no_moveto_after_closepath() -> Segments { + let mut builder = PathBuilder::default(); + + builder.move_to(10.0, 10.0); + builder.line_to(20.0, 10.0); + builder.line_to(20.0, 20.0); + builder.close_path(); + + builder.line_to(40.0, 30.0); + + Segments::from(&builder.into_path()) + } + + #[test] + fn path_to_segments_handles_no_moveto_after_closepath() { + let expected_segments: Segments = Segments(vec![ + Segment::line(10.0, 10.0, 20.0, 10.0), + Segment::line(20.0, 10.0, 20.0, 20.0), + Segment::line(20.0, 20.0, 10.0, 10.0), + Segment::line(10.0, 10.0, 40.0, 30.0), + ]); + + assert_eq!(setup_no_moveto_after_closepath(), expected_segments); + } + + // Sequence of moveto; should generate degenerate points. + // This test is not enabled right now! We create the + // path fixtures with Cairo, and Cairo compresses + // sequences of moveto into a single one. So, we can't + // really test this, as we don't get the fixture we want. + // + // Eventually we'll probably have to switch librsvg to + // its own internal path representation which should + // allow for unelided path commands, and which should + // only build a cairo_path_t for the final rendering step. + // + // fn setup_sequence_of_moveto () -> Segments { + // let mut builder = PathBuilder::default (); + // + // builder.move_to (10.0, 10.0); + // builder.move_to (20.0, 20.0); + // builder.move_to (30.0, 30.0); + // builder.move_to (40.0, 40.0); + // + // Segments::from(&builder.into_path()) + // } + // + // #[test] + // fn path_to_segments_handles_sequence_of_moveto () { + // let expected_segments: Segments = Segments(vec! [ + // Segment::degenerate(10.0, 10.0), + // Segment::degenerate(20.0, 20.0), + // Segment::degenerate(30.0, 30.0), + // Segment::degenerate(40.0, 40.0), + // ]); + // + // assert_eq!(setup_sequence_of_moveto(), expected_segments); + // } + + #[test] + fn degenerate_segment_has_no_directionality() { + let s = Segment::degenerate(1.0, 2.0); + assert!(s.get_directionalities().is_none()); + } + + #[test] + fn line_segment_has_directionality() { + let s = Segment::line(1.0, 2.0, 3.0, 4.0); + let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); + assert_eq!((2.0, 2.0), (v1x, v1y)); + assert_eq!((2.0, 2.0), (v2x, v2y)); + } + + #[test] + fn line_segment_with_coincident_ends_has_no_directionality() { + let s = Segment::line(1.0, 2.0, 1.0, 2.0); + assert!(s.get_directionalities().is_none()); + } + + #[test] + fn curve_has_directionality() { + let s = Segment::curve(1.0, 2.0, 3.0, 5.0, 8.0, 13.0, 20.0, 33.0); + let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); + assert_eq!((2.0, 3.0), (v1x, v1y)); + assert_eq!((12.0, 20.0), (v2x, v2y)); + } + + #[test] + fn curves_with_loops_and_coincident_ends_have_directionality() { + let s = Segment::curve(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0, 2.0); + let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); + assert_eq!((2.0, 2.0), (v1x, v1y)); + assert_eq!((-4.0, -4.0), (v2x, v2y)); + + let s = Segment::curve(1.0, 2.0, 1.0, 2.0, 3.0, 4.0, 1.0, 2.0); + let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); + assert_eq!((2.0, 2.0), (v1x, v1y)); + assert_eq!((-2.0, -2.0), (v2x, v2y)); + + let s = Segment::curve(1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 1.0, 2.0); + let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); + assert_eq!((2.0, 2.0), (v1x, v1y)); + assert_eq!((-2.0, -2.0), (v2x, v2y)); + } + + #[test] + fn curve_with_coincident_control_points_has_no_directionality() { + let s = Segment::curve(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0); + assert!(s.get_directionalities().is_none()); + } + + #[test] + fn curve_with_123_coincident_has_directionality() { + let s = Segment::curve(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 40.0); + let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); + assert_eq!((20.0, 40.0), (v1x, v1y)); + assert_eq!((20.0, 40.0), (v2x, v2y)); + } + + #[test] + fn curve_with_234_coincident_has_directionality() { + let s = Segment::curve(20.0, 40.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); + assert_eq!((-20.0, -40.0), (v1x, v1y)); + assert_eq!((-20.0, -40.0), (v2x, v2y)); + } + + #[test] + fn curve_with_12_34_coincident_has_directionality() { + let s = Segment::curve(20.0, 40.0, 20.0, 40.0, 60.0, 70.0, 60.0, 70.0); + let (v1x, v1y, v2x, v2y) = s.get_directionalities().unwrap(); + assert_eq!((40.0, 30.0), (v1x, v1y)); + assert_eq!((40.0, 30.0), (v2x, v2y)); + } +} + +#[cfg(test)] +mod marker_tests { + use super::*; + use crate::path_builder::PathBuilder; + + #[test] + fn emits_for_open_subpath() { + let mut builder = PathBuilder::default(); + builder.move_to(0.0, 0.0); + builder.line_to(1.0, 0.0); + builder.line_to(1.0, 1.0); + builder.line_to(0.0, 1.0); + + let mut v = Vec::new(); + + assert!(emit_markers_for_path( + &builder.into_path(), + BoundingBox::new(), + &mut |marker_type: MarkerType, + x: f64, + y: f64, + computed_angle: Angle| + -> Result<BoundingBox, RenderingError> { + v.push((marker_type, x, y, computed_angle)); + Ok(BoundingBox::new()) + } + ) + .is_ok()); + + assert_eq!( + v, + vec![ + (MarkerType::Start, 0.0, 0.0, Angle::new(0.0)), + (MarkerType::Middle, 1.0, 0.0, Angle::from_vector(1.0, 1.0)), + (MarkerType::Middle, 1.0, 1.0, Angle::from_vector(-1.0, 1.0)), + (MarkerType::End, 0.0, 1.0, Angle::from_vector(-1.0, 0.0)), + ] + ); + } + + #[test] + fn emits_for_closed_subpath() { + let mut builder = PathBuilder::default(); + builder.move_to(0.0, 0.0); + builder.line_to(1.0, 0.0); + builder.line_to(1.0, 1.0); + builder.line_to(0.0, 1.0); + builder.close_path(); + + let mut v = Vec::new(); + + assert!(emit_markers_for_path( + &builder.into_path(), + BoundingBox::new(), + &mut |marker_type: MarkerType, + x: f64, + y: f64, + computed_angle: Angle| + -> Result<BoundingBox, RenderingError> { + v.push((marker_type, x, y, computed_angle)); + Ok(BoundingBox::new()) + } + ) + .is_ok()); + + assert_eq!( + v, + vec![ + (MarkerType::Start, 0.0, 0.0, Angle::new(0.0)), + (MarkerType::Middle, 1.0, 0.0, Angle::from_vector(1.0, 1.0)), + (MarkerType::Middle, 1.0, 1.0, Angle::from_vector(-1.0, 1.0)), + (MarkerType::Middle, 0.0, 1.0, Angle::from_vector(-1.0, -1.0)), + (MarkerType::End, 0.0, 0.0, Angle::from_vector(1.0, -1.0)), + ] + ); + } +} diff --git a/rsvg/src/node.rs b/rsvg/src/node.rs new file mode 100644 index 00000000..1db4ea52 --- /dev/null +++ b/rsvg/src/node.rs @@ -0,0 +1,377 @@ +//! Tree nodes, the representation of SVG elements. +//! +//! Librsvg uses the [rctree crate][rctree] to represent the SVG tree of elements. +//! Its [`rctree::Node`] struct provides a generic wrapper over nodes in a tree. +//! Librsvg puts a [`NodeData`] as the type parameter of [`rctree::Node`]. For convenience, +//! librsvg has a type alias [`Node`]` = rctree::Node<NodeData>`. +//! +//! Nodes are not constructed directly by callers; + +use markup5ever::QualName; +use std::cell::{Ref, RefMut}; +use std::fmt; +use std::sync::Arc; + +use crate::bbox::BoundingBox; +use crate::document::AcquiredNodes; +use crate::drawing_ctx::{DrawingCtx, Viewport}; +use crate::element::*; +use crate::error::*; +use crate::paint_server::PaintSource; +use crate::properties::ComputedValues; +use crate::session::Session; +use crate::text::Chars; +use crate::xml::Attributes; + +/// Strong reference to an element in the SVG tree. +/// +/// See the [module documentation][self] for more information. +pub type Node = rctree::Node<NodeData>; + +/// Weak reference to an element in the SVG tree. +/// +/// See the [module documentation][self] for more information. +pub type WeakNode = rctree::WeakNode<NodeData>; + +/// Data for a single DOM node. +/// +/// ## Memory consumption +/// +/// SVG files look like this, roughly: +/// +/// ```xml +/// <svg> +/// <rect x="10" y="20"/> +/// <path d="..."/> +/// <text x="10" y="20">Hello</text> +/// <!-- etc --> +/// </svg> +/// ``` +/// +/// Each element has a bunch of data, including the styles, which is +/// the biggest consumer of memory within the `Element` struct. But +/// between each element there is a text node; in the example above +/// there are a bunch of text nodes with just whitespace (newlines and +/// spaces), and a single text node with "`Hello`" in it from the +/// `<text>` element. +/// +/// ## Accessing the node's contents +/// +/// Code that traverses the DOM tree needs to find out at runtime what +/// each node stands for. First, use the `is_chars` or `is_element` +/// methods from the `NodeBorrow` trait to see if you can then call +/// `borrow_chars`, `borrow_element`, or `borrow_element_mut`. +pub enum NodeData { + Element(Box<Element>), + Text(Box<Chars>), +} + +impl NodeData { + pub fn new_element(session: &Session, name: &QualName, attrs: Attributes) -> NodeData { + NodeData::Element(Box::new(Element::new(session, name, attrs))) + } + + pub fn new_chars(initial_text: &str) -> NodeData { + NodeData::Text(Box::new(Chars::new(initial_text))) + } +} + +impl fmt::Display for NodeData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + NodeData::Element(ref e) => { + write!(f, "{e}")?; + } + NodeData::Text(_) => { + write!(f, "Chars")?; + } + } + + Ok(()) + } +} + +/// Can obtain computed values from a node +/// +/// In our tree of SVG elements (Node in our parlance), each node stores a `ComputedValues` that +/// gets computed during the initial CSS cascade. However, sometimes nodes need to be rendered +/// outside the normal hierarchy. For example, the `<use>` element can "instance" a subtree from +/// elsewhere in the SVG; it causes the instanced subtree to re-cascade from the computed values for +/// the `<use>` element. +/// +/// You can then call the `get()` method on the resulting `CascadedValues` to get a +/// `&ComputedValues` whose fields you can access. +pub struct CascadedValues<'a> { + inner: CascadedInner<'a>, + pub context_stroke: Option<Arc<PaintSource>>, + pub context_fill: Option<Arc<PaintSource>>, +} + +enum CascadedInner<'a> { + FromNode(Ref<'a, Element>), + FromValues(Box<ComputedValues>), +} + +impl<'a> CascadedValues<'a> { + /// Creates a `CascadedValues` that has the same cascading mode as &self + /// + /// This is what nodes should normally use to draw their children from their `draw()` method. + /// Nodes that need to override the cascade for their children can use `new_from_values()` + /// instead. + pub fn clone_with_node(&self, node: &'a Node) -> CascadedValues<'a> { + match self.inner { + CascadedInner::FromNode(_) => CascadedValues { + inner: CascadedInner::FromNode(node.borrow_element()), + context_fill: self.context_fill.clone(), + context_stroke: self.context_stroke.clone(), + }, + + CascadedInner::FromValues(ref v) => CascadedValues::new_from_values( + node, + v, + self.context_fill.clone(), + self.context_stroke.clone(), + ), + } + } + + /// Creates a `CascadedValues` that will hold the `node`'s computed values + /// + /// This is to be used only in the toplevel drawing function, or in elements like `<marker>` + /// that don't propagate their parent's cascade to their children. All others should use + /// `new()` to derive the cascade from an existing one. + pub fn new_from_node(node: &Node) -> CascadedValues<'_> { + CascadedValues { + inner: CascadedInner::FromNode(node.borrow_element()), + context_fill: None, + context_stroke: None, + } + } + + /// Creates a `CascadedValues` that will override the `node`'s cascade with the specified + /// `values` + /// + /// This is for the `<use>` element, which draws the element which it references with the + /// `<use>`'s own cascade, not with the element's original cascade. + pub fn new_from_values( + node: &'a Node, + values: &ComputedValues, + fill: Option<Arc<PaintSource>>, + stroke: Option<Arc<PaintSource>>, + ) -> CascadedValues<'a> { + let mut v = Box::new(values.clone()); + node.borrow_element() + .get_specified_values() + .to_computed_values(&mut v); + + CascadedValues { + inner: CascadedInner::FromValues(v), + context_fill: fill, + context_stroke: stroke, + } + } + + /// Returns the cascaded `ComputedValues`. + /// + /// Nodes should use this from their `Draw::draw()` implementation to get the + /// `ComputedValues` from the `CascadedValues` that got passed to `draw()`. + pub fn get(&'a self) -> &'a ComputedValues { + match self.inner { + CascadedInner::FromNode(ref e) => e.get_computed_values(), + CascadedInner::FromValues(ref v) => v, + } + + // if values.fill == "context-fill" { + // values.fill=self.context_fill + // } + // if values.stroke == "context-stroke" { + // values.stroke=self.context_stroke + // } + } +} + +/// Helper trait to get different NodeData variants +pub trait NodeBorrow { + /// Returns `false` for NodeData::Text, `true` otherwise. + fn is_element(&self) -> bool; + + /// Returns `true` for NodeData::Text, `false` otherwise. + fn is_chars(&self) -> bool; + + /// Borrows a `Chars` reference. + /// + /// Panics: will panic if `&self` is not a `NodeData::Text` node + fn borrow_chars(&self) -> Ref<'_, Chars>; + + /// Borrows an `Element` reference + /// + /// Panics: will panic if `&self` is not a `NodeData::Element` node + fn borrow_element(&self) -> Ref<'_, Element>; + + /// Borrows an `Element` reference mutably + /// + /// Panics: will panic if `&self` is not a `NodeData::Element` node + fn borrow_element_mut(&mut self) -> RefMut<'_, Element>; + + /// Borrows an `ElementData` reference to the concrete element type. + /// + /// Panics: will panic if `&self` is not a `NodeData::Element` node + fn borrow_element_data(&self) -> Ref<'_, ElementData>; +} + +impl NodeBorrow for Node { + fn is_element(&self) -> bool { + matches!(*self.borrow(), NodeData::Element(_)) + } + + fn is_chars(&self) -> bool { + matches!(*self.borrow(), NodeData::Text(_)) + } + + fn borrow_chars(&self) -> Ref<'_, Chars> { + Ref::map(self.borrow(), |n| match n { + NodeData::Text(c) => &**c, + _ => panic!("tried to borrow_chars for a non-text node"), + }) + } + + fn borrow_element(&self) -> Ref<'_, Element> { + Ref::map(self.borrow(), |n| match n { + NodeData::Element(e) => &**e, + _ => panic!("tried to borrow_element for a non-element node"), + }) + } + + fn borrow_element_mut(&mut self) -> RefMut<'_, Element> { + RefMut::map(self.borrow_mut(), |n| match &mut *n { + NodeData::Element(e) => &mut **e, + _ => panic!("tried to borrow_element_mut for a non-element node"), + }) + } + + fn borrow_element_data(&self) -> Ref<'_, ElementData> { + Ref::map(self.borrow(), |n| match n { + NodeData::Element(e) => &e.element_data, + _ => panic!("tried to borrow_element_data for a non-element node"), + }) + } +} + +#[macro_export] +macro_rules! is_element_of_type { + ($node:expr, $element_type:ident) => { + matches!( + $node.borrow_element().element_data, + $crate::element::ElementData::$element_type(_) + ) + }; +} + +#[macro_export] +macro_rules! borrow_element_as { + ($node:expr, $element_type:ident) => { + std::cell::Ref::map($node.borrow_element_data(), |d| match d { + $crate::element::ElementData::$element_type(ref e) => &*e, + _ => panic!("tried to borrow_element_as {}", stringify!($element_type)), + }) + }; +} + +/// Helper trait for cascading recursively +pub trait NodeCascade { + fn cascade(&mut self, values: &ComputedValues); +} + +impl NodeCascade for Node { + fn cascade(&mut self, values: &ComputedValues) { + let mut values = values.clone(); + + { + let mut elt = self.borrow_element_mut(); + + elt.get_specified_values().to_computed_values(&mut values); + elt.set_computed_values(&values); + } + + for mut child in self.children().filter(|c| c.is_element()) { + child.cascade(&values); + } + } +} + +/// Helper trait for drawing recursively. +/// +/// This is a trait because [`Node`] is a type alias over [`rctree::Node`], not a concrete type. +pub trait NodeDraw { + fn draw( + &self, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, + ) -> Result<BoundingBox, RenderingError>; + + fn draw_children( + &self, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, + ) -> Result<BoundingBox, RenderingError>; +} + +impl NodeDraw for Node { + fn draw( + &self, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + match *self.borrow() { + NodeData::Element(ref e) => { + match e.draw(self, acquired_nodes, cascaded, viewport, draw_ctx, clipping) { + Ok(bbox) => Ok(bbox), + + // https://www.w3.org/TR/css-transforms-1/#transform-function-lists + // + // "If a transform function causes the current transformation matrix of an + // object to be non-invertible, the object and its content do not get + // displayed." + Err(RenderingError::InvalidTransform) => Ok(draw_ctx.empty_bbox()), + + Err(e) => Err(e), + } + } + + _ => Ok(draw_ctx.empty_bbox()), + } + } + + fn draw_children( + &self, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + let mut bbox = draw_ctx.empty_bbox(); + + for child in self.children().filter(|c| c.is_element()) { + let child_bbox = draw_ctx.draw_node_from_stack( + &child, + acquired_nodes, + &CascadedValues::clone_with_node(cascaded, &child), + viewport, + clipping, + )?; + bbox.insert(&child_bbox); + } + + Ok(bbox) + } +} diff --git a/rsvg/src/paint_server.rs b/rsvg/src/paint_server.rs new file mode 100644 index 00000000..01393b8d --- /dev/null +++ b/rsvg/src/paint_server.rs @@ -0,0 +1,413 @@ +//! SVG paint servers. + +use std::sync::Arc; + +use cssparser::Parser; + +use crate::document::{AcquiredNodes, NodeId}; +use crate::drawing_ctx::Viewport; +use crate::element::ElementData; +use crate::error::{AcquireError, NodeIdError, ParseError, ValueErrorKind}; +use crate::gradient::{ResolvedGradient, UserSpaceGradient}; +use crate::length::NormalizeValues; +use crate::node::NodeBorrow; +use crate::parsers::Parse; +use crate::pattern::{ResolvedPattern, UserSpacePattern}; +use crate::rect::Rect; +use crate::session::Session; +use crate::unit_interval::UnitInterval; +use crate::util; + +/// Unresolved SVG paint server straight from the DOM data. +/// +/// This is either a solid color (which if `currentColor` needs to be extracted from the +/// `ComputedValues`), or a paint server like a gradient or pattern which is referenced by +/// a URL that points to a certain document node. +/// +/// Use [`PaintServer.resolve`](#method.resolve) to turn this into a [`PaintSource`]. +#[derive(Debug, Clone, PartialEq)] +pub enum PaintServer { + /// For example, `fill="none"`. + None, + + /// For example, `fill="url(#some_gradient) fallback_color"`. + Iri { + iri: Box<NodeId>, + alternate: Option<cssparser::Color>, + }, + + /// For example, `fill="blue"`. + SolidColor(cssparser::Color), + + /// For example, `fill="context-fill"` + ContextFill, + + /// For example, `fill="context-stroke"` + ContextStroke, +} + +/// Paint server with resolved references, with unnormalized lengths. +/// +/// Use [`PaintSource.to_user_space`](#method.to_user_space) to turn this into a +/// [`UserSpacePaintSource`]. +pub enum PaintSource { + None, + Gradient(ResolvedGradient, Option<cssparser::RGBA>), + Pattern(ResolvedPattern, Option<cssparser::RGBA>), + SolidColor(cssparser::RGBA), +} + +/// Fully resolved paint server, in user-space units. +/// +/// This has everything required for rendering. +pub enum UserSpacePaintSource { + None, + Gradient(UserSpaceGradient, Option<cssparser::RGBA>), + Pattern(UserSpacePattern, Option<cssparser::RGBA>), + SolidColor(cssparser::RGBA), +} + +impl Parse for PaintServer { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<PaintServer, ParseError<'i>> { + if parser + .try_parse(|i| i.expect_ident_matching("none")) + .is_ok() + { + Ok(PaintServer::None) + } else if parser + .try_parse(|i| i.expect_ident_matching("context-fill")) + .is_ok() + { + Ok(PaintServer::ContextFill) + } else if parser + .try_parse(|i| i.expect_ident_matching("context-stroke")) + .is_ok() + { + Ok(PaintServer::ContextStroke) + } else if let Ok(url) = parser.try_parse(|i| i.expect_url()) { + let loc = parser.current_source_location(); + + let alternate = if !parser.is_exhausted() { + if parser + .try_parse(|i| i.expect_ident_matching("none")) + .is_ok() + { + None + } else { + Some(parser.try_parse(cssparser::Color::parse)?) + } + } else { + None + }; + + Ok(PaintServer::Iri { + iri: Box::new( + NodeId::parse(&url) + .map_err(|e: NodeIdError| -> ValueErrorKind { e.into() }) + .map_err(|e| loc.new_custom_error(e))?, + ), + alternate, + }) + } else { + Ok(cssparser::Color::parse(parser).map(PaintServer::SolidColor)?) + } + } +} + +impl PaintServer { + /// Resolves colors, plus node references for gradients and patterns. + /// + /// `opacity` depends on `strokeOpacity` or `fillOpacity` depending on whether + /// the paint server is for the `stroke` or `fill` properties. + /// + /// `current_color` should be the value of `ComputedValues.color()`. + /// + /// After a paint server is resolved, the resulting [`PaintSource`] can be used in + /// many places: for an actual shape, or for the `context-fill` of a marker for that + /// shape. Therefore, this returns an [`Arc`] so that the `PaintSource` may be shared + /// easily. + pub fn resolve( + &self, + acquired_nodes: &mut AcquiredNodes<'_>, + opacity: UnitInterval, + current_color: cssparser::RGBA, + context_fill: Option<Arc<PaintSource>>, + context_stroke: Option<Arc<PaintSource>>, + session: &Session, + ) -> Arc<PaintSource> { + match self { + PaintServer::Iri { + ref iri, + ref alternate, + } => acquired_nodes + .acquire(iri) + .and_then(|acquired| { + let node = acquired.get(); + assert!(node.is_element()); + + match *node.borrow_element_data() { + ElementData::LinearGradient(ref g) => { + g.resolve(node, acquired_nodes, opacity).map(|g| { + Arc::new(PaintSource::Gradient( + g, + alternate.map(|c| resolve_color(&c, opacity, current_color)), + )) + }) + } + ElementData::Pattern(ref p) => { + p.resolve(node, acquired_nodes, opacity, session).map(|p| { + Arc::new(PaintSource::Pattern( + p, + alternate.map(|c| resolve_color(&c, opacity, current_color)), + )) + }) + } + ElementData::RadialGradient(ref g) => { + g.resolve(node, acquired_nodes, opacity).map(|g| { + Arc::new(PaintSource::Gradient( + g, + alternate.map(|c| resolve_color(&c, opacity, current_color)), + )) + }) + } + _ => Err(AcquireError::InvalidLinkType(iri.as_ref().clone())), + } + }) + .unwrap_or_else(|_| match alternate { + // The following cases catch AcquireError::CircularReference and + // AcquireError::MaxReferencesExceeded. + // + // Circular references mean that there is a pattern or gradient with a + // reference cycle in its "href" attribute. This is an invalid paint + // server, and per + // https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint we should + // try to fall back to the alternate color. + // + // Exceeding the maximum number of references will get caught again + // later in the drawing code, so it should be fine to translate this + // condition to that for an invalid paint server. + Some(color) => { + rsvg_log!( + session, + "could not resolve paint server \"{}\", using alternate color", + iri + ); + + Arc::new(PaintSource::SolidColor(resolve_color( + color, + opacity, + current_color, + ))) + } + + None => { + rsvg_log!( + session, + "could not resolve paint server \"{}\", no alternate color specified", + iri + ); + + Arc::new(PaintSource::None) + } + }), + + PaintServer::SolidColor(color) => Arc::new(PaintSource::SolidColor(resolve_color( + color, + opacity, + current_color, + ))), + + PaintServer::ContextFill => { + if let Some(paint) = context_fill { + paint + } else { + Arc::new(PaintSource::None) + } + } + + PaintServer::ContextStroke => { + if let Some(paint) = context_stroke { + paint + } else { + Arc::new(PaintSource::None) + } + } + + PaintServer::None => Arc::new(PaintSource::None), + } + } +} + +impl PaintSource { + /// Converts lengths to user-space. + pub fn to_user_space( + &self, + object_bbox: &Option<Rect>, + viewport: &Viewport, + values: &NormalizeValues, + ) -> UserSpacePaintSource { + match *self { + PaintSource::None => UserSpacePaintSource::None, + PaintSource::SolidColor(c) => UserSpacePaintSource::SolidColor(c), + + PaintSource::Gradient(ref g, c) => { + match (g.to_user_space(object_bbox, viewport, values), c) { + (Some(gradient), c) => UserSpacePaintSource::Gradient(gradient, c), + (None, Some(c)) => UserSpacePaintSource::SolidColor(c), + (None, None) => UserSpacePaintSource::None, + } + } + + PaintSource::Pattern(ref p, c) => { + match (p.to_user_space(object_bbox, viewport, values), c) { + (Some(pattern), c) => UserSpacePaintSource::Pattern(pattern, c), + (None, Some(c)) => UserSpacePaintSource::SolidColor(c), + (None, None) => UserSpacePaintSource::None, + } + } + } + } +} + +/// Resolves a CSS color into an RGBA value. +/// +/// A CSS color can be `currentColor`, in which case the computed value comes from +/// the `color` property. You should pass the `color` property's value for `current_color`. +pub fn resolve_color( + color: &cssparser::Color, + opacity: UnitInterval, + current_color: cssparser::RGBA, +) -> cssparser::RGBA { + let rgba = match *color { + cssparser::Color::RGBA(rgba) => rgba, + cssparser::Color::CurrentColor => current_color, + }; + + let UnitInterval(o) = opacity; + + let alpha = (f64::from(rgba.alpha) * o).round(); + let alpha = util::clamp(alpha, 0.0, 255.0); + let alpha = cast::u8(alpha).unwrap(); + + cssparser::RGBA { alpha, ..rgba } +} + +impl std::fmt::Debug for PaintSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + match *self { + PaintSource::None => f.write_str("PaintSource::None"), + PaintSource::Gradient(_, _) => f.write_str("PaintSource::Gradient"), + PaintSource::Pattern(_, _) => f.write_str("PaintSource::Pattern"), + PaintSource::SolidColor(_) => f.write_str("PaintSource::SolidColor"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn catches_invalid_syntax() { + assert!(PaintServer::parse_str("").is_err()); + assert!(PaintServer::parse_str("42").is_err()); + assert!(PaintServer::parse_str("invalid").is_err()); + } + + #[test] + fn parses_none() { + assert_eq!(PaintServer::parse_str("none").unwrap(), PaintServer::None); + } + + #[test] + fn parses_solid_color() { + assert_eq!( + PaintServer::parse_str("rgb(255, 128, 64, 0.5)").unwrap(), + PaintServer::SolidColor(cssparser::Color::RGBA(cssparser::RGBA::new( + 255, 128, 64, 128 + ))) + ); + + assert_eq!( + PaintServer::parse_str("currentColor").unwrap(), + PaintServer::SolidColor(cssparser::Color::CurrentColor) + ); + } + + #[test] + fn parses_iri() { + assert_eq!( + PaintServer::parse_str("url(#link)").unwrap(), + PaintServer::Iri { + iri: Box::new(NodeId::Internal("link".to_string())), + alternate: None, + } + ); + + assert_eq!( + PaintServer::parse_str("url(foo#link) none").unwrap(), + PaintServer::Iri { + iri: Box::new(NodeId::External("foo".to_string(), "link".to_string())), + alternate: None, + } + ); + + assert_eq!( + PaintServer::parse_str("url(#link) #ff8040").unwrap(), + PaintServer::Iri { + iri: Box::new(NodeId::Internal("link".to_string())), + alternate: Some(cssparser::Color::RGBA(cssparser::RGBA::new( + 255, 128, 64, 255 + ))), + } + ); + + assert_eq!( + PaintServer::parse_str("url(#link) rgb(255, 128, 64, 0.5)").unwrap(), + PaintServer::Iri { + iri: Box::new(NodeId::Internal("link".to_string())), + alternate: Some(cssparser::Color::RGBA(cssparser::RGBA::new( + 255, 128, 64, 128 + ))), + } + ); + + assert_eq!( + PaintServer::parse_str("url(#link) currentColor").unwrap(), + PaintServer::Iri { + iri: Box::new(NodeId::Internal("link".to_string())), + alternate: Some(cssparser::Color::CurrentColor), + } + ); + + assert!(PaintServer::parse_str("url(#link) invalid").is_err()); + } + + #[test] + fn resolves_explicit_color() { + use cssparser::{Color, RGBA}; + + assert_eq!( + resolve_color( + &Color::RGBA(RGBA::new(255, 0, 0, 128)), + UnitInterval::clamp(0.5), + RGBA::new(0, 255, 0, 255) + ), + RGBA::new(255, 0, 0, 64), + ); + } + + #[test] + fn resolves_current_color() { + use cssparser::{Color, RGBA}; + + assert_eq!( + resolve_color( + &Color::CurrentColor, + UnitInterval::clamp(0.5), + RGBA::new(0, 255, 0, 128) + ), + RGBA::new(0, 255, 0, 64), + ); + } +} diff --git a/rsvg/src/parsers.rs b/rsvg/src/parsers.rs new file mode 100644 index 00000000..7dd3e441 --- /dev/null +++ b/rsvg/src/parsers.rs @@ -0,0 +1,424 @@ +//! The `Parse` trait for CSS properties, and utilities for parsers. + +use cssparser::{Parser, ParserInput, Token}; +use markup5ever::QualName; +use std::str; + +use crate::error::*; + +/// Trait to parse values using `cssparser::Parser`. +pub trait Parse: Sized { + /// Parses a value out of the `parser`. + /// + /// All value types should implement this for composability. + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>>; + + /// Convenience function to parse a value out of a `&str`. + /// + /// This is useful mostly for tests which want to avoid creating a + /// `cssparser::Parser` by hand. Property types do not need to reimplement this. + fn parse_str(s: &str) -> Result<Self, ParseError<'_>> { + let mut input = ParserInput::new(s); + let mut parser = Parser::new(&mut input); + + let res = Self::parse(&mut parser)?; + parser.expect_exhausted()?; + + Ok(res) + } +} + +/// Consumes a comma if it exists, or does nothing. +pub fn optional_comma(parser: &mut Parser<'_, '_>) { + let _ = parser.try_parse(|p| p.expect_comma()); +} + +/// Parses an `f32` and ensures that it is not an infinity or NaN. +pub fn finite_f32(n: f32) -> Result<f32, ValueErrorKind> { + if n.is_finite() { + Ok(n) + } else { + Err(ValueErrorKind::Value("expected finite number".to_string())) + } +} + +pub trait ParseValue<T: Parse> { + /// Parses a `value` string into a type `T`. + fn parse(&self, value: &str) -> Result<T, ElementError>; +} + +impl<T: Parse> ParseValue<T> for QualName { + fn parse(&self, value: &str) -> Result<T, ElementError> { + let mut input = ParserInput::new(value); + let mut parser = Parser::new(&mut input); + + T::parse(&mut parser).attribute(self.clone()) + } +} + +impl<T: Parse> Parse for Option<T> { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + T::parse(parser).map(Some) + } +} + +impl Parse for f64 { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + let loc = parser.current_source_location(); + let n = parser.expect_number()?; + if n.is_finite() { + Ok(f64::from(n)) + } else { + Err(loc.new_custom_error(ValueErrorKind::value_error("expected finite number"))) + } + } +} + +/// Non-Negative number +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct NonNegative(pub f64); + +impl Parse for NonNegative { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + let loc = parser.current_source_location(); + let n = Parse::parse(parser)?; + if n >= 0.0 { + Ok(NonNegative(n)) + } else { + Err(loc.new_custom_error(ValueErrorKind::value_error("expected non negative number"))) + } + } +} + +/// CSS number-optional-number +/// +/// SVG1.1: <https://www.w3.org/TR/SVG11/types.html#DataTypeNumberOptionalNumber> +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct NumberOptionalNumber<T: Parse>(pub T, pub T); + +impl<T: Parse + Copy> Parse for NumberOptionalNumber<T> { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + let x = Parse::parse(parser)?; + + if !parser.is_exhausted() { + optional_comma(parser); + let y = Parse::parse(parser)?; + Ok(NumberOptionalNumber(x, y)) + } else { + Ok(NumberOptionalNumber(x, x)) + } + } +} + +/// CSS number-percentage +/// +/// CSS Values and Units 3: <https://www.w3.org/TR/css3-values/#typedef-number-percentage> +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct NumberOrPercentage { + pub value: f64, +} + +impl Parse for NumberOrPercentage { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + let loc = parser.current_source_location(); + + let value = match parser.next()? { + Token::Number { value, .. } => Ok(*value), + Token::Percentage { unit_value, .. } => Ok(*unit_value), + tok => Err(loc.new_unexpected_token_error(tok.clone())), + }?; + + let v = finite_f32(value).map_err(|e| parser.new_custom_error(e))?; + Ok(NumberOrPercentage { + value: f64::from(v), + }) + } +} + +impl Parse for i32 { + /// CSS integer + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/types.html#DataTypeInteger> + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + Ok(parser.expect_integer()?) + } +} + +impl Parse for u32 { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + let loc = parser.current_source_location(); + let n = parser.expect_integer()?; + if n >= 0 { + Ok(n as u32) + } else { + Err(loc.new_custom_error(ValueErrorKind::value_error("expected unsigned number"))) + } + } +} + +/// Number lists with bounds for the required and maximum number of items. +#[derive(Clone, Debug, PartialEq)] +pub struct NumberList<const REQUIRED: usize, const MAX: usize>(pub Vec<f64>); + +impl<const REQUIRED: usize, const MAX: usize> Parse for NumberList<REQUIRED, MAX> { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + let loc = parser.current_source_location(); + let mut v = Vec::<f64>::with_capacity(MAX); + for i in 0..MAX { + if i != 0 { + optional_comma(parser); + } + + v.push(f64::parse(parser)?); + + if parser.is_exhausted() { + break; + } + } + + if REQUIRED > 0 && v.len() < REQUIRED { + Err(loc.new_custom_error(ValueErrorKind::value_error("expected more numbers"))) + } else { + Ok(NumberList(v)) + } + } +} + +/// Parses a list of identifiers from a `cssparser::Parser` +/// +/// # Example +/// +/// ``` +/// # use cssparser::{ParserInput, Parser}; +/// # use rsvg::parse_identifiers; +/// # fn main() -> Result<(), cssparser::BasicParseError<'static>> { +/// # let mut input = ParserInput::new("true"); +/// # let mut parser = Parser::new(&mut input); +/// let my_boolean = parse_identifiers!( +/// parser, +/// "true" => true, +/// "false" => false, +/// )?; +/// # Ok(()) +/// # } +/// ``` +#[macro_export] +macro_rules! parse_identifiers { + ($parser:expr, + $($str:expr => $val:expr,)+) => { + { + let loc = $parser.current_source_location(); + let token = $parser.next()?; + + match token { + $(cssparser::Token::Ident(ref cow) if cow.eq_ignore_ascii_case($str) => Ok($val),)+ + + _ => Err(loc.new_basic_unexpected_token_error(token.clone())) + } + } + }; +} + +/// CSS Custom identifier. +/// +/// CSS Values and Units 4: <https://www.w3.org/TR/css-values-4/#custom-idents> +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CustomIdent(pub String); + +impl Parse for CustomIdent { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + let loc = parser.current_source_location(); + let token = parser.next()?; + + match token { + // CSS-wide keywords and "default" are errors here + // https://www.w3.org/TR/css-values-4/#css-wide-keywords + Token::Ident(ref cow) => { + for s in &["initial", "inherit", "unset", "default"] { + if cow.eq_ignore_ascii_case(s) { + return Err(loc.new_basic_unexpected_token_error(token.clone()).into()); + } + } + + Ok(CustomIdent(cow.as_ref().to_string())) + } + + _ => Err(loc.new_basic_unexpected_token_error(token.clone()).into()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_number_optional_number() { + assert_eq!( + NumberOptionalNumber::parse_str("1, 2").unwrap(), + NumberOptionalNumber(1.0, 2.0) + ); + assert_eq!( + NumberOptionalNumber::parse_str("1 2").unwrap(), + NumberOptionalNumber(1.0, 2.0) + ); + assert_eq!( + NumberOptionalNumber::parse_str("1").unwrap(), + NumberOptionalNumber(1.0, 1.0) + ); + + assert_eq!( + NumberOptionalNumber::parse_str("-1, -2").unwrap(), + NumberOptionalNumber(-1.0, -2.0) + ); + assert_eq!( + NumberOptionalNumber::parse_str("-1 -2").unwrap(), + NumberOptionalNumber(-1.0, -2.0) + ); + assert_eq!( + NumberOptionalNumber::parse_str("-1").unwrap(), + NumberOptionalNumber(-1.0, -1.0) + ); + } + + #[test] + fn invalid_number_optional_number() { + assert!(NumberOptionalNumber::<f64>::parse_str("").is_err()); + assert!(NumberOptionalNumber::<f64>::parse_str("1x").is_err()); + assert!(NumberOptionalNumber::<f64>::parse_str("x1").is_err()); + assert!(NumberOptionalNumber::<f64>::parse_str("1 x").is_err()); + assert!(NumberOptionalNumber::<f64>::parse_str("1 , x").is_err()); + assert!(NumberOptionalNumber::<f64>::parse_str("1 , 2x").is_err()); + assert!(NumberOptionalNumber::<f64>::parse_str("1 2 x").is_err()); + } + + #[test] + fn parses_integer() { + assert_eq!(i32::parse_str("0").unwrap(), 0); + assert_eq!(i32::parse_str("1").unwrap(), 1); + assert_eq!(i32::parse_str("-1").unwrap(), -1); + + assert_eq!(u32::parse_str("0").unwrap(), 0); + assert_eq!(u32::parse_str("1").unwrap(), 1); + } + + #[test] + fn invalid_integer() { + assert!(i32::parse_str("").is_err()); + assert!(i32::parse_str("1x").is_err()); + assert!(i32::parse_str("1.5").is_err()); + + assert!(u32::parse_str("").is_err()); + assert!(u32::parse_str("1x").is_err()); + assert!(u32::parse_str("1.5").is_err()); + assert!(u32::parse_str("-1").is_err()); + } + + #[test] + fn parses_integer_optional_integer() { + assert_eq!( + NumberOptionalNumber::parse_str("1, 2").unwrap(), + NumberOptionalNumber(1, 2) + ); + assert_eq!( + NumberOptionalNumber::parse_str("1 2").unwrap(), + NumberOptionalNumber(1, 2) + ); + assert_eq!( + NumberOptionalNumber::parse_str("1").unwrap(), + NumberOptionalNumber(1, 1) + ); + + assert_eq!( + NumberOptionalNumber::parse_str("-1, -2").unwrap(), + NumberOptionalNumber(-1, -2) + ); + assert_eq!( + NumberOptionalNumber::parse_str("-1 -2").unwrap(), + NumberOptionalNumber(-1, -2) + ); + assert_eq!( + NumberOptionalNumber::parse_str("-1").unwrap(), + NumberOptionalNumber(-1, -1) + ); + } + + #[test] + fn invalid_integer_optional_integer() { + assert!(NumberOptionalNumber::<i32>::parse_str("").is_err()); + assert!(NumberOptionalNumber::<i32>::parse_str("1x").is_err()); + assert!(NumberOptionalNumber::<i32>::parse_str("x1").is_err()); + assert!(NumberOptionalNumber::<i32>::parse_str("1 x").is_err()); + assert!(NumberOptionalNumber::<i32>::parse_str("1 , x").is_err()); + assert!(NumberOptionalNumber::<i32>::parse_str("1 , 2x").is_err()); + assert!(NumberOptionalNumber::<i32>::parse_str("1 2 x").is_err()); + assert!(NumberOptionalNumber::<i32>::parse_str("1.5").is_err()); + assert!(NumberOptionalNumber::<i32>::parse_str("1 2.5").is_err()); + assert!(NumberOptionalNumber::<i32>::parse_str("1, 2.5").is_err()); + } + + #[test] + fn parses_number_list() { + assert_eq!( + NumberList::<1, 1>::parse_str("5").unwrap(), + NumberList(vec![5.0]) + ); + + assert_eq!( + NumberList::<4, 4>::parse_str("1 2 3 4").unwrap(), + NumberList(vec![1.0, 2.0, 3.0, 4.0]) + ); + + assert_eq!( + NumberList::<0, 5>::parse_str("1 2 3 4 5").unwrap(), + NumberList(vec![1.0, 2.0, 3.0, 4.0, 5.0]) + ); + + assert_eq!( + NumberList::<0, 5>::parse_str("1 2 3").unwrap(), + NumberList(vec![1.0, 2.0, 3.0]) + ); + } + + #[test] + fn errors_on_invalid_number_list() { + // empty + assert!(NumberList::<1, 1>::parse_str("").is_err()); + assert!(NumberList::<0, 1>::parse_str("").is_err()); + + // garbage + assert!(NumberList::<1, 1>::parse_str("foo").is_err()); + assert!(NumberList::<2, 2>::parse_str("1foo").is_err()); + assert!(NumberList::<2, 2>::parse_str("1 foo").is_err()); + assert!(NumberList::<2, 2>::parse_str("1 foo 2").is_err()); + assert!(NumberList::<2, 2>::parse_str("1,foo").is_err()); + + // too many + assert!(NumberList::<1, 1>::parse_str("1 2").is_err()); + + // extra token + assert!(NumberList::<1, 1>::parse_str("1,").is_err()); + assert!(NumberList::<0, 1>::parse_str("1,").is_err()); + + // too few + assert!(NumberList::<2, 2>::parse_str("1").is_err()); + assert!(NumberList::<3, 3>::parse_str("1 2").is_err()); + } + + #[test] + fn parses_custom_ident() { + assert_eq!( + CustomIdent::parse_str("hello").unwrap(), + CustomIdent("hello".to_string()) + ); + } + + #[test] + fn invalid_custom_ident_yields_error() { + assert!(CustomIdent::parse_str("initial").is_err()); + assert!(CustomIdent::parse_str("inherit").is_err()); + assert!(CustomIdent::parse_str("unset").is_err()); + assert!(CustomIdent::parse_str("default").is_err()); + assert!(CustomIdent::parse_str("").is_err()); + } +} diff --git a/rsvg/src/path_builder.rs b/rsvg/src/path_builder.rs new file mode 100644 index 00000000..1e6b5bd8 --- /dev/null +++ b/rsvg/src/path_builder.rs @@ -0,0 +1,875 @@ +//! Representation of Bézier paths. +//! +//! Path data can consume a significant amount of memory in complex SVG documents. This +//! module deals with this as follows: +//! +//! * The path parser pushes commands into a [`PathBuilder`]. This is a mutable, +//! temporary storage for path data. +//! +//! * Then, the [`PathBuilder`] gets turned into a long-term, immutable [`Path`] that has +//! a more compact representation. +//! +//! The code tries to reduce work in the allocator, by using a [`TinyVec`] with space for at +//! least 32 commands on the stack for `PathBuilder`; most paths in SVGs in the wild have +//! fewer than 32 commands, and larger ones will spill to the heap. +//! +//! See these blog posts for details and profiles: +//! +//! * [Compact representation for path data](https://people.gnome.org/~federico/blog/reducing-memory-consumption-in-librsvg-4.html) +//! * [Reducing slack space and allocator work](https://people.gnome.org/~federico/blog/reducing-memory-consumption-in-librsvg-3.html) + +use tinyvec::TinyVec; + +use std::f64; +use std::f64::consts::*; +use std::slice; + +use crate::float_eq_cairo::ApproxEqCairo; +use crate::path_parser::{ParseError, PathParser}; +use crate::util::clamp; + +/// Whether an arc's sweep should be >= 180 degrees, or smaller. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct LargeArc(pub bool); + +/// Angular direction in which an arc is drawn. +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum Sweep { + Negative, + Positive, +} + +/// "c" command for paths; describes a cubic Bézier segment. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct CubicBezierCurve { + /// The (x, y) coordinates of the first control point. + pub pt1: (f64, f64), + /// The (x, y) coordinates of the second control point. + pub pt2: (f64, f64), + /// The (x, y) coordinates of the end point of this path segment. + pub to: (f64, f64), +} + +impl CubicBezierCurve { + /// Consumes 6 coordinates and creates a curve segment. + fn from_coords(coords: &mut slice::Iter<'_, f64>) -> CubicBezierCurve { + let pt1 = take_two(coords); + let pt2 = take_two(coords); + let to = take_two(coords); + + CubicBezierCurve { pt1, pt2, to } + } + + /// Pushes 6 coordinates to `coords` and returns `PackedCommand::CurveTo`. + fn to_packed_and_coords(&self, coords: &mut Vec<f64>) -> PackedCommand { + coords.push(self.pt1.0); + coords.push(self.pt1.1); + coords.push(self.pt2.0); + coords.push(self.pt2.1); + coords.push(self.to.0); + coords.push(self.to.1); + PackedCommand::CurveTo + } +} + +/// Conversion from endpoint parameterization to center parameterization. +/// +/// SVG path data specifies elliptical arcs in terms of their endpoints, but +/// they are easier to process if they are converted to a center parameterization. +/// +/// When attempting to compute the center parameterization of the arc, +/// out of range parameters may see an arc omitted or treated as a line. +pub enum ArcParameterization { + /// Center parameterization of the arc. + CenterParameters { + /// Center of the ellipse. + center: (f64, f64), + /// Radii of the ellipse (corrected). + radii: (f64, f64), + /// Angle of the start point. + theta1: f64, + /// Delta angle to the end point. + delta_theta: f64, + }, + /// Treat the arc as a line to the end point. + LineTo, + /// Omit the arc. + Omit, +} + +/// "a" command for paths; describes an elliptical arc in terms of its endpoints. +#[derive(Debug, Clone, PartialEq)] +pub struct EllipticalArc { + /// The (x-axis, y-axis) radii for the ellipse. + pub r: (f64, f64), + /// The rotation angle in degrees for the ellipse's x-axis + /// relative to the x-axis of the user coordinate system. + pub x_axis_rotation: f64, + /// Flag indicating whether the arc sweep should be + /// greater than or equal to 180 degrees, or smaller than 180 degrees. + pub large_arc: LargeArc, + /// Flag indicating the angular direction in which the arc is drawn. + pub sweep: Sweep, + /// The (x, y) coordinates for the start point of this path segment. + pub from: (f64, f64), + /// The (x, y) coordinates for the end point of this path segment. + pub to: (f64, f64), +} + +impl EllipticalArc { + /// Calculates a center parameterization from the endpoint parameterization. + /// + /// Radii may be adjusted if there is no solution. + /// + /// See section [B.2.4. Conversion from endpoint to center + /// parameterization](https://www.w3.org/TR/SVG2/implnote.html#ArcConversionEndpointToCenter) + pub(crate) fn center_parameterization(&self) -> ArcParameterization { + let Self { + r: (mut rx, mut ry), + x_axis_rotation, + large_arc, + sweep, + from: (x1, y1), + to: (x2, y2), + } = *self; + + // Ensure radii are non-zero. + // Otherwise this arc is treated as a line segment joining the end points. + // + // A bit further down we divide by the square of the radii. + // Check that we won't divide by zero. + // See http://bugs.debian.org/508443 + if rx * rx < f64::EPSILON || ry * ry < f64::EPSILON { + return ArcParameterization::LineTo; + } + + let is_large_arc = large_arc.0; + let is_positive_sweep = sweep == Sweep::Positive; + + let phi = x_axis_rotation * PI / 180.0; + let (sin_phi, cos_phi) = phi.sin_cos(); + + // Ensure radii are positive. + rx = rx.abs(); + ry = ry.abs(); + + // The equations simplify after a translation which places + // the origin at the midpoint of the line joining (x1, y1) to (x2, y2), + // followed by a rotation to line up the coordinate axes + // with the axes of the ellipse. + // All transformed coordinates will be written with primes. + // + // Compute (x1', y1'). + let mid_x = (x1 - x2) / 2.0; + let mid_y = (y1 - y2) / 2.0; + let x1_ = cos_phi * mid_x + sin_phi * mid_y; + let y1_ = -sin_phi * mid_x + cos_phi * mid_y; + + // Ensure radii are large enough. + let lambda = (x1_ / rx).powi(2) + (y1_ / ry).powi(2); + if lambda > 1.0 { + // If not, scale up the ellipse uniformly + // until there is exactly one solution. + rx *= lambda.sqrt(); + ry *= lambda.sqrt(); + } + + // Compute the transformed center (cx', cy'). + let d = (rx * y1_).powi(2) + (ry * x1_).powi(2); + if d == 0.0 { + return ArcParameterization::Omit; + } + let k = { + let mut k = ((rx * ry).powi(2) / d - 1.0).abs().sqrt(); + if is_positive_sweep == is_large_arc { + k = -k; + } + k + }; + let cx_ = k * rx * y1_ / ry; + let cy_ = -k * ry * x1_ / rx; + + // Compute the center (cx, cy). + let cx = cos_phi * cx_ - sin_phi * cy_ + (x1 + x2) / 2.0; + let cy = sin_phi * cx_ + cos_phi * cy_ + (y1 + y2) / 2.0; + + // Compute the start angle θ1. + let ux = (x1_ - cx_) / rx; + let uy = (y1_ - cy_) / ry; + let u_len = (ux * ux + uy * uy).abs().sqrt(); + if u_len == 0.0 { + return ArcParameterization::Omit; + } + let cos_theta1 = clamp(ux / u_len, -1.0, 1.0); + let theta1 = { + let mut theta1 = cos_theta1.acos(); + if uy < 0.0 { + theta1 = -theta1; + } + theta1 + }; + + // Compute the total delta angle Δθ. + let vx = (-x1_ - cx_) / rx; + let vy = (-y1_ - cy_) / ry; + let v_len = (vx * vx + vy * vy).abs().sqrt(); + if v_len == 0.0 { + return ArcParameterization::Omit; + } + let dp_uv = ux * vx + uy * vy; + let cos_delta_theta = clamp(dp_uv / (u_len * v_len), -1.0, 1.0); + let delta_theta = { + let mut delta_theta = cos_delta_theta.acos(); + if ux * vy - uy * vx < 0.0 { + delta_theta = -delta_theta; + } + if is_positive_sweep && delta_theta < 0.0 { + delta_theta += PI * 2.0; + } else if !is_positive_sweep && delta_theta > 0.0 { + delta_theta -= PI * 2.0; + } + delta_theta + }; + + ArcParameterization::CenterParameters { + center: (cx, cy), + radii: (rx, ry), + theta1, + delta_theta, + } + } + + /// Consumes 7 coordinates and creates an arc segment. + fn from_coords( + large_arc: LargeArc, + sweep: Sweep, + coords: &mut slice::Iter<'_, f64>, + ) -> EllipticalArc { + let r = take_two(coords); + let x_axis_rotation = take_one(coords); + let from = take_two(coords); + let to = take_two(coords); + + EllipticalArc { + r, + x_axis_rotation, + large_arc, + sweep, + from, + to, + } + } + + /// Pushes 7 coordinates to `coords` and returns one of `PackedCommand::Arc*`. + fn to_packed_and_coords(&self, coords: &mut Vec<f64>) -> PackedCommand { + coords.push(self.r.0); + coords.push(self.r.1); + coords.push(self.x_axis_rotation); + coords.push(self.from.0); + coords.push(self.from.1); + coords.push(self.to.0); + coords.push(self.to.1); + + match (self.large_arc, self.sweep) { + (LargeArc(false), Sweep::Negative) => PackedCommand::ArcSmallNegative, + (LargeArc(false), Sweep::Positive) => PackedCommand::ArcSmallPositive, + (LargeArc(true), Sweep::Negative) => PackedCommand::ArcLargeNegative, + (LargeArc(true), Sweep::Positive) => PackedCommand::ArcLargePositive, + } + } +} + +/// Turns an arc segment into a cubic bezier curve. +/// +/// Takes the center, the radii and the x-axis rotation of the ellipse, +/// the angles of the start and end points, +/// and returns cubic bezier curve parameters. +pub(crate) fn arc_segment( + c: (f64, f64), + r: (f64, f64), + x_axis_rotation: f64, + th0: f64, + th1: f64, +) -> CubicBezierCurve { + let (cx, cy) = c; + let (rx, ry) = r; + let phi = x_axis_rotation * PI / 180.0; + let (sin_phi, cos_phi) = phi.sin_cos(); + let (sin_th0, cos_th0) = th0.sin_cos(); + let (sin_th1, cos_th1) = th1.sin_cos(); + + let th_half = 0.5 * (th1 - th0); + let t = (8.0 / 3.0) * (th_half * 0.5).sin().powi(2) / th_half.sin(); + let x1 = rx * (cos_th0 - t * sin_th0); + let y1 = ry * (sin_th0 + t * cos_th0); + let x3 = rx * cos_th1; + let y3 = ry * sin_th1; + let x2 = x3 + rx * (t * sin_th1); + let y2 = y3 + ry * (-t * cos_th1); + + CubicBezierCurve { + pt1: ( + cx + cos_phi * x1 - sin_phi * y1, + cy + sin_phi * x1 + cos_phi * y1, + ), + pt2: ( + cx + cos_phi * x2 - sin_phi * y2, + cy + sin_phi * x2 + cos_phi * y2, + ), + to: ( + cx + cos_phi * x3 - sin_phi * y3, + cy + sin_phi * x3 + cos_phi * y3, + ), + } +} + +/// Long-form version of a single path command. +/// +/// This is returned from iterators on paths and subpaths. +#[derive(Clone, Debug, PartialEq)] +pub enum PathCommand { + MoveTo(f64, f64), + LineTo(f64, f64), + CurveTo(CubicBezierCurve), + Arc(EllipticalArc), + ClosePath, +} + +// This is just so we can use TinyVec, whose type parameter requires T: Default. +// There is no actual default for path commands in the SVG spec; this is just our +// implementation detail. +enum_default!( + PathCommand, + PathCommand::CurveTo(CubicBezierCurve::default()) +); + +impl PathCommand { + /// Returns the number of coordinate values that this command will generate in a `Path`. + fn num_coordinates(&self) -> usize { + match *self { + PathCommand::MoveTo(..) => 2, + PathCommand::LineTo(..) => 2, + PathCommand::CurveTo(_) => 6, + PathCommand::Arc(_) => 7, + PathCommand::ClosePath => 0, + } + } + + /// Pushes a command's coordinates to `coords` and returns the corresponding `PackedCommand`. + fn to_packed(&self, coords: &mut Vec<f64>) -> PackedCommand { + match *self { + PathCommand::MoveTo(x, y) => { + coords.push(x); + coords.push(y); + PackedCommand::MoveTo + } + + PathCommand::LineTo(x, y) => { + coords.push(x); + coords.push(y); + PackedCommand::LineTo + } + + PathCommand::CurveTo(ref c) => c.to_packed_and_coords(coords), + + PathCommand::Arc(ref a) => a.to_packed_and_coords(coords), + + PathCommand::ClosePath => PackedCommand::ClosePath, + } + } + + /// Consumes a packed command's coordinates from the `coords` iterator and returns the rehydrated `PathCommand`. + fn from_packed(packed: PackedCommand, coords: &mut slice::Iter<'_, f64>) -> PathCommand { + match packed { + PackedCommand::MoveTo => { + let x = take_one(coords); + let y = take_one(coords); + PathCommand::MoveTo(x, y) + } + + PackedCommand::LineTo => { + let x = take_one(coords); + let y = take_one(coords); + PathCommand::LineTo(x, y) + } + + PackedCommand::CurveTo => PathCommand::CurveTo(CubicBezierCurve::from_coords(coords)), + + PackedCommand::ClosePath => PathCommand::ClosePath, + + PackedCommand::ArcSmallNegative => PathCommand::Arc(EllipticalArc::from_coords( + LargeArc(false), + Sweep::Negative, + coords, + )), + + PackedCommand::ArcSmallPositive => PathCommand::Arc(EllipticalArc::from_coords( + LargeArc(false), + Sweep::Positive, + coords, + )), + + PackedCommand::ArcLargeNegative => PathCommand::Arc(EllipticalArc::from_coords( + LargeArc(true), + Sweep::Negative, + coords, + )), + + PackedCommand::ArcLargePositive => PathCommand::Arc(EllipticalArc::from_coords( + LargeArc(true), + Sweep::Positive, + coords, + )), + } + } +} + +/// Constructs a path out of commands. +/// +/// Create this with `PathBuilder::default`; you can then add commands to it or call the +/// `parse` method. When you are finished constructing a path builder, turn it into a +/// `Path` with `into_path`. You can then iterate on that `Path`'s commands with its +/// methods. +#[derive(Default)] +pub struct PathBuilder { + path_commands: TinyVec<[PathCommand; 32]>, +} + +/// An immutable path with a compact representation. +/// +/// This is constructed from a `PathBuilder` once it is finished. You +/// can get an iterator for the path's commands with the `iter` +/// method, or an iterator for its subpaths (subsequences of commands that +/// start with a MoveTo) with the `iter_subpath` method. +/// +/// The variants in `PathCommand` have different sizes, so a simple array of `PathCommand` +/// would have a lot of slack space. We reduce this to a minimum by separating the +/// commands from their coordinates. Then, we can have two dense arrays: one with a compact +/// representation of commands, and another with a linear list of the coordinates for each +/// command. +/// +/// Both `PathCommand` and `PackedCommand` know how many coordinates they ought to +/// produce, with their `num_coordinates` methods. +/// +/// This struct implements `Default`, and it yields an empty path. +#[derive(Default)] +pub struct Path { + commands: Box<[PackedCommand]>, + coords: Box<[f64]>, +} + +/// Packed version of a `PathCommand`, used in `Path`. +/// +/// MoveTo/LineTo/CurveTo have only pairs of coordinates, while ClosePath has no coordinates, +/// and EllipticalArc has a bunch of coordinates plus two flags. Here we represent the flags +/// as four variants. +/// +/// This is `repr(u8)` to keep it as small as possible. +#[repr(u8)] +#[derive(Debug, Clone, Copy)] +enum PackedCommand { + MoveTo, + LineTo, + CurveTo, + ArcSmallNegative, + ArcSmallPositive, + ArcLargeNegative, + ArcLargePositive, + ClosePath, +} + +impl PackedCommand { + // Returns the number of coordinate values that this command will generate in a `Path`. + fn num_coordinates(&self) -> usize { + match *self { + PackedCommand::MoveTo => 2, + PackedCommand::LineTo => 2, + PackedCommand::CurveTo => 6, + PackedCommand::ArcSmallNegative + | PackedCommand::ArcSmallPositive + | PackedCommand::ArcLargeNegative + | PackedCommand::ArcLargePositive => 7, + PackedCommand::ClosePath => 0, + } + } +} + +impl PathBuilder { + pub fn parse(&mut self, path_str: &str) -> Result<(), ParseError> { + let mut parser = PathParser::new(self, path_str); + parser.parse() + } + + /// Consumes the `PathBuilder` and returns a compact, immutable representation as a `Path`. + pub fn into_path(self) -> Path { + let num_coords = self + .path_commands + .iter() + .map(PathCommand::num_coordinates) + .sum(); + + let mut coords = Vec::with_capacity(num_coords); + let packed_commands: Vec<_> = self + .path_commands + .iter() + .map(|cmd| cmd.to_packed(&mut coords)) + .collect(); + + Path { + commands: packed_commands.into_boxed_slice(), + coords: coords.into_boxed_slice(), + } + } + + /// Adds a MoveTo command to the path. + pub fn move_to(&mut self, x: f64, y: f64) { + self.path_commands.push(PathCommand::MoveTo(x, y)); + } + + /// Adds a LineTo command to the path. + pub fn line_to(&mut self, x: f64, y: f64) { + self.path_commands.push(PathCommand::LineTo(x, y)); + } + + /// Adds a CurveTo command to the path. + pub fn curve_to(&mut self, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) { + let curve = CubicBezierCurve { + pt1: (x2, y2), + pt2: (x3, y3), + to: (x4, y4), + }; + self.path_commands.push(PathCommand::CurveTo(curve)); + } + + /// Adds an EllipticalArc command to the path. + pub fn arc( + &mut self, + x1: f64, + y1: f64, + rx: f64, + ry: f64, + x_axis_rotation: f64, + large_arc: LargeArc, + sweep: Sweep, + x2: f64, + y2: f64, + ) { + let arc = EllipticalArc { + r: (rx, ry), + x_axis_rotation, + large_arc, + sweep, + from: (x1, y1), + to: (x2, y2), + }; + self.path_commands.push(PathCommand::Arc(arc)); + } + + /// Adds a ClosePath command to the path. + pub fn close_path(&mut self) { + self.path_commands.push(PathCommand::ClosePath); + } +} + +/// An iterator over the subpaths of a `Path`. +pub struct SubPathIter<'a> { + path: &'a Path, + commands_start: usize, + coords_start: usize, +} + +/// A slice of commands and coordinates with a single `MoveTo` at the beginning. +pub struct SubPath<'a> { + commands: &'a [PackedCommand], + coords: &'a [f64], +} + +/// An iterator over the commands/coordinates of a subpath. +pub struct SubPathCommandsIter<'a> { + commands_iter: slice::Iter<'a, PackedCommand>, + coords_iter: slice::Iter<'a, f64>, +} + +impl<'a> SubPath<'a> { + /// Returns an iterator over the subpath's commands. + pub fn iter_commands(&self) -> SubPathCommandsIter<'_> { + SubPathCommandsIter { + commands_iter: self.commands.iter(), + coords_iter: self.coords.iter(), + } + } + + /// Each subpath starts with a MoveTo; this returns its `(x, y)` coordinates. + pub fn origin(&self) -> (f64, f64) { + let first = *self.commands.first().unwrap(); + assert!(matches!(first, PackedCommand::MoveTo)); + let command = PathCommand::from_packed(first, &mut self.coords.iter()); + + match command { + PathCommand::MoveTo(x, y) => (x, y), + _ => unreachable!(), + } + } + + /// Returns whether the length of a subpath is approximately zero. + pub fn is_zero_length(&self) -> bool { + let (cur_x, cur_y) = self.origin(); + + for cmd in self.iter_commands().skip(1) { + let (end_x, end_y) = match cmd { + PathCommand::MoveTo(_, _) => unreachable!( + "A MoveTo cannot appear in a subpath if it's not the first element" + ), + PathCommand::LineTo(x, y) => (x, y), + PathCommand::CurveTo(curve) => curve.to, + PathCommand::Arc(arc) => arc.to, + // If we get a `ClosePath and haven't returned yet then we haven't moved at all making + // it an empty subpath` + PathCommand::ClosePath => return true, + }; + + if !end_x.approx_eq_cairo(cur_x) || !end_y.approx_eq_cairo(cur_y) { + return false; + } + } + + true + } +} + +impl<'a> Iterator for SubPathIter<'a> { + type Item = SubPath<'a>; + + fn next(&mut self) -> Option<Self::Item> { + // If we ended on our last command in the previous iteration, we're done here + if self.commands_start >= self.path.commands.len() { + return None; + } + + // Otherwise we have at least one command left, we setup the slice to be all the remaining + // commands. + let commands = &self.path.commands[self.commands_start..]; + + assert!(matches!(commands.first().unwrap(), PackedCommand::MoveTo)); + let mut num_coords = PackedCommand::MoveTo.num_coordinates(); + + // Skip over the initial MoveTo + for (i, cmd) in commands.iter().enumerate().skip(1) { + // If we encounter a MoveTo , we ended our current subpath, we + // return the commands until this command and set commands_start to be the index of the + // next command + if let PackedCommand::MoveTo = cmd { + let subpath_coords_start = self.coords_start; + + self.commands_start += i; + self.coords_start += num_coords; + + return Some(SubPath { + commands: &commands[..i], + coords: &self.path.coords + [subpath_coords_start..subpath_coords_start + num_coords], + }); + } else { + num_coords += cmd.num_coordinates(); + } + } + + // If we didn't find any MoveTo, we're done here. We return the rest of the path + // and set commands_start so next iteration will return None. + + self.commands_start = self.path.commands.len(); + + let subpath_coords_start = self.coords_start; + assert!(subpath_coords_start + num_coords == self.path.coords.len()); + self.coords_start = self.path.coords.len(); + + Some(SubPath { + commands, + coords: &self.path.coords[subpath_coords_start..], + }) + } +} + +impl<'a> Iterator for SubPathCommandsIter<'a> { + type Item = PathCommand; + + fn next(&mut self) -> Option<Self::Item> { + self.commands_iter + .next() + .map(|packed| PathCommand::from_packed(*packed, &mut self.coords_iter)) + } +} + +impl Path { + /// Get an iterator over a path `Subpath`s. + pub fn iter_subpath(&self) -> SubPathIter<'_> { + SubPathIter { + path: self, + commands_start: 0, + coords_start: 0, + } + } + + /// Get an iterator over a path's commands. + pub fn iter(&self) -> impl Iterator<Item = PathCommand> + '_ { + let commands = self.commands.iter(); + let mut coords = self.coords.iter(); + + commands.map(move |cmd| PathCommand::from_packed(*cmd, &mut coords)) + } + + /// Returns whether there are no commands in the path. + pub fn is_empty(&self) -> bool { + self.commands.is_empty() + } +} + +fn take_one(iter: &mut slice::Iter<'_, f64>) -> f64 { + *iter.next().unwrap() +} + +fn take_two(iter: &mut slice::Iter<'_, f64>) -> (f64, f64) { + (take_one(iter), take_one(iter)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_builder() { + let builder = PathBuilder::default(); + let path = builder.into_path(); + assert!(path.is_empty()); + assert_eq!(path.iter().count(), 0); + } + + #[test] + fn empty_path() { + let path = Path::default(); + assert!(path.is_empty()); + assert_eq!(path.iter().count(), 0); + } + + #[test] + fn all_commands() { + let mut builder = PathBuilder::default(); + builder.move_to(42.0, 43.0); + builder.line_to(42.0, 43.0); + builder.curve_to(42.0, 43.0, 44.0, 45.0, 46.0, 47.0); + builder.arc( + 42.0, + 43.0, + 44.0, + 45.0, + 46.0, + LargeArc(true), + Sweep::Positive, + 47.0, + 48.0, + ); + builder.close_path(); + let path = builder.into_path(); + assert!(path.iter().eq(vec![ + PathCommand::MoveTo(42.0, 43.0), + PathCommand::LineTo(42.0, 43.0), + PathCommand::CurveTo(CubicBezierCurve { + pt1: (42.0, 43.0), + pt2: (44.0, 45.0), + to: (46.0, 47.0), + }), + PathCommand::Arc(EllipticalArc { + from: (42.0, 43.0), + r: (44.0, 45.0), + to: (47.0, 48.0), + x_axis_rotation: 46.0, + large_arc: LargeArc(true), + sweep: Sweep::Positive, + }), + PathCommand::ClosePath, + ])); + } + + #[test] + fn subpath_iter() { + let mut builder = PathBuilder::default(); + builder.move_to(42.0, 43.0); + builder.line_to(42.0, 43.0); + builder.close_path(); + + builder.move_to(22.0, 22.0); + builder.curve_to(22.0, 22.0, 44.0, 45.0, 46.0, 47.0); + + builder.move_to(69.0, 69.0); + builder.line_to(42.0, 43.0); + let path = builder.into_path(); + + let subpaths = path + .iter_subpath() + .map(|subpath| { + ( + subpath.origin(), + subpath.iter_commands().collect::<Vec<PathCommand>>(), + ) + }) + .collect::<Vec<((f64, f64), Vec<PathCommand>)>>(); + + assert_eq!( + subpaths, + vec![ + ( + (42.0, 43.0), + vec![ + PathCommand::MoveTo(42.0, 43.0), + PathCommand::LineTo(42.0, 43.0), + PathCommand::ClosePath + ] + ), + ( + (22.0, 22.0), + vec![ + PathCommand::MoveTo(22.0, 22.0), + PathCommand::CurveTo(CubicBezierCurve { + pt1: (22.0, 22.0), + pt2: (44.0, 45.0), + to: (46.0, 47.0) + }) + ] + ), + ( + (69.0, 69.0), + vec![ + PathCommand::MoveTo(69.0, 69.0), + PathCommand::LineTo(42.0, 43.0) + ] + ) + ] + ); + } + + #[test] + fn zero_length_subpaths() { + let mut builder = PathBuilder::default(); + builder.move_to(42.0, 43.0); + builder.move_to(44.0, 45.0); + builder.close_path(); + builder.move_to(46.0, 47.0); + builder.line_to(48.0, 49.0); + + let path = builder.into_path(); + + let subpaths = path + .iter_subpath() + .map(|subpath| (subpath.is_zero_length(), subpath.origin())) + .collect::<Vec<(bool, (f64, f64))>>(); + + assert_eq!( + subpaths, + vec![ + (true, (42.0, 43.0)), + (true, (44.0, 45.0)), + (false, (46.0, 47.0)), + ] + ); + } +} diff --git a/rsvg/src/path_parser.rs b/rsvg/src/path_parser.rs new file mode 100644 index 00000000..9b6ae0c9 --- /dev/null +++ b/rsvg/src/path_parser.rs @@ -0,0 +1,2223 @@ +//! Parser for SVG path data. + +use std::fmt; +use std::iter::Enumerate; +use std::str; +use std::str::Bytes; + +use crate::path_builder::*; + +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum Token { + // pub to allow benchmarking + Number(f64), + Flag(bool), + Command(u8), + Comma, +} + +use crate::path_parser::Token::{Comma, Command, Flag, Number}; + +#[derive(Debug)] +pub struct Lexer<'a> { + // pub to allow benchmarking + input: &'a [u8], + ci: Enumerate<Bytes<'a>>, + current: Option<(usize, u8)>, + flags_required: u8, +} + +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum LexError { + // pub to allow benchmarking + ParseFloatError, + UnexpectedByte(u8), + UnexpectedEof, +} + +impl<'a> Lexer<'_> { + pub fn new(input: &'a str) -> Lexer<'a> { + let mut ci = input.bytes().enumerate(); + let current = ci.next(); + Lexer { + input: input.as_bytes(), + ci, + current, + flags_required: 0, + } + } + + // The way Flag tokens work is a little annoying. We don't have + // any way to distinguish between numbers and flags without context + // from the parser. The only time we need to return flags is within the + // argument sequence of an elliptical arc, and then we need 2 in a row + // or it's an error. So, when the parser gets to that point, it calls + // this method and we switch from our usual mode of handling digits as + // numbers to looking for two 'flag' characters (either 0 or 1) in a row + // (with optional intervening whitespace, and possibly comma tokens.) + // Every time we find a flag we decrement flags_required. + pub fn require_flags(&mut self) { + self.flags_required = 2; + } + + fn current_pos(&mut self) -> usize { + match self.current { + None => self.input.len(), + Some((pos, _)) => pos, + } + } + + fn advance(&mut self) { + self.current = self.ci.next(); + } + + fn advance_over_whitespace(&mut self) -> bool { + let mut found_some = false; + while self.current.is_some() && self.current.unwrap().1.is_ascii_whitespace() { + found_some = true; + self.current = self.ci.next(); + } + found_some + } + + fn advance_over_optional(&mut self, needle: u8) -> bool { + match self.current { + Some((_, c)) if c == needle => { + self.advance(); + true + } + _ => false, + } + } + + fn advance_over_digits(&mut self) -> bool { + let mut found_some = false; + while self.current.is_some() && self.current.unwrap().1.is_ascii_digit() { + found_some = true; + self.current = self.ci.next(); + } + found_some + } + + fn advance_over_simple_number(&mut self) -> bool { + let _ = self.advance_over_optional(b'-') || self.advance_over_optional(b'+'); + let found_digit = self.advance_over_digits(); + let _ = self.advance_over_optional(b'.'); + self.advance_over_digits() || found_digit + } + + fn match_number(&mut self) -> Result<Token, LexError> { + // remember the beginning + let (start_pos, _) = self.current.unwrap(); + if !self.advance_over_simple_number() && start_pos != self.current_pos() { + match self.current { + None => return Err(LexError::UnexpectedEof), + Some((_pos, c)) => return Err(LexError::UnexpectedByte(c)), + } + } + if self.advance_over_optional(b'e') || self.advance_over_optional(b'E') { + let _ = self.advance_over_optional(b'-') || self.advance_over_optional(b'+'); + let _ = self.advance_over_digits(); + } + let end_pos = match self.current { + None => self.input.len(), + Some((i, _)) => i, + }; + + // If you need path parsing to be faster, you can do from_utf8_unchecked to + // avoid re-validating all the chars, and std::str::parse<i*> calls are + // faster than std::str::parse<f64> for numbers that are not floats. + + // bare unwrap here should be safe since we've already checked all the bytes + // in the range + match std::str::from_utf8(&self.input[start_pos..end_pos]) + .unwrap() + .parse::<f64>() + { + Ok(n) => Ok(Number(n)), + Err(_e) => Err(LexError::ParseFloatError), + } + } +} + +impl Iterator for Lexer<'_> { + type Item = (usize, Result<Token, LexError>); + + fn next(&mut self) -> Option<Self::Item> { + // eat whitespace + self.advance_over_whitespace(); + + match self.current { + // commas are separators + Some((pos, c)) if c == b',' => { + self.advance(); + Some((pos, Ok(Comma))) + } + + // alphabetic chars are commands + Some((pos, c)) if c.is_ascii_alphabetic() => { + let token = Command(c); + self.advance(); + Some((pos, Ok(token))) + } + + Some((pos, c)) if self.flags_required > 0 && c.is_ascii_digit() => match c { + b'0' => { + self.flags_required -= 1; + self.advance(); + Some((pos, Ok(Flag(false)))) + } + b'1' => { + self.flags_required -= 1; + self.advance(); + Some((pos, Ok(Flag(true)))) + } + _ => Some((pos, Err(LexError::UnexpectedByte(c)))), + }, + + Some((pos, c)) if c.is_ascii_digit() || c == b'-' || c == b'+' || c == b'.' => { + Some((pos, self.match_number())) + } + + Some((pos, c)) => { + self.advance(); + Some((pos, Err(LexError::UnexpectedByte(c)))) + } + + None => None, + } + } +} + +pub struct PathParser<'b> { + tokens: Lexer<'b>, + current_pos_and_token: Option<(usize, Result<Token, LexError>)>, + + builder: &'b mut PathBuilder, + + // Current point; adjusted at every command + current_x: f64, + current_y: f64, + + // Last control point from previous cubic curve command, used to reflect + // the new control point for smooth cubic curve commands. + cubic_reflection_x: f64, + cubic_reflection_y: f64, + + // Last control point from previous quadratic curve command, used to reflect + // the new control point for smooth quadratic curve commands. + quadratic_reflection_x: f64, + quadratic_reflection_y: f64, + + // Start point of current subpath (i.e. position of last moveto); + // used for closepath. + subpath_start_x: f64, + subpath_start_y: f64, +} + +// This is a recursive descent parser for path data in SVG files, +// as specified in https://www.w3.org/TR/SVG/paths.html#PathDataBNF +// Some peculiarities: +// +// - SVG allows optional commas inside coordinate pairs, and between +// coordinate pairs. So, for example, these are equivalent: +// +// M 10 20 30 40 +// M 10, 20 30, 40 +// M 10, 20, 30, 40 +// +// - Whitespace is optional. These are equivalent: +// +// M10,20 30,40 +// M10,20,30,40 +// +// These are also equivalent: +// +// M-10,20-30-40 +// M -10 20 -30 -40 +// +// M.1-2,3E2-4 +// M 0.1 -2 300 -4 +impl<'b> PathParser<'b> { + pub fn new(builder: &'b mut PathBuilder, path_str: &'b str) -> PathParser<'b> { + let mut lexer = Lexer::new(path_str); + let pt = lexer.next(); + PathParser { + tokens: lexer, + current_pos_and_token: pt, + + builder, + + current_x: 0.0, + current_y: 0.0, + + cubic_reflection_x: 0.0, + cubic_reflection_y: 0.0, + + quadratic_reflection_x: 0.0, + quadratic_reflection_y: 0.0, + + subpath_start_x: 0.0, + subpath_start_y: 0.0, + } + } + + // Our match_* methods all either consume the token we requested + // and return the unwrapped value, or return an error without + // advancing the token stream. + // + // You can safely use them to probe for a particular kind of token, + // fail to match it, and try some other type. + + fn match_command(&mut self) -> Result<u8, ParseError> { + let result = match &self.current_pos_and_token { + Some((_, Ok(Command(c)))) => Ok(*c), + Some((pos, Ok(t))) => Err(ParseError::new(*pos, UnexpectedToken(*t))), + Some((pos, Err(e))) => Err(ParseError::new(*pos, LexError(*e))), + None => Err(ParseError::new(self.tokens.input.len(), UnexpectedEof)), + }; + if result.is_ok() { + self.current_pos_and_token = self.tokens.next(); + } + result + } + + fn match_number(&mut self) -> Result<f64, ParseError> { + let result = match &self.current_pos_and_token { + Some((_, Ok(Number(n)))) => Ok(*n), + Some((pos, Ok(t))) => Err(ParseError::new(*pos, UnexpectedToken(*t))), + Some((pos, Err(e))) => Err(ParseError::new(*pos, LexError(*e))), + None => Err(ParseError::new(self.tokens.input.len(), UnexpectedEof)), + }; + if result.is_ok() { + self.current_pos_and_token = self.tokens.next(); + } + result + } + + fn match_number_and_flags(&mut self) -> Result<(f64, bool, bool), ParseError> { + // We can't just do self.match_number() here, because we have to + // tell the lexer, if we do find a number, to switch to looking for flags + // before we advance it to the next token. Otherwise it will treat the flag + // characters as numbers. + // + // So, first we do the guts of match_number... + let n = match &self.current_pos_and_token { + Some((_, Ok(Number(n)))) => Ok(*n), + Some((pos, Ok(t))) => Err(ParseError::new(*pos, UnexpectedToken(*t))), + Some((pos, Err(e))) => Err(ParseError::new(*pos, LexError(*e))), + None => Err(ParseError::new(self.tokens.input.len(), UnexpectedEof)), + }?; + + // Then we tell the lexer that we're going to need to find Flag tokens, + // *then* we can advance the token stream. + self.tokens.require_flags(); + self.current_pos_and_token = self.tokens.next(); + + self.eat_optional_comma(); + let f1 = self.match_flag()?; + + self.eat_optional_comma(); + let f2 = self.match_flag()?; + + Ok((n, f1, f2)) + } + + fn match_comma(&mut self) -> Result<(), ParseError> { + let result = match &self.current_pos_and_token { + Some((_, Ok(Comma))) => Ok(()), + Some((pos, Ok(t))) => Err(ParseError::new(*pos, UnexpectedToken(*t))), + Some((pos, Err(e))) => Err(ParseError::new(*pos, LexError(*e))), + None => Err(ParseError::new(self.tokens.input.len(), UnexpectedEof)), + }; + if result.is_ok() { + self.current_pos_and_token = self.tokens.next(); + } + result + } + + fn eat_optional_comma(&mut self) { + let _ = self.match_comma(); + } + + // Convenience function; like match_number, but eats a leading comma if present. + fn match_comma_number(&mut self) -> Result<f64, ParseError> { + self.eat_optional_comma(); + self.match_number() + } + + fn match_flag(&mut self) -> Result<bool, ParseError> { + let result = match self.current_pos_and_token { + Some((_, Ok(Flag(f)))) => Ok(f), + Some((pos, Ok(t))) => Err(ParseError::new(pos, UnexpectedToken(t))), + Some((pos, Err(e))) => Err(ParseError::new(pos, LexError(e))), + None => Err(ParseError::new(self.tokens.input.len(), UnexpectedEof)), + }; + if result.is_ok() { + self.current_pos_and_token = self.tokens.next(); + } + result + } + + // peek_* methods are the twins of match_*, but don't consume the token, and so + // can't return ParseError + + fn peek_command(&mut self) -> Option<u8> { + match &self.current_pos_and_token { + Some((_, Ok(Command(c)))) => Some(*c), + _ => None, + } + } + + fn peek_number(&mut self) -> Option<f64> { + match &self.current_pos_and_token { + Some((_, Ok(Number(n)))) => Some(*n), + _ => None, + } + } + + // This is the entry point for parsing a given blob of path data. + // All the parsing just uses various match_* methods to consume tokens + // and retrieve the values. + pub fn parse(&mut self) -> Result<(), ParseError> { + if self.current_pos_and_token.is_none() { + return Ok(()); + } + + self.moveto_drawto_command_groups() + } + + fn error(&self, kind: ErrorKind) -> ParseError { + match self.current_pos_and_token { + Some((pos, _)) => ParseError { + position: pos, + kind, + }, + None => ParseError { position: 0, kind }, // FIXME: ??? + } + } + + fn coordinate_pair(&mut self) -> Result<(f64, f64), ParseError> { + Ok((self.match_number()?, self.match_comma_number()?)) + } + + fn set_current_point(&mut self, x: f64, y: f64) { + self.current_x = x; + self.current_y = y; + + self.cubic_reflection_x = self.current_x; + self.cubic_reflection_y = self.current_y; + + self.quadratic_reflection_x = self.current_x; + self.quadratic_reflection_y = self.current_y; + } + + fn set_cubic_reflection_and_current_point(&mut self, x3: f64, y3: f64, x4: f64, y4: f64) { + self.cubic_reflection_x = x3; + self.cubic_reflection_y = y3; + + self.current_x = x4; + self.current_y = y4; + + self.quadratic_reflection_x = self.current_x; + self.quadratic_reflection_y = self.current_y; + } + + fn set_quadratic_reflection_and_current_point(&mut self, a: f64, b: f64, c: f64, d: f64) { + self.quadratic_reflection_x = a; + self.quadratic_reflection_y = b; + + self.current_x = c; + self.current_y = d; + + self.cubic_reflection_x = self.current_x; + self.cubic_reflection_y = self.current_y; + } + + fn emit_move_to(&mut self, x: f64, y: f64) { + self.set_current_point(x, y); + + self.subpath_start_x = self.current_x; + self.subpath_start_y = self.current_y; + + self.builder.move_to(self.current_x, self.current_y); + } + + fn emit_line_to(&mut self, x: f64, y: f64) { + self.set_current_point(x, y); + + self.builder.line_to(self.current_x, self.current_y); + } + + fn emit_curve_to(&mut self, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) { + self.set_cubic_reflection_and_current_point(x3, y3, x4, y4); + + self.builder.curve_to(x2, y2, x3, y3, x4, y4); + } + + fn emit_quadratic_curve_to(&mut self, a: f64, b: f64, c: f64, d: f64) { + // raise quadratic Bézier to cubic + let x2 = (self.current_x + 2.0 * a) / 3.0; + let y2 = (self.current_y + 2.0 * b) / 3.0; + let x4 = c; + let y4 = d; + let x3 = (x4 + 2.0 * a) / 3.0; + let y3 = (y4 + 2.0 * b) / 3.0; + + self.set_quadratic_reflection_and_current_point(a, b, c, d); + + self.builder.curve_to(x2, y2, x3, y3, x4, y4); + } + + fn emit_arc( + &mut self, + rx: f64, + ry: f64, + x_axis_rotation: f64, + large_arc: LargeArc, + sweep: Sweep, + x: f64, + y: f64, + ) { + let (start_x, start_y) = (self.current_x, self.current_y); + + self.set_current_point(x, y); + + self.builder.arc( + start_x, + start_y, + rx, + ry, + x_axis_rotation, + large_arc, + sweep, + self.current_x, + self.current_y, + ); + } + + fn moveto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { + let (mut x, mut y) = self.coordinate_pair()?; + + if !absolute { + x += self.current_x; + y += self.current_y; + } + + self.emit_move_to(x, y); + + if self.match_comma().is_ok() || self.peek_number().is_some() { + self.lineto_argument_sequence(absolute) + } else { + Ok(()) + } + } + + fn moveto(&mut self) -> Result<(), ParseError> { + match self.match_command()? { + b'M' => self.moveto_argument_sequence(true), + b'm' => self.moveto_argument_sequence(false), + c => Err(self.error(ErrorKind::UnexpectedCommand(c))), + } + } + + fn moveto_drawto_command_group(&mut self) -> Result<(), ParseError> { + self.moveto()?; + self.optional_drawto_commands().map(|_| ()) + } + + fn moveto_drawto_command_groups(&mut self) -> Result<(), ParseError> { + loop { + self.moveto_drawto_command_group()?; + + if self.current_pos_and_token.is_none() { + break; + } + } + + Ok(()) + } + + fn optional_drawto_commands(&mut self) -> Result<bool, ParseError> { + while self.drawto_command()? { + // everything happens in the drawto_command() calls. + } + + Ok(false) + } + + // FIXME: This should not just fail to match 'M' and 'm', but make sure the + // command is in the set of drawto command characters. + fn match_if_drawto_command_with_absolute(&mut self) -> Option<(u8, bool)> { + let cmd = self.peek_command(); + let result = match cmd { + Some(b'M') => None, + Some(b'm') => None, + Some(c) => { + let c_up = c.to_ascii_uppercase(); + if c == c_up { + Some((c_up, true)) + } else { + Some((c_up, false)) + } + } + _ => None, + }; + if result.is_some() { + let _ = self.match_command(); + } + result + } + + fn drawto_command(&mut self) -> Result<bool, ParseError> { + match self.match_if_drawto_command_with_absolute() { + Some((b'Z', _)) => { + self.emit_close_path(); + Ok(true) + } + Some((b'L', abs)) => { + self.lineto_argument_sequence(abs)?; + Ok(true) + } + Some((b'H', abs)) => { + self.horizontal_lineto_argument_sequence(abs)?; + Ok(true) + } + Some((b'V', abs)) => { + self.vertical_lineto_argument_sequence(abs)?; + Ok(true) + } + Some((b'C', abs)) => { + self.curveto_argument_sequence(abs)?; + Ok(true) + } + Some((b'S', abs)) => { + self.smooth_curveto_argument_sequence(abs)?; + Ok(true) + } + Some((b'Q', abs)) => { + self.quadratic_curveto_argument_sequence(abs)?; + Ok(true) + } + Some((b'T', abs)) => { + self.smooth_quadratic_curveto_argument_sequence(abs)?; + Ok(true) + } + Some((b'A', abs)) => { + self.elliptical_arc_argument_sequence(abs)?; + Ok(true) + } + _ => Ok(false), + } + } + + fn emit_close_path(&mut self) { + let (x, y) = (self.subpath_start_x, self.subpath_start_y); + self.set_current_point(x, y); + + self.builder.close_path(); + } + + fn should_break_arg_sequence(&mut self) -> bool { + if self.match_comma().is_ok() { + // if there is a comma (indicating we should continue to loop), eat the comma + // so we're ready at the next start of the loop to process the next token. + false + } else { + // continue to process args in the sequence unless the next token is a comma + self.peek_number().is_none() + } + } + + fn lineto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { + loop { + let (mut x, mut y) = self.coordinate_pair()?; + + if !absolute { + x += self.current_x; + y += self.current_y; + } + + self.emit_line_to(x, y); + + if self.should_break_arg_sequence() { + break; + } + } + + Ok(()) + } + + fn horizontal_lineto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { + loop { + let mut x = self.match_number()?; + + if !absolute { + x += self.current_x; + } + + let y = self.current_y; + + self.emit_line_to(x, y); + + if self.should_break_arg_sequence() { + break; + } + } + + Ok(()) + } + + fn vertical_lineto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { + loop { + let mut y = self.match_number()?; + + if !absolute { + y += self.current_y; + } + + let x = self.current_x; + + self.emit_line_to(x, y); + + if self.should_break_arg_sequence() { + break; + } + } + + Ok(()) + } + + fn curveto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { + loop { + let (mut x2, mut y2) = self.coordinate_pair()?; + + self.eat_optional_comma(); + let (mut x3, mut y3) = self.coordinate_pair()?; + + self.eat_optional_comma(); + let (mut x4, mut y4) = self.coordinate_pair()?; + + if !absolute { + x2 += self.current_x; + y2 += self.current_y; + x3 += self.current_x; + y3 += self.current_y; + x4 += self.current_x; + y4 += self.current_y; + } + + self.emit_curve_to(x2, y2, x3, y3, x4, y4); + + if self.should_break_arg_sequence() { + break; + } + } + + Ok(()) + } + + fn smooth_curveto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { + loop { + let (mut x3, mut y3) = self.coordinate_pair()?; + self.eat_optional_comma(); + let (mut x4, mut y4) = self.coordinate_pair()?; + + if !absolute { + x3 += self.current_x; + y3 += self.current_y; + x4 += self.current_x; + y4 += self.current_y; + } + + let (x2, y2) = ( + self.current_x + self.current_x - self.cubic_reflection_x, + self.current_y + self.current_y - self.cubic_reflection_y, + ); + + self.emit_curve_to(x2, y2, x3, y3, x4, y4); + + if self.should_break_arg_sequence() { + break; + } + } + + Ok(()) + } + + fn quadratic_curveto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { + loop { + let (mut a, mut b) = self.coordinate_pair()?; + self.eat_optional_comma(); + let (mut c, mut d) = self.coordinate_pair()?; + + if !absolute { + a += self.current_x; + b += self.current_y; + c += self.current_x; + d += self.current_y; + } + + self.emit_quadratic_curve_to(a, b, c, d); + + if self.should_break_arg_sequence() { + break; + } + } + + Ok(()) + } + + fn smooth_quadratic_curveto_argument_sequence( + &mut self, + absolute: bool, + ) -> Result<(), ParseError> { + loop { + let (mut c, mut d) = self.coordinate_pair()?; + + if !absolute { + c += self.current_x; + d += self.current_y; + } + + let (a, b) = ( + self.current_x + self.current_x - self.quadratic_reflection_x, + self.current_y + self.current_y - self.quadratic_reflection_y, + ); + + self.emit_quadratic_curve_to(a, b, c, d); + + if self.should_break_arg_sequence() { + break; + } + } + + Ok(()) + } + + fn elliptical_arc_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> { + loop { + let rx = self.match_number()?.abs(); + let ry = self.match_comma_number()?.abs(); + + self.eat_optional_comma(); + let (x_axis_rotation, f1, f2) = self.match_number_and_flags()?; + + let large_arc = LargeArc(f1); + + let sweep = if f2 { Sweep::Positive } else { Sweep::Negative }; + + self.eat_optional_comma(); + + let (mut x, mut y) = self.coordinate_pair()?; + + if !absolute { + x += self.current_x; + y += self.current_y; + } + + self.emit_arc(rx, ry, x_axis_rotation, large_arc, sweep, x, y); + + if self.should_break_arg_sequence() { + break; + } + } + + Ok(()) + } +} + +#[derive(Debug, PartialEq)] +pub enum ErrorKind { + UnexpectedToken(Token), + UnexpectedCommand(u8), + UnexpectedEof, + LexError(LexError), +} + +#[derive(Debug, PartialEq)] +pub struct ParseError { + pub position: usize, + pub kind: ErrorKind, +} + +impl ParseError { + fn new(pos: usize, k: ErrorKind) -> ParseError { + ParseError { + position: pos, + kind: k, + } + } +} + +use crate::path_parser::ErrorKind::*; + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let description = match self.kind { + UnexpectedToken(_t) => "unexpected token", + UnexpectedCommand(_c) => "unexpected command", + UnexpectedEof => "unexpected end of data", + LexError(_le) => "error processing token", + }; + write!(f, "error at position {}: {}", self.position, description) + } +} + +#[cfg(test)] +#[rustfmt::skip] +mod tests { + use super::*; + + fn find_error_pos(s: &str) -> Option<usize> { + s.find('^') + } + + fn make_parse_result( + error_pos_str: &str, + error_kind: Option<ErrorKind>, + ) -> Result<(), ParseError> { + if let Some(pos) = find_error_pos(error_pos_str) { + Err(ParseError { + position: pos, + kind: error_kind.unwrap(), + }) + } else { + assert!(error_kind.is_none()); + Ok(()) + } + } + + fn test_parser( + path_str: &str, + error_pos_str: &str, + expected_commands: &[PathCommand], + expected_error_kind: Option<ErrorKind>, + ) { + let expected_result = make_parse_result(error_pos_str, expected_error_kind); + + let mut builder = PathBuilder::default(); + let result = builder.parse(path_str); + + let path = builder.into_path(); + let commands = path.iter().collect::<Vec<_>>(); + + assert_eq!(expected_commands, commands.as_slice()); + assert_eq!(expected_result, result); + } + + fn moveto(x: f64, y: f64) -> PathCommand { + PathCommand::MoveTo(x, y) + } + + fn lineto(x: f64, y: f64) -> PathCommand { + PathCommand::LineTo(x, y) + } + + fn curveto(x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> PathCommand { + PathCommand::CurveTo(CubicBezierCurve { + pt1: (x2, y2), + pt2: (x3, y3), + to: (x4, y4), + }) + } + + fn arc(x2: f64, y2: f64, xr: f64, large_arc: bool, sweep: bool, + x3: f64, y3: f64, x4: f64, y4: f64) -> PathCommand { + PathCommand::Arc(EllipticalArc { + r: (x2, y2), + x_axis_rotation: xr, + large_arc: LargeArc(large_arc), + sweep: match sweep { + true => Sweep::Positive, + false => Sweep::Negative, + }, + from: (x3, y3), + to: (x4, y4), + }) + } + + fn closepath() -> PathCommand { + PathCommand::ClosePath + } + + #[test] + fn handles_empty_data() { + test_parser( + "", + "", + &Vec::<PathCommand>::new(), + None, + ); + } + + #[test] + fn handles_numbers() { + test_parser( + "M 10 20", + "", + &vec![moveto(10.0, 20.0)], + None, + ); + + test_parser( + "M -10 -20", + "", + &vec![moveto(-10.0, -20.0)], + None, + ); + + test_parser( + "M .10 0.20", + "", + &vec![moveto(0.10, 0.20)], + None, + ); + + test_parser( + "M -.10 -0.20", + "", + &vec![moveto(-0.10, -0.20)], + None, + ); + + test_parser( + "M-.10-0.20", + "", + &vec![moveto(-0.10, -0.20)], + None, + ); + + test_parser( + "M10.5.50", + "", + &vec![moveto(10.5, 0.50)], + None, + ); + + test_parser( + "M.10.20", + "", + &vec![moveto(0.10, 0.20)], + None, + ); + + test_parser( + "M .10E1 .20e-4", + "", + &vec![moveto(1.0, 0.000020)], + None, + ); + + test_parser( + "M-.10E1-.20", + "", + &vec![moveto(-1.0, -0.20)], + None, + ); + + test_parser( + "M10.10E2 -0.20e3", + "", + &vec![moveto(1010.0, -200.0)], + None, + ); + + test_parser( + "M-10.10E2-0.20e-3", + "", + &vec![moveto(-1010.0, -0.00020)], + None, + ); + + test_parser( + "M1e2.5", // a decimal after exponent start the next number + "", + &vec![moveto(100.0, 0.5)], + None, + ); + + test_parser( + "M1e-2.5", // but we are allowed a sign after exponent + "", + &vec![moveto(0.01, 0.5)], + None, + ); + + test_parser( + "M1e+2.5", // but we are allowed a sign after exponent + "", + &vec![moveto(100.0, 0.5)], + None, + ); + } + + #[test] + fn detects_bogus_numbers() { + test_parser( + "M+", + " ^", + &vec![], + Some(ErrorKind::LexError(LexError::UnexpectedEof)), + ); + + test_parser( + "M-", + " ^", + &vec![], + Some(ErrorKind::LexError(LexError::UnexpectedEof)), + ); + + test_parser( + "M+x", + " ^", + &vec![], + Some(ErrorKind::LexError(LexError::UnexpectedByte(b'x'))), + ); + + test_parser( + "M10e", + " ^", + &vec![], + Some(ErrorKind::LexError(LexError::ParseFloatError)), + ); + + test_parser( + "M10ex", + " ^", + &vec![], + Some(ErrorKind::LexError(LexError::ParseFloatError)), + ); + + test_parser( + "M10e-", + " ^", + &vec![], + Some(ErrorKind::LexError(LexError::ParseFloatError)), + ); + + test_parser( + "M10e+x", + " ^", + &vec![], + Some(ErrorKind::LexError(LexError::ParseFloatError)), + ); + } + + #[test] + fn handles_numbers_with_comma() { + test_parser( + "M 10, 20", + "", + &vec![moveto(10.0, 20.0)], + None, + ); + + test_parser( + "M -10,-20", + "", + &vec![moveto(-10.0, -20.0)], + None, + ); + + test_parser( + "M.10 , 0.20", + "", + &vec![moveto(0.10, 0.20)], + None, + ); + + test_parser( + "M -.10, -0.20 ", + "", + &vec![moveto(-0.10, -0.20)], + None, + ); + + test_parser( + "M-.10-0.20", + "", + &vec![moveto(-0.10, -0.20)], + None, + ); + + test_parser( + "M.10.20", + "", + &vec![moveto(0.10, 0.20)], + None, + ); + + test_parser( + "M .10E1,.20e-4", + "", + &vec![moveto(1.0, 0.000020)], + None, + ); + + test_parser( + "M-.10E-2,-.20", + "", + &vec![moveto(-0.0010, -0.20)], + None, + ); + + test_parser( + "M10.10E2,-0.20e3", + "", + &vec![moveto(1010.0, -200.0)], + None, + ); + + test_parser( + "M-10.10E2,-0.20e-3", + "", + &vec![moveto(-1010.0, -0.00020)], + None, + ); + } + + #[test] + fn handles_single_moveto() { + test_parser( + "M 10 20 ", + "", + &vec![moveto(10.0, 20.0)], + None, + ); + + test_parser( + "M10,20 ", + "", + &vec![moveto(10.0, 20.0)], + None, + ); + + test_parser( + "M10 20 ", + "", + &vec![moveto(10.0, 20.0)], + None, + ); + + test_parser( + " M10,20 ", + "", + &vec![moveto(10.0, 20.0)], + None, + ); + } + + #[test] + fn handles_relative_moveto() { + test_parser( + "m10 20", + "", + &vec![moveto(10.0, 20.0)], + None, + ); + } + + #[test] + fn handles_absolute_moveto_with_implicit_lineto() { + test_parser( + "M10 20 30 40", + "", + &vec![moveto(10.0, 20.0), lineto(30.0, 40.0)], + None, + ); + + test_parser( + "M10,20,30,40", + "", + &vec![moveto(10.0, 20.0), lineto(30.0, 40.0)], + None, + ); + + test_parser( + "M.1-2,3E2-4", + "", + &vec![moveto(0.1, -2.0), lineto(300.0, -4.0)], + None, + ); + } + + #[test] + fn handles_relative_moveto_with_implicit_lineto() { + test_parser( + "m10 20 30 40", + "", + &vec![moveto(10.0, 20.0), lineto(40.0, 60.0)], + None, + ); + } + + #[test] + fn handles_relative_moveto_with_relative_lineto_sequence() { + test_parser( + // 1 2 3 4 5 + "m 46,447 l 0,0.5 -1,0 -1,0 0,1 0,12", + "", + &vec![moveto(46.0, 447.0), lineto(46.0, 447.5), lineto(45.0, 447.5), + lineto(44.0, 447.5), lineto(44.0, 448.5), lineto(44.0, 460.5)], + None, + ); + } + + #[test] + fn handles_absolute_moveto_with_implicit_linetos() { + test_parser( + "M10,20 30,40,50 60", + "", + &vec![moveto(10.0, 20.0), lineto(30.0, 40.0), lineto(50.0, 60.0)], + None, + ); + } + + #[test] + fn handles_relative_moveto_with_implicit_linetos() { + test_parser( + "m10 20 30 40 50 60", + "", + &vec![moveto(10.0, 20.0), lineto(40.0, 60.0), lineto(90.0, 120.0)], + None, + ); + } + + #[test] + fn handles_absolute_moveto_moveto() { + test_parser( + "M10 20 M 30 40", + "", + &vec![moveto(10.0, 20.0), moveto(30.0, 40.0)], + None, + ); + } + + #[test] + fn handles_relative_moveto_moveto() { + test_parser( + "m10 20 m 30 40", + "", + &vec![moveto(10.0, 20.0), moveto(40.0, 60.0)], + None, + ); + } + + #[test] + fn handles_relative_moveto_lineto_moveto() { + test_parser( + "m10 20 30 40 m 50 60", + "", + &vec![moveto(10.0, 20.0), lineto(40.0, 60.0), moveto(90.0, 120.0)], + None, + ); + } + + #[test] + fn handles_absolute_moveto_lineto() { + test_parser( + "M10 20 L30,40", + "", + &vec![moveto(10.0, 20.0), lineto(30.0, 40.0)], + None, + ); + } + + #[test] + fn handles_relative_moveto_lineto() { + test_parser( + "m10 20 l30,40", + "", + &vec![moveto(10.0, 20.0), lineto(40.0, 60.0)], + None, + ); + } + + #[test] + fn handles_relative_moveto_lineto_lineto_abs_lineto() { + test_parser( + "m10 20 30 40l30,40,50 60L200,300", + "", + &vec![ + moveto(10.0, 20.0), + lineto(40.0, 60.0), + lineto(70.0, 100.0), + lineto(120.0, 160.0), + lineto(200.0, 300.0), + ], + None, + ); + } + + #[test] + fn handles_horizontal_lineto() { + test_parser( + "M10 20 H30", + "", + &vec![moveto(10.0, 20.0), lineto(30.0, 20.0)], + None, + ); + + test_parser( + "M10 20 H30 40", + "", + &vec![moveto(10.0, 20.0), lineto(30.0, 20.0), lineto(40.0, 20.0)], + None, + ); + + test_parser( + "M10 20 H30,40-50", + "", + &vec![ + moveto(10.0, 20.0), + lineto(30.0, 20.0), + lineto(40.0, 20.0), + lineto(-50.0, 20.0), + ], + None, + ); + + test_parser( + "m10 20 h30,40-50", + "", + &vec![ + moveto(10.0, 20.0), + lineto(40.0, 20.0), + lineto(80.0, 20.0), + lineto(30.0, 20.0), + ], + None, + ); + } + + #[test] + fn handles_vertical_lineto() { + test_parser( + "M10 20 V30", + "", + &vec![moveto(10.0, 20.0), lineto(10.0, 30.0)], + None, + ); + + test_parser( + "M10 20 V30 40", + "", + &vec![moveto(10.0, 20.0), lineto(10.0, 30.0), lineto(10.0, 40.0)], + None, + ); + + test_parser( + "M10 20 V30,40-50", + "", + &vec![ + moveto(10.0, 20.0), + lineto(10.0, 30.0), + lineto(10.0, 40.0), + lineto(10.0, -50.0), + ], + None, + ); + + test_parser( + "m10 20 v30,40-50", + "", + &vec![ + moveto(10.0, 20.0), + lineto(10.0, 50.0), + lineto(10.0, 90.0), + lineto(10.0, 40.0), + ], + None, + ); + } + + #[test] + fn handles_curveto() { + test_parser( + "M10 20 C 30,40 50 60-70,80", + "", + &vec![ + moveto(10.0, 20.0), + curveto(30.0, 40.0, 50.0, 60.0, -70.0, 80.0), + ], + None, + ); + + test_parser( + "M10 20 C 30,40 50 60-70,80,90 100,110 120,130,140", + "", + &vec![ + moveto(10.0, 20.0), + curveto(30.0, 40.0, 50.0, 60.0, -70.0, 80.0), + curveto(90.0, 100.0, 110.0, 120.0, 130.0, 140.0), + ], + None, + ); + + test_parser( + "m10 20 c 30,40 50 60-70,80,90 100,110 120,130,140", + "", + &vec![ + moveto(10.0, 20.0), + curveto(40.0, 60.0, 60.0, 80.0, -60.0, 100.0), + curveto(30.0, 200.0, 50.0, 220.0, 70.0, 240.0), + ], + None, + ); + + test_parser( + "m10 20 c 30,40 50 60-70,80,90 100,110 120,130,140", + "", + &vec![ + moveto(10.0, 20.0), + curveto(40.0, 60.0, 60.0, 80.0, -60.0, 100.0), + curveto(30.0, 200.0, 50.0, 220.0, 70.0, 240.0), + ], + None, + ); + } + + #[test] + fn handles_smooth_curveto() { + test_parser( + "M10 20 S 30,40-50,60", + "", + &vec![ + moveto(10.0, 20.0), + curveto(10.0, 20.0, 30.0, 40.0, -50.0, 60.0), + ], + None, + ); + + test_parser( + "M10 20 S 30,40 50 60-70,80,90 100", + "", + &vec![ + moveto(10.0, 20.0), + curveto(10.0, 20.0, 30.0, 40.0, 50.0, 60.0), + curveto(70.0, 80.0, -70.0, 80.0, 90.0, 100.0), + ], + None, + ); + + test_parser( + "m10 20 s 30,40 50 60-70,80,90 100", + "", + &vec![ + moveto(10.0, 20.0), + curveto(10.0, 20.0, 40.0, 60.0, 60.0, 80.0), + curveto(80.0, 100.0, -10.0, 160.0, 150.0, 180.0), + ], + None, + ); + } + + #[test] + fn handles_quadratic_curveto() { + test_parser( + "M10 20 Q30 40 50 60", + "", + &vec![ + moveto(10.0, 20.0), + curveto( + 70.0 / 3.0, + 100.0 / 3.0, + 110.0 / 3.0, + 140.0 / 3.0, + 50.0, + 60.0, + ), + ], + None, + ); + + test_parser( + "M10 20 Q30 40 50 60,70,80-90 100", + "", + &vec![ + moveto(10.0, 20.0), + curveto( + 70.0 / 3.0, + 100.0 / 3.0, + 110.0 / 3.0, + 140.0 / 3.0, + 50.0, + 60.0, + ), + curveto( + 190.0 / 3.0, + 220.0 / 3.0, + 50.0 / 3.0, + 260.0 / 3.0, + -90.0, + 100.0, + ), + ], + None, + ); + + test_parser( + "m10 20 q 30,40 50 60-70,80 90 100", + "", + &vec![ + moveto(10.0, 20.0), + curveto( + 90.0 / 3.0, + 140.0 / 3.0, + 140.0 / 3.0, + 200.0 / 3.0, + 60.0, + 80.0, + ), + curveto( + 40.0 / 3.0, + 400.0 / 3.0, + 130.0 / 3.0, + 500.0 / 3.0, + 150.0, + 180.0, + ), + ], + None, + ); + } + + #[test] + fn handles_smooth_quadratic_curveto() { + test_parser( + "M10 20 T30 40", + "", + &vec![ + moveto(10.0, 20.0), + curveto(10.0, 20.0, 50.0 / 3.0, 80.0 / 3.0, 30.0, 40.0), + ], + None, + ); + + test_parser( + "M10 20 Q30 40 50 60 T70 80", + "", + &vec![ + moveto(10.0, 20.0), + curveto( + 70.0 / 3.0, + 100.0 / 3.0, + 110.0 / 3.0, + 140.0 / 3.0, + 50.0, + 60.0, + ), + curveto(190.0 / 3.0, 220.0 / 3.0, 70.0, 80.0, 70.0, 80.0), + ], + None, + ); + + test_parser( + "m10 20 q 30,40 50 60t-70,80", + "", + &vec![ + moveto(10.0, 20.0), + curveto( + 90.0 / 3.0, + 140.0 / 3.0, + 140.0 / 3.0, + 200.0 / 3.0, + 60.0, + 80.0, + ), + curveto(220.0 / 3.0, 280.0 / 3.0, 50.0, 120.0, -10.0, 160.0), + ], + None, + ); + } + + #[test] + fn handles_elliptical_arc() { + // no space required between arc flags + test_parser("M 1 2 A 1 2 3 00 6 7", + "", + &vec![moveto(1.0, 2.0), + arc(1.0, 2.0, 3.0, false, false, 1.0, 2.0, 6.0, 7.0)], + None); + // or after... + test_parser("M 1 2 A 1 2 3 016 7", + "", + &vec![moveto(1.0, 2.0), + arc(1.0, 2.0, 3.0, false, true, 1.0, 2.0, 6.0, 7.0)], + None); + // commas and whitespace are optionally allowed + test_parser("M 1 2 A 1 2 3 10,6 7", + "", + &vec![moveto(1.0, 2.0), + arc(1.0, 2.0, 3.0, true, false, 1.0, 2.0, 6.0, 7.0)], + None); + test_parser("M 1 2 A 1 2 3 1,16, 7", + "", + &vec![moveto(1.0, 2.0), + arc(1.0, 2.0, 3.0, true, true, 1.0, 2.0, 6.0, 7.0)], + None); + test_parser("M 1 2 A 1 2 3 1,1 6 7", + "", + &vec![moveto(1.0, 2.0), + arc(1.0, 2.0, 3.0, true, true, 1.0, 2.0, 6.0, 7.0)], + None); + test_parser("M 1 2 A 1 2 3 1 1 6 7", + "", + &vec![moveto(1.0, 2.0), + arc(1.0, 2.0, 3.0, true, true, 1.0, 2.0, 6.0, 7.0)], + None); + test_parser("M 1 2 A 1 2 3 1 16 7", + "", + &vec![moveto(1.0, 2.0), + arc(1.0, 2.0, 3.0, true, true, 1.0, 2.0, 6.0, 7.0)], + None); + } + + #[test] + fn handles_close_path() { + test_parser("M10 20 Z", "", &vec![moveto(10.0, 20.0), closepath()], None); + + test_parser( + "m10 20 30 40 m 50 60 70 80 90 100z", + "", + &vec![ + moveto(10.0, 20.0), + lineto(40.0, 60.0), + moveto(90.0, 120.0), + lineto(160.0, 200.0), + lineto(250.0, 300.0), + closepath(), + ], + None, + ); + } + + #[test] + fn first_command_must_be_moveto() { + test_parser( + " L10 20", + " ^", // FIXME: why is this not at position 2? + &vec![], + Some(ErrorKind::UnexpectedCommand(b'L')), + ); + } + + #[test] + fn moveto_args() { + test_parser( + "M", + " ^", + &vec![], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M,", + " ^", + &vec![], + Some(ErrorKind::UnexpectedToken(Comma)), + ); + + test_parser( + "M10", + " ^", + &vec![], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10,", + " ^", + &vec![], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10x", + " ^", + &vec![], + Some(ErrorKind::UnexpectedToken(Command(b'x'))), + ); + + test_parser( + "M10,x", + " ^", + &vec![], + Some(ErrorKind::UnexpectedToken(Command(b'x'))), + ); + } + + #[test] + fn moveto_implicit_lineto_args() { + test_parser( + "M10-20,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20-30", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20-30 x", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedToken(Command(b'x'))), + ); + } + + #[test] + fn closepath_no_args() { + test_parser( + "M10-20z10", + " ^", + &vec![moveto(10.0, -20.0), closepath()], + Some(ErrorKind::UnexpectedToken(Number(10.0))), + ); + + test_parser( + "M10-20z,", + " ^", + &vec![moveto(10.0, -20.0), closepath()], + Some(ErrorKind::UnexpectedToken(Comma)), + ); + } + + #[test] + fn lineto_args() { + test_parser( + "M10-20L10", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M 10,10 L 20,20,30", + " ^", + &vec![moveto(10.0, 10.0), lineto(20.0, 20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M 10,10 L 20,20,", + " ^", + &vec![moveto(10.0, 10.0), lineto(20.0, 20.0)], + Some(ErrorKind::UnexpectedEof), + ); + } + + #[test] + fn horizontal_lineto_args() { + test_parser( + "M10-20H", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20H,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedToken(Comma)), + ); + + test_parser( + "M10-20H30,", + " ^", + &vec![moveto(10.0, -20.0), lineto(30.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + } + + #[test] + fn vertical_lineto_args() { + test_parser( + "M10-20v", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20v,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedToken(Comma)), + ); + + test_parser( + "M10-20v30,", + " ^", + &vec![moveto(10.0, -20.0), lineto(10.0, 10.0)], + Some(ErrorKind::UnexpectedEof), + ); + } + + #[test] + fn curveto_args() { + test_parser( + "M10-20C1", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20C1,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20C1 2", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20C1,2,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20C1 2 3", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20C1,2,3", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20C1,2,3,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20C1 2 3 4", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20C1,2,3,4", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20C1,2,3,4,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20C1 2 3 4 5", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20C1,2,3,4,5", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20C1,2,3,4,5,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20C1,2,3,4,5,6,", + " ^", + &vec![moveto(10.0, -20.0), curveto(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)], + Some(ErrorKind::UnexpectedEof), + ); + } + + #[test] + fn smooth_curveto_args() { + test_parser( + "M10-20S1", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20S1,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20S1 2", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20S1,2,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20S1 2 3", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20S1,2,3", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20S1,2,3,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20S1,2,3,4,", + " ^", + &vec![ + moveto(10.0, -20.0), + curveto(10.0, -20.0, 1.0, 2.0, 3.0, 4.0), + ], + Some(ErrorKind::UnexpectedEof), + ); + } + + #[test] + fn quadratic_bezier_curveto_args() { + test_parser( + "M10-20Q1", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20Q1,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20Q1 2", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20Q1,2,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20Q1 2 3", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20Q1,2,3", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20Q1,2,3,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10 20 Q30 40 50 60,", + " ^", + &vec![ + moveto(10.0, 20.0), + curveto( + 70.0 / 3.0, + 100.0 / 3.0, + 110.0 / 3.0, + 140.0 / 3.0, + 50.0, + 60.0, + ), + ], + Some(ErrorKind::UnexpectedEof), + ); + } + + #[test] + fn smooth_quadratic_bezier_curveto_args() { + test_parser( + "M10-20T1", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20T1,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10 20 T30 40,", + " ^", + &vec![ + moveto(10.0, 20.0), + curveto(10.0, 20.0, 50.0 / 3.0, 80.0 / 3.0, 30.0, 40.0), + ], + Some(ErrorKind::UnexpectedEof), + ); + } + + #[test] + fn elliptical_arc_args() { + test_parser( + "M10-20A1", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20A1,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20A1 2", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20A1 2,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20A1 2 3", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20A1 2 3,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20A1 2 3 4", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::LexError(LexError::UnexpectedByte(b'4'))), + ); + + test_parser( + "M10-20A1 2 3 1", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20A1 2 3,1,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20A1 2 3 1 5", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::LexError(LexError::UnexpectedByte(b'5'))), + ); + + test_parser( + "M10-20A1 2 3 1 1", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20A1 2 3,1,1,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + test_parser( + "M10-20A1 2 3 1 1 6", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + test_parser( + "M10-20A1 2 3,1,1,6,", + " ^", + &vec![moveto(10.0, -20.0)], + Some(ErrorKind::UnexpectedEof), + ); + + // no non 0|1 chars allowed for flags + test_parser("M 1 2 A 1 2 3 1.0 0.0 6 7", + " ^", + &vec![moveto(1.0, 2.0)], + Some(ErrorKind::UnexpectedToken(Number(0.0)))); + + test_parser("M10-20A1 2 3,1,1,6,7,", + " ^", + &vec![moveto(10.0, -20.0), + arc(1.0, 2.0, 3.0, true, true, 10.0, -20.0, 6.0, 7.0)], + Some(ErrorKind::UnexpectedEof)); + } + + #[test] + fn bugs() { + // https://gitlab.gnome.org/GNOME/librsvg/issues/345 + test_parser( + "M.. 1,0 0,100000", + " ^", // FIXME: we have to report position of error in lexer errors to make this right + &vec![], + Some(ErrorKind::LexError(LexError::UnexpectedByte(b'.'))), + ); + } +} diff --git a/rsvg/src/pattern.rs b/rsvg/src/pattern.rs new file mode 100644 index 00000000..2fbbcc16 --- /dev/null +++ b/rsvg/src/pattern.rs @@ -0,0 +1,525 @@ +//! The `pattern` element. + +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::aspect_ratio::*; +use crate::coord_units::CoordUnits; +use crate::document::{AcquiredNode, AcquiredNodes, NodeId, NodeStack}; +use crate::drawing_ctx::Viewport; +use crate::element::{set_attribute, ElementData, ElementTrait}; +use crate::error::*; +use crate::href::{is_href, set_href}; +use crate::length::*; +use crate::node::{Node, NodeBorrow, WeakNode}; +use crate::parsers::ParseValue; +use crate::rect::Rect; +use crate::session::Session; +use crate::transform::{Transform, TransformAttribute}; +use crate::unit_interval::UnitInterval; +use crate::viewbox::*; +use crate::xml::Attributes; + +coord_units!(PatternUnits, CoordUnits::ObjectBoundingBox); +coord_units!(PatternContentUnits, CoordUnits::UserSpaceOnUse); + +#[derive(Clone, Default)] +struct Common { + units: Option<PatternUnits>, + content_units: Option<PatternContentUnits>, + // This Option<Option<ViewBox>> is a bit strange. We want a field + // with value None to mean, "this field isn't resolved yet". However, + // the vbox can very well be *not* specified in the SVG file. + // In that case, the fully resolved pattern will have a .vbox=Some(None) value. + vbox: Option<Option<ViewBox>>, + preserve_aspect_ratio: Option<AspectRatio>, + transform: Option<TransformAttribute>, + x: Option<Length<Horizontal>>, + y: Option<Length<Vertical>>, + width: Option<ULength<Horizontal>>, + height: Option<ULength<Vertical>>, +} + +/// State used during the pattern resolution process +/// +/// This is the current node's pattern information, plus the fallback +/// that should be used in case that information is not complete for a +/// resolved pattern yet. +struct Unresolved { + pattern: UnresolvedPattern, + fallback: Option<NodeId>, +} + +/// Keeps track of which Pattern provided a non-empty set of children during pattern resolution +#[derive(Clone)] +enum UnresolvedChildren { + /// Points back to the original Pattern if it had no usable children + Unresolved, + + /// Points back to the original Pattern, as no pattern in the + /// chain of fallbacks had usable children. This only gets returned + /// by resolve_from_defaults(). + ResolvedEmpty, + + /// Points back to the Pattern that had usable children. + WithChildren(WeakNode), +} + +/// Keeps track of which Pattern provided a non-empty set of children during pattern resolution +#[derive(Clone)] +enum Children { + Empty, + + /// Points back to the Pattern that had usable children + WithChildren(WeakNode), +} + +/// Main structure used during pattern resolution. For unresolved +/// patterns, we store all fields as `Option<T>` - if `None`, it means +/// that the field is not specified; if `Some(T)`, it means that the +/// field was specified. +struct UnresolvedPattern { + common: Common, + + // Point back to our corresponding node, or to the fallback node which has children. + // If the value is None, it means we are fully resolved and didn't find any children + // among the fallbacks. + children: UnresolvedChildren, +} + +#[derive(Clone)] +pub struct ResolvedPattern { + units: PatternUnits, + content_units: PatternContentUnits, + vbox: Option<ViewBox>, + preserve_aspect_ratio: AspectRatio, + transform: TransformAttribute, + x: Length<Horizontal>, + y: Length<Vertical>, + width: ULength<Horizontal>, + height: ULength<Vertical>, + opacity: UnitInterval, + + // Link to the node whose children are the pattern's resolved children. + children: Children, +} + +/// Pattern normalized to user-space units. +pub struct UserSpacePattern { + pub width: f64, + pub height: f64, + pub transform: Transform, + pub coord_transform: Transform, + pub content_transform: Transform, + pub opacity: UnitInterval, + + // This one is private so the caller has to go through fn acquire_pattern_node() + node_with_children: Node, +} + +#[derive(Default)] +pub struct Pattern { + common: Common, + fallback: Option<NodeId>, +} + +impl ElementTrait for Pattern { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "patternUnits") => { + set_attribute(&mut self.common.units, attr.parse(value), session) + } + expanded_name!("", "patternContentUnits") => { + set_attribute(&mut self.common.content_units, attr.parse(value), session); + } + expanded_name!("", "viewBox") => { + set_attribute(&mut self.common.vbox, attr.parse(value), session) + } + expanded_name!("", "preserveAspectRatio") => { + set_attribute( + &mut self.common.preserve_aspect_ratio, + attr.parse(value), + session, + ); + } + expanded_name!("", "patternTransform") => { + set_attribute(&mut self.common.transform, attr.parse(value), session); + } + ref a if is_href(a) => { + let mut href = None; + set_attribute( + &mut href, + NodeId::parse(value).map(Some).attribute(attr.clone()), + session, + ); + set_href(a, &mut self.fallback, href); + } + expanded_name!("", "x") => { + set_attribute(&mut self.common.x, attr.parse(value), session) + } + expanded_name!("", "y") => { + set_attribute(&mut self.common.y, attr.parse(value), session) + } + expanded_name!("", "width") => { + set_attribute(&mut self.common.width, attr.parse(value), session) + } + expanded_name!("", "height") => { + set_attribute(&mut self.common.height, attr.parse(value), session) + } + _ => (), + } + } + } +} + +impl UnresolvedPattern { + fn into_resolved(self, opacity: UnitInterval) -> ResolvedPattern { + assert!(self.is_resolved()); + + ResolvedPattern { + units: self.common.units.unwrap(), + content_units: self.common.content_units.unwrap(), + vbox: self.common.vbox.unwrap(), + preserve_aspect_ratio: self.common.preserve_aspect_ratio.unwrap(), + transform: self.common.transform.unwrap(), + x: self.common.x.unwrap(), + y: self.common.y.unwrap(), + width: self.common.width.unwrap(), + height: self.common.height.unwrap(), + opacity, + + children: self.children.to_resolved(), + } + } + + fn is_resolved(&self) -> bool { + self.common.units.is_some() + && self.common.content_units.is_some() + && self.common.vbox.is_some() + && self.common.preserve_aspect_ratio.is_some() + && self.common.transform.is_some() + && self.common.x.is_some() + && self.common.y.is_some() + && self.common.width.is_some() + && self.common.height.is_some() + && self.children.is_resolved() + } + + fn resolve_from_fallback(&self, fallback: &UnresolvedPattern) -> UnresolvedPattern { + let units = self.common.units.or(fallback.common.units); + let content_units = self.common.content_units.or(fallback.common.content_units); + let vbox = self.common.vbox.or(fallback.common.vbox); + let preserve_aspect_ratio = self + .common + .preserve_aspect_ratio + .or(fallback.common.preserve_aspect_ratio); + let transform = self.common.transform.or(fallback.common.transform); + let x = self.common.x.or(fallback.common.x); + let y = self.common.y.or(fallback.common.y); + let width = self.common.width.or(fallback.common.width); + let height = self.common.height.or(fallback.common.height); + let children = self.children.resolve_from_fallback(&fallback.children); + + UnresolvedPattern { + common: Common { + units, + content_units, + vbox, + preserve_aspect_ratio, + transform, + x, + y, + width, + height, + }, + children, + } + } + + fn resolve_from_defaults(&self) -> UnresolvedPattern { + let units = self.common.units.or_else(|| Some(PatternUnits::default())); + let content_units = self + .common + .content_units + .or_else(|| Some(PatternContentUnits::default())); + let vbox = self.common.vbox.or(Some(None)); + let preserve_aspect_ratio = self + .common + .preserve_aspect_ratio + .or_else(|| Some(AspectRatio::default())); + let transform = self + .common + .transform + .or_else(|| Some(TransformAttribute::default())); + let x = self.common.x.or_else(|| Some(Default::default())); + let y = self.common.y.or_else(|| Some(Default::default())); + let width = self.common.width.or_else(|| Some(Default::default())); + let height = self.common.height.or_else(|| Some(Default::default())); + let children = self.children.resolve_from_defaults(); + + UnresolvedPattern { + common: Common { + units, + content_units, + vbox, + preserve_aspect_ratio, + transform, + x, + y, + width, + height, + }, + children, + } + } +} + +impl UnresolvedChildren { + fn from_node(node: &Node) -> UnresolvedChildren { + let weak = node.downgrade(); + + if node.children().any(|child| child.is_element()) { + UnresolvedChildren::WithChildren(weak) + } else { + UnresolvedChildren::Unresolved + } + } + + fn is_resolved(&self) -> bool { + !matches!(*self, UnresolvedChildren::Unresolved) + } + + fn resolve_from_fallback(&self, fallback: &UnresolvedChildren) -> UnresolvedChildren { + use UnresolvedChildren::*; + + match (self, fallback) { + (&Unresolved, &Unresolved) => Unresolved, + (WithChildren(wc), _) => WithChildren(wc.clone()), + (_, WithChildren(wc)) => WithChildren(wc.clone()), + (_, _) => unreachable!(), + } + } + + fn resolve_from_defaults(&self) -> UnresolvedChildren { + use UnresolvedChildren::*; + + match *self { + Unresolved => ResolvedEmpty, + _ => (*self).clone(), + } + } + + fn to_resolved(&self) -> Children { + use UnresolvedChildren::*; + + assert!(self.is_resolved()); + + match *self { + ResolvedEmpty => Children::Empty, + WithChildren(ref wc) => Children::WithChildren(wc.clone()), + _ => unreachable!(), + } + } +} + +fn nonempty_rect(bbox: &Option<Rect>) -> Option<Rect> { + match *bbox { + None => None, + Some(r) if r.is_empty() => None, + Some(r) => Some(r), + } +} + +impl ResolvedPattern { + fn node_with_children(&self) -> Option<Node> { + match self.children { + // This means we didn't find any children among the fallbacks, + // so there is nothing to render. + Children::Empty => None, + + Children::WithChildren(ref wc) => Some(wc.upgrade().unwrap()), + } + } + + fn get_rect(&self, params: &NormalizeParams) -> Rect { + let x = self.x.to_user(params); + let y = self.y.to_user(params); + let w = self.width.to_user(params); + let h = self.height.to_user(params); + + Rect::new(x, y, x + w, y + h) + } + + pub fn to_user_space( + &self, + object_bbox: &Option<Rect>, + viewport: &Viewport, + values: &NormalizeValues, + ) -> Option<UserSpacePattern> { + let node_with_children = self.node_with_children()?; + + let view_params = viewport.with_units(self.units.0); + let params = NormalizeParams::from_values(values, &view_params); + + let rect = self.get_rect(¶ms); + + // Create the pattern coordinate system + let (width, height, coord_transform) = match self.units { + PatternUnits(CoordUnits::ObjectBoundingBox) => { + let bbrect = nonempty_rect(object_bbox)?; + ( + rect.width() * bbrect.width(), + rect.height() * bbrect.height(), + Transform::new_translate( + bbrect.x0 + rect.x0 * bbrect.width(), + bbrect.y0 + rect.y0 * bbrect.height(), + ), + ) + } + PatternUnits(CoordUnits::UserSpaceOnUse) => ( + rect.width(), + rect.height(), + Transform::new_translate(rect.x0, rect.y0), + ), + }; + + let pattern_transform = self.transform.to_transform(); + + let coord_transform = coord_transform.post_transform(&pattern_transform); + + // Create the pattern contents coordinate system + let content_transform = if let Some(vbox) = self.vbox { + // If there is a vbox, use that + let r = self + .preserve_aspect_ratio + .compute(&vbox, &Rect::from_size(width, height)); + + let sw = r.width() / vbox.width(); + let sh = r.height() / vbox.height(); + let x = r.x0 - vbox.x0 * sw; + let y = r.y0 - vbox.y0 * sh; + + Transform::new_scale(sw, sh).pre_translate(x, y) + } else { + match self.content_units { + PatternContentUnits(CoordUnits::ObjectBoundingBox) => { + let bbrect = nonempty_rect(object_bbox)?; + Transform::new_scale(bbrect.width(), bbrect.height()) + } + PatternContentUnits(CoordUnits::UserSpaceOnUse) => Transform::identity(), + } + }; + + Some(UserSpacePattern { + width, + height, + transform: pattern_transform, + coord_transform, + content_transform, + opacity: self.opacity, + node_with_children, + }) + } +} + +impl UserSpacePattern { + /// Gets the `<pattern>` node that contains the children to be drawn for the pattern's contents. + /// + /// This has to go through [AcquiredNodes] to catch circular references among + /// patterns and their children. + pub fn acquire_pattern_node( + &self, + acquired_nodes: &mut AcquiredNodes<'_>, + ) -> Result<AcquiredNode, AcquireError> { + acquired_nodes.acquire_ref(&self.node_with_children) + } +} + +impl Pattern { + fn get_unresolved(&self, node: &Node) -> Unresolved { + let pattern = UnresolvedPattern { + common: self.common.clone(), + children: UnresolvedChildren::from_node(node), + }; + + Unresolved { + pattern, + fallback: self.fallback.clone(), + } + } + + pub fn resolve( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + opacity: UnitInterval, + session: &Session, + ) -> Result<ResolvedPattern, AcquireError> { + let Unresolved { + mut pattern, + mut fallback, + } = self.get_unresolved(node); + + let mut stack = NodeStack::new(); + + while !pattern.is_resolved() { + if let Some(ref node_id) = fallback { + match acquired_nodes.acquire(node_id) { + Ok(acquired) => { + let acquired_node = acquired.get(); + + if stack.contains(acquired_node) { + return Err(AcquireError::CircularReference(acquired_node.clone())); + } + + match *acquired_node.borrow_element_data() { + ElementData::Pattern(ref p) => { + let unresolved = p.get_unresolved(acquired_node); + pattern = pattern.resolve_from_fallback(&unresolved.pattern); + fallback = unresolved.fallback; + + stack.push(acquired_node); + } + _ => return Err(AcquireError::InvalidLinkType(node_id.clone())), + } + } + + Err(AcquireError::MaxReferencesExceeded) => { + return Err(AcquireError::MaxReferencesExceeded) + } + + Err(e) => { + rsvg_log!(session, "Stopping pattern resolution: {}", e); + pattern = pattern.resolve_from_defaults(); + break; + } + } + } else { + pattern = pattern.resolve_from_defaults(); + break; + } + } + + Ok(pattern.into_resolved(opacity)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::node::NodeData; + use markup5ever::{namespace_url, ns, QualName}; + + #[test] + fn pattern_resolved_from_defaults_is_really_resolved() { + let node = Node::new(NodeData::new_element( + &Session::default(), + &QualName::new(None, ns!(svg), local_name!("pattern")), + Attributes::new(), + )); + + let unresolved = borrow_element_as!(node, Pattern).get_unresolved(&node); + let pattern = unresolved.pattern.resolve_from_defaults(); + assert!(pattern.is_resolved()); + } +} diff --git a/rsvg/src/properties.rs b/rsvg/src/properties.rs new file mode 100644 index 00000000..dd48b76a --- /dev/null +++ b/rsvg/src/properties.rs @@ -0,0 +1,1131 @@ +//! CSS properties, specified values, computed values. +//! +//! To implement support for a CSS property, do the following: +//! +//! * Create a type that will hold the property's values. Please do this in the file +//! `property_defs.rs`; you should cut-and-paste from the existing property definitions or +//! read the documentation of the [`make_property`] macro. You should read the +//! documentation for the [`property_defs`][crate::property_defs] module to see all that +//! is involved in creating a type for a property. +//! +//! * Modify the call to the `make_properties` macro in this module to include the new +//! property's name. +//! +//! * Modify the rest of librsvg wherever the computed value of the property needs to be used. +//! This is available in methods that take an argument of type [`ComputedValues`]. + +use cssparser::{ + self, BasicParseErrorKind, DeclarationListParser, ParseErrorKind, Parser, ParserInput, ToCss, +}; +use markup5ever::{ + expanded_name, local_name, namespace_url, ns, ExpandedName, LocalName, QualName, +}; +use std::collections::HashSet; + +use crate::css::{DeclParser, Declaration, Origin}; +use crate::error::*; +use crate::parsers::{Parse, ParseValue}; +use crate::property_macros::Property; +use crate::session::Session; +use crate::transform::{Transform, TransformAttribute, TransformProperty}; +use crate::xml::Attributes; + +// Re-export the actual properties so they are easy to find from a single place `properties::*`. +pub use crate::font_props::*; +pub use crate::property_defs::*; + +/// Representation of a single CSS property value. +/// +/// `Unspecified` is the `Default`; it means that the corresponding property is not present. +/// +/// `Inherit` means that the property is explicitly set to inherit +/// from the parent element. This is useful for properties which the +/// SVG or CSS specs mandate that should not be inherited by default. +/// +/// `Specified` is a value given by the SVG or CSS stylesheet. This will later be +/// resolved into part of a `ComputedValues` struct. +#[derive(Clone)] +pub enum SpecifiedValue<T> +where + T: Property + Clone + Default, +{ + Unspecified, + Inherit, + Specified(T), +} + +impl<T> SpecifiedValue<T> +where + T: Property + Clone + Default, +{ + pub fn compute(&self, src: &T, src_values: &ComputedValues) -> T { + let value: T = match *self { + SpecifiedValue::Unspecified => { + if <T as Property>::inherits_automatically() { + src.clone() + } else { + Default::default() + } + } + + SpecifiedValue::Inherit => src.clone(), + + SpecifiedValue::Specified(ref v) => v.clone(), + }; + + value.compute(src_values) + } +} + +/// Whether a property also has a presentation attribute. +/// +/// <https://svgwg.org/svg2-draft/styling.html#PresentationAttributes> +#[derive(PartialEq)] +enum PresentationAttr { + No, + Yes, +} + +/// How to parse a value, whether it comes from a property or from a presentation attribute +#[derive(PartialEq)] +pub enum ParseAs { + Property, + PresentationAttr, +} + +impl PropertyId { + fn as_u8(&self) -> u8 { + *self as u8 + } + + fn as_usize(&self) -> usize { + *self as usize + } +} + +/// Holds the specified values for the CSS properties of an element. +#[derive(Clone)] +pub struct SpecifiedValues { + indices: [u8; PropertyId::UnsetProperty as usize], + props: Vec<ParsedProperty>, + + transform: Option<Transform>, +} + +impl Default for SpecifiedValues { + fn default() -> Self { + SpecifiedValues { + // this many elements, with the same value + indices: [PropertyId::UnsetProperty.as_u8(); PropertyId::UnsetProperty as usize], + props: Vec::new(), + transform: None, + } + } +} + +impl ComputedValues { + // TODO for madds: this function will go away, to be replaced by the one generated + // automatically by the macros. + pub fn transform(&self) -> Transform { + self.transform + } + + pub fn is_overflow(&self) -> bool { + matches!(self.overflow(), Overflow::Auto | Overflow::Visible) + } + + /// Whether we should draw the element or skip both space allocation + /// and drawing. + /// <https://www.w3.org/TR/SVG2/render.html#VisibilityControl> + pub fn is_displayed(&self) -> bool { + self.display() != Display::None + } + + /// Whether we should draw the element or allocate its space but + /// skip drawing. + /// <https://www.w3.org/TR/SVG2/render.html#VisibilityControl> + pub fn is_visible(&self) -> bool { + self.visibility() == Visibility::Visible + } +} + +/// Macro to generate all the machinery for properties. +/// +/// This generates the following: +/// +/// * `PropertyId`, an fieldless enum with simple values to identify all the properties. +/// * `ParsedProperty`, a variant enum for all the specified property values. +/// * `ComputedValue`, a variant enum for all the computed values. +/// * `parse_value`, the main function to parse a property or attribute value from user input. +/// +/// There is a lot of repetitive code, for example, because sometimes +/// we need to operate on `PropertyId::Foo`, `ParsedProperty::Foo` and +/// `ComputedValue::Foo` together. This is why all this is done with a macro. +/// +/// See the only invocation of this macro to see how it is used; it is just +/// a declarative list of property names. +/// +/// **NOTE:** If you get a compiler error similar to this: +/// +/// ```text +/// 362 | "mix-blend-mode" => mix_blend_mode : MixBlendMode, +/// | ^^^^^^^^^^^^^^^^ no rules expected this token in macro call +/// ``` +/// +/// Then it may be that you put the name inside the `longhands` block, when it should be +/// inside the `longhands_not_supported_by_markup5ever` block. This is because the +/// [`markup5ever`] crate does not have predefined names for every single property out +/// there; just the common ones. +/// +/// [`markup5ever`]: https://docs.rs/markup5ever +macro_rules! make_properties { + { + shorthands: { + $($short_str:tt => ( $short_presentation_attr:expr, $short_field:ident: $short_name:ident ),)* + } + + longhands: { + $($long_str:tt => ( $long_presentation_attr:expr, $long_field:ident: $long_name:ident ),)+ + } + + // These are for when expanded_name!("" "foo") is not defined yet + // in markup5ever. We create an ExpandedName by hand in that case. + longhands_not_supported_by_markup5ever: { + $($long_m5e_str:tt => ($long_m5e_presentation_attr:expr, $long_m5e_field:ident: $long_m5e_name:ident ),)+ + } + + non_properties: { + $($nonprop_field:ident: $nonprop_name:ident,)+ + } + }=> { + /// Used to match `ParsedProperty` to their discriminant + /// + /// The `PropertyId::UnsetProperty` can be used as a sentinel value, as + /// it does not match any `ParsedProperty` discriminant; it is really the + /// number of valid values in this enum. + #[repr(u8)] + #[derive(Copy, Clone, PartialEq)] + enum PropertyId { + $($short_name,)+ + $($long_name,)+ + $($long_m5e_name,)+ + $($nonprop_name,)+ + + UnsetProperty, + } + + impl PropertyId { + fn is_shorthand(self) -> bool { + match self { + $(PropertyId::$short_name => true,)+ + _ => false, + } + } + } + + /// Embodies "which property is this" plus the property's value + #[derive(Clone)] + pub enum ParsedProperty { + // we put all the properties here; these are for SpecifiedValues + $($short_name(SpecifiedValue<$short_name>),)+ + $($long_name(SpecifiedValue<$long_name>),)+ + $($long_m5e_name(SpecifiedValue<$long_m5e_name>),)+ + $($nonprop_name(SpecifiedValue<$nonprop_name>),)+ + } + + enum ComputedValue { + $( + $long_name($long_name), + )+ + + $( + $long_m5e_name($long_m5e_name), + )+ + + $( + $nonprop_name($nonprop_name), + )+ + } + + /// Holds the computed values for the CSS properties of an element. + #[derive(Debug, Default, Clone)] + pub struct ComputedValues { + $( + $long_field: $long_name, + )+ + + $( + $long_m5e_field: $long_m5e_name, + )+ + + $( + $nonprop_field: $nonprop_name, + )+ + + transform: Transform, + } + + impl ParsedProperty { + fn get_property_id(&self) -> PropertyId { + match *self { + $(ParsedProperty::$long_name(_) => PropertyId::$long_name,)+ + $(ParsedProperty::$long_m5e_name(_) => PropertyId::$long_m5e_name,)+ + $(ParsedProperty::$short_name(_) => PropertyId::$short_name,)+ + $(ParsedProperty::$nonprop_name(_) => PropertyId::$nonprop_name,)+ + } + } + + fn unspecified(id: PropertyId) -> Self { + use SpecifiedValue::Unspecified; + + match id { + $(PropertyId::$long_name => ParsedProperty::$long_name(Unspecified),)+ + $(PropertyId::$long_m5e_name => ParsedProperty::$long_m5e_name(Unspecified),)+ + $(PropertyId::$short_name => ParsedProperty::$short_name(Unspecified),)+ + $(PropertyId::$nonprop_name => ParsedProperty::$nonprop_name(Unspecified),)+ + + PropertyId::UnsetProperty => unreachable!(), + } + } + } + + impl ComputedValues { + $( + pub fn $long_field(&self) -> $long_name { + if let ComputedValue::$long_name(v) = self.get_value(PropertyId::$long_name) { + v + } else { + unreachable!(); + } + } + )+ + + $( + pub fn $long_m5e_field(&self) -> $long_m5e_name { + if let ComputedValue::$long_m5e_name(v) = self.get_value(PropertyId::$long_m5e_name) { + v + } else { + unreachable!(); + } + } + )+ + + $( + pub fn $nonprop_field(&self) -> $nonprop_name { + if let ComputedValue::$nonprop_name(v) = self.get_value(PropertyId::$nonprop_name) { + v + } else { + unreachable!(); + } + } + )+ + + fn set_value(&mut self, computed: ComputedValue) { + match computed { + $(ComputedValue::$long_name(v) => self.$long_field = v,)+ + $(ComputedValue::$long_m5e_name(v) => self.$long_m5e_field = v,)+ + $(ComputedValue::$nonprop_name(v) => self.$nonprop_field = v,)+ + } + } + + fn get_value(&self, id: PropertyId) -> ComputedValue { + assert!(!id.is_shorthand()); + + match id { + $( + PropertyId::$long_name => + ComputedValue::$long_name(self.$long_field.clone()), + )+ + $( + PropertyId::$long_m5e_name => + ComputedValue::$long_m5e_name(self.$long_m5e_field.clone()), + )+ + $( + PropertyId::$nonprop_name => + ComputedValue::$nonprop_name(self.$nonprop_field.clone()), + )+ + _ => unreachable!(), + } + } + } + + /// Parses a value from either a style property or from an element's attribute. + pub fn parse_value<'i>( + prop_name: &QualName, + input: &mut Parser<'i, '_>, + parse_as: ParseAs, + ) -> Result<ParsedProperty, ParseError<'i>> { + match prop_name.expanded() { + $( + expanded_name!("", $long_str) if !(parse_as == ParseAs::PresentationAttr && $long_presentation_attr == PresentationAttr::No) => { + Ok(ParsedProperty::$long_name(parse_input(input)?)) + } + )+ + + $( + e if e == ExpandedName { + ns: &ns!(), + local: &LocalName::from($long_m5e_str), + } && !(parse_as == ParseAs::PresentationAttr && $long_m5e_presentation_attr == PresentationAttr::No) => { + Ok(ParsedProperty::$long_m5e_name(parse_input(input)?)) + } + )+ + + $( + expanded_name!("", $short_str) if parse_as == ParseAs::Property => { + // No shorthand has a presentation attribute. + assert!($short_presentation_attr == PresentationAttr::No); + + Ok(ParsedProperty::$short_name(parse_input(input)?)) + } + )+ + + _ => { + let loc = input.current_source_location(); + Err(loc.new_custom_error(ValueErrorKind::UnknownProperty)) + } + } + } + }; +} + +#[rustfmt::skip] +make_properties! { + shorthands: { + // No shorthand has a presentation attribute. + "font" => (PresentationAttr::No, font : Font), + "marker" => (PresentationAttr::No, marker : Marker), + } + + // longhands that are presentation attributes right now, but need to be turned into properties: + // "d" - applies only to path + + longhands: { + // "alignment-baseline" => (PresentationAttr::Yes, unimplemented), + "baseline-shift" => (PresentationAttr::Yes, baseline_shift : BaselineShift), + "clip-path" => (PresentationAttr::Yes, clip_path : ClipPath), + "clip-rule" => (PresentationAttr::Yes, clip_rule : ClipRule), + "color" => (PresentationAttr::Yes, color : Color), + // "color-interpolation" => (PresentationAttr::Yes, unimplemented), + "color-interpolation-filters" => (PresentationAttr::Yes, color_interpolation_filters : ColorInterpolationFilters), + // "cursor" => (PresentationAttr::Yes, unimplemented), + "cx" => (PresentationAttr::Yes, cx: CX), + "cy" => (PresentationAttr::Yes, cy: CY), + "direction" => (PresentationAttr::Yes, direction : Direction), + "display" => (PresentationAttr::Yes, display : Display), + // "dominant-baseline" => (PresentationAttr::Yes, unimplemented), + "enable-background" => (PresentationAttr::Yes, enable_background : EnableBackground), + + // "applies to any element except animation elements" + // https://www.w3.org/TR/SVG2/styling.html#PresentationAttributes + "fill" => (PresentationAttr::Yes, fill : Fill), + + "fill-opacity" => (PresentationAttr::Yes, fill_opacity : FillOpacity), + "fill-rule" => (PresentationAttr::Yes, fill_rule : FillRule), + "filter" => (PresentationAttr::Yes, filter : Filter), + "flood-color" => (PresentationAttr::Yes, flood_color : FloodColor), + "flood-opacity" => (PresentationAttr::Yes, flood_opacity : FloodOpacity), + "font-family" => (PresentationAttr::Yes, font_family : FontFamily), + "font-size" => (PresentationAttr::Yes, font_size : FontSize), + // "font-size-adjust" => (PresentationAttr::Yes, unimplemented), + "font-stretch" => (PresentationAttr::Yes, font_stretch : FontStretch), + "font-style" => (PresentationAttr::Yes, font_style : FontStyle), + "font-variant" => (PresentationAttr::Yes, font_variant : FontVariant), + "font-weight" => (PresentationAttr::Yes, font_weight : FontWeight), + + // "glyph-orientation-horizontal" - obsolete, removed from SVG2 + + // "glyph-orientation-vertical" - obsolete, now shorthand - + // https://svgwg.org/svg2-draft/text.html#GlyphOrientationVerticalProperty + // https://www.w3.org/TR/css-writing-modes-3/#propdef-glyph-orientation-vertical + // + // Note that even though CSS Writing Modes 3 turned glyph-orientation-vertical + // into a shorthand, SVG1.1 still makes it available as a presentation attribute. + // So, we put the property here, not in the shorthands, and deal with it as a + // special case in the text handling code. + "glyph-orientation-vertical" => (PresentationAttr::Yes, glyph_orientation_vertical : GlyphOrientationVertical), + "height" => (PresentationAttr::Yes, height: Height), + + // "image-rendering" => (PresentationAttr::Yes, unimplemented), + "letter-spacing" => (PresentationAttr::Yes, letter_spacing : LetterSpacing), + "lighting-color" => (PresentationAttr::Yes, lighting_color : LightingColor), + "marker-end" => (PresentationAttr::Yes, marker_end : MarkerEnd), + "marker-mid" => (PresentationAttr::Yes, marker_mid : MarkerMid), + "marker-start" => (PresentationAttr::Yes, marker_start : MarkerStart), + "mask" => (PresentationAttr::Yes, mask : Mask), + "opacity" => (PresentationAttr::Yes, opacity : Opacity), + "overflow" => (PresentationAttr::Yes, overflow : Overflow), + // "pointer-events" => (PresentationAttr::Yes, unimplemented), + "r" => (PresentationAttr::Yes, r: R), + "rx" => (PresentationAttr::Yes, rx: RX), + "ry" => (PresentationAttr::Yes, ry: RY), + "shape-rendering" => (PresentationAttr::Yes, shape_rendering : ShapeRendering), + "stop-color" => (PresentationAttr::Yes, stop_color : StopColor), + "stop-opacity" => (PresentationAttr::Yes, stop_opacity : StopOpacity), + "stroke" => (PresentationAttr::Yes, stroke : Stroke), + "stroke-dasharray" => (PresentationAttr::Yes, stroke_dasharray : StrokeDasharray), + "stroke-dashoffset" => (PresentationAttr::Yes, stroke_dashoffset : StrokeDashoffset), + "stroke-linecap" => (PresentationAttr::Yes, stroke_line_cap : StrokeLinecap), + "stroke-linejoin" => (PresentationAttr::Yes, stroke_line_join : StrokeLinejoin), + "stroke-miterlimit" => (PresentationAttr::Yes, stroke_miterlimit : StrokeMiterlimit), + "stroke-opacity" => (PresentationAttr::Yes, stroke_opacity : StrokeOpacity), + "stroke-width" => (PresentationAttr::Yes, stroke_width : StrokeWidth), + "text-anchor" => (PresentationAttr::Yes, text_anchor : TextAnchor), + "text-decoration" => (PresentationAttr::Yes, text_decoration : TextDecoration), + // "text-overflow" => (PresentationAttr::Yes, unimplemented), + "text-rendering" => (PresentationAttr::Yes, text_rendering : TextRendering), + + // "transform" - Special case as presentation attribute: + // The SVG1.1 "transform" attribute has a different grammar than the + // SVG2 "transform" property. Here we define for the properties machinery, + // and it is handled specially as an attribute in parse_presentation_attributes(). + "transform" => (PresentationAttr::No, transform_property : TransformProperty), + + // "transform-box" => (PresentationAttr::Yes, unimplemented), + // "transform-origin" => (PresentationAttr::Yes, unimplemented), + "unicode-bidi" => (PresentationAttr::Yes, unicode_bidi : UnicodeBidi), + "visibility" => (PresentationAttr::Yes, visibility : Visibility), + // "white-space" => (PresentationAttr::Yes, unimplemented), + // "word-spacing" => (PresentationAttr::Yes, unimplemented), + "width" => (PresentationAttr::Yes, width: Width), + "writing-mode" => (PresentationAttr::Yes, writing_mode : WritingMode), + "x" => (PresentationAttr::Yes, x: X), + "y" => (PresentationAttr::Yes, y: Y), + } + + longhands_not_supported_by_markup5ever: { + "isolation" => (PresentationAttr::No, isolation : Isolation), + "line-height" => (PresentationAttr::No, line_height : LineHeight), + "mask-type" => (PresentationAttr::Yes, mask_type : MaskType), + "mix-blend-mode" => (PresentationAttr::No, mix_blend_mode : MixBlendMode), + "paint-order" => (PresentationAttr::Yes, paint_order : PaintOrder), + "text-orientation" => (PresentationAttr::No, text_orientation : TextOrientation), + "vector-effect" => (PresentationAttr::Yes, vector_effect : VectorEffect), + } + + // These are not properties, but presentation attributes. However, + // both xml:lang and xml:space *do* inherit. We are abusing the + // property inheritance code for these XML-specific attributes. + non_properties: { + xml_lang: XmlLang, + xml_space: XmlSpace, + } +} + +impl SpecifiedValues { + fn property_index(&self, id: PropertyId) -> Option<usize> { + let v = self.indices[id.as_usize()]; + + if v == PropertyId::UnsetProperty.as_u8() { + None + } else { + Some(v as usize) + } + } + + fn set_property(&mut self, prop: &ParsedProperty, replace: bool) { + let id = prop.get_property_id(); + assert!(!id.is_shorthand()); + + if let Some(index) = self.property_index(id) { + if replace { + self.props[index] = prop.clone(); + } + } else { + self.props.push(prop.clone()); + let pos = self.props.len() - 1; + self.indices[id.as_usize()] = pos as u8; + } + } + + fn get_property(&self, id: PropertyId) -> ParsedProperty { + assert!(!id.is_shorthand()); + + if let Some(index) = self.property_index(id) { + self.props[index].clone() + } else { + ParsedProperty::unspecified(id) + } + } + + fn set_property_expanding_shorthands(&mut self, prop: &ParsedProperty, replace: bool) { + match *prop { + ParsedProperty::Font(SpecifiedValue::Specified(ref f)) => { + self.expand_font_shorthand(f, replace) + } + ParsedProperty::Marker(SpecifiedValue::Specified(ref m)) => { + self.expand_marker_shorthand(m, replace) + } + ParsedProperty::Font(SpecifiedValue::Inherit) => { + self.expand_font_shorthand_inherit(replace) + } + ParsedProperty::Marker(SpecifiedValue::Inherit) => { + self.expand_marker_shorthand_inherit(replace) + } + + _ => self.set_property(prop, replace), + } + } + + fn expand_font_shorthand(&mut self, font: &Font, replace: bool) { + let FontSpec { + style, + variant, + weight, + stretch, + size, + line_height, + family, + } = font.to_font_spec(); + + self.set_property( + &ParsedProperty::FontStyle(SpecifiedValue::Specified(style)), + replace, + ); + self.set_property( + &ParsedProperty::FontVariant(SpecifiedValue::Specified(variant)), + replace, + ); + self.set_property( + &ParsedProperty::FontWeight(SpecifiedValue::Specified(weight)), + replace, + ); + self.set_property( + &ParsedProperty::FontStretch(SpecifiedValue::Specified(stretch)), + replace, + ); + self.set_property( + &ParsedProperty::FontSize(SpecifiedValue::Specified(size)), + replace, + ); + self.set_property( + &ParsedProperty::LineHeight(SpecifiedValue::Specified(line_height)), + replace, + ); + self.set_property( + &ParsedProperty::FontFamily(SpecifiedValue::Specified(family)), + replace, + ); + } + + fn expand_marker_shorthand(&mut self, marker: &Marker, replace: bool) { + let Marker(v) = marker; + + self.set_property( + &ParsedProperty::MarkerStart(SpecifiedValue::Specified(MarkerStart(v.clone()))), + replace, + ); + self.set_property( + &ParsedProperty::MarkerMid(SpecifiedValue::Specified(MarkerMid(v.clone()))), + replace, + ); + self.set_property( + &ParsedProperty::MarkerEnd(SpecifiedValue::Specified(MarkerEnd(v.clone()))), + replace, + ); + } + + fn expand_font_shorthand_inherit(&mut self, replace: bool) { + self.set_property(&ParsedProperty::FontStyle(SpecifiedValue::Inherit), replace); + self.set_property( + &ParsedProperty::FontVariant(SpecifiedValue::Inherit), + replace, + ); + self.set_property( + &ParsedProperty::FontWeight(SpecifiedValue::Inherit), + replace, + ); + self.set_property( + &ParsedProperty::FontStretch(SpecifiedValue::Inherit), + replace, + ); + self.set_property(&ParsedProperty::FontSize(SpecifiedValue::Inherit), replace); + self.set_property( + &ParsedProperty::LineHeight(SpecifiedValue::Inherit), + replace, + ); + self.set_property( + &ParsedProperty::FontFamily(SpecifiedValue::Inherit), + replace, + ); + } + + fn expand_marker_shorthand_inherit(&mut self, replace: bool) { + self.set_property( + &ParsedProperty::MarkerStart(SpecifiedValue::Inherit), + replace, + ); + self.set_property(&ParsedProperty::MarkerMid(SpecifiedValue::Inherit), replace); + self.set_property(&ParsedProperty::MarkerEnd(SpecifiedValue::Inherit), replace); + } + + pub fn set_parsed_property(&mut self, prop: &ParsedProperty) { + self.set_property_expanding_shorthands(prop, true); + } + + /* user agent property have less priority than presentation attributes */ + pub fn set_parsed_property_user_agent(&mut self, prop: &ParsedProperty) { + self.set_property_expanding_shorthands(prop, false); + } + + pub fn to_computed_values(&self, computed: &mut ComputedValues) { + macro_rules! compute { + ($name:ident, $field:ident) => {{ + // This extra block --------^ + // is so that prop_val will be dropped within the macro invocation; + // otherwise all the temporary values cause this function to use + // an unreasonably large amount of stack space. + let prop_val = self.get_property(PropertyId::$name); + if let ParsedProperty::$name(s) = prop_val { + computed.set_value(ComputedValue::$name( + s.compute(&computed.$field(), computed), + )); + } else { + unreachable!(); + } + }}; + } + + // First, compute font_size. It needs to be done before everything + // else, so that properties that depend on its computed value + // will be able to use it. For example, baseline-shift + // depends on font-size. + + compute!(FontSize, font_size); + + // Then, do all the other properties. + + compute!(BaselineShift, baseline_shift); + compute!(ClipPath, clip_path); + compute!(ClipRule, clip_rule); + compute!(Color, color); + compute!(ColorInterpolationFilters, color_interpolation_filters); + compute!(CX, cx); + compute!(CY, cy); + compute!(Direction, direction); + compute!(Display, display); + compute!(EnableBackground, enable_background); + compute!(Fill, fill); + compute!(FillOpacity, fill_opacity); + compute!(FillRule, fill_rule); + compute!(Filter, filter); + compute!(FloodColor, flood_color); + compute!(FloodOpacity, flood_opacity); + compute!(FontFamily, font_family); + compute!(FontStretch, font_stretch); + compute!(FontStyle, font_style); + compute!(FontVariant, font_variant); + compute!(FontWeight, font_weight); + compute!(GlyphOrientationVertical, glyph_orientation_vertical); + compute!(Height, height); + compute!(Isolation, isolation); + compute!(LetterSpacing, letter_spacing); + compute!(LightingColor, lighting_color); + compute!(MarkerEnd, marker_end); + compute!(MarkerMid, marker_mid); + compute!(MarkerStart, marker_start); + compute!(Mask, mask); + compute!(MaskType, mask_type); + compute!(MixBlendMode, mix_blend_mode); + compute!(Opacity, opacity); + compute!(Overflow, overflow); + compute!(PaintOrder, paint_order); + compute!(R, r); + compute!(RX, rx); + compute!(RY, ry); + compute!(ShapeRendering, shape_rendering); + compute!(StopColor, stop_color); + compute!(StopOpacity, stop_opacity); + compute!(Stroke, stroke); + compute!(StrokeDasharray, stroke_dasharray); + compute!(StrokeDashoffset, stroke_dashoffset); + compute!(StrokeLinecap, stroke_line_cap); + compute!(StrokeLinejoin, stroke_line_join); + compute!(StrokeOpacity, stroke_opacity); + compute!(StrokeMiterlimit, stroke_miterlimit); + compute!(StrokeWidth, stroke_width); + compute!(TextAnchor, text_anchor); + compute!(TextDecoration, text_decoration); + compute!(TextOrientation, text_orientation); + compute!(TextRendering, text_rendering); + compute!(TransformProperty, transform_property); + compute!(UnicodeBidi, unicode_bidi); + compute!(VectorEffect, vector_effect); + compute!(Visibility, visibility); + compute!(Width, width); + compute!(WritingMode, writing_mode); + compute!(X, x); + compute!(XmlSpace, xml_space); + compute!(XmlLang, xml_lang); + compute!(Y, y); + + computed.transform = self.transform.unwrap_or_else(|| { + match self.get_property(PropertyId::TransformProperty) { + ParsedProperty::TransformProperty(SpecifiedValue::Specified(ref t)) => { + t.to_transform() + } + _ => Transform::identity(), + } + }); + } + + /// This is a somewhat egregious hack to allow xml:lang to be stored as a presentational + /// attribute. Presentational attributes can often be influenced by stylesheets, + /// so they're cascaded after selector matching is done, but xml:lang can be queried by + /// CSS selectors, so they need to be cascaded *first*. + pub fn inherit_xml_lang( + &self, + computed: &mut ComputedValues, + parent: Option<crate::node::Node>, + ) { + use crate::node::NodeBorrow; + let prop_val = self.get_property(PropertyId::XmlLang); + if let ParsedProperty::XmlLang(s) = prop_val { + if let Some(parent) = parent { + computed.set_value(ComputedValue::XmlLang( + parent.borrow_element().get_computed_values().xml_lang(), + )); + } + computed.set_value(ComputedValue::XmlLang( + s.compute(&computed.xml_lang(), computed), + )); + } else { + unreachable!(); + } + } + + pub fn is_overflow(&self) -> bool { + if let Some(overflow_index) = self.property_index(PropertyId::Overflow) { + match self.props[overflow_index] { + ParsedProperty::Overflow(SpecifiedValue::Specified(Overflow::Auto)) => true, + ParsedProperty::Overflow(SpecifiedValue::Specified(Overflow::Visible)) => true, + ParsedProperty::Overflow(_) => false, + _ => unreachable!(), + } + } else { + false + } + } + + fn parse_one_presentation_attribute(&mut self, session: &Session, attr: QualName, value: &str) { + let mut input = ParserInput::new(value); + let mut parser = Parser::new(&mut input); + + match parse_value(&attr, &mut parser, ParseAs::PresentationAttr) { + Ok(prop) => { + if parser.expect_exhausted().is_ok() { + self.set_parsed_property(&prop); + } else { + rsvg_log!( + session, + "(ignoring invalid presentation attribute {:?}\n value=\"{}\")\n", + attr.expanded(), + value, + ); + } + } + + // not a presentation attribute; just ignore it + Err(ParseError { + kind: ParseErrorKind::Custom(ValueErrorKind::UnknownProperty), + .. + }) => (), + + // https://www.w3.org/TR/CSS2/syndata.html#unsupported-values + // For all the following cases, ignore illegal values; don't set the whole node to + // be in error in that case. + Err(ParseError { + kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(ref t)), + .. + }) => { + let mut tok = String::new(); + + t.to_css(&mut tok).unwrap(); // FIXME: what do we do with a fmt::Error? + rsvg_log!( + session, + "(ignoring invalid presentation attribute {:?}\n value=\"{}\"\n \ + unexpected token '{}')", + attr.expanded(), + value, + tok, + ); + } + + Err(ParseError { + kind: ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput), + .. + }) => { + rsvg_log!( + session, + "(ignoring invalid presentation attribute {:?}\n value=\"{}\"\n \ + unexpected end of input)", + attr.expanded(), + value, + ); + } + + Err(ParseError { + kind: ParseErrorKind::Basic(_), + .. + }) => { + rsvg_log!( + session, + "(ignoring invalid presentation attribute {:?}\n value=\"{}\"\n \ + unexpected error)", + attr.expanded(), + value, + ); + } + + Err(ParseError { + kind: ParseErrorKind::Custom(ref v), + .. + }) => { + rsvg_log!( + session, + "(ignoring invalid presentation attribute {:?}\n value=\"{}\"\n {})", + attr.expanded(), + value, + v + ); + } + } + } + + pub fn parse_presentation_attributes(&mut self, session: &Session, attrs: &Attributes) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "transform") => { + // FIXME: we parse the transform attribute here because we don't yet have + // a better way to distinguish attributes whose values have different + // grammars than properties. + let transform_attr = TransformAttribute::parse_str(value) + .unwrap_or_else(|_| TransformAttribute::default()); + self.transform = Some(transform_attr.to_transform()); + } + + expanded_name!(xml "lang") => { + // xml:lang is a non-presentation attribute and as such cannot have the + // "inherit" value. So, we don't call parse_one_presentation_attribute() + // for it, but rather call its parser directly. + let parse_result: Result<XmlLang, _> = attr.parse(value); + match parse_result { + Ok(lang) => { + self.set_parsed_property(&ParsedProperty::XmlLang( + SpecifiedValue::Specified(lang), + )); + } + + Err(e) => { + rsvg_log!(session, "ignoring attribute with invalid value: {}", e); + } + } + } + + expanded_name!(xml "space") => { + // xml:space is a non-presentation attribute and as such cannot have the + // "inherit" value. So, we don't call parse_one_presentation_attribute() + // for it, but rather call its parser directly. + let parse_result: Result<XmlSpace, _> = attr.parse(value); + match parse_result { + Ok(space) => { + self.set_parsed_property(&ParsedProperty::XmlSpace( + SpecifiedValue::Specified(space), + )); + } + + Err(e) => { + rsvg_log!(session, "ignoring attribute with invalid value: {}", e); + } + } + } + + _ => self.parse_one_presentation_attribute(session, attr, value), + } + } + } + + pub fn set_property_from_declaration( + &mut self, + declaration: &Declaration, + origin: Origin, + important_styles: &mut HashSet<QualName>, + ) { + if !declaration.important && important_styles.contains(&declaration.prop_name) { + return; + } + + if declaration.important { + important_styles.insert(declaration.prop_name.clone()); + } + + if origin == Origin::UserAgent { + self.set_parsed_property_user_agent(&declaration.property); + } else { + self.set_parsed_property(&declaration.property); + } + } + + pub fn parse_style_declarations( + &mut self, + declarations: &str, + origin: Origin, + important_styles: &mut HashSet<QualName>, + session: &Session, + ) { + let mut input = ParserInput::new(declarations); + let mut parser = Parser::new(&mut input); + + DeclarationListParser::new(&mut parser, DeclParser) + .filter_map(|r| match r { + Ok(decl) => Some(decl), + Err(e) => { + rsvg_log!(session, "Invalid declaration; ignoring: {:?}", e); + None + } + }) + .for_each(|decl| self.set_property_from_declaration(&decl, origin, important_styles)); + } +} + +// Parses the value for the type `T` of the property out of the Parser, including `inherit` values. +fn parse_input<'i, T>(input: &mut Parser<'i, '_>) -> Result<SpecifiedValue<T>, ParseError<'i>> +where + T: Property + Clone + Default + Parse, +{ + if input + .try_parse(|p| p.expect_ident_matching("inherit")) + .is_ok() + { + Ok(SpecifiedValue::Inherit) + } else { + Parse::parse(input).map(SpecifiedValue::Specified) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::iri::Iri; + use crate::length::*; + + #[test] + fn empty_values_computes_to_defaults() { + let specified = SpecifiedValues::default(); + + let mut computed = ComputedValues::default(); + specified.to_computed_values(&mut computed); + + assert_eq!(computed.stroke_width(), StrokeWidth::default()); + } + + #[test] + fn set_one_property() { + let length = Length::<Both>::new(42.0, LengthUnit::Px); + + let mut specified = SpecifiedValues::default(); + specified.set_parsed_property(&ParsedProperty::StrokeWidth(SpecifiedValue::Specified( + StrokeWidth(length), + ))); + + let mut computed = ComputedValues::default(); + specified.to_computed_values(&mut computed); + + assert_eq!(computed.stroke_width(), StrokeWidth(length)); + } + + #[test] + fn replace_existing_property() { + let length1 = Length::<Both>::new(42.0, LengthUnit::Px); + let length2 = Length::<Both>::new(42.0, LengthUnit::Px); + + let mut specified = SpecifiedValues::default(); + + specified.set_parsed_property(&ParsedProperty::StrokeWidth(SpecifiedValue::Specified( + StrokeWidth(length1), + ))); + + specified.set_parsed_property(&ParsedProperty::StrokeWidth(SpecifiedValue::Specified( + StrokeWidth(length2), + ))); + + let mut computed = ComputedValues::default(); + specified.to_computed_values(&mut computed); + + assert_eq!(computed.stroke_width(), StrokeWidth(length2)); + } + + #[test] + fn expands_marker_shorthand() { + let mut specified = SpecifiedValues::default(); + let iri = Iri::parse_str("url(#foo)").unwrap(); + + let marker = Marker(iri.clone()); + specified.set_parsed_property(&ParsedProperty::Marker(SpecifiedValue::Specified(marker))); + + let mut computed = ComputedValues::default(); + specified.to_computed_values(&mut computed); + + assert_eq!(computed.marker_start(), MarkerStart(iri.clone())); + assert_eq!(computed.marker_mid(), MarkerMid(iri.clone())); + assert_eq!(computed.marker_end(), MarkerEnd(iri.clone())); + } + + #[test] + fn replaces_marker_shorthand() { + let mut specified = SpecifiedValues::default(); + let iri1 = Iri::parse_str("url(#foo)").unwrap(); + let iri2 = Iri::None; + + let marker1 = Marker(iri1.clone()); + specified.set_parsed_property(&ParsedProperty::Marker(SpecifiedValue::Specified(marker1))); + + let marker2 = Marker(iri2.clone()); + specified.set_parsed_property(&ParsedProperty::Marker(SpecifiedValue::Specified(marker2))); + + let mut computed = ComputedValues::default(); + specified.to_computed_values(&mut computed); + + assert_eq!(computed.marker_start(), MarkerStart(iri2.clone())); + assert_eq!(computed.marker_mid(), MarkerMid(iri2.clone())); + assert_eq!(computed.marker_end(), MarkerEnd(iri2.clone())); + } + + #[test] + fn computes_property_that_does_not_inherit_automatically() { + assert_eq!(<Opacity as Property>::inherits_automatically(), false); + + let half_opacity = Opacity::parse_str("0.5").unwrap(); + + // first level, as specified with opacity + + let mut with_opacity = SpecifiedValues::default(); + with_opacity.set_parsed_property(&ParsedProperty::Opacity(SpecifiedValue::Specified( + half_opacity.clone(), + ))); + + let mut computed_0_5 = ComputedValues::default(); + with_opacity.to_computed_values(&mut computed_0_5); + + assert_eq!(computed_0_5.opacity(), half_opacity.clone()); + + // second level, no opacity specified, and it doesn't inherit + + let without_opacity = SpecifiedValues::default(); + + let mut computed = computed_0_5.clone(); + without_opacity.to_computed_values(&mut computed); + + assert_eq!(computed.opacity(), Opacity::default()); + + // another at second level, opacity set to explicitly inherit + + let mut with_inherit_opacity = SpecifiedValues::default(); + with_inherit_opacity.set_parsed_property(&ParsedProperty::Opacity(SpecifiedValue::Inherit)); + + let mut computed = computed_0_5.clone(); + with_inherit_opacity.to_computed_values(&mut computed); + + assert_eq!(computed.opacity(), half_opacity.clone()); + } +} diff --git a/rsvg/src/property_defs.rs b/rsvg/src/property_defs.rs new file mode 100644 index 00000000..0018bab2 --- /dev/null +++ b/rsvg/src/property_defs.rs @@ -0,0 +1,1328 @@ +//! Definitions for CSS property types. +//! +//! Do not import things directly from this module; use the `properties` module instead, +//! which re-exports things from here. +//! +//! This module defines most of the CSS property types that librsvg supports. Each +//! property requires a Rust type that will hold its values, and that type should +//! implement a few traits, as follows. +//! +//! # Requirements for a property type +//! +//! You should call the [`make_property`] macro to take care of most of these requirements +//! automatically: +//! +//! * A name for the type. For example, the `fill` property has a [`Fill`] type defined +//! in this module. +//! +//! * An initial value per the CSS or SVG specs, given through an implementation of the +//! [`Default`] trait. +//! +//! * Whether the property's computed value inherits to child elements, given through an +//! implementation of the [`Property`] trait and its +//! [`inherits_automatically`][Property::inherits_automatically] method. +//! +//! * A way to derive the CSS *computed value* for the property, given through an +//! implementation of the [`Property`] trait and its [`compute`][Property::compute] method. +//! +//! * The actual underlying type. For example, the [`make_property`] macro can generate a +//! field-less enum for properties like the `clip-rule` property, which just has +//! identifier-based values like `nonzero` and `evenodd`. For general-purpose types like +//! [`Length`], the macro can wrap them in a newtype like `struct` +//! [`StrokeWidth`]`(`[`Length`]`)`. For custom types, the macro call can be used just to +//! define the initial/default value and whether the property inherits automatically; you +//! should provide the other required trait implementations separately. +//! +//! * An implementation of the [`Parse`] trait for the underlying type. +use std::convert::TryInto; +use std::str::FromStr; + +use cssparser::{Parser, Token}; +use language_tags::LanguageTag; + +use crate::dasharray::Dasharray; +use crate::error::*; +use crate::filter::FilterValueList; +use crate::font_props::{ + Font, FontFamily, FontSize, FontWeight, GlyphOrientationVertical, LetterSpacing, LineHeight, +}; +use crate::iri::Iri; +use crate::length::*; +use crate::paint_server::PaintServer; +use crate::parsers::Parse; +use crate::properties::ComputedValues; +use crate::property_macros::Property; +use crate::rect::Rect; +use crate::transform::TransformProperty; +use crate::unit_interval::UnitInterval; + +make_property!( + /// `baseline-shift` property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/text.html#BaselineShiftProperty> + /// + /// SVG2: <https://www.w3.org/TR/SVG2/text.html#BaselineShiftProperty> + BaselineShift, + default: Length::<Both>::parse_str("0.0").unwrap(), + newtype: Length<Both>, + property_impl: { + impl Property for BaselineShift { + fn inherits_automatically() -> bool { + false + } + + fn compute(&self, v: &ComputedValues) -> Self { + let font_size = v.font_size().value(); + let parent = v.baseline_shift(); + + match (self.0.unit, parent.0.unit) { + (LengthUnit::Percent, _) => { + BaselineShift(Length::<Both>::new(self.0.length * font_size.length + parent.0.length, font_size.unit)) + } + + (x, y) if x == y || parent.0.length == 0.0 => { + BaselineShift(Length::<Both>::new(self.0.length + parent.0.length, self.0.unit)) + } + + _ => { + // FIXME: the limitation here is that the parent's baseline_shift + // and ours have different units. We should be able to normalize + // the lengths and add them even if they have different units, but + // at the moment that requires access to the draw_ctx, which we + // don't have here. + // + // So for now we won't add to the parent's baseline_shift. + + parent + } + } + } + } + }, + parse_impl: { + impl Parse for BaselineShift { + // These values come from Inkscape's SP_CSS_BASELINE_SHIFT_(SUB/SUPER/BASELINE); + // see sp_style_merge_baseline_shift_from_parent() + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<BaselineShift, crate::error::ParseError<'i>> { + parser.try_parse(|p| Ok(BaselineShift(Length::<Both>::parse(p)?))) + .or_else(|_: ParseError<'_>| { + Ok(parse_identifiers!( + parser, + "baseline" => BaselineShift(Length::<Both>::new(0.0, LengthUnit::Percent)), + "sub" => BaselineShift(Length::<Both>::new(-0.2, LengthUnit::Percent)), + + "super" => BaselineShift(Length::<Both>::new(0.4, LengthUnit::Percent)), + )?) + }) + } + } + } +); + +make_property!( + /// `clip-path` property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/masking.html#ClipPathProperty> + /// + /// CSS Masking 1: <https://www.w3.org/TR/css-masking-1/#the-clip-path> + ClipPath, + default: Iri::None, + inherits_automatically: false, + newtype_parse: Iri, +); + +make_property!( + /// `clip-rule` property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/masking.html#ClipRuleProperty> + /// + /// CSS Masking 1: <https://www.w3.org/TR/css-masking-1/#the-clip-rule> + ClipRule, + default: NonZero, + inherits_automatically: true, + + identifiers: + "nonzero" => NonZero, + "evenodd" => EvenOdd, +); + +make_property!( + /// `color` property, the fallback for `currentColor` values. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/color.html#ColorProperty> + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#ColorProperty> + /// + /// The SVG spec allows the user agent to choose its own initial value for the "color" + /// property. Here we start with opaque black for the initial value. Clients can + /// override this by specifing a custom CSS stylesheet. + /// + /// Most of the time the `color` property is used to call + /// [`crate::paint_server::resolve_color`]. + Color, + default: cssparser::RGBA::new(0, 0, 0, 0xff), + inherits_automatically: true, + newtype_parse: cssparser::RGBA, +); + +make_property!( + /// `color-interpolation-filters` property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/painting.html#ColorInterpolationFiltersProperty> + /// + /// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#propdef-color-interpolation-filters> + ColorInterpolationFilters, + default: LinearRgb, + inherits_automatically: true, + + identifiers: + "auto" => Auto, + "linearRGB" => LinearRgb, + "sRGB" => Srgb, +); + +make_property!( + /// `cx` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/geometry.html#CX> + /// + /// Note that in SVG1.1, this was an attribute, not a property. + CX, + default: Length::<Horizontal>::parse_str("0").unwrap(), + inherits_automatically: false, + newtype_parse: Length<Horizontal>, +); + +make_property!( + /// `cy` attribute. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/geometry.html#CY> + /// + /// Note that in SVG1.1, this was an attribute, not a property. + CY, + default: Length::<Vertical>::parse_str("0").unwrap(), + inherits_automatically: false, + newtype_parse: Length<Vertical>, +); + +make_property!( + /// `direction` property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/text.html#DirectionProperty> + /// + /// SVG2: <https://www.w3.org/TR/SVG2/text.html#DirectionProperty> + Direction, + default: Ltr, + inherits_automatically: true, + + identifiers: + "ltr" => Ltr, + "rtl" => Rtl, +); + +make_property!( + /// `display` property. + /// + /// SVG1.1: <https://www.w3.org/TR/CSS2/visuren.html#display-prop> + /// + /// SVG2: <https://www.w3.org/TR/SVG2/render.html#VisibilityControl> + Display, + default: Inline, + inherits_automatically: false, + + identifiers: + "inline" => Inline, + "block" => Block, + "list-item" => ListItem, + "run-in" => RunIn, + "compact" => Compact, + "marker" => Marker, + "table" => Table, + "inline-table" => InlineTable, + "table-row-group" => TableRowGroup, + "table-header-group" => TableHeaderGroup, + "table-footer-group" => TableFooterGroup, + "table-row" => TableRow, + "table-column-group" => TableColumnGroup, + "table-column" => TableColumn, + "table-cell" => TableCell, + "table-caption" => TableCaption, + "none" => None, +); + +/// `enable-background` property. +/// +/// SVG1.1: <https://www.w3.org/TR/SVG11/filters.html#EnableBackgroundProperty> +/// +/// This is deprecated in SVG2. We just have a parser for it to avoid setting elements in +/// error if they have this property. Librsvg does not use the value of this property. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum EnableBackground { + Accumulate, + New(Option<Rect>), +} + +make_property!( + EnableBackground, + default: EnableBackground::Accumulate, + inherits_automatically: false, + + parse_impl: { + impl Parse for EnableBackground { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, crate::error::ParseError<'i>> { + let loc = parser.current_source_location(); + + if parser + .try_parse(|p| p.expect_ident_matching("accumulate")) + .is_ok() + { + return Ok(EnableBackground::Accumulate); + } + + if parser.try_parse(|p| p.expect_ident_matching("new")).is_ok() { + parser.try_parse(|p| -> Result<_, ParseError<'_>> { + let x = f64::parse(p)?; + let y = f64::parse(p)?; + let w = f64::parse(p)?; + let h = f64::parse(p)?; + + Ok(EnableBackground::New(Some(Rect::new(x, y, x + w, y + h)))) + }).or(Ok(EnableBackground::New(None))) + } else { + Err(loc.new_custom_error(ValueErrorKind::parse_error("invalid syntax for 'enable-background' property"))) + } + } + } + + } +); + +#[cfg(test)] +#[test] +fn parses_enable_background() { + assert_eq!( + EnableBackground::parse_str("accumulate").unwrap(), + EnableBackground::Accumulate + ); + + assert_eq!( + EnableBackground::parse_str("new").unwrap(), + EnableBackground::New(None) + ); + + assert_eq!( + EnableBackground::parse_str("new 1 2 3 4").unwrap(), + EnableBackground::New(Some(Rect::new(1.0, 2.0, 4.0, 6.0))) + ); + + assert!(EnableBackground::parse_str("new foo").is_err()); + + assert!(EnableBackground::parse_str("plonk").is_err()); +} + +make_property!( + /// `fill` property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/painting.html#FillProperty> + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#FillProperty> + Fill, + default: PaintServer::parse_str("#000").unwrap(), + inherits_automatically: true, + newtype_parse: PaintServer, +); + +make_property!( + /// `fill-opacity` property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/painting.html#FillOpacityProperty> + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#FillOpacity> + FillOpacity, + default: UnitInterval(1.0), + inherits_automatically: true, + newtype_parse: UnitInterval, +); + +make_property!( + /// `fill-rule` property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/painting.html#FillRuleProperty> + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#WindingRule> + FillRule, + default: NonZero, + inherits_automatically: true, + + identifiers: + "nonzero" => NonZero, + "evenodd" => EvenOdd, +); + +/// `filter` property. +/// +/// SVG1.1: <https://www.w3.org/TR/SVG11/filters.html#FilterProperty> +/// +/// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#FilterProperty> +/// +/// Note that in SVG2, the filters got offloaded to the [Filter Effects Module Level +/// 1](https://www.w3.org/TR/filter-effects/) specification. +#[derive(Debug, Clone, PartialEq)] +pub enum Filter { + None, + List(FilterValueList), +} + +make_property!( + Filter, + default: Filter::None, + inherits_automatically: false, + parse_impl: { + impl Parse for Filter { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, crate::error::ParseError<'i>> { + + if parser + .try_parse(|p| p.expect_ident_matching("none")) + .is_ok() + { + return Ok(Filter::None); + } + + Ok(Filter::List(FilterValueList::parse(parser)?)) + } + } + } +); + +make_property!( + /// `flood-color` property, for `feFlood` and `feDropShadow` filter elements. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/filters.html#feFloodElement> + /// + /// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#FloodColorProperty> + FloodColor, + default: cssparser::Color::RGBA(cssparser::RGBA::new(0, 0, 0, 255)), + inherits_automatically: false, + newtype_parse: cssparser::Color, +); + +make_property!( + /// `flood-opacity` property, for `feFlood` and `feDropShadow` filter elements. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/filters.html#feFloodElement> + /// + /// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#FloodOpacityProperty> + FloodOpacity, + default: UnitInterval(1.0), + inherits_automatically: false, + newtype_parse: UnitInterval, +); + +make_property!( + // docs are in font_props.rs + Font, + default: Font::Spec(Default::default()), + inherits_automatically: true, +); + +make_property!( + // docs are in font_props.rs + FontFamily, + default: FontFamily("Times New Roman".to_string()), + inherits_automatically: true, +); + +make_property!( + // docs are in font_props.rs + FontSize, + default: FontSize::Value(Length::<Both>::parse_str("12.0").unwrap()), + property_impl: { + impl Property for FontSize { + fn inherits_automatically() -> bool { + true + } + + fn compute(&self, v: &ComputedValues) -> Self { + self.compute(v) + } + } + } +); + +make_property!( + /// `font-stretch` property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/text.html#FontStretchProperty> + /// + /// CSS Fonts 3: <https://www.w3.org/TR/css-fonts-3/#font-size-propstret> + FontStretch, + default: Normal, + inherits_automatically: true, + + identifiers: + "normal" => Normal, + "wider" => Wider, + "narrower" => Narrower, + "ultra-condensed" => UltraCondensed, + "extra-condensed" => ExtraCondensed, + "condensed" => Condensed, + "semi-condensed" => SemiCondensed, + "semi-expanded" => SemiExpanded, + "expanded" => Expanded, + "extra-expanded" => ExtraExpanded, + "ultra-expanded" => UltraExpanded, +); + +make_property!( + /// `font-style` property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/text.html#FontStyleProperty> + /// + /// CSS Fonts 3: <https://www.w3.org/TR/css-fonts-3/#font-size-propstret> + FontStyle, + default: Normal, + inherits_automatically: true, + + identifiers: + "normal" => Normal, + "italic" => Italic, + "oblique" => Oblique, +); + +make_property!( + /// `font-variant` property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/text.html#FontVariantProperty> + /// + /// CSS Fonts 3: <https://www.w3.org/TR/css-fonts-3/#propdef-font-variant> + /// + /// Note that in CSS3, this is a lot more complex than CSS2.1 / SVG1.1. + FontVariant, + default: Normal, + inherits_automatically: true, + + identifiers: + "normal" => Normal, + "small-caps" => SmallCaps, +); + +make_property!( + // docs are in font_props.rs + FontWeight, + default: FontWeight::Normal, + property_impl: { + impl Property for FontWeight { + fn inherits_automatically() -> bool { + true + } + + fn compute(&self, v: &ComputedValues) -> Self { + self.compute(&v.font_weight()) + } + } + } +); + +make_property!( + // docs are in font_props.rs + // + // Although https://www.w3.org/TR/css-writing-modes-3/#propdef-glyph-orientation-vertical specifies + // "n/a" for both the initial value (default) and inheritance, we'll use Auto here for the default, + // since it translates to TextOrientation::Mixed - which is text-orientation's initial value. + GlyphOrientationVertical, + default: GlyphOrientationVertical::Auto, + inherits_automatically: false, +); + +make_property!( + /// `height` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/geometry.html#Sizing> + /// + /// Note that in SVG1.1, this was an attribute, not a property. + Height, + default: LengthOrAuto::<Vertical>::Auto, + inherits_automatically: false, + newtype_parse: LengthOrAuto<Vertical>, +); + +make_property!( + /// `isolation` property. + /// + /// CSS Compositing and Blending 1: <https://www.w3.org/TR/compositing-1/#isolation> + Isolation, + default: Auto, + inherits_automatically: false, + + identifiers: + "auto" => Auto, + "isolate" => Isolate, +); + +make_property!( + // docs are in font_props.rs + LetterSpacing, + default: LetterSpacing::Normal, + property_impl: { + impl Property for LetterSpacing { + fn inherits_automatically() -> bool { + true + } + + fn compute(&self, _v: &ComputedValues) -> Self { + self.compute() + } + } + } +); + +make_property!( + // docs are in font_props.rs + LineHeight, + default: LineHeight::Normal, + inherits_automatically: true, +); + +make_property!( + /// `lighting-color` property for `feDiffuseLighting` and `feSpecularLighting` filter elements. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/filters.html#LightingColorProperty> + /// + /// Filter Effects 1: <https://www.w3.org/TR/filter-effects/#LightingColorProperty> + LightingColor, + default: cssparser::Color::RGBA(cssparser::RGBA::new(255, 255, 255, 255)), + inherits_automatically: false, + newtype_parse: cssparser::Color, +); + +make_property!( + /// `marker` shorthand property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#MarkerShorthand> + /// + /// This is a shorthand, which expands to the `marker-start`, `marker-mid`, + /// `marker-end` longhand properties. + Marker, + default: Iri::None, + inherits_automatically: true, + newtype_parse: Iri, +); + +make_property!( + /// `marker-end` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#VertexMarkerProperties> + MarkerEnd, + default: Iri::None, + inherits_automatically: true, + newtype_parse: Iri, +); + +make_property!( + /// `marker-mid` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#VertexMarkerProperties> + MarkerMid, + default: Iri::None, + inherits_automatically: true, + newtype_parse: Iri, +); + +make_property!( + /// `marker-start` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#VertexMarkerProperties> + MarkerStart, + default: Iri::None, + inherits_automatically: true, + newtype_parse: Iri, +); + +make_property!( + /// `mask` shorthand property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/masking.html#MaskProperty> + /// + /// CSS Masking 1: <https://www.w3.org/TR/css-masking-1/#the-mask> + /// + /// Note that librsvg implements SVG1.1 semantics, where this is not a shorthand. + Mask, + default: Iri::None, + inherits_automatically: false, + newtype_parse: Iri, +); + +make_property!( + /// `mask-type` property. + /// + /// CSS Masking 1: <https://www.w3.org/TR/css-masking-1/#the-mask-type> + MaskType, + default: Luminance, + inherits_automatically: false, + + identifiers: + "luminance" => Luminance, + "alpha" => Alpha, +); + +make_property!( + /// `mix-blend-mode` property. + /// + /// Compositing and Blending 1: <https://www.w3.org/TR/compositing/#mix-blend-mode> + MixBlendMode, + default: Normal, + inherits_automatically: false, + + identifiers: + "normal" => Normal, + "multiply" => Multiply, + "screen" => Screen, + "overlay" => Overlay, + "darken" => Darken, + "lighten" => Lighten, + "color-dodge" => ColorDodge, + "color-burn" => ColorBurn, + "hard-light" => HardLight, + "soft-light" => SoftLight, + "difference" => Difference, + "exclusion" => Exclusion, + "hue" => Hue, + "saturation" => Saturation, + "color" => Color, + "luminosity" => Luminosity, +); + +make_property!( + /// `opacity` property. + /// + /// CSS Color 3: <https://www.w3.org/TR/css-color-3/#opacity> + Opacity, + default: UnitInterval(1.0), + inherits_automatically: false, + newtype_parse: UnitInterval, +); + +make_property!( + /// `overflow` shorthand property. + /// + /// CSS2: <https://www.w3.org/TR/CSS2/visufx.html#overflow> + /// + /// CSS Overflow 3: <https://www.w3.org/TR/css-overflow-3/#propdef-overflow> + /// + /// Note that librsvg implements SVG1.1 semantics, where this is not a shorthand. + Overflow, + default: Visible, + inherits_automatically: false, + + identifiers: + "visible" => Visible, + "hidden" => Hidden, + "scroll" => Scroll, + "auto" => Auto, +); + +/// One of the three operations for the `paint-order` property; see [`PaintOrder`]. +#[repr(u8)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum PaintTarget { + Fill, + Stroke, + Markers, +} + +make_property!( + /// `paint-order` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#PaintOrder> + /// + /// The `targets` field specifies the order in which graphic elements should be filled/stroked. + /// Instead of hard-coding an order of fill/stroke/markers, use the order specified by the `targets`. + PaintOrder, + inherits_automatically: true, + fields: { + targets: [PaintTarget; 3], default: [PaintTarget::Fill, PaintTarget::Stroke, PaintTarget::Markers], + } + + parse_impl: { + impl Parse for PaintOrder { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<PaintOrder, ParseError<'i>> { + let allowed_targets = 3; + let mut targets = Vec::with_capacity(allowed_targets); + + if parser.try_parse(|p| p.expect_ident_matching("normal")).is_ok() { + return Ok(PaintOrder::default()); + } + + while !parser.is_exhausted() { + let loc = parser.current_source_location(); + let token = parser.next()?; + + let value = match token { + Token::Ident(ref cow) if cow.eq_ignore_ascii_case("fill") && !targets.contains(&PaintTarget::Fill) => PaintTarget::Fill, + Token::Ident(ref cow) if cow.eq_ignore_ascii_case("stroke") && !targets.contains(&PaintTarget::Stroke) => PaintTarget::Stroke, + Token::Ident(ref cow) if cow.eq_ignore_ascii_case("markers") && !targets.contains(&PaintTarget::Markers) => PaintTarget::Markers, + _ => return Err(loc.new_basic_unexpected_token_error(token.clone()).into()), + }; + + targets.push(value); + }; + + // any values which were not specfied should be painted in default order + // (fill, stroke, markers) following the values which were explicitly specified. + for &target in &[PaintTarget::Fill, PaintTarget::Stroke, PaintTarget::Markers] { + if !targets.contains(&target) { + targets.push(target); + } + } + Ok(PaintOrder { + targets: targets[..].try_into().expect("Incorrect number of targets in paint-order") + }) + } + } + } +); + +#[cfg(test)] +#[test] +fn parses_paint_order() { + assert_eq!( + PaintOrder::parse_str("normal").unwrap(), + PaintOrder { + targets: [PaintTarget::Fill, PaintTarget::Stroke, PaintTarget::Markers] + } + ); + + assert_eq!( + PaintOrder::parse_str("markers fill").unwrap(), + PaintOrder { + targets: [PaintTarget::Markers, PaintTarget::Fill, PaintTarget::Stroke] + } + ); + + assert_eq!( + PaintOrder::parse_str("stroke").unwrap(), + PaintOrder { + targets: [PaintTarget::Stroke, PaintTarget::Fill, PaintTarget::Markers] + } + ); + + assert!(PaintOrder::parse_str("stroke stroke").is_err()); + assert!(PaintOrder::parse_str("markers stroke fill hello").is_err()); +} + +make_property!( + /// `r` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/geometry.html#R> + /// + /// Note that in SVG1.1, this was an attribute, not a property. + R, + default: ULength::<Both>::parse_str("0").unwrap(), + inherits_automatically: false, + newtype_parse: ULength<Both>, +); + +make_property!( + /// `rx` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/geometry.html#RX> + /// + /// Note that in SVG1.1, this was an attribute, not a property. + RX, + default: LengthOrAuto::<Horizontal>::Auto, + inherits_automatically: false, + newtype_parse: LengthOrAuto<Horizontal>, +); + +make_property!( + /// `ry` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/geometry.html#RY> + /// + /// Note that in SVG1.1, this was an attribute, not a property. + RY, + default: LengthOrAuto::<Vertical>::Auto, + inherits_automatically: false, + newtype_parse: LengthOrAuto<Vertical>, +); + +make_property!( + /// `shape-rendering` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#ShapeRendering> + ShapeRendering, + default: Auto, + inherits_automatically: true, + + identifiers: + "auto" => Auto, + "optimizeSpeed" => OptimizeSpeed, + "geometricPrecision" => GeometricPrecision, + "crispEdges" => CrispEdges, +); + +make_property!( + /// `stop-color` property for gradient stops. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/pservers.html#StopColorProperty> + StopColor, + default: cssparser::Color::RGBA(cssparser::RGBA::new(0, 0, 0, 255)), + inherits_automatically: false, + newtype_parse: cssparser::Color, +); + +make_property!( + /// `stop-opacity` property for gradient stops. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/pservers.html#StopOpacityProperty> + StopOpacity, + default: UnitInterval(1.0), + inherits_automatically: false, + newtype_parse: UnitInterval, +); + +make_property!( + /// `stroke` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#SpecifyingStrokePaint> + Stroke, + default: PaintServer::None, + inherits_automatically: true, + newtype_parse: PaintServer, +); + +make_property!( + /// `stroke-dasharray` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#StrokeDashing> + StrokeDasharray, + default: Dasharray::default(), + inherits_automatically: true, + newtype_parse: Dasharray, +); + +make_property!( + /// `stroke-dashoffset` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#StrokeDashingdas> + StrokeDashoffset, + default: Length::<Both>::default(), + inherits_automatically: true, + newtype_parse: Length<Both>, +); + +make_property!( + /// `stroke-linecap` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#LineCaps> + StrokeLinecap, + default: Butt, + inherits_automatically: true, + + identifiers: + "butt" => Butt, + "round" => Round, + "square" => Square, +); + +make_property!( + /// `stroke-linejoin` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#LineJoin> + StrokeLinejoin, + default: Miter, + inherits_automatically: true, + + identifiers: + "miter" => Miter, + "round" => Round, + "bevel" => Bevel, +); + +make_property!( + /// `stroke-miterlimit` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#StrokeMiterlimitProperty> + StrokeMiterlimit, + default: 4f64, + inherits_automatically: true, + newtype_parse: f64, +); + +make_property!( + /// `stroke-opacity` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#StrokeOpacity> + StrokeOpacity, + default: UnitInterval(1.0), + inherits_automatically: true, + newtype_parse: UnitInterval, +); + +make_property!( + /// `stroke-width` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/painting.html#StrokeWidth> + StrokeWidth, + default: Length::<Both>::parse_str("1.0").unwrap(), + inherits_automatically: true, + newtype_parse: Length::<Both>, +); + +make_property!( + /// `text-anchor` property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/text.html#TextAnchorProperty> + TextAnchor, + default: Start, + inherits_automatically: true, + + identifiers: + "start" => Start, + "middle" => Middle, + "end" => End, +); + +make_property!( + /// `text-decoration` shorthand property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/text.html#TextDecorationProperty> + /// + /// CSS Text Decoration 3: <https://www.w3.org/TR/css-text-decor-3/#text-decoration-property> + /// + /// Note that librsvg implements SVG1.1 semantics, where this is not a shorthand. + TextDecoration, + inherits_automatically: false, + + fields: { + overline: bool, default: false, + underline: bool, default: false, + strike: bool, default: false, + } + + parse_impl: { + impl Parse for TextDecoration { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<TextDecoration, ParseError<'i>> { + let mut overline = false; + let mut underline = false; + let mut strike = false; + + if parser.try_parse(|p| p.expect_ident_matching("none")).is_ok() { + return Ok(TextDecoration::default()); + } + + while !parser.is_exhausted() { + let loc = parser.current_source_location(); + let token = parser.next()?; + + match token { + Token::Ident(ref cow) if cow.eq_ignore_ascii_case("overline") => overline = true, + Token::Ident(ref cow) if cow.eq_ignore_ascii_case("underline") => underline = true, + Token::Ident(ref cow) if cow.eq_ignore_ascii_case("line-through") => strike = true, + _ => return Err(loc.new_basic_unexpected_token_error(token.clone()).into()), + } + } + + Ok(TextDecoration { + overline, + underline, + strike, + }) + } + } + } +); + +#[cfg(test)] +#[test] +fn parses_text_decoration() { + assert_eq!( + TextDecoration::parse_str("none").unwrap(), + TextDecoration { + overline: false, + underline: false, + strike: false, + } + ); + + assert_eq!( + TextDecoration::parse_str("overline").unwrap(), + TextDecoration { + overline: true, + underline: false, + strike: false, + } + ); + + assert_eq!( + TextDecoration::parse_str("underline").unwrap(), + TextDecoration { + overline: false, + underline: true, + strike: false, + } + ); + + assert_eq!( + TextDecoration::parse_str("line-through").unwrap(), + TextDecoration { + overline: false, + underline: false, + strike: true, + } + ); + + assert_eq!( + TextDecoration::parse_str("underline overline").unwrap(), + TextDecoration { + overline: true, + underline: true, + strike: false, + } + ); + + assert!(TextDecoration::parse_str("airline").is_err()) +} + +make_property!( + /// `text-orientation` property. + /// + /// CSS Writing Modes 3: <https://www.w3.org/TR/css-writing-modes-3/#propdef-text-orientation> + TextOrientation, + default: Mixed, + inherits_automatically: true, + + identifiers: + "mixed" => Mixed, + "upright" => Upright, + "sideways" => Sideways, +); + +impl From<GlyphOrientationVertical> for TextOrientation { + /// Converts the `glyph-orientation-vertical` shorthand to a `text-orientation` longhand. + /// + /// See <https://www.w3.org/TR/css-writing-modes-3/#propdef-glyph-orientation-vertical> for the conversion table. + fn from(o: GlyphOrientationVertical) -> TextOrientation { + match o { + GlyphOrientationVertical::Auto => TextOrientation::Mixed, + GlyphOrientationVertical::Angle0 => TextOrientation::Upright, + GlyphOrientationVertical::Angle90 => TextOrientation::Sideways, + } + } +} + +make_property!( + /// `text-rendering` property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/painting.html#TextRenderingProperty> + TextRendering, + default: Auto, + inherits_automatically: true, + + identifiers: + "auto" => Auto, + "optimizeSpeed" => OptimizeSpeed, + "optimizeLegibility" => OptimizeLegibility, + "geometricPrecision" => GeometricPrecision, +); + +make_property!( + /// `transform` property. + /// + /// CSS Transforms 1: <https://www.w3.org/TR/css-transforms-1/#transform-property> + Transform, + default: TransformProperty::None, + inherits_automatically: false, + newtype_parse: TransformProperty, +); + +make_property!( + /// `unicode-bidi` property. + /// + /// CSS Writing Modes 3: <https://www.w3.org/TR/css-writing-modes-3/#unicode-bidi> + UnicodeBidi, + default: Normal, + inherits_automatically: false, + + identifiers: + "normal" => Normal, + "embed" => Embed, + "isolate" => Isolate, + "bidi-override" => BidiOverride, + "isolate-override" => IsolateOverride, + "plaintext" => Plaintext, +); + +make_property!( + /// `vector-effect` property. + /// + /// SVG2: <https://svgwg.org/svg2-draft/coords.html#VectorEffectProperty> + VectorEffect, + default: None, + inherits_automatically: false, + + identifiers: + "none" => None, + "non-scaling-stroke" => NonScalingStroke, + // non-scaling-size, non-rotation, fixed-position not implemented +); + +make_property!( + /// `visibility` property. + /// + /// CSS2: <https://www.w3.org/TR/CSS2/visufx.html#visibility> + Visibility, + default: Visible, + inherits_automatically: true, + + identifiers: + "visible" => Visible, + "hidden" => Hidden, + "collapse" => Collapse, +); + +make_property!( + /// `width` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/geometry.html#Sizing> + /// + /// Note that in SVG1.1, this was an attribute, not a property. + Width, + default: LengthOrAuto::<Horizontal>::Auto, + inherits_automatically: false, + newtype_parse: LengthOrAuto<Horizontal>, +); + +make_property!( + /// `writing-mode` property. + /// + /// SVG1.1: <https://www.w3.org/TR/SVG11/text.html#WritingModeProperty> + /// + /// SVG2: <https://svgwg.org/svg2-draft/text.html#WritingModeProperty> + /// + /// CSS Writing Modes 3: <https://www.w3.org/TR/css-writing-modes-3/#block-flow> + /// + /// See the comments in the SVG2 spec for how the SVG1.1 values must be translated + /// into CSS Writing Modes 3 values. + WritingMode, + default: HorizontalTb, + identifiers: { + "horizontal-tb" => HorizontalTb, + "vertical-rl" => VerticalRl, + "vertical-lr" => VerticalLr, + "lr" => Lr, + "lr-tb" => LrTb, + "rl" => Rl, + "rl-tb" => RlTb, + "tb" => Tb, + "tb-rl" => TbRl, + }, + property_impl: { + impl Property for WritingMode { + fn inherits_automatically() -> bool { + true + } + + fn compute(&self, _v: &ComputedValues) -> Self { + use WritingMode::*; + + // Translate SVG1.1 compatibility values to SVG2 / CSS Writing Modes 3. + match *self { + Lr | LrTb | Rl | RlTb => HorizontalTb, + Tb | TbRl => VerticalRl, + _ => *self, + } + } + } + } +); + +impl WritingMode { + pub fn is_horizontal(self) -> bool { + use WritingMode::*; + + matches!(self, HorizontalTb | Lr | LrTb | Rl | RlTb) + } +} + +make_property!( + /// `x` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/geometry.html#X> + /// + /// Note that in SVG1.1, this was an attribute, not a property. + X, + default: Length::<Horizontal>::parse_str("0").unwrap(), + inherits_automatically: false, + newtype_parse: Length<Horizontal>, +); + +make_property!( + /// `xml:lang` attribute. + /// + /// XML1.0: <https://www.w3.org/TR/xml/#sec-lang-tag> + /// + /// Similar to `XmlSpace`, this is a hack in librsvg: the `xml:lang` attribute is + /// supposed to apply to an element and all its children. This more or less matches + /// CSS property inheritance, so librsvg reuses the machinery for property inheritance + /// to propagate down the value of the `xml:lang` attribute to an element's children. + XmlLang, + default: None, + inherits_automatically: true, + newtype: Option<Box<LanguageTag>>, + parse_impl: { + impl Parse for XmlLang { + fn parse<'i>( + parser: &mut Parser<'i, '_>, + ) -> Result<XmlLang, ParseError<'i>> { + let language_tag = parser.expect_ident()?; + let language_tag = LanguageTag::from_str(language_tag).map_err(|_| { + parser.new_custom_error(ValueErrorKind::parse_error("invalid syntax for 'xml:lang' parameter")) + })?; + Ok(XmlLang(Some(Box::new(language_tag)))) + } + } + }, +); + +#[cfg(test)] +#[test] +fn parses_xml_lang() { + assert_eq!( + XmlLang::parse_str("es-MX").unwrap(), + XmlLang(Some(Box::new(LanguageTag::from_str("es-MX").unwrap()))) + ); + + assert!(XmlLang::parse_str("").is_err()); +} + +make_property!( + /// `xml:space` attribute. + /// + /// XML1.0: <https://www.w3.org/TR/xml/#sec-white-space> + /// + /// Similar to `XmlLang`, this is a hack in librsvg. The `xml:space` attribute is + /// supposed to be applied to all the children of the element in which it appears, so + /// it works more or less the same as CSS property inheritance. Librsvg reuses the + /// machinery for CSS property inheritance to propagate down the value of `xml:space` + /// to an element's children. + XmlSpace, + default: Default, + inherits_automatically: true, + + identifiers: + "default" => Default, + "preserve" => Preserve, +); + +make_property!( + /// `y` property. + /// + /// SVG2: <https://www.w3.org/TR/SVG2/geometry.html#Y> + /// + /// Note that in SVG1.1, this was an attribute, not a property. + Y, + default: Length::<Vertical>::parse_str("0").unwrap(), + inherits_automatically: false, + newtype_parse: Length<Vertical>, +); diff --git a/rsvg/src/property_macros.rs b/rsvg/src/property_macros.rs new file mode 100644 index 00000000..0a51649e --- /dev/null +++ b/rsvg/src/property_macros.rs @@ -0,0 +1,288 @@ +//! Macros to define CSS properties. + +use crate::properties::ComputedValues; + +/// Trait which all CSS property types should implement. +pub trait Property { + /// Whether the property's computed value inherits from parent to child elements. + /// + /// For each property, the CSS or SVG specs say whether the property inherits + /// automatically. When a property is not specified in an element, the return value + /// of this method determines whether the property's value is copied from the parent + /// element (`true`), or whether it resets to the initial/default value (`false`). + fn inherits_automatically() -> bool; + + /// Derive the CSS computed value from the parent element's + /// [`ComputedValues`][crate::properties::ComputedValues] and the + /// `self` value. + /// + /// The CSS or SVG specs say how to derive this for each property. + fn compute(&self, _: &ComputedValues) -> Self; +} + +/// Generates a type for a CSS property. +/// +/// Writing a property by hand takes a bit of boilerplate: +/// +/// * Define a type to represent the property's values. +/// +/// * A [`Parse`] implementation to parse the property. +/// +/// * A [`Default`] implementation to define the property's *initial* value. +/// +/// * A [`Property`] implementation to define whether the property +/// inherits from the parent element, and how the property derives its +/// computed value. +/// +/// When going from [`SpecifiedValues`] to [`ComputedValues`], +/// properties which inherit automatically from the parent element +/// will just have their values cloned. Properties which do not +/// inherit will be reset back to their initial value (i.e. their +/// [`Default`]). +/// +/// The default implementation of [`Property::compute()`] is to just +/// clone the property's value. Properties which need more +/// sophisticated computation can override this. +/// +/// This macro allows defining properties of different kinds; see the following +/// sections for examples. +/// +/// # Simple identifiers +/// +/// Many properties are just sets of identifiers and can be represented +/// by simple enums. In this case, you can use the following: +/// +/// ```text +/// make_property!( +/// /// Documentation here. +/// StrokeLinejoin, +/// default: Miter, +/// inherits_automatically: true, +/// +/// identifiers: +/// "miter" => Miter, +/// "round" => Round, +/// "bevel" => Bevel, +/// ); +/// ``` +/// +/// This generates a simple enum like the following, with implementations of [`Parse`], +/// [`Default`], and [`Property`]. +/// +/// ``` +/// pub enum StrokeLinejoin { Miter, Round, Bevel } +/// ``` +/// +/// # Properties from an existing, general-purpose type +/// +/// For example, both the `lightingColor` and `floodColor` properties can be represented +/// with a `cssparser::Color`, but their intial values are different. In this case, the macro +/// can generate a newtype around `cssparser::Color` for each case: +/// +/// ```text +/// make_property!( +/// /// Documentation here. +/// FloodColor, +/// default: cssparser::Color::RGBA(cssparser::RGBA::new(0, 0, 0, 0)), +/// inherits_automatically: false, +/// newtype_parse: cssparser::Color, +/// ); +/// ``` +/// +/// # Properties from custom specific types +/// +/// For example, font-related properties have custom, complex types that require an +/// implentation of `Property::compute` that is more than a simple `clone`. In this case, +/// define the custom type separately, and use the macro to specify the default value and +/// the `Property` implementation. +/// +/// [`Parse`]: crate::parsers::Parse +/// [`Property`]: crate::property_macros::Property +/// [`ComputedValues`]: crate::properties::ComputedValues +/// [`SpecifiedValues`]: crate::properties::SpecifiedValues +/// [`Property::compute()`]: crate::property_macros::Property::compute +/// +#[macro_export] +macro_rules! make_property { + ($(#[$attr:meta])* + $name: ident, + default: $default: ident, + inherits_automatically: $inherits_automatically: expr, + identifiers: + $($str_prop: expr => $variant: ident,)+ + ) => { + $(#[$attr])* + #[derive(Debug, Copy, Clone, PartialEq)] + #[repr(C)] + pub enum $name { + $($variant),+ + } + + impl_default!($name, $name::$default); + impl_property!($name, $inherits_automatically); + + impl $crate::parsers::Parse for $name { + fn parse<'i>(parser: &mut ::cssparser::Parser<'i, '_>) -> Result<$name, $crate::error::ParseError<'i>> { + Ok(parse_identifiers!( + parser, + $($str_prop => $name::$variant,)+ + )?) + } + } + }; + + ($(#[$attr:meta])* + $name: ident, + default: $default: ident, + identifiers: { $($str_prop: expr => $variant: ident,)+ }, + property_impl: { $prop: item } + ) => { + $(#[$attr])* + #[derive(Debug, Copy, Clone, PartialEq)] + #[repr(C)] + pub enum $name { + $($variant),+ + } + + impl_default!($name, $name::$default); + $prop + + impl $crate::parsers::Parse for $name { + fn parse<'i>(parser: &mut ::cssparser::Parser<'i, '_>) -> Result<$name, $crate::error::ParseError<'i>> { + Ok(parse_identifiers!( + parser, + $($str_prop => $name::$variant,)+ + )?) + } + } + }; + + ($(#[$attr:meta])* + $name: ident, + default: $default: expr, + inherits_automatically: $inherits_automatically: expr, + newtype_parse: $type: ty, + ) => { + $(#[$attr])* + #[derive(Debug, Clone, PartialEq)] + pub struct $name(pub $type); + + impl_default!($name, $name($default)); + impl_property!($name, $inherits_automatically); + + impl $crate::parsers::Parse for $name { + fn parse<'i>(parser: &mut ::cssparser::Parser<'i, '_>) -> Result<$name, $crate::error::ParseError<'i>> { + Ok($name(<$type as $crate::parsers::Parse>::parse(parser)?)) + } + } + }; + + ($(#[$attr:meta])* + $name: ident, + default: $default: expr, + property_impl: { $prop: item } + ) => { + impl_default!($name, $default); + + $prop + }; + + ($name: ident, + default: $default: expr, + inherits_automatically: $inherits_automatically: expr, + ) => { + impl_default!($name, $default); + impl_property!($name, $inherits_automatically); + }; + + ($name: ident, + default: $default: expr, + inherits_automatically: $inherits_automatically: expr, + parse_impl: { $parse: item } + ) => { + impl_default!($name, $default); + impl_property!($name, $inherits_automatically); + + $parse + }; + + ($(#[$attr:meta])* + $name: ident, + default: $default: expr, + newtype: $type: ty, + property_impl: { $prop: item }, + parse_impl: { $parse: item } + ) => { + $(#[$attr])* + #[derive(Debug, Clone, PartialEq)] + pub struct $name(pub $type); + + impl_default!($name, $name($default)); + + $prop + + $parse + }; + + // pending - only XmlLang + ($(#[$attr:meta])* + $name: ident, + default: $default: expr, + inherits_automatically: $inherits_automatically: expr, + newtype: $type: ty, + parse_impl: { $parse: item }, + ) => { + $(#[$attr])* + #[derive(Debug, Clone, PartialEq)] + pub struct $name(pub $type); + + impl_default!($name, $name($default)); + impl_property!($name, $inherits_automatically); + + $parse + }; + + ($(#[$attr:meta])* + $name: ident, + inherits_automatically: $inherits_automatically: expr, + fields: { + $($field_name: ident : $field_type: ty, default: $field_default : expr,)+ + } + parse_impl: { $parse: item } + ) => { + $(#[$attr])* + #[derive(Debug, Clone, PartialEq)] + pub struct $name { + $(pub $field_name: $field_type),+ + } + + impl_default!($name, $name { $($field_name: $field_default),+ }); + impl_property!($name, $inherits_automatically); + + $parse + }; +} + +macro_rules! impl_default { + ($name:ident, $default:expr) => { + impl Default for $name { + fn default() -> $name { + $default + } + } + }; +} + +macro_rules! impl_property { + ($name:ident, $inherits_automatically:expr) => { + impl $crate::property_macros::Property for $name { + fn inherits_automatically() -> bool { + $inherits_automatically + } + + fn compute(&self, _v: &$crate::properties::ComputedValues) -> Self { + self.clone() + } + } + }; +} diff --git a/rsvg/src/rect.rs b/rsvg/src/rect.rs new file mode 100644 index 00000000..f805211e --- /dev/null +++ b/rsvg/src/rect.rs @@ -0,0 +1,274 @@ +//! Types for rectangles. + +use crate::coord_units::CoordUnits; +use crate::transform::Transform; + +#[allow(clippy::module_inception)] +mod rect { + use crate::float_eq_cairo::ApproxEqCairo; + use core::ops::{Add, Range, Sub}; + use float_cmp::approx_eq; + use num_traits::Zero; + + // Use our own min() and max() that are acceptable for floating point + + fn min<T: PartialOrd>(x: T, y: T) -> T { + if x <= y { + x + } else { + y + } + } + + fn max<T: PartialOrd>(x: T, y: T) -> T { + if x >= y { + x + } else { + y + } + } + + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] + pub struct Rect<T> { + pub x0: T, + pub y0: T, + pub x1: T, + pub y1: T, + } + + impl<T> Rect<T> { + #[inline] + pub fn new(x0: T, y0: T, x1: T, y1: T) -> Self { + Self { x0, y0, x1, y1 } + } + } + + impl<T> Rect<T> + where + T: Copy + PartialOrd + PartialEq + Add<T, Output = T> + Sub<T, Output = T> + Zero, + { + #[inline] + pub fn from_size(w: T, h: T) -> Self { + Self { + x0: Zero::zero(), + y0: Zero::zero(), + x1: w, + y1: h, + } + } + + #[inline] + pub fn width(&self) -> T { + self.x1 - self.x0 + } + + #[inline] + pub fn height(&self) -> T { + self.y1 - self.y0 + } + + #[inline] + pub fn size(&self) -> (T, T) { + (self.width(), self.height()) + } + + #[inline] + pub fn x_range(&self) -> Range<T> { + self.x0..self.x1 + } + + #[inline] + pub fn y_range(&self) -> Range<T> { + self.y0..self.y1 + } + + #[inline] + pub fn contains(self, x: T, y: T) -> bool { + x >= self.x0 && x < self.x1 && y >= self.y0 && y < self.y1 + } + + #[inline] + pub fn translate(&self, by: (T, T)) -> Self { + Self { + x0: self.x0 + by.0, + y0: self.y0 + by.1, + x1: self.x1 + by.0, + y1: self.y1 + by.1, + } + } + + #[inline] + pub fn intersection(&self, rect: &Self) -> Option<Self> { + let (x0, y0, x1, y1) = ( + max(self.x0, rect.x0), + max(self.y0, rect.y0), + min(self.x1, rect.x1), + min(self.y1, rect.y1), + ); + + if x1 > x0 && y1 > y0 { + Some(Self { x0, y0, x1, y1 }) + } else { + None + } + } + + #[inline] + pub fn union(&self, rect: &Self) -> Self { + Self { + x0: min(self.x0, rect.x0), + y0: min(self.y0, rect.y0), + x1: max(self.x1, rect.x1), + y1: max(self.y1, rect.y1), + } + } + } + + impl Rect<i32> { + #[inline] + pub fn is_empty(&self) -> bool { + // Give an explicit type to the right hand side of the ==, since sometimes + // type inference fails to figure it out. I have no idea why. + self.width() == <i32 as Zero>::zero() || self.height() == <i32 as Zero>::zero() + } + + #[inline] + pub fn scale(self, x: f64, y: f64) -> Self { + Self { + x0: (f64::from(self.x0) * x).floor() as i32, + y0: (f64::from(self.y0) * y).floor() as i32, + x1: (f64::from(self.x1) * x).ceil() as i32, + y1: (f64::from(self.y1) * y).ceil() as i32, + } + } + } + + impl Rect<f64> { + #[inline] + pub fn is_empty(&self) -> bool { + self.width().approx_eq_cairo(0.0) || self.height().approx_eq_cairo(0.0) + } + + #[inline] + pub fn scale(self, x: f64, y: f64) -> Self { + Self { + x0: self.x0 * x, + y0: self.y0 * y, + x1: self.x1 * x, + y1: self.y1 * y, + } + } + + pub fn approx_eq(&self, other: &Self) -> bool { + // FIXME: this is super fishy; shouldn't we be using 2x the epsilon against the width/height + // instead of the raw coordinates? + approx_eq!(f64, self.x0, other.x0, epsilon = 0.0001) + && approx_eq!(f64, self.y0, other.y0, epsilon = 0.0001) + && approx_eq!(f64, self.x1, other.x1, epsilon = 0.0001) + && approx_eq!(f64, self.y1, other.y1, epsilon = 0.00012) + } + } +} + +pub type Rect = rect::Rect<f64>; + +impl From<Rect> for IRect { + #[inline] + fn from(r: Rect) -> Self { + Self { + x0: r.x0.floor() as i32, + y0: r.y0.floor() as i32, + x1: r.x1.ceil() as i32, + y1: r.y1.ceil() as i32, + } + } +} + +impl From<cairo::Rectangle> for Rect { + #[inline] + fn from(r: cairo::Rectangle) -> Self { + Self { + x0: r.x(), + y0: r.y(), + x1: r.x() + r.width(), + y1: r.y() + r.height(), + } + } +} + +impl From<Rect> for cairo::Rectangle { + #[inline] + fn from(r: Rect) -> Self { + Self::new(r.x0, r.y0, r.x1 - r.x0, r.y1 - r.y0) + } +} + +/// Creates a transform to map to a rectangle. +/// +/// The rectangle is an `Option<Rect>` to indicate the possibility that there is no +/// bounding box from where the rectangle could be obtained. +/// +/// This depends on a `CoordUnits` parameter. When this is +/// `CoordUnits::ObjectBoundingBox`, the bounding box must not be empty, since the calling +/// code would then not have a usable size to work with. In that case, if the bbox is +/// empty, this function returns `Err(())`. +/// +/// Usually calling code can simply ignore the action it was about to take if this +/// function returns an error. +pub fn rect_to_transform(rect: &Option<Rect>, units: CoordUnits) -> Result<Transform, ()> { + match units { + CoordUnits::UserSpaceOnUse => Ok(Transform::identity()), + CoordUnits::ObjectBoundingBox => { + if rect.as_ref().map_or(true, |r| r.is_empty()) { + Err(()) + } else { + let r = rect.as_ref().unwrap(); + let t = Transform::new_unchecked(r.width(), 0.0, 0.0, r.height(), r.x0, r.y0); + + if t.is_invertible() { + Ok(t) + } else { + Err(()) + } + } + } + } +} + +pub type IRect = rect::Rect<i32>; + +impl From<IRect> for Rect { + #[inline] + fn from(r: IRect) -> Self { + Self { + x0: f64::from(r.x0), + y0: f64::from(r.y0), + x1: f64::from(r.x1), + y1: f64::from(r.y1), + } + } +} + +impl From<cairo::Rectangle> for IRect { + #[inline] + fn from(r: cairo::Rectangle) -> Self { + Self { + x0: r.x().floor() as i32, + y0: r.y().floor() as i32, + x1: (r.x() + r.width()).ceil() as i32, + y1: (r.y() + r.height()).ceil() as i32, + } + } +} + +impl From<IRect> for cairo::Rectangle { + #[inline] + fn from(r: IRect) -> Self { + Self::new( + f64::from(r.x0), + f64::from(r.y0), + f64::from(r.x1 - r.x0), + f64::from(r.y1 - r.y0), + ) + } +} diff --git a/rsvg/src/session.rs b/rsvg/src/session.rs new file mode 100644 index 00000000..fe556cef --- /dev/null +++ b/rsvg/src/session.rs @@ -0,0 +1,44 @@ +//! Tracks metadata for a loading/rendering session. + +use std::sync::Arc; + +/// Metadata for a loading/rendering session. +/// +/// When the calling program first uses one of the API entry points (e.g. `Loader::new()` +/// in the Rust API, or `rsvg_handle_new()` in the C API), there is no context yet where +/// librsvg's code may start to track things. This struct provides that context. +#[derive(Clone)] +pub struct Session { + inner: Arc<SessionInner>, +} + +struct SessionInner { + log_enabled: bool, +} + +fn log_enabled_via_env_var() -> bool { + ::std::env::var_os("RSVG_LOG").is_some() +} + +impl Default for Session { + fn default() -> Self { + Self { + inner: Arc::new(SessionInner { + log_enabled: log_enabled_via_env_var(), + }), + } + } +} + +impl Session { + #[cfg(test)] + pub fn new_for_test_suite() -> Self { + Self { + inner: Arc::new(SessionInner { log_enabled: false }), + } + } + + pub fn log_enabled(&self) -> bool { + self.inner.log_enabled + } +} diff --git a/rsvg/src/shapes.rs b/rsvg/src/shapes.rs new file mode 100644 index 00000000..2aabd174 --- /dev/null +++ b/rsvg/src/shapes.rs @@ -0,0 +1,743 @@ +//! Basic SVG shapes: the `path`, `polygon`, `polyline`, `line`, +//! `rect`, `circle`, `ellipse` elements. + +use cssparser::{Parser, Token}; +use markup5ever::{expanded_name, local_name, namespace_url, ns}; +use std::ops::Deref; +use std::rc::Rc; + +use crate::bbox::BoundingBox; +use crate::document::AcquiredNodes; +use crate::drawing_ctx::{DrawingCtx, Viewport}; +use crate::element::{set_attribute, ElementTrait}; +use crate::error::*; +use crate::iri::Iri; +use crate::layout::{Layer, LayerKind, Marker, Shape, StackingContext, Stroke}; +use crate::length::*; +use crate::node::{CascadedValues, Node, NodeBorrow}; +use crate::parsers::{optional_comma, Parse, ParseValue}; +use crate::path_builder::{LargeArc, Path as SvgPath, PathBuilder, Sweep}; +use crate::properties::ComputedValues; +use crate::session::Session; +use crate::xml::Attributes; + +#[derive(PartialEq)] +enum Markers { + No, + Yes, +} + +struct ShapeDef { + path: Rc<SvgPath>, + markers: Markers, +} + +impl ShapeDef { + fn new(path: Rc<SvgPath>, markers: Markers) -> ShapeDef { + ShapeDef { path, markers } + } +} + +trait BasicShape { + fn make_shape(&self, params: &NormalizeParams, values: &ComputedValues) -> ShapeDef; +} + +fn draw_basic_shape( + basic_shape: &dyn BasicShape, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, +) -> Result<BoundingBox, RenderingError> { + let values = cascaded.get(); + let params = NormalizeParams::new(values, viewport); + let shape_def = basic_shape.make_shape(¶ms, values); + + let is_visible = values.is_visible(); + let paint_order = values.paint_order(); + + let stroke = Stroke::new(values, ¶ms); + + let session = draw_ctx.session(); + + let stroke_paint = values.stroke().0.resolve( + acquired_nodes, + values.stroke_opacity().0, + values.color().0, + cascaded.context_fill.clone(), + cascaded.context_stroke.clone(), + session, + ); + + let fill_paint = values.fill().0.resolve( + acquired_nodes, + values.fill_opacity().0, + values.color().0, + cascaded.context_fill.clone(), + cascaded.context_stroke.clone(), + session, + ); + + let fill_rule = values.fill_rule(); + let clip_rule = values.clip_rule(); + let shape_rendering = values.shape_rendering(); + + let marker_start_node; + let marker_mid_node; + let marker_end_node; + + if shape_def.markers == Markers::Yes { + marker_start_node = acquire_marker(session, acquired_nodes, &values.marker_start().0); + marker_mid_node = acquire_marker(session, acquired_nodes, &values.marker_mid().0); + marker_end_node = acquire_marker(session, acquired_nodes, &values.marker_end().0); + } else { + marker_start_node = None; + marker_mid_node = None; + marker_end_node = None; + } + + let marker_start = Marker { + node_ref: marker_start_node, + context_stroke: stroke_paint.clone(), + context_fill: fill_paint.clone(), + }; + + let marker_mid = Marker { + node_ref: marker_mid_node, + context_stroke: stroke_paint.clone(), + context_fill: fill_paint.clone(), + }; + + let marker_end = Marker { + node_ref: marker_end_node, + context_stroke: stroke_paint.clone(), + context_fill: fill_paint.clone(), + }; + + let extents = draw_ctx.compute_path_extents(&shape_def.path)?; + + let normalize_values = NormalizeValues::new(values); + + let stroke_paint = stroke_paint.to_user_space(&extents, viewport, &normalize_values); + let fill_paint = fill_paint.to_user_space(&extents, viewport, &normalize_values); + + let shape = Box::new(Shape { + path: shape_def.path, + extents, + is_visible, + paint_order, + stroke, + stroke_paint, + fill_paint, + fill_rule, + clip_rule, + shape_rendering, + marker_start, + marker_mid, + marker_end, + }); + + let elt = node.borrow_element(); + let stacking_ctx = StackingContext::new( + draw_ctx.session(), + acquired_nodes, + &elt, + values.transform(), + values, + ); + + let layer = Layer { + kind: LayerKind::Shape(shape), + stacking_ctx, + }; + + draw_ctx.draw_layer(&layer, acquired_nodes, clipping, viewport) +} + +macro_rules! impl_draw { + () => { + fn draw( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + draw_basic_shape( + self, + node, + acquired_nodes, + cascaded, + viewport, + draw_ctx, + clipping, + ) + } + }; +} + +fn acquire_marker( + session: &Session, + acquired_nodes: &mut AcquiredNodes<'_>, + iri: &Iri, +) -> Option<Node> { + iri.get().and_then(|id| { + acquired_nodes + .acquire(id) + .map_err(|e| { + rsvg_log!(session, "cannot render marker: {}", e); + }) + .ok() + .and_then(|acquired| { + let node = acquired.get(); + + if is_element_of_type!(node, Marker) { + Some(node.clone()) + } else { + rsvg_log!(session, "{} is not a marker element", id); + None + } + }) + }) +} + +fn make_ellipse(cx: f64, cy: f64, rx: f64, ry: f64) -> SvgPath { + let mut builder = PathBuilder::default(); + + // Per the spec, rx and ry must be nonnegative + if rx <= 0.0 || ry <= 0.0 { + return builder.into_path(); + } + + // 4/3 * (1-cos 45°)/sin 45° = 4/3 * sqrt(2) - 1 + let arc_magic: f64 = 0.5522847498; + + // approximate an ellipse using 4 Bézier curves + + builder.move_to(cx + rx, cy); + + builder.curve_to( + cx + rx, + cy + arc_magic * ry, + cx + arc_magic * rx, + cy + ry, + cx, + cy + ry, + ); + + builder.curve_to( + cx - arc_magic * rx, + cy + ry, + cx - rx, + cy + arc_magic * ry, + cx - rx, + cy, + ); + + builder.curve_to( + cx - rx, + cy - arc_magic * ry, + cx - arc_magic * rx, + cy - ry, + cx, + cy - ry, + ); + + builder.curve_to( + cx + arc_magic * rx, + cy - ry, + cx + rx, + cy - arc_magic * ry, + cx + rx, + cy, + ); + + builder.close_path(); + + builder.into_path() +} + +#[derive(Default)] +pub struct Path { + path: Rc<SvgPath>, +} + +impl ElementTrait for Path { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + if attr.expanded() == expanded_name!("", "d") { + let mut builder = PathBuilder::default(); + if let Err(e) = builder.parse(value) { + // Creating a partial path is OK per the spec; we don't throw away the partial + // result in case of an error. + + rsvg_log!(session, "could not parse path: {}", e); + } + self.path = Rc::new(builder.into_path()); + } + } + } + + impl_draw!(); +} + +impl BasicShape for Path { + fn make_shape(&self, _params: &NormalizeParams, _values: &ComputedValues) -> ShapeDef { + ShapeDef::new(self.path.clone(), Markers::Yes) + } +} + +/// List-of-points for polyline and polygon elements. +/// +/// SVG1.1: <https://www.w3.org/TR/SVG/shapes.html#PointsBNF> +/// +/// SVG2: <https://www.w3.org/TR/SVG/shapes.html#DataTypePoints> +#[derive(Debug, Default, PartialEq)] +struct Points(Vec<(f64, f64)>); + +impl Deref for Points { + type Target = [(f64, f64)]; + + fn deref(&self) -> &[(f64, f64)] { + &self.0 + } +} + +impl Parse for Points { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Points, ParseError<'i>> { + let mut v = Vec::new(); + + loop { + let x = f64::parse(parser)?; + optional_comma(parser); + let y = f64::parse(parser)?; + + v.push((x, y)); + + if parser.is_exhausted() { + break; + } + + match parser.next_including_whitespace() { + Ok(&Token::WhiteSpace(_)) => (), + _ => optional_comma(parser), + } + } + + Ok(Points(v)) + } +} + +fn make_poly(points: &Points, closed: bool) -> SvgPath { + let mut builder = PathBuilder::default(); + + for (i, &(x, y)) in points.iter().enumerate() { + if i == 0 { + builder.move_to(x, y); + } else { + builder.line_to(x, y); + } + } + + if closed && !points.is_empty() { + builder.close_path(); + } + + builder.into_path() +} + +#[derive(Default)] +pub struct Polygon { + points: Points, +} + +impl ElementTrait for Polygon { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + if attr.expanded() == expanded_name!("", "points") { + set_attribute(&mut self.points, attr.parse(value), session); + } + } + } + + impl_draw!(); +} + +impl BasicShape for Polygon { + fn make_shape(&self, _params: &NormalizeParams, _values: &ComputedValues) -> ShapeDef { + ShapeDef::new(Rc::new(make_poly(&self.points, true)), Markers::Yes) + } +} + +#[derive(Default)] +pub struct Polyline { + points: Points, +} + +impl ElementTrait for Polyline { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + if attr.expanded() == expanded_name!("", "points") { + set_attribute(&mut self.points, attr.parse(value), session); + } + } + } + + impl_draw!(); +} + +impl BasicShape for Polyline { + fn make_shape(&self, _params: &NormalizeParams, _values: &ComputedValues) -> ShapeDef { + ShapeDef::new(Rc::new(make_poly(&self.points, false)), Markers::Yes) + } +} + +#[derive(Default)] +pub struct Line { + x1: Length<Horizontal>, + y1: Length<Vertical>, + x2: Length<Horizontal>, + y2: Length<Vertical>, +} + +impl ElementTrait for Line { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "x1") => set_attribute(&mut self.x1, attr.parse(value), session), + expanded_name!("", "y1") => set_attribute(&mut self.y1, attr.parse(value), session), + expanded_name!("", "x2") => set_attribute(&mut self.x2, attr.parse(value), session), + expanded_name!("", "y2") => set_attribute(&mut self.y2, attr.parse(value), session), + _ => (), + } + } + } + + impl_draw!(); +} + +impl BasicShape for Line { + fn make_shape(&self, params: &NormalizeParams, _values: &ComputedValues) -> ShapeDef { + let mut builder = PathBuilder::default(); + + let x1 = self.x1.to_user(params); + let y1 = self.y1.to_user(params); + let x2 = self.x2.to_user(params); + let y2 = self.y2.to_user(params); + + builder.move_to(x1, y1); + builder.line_to(x2, y2); + + ShapeDef::new(Rc::new(builder.into_path()), Markers::Yes) + } +} + +/// The `<rect>` element. +/// +/// Note that its x/y/width/height/rx/ry are properties in SVG2, so they are +/// defined as part of [the properties machinery](properties.rs). +#[derive(Default)] +pub struct Rect {} + +impl ElementTrait for Rect { + impl_draw!(); +} + +impl BasicShape for Rect { + #[allow(clippy::many_single_char_names)] + fn make_shape(&self, params: &NormalizeParams, values: &ComputedValues) -> ShapeDef { + let x = values.x().0.to_user(params); + let y = values.y().0.to_user(params); + + let w = match values.width().0 { + LengthOrAuto::Length(l) => l.to_user(params), + LengthOrAuto::Auto => 0.0, + }; + let h = match values.height().0 { + LengthOrAuto::Length(l) => l.to_user(params), + LengthOrAuto::Auto => 0.0, + }; + + let norm_rx = match values.rx().0 { + LengthOrAuto::Length(l) => Some(l.to_user(params)), + LengthOrAuto::Auto => None, + }; + let norm_ry = match values.ry().0 { + LengthOrAuto::Length(l) => Some(l.to_user(params)), + LengthOrAuto::Auto => None, + }; + + let mut rx; + let mut ry; + + match (norm_rx, norm_ry) { + (None, None) => { + rx = 0.0; + ry = 0.0; + } + + (Some(_rx), None) => { + rx = _rx; + ry = _rx; + } + + (None, Some(_ry)) => { + rx = _ry; + ry = _ry; + } + + (Some(_rx), Some(_ry)) => { + rx = _rx; + ry = _ry; + } + } + + let mut builder = PathBuilder::default(); + + // Per the spec, w,h must be >= 0 + if w <= 0.0 || h <= 0.0 { + return ShapeDef::new(Rc::new(builder.into_path()), Markers::No); + } + + let half_w = w / 2.0; + let half_h = h / 2.0; + + if rx > half_w { + rx = half_w; + } + + if ry > half_h { + ry = half_h; + } + + if rx == 0.0 { + ry = 0.0; + } else if ry == 0.0 { + rx = 0.0; + } + + if rx == 0.0 { + // Easy case, no rounded corners + builder.move_to(x, y); + builder.line_to(x + w, y); + builder.line_to(x + w, y + h); + builder.line_to(x, y + h); + builder.line_to(x, y); + } else { + /* Hard case, rounded corners + * + * (top_x1, top_y) (top_x2, top_y) + * *--------------------------------* + * / \ + * * (left_x, left_y1) * (right_x, right_y1) + * | | + * | | + * | | + * | | + * | | + * | | + * | | + * | | + * | | + * * (left_x, left_y2) * (right_x, right_y2) + * \ / + * *--------------------------------* + * (bottom_x1, bottom_y) (bottom_x2, bottom_y) + */ + + let top_x1 = x + rx; + let top_x2 = x + w - rx; + let top_y = y; + + let bottom_x1 = top_x1; + let bottom_x2 = top_x2; + let bottom_y = y + h; + + let left_x = x; + let left_y1 = y + ry; + let left_y2 = y + h - ry; + + let right_x = x + w; + let right_y1 = left_y1; + let right_y2 = left_y2; + + builder.move_to(top_x1, top_y); + builder.line_to(top_x2, top_y); + + builder.arc( + top_x2, + top_y, + rx, + ry, + 0.0, + LargeArc(false), + Sweep::Positive, + right_x, + right_y1, + ); + + builder.line_to(right_x, right_y2); + + builder.arc( + right_x, + right_y2, + rx, + ry, + 0.0, + LargeArc(false), + Sweep::Positive, + bottom_x2, + bottom_y, + ); + + builder.line_to(bottom_x1, bottom_y); + + builder.arc( + bottom_x1, + bottom_y, + rx, + ry, + 0.0, + LargeArc(false), + Sweep::Positive, + left_x, + left_y2, + ); + + builder.line_to(left_x, left_y1); + + builder.arc( + left_x, + left_y1, + rx, + ry, + 0.0, + LargeArc(false), + Sweep::Positive, + top_x1, + top_y, + ); + } + + builder.close_path(); + + ShapeDef::new(Rc::new(builder.into_path()), Markers::No) + } +} + +/// The `<circle>` element. +/// +/// Note that its cx/cy/r are properties in SVG2, so they are +/// defined as part of [the properties machinery](properties.rs). +#[derive(Default)] +pub struct Circle {} + +impl ElementTrait for Circle { + impl_draw!(); +} + +impl BasicShape for Circle { + fn make_shape(&self, params: &NormalizeParams, values: &ComputedValues) -> ShapeDef { + let cx = values.cx().0.to_user(params); + let cy = values.cy().0.to_user(params); + let r = values.r().0.to_user(params); + + ShapeDef::new(Rc::new(make_ellipse(cx, cy, r, r)), Markers::No) + } +} + +/// The `<ellipse>` element. +/// +/// Note that its cx/cy/rx/ry are properties in SVG2, so they are +/// defined as part of [the properties machinery](properties.rs). +#[derive(Default)] +pub struct Ellipse {} + +impl ElementTrait for Ellipse { + impl_draw!(); +} + +impl BasicShape for Ellipse { + fn make_shape(&self, params: &NormalizeParams, values: &ComputedValues) -> ShapeDef { + let cx = values.cx().0.to_user(params); + let cy = values.cy().0.to_user(params); + let norm_rx = match values.rx().0 { + LengthOrAuto::Length(l) => Some(l.to_user(params)), + LengthOrAuto::Auto => None, + }; + let norm_ry = match values.ry().0 { + LengthOrAuto::Length(l) => Some(l.to_user(params)), + LengthOrAuto::Auto => None, + }; + + let rx; + let ry; + + match (norm_rx, norm_ry) { + (None, None) => { + rx = 0.0; + ry = 0.0; + } + + (Some(_rx), None) => { + rx = _rx; + ry = _rx; + } + + (None, Some(_ry)) => { + rx = _ry; + ry = _ry; + } + + (Some(_rx), Some(_ry)) => { + rx = _rx; + ry = _ry; + } + } + + ShapeDef::new(Rc::new(make_ellipse(cx, cy, rx, ry)), Markers::No) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_points() { + assert_eq!( + Points::parse_str(" 1 2 ").unwrap(), + Points(vec![(1.0, 2.0)]) + ); + assert_eq!( + Points::parse_str("1 2 3 4").unwrap(), + Points(vec![(1.0, 2.0), (3.0, 4.0)]) + ); + assert_eq!( + Points::parse_str("1,2,3,4").unwrap(), + Points(vec![(1.0, 2.0), (3.0, 4.0)]) + ); + assert_eq!( + Points::parse_str("1,2 3,4").unwrap(), + Points(vec![(1.0, 2.0), (3.0, 4.0)]) + ); + assert_eq!( + Points::parse_str("1,2 -3,4").unwrap(), + Points(vec![(1.0, 2.0), (-3.0, 4.0)]) + ); + assert_eq!( + Points::parse_str("1,2,-3,4").unwrap(), + Points(vec![(1.0, 2.0), (-3.0, 4.0)]) + ); + } + + #[test] + fn errors_on_invalid_points() { + assert!(Points::parse_str("-1-2-3-4").is_err()); + assert!(Points::parse_str("1 2-3,-4").is_err()); + } +} diff --git a/rsvg/src/space.rs b/rsvg/src/space.rs new file mode 100644 index 00000000..50f52807 --- /dev/null +++ b/rsvg/src/space.rs @@ -0,0 +1,184 @@ +//! Processing of the `xml:space` attribute. + +use itertools::Itertools; + +pub struct NormalizeDefault { + pub has_element_before: bool, + pub has_element_after: bool, +} + +pub enum XmlSpaceNormalize { + Default(NormalizeDefault), + Preserve, +} + +/// Implements `xml:space` handling per the SVG spec +/// +/// Normalizes a string as it comes out of the XML parser's handler +/// for character data according to the SVG rules in +/// <https://www.w3.org/TR/SVG/text.html#WhiteSpace> +pub fn xml_space_normalize(mode: XmlSpaceNormalize, s: &str) -> String { + match mode { + XmlSpaceNormalize::Default(d) => normalize_default(d, s), + XmlSpaceNormalize::Preserve => normalize_preserve(s), + } +} + +// From https://www.w3.org/TR/SVG/text.html#WhiteSpace +// +// When xml:space="default", the SVG user agent will do the following +// using a copy of the original character data content. First, it will +// remove all newline characters. Then it will convert all tab +// characters into space characters. Then, it will strip off all +// leading and trailing space characters. Then, all contiguous space +// characters will be consolidated. +fn normalize_default(elements: NormalizeDefault, mut s: &str) -> String { + if !elements.has_element_before { + s = s.trim_start(); + } + + if !elements.has_element_after { + s = s.trim_end(); + } + + s.chars() + .filter(|ch| *ch != '\n') + .map(|ch| match ch { + '\t' => ' ', + c => c, + }) + .coalesce(|current, next| match (current, next) { + (' ', ' ') => Ok(' '), + (_, _) => Err((current, next)), + }) + .collect::<String>() +} + +// From https://www.w3.org/TR/SVG/text.html#WhiteSpace +// +// When xml:space="preserve", the SVG user agent will do the following +// using a copy of the original character data content. It will +// convert all newline and tab characters into space characters. Then, +// it will draw all space characters, including leading, trailing and +// multiple contiguous space characters. Thus, when drawn with +// xml:space="preserve", the string "a b" (three spaces between "a" +// and "b") will produce a larger separation between "a" and "b" than +// "a b" (one space between "a" and "b"). +fn normalize_preserve(s: &str) -> String { + s.chars() + .map(|ch| match ch { + '\n' | '\t' => ' ', + + c => c, + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn xml_space_default() { + assert_eq!( + xml_space_normalize( + XmlSpaceNormalize::Default(NormalizeDefault { + has_element_before: false, + has_element_after: false, + }), + "\n WS example\n indented lines\n " + ), + "WS example indented lines" + ); + assert_eq!( + xml_space_normalize( + XmlSpaceNormalize::Default(NormalizeDefault { + has_element_before: false, + has_element_after: false, + }), + "\n \t \tWS \t\t\texample\n \t indented lines\t\t \n " + ), + "WS example indented lines" + ); + assert_eq!( + xml_space_normalize( + XmlSpaceNormalize::Default(NormalizeDefault { + has_element_before: false, + has_element_after: false, + }), + "\n \t \tWS \t\t\texample\n \t duplicate letters\t\t \n " + ), + "WS example duplicate letters" + ); + assert_eq!( + xml_space_normalize( + XmlSpaceNormalize::Default(NormalizeDefault { + has_element_before: false, + has_element_after: false, + }), + "\nWS example\nnon-indented lines\n " + ), + "WS examplenon-indented lines" + ); + assert_eq!( + xml_space_normalize( + XmlSpaceNormalize::Default(NormalizeDefault { + has_element_before: false, + has_element_after: false, + }), + "\nWS example\tnon-indented lines\n " + ), + "WS example non-indented lines" + ); + } + + #[test] + fn xml_space_default_with_elements() { + assert_eq!( + xml_space_normalize( + XmlSpaceNormalize::Default(NormalizeDefault { + has_element_before: true, + has_element_after: false, + }), + " foo \n\t bar " + ), + " foo bar" + ); + + assert_eq!( + xml_space_normalize( + XmlSpaceNormalize::Default(NormalizeDefault { + has_element_before: false, + has_element_after: true, + }), + " foo \nbar " + ), + "foo bar " + ); + } + + #[test] + fn xml_space_preserve() { + assert_eq!( + xml_space_normalize( + XmlSpaceNormalize::Preserve, + "\n WS example\n indented lines\n " + ), + " WS example indented lines " + ); + assert_eq!( + xml_space_normalize( + XmlSpaceNormalize::Preserve, + "\n \t \tWS \t\t\texample\n \t indented lines\t\t \n " + ), + " WS example indented lines " + ); + assert_eq!( + xml_space_normalize( + XmlSpaceNormalize::Preserve, + "\n \t \tWS \t\t\texample\n \t duplicate letters\t\t \n " + ), + " WS example duplicate letters " + ); + } +} diff --git a/rsvg/src/structure.rs b/rsvg/src/structure.rs new file mode 100644 index 00000000..7e09ed1a --- /dev/null +++ b/rsvg/src/structure.rs @@ -0,0 +1,632 @@ +//! Structural elements in SVG: the `g`, `switch`, `svg`, `use`, `symbol`, `clip_path`, `mask`, `link` elements. + +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::aspect_ratio::*; +use crate::bbox::BoundingBox; +use crate::coord_units::CoordUnits; +use crate::document::{AcquiredNodes, NodeId}; +use crate::drawing_ctx::{ClipMode, DrawingCtx, Viewport}; +use crate::element::{set_attribute, ElementData, ElementTrait}; +use crate::error::*; +use crate::href::{is_href, set_href}; +use crate::layout::StackingContext; +use crate::length::*; +use crate::node::{CascadedValues, Node, NodeBorrow, NodeDraw}; +use crate::parsers::{Parse, ParseValue}; +use crate::properties::ComputedValues; +use crate::rect::Rect; +use crate::session::Session; +use crate::viewbox::*; +use crate::xml::Attributes; + +#[derive(Default)] +pub struct Group(); + +impl ElementTrait for Group { + fn draw( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + let values = cascaded.get(); + + let elt = node.borrow_element(); + let stacking_ctx = StackingContext::new( + draw_ctx.session(), + acquired_nodes, + &elt, + values.transform(), + values, + ); + + draw_ctx.with_discrete_layer( + &stacking_ctx, + acquired_nodes, + viewport, + clipping, + None, + &mut |an, dc| node.draw_children(an, cascaded, viewport, dc, clipping), + ) + } +} + +/// A no-op node that does not render anything +/// +/// Sometimes we just need a node that can contain children, but doesn't +/// render itself or its children. This is just that kind of node. +#[derive(Default)] +pub struct NonRendering; + +impl ElementTrait for NonRendering {} + +/// The `<switch>` element. +#[derive(Default)] +pub struct Switch(); + +impl ElementTrait for Switch { + fn draw( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + let values = cascaded.get(); + + let elt = node.borrow_element(); + let stacking_ctx = StackingContext::new( + draw_ctx.session(), + acquired_nodes, + &elt, + values.transform(), + values, + ); + + draw_ctx.with_discrete_layer( + &stacking_ctx, + acquired_nodes, + viewport, + clipping, + None, + &mut |an, dc| { + if let Some(child) = node.children().filter(|c| c.is_element()).find(|c| { + let elt = c.borrow_element(); + elt.get_cond(dc.user_language()) + }) { + child.draw( + an, + &CascadedValues::clone_with_node(cascaded, &child), + viewport, + dc, + clipping, + ) + } else { + Ok(dc.empty_bbox()) + } + }, + ) + } +} + +/// Intrinsic dimensions of an SVG document fragment: its `width/height` properties and `viewBox` attribute. +/// +/// Note that in SVG2, `width` and `height` are properties, not +/// attributes. If either is omitted, it defaults to `auto`. which +/// computes to `100%`. +/// +/// The `viewBox` attribute can also be omitted, hence an `Option`. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct IntrinsicDimensions { + /// Computed value of the `width` property. + pub width: ULength<Horizontal>, + + /// Computed value of the `height` property. + pub height: ULength<Vertical>, + + /// Contents of the `viewBox` attribute. + pub vbox: Option<ViewBox>, +} + +/// The `<svg>` element. +/// +/// Note that its x/y/width/height are properties in SVG2, so they are +/// defined as part of [the properties machinery](properties.rs). +#[derive(Default)] +pub struct Svg { + preserve_aspect_ratio: AspectRatio, + vbox: Option<ViewBox>, +} + +impl Svg { + pub fn get_intrinsic_dimensions(&self, values: &ComputedValues) -> IntrinsicDimensions { + let w = match values.width().0 { + LengthOrAuto::Auto => ULength::<Horizontal>::parse_str("100%").unwrap(), + LengthOrAuto::Length(l) => l, + }; + + let h = match values.height().0 { + LengthOrAuto::Auto => ULength::<Vertical>::parse_str("100%").unwrap(), + LengthOrAuto::Length(l) => l, + }; + + IntrinsicDimensions { + width: w, + height: h, + vbox: self.vbox, + } + } + + fn get_unnormalized_offset( + &self, + values: &ComputedValues, + ) -> (Length<Horizontal>, Length<Vertical>) { + // these defaults are per the spec + let x = values.x().0; + let y = values.y().0; + + (x, y) + } + + fn get_unnormalized_size( + &self, + values: &ComputedValues, + ) -> (ULength<Horizontal>, ULength<Vertical>) { + // these defaults are per the spec + let w = match values.width().0 { + LengthOrAuto::Auto => ULength::<Horizontal>::parse_str("100%").unwrap(), + LengthOrAuto::Length(l) => l, + }; + let h = match values.height().0 { + LengthOrAuto::Auto => ULength::<Vertical>::parse_str("100%").unwrap(), + LengthOrAuto::Length(l) => l, + }; + (w, h) + } + + fn get_viewport( + &self, + params: &NormalizeParams, + values: &ComputedValues, + outermost: bool, + ) -> Rect { + // x & y attributes have no effect on outermost svg + // http://www.w3.org/TR/SVG/struct.html#SVGElement + let (nx, ny) = if outermost { + (0.0, 0.0) + } else { + let (x, y) = self.get_unnormalized_offset(values); + (x.to_user(params), y.to_user(params)) + }; + + let (w, h) = self.get_unnormalized_size(values); + let (nw, nh) = (w.to_user(params), h.to_user(params)); + + Rect::new(nx, ny, nx + nw, ny + nh) + } + + pub fn get_viewbox(&self) -> Option<ViewBox> { + self.vbox + } + + pub fn get_preserve_aspect_ratio(&self) -> AspectRatio { + self.preserve_aspect_ratio + } + + fn make_svg_viewport( + &self, + node: &Node, + cascaded: &CascadedValues<'_>, + current_viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + ) -> Option<Viewport> { + let values = cascaded.get(); + + let params = NormalizeParams::new(values, current_viewport); + + let has_parent = node.parent().is_some(); + + // FIXME: do we need to look at preserveAspectRatio.slice, like in DrawingCtx::draw_image()? + let clip_mode = if !values.is_overflow() && has_parent { + ClipMode::ClipToViewport + } else { + ClipMode::NoClip + }; + + let svg_viewport = self.get_viewport(¶ms, values, !has_parent); + + let is_measuring_toplevel_svg = !has_parent && draw_ctx.is_measuring(); + + let (viewport, vbox) = if is_measuring_toplevel_svg { + // We are obtaining the toplevel SVG's geometry. This means, don't care about the + // DrawingCtx's viewport, just use the SVG's intrinsic dimensions and see how far + // it wants to extend. + (svg_viewport, self.vbox) + } else { + ( + // The client's viewport overrides the toplevel's x/y/w/h viewport + if has_parent { + svg_viewport + } else { + draw_ctx.toplevel_viewport() + }, + // Use our viewBox if available, or try to derive one from + // the intrinsic dimensions. + self.vbox.or_else(|| { + Some(ViewBox::from(Rect::from_size( + svg_viewport.width(), + svg_viewport.height(), + ))) + }), + ) + }; + + draw_ctx.push_new_viewport( + current_viewport, + vbox, + viewport, + self.preserve_aspect_ratio, + clip_mode, + ) + } +} + +impl ElementTrait for Svg { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "preserveAspectRatio") => { + set_attribute(&mut self.preserve_aspect_ratio, attr.parse(value), session) + } + expanded_name!("", "viewBox") => { + set_attribute(&mut self.vbox, attr.parse(value), session) + } + _ => (), + } + } + } + + fn draw( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + let values = cascaded.get(); + + let elt = node.borrow_element(); + let stacking_ctx = StackingContext::new( + draw_ctx.session(), + acquired_nodes, + &elt, + values.transform(), + values, + ); + + draw_ctx.with_discrete_layer( + &stacking_ctx, + acquired_nodes, + viewport, // FIXME: should this be the svg_viewport from below? + clipping, + None, + &mut |an, dc| { + if let Some(svg_viewport) = self.make_svg_viewport(node, cascaded, viewport, dc) { + node.draw_children(an, cascaded, &svg_viewport, dc, clipping) + } else { + Ok(dc.empty_bbox()) + } + }, + ) + } +} + +/// The `<use>` element. +pub struct Use { + link: Option<NodeId>, + x: Length<Horizontal>, + y: Length<Vertical>, + width: ULength<Horizontal>, + height: ULength<Vertical>, +} + +impl Use { + fn get_rect(&self, params: &NormalizeParams) -> Rect { + let x = self.x.to_user(params); + let y = self.y.to_user(params); + let w = self.width.to_user(params); + let h = self.height.to_user(params); + + Rect::new(x, y, x + w, y + h) + } +} + +impl Default for Use { + fn default() -> Use { + Use { + link: None, + x: Default::default(), + y: Default::default(), + width: ULength::<Horizontal>::parse_str("100%").unwrap(), + height: ULength::<Vertical>::parse_str("100%").unwrap(), + } + } +} + +impl ElementTrait for Use { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + ref a if is_href(a) => { + let mut href = None; + set_attribute( + &mut href, + NodeId::parse(value).map(Some).attribute(attr.clone()), + session, + ); + set_href(a, &mut self.link, href); + } + expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), + expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), + expanded_name!("", "width") => { + set_attribute(&mut self.width, attr.parse(value), session) + } + expanded_name!("", "height") => { + set_attribute(&mut self.height, attr.parse(value), session) + } + _ => (), + } + } + } + + fn draw( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + if let Some(link) = self.link.as_ref() { + let values = cascaded.get(); + let params = NormalizeParams::new(values, viewport); + let rect = self.get_rect(¶ms); + + let stroke_paint = values.stroke().0.resolve( + acquired_nodes, + values.stroke_opacity().0, + values.color().0, + cascaded.context_fill.clone(), + cascaded.context_stroke.clone(), + draw_ctx.session(), + ); + + let fill_paint = values.fill().0.resolve( + acquired_nodes, + values.fill_opacity().0, + values.color().0, + cascaded.context_fill.clone(), + cascaded.context_stroke.clone(), + draw_ctx.session(), + ); + + draw_ctx.draw_from_use_node( + node, + acquired_nodes, + values, + rect, + link, + clipping, + viewport, + fill_paint, + stroke_paint, + ) + } else { + Ok(draw_ctx.empty_bbox()) + } + } +} + +/// The `<symbol>` element. +#[derive(Default)] +pub struct Symbol { + preserve_aspect_ratio: AspectRatio, + vbox: Option<ViewBox>, +} + +impl Symbol { + pub fn get_viewbox(&self) -> Option<ViewBox> { + self.vbox + } + + pub fn get_preserve_aspect_ratio(&self) -> AspectRatio { + self.preserve_aspect_ratio + } +} + +impl ElementTrait for Symbol { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "preserveAspectRatio") => { + set_attribute(&mut self.preserve_aspect_ratio, attr.parse(value), session) + } + expanded_name!("", "viewBox") => { + set_attribute(&mut self.vbox, attr.parse(value), session) + } + _ => (), + } + } + } +} + +coord_units!(ClipPathUnits, CoordUnits::UserSpaceOnUse); + +/// The `<clipPath>` element. +#[derive(Default)] +pub struct ClipPath { + units: ClipPathUnits, +} + +impl ClipPath { + pub fn get_units(&self) -> CoordUnits { + CoordUnits::from(self.units) + } +} + +impl ElementTrait for ClipPath { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + if attr.expanded() == expanded_name!("", "clipPathUnits") { + set_attribute(&mut self.units, attr.parse(value), session); + } + } + } +} + +coord_units!(MaskUnits, CoordUnits::ObjectBoundingBox); +coord_units!(MaskContentUnits, CoordUnits::UserSpaceOnUse); + +/// The `<mask>` element. +pub struct Mask { + x: Length<Horizontal>, + y: Length<Vertical>, + width: ULength<Horizontal>, + height: ULength<Vertical>, + + units: MaskUnits, + content_units: MaskContentUnits, +} + +impl Default for Mask { + fn default() -> Mask { + Mask { + // these values are per the spec + x: Length::<Horizontal>::parse_str("-10%").unwrap(), + y: Length::<Vertical>::parse_str("-10%").unwrap(), + width: ULength::<Horizontal>::parse_str("120%").unwrap(), + height: ULength::<Vertical>::parse_str("120%").unwrap(), + + units: MaskUnits::default(), + content_units: MaskContentUnits::default(), + } + } +} + +impl Mask { + pub fn get_units(&self) -> CoordUnits { + CoordUnits::from(self.units) + } + + pub fn get_content_units(&self) -> CoordUnits { + CoordUnits::from(self.content_units) + } + + pub fn get_rect(&self, params: &NormalizeParams) -> Rect { + let x = self.x.to_user(params); + let y = self.y.to_user(params); + let w = self.width.to_user(params); + let h = self.height.to_user(params); + + Rect::new(x, y, x + w, y + h) + } +} + +impl ElementTrait for Mask { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), + expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), + expanded_name!("", "width") => { + set_attribute(&mut self.width, attr.parse(value), session) + } + expanded_name!("", "height") => { + set_attribute(&mut self.height, attr.parse(value), session) + } + expanded_name!("", "maskUnits") => { + set_attribute(&mut self.units, attr.parse(value), session) + } + expanded_name!("", "maskContentUnits") => { + set_attribute(&mut self.content_units, attr.parse(value), session) + } + _ => (), + } + } + } +} + +/// The `<a>` element. +#[derive(Default)] +pub struct Link { + pub link: Option<String>, +} + +impl ElementTrait for Link { + fn set_attributes(&mut self, attrs: &Attributes, _session: &Session) { + for (attr, value) in attrs.iter() { + let expanded = attr.expanded(); + if is_href(&expanded) { + set_href(&expanded, &mut self.link, Some(value.to_owned())); + } + } + } + + fn draw( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + // If this element is inside of <text>, do not draw it. + // The <text> takes care of it. + for an in node.ancestors() { + if matches!(&*an.borrow_element_data(), ElementData::Text(_)) { + return Ok(draw_ctx.empty_bbox()); + } + } + + let cascaded = CascadedValues::clone_with_node(cascaded, node); + let values = cascaded.get(); + + let elt = node.borrow_element(); + + let link_is_empty = self.link.as_ref().map(|l| l.is_empty()).unwrap_or(true); + + let link_target = if link_is_empty { + None + } else { + self.link.clone() + }; + + let stacking_ctx = StackingContext::new_with_link( + draw_ctx.session(), + acquired_nodes, + &elt, + values.transform(), + values, + link_target, + ); + + draw_ctx.with_discrete_layer( + &stacking_ctx, + acquired_nodes, + viewport, + clipping, + None, + &mut |an, dc| node.draw_children(an, &cascaded, viewport, dc, clipping), + ) + } +} diff --git a/rsvg/src/style.rs b/rsvg/src/style.rs new file mode 100644 index 00000000..1afa2a00 --- /dev/null +++ b/rsvg/src/style.rs @@ -0,0 +1,84 @@ +//! The `style` element. + +use markup5ever::{expanded_name, local_name, namespace_url, ns}; + +use crate::element::{set_attribute, ElementTrait}; +use crate::error::*; +use crate::session::Session; +use crate::xml::Attributes; + +/// Represents the syntax used in the `<style>` node. +/// +/// Currently only "text/css" is supported. +/// +/// <https://www.w3.org/TR/SVG11/styling.html#StyleElementTypeAttribute> +/// <https://www.w3.org/TR/SVG11/styling.html#ContentStyleTypeAttribute> +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum StyleType { + TextCss, +} + +enum_default!(StyleType, StyleType::TextCss); + +impl StyleType { + fn parse(value: &str) -> Result<StyleType, ValueErrorKind> { + // https://html.spec.whatwg.org/multipage/semantics.html#the-style-element + // + // 4. If element's type attribute is present and its value is + // neither the empty string nor an ASCII case-insensitive + // match for "text/css", then return. + + if value.eq_ignore_ascii_case("text/css") { + Ok(StyleType::TextCss) + } else { + Err(ValueErrorKind::parse_error( + "invalid value for type attribute in style element", + )) + } + } +} + +/// Represents a `<style>` node. +/// +/// It does not render itself, and just holds CSS stylesheet information for the rest of +/// the code to use. +#[derive(Default)] +pub struct Style { + type_: StyleType, +} + +impl Style { + pub fn style_type(&self) -> StyleType { + self.type_ + } +} + +impl ElementTrait for Style { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + if attr.expanded() == expanded_name!("", "type") { + set_attribute( + &mut self.type_, + StyleType::parse(value).attribute(attr), + session, + ); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_style_type() { + assert_eq!(StyleType::parse("text/css").unwrap(), StyleType::TextCss); + } + + #[test] + fn invalid_style_type_yields_error() { + assert!(StyleType::parse("").is_err()); + assert!(StyleType::parse("some-other-stylesheet-language").is_err()); + } +} diff --git a/rsvg/src/surface_utils/iterators.rs b/rsvg/src/surface_utils/iterators.rs new file mode 100644 index 00000000..5227c62c --- /dev/null +++ b/rsvg/src/surface_utils/iterators.rs @@ -0,0 +1,247 @@ +//! Pixel iterators for `SharedImageSurface`. +use crate::rect::IRect; +use crate::util::clamp; + +use super::shared_surface::SharedImageSurface; +use super::{EdgeMode, Pixel}; + +/// Iterator over pixels of a `SharedImageSurface`. +#[derive(Debug, Clone, Copy)] +pub struct Pixels<'a> { + surface: &'a SharedImageSurface, + bounds: IRect, + x: u32, + y: u32, + offset: isize, +} + +/// Iterator over a (potentially out of bounds) rectangle of pixels of a `SharedImageSurface`. +#[derive(Debug, Clone, Copy)] +pub struct PixelRectangle<'a> { + surface: &'a SharedImageSurface, + bounds: IRect, + rectangle: IRect, + edge_mode: EdgeMode, + x: i32, + y: i32, +} + +impl<'a> Pixels<'a> { + /// Creates an iterator over the image surface pixels + #[inline] + pub fn new(surface: &'a SharedImageSurface) -> Self { + let bounds = IRect::from_size(surface.width(), surface.height()); + + Self::within(surface, bounds) + } + + /// Creates an iterator over the image surface pixels, constrained within the given bounds. + #[inline] + pub fn within(surface: &'a SharedImageSurface, bounds: IRect) -> Self { + // Sanity checks. + assert!(bounds.x0 >= 0); + assert!(bounds.x0 <= surface.width()); + assert!(bounds.x1 >= bounds.x0); + assert!(bounds.x1 <= surface.width()); + assert!(bounds.y0 >= 0); + assert!(bounds.y0 <= surface.height()); + assert!(bounds.y1 >= bounds.y0); + assert!(bounds.y1 <= surface.height()); + + Self { + surface, + bounds, + x: bounds.x0 as u32, + y: bounds.y0 as u32, + offset: bounds.y0 as isize * surface.stride() + bounds.x0 as isize * 4, + } + } +} + +impl<'a> PixelRectangle<'a> { + /// Creates an iterator over the image surface pixels + #[inline] + pub fn new(surface: &'a SharedImageSurface, rectangle: IRect, edge_mode: EdgeMode) -> Self { + let bounds = IRect::from_size(surface.width(), surface.height()); + + Self::within(surface, bounds, rectangle, edge_mode) + } + + /// Creates an iterator over the image surface pixels, constrained within the given bounds. + #[inline] + pub fn within( + surface: &'a SharedImageSurface, + bounds: IRect, + rectangle: IRect, + edge_mode: EdgeMode, + ) -> Self { + // Sanity checks. + assert!(bounds.x0 >= 0); + assert!(bounds.x0 <= surface.width()); + assert!(bounds.x1 >= bounds.x0); + assert!(bounds.x1 <= surface.width()); + assert!(bounds.y0 >= 0); + assert!(bounds.y0 <= surface.height()); + assert!(bounds.y1 >= bounds.y0); + assert!(bounds.y1 <= surface.height()); + + // Non-None EdgeMode values need at least one pixel available. + if edge_mode != EdgeMode::None { + assert!(bounds.x1 > bounds.x0); + assert!(bounds.y1 > bounds.y0); + } + + assert!(rectangle.x1 >= rectangle.x0); + assert!(rectangle.y1 >= rectangle.y0); + + Self { + surface, + bounds, + rectangle, + edge_mode, + x: rectangle.x0, + y: rectangle.y0, + } + } +} + +impl<'a> Iterator for Pixels<'a> { + type Item = (u32, u32, Pixel); + + #[inline] + fn next(&mut self) -> Option<Self::Item> { + // This means we hit the end on the last iteration. + if self.x == self.bounds.x1 as u32 || self.y == self.bounds.y1 as u32 { + return None; + } + + let rv = Some(( + self.x, + self.y, + self.surface.get_pixel_by_offset(self.offset), + )); + + if self.x + 1 == self.bounds.x1 as u32 { + self.x = self.bounds.x0 as u32; + self.y += 1; + self.offset += self.surface.stride() - (self.bounds.width() - 1) as isize * 4; + } else { + self.x += 1; + self.offset += 4; + } + + rv + } +} + +impl<'a> Iterator for PixelRectangle<'a> { + type Item = (i32, i32, Pixel); + + #[inline(always)] + fn next(&mut self) -> Option<Self::Item> { + // This means we hit the end on the last iteration. + if self.x == self.rectangle.x1 || self.y == self.rectangle.y1 { + return None; + } + + let rv = { + let get_pixel = |x, y| { + if !self.bounds.contains(x, y) { + match self.edge_mode { + EdgeMode::None => Pixel { + r: 0, + g: 0, + b: 0, + a: 0, + }, + EdgeMode::Duplicate => { + let x = clamp(x, self.bounds.x0, self.bounds.x1 - 1); + let y = clamp(y, self.bounds.y0, self.bounds.y1 - 1); + self.surface.get_pixel(x as u32, y as u32) + } + EdgeMode::Wrap => { + let wrap = |mut x, v| { + while x < 0 { + x += v; + } + x % v + }; + + let x = self.bounds.x0 + wrap(x - self.bounds.x0, self.bounds.width()); + let y = self.bounds.y0 + wrap(y - self.bounds.y0, self.bounds.height()); + self.surface.get_pixel(x as u32, y as u32) + } + } + } else { + self.surface.get_pixel(x as u32, y as u32) + } + }; + + Some((self.x, self.y, get_pixel(self.x, self.y))) + }; + + if self.x + 1 == self.rectangle.x1 { + self.x = self.rectangle.x0; + self.y += 1; + } else { + self.x += 1; + } + + rv + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::surface_utils::shared_surface::SurfaceType; + + #[test] + fn pixels_count() { + const WIDTH: i32 = 32; + const HEIGHT: i32 = 64; + + let surface = SharedImageSurface::empty(WIDTH, HEIGHT, SurfaceType::SRgb).unwrap(); + + // Full image. + assert_eq!(Pixels::new(&surface).count(), (WIDTH * HEIGHT) as usize); + + // 1-wide column. + let bounds = IRect::from_size(1, HEIGHT); + assert_eq!(Pixels::within(&surface, bounds).count(), HEIGHT as usize); + + // 1-tall row. + let bounds = IRect::from_size(WIDTH, 1); + assert_eq!(Pixels::within(&surface, bounds).count(), WIDTH as usize); + + // 1×1. + let bounds = IRect::from_size(1, 1); + assert_eq!(Pixels::within(&surface, bounds).count(), 1); + + // Nothing (x0 == x1). + let bounds = IRect::from_size(0, HEIGHT); + assert_eq!(Pixels::within(&surface, bounds).count(), 0); + + // Nothing (y0 == y1). + let bounds = IRect::from_size(WIDTH, 0); + assert_eq!(Pixels::within(&surface, bounds).count(), 0); + + // Nothing (x0 == x1, y0 == y1). + let bounds = IRect::new(0, 0, 0, 0); + assert_eq!(Pixels::within(&surface, bounds).count(), 0); + } + + #[test] + fn pixel_rectangle() { + const WIDTH: i32 = 32; + const HEIGHT: i32 = 64; + + let surface = SharedImageSurface::empty(WIDTH, HEIGHT, SurfaceType::SRgb).unwrap(); + + let rect_bounds = IRect::new(-8, -8, 8, 8); + assert_eq!( + PixelRectangle::new(&surface, rect_bounds, EdgeMode::None).count(), + (16 * 16) as usize + ); + } +} diff --git a/rsvg/src/surface_utils/mod.rs b/rsvg/src/surface_utils/mod.rs new file mode 100644 index 00000000..8529bf68 --- /dev/null +++ b/rsvg/src/surface_utils/mod.rs @@ -0,0 +1,338 @@ +//! Various utilities for working with Cairo image surfaces. + +use std::alloc; +use std::slice; + +pub mod iterators; +pub mod shared_surface; +pub mod srgb; + +// These two are for Cairo's platform-endian 0xaarrggbb pixels + +#[cfg(target_endian = "little")] +use rgb::alt::BGRA8; +#[cfg(target_endian = "little")] +#[allow(clippy::upper_case_acronyms)] +pub type CairoARGB = BGRA8; + +#[cfg(target_endian = "big")] +use rgb::alt::ARGB8; +#[cfg(target_endian = "big")] +#[allow(clippy::upper_case_acronyms)] +pub type CairoARGB = ARGB8; + +/// GdkPixbuf's endian-independent RGBA8 pixel layout. +pub type GdkPixbufRGBA = rgb::RGBA8; + +/// GdkPixbuf's packed RGB pixel layout. +pub type GdkPixbufRGB = rgb::RGB8; + +/// Analogous to `rgb::FromSlice`, to convert from `[T]` to `[CairoARGB]` +#[allow(clippy::upper_case_acronyms)] +pub trait AsCairoARGB { + /// Reinterpret slice as `CairoARGB` pixels. + fn as_cairo_argb(&self) -> &[CairoARGB]; + + /// Reinterpret mutable slice as `CairoARGB` pixels. + fn as_cairo_argb_mut(&mut self) -> &mut [CairoARGB]; +} + +// SAFETY: transmuting from u32 to CairoRGB is based on the following assumptions: +// * there are no invalid bit representations for ARGB +// * u32 and ARGB are the same size +// * u32 is sufficiently aligned +impl AsCairoARGB for [u32] { + fn as_cairo_argb(&self) -> &[CairoARGB] { + const LAYOUT_U32: alloc::Layout = alloc::Layout::new::<u32>(); + const LAYOUT_ARGB: alloc::Layout = alloc::Layout::new::<CairoARGB>(); + let _: [(); LAYOUT_U32.size()] = [(); LAYOUT_ARGB.size()]; + let _: [(); 0] = [(); LAYOUT_U32.align() % LAYOUT_ARGB.align()]; + unsafe { slice::from_raw_parts(self.as_ptr() as *const _, self.len()) } + } + + fn as_cairo_argb_mut(&mut self) -> &mut [CairoARGB] { + unsafe { slice::from_raw_parts_mut(self.as_mut_ptr() as *mut _, self.len()) } + } +} + +/// Modes which specify how the values of out of bounds pixels are computed. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum EdgeMode { + /// The nearest inbounds pixel value is returned. + Duplicate, + /// The image is extended by taking the color values from the opposite of the image. + /// + /// Imagine the image being tiled infinitely, with the original image at the origin. + Wrap, + /// Zero RGBA values are returned. + None, +} + +/// Trait to convert pixels in various formats to RGBA, for GdkPixbuf. +/// +/// GdkPixbuf unconditionally uses RGBA ordering regardless of endianness, +/// but we need to convert to it from Cairo's endian-dependent 0xaarrggbb. +pub trait ToGdkPixbufRGBA { + fn to_pixbuf_rgba(&self) -> GdkPixbufRGBA; +} + +/// Trait to convert pixels in various formats to our own Pixel layout. +pub trait ToPixel { + fn to_pixel(&self) -> Pixel; +} + +/// Trait to convert pixels in various formats to Cairo's endian-dependent 0xaarrggbb. +pub trait ToCairoARGB { + fn to_cairo_argb(&self) -> CairoARGB; +} + +impl ToGdkPixbufRGBA for Pixel { + #[inline] + fn to_pixbuf_rgba(&self) -> GdkPixbufRGBA { + GdkPixbufRGBA { + r: self.r, + g: self.g, + b: self.b, + a: self.a, + } + } +} + +impl ToPixel for CairoARGB { + #[inline] + fn to_pixel(&self) -> Pixel { + Pixel { + r: self.r, + g: self.g, + b: self.b, + a: self.a, + } + } +} + +impl ToPixel for GdkPixbufRGBA { + #[inline] + fn to_pixel(&self) -> Pixel { + Pixel { + r: self.r, + g: self.g, + b: self.b, + a: self.a, + } + } +} + +impl ToPixel for GdkPixbufRGB { + #[inline] + fn to_pixel(&self) -> Pixel { + Pixel { + r: self.r, + g: self.g, + b: self.b, + a: 255, + } + } +} + +impl ToCairoARGB for Pixel { + #[inline] + fn to_cairo_argb(&self) -> CairoARGB { + CairoARGB { + r: self.r, + g: self.g, + b: self.b, + a: self.a, + } + } +} + +/// Extension methods for `cairo::ImageSurfaceData`. +pub trait ImageSurfaceDataExt { + /// Sets the pixel at the given coordinates. Assumes the `ARgb32` format. + fn set_pixel(&mut self, stride: usize, pixel: Pixel, x: u32, y: u32); +} + +/// A pixel consisting of R, G, B and A values. +pub type Pixel = rgb::RGBA8; + +pub trait PixelOps { + fn premultiply(self) -> Self; + fn unpremultiply(self) -> Self; + fn diff(&self, other: &Self) -> Self; + fn to_luminance_mask(&self) -> Self; + fn to_u32(&self) -> u32; + fn from_u32(x: u32) -> Self; +} + +impl PixelOps for Pixel { + /// Returns an unpremultiplied value of this pixel. + /// + /// For a fully transparent pixel, a transparent black pixel will be returned. + #[inline] + fn unpremultiply(self) -> Self { + if self.a == 0 { + Self { + r: 0, + g: 0, + b: 0, + a: 0, + } + } else { + let alpha = f32::from(self.a) / 255.0; + self.map_rgb(|x| ((f32::from(x) / alpha) + 0.5) as u8) + } + } + + /// Returns a premultiplied value of this pixel. + #[inline] + fn premultiply(self) -> Self { + let a = self.a as u32; + self.map_rgb(|x| (((x as u32) * a + 127) / 255) as u8) + } + + #[inline] + fn diff(&self, other: &Pixel) -> Pixel { + self.iter() + .zip(other.iter()) + .map(|(l, r)| (l as i32 - r as i32).unsigned_abs() as u8) + .collect() + } + + /// Returns a 'mask' pixel with only the alpha channel + /// + /// Assuming, the pixel is linear RGB (not sRGB) + /// y = luminance + /// Y = 0.2126 R + 0.7152 G + 0.0722 B + /// 1.0 opacity = 255 + /// + /// When Y = 1.0, pixel for mask should be 0xFFFFFFFF + /// (you get 1.0 luminance from 255 from R, G and B) + /// + /// r_mult = 0xFFFFFFFF / (255.0 * 255.0) * .2126 = 14042.45 ~= 14042 + /// g_mult = 0xFFFFFFFF / (255.0 * 255.0) * .7152 = 47239.69 ~= 47240 + /// b_mult = 0xFFFFFFFF / (255.0 * 255.0) * .0722 = 4768.88 ~= 4769 + /// + /// This allows for the following expected behaviour: + /// (we only care about the most significant byte) + /// if pixel = 0x00FFFFFF, pixel' = 0xFF...... + /// if pixel = 0x00020202, pixel' = 0x02...... + + /// if pixel = 0x00000000, pixel' = 0x00...... + #[inline] + fn to_luminance_mask(&self) -> Self { + let r = u32::from(self.r); + let g = u32::from(self.g); + let b = u32::from(self.b); + + Self { + r: 0, + g: 0, + b: 0, + a: (((r * 14042 + g * 47240 + b * 4769) * 255) >> 24) as u8, + } + } + + /// Returns the pixel value as a `u32`, in the same format as `cairo::Format::ARgb32`. + #[inline] + fn to_u32(&self) -> u32 { + (u32::from(self.a) << 24) + | (u32::from(self.r) << 16) + | (u32::from(self.g) << 8) + | u32::from(self.b) + } + + /// Converts a `u32` in the same format as `cairo::Format::ARgb32` into a `Pixel`. + #[inline] + fn from_u32(x: u32) -> Self { + Self { + r: ((x >> 16) & 0xFF) as u8, + g: ((x >> 8) & 0xFF) as u8, + b: (x & 0xFF) as u8, + a: ((x >> 24) & 0xFF) as u8, + } + } +} + +impl<'a> ImageSurfaceDataExt for cairo::ImageSurfaceData<'a> { + #[inline] + fn set_pixel(&mut self, stride: usize, pixel: Pixel, x: u32, y: u32) { + let this: &mut [u8] = &mut *self; + // SAFETY: this code assumes that cairo image surface data is correctly + // aligned for u32. This assumption is justified by the Cairo docs, + // which say this: + // + // https://cairographics.org/manual/cairo-Image-Surfaces.html#cairo-image-surface-create-for-data + // + // > This pointer must be suitably aligned for any kind of variable, + // > (for example, a pointer returned by malloc). + #[allow(clippy::cast_ptr_alignment)] + let this: &mut [u32] = + unsafe { slice::from_raw_parts_mut(this.as_mut_ptr() as *mut u32, this.len() / 4) }; + this.set_pixel(stride, pixel, x, y); + } +} +impl ImageSurfaceDataExt for [u8] { + #[inline] + fn set_pixel(&mut self, stride: usize, pixel: Pixel, x: u32, y: u32) { + use byteorder::{NativeEndian, WriteBytesExt}; + let mut this = &mut self[y as usize * stride + x as usize * 4..]; + this.write_u32::<NativeEndian>(pixel.to_u32()) + .expect("out of bounds pixel access on [u8]"); + } +} +impl ImageSurfaceDataExt for [u32] { + #[inline] + fn set_pixel(&mut self, stride: usize, pixel: Pixel, x: u32, y: u32) { + self[(y as usize * stride + x as usize * 4) / 4] = pixel.to_u32(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + + #[test] + fn pixel_diff() { + let a = Pixel::new(0x10, 0x20, 0xf0, 0x40); + assert_eq!(a, a.diff(&Pixel::default())); + let b = Pixel::new(0x50, 0xff, 0x20, 0x10); + assert_eq!(a.diff(&b), Pixel::new(0x40, 0xdf, 0xd0, 0x30)); + } + + // Floating-point reference implementation + fn premultiply_float(pixel: Pixel) -> Pixel { + let alpha = f64::from(pixel.a) / 255.0; + pixel.map_rgb(|x| ((f64::from(x) * alpha) + 0.5) as u8) + } + + prop_compose! { + fn arbitrary_pixel()(a: u8, r: u8, g: u8, b: u8) -> Pixel { + Pixel { r, g, b, a } + } + } + + proptest! { + #[test] + fn pixel_premultiply(pixel in arbitrary_pixel()) { + prop_assert_eq!(pixel.premultiply(), premultiply_float(pixel)); + } + + #[test] + fn pixel_unpremultiply(pixel in arbitrary_pixel()) { + let roundtrip = pixel.premultiply().unpremultiply(); + if pixel.a == 0 { + prop_assert_eq!(roundtrip, Pixel::default()); + } else { + // roundtrip can't be perfect, the accepted error depends on alpha + let tolerance = 0xff / pixel.a; + let diff = roundtrip.diff(&pixel); + prop_assert!(diff.r <= tolerance, "red component value differs by more than {}: {:?}", tolerance, roundtrip); + prop_assert!(diff.g <= tolerance, "green component value differs by more than {}: {:?}", tolerance, roundtrip); + prop_assert!(diff.b <= tolerance, "blue component value differs by more than {}: {:?}", tolerance, roundtrip); + + prop_assert_eq!(pixel.a, roundtrip.a); + } + } + } +} diff --git a/rsvg/src/surface_utils/shared_surface.rs b/rsvg/src/surface_utils/shared_surface.rs new file mode 100644 index 00000000..b94f2ebb --- /dev/null +++ b/rsvg/src/surface_utils/shared_surface.rs @@ -0,0 +1,1477 @@ +//! Shared access to Cairo image surfaces. +use std::cmp::min; +use std::marker::PhantomData; +use std::ptr::NonNull; +use std::slice; + +use gdk_pixbuf::{Colorspace, Pixbuf}; +use nalgebra::{storage::Storage, Dim, Matrix}; +use rgb::FromSlice; + +use crate::error::*; +use crate::rect::{IRect, Rect}; +use crate::surface_utils::srgb; +use crate::util::clamp; + +use super::{ + iterators::{PixelRectangle, Pixels}, + AsCairoARGB, CairoARGB, EdgeMode, ImageSurfaceDataExt, Pixel, PixelOps, ToCairoARGB, + ToGdkPixbufRGBA, ToPixel, +}; + +/// Types of pixel data in a `ImageSurface`. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum SurfaceType { + /// The pixel data is in the sRGB color space. + SRgb, + /// The pixel data is in the linear sRGB color space. + LinearRgb, + /// The pixel data is alpha-only (contains meaningful data only in the alpha channel). + /// + /// A number of methods are optimized for alpha-only surfaces. For example, linearization and + /// unlinearization have no effect for alpha-only surfaces. + AlphaOnly, +} + +impl SurfaceType { + /// Combines surface types + /// + /// If combining two alpha-only surfaces, the result is alpha-only. + /// If one is alpha-only, the result is the other. + /// If none is alpha-only, the types should be the same. + /// + /// # Panics + /// Panics if the surface types are not alpha-only and differ. + pub fn combine(self, other: SurfaceType) -> SurfaceType { + match (self, other) { + (SurfaceType::AlphaOnly, t) => t, + (t, SurfaceType::AlphaOnly) => t, + (t1, t2) if t1 == t2 => t1, + _ => panic!(), + } + } +} + +/// Operators supported by `ImageSurface<Shared>::compose`. +pub enum Operator { + Over, + In, + Out, + Atop, + Xor, + Multiply, + Screen, + Darken, + Lighten, + Overlay, + ColorDodge, + ColorBurn, + HardLight, + SoftLight, + Difference, + Exclusion, + HslHue, + HslSaturation, + HslColor, + HslLuminosity, +} + +/// Wrapper for a Cairo image surface that enforces exclusive access when modifying it. +/// +/// Shared access to `cairo::ImageSurface` is tricky since a read-only borrowed reference +/// can still be cloned and then modified. We can't simply use `cairo::ImageSurface::data()` +/// because in the filter code we have surfaces referenced from multiple places and it would +/// probably add more complexity to remove that and start passing around references. +/// +/// This wrapper asserts the uniqueness of its image surface. +/// +/// It uses the typestate pattern to ensure that the surface can be modified only when +/// it is in the `Exclusive` state, while in the `Shared` state it only allows read-only access. +#[derive(Debug, Clone)] +pub struct ImageSurface<T> { + surface: cairo::ImageSurface, + + data_ptr: NonNull<u8>, // *const. + width: i32, + height: i32, + stride: isize, + + surface_type: SurfaceType, + + _state: PhantomData<T>, +} + +#[derive(Debug, Clone)] +pub struct Shared; + +/// Shared state of `ImageSurface` +pub type SharedImageSurface = ImageSurface<Shared>; + +#[derive(Debug, Clone)] +pub struct Exclusive; + +/// Exclusive state of `ImageSurface` +pub type ExclusiveImageSurface = ImageSurface<Exclusive>; + +// The access is read-only, the ref-counting on an `cairo::ImageSurface` is atomic. +unsafe impl Sync for SharedImageSurface {} + +/// A compile-time blur direction variable. +pub trait BlurDirection { + const IS_VERTICAL: bool; +} + +/// Vertical blur direction. +pub enum Vertical {} +/// Horizontal blur direction. +pub enum Horizontal {} + +impl BlurDirection for Vertical { + const IS_VERTICAL: bool = true; +} + +impl BlurDirection for Horizontal { + const IS_VERTICAL: bool = false; +} + +/// A compile-time alpha-only marker variable. +pub trait IsAlphaOnly { + const IS_ALPHA_ONLY: bool; +} + +/// Alpha-only. +pub enum AlphaOnly {} +/// Not alpha-only. +pub enum NotAlphaOnly {} + +/// Iterator over the rows of a `SharedImageSurface`. +pub struct Rows<'a> { + surface: &'a SharedImageSurface, + next_row: i32, +} + +/// Iterator over the mutable rows of an `ExclusiveImageSurface`. +pub struct RowsMut<'a> { + // Keep an ImageSurfaceData here instead of a raw mutable pointer to the bytes, + // so that the ImageSurfaceData will mark the surface as dirty when it is dropped. + data: cairo::ImageSurfaceData<'a>, + + width: i32, + height: i32, + stride: i32, + + next_row: i32, +} + +impl IsAlphaOnly for AlphaOnly { + const IS_ALPHA_ONLY: bool = true; +} + +impl IsAlphaOnly for NotAlphaOnly { + const IS_ALPHA_ONLY: bool = false; +} + +impl<T> ImageSurface<T> { + /// Returns the surface width. + #[inline] + pub fn width(&self) -> i32 { + self.width + } + + /// Returns the surface height. + #[inline] + pub fn height(&self) -> i32 { + self.height + } + + /// Returns the surface stride. + #[inline] + pub fn stride(&self) -> isize { + self.stride + } +} + +impl ImageSurface<Shared> { + /// Creates a `SharedImageSurface` from a unique `cairo::ImageSurface`. + /// + /// # Panics + /// Panics if the surface format isn't `ARgb32` and if the surface is not unique, that is, its + /// reference count isn't 1. + #[inline] + pub fn wrap( + surface: cairo::ImageSurface, + surface_type: SurfaceType, + ) -> Result<SharedImageSurface, cairo::Error> { + // get_pixel() assumes ARgb32. + assert_eq!(surface.format(), cairo::Format::ARgb32); + + let reference_count = + unsafe { cairo::ffi::cairo_surface_get_reference_count(surface.to_raw_none()) }; + assert_eq!(reference_count, 1); + + let (width, height) = (surface.width(), surface.height()); + + // Cairo allows zero-sized surfaces, but it does malloc(0), whose result + // is implementation-defined. So, we can't assume NonNull below. This is + // why we disallow zero-sized surfaces here. + assert!(width > 0 && height > 0); + + surface.flush(); + + let data_ptr = NonNull::new(unsafe { + cairo::ffi::cairo_image_surface_get_data(surface.to_raw_none()) + }) + .unwrap(); + + let stride = surface.stride() as isize; + + Ok(SharedImageSurface { + surface, + data_ptr, + width, + height, + stride, + surface_type, + _state: PhantomData, + }) + } + + /// Creates a `SharedImageSurface` copying from a `cairo::ImageSurface`, even if it + /// does not have a reference count of 1. + #[inline] + pub fn copy_from_surface(surface: &cairo::ImageSurface) -> Result<Self, cairo::Error> { + let copy = + cairo::ImageSurface::create(cairo::Format::ARgb32, surface.width(), surface.height())?; + + { + let cr = cairo::Context::new(©)?; + cr.set_source_surface(surface, 0f64, 0f64)?; + cr.paint()?; + } + + SharedImageSurface::wrap(copy, SurfaceType::SRgb) + } + + /// Creates an empty `SharedImageSurface` of the given size and `type`. + #[inline] + pub fn empty(width: i32, height: i32, surface_type: SurfaceType) -> Result<Self, cairo::Error> { + let s = cairo::ImageSurface::create(cairo::Format::ARgb32, width, height)?; + + SharedImageSurface::wrap(s, surface_type) + } + + /// Converts this `SharedImageSurface` back into a Cairo image surface. + #[inline] + pub fn into_image_surface(self) -> Result<cairo::ImageSurface, cairo::Error> { + let reference_count = + unsafe { cairo::ffi::cairo_surface_get_reference_count(self.surface.to_raw_none()) }; + + if reference_count == 1 { + Ok(self.surface) + } else { + // If there are any other references, copy the underlying surface. + self.copy_surface(IRect::from_size(self.width, self.height)) + } + } + + pub fn from_pixbuf( + pixbuf: &Pixbuf, + content_type: Option<&str>, + mime_data: Option<Vec<u8>>, + ) -> Result<SharedImageSurface, cairo::Error> { + assert!(pixbuf.colorspace() == Colorspace::Rgb); + assert!(pixbuf.bits_per_sample() == 8); + + let n_channels = pixbuf.n_channels(); + assert!(n_channels == 3 || n_channels == 4); + let has_alpha = n_channels == 4; + + let width = pixbuf.width(); + let height = pixbuf.height(); + let stride = pixbuf.rowstride() as usize; + assert!(width > 0 && height > 0 && stride > 0); + + let pixbuf_data = unsafe { pixbuf.pixels() }; + + let mut surf = ExclusiveImageSurface::new(width, height, SurfaceType::SRgb)?; + + // We use chunks(), not chunks_exact(), because gdk-pixbuf tends + // to make the last row *not* have the full stride (i.e. it is + // only as wide as the pixels in that row). + let pixbuf_rows = pixbuf_data.chunks(stride).take(height as usize); + + if has_alpha { + pixbuf_rows + .map(|row| row.as_rgba()) + .zip(surf.rows_mut()) + .flat_map(|(src_row, dest_row)| src_row.iter().zip(dest_row.iter_mut())) + .for_each(|(src, dest)| *dest = src.to_pixel().premultiply().to_cairo_argb()); + } else { + pixbuf_rows + .map(|row| row.as_rgb()) + .zip(surf.rows_mut()) + .flat_map(|(src_row, dest_row)| src_row.iter().zip(dest_row.iter_mut())) + .for_each(|(src, dest)| *dest = src.to_pixel().to_cairo_argb()); + } + + if let (Some(content_type), Some(bytes)) = (content_type, mime_data) { + surf.surface.set_mime_data(content_type, bytes)?; + } + + surf.share() + } + + pub fn to_pixbuf(&self) -> Option<Pixbuf> { + let width = self.width(); + let height = self.height(); + + let pixbuf = Pixbuf::new(Colorspace::Rgb, true, 8, width, height)?; + + assert!(pixbuf.colorspace() == Colorspace::Rgb); + assert!(pixbuf.bits_per_sample() == 8); + assert!(pixbuf.n_channels() == 4); + + let pixbuf_data = unsafe { pixbuf.pixels() }; + let stride = pixbuf.rowstride() as usize; + + // We use chunks_mut(), not chunks_exact_mut(), because gdk-pixbuf tends + // to make the last row *not* have the full stride (i.e. it is + // only as wide as the pixels in that row). + pixbuf_data + .chunks_mut(stride) + .take(height as usize) + .map(|row| row.as_rgba_mut()) + .zip(self.rows()) + .flat_map(|(dest_row, src_row)| src_row.iter().zip(dest_row.iter_mut())) + .for_each(|(src, dest)| *dest = src.to_pixel().unpremultiply().to_pixbuf_rgba()); + + Some(pixbuf) + } + + /// Returns `true` if the surface contains meaningful data only in the alpha channel. + #[inline] + fn is_alpha_only(&self) -> bool { + self.surface_type == SurfaceType::AlphaOnly + } + + /// Returns the type of this surface. + #[inline] + pub fn surface_type(&self) -> SurfaceType { + self.surface_type + } + + /// Retrieves the pixel value at the given coordinates. + #[inline] + pub fn get_pixel(&self, x: u32, y: u32) -> Pixel { + assert!(x < self.width as u32); + assert!(y < self.height as u32); + + #[allow(clippy::cast_ptr_alignment)] + let value = unsafe { + *(self + .data_ptr + .as_ptr() + .offset(y as isize * self.stride + x as isize * 4) as *const u32) + }; + + Pixel::from_u32(value) + } + + /// Retrieves the pixel value by offset into the pixel data array. + #[inline] + pub fn get_pixel_by_offset(&self, offset: isize) -> Pixel { + assert!(offset < self.stride * self.height as isize); + + #[allow(clippy::cast_ptr_alignment)] + let value = unsafe { *(self.data_ptr.as_ptr().offset(offset) as *const u32) }; + Pixel::from_u32(value) + } + + /// Calls `set_source_surface()` on the given Cairo context. + #[inline] + pub fn set_as_source_surface( + &self, + cr: &cairo::Context, + x: f64, + y: f64, + ) -> Result<(), cairo::Error> { + cr.set_source_surface(&self.surface, x, y) + } + + /// Creates a Cairo surface pattern from the surface + pub fn to_cairo_pattern(&self) -> cairo::SurfacePattern { + cairo::SurfacePattern::create(&self.surface) + } + + /// Returns a new `cairo::ImageSurface` with the same contents as the one stored in this + /// `SharedImageSurface` within the given bounds. + fn copy_surface(&self, bounds: IRect) -> Result<cairo::ImageSurface, cairo::Error> { + let output_surface = + cairo::ImageSurface::create(cairo::Format::ARgb32, self.width, self.height)?; + + let cr = cairo::Context::new(&output_surface)?; + let r = cairo::Rectangle::from(bounds); + cr.rectangle(r.x(), r.y(), r.width(), r.height()); + cr.clip(); + + cr.set_source_surface(&self.surface, 0f64, 0f64)?; + cr.paint()?; + + Ok(output_surface) + } + + /// Scales the given surface by `x` and `y` into a surface `width`×`height` in size, clipped by + /// `bounds`. + pub fn scale_to( + &self, + width: i32, + height: i32, + bounds: IRect, + x: f64, + y: f64, + ) -> Result<SharedImageSurface, cairo::Error> { + let output_surface = cairo::ImageSurface::create(cairo::Format::ARgb32, width, height)?; + + { + let cr = cairo::Context::new(&output_surface)?; + let r = cairo::Rectangle::from(bounds); + cr.rectangle(r.x(), r.y(), r.width(), r.height()); + cr.clip(); + + cr.scale(x, y); + self.set_as_source_surface(&cr, 0.0, 0.0)?; + cr.paint()?; + } + + SharedImageSurface::wrap(output_surface, self.surface_type) + } + + /// Returns a scaled version of a surface and bounds. + #[inline] + pub fn scale( + &self, + bounds: IRect, + x: f64, + y: f64, + ) -> Result<(SharedImageSurface, IRect), cairo::Error> { + let new_width = (f64::from(self.width) * x).ceil() as i32; + let new_height = (f64::from(self.height) * y).ceil() as i32; + let new_bounds = bounds.scale(x, y); + + Ok(( + self.scale_to(new_width, new_height, new_bounds, x, y)?, + new_bounds, + )) + } + + /// Returns a surface with black background and alpha channel matching this surface. + pub fn extract_alpha(&self, bounds: IRect) -> Result<SharedImageSurface, cairo::Error> { + let mut output_surface = + cairo::ImageSurface::create(cairo::Format::ARgb32, self.width, self.height)?; + + let output_stride = output_surface.stride() as usize; + { + let mut output_data = output_surface.data().unwrap(); + + for (x, y, Pixel { a, .. }) in Pixels::within(self, bounds) { + let output_pixel = Pixel { + r: 0, + g: 0, + b: 0, + a, + }; + output_data.set_pixel(output_stride, output_pixel, x, y); + } + } + + SharedImageSurface::wrap(output_surface, SurfaceType::AlphaOnly) + } + + /// Returns a surface whose alpha channel for each pixel is equal to the + /// luminance of that pixel's unpremultiplied RGB values. The resulting + /// surface's RGB values are not meanignful; only the alpha channel has + /// useful luminance data. + /// + /// This is to get a mask suitable for use with cairo_mask_surface(). + pub fn to_luminance_mask(&self) -> Result<SharedImageSurface, cairo::Error> { + let bounds = IRect::from_size(self.width, self.height); + + let mut output_surface = + cairo::ImageSurface::create(cairo::Format::ARgb32, self.width, self.height)?; + + let stride = output_surface.stride() as usize; + { + let mut data = output_surface.data().unwrap(); + + for (x, y, pixel) in Pixels::within(self, bounds) { + data.set_pixel(stride, pixel.to_luminance_mask(), x, y); + } + } + + SharedImageSurface::wrap(output_surface, self.surface_type) + } + + /// Returns a surface with pre-multiplication of color values undone. + /// + /// HACK: this is storing unpremultiplied pixels in an ARGB32 image surface (which is supposed + /// to be premultiplied pixels). + pub fn unpremultiply(&self, bounds: IRect) -> Result<SharedImageSurface, cairo::Error> { + // Unpremultiplication doesn't affect the alpha channel. + if self.is_alpha_only() { + return Ok(self.clone()); + } + + let mut output_surface = + cairo::ImageSurface::create(cairo::Format::ARgb32, self.width, self.height)?; + + let stride = output_surface.stride() as usize; + { + let mut data = output_surface.data().unwrap(); + + for (x, y, pixel) in Pixels::within(self, bounds) { + data.set_pixel(stride, pixel.unpremultiply(), x, y); + } + } + + SharedImageSurface::wrap(output_surface, self.surface_type) + } + + /// Converts the surface to the linear sRGB color space. + #[inline] + pub fn to_linear_rgb(&self, bounds: IRect) -> Result<SharedImageSurface, cairo::Error> { + match self.surface_type { + SurfaceType::LinearRgb | SurfaceType::AlphaOnly => Ok(self.clone()), + _ => srgb::linearize_surface(self, bounds), + } + } + + /// Converts the surface to the sRGB color space. + #[inline] + pub fn to_srgb(&self, bounds: IRect) -> Result<SharedImageSurface, cairo::Error> { + match self.surface_type { + SurfaceType::SRgb | SurfaceType::AlphaOnly => Ok(self.clone()), + _ => srgb::unlinearize_surface(self, bounds), + } + } + + /// Performs a convolution. + /// + /// Note that `kernel` is rotated 180 degrees. + /// + /// The `target` parameter determines the position of the kernel relative to each pixel of the + /// image. The value of `(0, 0)` indicates that the top left pixel of the (180-degrees-rotated) + /// kernel corresponds to the current pixel, and the rest of the kernel is to the right and + /// bottom of the pixel. The value of `(cols / 2, rows / 2)` centers a kernel with an odd + /// number of rows and columns. + /// + /// # Panics + /// Panics if `kernel` has zero rows or columns. + pub fn convolve<R: Dim, C: Dim, S: Storage<f64, R, C>>( + &self, + bounds: IRect, + target: (i32, i32), + kernel: &Matrix<f64, R, C, S>, + edge_mode: EdgeMode, + ) -> Result<SharedImageSurface, cairo::Error> { + assert!(kernel.nrows() >= 1); + assert!(kernel.ncols() >= 1); + + let mut output_surface = + cairo::ImageSurface::create(cairo::Format::ARgb32, self.width, self.height)?; + + let output_stride = output_surface.stride() as usize; + { + let mut output_data = output_surface.data().unwrap(); + + if self.is_alpha_only() { + for (x, y, _pixel) in Pixels::within(self, bounds) { + let kernel_bounds = IRect::new( + x as i32 - target.0, + y as i32 - target.1, + x as i32 - target.0 + kernel.ncols() as i32, + y as i32 - target.1 + kernel.nrows() as i32, + ); + + let mut a = 0.0; + + for (x, y, pixel) in + PixelRectangle::within(self, bounds, kernel_bounds, edge_mode) + { + let kernel_x = (kernel_bounds.x1 - x - 1) as usize; + let kernel_y = (kernel_bounds.y1 - y - 1) as usize; + let factor = kernel[(kernel_y, kernel_x)]; + + a += f64::from(pixel.a) * factor; + } + + let convert = |x: f64| (clamp(x, 0.0, 255.0) + 0.5) as u8; + + let output_pixel = Pixel { + r: 0, + g: 0, + b: 0, + a: convert(a), + }; + + output_data.set_pixel(output_stride, output_pixel, x, y); + } + } else { + for (x, y, _pixel) in Pixels::within(self, bounds) { + let kernel_bounds = IRect::new( + x as i32 - target.0, + y as i32 - target.1, + x as i32 - target.0 + kernel.ncols() as i32, + y as i32 - target.1 + kernel.nrows() as i32, + ); + + let mut r = 0.0; + let mut g = 0.0; + let mut b = 0.0; + let mut a = 0.0; + + for (x, y, pixel) in + PixelRectangle::within(self, bounds, kernel_bounds, edge_mode) + { + let kernel_x = (kernel_bounds.x1 - x - 1) as usize; + let kernel_y = (kernel_bounds.y1 - y - 1) as usize; + let factor = kernel[(kernel_y, kernel_x)]; + + r += f64::from(pixel.r) * factor; + g += f64::from(pixel.g) * factor; + b += f64::from(pixel.b) * factor; + a += f64::from(pixel.a) * factor; + } + + let convert = |x: f64| (clamp(x, 0.0, 255.0) + 0.5) as u8; + + let output_pixel = Pixel { + r: convert(r), + g: convert(g), + b: convert(b), + a: convert(a), + }; + + output_data.set_pixel(output_stride, output_pixel, x, y); + } + } + } + + SharedImageSurface::wrap(output_surface, self.surface_type) + } + + /// Performs a horizontal or vertical box blur. + /// + /// The `target` parameter determines the position of the kernel relative to each pixel of the + /// image. The value of `0` indicates that the first pixel of the kernel corresponds to the + /// current pixel, and the rest of the kernel is to the right or bottom of the pixel. The value + /// of `kernel_size / 2` centers a kernel with an odd size. + /// + /// # Panics + /// Panics if `kernel_size` is `0` or if `target >= kernel_size`. + // This is public (and not inlined into box_blur()) for the purpose of accessing it from the + // benchmarks. + pub fn box_blur_loop<B: BlurDirection, A: IsAlphaOnly>( + &self, + output_surface: &mut cairo::ImageSurface, + bounds: IRect, + kernel_size: usize, + target: usize, + ) { + assert_ne!(kernel_size, 0); + assert!(target < kernel_size); + assert_eq!(self.is_alpha_only(), A::IS_ALPHA_ONLY); + + { + // The following code is needed for a parallel implementation of the blur loop. The + // blurring is done either for each row or for each column of pixels, depending on the + // value of `vertical`, independently of the others. Naturally, we want to run the + // outer loop on a thread pool. + // + // The case of `vertical == false` is simple since the input image slice can be + // partitioned into chunks for each row of pixels and processed in parallel with rayon. + // The case of `vertical == true`, however, is more involved because we can't just make + // mutable slices for all pixel columns (they would be overlapping which is forbidden + // by the aliasing rules). + // + // This is where the following struct comes into play: it stores a sub-slice of the + // pixel data and can be split at any row or column into two parts (similar to + // slice::split_at_mut()). + struct UnsafeSendPixelData<'a> { + width: u32, + height: u32, + stride: isize, + ptr: NonNull<u8>, + _marker: PhantomData<&'a mut ()>, + } + + unsafe impl<'a> Send for UnsafeSendPixelData<'a> {} + + impl<'a> UnsafeSendPixelData<'a> { + /// Creates a new `UnsafeSendPixelData`. + /// + /// # Safety + /// You must call `cairo_surface_mark_dirty()` on the surface once all instances of + /// `UnsafeSendPixelData` are dropped to make sure the pixel changes are committed + /// to Cairo. + #[inline] + unsafe fn new(surface: &mut cairo::ImageSurface) -> Self { + assert_eq!(surface.format(), cairo::Format::ARgb32); + let ptr = surface.data().unwrap().as_mut_ptr(); + + Self { + width: surface.width() as u32, + height: surface.height() as u32, + stride: surface.stride() as isize, + ptr: NonNull::new(ptr).unwrap(), + _marker: PhantomData, + } + } + + /// Sets a pixel value at the given coordinates. + #[inline] + fn set_pixel(&mut self, pixel: Pixel, x: u32, y: u32) { + assert!(x < self.width); + assert!(y < self.height); + + let value = pixel.to_u32(); + + #[allow(clippy::cast_ptr_alignment)] + unsafe { + let ptr = self + .ptr + .as_ptr() + .offset(y as isize * self.stride + x as isize * 4) + as *mut u32; + *ptr = value; + } + } + + /// Splits this `UnsafeSendPixelData` into two at the given row. + /// + /// The first one contains rows `0..index` (`index` not included) and the second one + /// contains rows `index..height`. + #[inline] + fn split_at_row(self, index: u32) -> (Self, Self) { + assert!(index <= self.height); + + ( + UnsafeSendPixelData { + width: self.width, + height: index, + stride: self.stride, + ptr: self.ptr, + _marker: PhantomData, + }, + UnsafeSendPixelData { + width: self.width, + height: self.height - index, + stride: self.stride, + ptr: NonNull::new(unsafe { + self.ptr.as_ptr().offset(index as isize * self.stride) + }) + .unwrap(), + _marker: PhantomData, + }, + ) + } + + /// Splits this `UnsafeSendPixelData` into two at the given column. + /// + /// The first one contains columns `0..index` (`index` not included) and the second + /// one contains columns `index..width`. + #[inline] + fn split_at_column(self, index: u32) -> (Self, Self) { + assert!(index <= self.width); + + ( + UnsafeSendPixelData { + width: index, + height: self.height, + stride: self.stride, + ptr: self.ptr, + _marker: PhantomData, + }, + UnsafeSendPixelData { + width: self.width - index, + height: self.height, + stride: self.stride, + ptr: NonNull::new(unsafe { + self.ptr.as_ptr().offset(index as isize * 4) + }) + .unwrap(), + _marker: PhantomData, + }, + ) + } + } + + let output_data = unsafe { UnsafeSendPixelData::new(output_surface) }; + + // Shift is target into the opposite direction. + let shift = (kernel_size - target) as i32; + let target = target as i32; + + // Convert to f64 once since we divide by it. + let kernel_size_f64 = kernel_size as f64; + let compute = |x: u32| (f64::from(x) / kernel_size_f64 + 0.5) as u8; + + // Depending on `vertical`, we're blurring either horizontally line-by-line, or + // vertically column-by-column. In the code below, the main axis is the axis along + // which the blurring happens (so if `vertical` is false, the main axis is the + // horizontal axis). The other axis is the outer loop axis. The code uses `i` and `j` + // for the other axis and main axis coordinates, respectively. + let (main_axis_min, main_axis_max, other_axis_min, other_axis_max) = if B::IS_VERTICAL { + (bounds.y0, bounds.y1, bounds.x0, bounds.x1) + } else { + (bounds.x0, bounds.x1, bounds.y0, bounds.y1) + }; + + // Helper function for getting the pixels. + let pixel = |i, j| { + let (x, y) = if B::IS_VERTICAL { (i, j) } else { (j, i) }; + + self.get_pixel(x as u32, y as u32) + }; + + // The following loop assumes the first row or column of `output_data` is the first row + // or column inside `bounds`. + let mut output_data = if B::IS_VERTICAL { + output_data.split_at_column(bounds.x0 as u32).1 + } else { + output_data.split_at_row(bounds.y0 as u32).1 + }; + + rayon::scope(|s| { + for i in other_axis_min..other_axis_max { + // Split off one row or column and launch its processing on another thread. + // Thanks to the initial split before the loop, there's no special case for the + // very first split. + let (mut current, remaining) = if B::IS_VERTICAL { + output_data.split_at_column(1) + } else { + output_data.split_at_row(1) + }; + + output_data = remaining; + + s.spawn(move |_| { + // Helper function for setting the pixels. + let mut set_pixel = |j, pixel| { + // We're processing rows or columns one-by-one, so the other coordinate + // is always 0. + let (x, y) = if B::IS_VERTICAL { (0, j) } else { (j, 0) }; + current.set_pixel(pixel, x, y); + }; + + // The idea is that since all weights of the box blur kernel are equal, for + // each step along the main axis, instead of recomputing the full sum, we + // can take the previous sum, subtract the "oldest" pixel value and add the + // "newest" pixel value. + // + // The sum is u32 so that it can fit MAXIMUM_KERNEL_SIZE * 255. + let mut sum_r = 0; + let mut sum_g = 0; + let mut sum_b = 0; + let mut sum_a = 0; + + // The whole sum needs to be computed for the first pixel. However, we know + // that values outside of bounds are transparent, so the loop starts on the + // first pixel in bounds. + for j in main_axis_min..min(main_axis_max, main_axis_min + shift) { + let Pixel { r, g, b, a } = pixel(i, j); + + if !A::IS_ALPHA_ONLY { + sum_r += u32::from(r); + sum_g += u32::from(g); + sum_b += u32::from(b); + } + + sum_a += u32::from(a); + } + + set_pixel( + main_axis_min as u32, + Pixel { + r: compute(sum_r), + g: compute(sum_g), + b: compute(sum_b), + a: compute(sum_a), + }, + ); + + // Now, go through all the other pixels. + // + // j - target - 1 >= main_axis_min + // j >= main_axis_min + target + 1 + let start_subtracting_at = main_axis_min + target + 1; + + // j + shift - 1 < main_axis_max + // j < main_axis_max - shift + 1 + let stop_adding_at = main_axis_max - shift + 1; + + for j in main_axis_min + 1..main_axis_max { + if j >= start_subtracting_at { + let old_pixel = pixel(i, j - target - 1); + + if !A::IS_ALPHA_ONLY { + sum_r -= u32::from(old_pixel.r); + sum_g -= u32::from(old_pixel.g); + sum_b -= u32::from(old_pixel.b); + } + + sum_a -= u32::from(old_pixel.a); + } + + if j < stop_adding_at { + let new_pixel = pixel(i, j + shift - 1); + + if !A::IS_ALPHA_ONLY { + sum_r += u32::from(new_pixel.r); + sum_g += u32::from(new_pixel.g); + sum_b += u32::from(new_pixel.b); + } + + sum_a += u32::from(new_pixel.a); + } + + set_pixel( + j as u32, + Pixel { + r: compute(sum_r), + g: compute(sum_g), + b: compute(sum_b), + a: compute(sum_a), + }, + ); + } + }); + } + }); + } + + // Don't forget to manually mark the surface as dirty (due to usage of + // `UnsafeSendPixelData`). + unsafe { cairo::ffi::cairo_surface_mark_dirty(output_surface.to_raw_none()) } + } + + /// Performs a horizontal or vertical box blur. + /// + /// The `target` parameter determines the position of the kernel relative to each pixel of the + /// image. The value of `0` indicates that the first pixel of the kernel corresponds to the + /// current pixel, and the rest of the kernel is to the right or bottom of the pixel. The value + /// of `kernel_size / 2` centers a kernel with an odd size. + /// + /// # Panics + /// Panics if `kernel_size` is `0` or if `target >= kernel_size`. + #[inline] + pub fn box_blur<B: BlurDirection>( + &self, + bounds: IRect, + kernel_size: usize, + target: usize, + ) -> Result<SharedImageSurface, cairo::Error> { + let mut output_surface = + cairo::ImageSurface::create(cairo::Format::ARgb32, self.width, self.height)?; + + if self.is_alpha_only() { + self.box_blur_loop::<B, AlphaOnly>(&mut output_surface, bounds, kernel_size, target); + } else { + self.box_blur_loop::<B, NotAlphaOnly>(&mut output_surface, bounds, kernel_size, target); + } + + SharedImageSurface::wrap(output_surface, self.surface_type) + } + + /// Fills the with a specified color. + #[inline] + pub fn flood( + &self, + bounds: IRect, + color: cssparser::RGBA, + ) -> Result<SharedImageSurface, cairo::Error> { + let output_surface = + cairo::ImageSurface::create(cairo::Format::ARgb32, self.width, self.height)?; + + if color.alpha > 0 { + let cr = cairo::Context::new(&output_surface)?; + let r = cairo::Rectangle::from(bounds); + cr.rectangle(r.x(), r.y(), r.width(), r.height()); + cr.clip(); + + cr.set_source_rgba( + f64::from(color.red_f32()), + f64::from(color.green_f32()), + f64::from(color.blue_f32()), + f64::from(color.alpha_f32()), + ); + cr.paint()?; + } + + SharedImageSurface::wrap(output_surface, self.surface_type) + } + + /// Offsets the image of the specified amount. + #[inline] + pub fn offset( + &self, + bounds: IRect, + dx: f64, + dy: f64, + ) -> Result<SharedImageSurface, cairo::Error> { + let output_surface = + cairo::ImageSurface::create(cairo::Format::ARgb32, self.width, self.height)?; + + // output_bounds contains all pixels within bounds, + // for which (x - ox) and (y - oy) also lie within bounds. + if let Some(output_bounds) = bounds + .translate((dx as i32, dy as i32)) + .intersection(&bounds) + { + let cr = cairo::Context::new(&output_surface)?; + let r = cairo::Rectangle::from(output_bounds); + cr.rectangle(r.x(), r.y(), r.width(), r.height()); + cr.clip(); + + self.set_as_source_surface(&cr, dx, dy)?; + cr.paint()?; + } + + SharedImageSurface::wrap(output_surface, self.surface_type) + } + + /// Returns a new surface of the same size, with the contents of the + /// specified image, optionally transformed to match a given box + #[inline] + pub fn paint_image( + &self, + bounds: Rect, + image: &SharedImageSurface, + rect: Option<Rect>, + ) -> Result<SharedImageSurface, cairo::Error> { + let output_surface = + cairo::ImageSurface::create(cairo::Format::ARgb32, self.width, self.height)?; + + if rect.is_none() || !rect.unwrap().is_empty() { + let cr = cairo::Context::new(&output_surface)?; + let r = cairo::Rectangle::from(bounds); + cr.rectangle(r.x(), r.y(), r.width(), r.height()); + cr.clip(); + + image.set_as_source_surface(&cr, 0f64, 0f64)?; + + if let Some(rect) = rect { + let mut matrix = cairo::Matrix::new( + rect.width() / f64::from(image.width()), + 0.0, + 0.0, + rect.height() / f64::from(image.height()), + rect.x0, + rect.y0, + ); + matrix.invert(); + + cr.source().set_matrix(matrix); + } + + cr.paint()?; + } + + SharedImageSurface::wrap(output_surface, image.surface_type) + } + + /// Creates a new surface with the size and content specified in `bounds` + /// + /// # Panics + /// Panics if `bounds` is an empty rectangle, since `SharedImageSurface` cannot + /// represent zero-sized images. + #[inline] + pub fn tile(&self, bounds: IRect) -> Result<SharedImageSurface, cairo::Error> { + // Cairo lets us create zero-sized surfaces, but the call to SharedImageSurface::wrap() + // below will panic in that case. So, disallow requesting a zero-sized subregion. + assert!(!bounds.is_empty()); + + let output_surface = + cairo::ImageSurface::create(cairo::Format::ARgb32, bounds.width(), bounds.height())?; + + { + let cr = cairo::Context::new(&output_surface)?; + self.set_as_source_surface(&cr, f64::from(-bounds.x0), f64::from(-bounds.y0))?; + cr.paint()?; + } + + SharedImageSurface::wrap(output_surface, self.surface_type) + } + + /// Returns a new surface of the same size, with the contents of the specified + /// image repeated to fill the bounds and starting from the given position. + #[inline] + pub fn paint_image_tiled( + &self, + bounds: IRect, + image: &SharedImageSurface, + x: i32, + y: i32, + ) -> Result<SharedImageSurface, cairo::Error> { + let output_surface = + cairo::ImageSurface::create(cairo::Format::ARgb32, self.width, self.height)?; + + { + let cr = cairo::Context::new(&output_surface)?; + + let ptn = image.to_cairo_pattern(); + ptn.set_extend(cairo::Extend::Repeat); + let mut mat = cairo::Matrix::identity(); + mat.translate(f64::from(-x), f64::from(-y)); + ptn.set_matrix(mat); + + let r = cairo::Rectangle::from(bounds); + cr.rectangle(r.x(), r.y(), r.width(), r.height()); + cr.clip(); + + cr.set_source(&ptn)?; + cr.paint()?; + } + + SharedImageSurface::wrap(output_surface, image.surface_type) + } + + /// Performs the combination of two input surfaces using Porter-Duff + /// compositing operators. + /// + /// # Panics + /// Panics if the two surface types are not compatible. + #[inline] + pub fn compose( + &self, + other: &SharedImageSurface, + bounds: IRect, + operator: Operator, + ) -> Result<SharedImageSurface, cairo::Error> { + let output_surface = other.copy_surface(bounds)?; + + { + let cr = cairo::Context::new(&output_surface)?; + let r = cairo::Rectangle::from(bounds); + cr.rectangle(r.x(), r.y(), r.width(), r.height()); + cr.clip(); + + self.set_as_source_surface(&cr, 0.0, 0.0)?; + cr.set_operator(operator.into()); + cr.paint()?; + } + + SharedImageSurface::wrap( + output_surface, + self.surface_type.combine(other.surface_type), + ) + } + + /// Performs the combination of two input surfaces. + /// + /// Each pixel of the resulting image is computed using the following formula: + /// `res = k1*i1*i2 + k2*i1 + k3*i2 + k4` + /// + /// # Panics + /// Panics if the two surface types are not compatible. + #[inline] + pub fn compose_arithmetic( + &self, + other: &SharedImageSurface, + bounds: IRect, + k1: f64, + k2: f64, + k3: f64, + k4: f64, + ) -> Result<SharedImageSurface, cairo::Error> { + let mut output_surface = ExclusiveImageSurface::new( + self.width, + self.height, + self.surface_type.combine(other.surface_type), + )?; + + composite_arithmetic(self, other, &mut output_surface, bounds, k1, k2, k3, k4); + + output_surface.share() + } + + pub fn rows(&self) -> Rows<'_> { + Rows { + surface: self, + next_row: 0, + } + } +} + +impl<'a> Iterator for Rows<'a> { + type Item = &'a [CairoARGB]; + + fn next(&mut self) -> Option<Self::Item> { + if self.next_row == self.surface.height { + return None; + } + + let row = self.next_row; + + self.next_row += 1; + + // SAFETY: this code assumes that cairo image surface data is correctly + // aligned for u32. This assumption is justified by the Cairo docs, + // which say this: + // + // https://cairographics.org/manual/cairo-Image-Surfaces.html#cairo-image-surface-create-for-data + // + // > This pointer must be suitably aligned for any kind of variable, + // > (for example, a pointer returned by malloc). + unsafe { + let row_ptr: *const u8 = self + .surface + .data_ptr + .as_ptr() + .offset(row as isize * self.surface.stride); + let row_of_u32: &[u32] = + slice::from_raw_parts(row_ptr as *const u32, self.surface.width as usize); + let pixels = row_of_u32.as_cairo_argb(); + assert!(pixels.len() == self.surface.width as usize); + Some(pixels) + } + } +} + +impl<'a> Iterator for RowsMut<'a> { + type Item = &'a mut [CairoARGB]; + + fn next(&mut self) -> Option<Self::Item> { + if self.next_row == self.height { + return None; + } + + let row = self.next_row as usize; + + self.next_row += 1; + + // SAFETY: this code assumes that cairo image surface data is correctly + // aligned for u32. This assumption is justified by the Cairo docs, + // which say this: + // + // https://cairographics.org/manual/cairo-Image-Surfaces.html#cairo-image-surface-create-for-data + // + // > This pointer must be suitably aligned for any kind of variable, + // > (for example, a pointer returned by malloc). + unsafe { + // We do this with raw pointers, instead of re-slicing the &mut self.data[....], + // because with the latter we can't synthesize an appropriate lifetime for + // the return value. + + let data_ptr = self.data.as_mut_ptr(); + let row_ptr: *mut u8 = data_ptr.offset(row as isize * self.stride as isize); + let row_of_u32: &mut [u32] = + slice::from_raw_parts_mut(row_ptr as *mut u32, self.width as usize); + let pixels = row_of_u32.as_cairo_argb_mut(); + assert!(pixels.len() == self.width as usize); + Some(pixels) + } + } +} + +/// Performs the arithmetic composite operation. Public for benchmarking. +#[inline] +pub fn composite_arithmetic( + surface1: &SharedImageSurface, + surface2: &SharedImageSurface, + output_surface: &mut ExclusiveImageSurface, + bounds: IRect, + k1: f64, + k2: f64, + k3: f64, + k4: f64, +) { + output_surface.modify(&mut |data, stride| { + for (x, y, pixel, pixel_2) in + Pixels::within(surface1, bounds).map(|(x, y, p)| (x, y, p, surface2.get_pixel(x, y))) + { + let i1a = f64::from(pixel.a) / 255f64; + let i2a = f64::from(pixel_2.a) / 255f64; + let oa = k1 * i1a * i2a + k2 * i1a + k3 * i2a + k4; + let oa = clamp(oa, 0f64, 1f64); + + // Contents of image surfaces are transparent by default, so if the resulting pixel is + // transparent there's no need to do anything. + if oa > 0f64 { + let compute = |i1, i2| { + let i1 = f64::from(i1) / 255f64; + let i2 = f64::from(i2) / 255f64; + + let o = k1 * i1 * i2 + k2 * i1 + k3 * i2 + k4; + let o = clamp(o, 0f64, oa); + + ((o * 255f64) + 0.5) as u8 + }; + + let output_pixel = Pixel { + r: compute(pixel.r, pixel_2.r), + g: compute(pixel.g, pixel_2.g), + b: compute(pixel.b, pixel_2.b), + a: ((oa * 255f64) + 0.5) as u8, + }; + + data.set_pixel(stride, output_pixel, x, y); + } + } + }); +} + +impl ImageSurface<Exclusive> { + #[inline] + pub fn new( + width: i32, + height: i32, + surface_type: SurfaceType, + ) -> Result<ExclusiveImageSurface, cairo::Error> { + let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, width, height)?; + + let (width, height) = (surface.width(), surface.height()); + + // Cairo allows zero-sized surfaces, but it does malloc(0), whose result + // is implementation-defined. So, we can't assume NonNull below. This is + // why we disallow zero-sized surfaces here. + assert!(width > 0 && height > 0); + + let data_ptr = NonNull::new(unsafe { + cairo::ffi::cairo_image_surface_get_data(surface.to_raw_none()) + }) + .unwrap(); + + let stride = surface.stride() as isize; + + Ok(ExclusiveImageSurface { + surface, + data_ptr, + width, + height, + stride, + surface_type, + _state: PhantomData, + }) + } + + #[inline] + pub fn share(self) -> Result<SharedImageSurface, cairo::Error> { + SharedImageSurface::wrap(self.surface, self.surface_type) + } + + /// Raw access to the image data as a slice + #[inline] + pub fn data(&mut self) -> cairo::ImageSurfaceData<'_> { + self.surface.data().unwrap() + } + + /// Modify the image data + #[inline] + pub fn modify(&mut self, draw_fn: &mut dyn FnMut(&mut cairo::ImageSurfaceData<'_>, usize)) { + let stride = self.stride() as usize; + let mut data = self.data(); + + draw_fn(&mut data, stride) + } + + /// Draw on the surface using cairo + #[inline] + pub fn draw( + &mut self, + draw_fn: &mut dyn FnMut(cairo::Context) -> Result<(), RenderingError>, + ) -> Result<(), RenderingError> { + let cr = cairo::Context::new(&self.surface)?; + draw_fn(cr) + } + + pub fn rows_mut(&mut self) -> RowsMut<'_> { + let width = self.surface.width(); + let height = self.surface.height(); + let stride = self.surface.stride(); + + let data = self.surface.data().unwrap(); + + RowsMut { + width, + height, + stride, + data, + next_row: 0, + } + } +} + +impl From<Operator> for cairo::Operator { + fn from(op: Operator) -> cairo::Operator { + use cairo::Operator as Cairo; + use Operator::*; + + match op { + Over => Cairo::Over, + In => Cairo::In, + Out => Cairo::Out, + Atop => Cairo::Atop, + Xor => Cairo::Xor, + Multiply => Cairo::Multiply, + Screen => Cairo::Screen, + Darken => Cairo::Darken, + Lighten => Cairo::Lighten, + Overlay => Cairo::Overlay, + ColorDodge => Cairo::ColorDodge, + ColorBurn => Cairo::ColorBurn, + HardLight => Cairo::HardLight, + SoftLight => Cairo::SoftLight, + Difference => Cairo::Difference, + Exclusion => Cairo::Exclusion, + HslHue => Cairo::HslHue, + HslSaturation => Cairo::HslSaturation, + HslColor => Cairo::HslColor, + HslLuminosity => Cairo::HslLuminosity, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::surface_utils::iterators::Pixels; + + #[test] + fn test_extract_alpha() { + const WIDTH: i32 = 32; + const HEIGHT: i32 = 64; + + let bounds = IRect::new(8, 24, 16, 48); + let full_bounds = IRect::from_size(WIDTH, HEIGHT); + + let mut surface = ExclusiveImageSurface::new(WIDTH, HEIGHT, SurfaceType::SRgb).unwrap(); + + // Fill the surface with some data. + { + let mut data = surface.data(); + + let mut counter = 0u16; + for x in data.iter_mut() { + *x = counter as u8; + counter = (counter + 1) % 256; + } + } + + let surface = surface.share().unwrap(); + let alpha = surface.extract_alpha(bounds).unwrap(); + + for (x, y, p, pa) in + Pixels::within(&surface, full_bounds).map(|(x, y, p)| (x, y, p, alpha.get_pixel(x, y))) + { + assert_eq!(pa.r, 0); + assert_eq!(pa.g, 0); + assert_eq!(pa.b, 0); + + if !bounds.contains(x as i32, y as i32) { + assert_eq!(pa.a, 0); + } else { + assert_eq!(pa.a, p.a); + } + } + } +} diff --git a/rsvg/src/surface_utils/srgb.rs b/rsvg/src/surface_utils/srgb.rs new file mode 100644 index 00000000..c745d0b4 --- /dev/null +++ b/rsvg/src/surface_utils/srgb.rs @@ -0,0 +1,95 @@ +//! Utility functions for dealing with sRGB colors. +//! +//! The constant values in this module are taken from <http://www.color.org/chardata/rgb/srgb.xalter> + +use crate::rect::IRect; +use crate::surface_utils::{ + iterators::Pixels, + shared_surface::{ExclusiveImageSurface, SharedImageSurface, SurfaceType}, + ImageSurfaceDataExt, Pixel, +}; + +// Include the linearization and unlinearization tables. +include!(concat!(env!("OUT_DIR"), "/srgb-codegen.rs")); + +/// Converts an sRGB color value to a linear sRGB color value (undoes the gamma correction). +#[inline] +pub fn linearize(c: u8) -> u8 { + LINEARIZE[usize::from(c)] +} + +/// Converts a linear sRGB color value to a normal sRGB color value (applies the gamma correction). +#[inline] +pub fn unlinearize(c: u8) -> u8 { + UNLINEARIZE[usize::from(c)] +} + +/// Processing loop of `map_unpremultiplied_components`. Extracted (and public) for benchmarking. +#[inline] +pub fn map_unpremultiplied_components_loop<F: Fn(u8) -> u8>( + surface: &SharedImageSurface, + output_surface: &mut ExclusiveImageSurface, + bounds: IRect, + f: F, +) { + output_surface.modify(&mut |data, stride| { + for (x, y, pixel) in Pixels::within(surface, bounds) { + if pixel.a > 0 { + let alpha = f64::from(pixel.a) / 255f64; + + let compute = |x| { + let x = f64::from(x) / alpha; // Unpremultiply alpha. + let x = (x + 0.5) as u8; // Round to nearest u8. + let x = f(x); + let x = f64::from(x) * alpha; // Premultiply alpha again. + (x + 0.5) as u8 + }; + + let output_pixel = Pixel { + r: compute(pixel.r), + g: compute(pixel.g), + b: compute(pixel.b), + a: pixel.a, + }; + + data.set_pixel(stride, output_pixel, x, y); + } + } + }); +} + +/// Applies the function to each pixel component after unpremultiplying. +fn map_unpremultiplied_components<F: Fn(u8) -> u8>( + surface: &SharedImageSurface, + bounds: IRect, + f: F, + new_type: SurfaceType, +) -> Result<SharedImageSurface, cairo::Error> { + let (width, height) = (surface.width(), surface.height()); + let mut output_surface = ExclusiveImageSurface::new(width, height, new_type)?; + map_unpremultiplied_components_loop(surface, &mut output_surface, bounds, f); + + output_surface.share() +} + +/// Converts an sRGB surface to a linear sRGB surface (undoes the gamma correction). +#[inline] +pub fn linearize_surface( + surface: &SharedImageSurface, + bounds: IRect, +) -> Result<SharedImageSurface, cairo::Error> { + assert_eq!(surface.surface_type(), SurfaceType::SRgb); + + map_unpremultiplied_components(surface, bounds, linearize, SurfaceType::LinearRgb) +} + +/// Converts a linear sRGB surface to a normal sRGB surface (applies the gamma correction). +#[inline] +pub fn unlinearize_surface( + surface: &SharedImageSurface, + bounds: IRect, +) -> Result<SharedImageSurface, cairo::Error> { + assert_eq!(surface.surface_type(), SurfaceType::LinearRgb); + + map_unpremultiplied_components(surface, bounds, unlinearize, SurfaceType::SRgb) +} diff --git a/rsvg/src/test_utils/compare_surfaces.rs b/rsvg/src/test_utils/compare_surfaces.rs new file mode 100644 index 00000000..06100db9 --- /dev/null +++ b/rsvg/src/test_utils/compare_surfaces.rs @@ -0,0 +1,112 @@ +use std::fmt; + +use crate::surface_utils::{ + iterators::Pixels, + shared_surface::{SharedImageSurface, SurfaceType}, + ImageSurfaceDataExt, Pixel, PixelOps, +}; + +use rgb::{ComponentMap, RGB}; + +pub enum BufferDiff { + DifferentSizes, + Diff(Diff), +} + +pub struct Diff { + pub num_pixels_changed: usize, + pub max_diff: u8, + pub surface: SharedImageSurface, +} + +impl fmt::Display for BufferDiff { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BufferDiff::DifferentSizes => write!(f, "different sizes"), + BufferDiff::Diff(diff) => diff.fmt(f), + } + } +} + +impl fmt::Display for Diff { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} pixels are different, with a maximum difference of {}", + self.num_pixels_changed, self.max_diff + ) + } +} + +#[inline] +fn emphasize(p: &Pixel) -> Pixel { + let emphasize_component = |c| { + // emphasize + let mut c = c as u32 * 4; + // make sure it's visible + if c > 0 { + c += 128; + } + c.min(255) as u8 + }; + p.map(emphasize_component) +} + +pub fn compare_surfaces( + surf_a: &SharedImageSurface, + surf_b: &SharedImageSurface, +) -> Result<BufferDiff, cairo::Error> { + let a_width = surf_a.width(); + let a_height = surf_a.height(); + + let b_width = surf_b.width(); + let b_height = surf_b.height(); + + if a_width != b_width || a_height != b_height { + return Ok(BufferDiff::DifferentSizes); + } + + let mut surf_diff = cairo::ImageSurface::create(cairo::Format::ARgb32, a_width, a_height)?; + let diff_stride = surf_diff.stride() as usize; + + let mut num_pixels_changed = 0; + let mut max_diff = 0; + + let black = Pixel::default().alpha(255); + + { + let mut diff_data = surf_diff.data().unwrap(); + + for ((xa, ya, pixel_a), (_, _, pixel_b)) in Pixels::new(surf_a).zip(Pixels::new(surf_b)) { + let dest = if pixel_a != pixel_b { + num_pixels_changed += 1; + + let pixel_diff = pixel_a.diff(&pixel_b); + + max_diff = pixel_diff.iter().fold(max_diff, |acc, c| acc.max(c)); + + let pixel_diff = emphasize(&pixel_diff); + + if pixel_diff.rgb() == RGB::default() { + // alpha only difference; convert alpha to gray + let a = pixel_diff.a; + pixel_diff.map_rgb(|_| a) + } else { + pixel_diff.alpha(255) + } + } else { + black + }; + + diff_data.set_pixel(diff_stride, dest, xa, ya); + } + } + + let surface = SharedImageSurface::wrap(surf_diff, SurfaceType::SRgb)?; + + Ok(BufferDiff::Diff(Diff { + num_pixels_changed, + max_diff, + surface, + })) +} diff --git a/rsvg/src/test_utils/mod.rs b/rsvg/src/test_utils/mod.rs new file mode 100644 index 00000000..c5aabb91 --- /dev/null +++ b/rsvg/src/test_utils/mod.rs @@ -0,0 +1,119 @@ +pub mod compare_surfaces; +pub mod reference_utils; + +use cairo; +use gio; +use glib; +use glib::translate::*; +use libc; +use std::env; +use std::ffi::CString; +use std::sync::Once; + +use crate::{ + surface_utils::shared_surface::{SharedImageSurface, SurfaceType}, + CairoRenderer, Loader, LoadingError, RenderingError, SvgHandle, +}; + +pub fn load_svg(input: &'static [u8]) -> Result<SvgHandle, LoadingError> { + let bytes = glib::Bytes::from_static(input); + let stream = gio::MemoryInputStream::from_bytes(&bytes); + + Loader::new().read_stream(&stream, None::<&gio::File>, None::<&gio::Cancellable>) +} + +#[derive(Copy, Clone)] +pub struct SurfaceSize(pub i32, pub i32); + +pub fn render_document<F: FnOnce(&cairo::Context)>( + svg: &SvgHandle, + surface_size: SurfaceSize, + cr_transform: F, + viewport: cairo::Rectangle, +) -> Result<SharedImageSurface, RenderingError> { + let renderer = CairoRenderer::new(svg); + + let SurfaceSize(width, height) = surface_size; + + let output = cairo::ImageSurface::create(cairo::Format::ARgb32, width, height).unwrap(); + + let res = { + let cr = cairo::Context::new(&output).expect("Failed to create a cairo context"); + cr_transform(&cr); + Ok(renderer.render_document(&cr, &viewport)?) + }; + + res.and_then(|_| Ok(SharedImageSurface::wrap(output, SurfaceType::SRgb)?)) +} + +#[cfg(system_deps_have_pangoft2)] +mod pango_ft2 { + use super::*; + use glib::prelude::*; + use pangocairo::FontMap; + + extern "C" { + // pango_fc_font_map_set_config (PangoFcFontMap *fcfontmap, + // FcConfig *fcconfig); + // This is not bound in gtk-rs, and PangoFcFontMap is not even exposed, so we'll bind it by hand. + fn pango_fc_font_map_set_config( + font_map: *mut libc::c_void, + config: *mut fontconfig_sys::FcConfig, + ); + } + + pub unsafe fn load_test_fonts() { + let font_paths = [ + "tests/resources/Ahem.ttf", + "tests/resources/NotoSansHebrew-Regular.ttf", + "tests/resources/Roboto-Regular.ttf", + "tests/resources/Roboto-Italic.ttf", + "tests/resources/Roboto-Bold.ttf", + "tests/resources/Roboto-BoldItalic.ttf", + ]; + + let config = fontconfig_sys::FcConfigCreate(); + if fontconfig_sys::FcConfigSetCurrent(config) == 0 { + panic!("Could not set a fontconfig configuration"); + } + + for path in &font_paths { + let path_cstring = CString::new(*path).unwrap(); + + if fontconfig_sys::FcConfigAppFontAddFile(config, path_cstring.as_ptr() as *const _) + == 0 + { + panic!("Could not load font file {} for tests; aborting", path,); + } + } + + let font_map = FontMap::for_font_type(cairo::FontType::FontTypeFt).unwrap(); + let raw_font_map: *mut pango::ffi::PangoFontMap = font_map.to_glib_none().0; + + pango_fc_font_map_set_config(raw_font_map as *mut _, config); + fontconfig_sys::FcConfigDestroy(config); + + FontMap::set_default(Some(&font_map.downcast::<pangocairo::FontMap>().unwrap())); + } +} + +#[cfg(system_deps_have_pangoft2)] +pub fn setup_font_map() { + unsafe { + self::pango_ft2::load_test_fonts(); + } +} + +#[cfg(not(system_deps_have_pangoft2))] +pub fn setup_font_map() {} + +pub fn setup_language() { + static ONCE: Once = Once::new(); + + ONCE.call_once(|| { + // For systemLanguage attribute tests. + // The trailing ":" is intentional to test gitlab#425. + env::set_var("LANGUAGE", "de:en_US:en:"); + env::set_var("LC_ALL", "de:en_US:en:"); + }); +} diff --git a/rsvg/src/test_utils/reference_utils.rs b/rsvg/src/test_utils/reference_utils.rs new file mode 100644 index 00000000..04e12009 --- /dev/null +++ b/rsvg/src/test_utils/reference_utils.rs @@ -0,0 +1,309 @@ +//! Utilities for the reference image test suite. +//! +//! This module has utility functions that are used in the test suite +//! to compare rendered surfaces to reference images. + +use cairo; + +use std::convert::TryFrom; +use std::env; +use std::fs::{self, File}; +use std::io::{BufReader, Read}; +use std::path::{Path, PathBuf}; +use std::sync::Once; + +use crate::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; +use crate::test_utils::{render_document, setup_font_map, SurfaceSize}; +use crate::{CairoRenderer, Loader}; + +use super::compare_surfaces::{compare_surfaces, BufferDiff, Diff}; +use super::load_svg; + +pub struct Reference(SharedImageSurface); + +impl Reference { + pub fn from_png<P>(path: P) -> Result<Self, cairo::IoError> + where + P: AsRef<Path>, + { + let file = File::open(path).map_err(cairo::IoError::Io)?; + let mut reader = BufReader::new(file); + let surface = surface_from_png(&mut reader)?; + Self::from_surface(surface) + } + + pub fn from_surface(surface: cairo::ImageSurface) -> Result<Self, cairo::IoError> { + let shared = SharedImageSurface::wrap(surface, SurfaceType::SRgb)?; + Ok(Self(shared)) + } +} + +pub trait Compare { + fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError>; +} + +impl Compare for &Reference { + fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError> { + compare_surfaces(&self.0, surface).map_err(cairo::IoError::from) + } +} + +impl Compare for Result<Reference, cairo::IoError> { + fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError> { + self.map(|reference| reference.compare(surface)) + .and_then(std::convert::identity) + } +} + +pub trait Evaluate { + fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str); +} + +impl Evaluate for BufferDiff { + /// Evaluates a BufferDiff and panics if there are relevant differences + /// + /// The `output_base_name` is used to write test results if the + /// surfaces are different. If this is `foo`, this will write + /// `foo-out.png` with the `output_surf` and `foo-diff.png` with a + /// visual diff between `output_surf` and the `Reference` that this + /// diff was created from. + /// + /// # Panics + /// + /// Will panic if the surfaces are too different to be acceptable. + fn evaluate(&self, output_surf: &SharedImageSurface, output_base_name: &str) { + match self { + BufferDiff::DifferentSizes => unreachable!("surfaces should be of the same size"), + + BufferDiff::Diff(diff) => { + if diff.distinguishable() { + println!( + "{}: {} pixels changed with maximum difference of {}", + output_base_name, diff.num_pixels_changed, diff.max_diff, + ); + + write_to_file(output_surf, output_base_name, "out"); + write_to_file(&diff.surface, output_base_name, "diff"); + + if diff.inacceptable() { + panic!("surfaces are too different"); + } + } + } + } + } +} + +impl Evaluate for Result<BufferDiff, cairo::IoError> { + fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str) { + self.as_ref() + .map(|diff| diff.evaluate(output_surface, output_base_name)) + .unwrap(); + } +} + +fn write_to_file(input: &SharedImageSurface, output_base_name: &str, suffix: &str) { + let path = output_dir().join(format!("{}-{}.png", output_base_name, suffix)); + println!("{}: {}", suffix, path.to_string_lossy()); + let mut output_file = File::create(path).unwrap(); + input + .clone() + .into_image_surface() + .unwrap() + .write_to_png(&mut output_file) + .unwrap(); +} + +/// Creates a directory for test output and returns its path. +/// +/// The location for the output directory is taken from the `TESTS_OUTPUT_DIR` environment +/// variable if that is set. Otherwise std::env::temp_dir() will be used, which is +/// a platform dependent location for temporary files. +/// +/// # Panics +/// +/// Will panic if the output directory can not be created. +pub fn output_dir() -> PathBuf { + let tempdir = || { + let mut path = env::temp_dir(); + path.push("rsvg-test-output"); + path + }; + let path = env::var_os("TESTS_OUTPUT_DIR").map_or_else(tempdir, PathBuf::from); + + fs::create_dir_all(&path).expect("could not create output directory for tests"); + + path +} + +fn tolerable_difference() -> u8 { + static mut TOLERANCE: u8 = 8; + + static ONCE: Once = Once::new(); + ONCE.call_once(|| unsafe { + if let Ok(str) = env::var("RSVG_TEST_TOLERANCE") { + let value: usize = str + .parse() + .expect("Can not parse RSVG_TEST_TOLERANCE as a number"); + TOLERANCE = + u8::try_from(value).expect("RSVG_TEST_TOLERANCE should be between 0 and 255"); + } + }); + + unsafe { TOLERANCE } +} + +pub trait Deviation { + fn distinguishable(&self) -> bool; + fn inacceptable(&self) -> bool; +} + +impl Deviation for Diff { + fn distinguishable(&self) -> bool { + self.max_diff > 2 + } + + fn inacceptable(&self) -> bool { + self.max_diff > tolerable_difference() + } +} + +/// Creates a cairo::ImageSurface from a stream of PNG data. +/// +/// The surface is converted to ARGB if needed. Use this helper function with `Reference`. +pub fn surface_from_png<R>(stream: &mut R) -> Result<cairo::ImageSurface, cairo::IoError> +where + R: Read, +{ + let png = cairo::ImageSurface::create_from_png(stream)?; + let argb = cairo::ImageSurface::create(cairo::Format::ARgb32, png.width(), png.height())?; + { + // convert to ARGB; the PNG may come as Rgb24 + let cr = cairo::Context::new(&argb).expect("Failed to create a cairo context"); + cr.set_source_surface(&png, 0.0, 0.0).unwrap(); + cr.paint().unwrap(); + } + Ok(argb) +} + +/// Macro test that compares render outputs +/// +/// Takes in SurfaceSize width and height, setting the cairo surface +#[macro_export] +macro_rules! test_compare_render_output { + ($test_name:ident, $width:expr, $height:expr, $test:expr, $reference:expr $(,)?) => { + #[test] + fn $test_name() { + $crate::test_utils::reference_utils::compare_render_output( + stringify!($test_name), + $width, + $height, + $test, + $reference, + ); + } + }; +} + +pub fn compare_render_output( + test_name: &str, + width: i32, + height: i32, + test: &'static [u8], + reference: &'static [u8], +) { + setup_font_map(); + + let svg = load_svg(test).unwrap(); + let output_surf = render_document( + &svg, + SurfaceSize(width, height), + |_| (), + cairo::Rectangle::new(0.0, 0.0, f64::from(width), f64::from(height)), + ) + .unwrap(); + + let reference = load_svg(reference).unwrap(); + let reference_surf = render_document( + &reference, + SurfaceSize(width, height), + |_| (), + cairo::Rectangle::new(0.0, 0.0, f64::from(width), f64::from(height)), + ) + .unwrap(); + + Reference::from_surface(reference_surf.into_image_surface().unwrap()) + .compare(&output_surf) + .evaluate(&output_surf, test_name); +} + +/// Render two SVG files and compare them. +/// +/// This is used to implement reference tests, or reftests. Use it like this: +/// +/// ```no_run +/// use rsvg::test_svg_reference; +/// test_svg_reference!(test_name, "tests/fixtures/blah/foo.svg", "tests/fixtures/blah/foo-ref.svg"); +/// ``` +/// +/// This will ensure that `foo.svg` and `foo-ref.svg` have exactly the same intrinsic dimensions, +/// and that they produce the same rendered output. +#[macro_export] +macro_rules! test_svg_reference { + ($test_name:ident, $test_filename:expr, $reference_filename:expr) => { + #[test] + fn $test_name() { + $crate::test_utils::reference_utils::svg_reference_test( + stringify!($test_name), + $test_filename, + $reference_filename, + ); + } + }; +} + +pub fn svg_reference_test(test_name: &str, test_filename: &str, reference_filename: &str) { + setup_font_map(); + + let svg = Loader::new() + .read_path(test_filename) + .expect("reading SVG test file"); + let reference = Loader::new() + .read_path(reference_filename) + .expect("reading reference file"); + + let svg_renderer = CairoRenderer::new(&svg); + let ref_renderer = CairoRenderer::new(&reference); + + let svg_dim = svg_renderer.intrinsic_dimensions(); + let ref_dim = ref_renderer.intrinsic_dimensions(); + + assert_eq!( + svg_dim, ref_dim, + "sizes of SVG document and reference file are different" + ); + + let pixels = svg_renderer + .intrinsic_size_in_pixels() + .unwrap_or((100.0, 100.0)); + + let output_surf = render_document( + &svg, + SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32), + |_| (), + cairo::Rectangle::new(0.0, 0.0, pixels.0, pixels.1), + ) + .unwrap(); + + let reference_surf = render_document( + &reference, + SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32), + |_| (), + cairo::Rectangle::new(0.0, 0.0, pixels.0, pixels.1), + ) + .unwrap(); + + Reference::from_surface(reference_surf.into_image_surface().unwrap()) + .compare(&output_surf) + .evaluate(&output_surf, test_name); +} diff --git a/rsvg/src/text.rs b/rsvg/src/text.rs new file mode 100644 index 00000000..a0e51fc6 --- /dev/null +++ b/rsvg/src/text.rs @@ -0,0 +1,1456 @@ +//! Text elements: `text`, `tspan`, `tref`. + +use markup5ever::{expanded_name, local_name, namespace_url, ns}; +use pango::IsAttribute; +use std::cell::RefCell; +use std::convert::TryFrom; +use std::rc::Rc; +use std::sync::Arc; + +use crate::bbox::BoundingBox; +use crate::document::{AcquiredNodes, NodeId}; +use crate::drawing_ctx::{create_pango_context, DrawingCtx, FontOptions, Viewport}; +use crate::element::{set_attribute, ElementData, ElementTrait}; +use crate::error::*; +use crate::layout::{self, FontProperties, Layer, LayerKind, StackingContext, Stroke, TextSpan}; +use crate::length::*; +use crate::node::{CascadedValues, Node, NodeBorrow}; +use crate::paint_server::PaintSource; +use crate::parsers::ParseValue; +use crate::properties::{ + ComputedValues, Direction, FontStretch, FontStyle, FontVariant, FontWeight, PaintOrder, + TextAnchor, TextRendering, UnicodeBidi, WritingMode, XmlLang, XmlSpace, +}; +use crate::rect::Rect; +use crate::session::Session; +use crate::space::{xml_space_normalize, NormalizeDefault, XmlSpaceNormalize}; +use crate::transform::{Transform, ValidTransform}; +use crate::xml::Attributes; + +/// The state of a text layout operation. +struct LayoutContext { + /// `writing-mode` property from the `<text>` element. + writing_mode: WritingMode, + + /// Current transform in the DrawingCtx. + transform: ValidTransform, + + /// Font options from the DrawingCtx. + font_options: FontOptions, + + /// For normalizing lengths. + viewport: Viewport, + + /// Session metadata for the document + session: Session, +} + +/// An absolutely-positioned array of `Span`s +/// +/// SVG defines a "[text chunk]" to occur when a text-related element +/// has an absolute position adjustment, that is, `x` or `y` +/// attributes. +/// +/// A `<text>` element always starts with an absolute position from +/// such attributes, or (0, 0) if they are not specified. +/// +/// Subsequent children of the `<text>` element will create new chunks +/// whenever they have `x` or `y` attributes. +/// +/// [text chunk]: https://www.w3.org/TR/SVG11/text.html#TextLayoutIntroduction +struct Chunk { + values: Rc<ComputedValues>, + x: Option<f64>, + y: Option<f64>, + spans: Vec<Span>, +} + +struct MeasuredChunk { + values: Rc<ComputedValues>, + x: Option<f64>, + y: Option<f64>, + dx: f64, + dy: f64, + spans: Vec<MeasuredSpan>, +} + +struct PositionedChunk { + next_chunk_x: f64, + next_chunk_y: f64, + spans: Vec<PositionedSpan>, +} + +struct Span { + values: Rc<ComputedValues>, + text: String, + dx: f64, + dy: f64, + _depth: usize, + link_target: Option<String>, +} + +struct MeasuredSpan { + values: Rc<ComputedValues>, + layout: pango::Layout, + layout_size: (f64, f64), + advance: (f64, f64), + dx: f64, + dy: f64, + link_target: Option<String>, +} + +struct PositionedSpan { + layout: pango::Layout, + values: Rc<ComputedValues>, + rendered_position: (f64, f64), + next_span_position: (f64, f64), + link_target: Option<String>, +} + +/// A laid-out and resolved text span. +/// +/// The only thing not in user-space units are the `stroke_paint` and `fill_paint`. +/// +/// This is the non-user-space version of `layout::TextSpan`. +struct LayoutSpan { + layout: pango::Layout, + gravity: pango::Gravity, + bbox: Option<BoundingBox>, + is_visible: bool, + x: f64, + y: f64, + paint_order: PaintOrder, + stroke: Stroke, + stroke_paint: Arc<PaintSource>, + fill_paint: Arc<PaintSource>, + text_rendering: TextRendering, + link_target: Option<String>, + values: Rc<ComputedValues>, +} + +impl Chunk { + fn new(values: &ComputedValues, x: Option<f64>, y: Option<f64>) -> Chunk { + Chunk { + values: Rc::new(values.clone()), + x, + y, + spans: Vec::new(), + } + } +} + +impl MeasuredChunk { + fn from_chunk(layout_context: &LayoutContext, chunk: &Chunk) -> MeasuredChunk { + let mut measured_spans: Vec<MeasuredSpan> = chunk + .spans + .iter() + .filter_map(|span| MeasuredSpan::from_span(layout_context, span)) + .collect(); + + // The first span contains the (dx, dy) that will be applied to the whole chunk. + // Make them 0 in the span, and extract the values to set them on the chunk. + // This is a hack until librsvg adds support for multiple dx/dy values per text/tspan. + + let (chunk_dx, chunk_dy) = if let Some(first) = measured_spans.first_mut() { + let dx = first.dx; + let dy = first.dy; + first.dx = 0.0; + first.dy = 0.0; + (dx, dy) + } else { + (0.0, 0.0) + }; + + MeasuredChunk { + values: chunk.values.clone(), + x: chunk.x, + y: chunk.y, + dx: chunk_dx, + dy: chunk_dy, + spans: measured_spans, + } + } +} + +impl PositionedChunk { + fn from_measured( + layout_context: &LayoutContext, + measured: &MeasuredChunk, + chunk_x: f64, + chunk_y: f64, + ) -> PositionedChunk { + let chunk_direction = measured.values.direction(); + + // Position the spans relatively to each other, starting at (0, 0) + + let mut positioned = Vec::new(); + + // Start position of each span; gets advanced as each span is laid out. + // This is the text's start position, not the bounding box. + let mut x = 0.0; + let mut y = 0.0; + + let mut chunk_bounds: Option<Rect> = None; + + for mspan in &measured.spans { + let params = NormalizeParams::new(&mspan.values, &layout_context.viewport); + + let layout = mspan.layout.clone(); + let layout_size = mspan.layout_size; + let values = mspan.values.clone(); + let dx = mspan.dx; + let dy = mspan.dy; + let advance = mspan.advance; + + let baseline_offset = compute_baseline_offset(&layout, &values, ¶ms); + + let start_pos = match chunk_direction { + Direction::Ltr => (x, y), + Direction::Rtl => (x - advance.0, y), + }; + + let span_advance = match chunk_direction { + Direction::Ltr => (advance.0, advance.1), + Direction::Rtl => (-advance.0, advance.1), + }; + + let rendered_position = if layout_context.writing_mode.is_horizontal() { + (start_pos.0 + dx, start_pos.1 - baseline_offset + dy) + } else { + (start_pos.0 + baseline_offset + dx, start_pos.1 + dy) + }; + + let span_bounds = + Rect::from_size(layout_size.0, layout_size.1).translate(rendered_position); + + if let Some(bounds) = chunk_bounds { + chunk_bounds = Some(bounds.union(&span_bounds)); + } else { + chunk_bounds = Some(span_bounds); + } + + x = x + span_advance.0 + dx; + y = y + span_advance.1 + dy; + + let positioned_span = PositionedSpan { + layout, + values, + rendered_position, + next_span_position: (x, y), + link_target: mspan.link_target.clone(), + }; + + positioned.push(positioned_span); + } + + // Compute the offsets needed to align the chunk per the text-anchor property (start, middle, end): + + let anchor_offset = text_anchor_offset( + measured.values.text_anchor(), + chunk_direction, + layout_context.writing_mode, + chunk_bounds.unwrap_or_default(), + ); + + // Apply the text-anchor offset to each individually-positioned span, and compute the + // start position of the next chunk. Also add in the chunk's dx/dy. + + let mut next_chunk_x = chunk_x; + let mut next_chunk_y = chunk_y; + + for pspan in &mut positioned { + // Add the chunk's position, plus the text-anchor offset, plus the chunk's dx/dy. + // This last term is a hack until librsvg adds support for multiple dx/dy values per text/tspan; + // see the corresponding part in MeasuredChunk::from_chunk(). + pspan.rendered_position.0 += chunk_x + anchor_offset.0 + measured.dx; + pspan.rendered_position.1 += chunk_y + anchor_offset.1 + measured.dy; + + next_chunk_x = chunk_x + pspan.next_span_position.0 + anchor_offset.0 + measured.dx; + next_chunk_y = chunk_y + pspan.next_span_position.1 + anchor_offset.1 + measured.dy; + } + + PositionedChunk { + next_chunk_x, + next_chunk_y, + spans: positioned, + } + } +} + +fn compute_baseline_offset( + layout: &pango::Layout, + values: &ComputedValues, + params: &NormalizeParams, +) -> f64 { + let baseline = f64::from(layout.baseline()) / f64::from(pango::SCALE); + let baseline_shift = values.baseline_shift().0.to_user(params); + baseline + baseline_shift +} + +/// Computes the (x, y) offsets to be applied to spans after applying the text-anchor property (start, middle, end). +#[rustfmt::skip] +fn text_anchor_offset( + anchor: TextAnchor, + direction: Direction, + writing_mode: WritingMode, + chunk_bounds: Rect, +) -> (f64, f64) { + let (w, h) = (chunk_bounds.width(), chunk_bounds.height()); + + let x0 = chunk_bounds.x0; + + if writing_mode.is_horizontal() { + match (anchor, direction) { + (TextAnchor::Start, Direction::Ltr) => (-x0, 0.0), + (TextAnchor::Start, Direction::Rtl) => (-x0 - w, 0.0), + + (TextAnchor::Middle, Direction::Ltr) => (-x0 - w / 2.0, 0.0), + (TextAnchor::Middle, Direction::Rtl) => (-x0 - w / 2.0, 0.0), + + (TextAnchor::End, Direction::Ltr) => (-x0 - w, 0.0), + (TextAnchor::End, Direction::Rtl) => (-x0, 0.0), + } + } else { + // FIXME: we don't deal with text direction for vertical text yet. + match anchor { + TextAnchor::Start => (0.0, 0.0), + TextAnchor::Middle => (0.0, -h / 2.0), + TextAnchor::End => (0.0, -h), + } + } +} + +impl Span { + fn new( + text: &str, + values: Rc<ComputedValues>, + dx: f64, + dy: f64, + depth: usize, + link_target: Option<String>, + ) -> Span { + Span { + values, + text: text.to_string(), + dx, + dy, + _depth: depth, + link_target, + } + } +} + +/// Use as `PangoUnits::from_pixels()` so that we can check for overflow. +struct PangoUnits(i32); + +impl PangoUnits { + fn from_pixels(v: f64) -> Option<Self> { + // We want (v * f64::from(pango::SCALE) + 0.5) as i32 + // + // But check for overflow. + + cast::i32(v * f64::from(pango::SCALE) + 0.5) + .ok() + .map(PangoUnits) + } +} + +impl MeasuredSpan { + fn from_span(layout_context: &LayoutContext, span: &Span) -> Option<MeasuredSpan> { + let values = span.values.clone(); + + let params = NormalizeParams::new(&values, &layout_context.viewport); + + let properties = FontProperties::new(&values, ¶ms); + + let bidi_control = BidiControl::from_unicode_bidi_and_direction( + properties.unicode_bidi, + properties.direction, + ); + + let with_control_chars = wrap_with_direction_control_chars(&span.text, &bidi_control); + + if let Some(layout) = create_pango_layout(layout_context, &properties, &with_control_chars) + { + let (w, h) = layout.size(); + + let w = f64::from(w) / f64::from(pango::SCALE); + let h = f64::from(h) / f64::from(pango::SCALE); + + let advance = if layout_context.writing_mode.is_horizontal() { + (w, 0.0) + } else { + (0.0, w) + }; + + Some(MeasuredSpan { + values, + layout, + layout_size: (w, h), + advance, + dx: span.dx, + dy: span.dy, + link_target: span.link_target.clone(), + }) + } else { + None + } + } +} + +// FIXME: should the pango crate provide this like PANGO_GRAVITY_IS_VERTICAL() ? +fn gravity_is_vertical(gravity: pango::Gravity) -> bool { + matches!(gravity, pango::Gravity::East | pango::Gravity::West) +} + +fn compute_text_box( + layout: &pango::Layout, + x: f64, + y: f64, + transform: Transform, + gravity: pango::Gravity, +) -> Option<BoundingBox> { + #![allow(clippy::many_single_char_names)] + + let (ink, _) = layout.extents(); + if ink.width() == 0 || ink.height() == 0 { + return None; + } + + let ink_x = f64::from(ink.x()); + let ink_y = f64::from(ink.y()); + let ink_width = f64::from(ink.width()); + let ink_height = f64::from(ink.height()); + let pango_scale = f64::from(pango::SCALE); + + let (x, y, w, h) = if gravity_is_vertical(gravity) { + ( + x + (ink_x - ink_height) / pango_scale, + y + ink_y / pango_scale, + ink_height / pango_scale, + ink_width / pango_scale, + ) + } else { + ( + x + ink_x / pango_scale, + y + ink_y / pango_scale, + ink_width / pango_scale, + ink_height / pango_scale, + ) + }; + + let r = Rect::new(x, y, x + w, y + h); + let bbox = BoundingBox::new() + .with_transform(transform) + .with_rect(r) + .with_ink_rect(r); + + Some(bbox) +} + +impl PositionedSpan { + fn layout( + &self, + layout_context: &LayoutContext, + acquired_nodes: &mut AcquiredNodes<'_>, + ) -> LayoutSpan { + let params = NormalizeParams::new(&self.values, &layout_context.viewport); + + let layout = self.layout.clone(); + let is_visible = self.values.is_visible(); + let (x, y) = self.rendered_position; + + let stroke = Stroke::new(&self.values, ¶ms); + + let gravity = layout.context().gravity(); + + let bbox = compute_text_box(&layout, x, y, *layout_context.transform, gravity); + + let stroke_paint = self.values.stroke().0.resolve( + acquired_nodes, + self.values.stroke_opacity().0, + self.values.color().0, + None, + None, + &layout_context.session, + ); + + let fill_paint = self.values.fill().0.resolve( + acquired_nodes, + self.values.fill_opacity().0, + self.values.color().0, + None, + None, + &layout_context.session, + ); + + let paint_order = self.values.paint_order(); + let text_rendering = self.values.text_rendering(); + + LayoutSpan { + layout, + gravity, + bbox, + is_visible, + x, + y, + paint_order, + stroke, + stroke_paint, + fill_paint, + text_rendering, + values: self.values.clone(), + link_target: self.link_target.clone(), + } + } +} + +/// Walks the children of a `<text>`, `<tspan>`, or `<tref>` element +/// and appends chunks/spans from them into the specified `chunks` +/// array. +fn children_to_chunks( + chunks: &mut Vec<Chunk>, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + layout_context: &LayoutContext, + dx: f64, + dy: f64, + depth: usize, + link: Option<String>, +) { + let mut dx = dx; + let mut dy = dy; + + for child in node.children() { + if child.is_chars() { + let values = cascaded.get(); + child.borrow_chars().to_chunks( + &child, + Rc::new(values.clone()), + chunks, + dx, + dy, + depth, + link.clone(), + ); + } else { + assert!(child.is_element()); + + match *child.borrow_element_data() { + ElementData::TSpan(ref tspan) => { + let cascaded = CascadedValues::clone_with_node(cascaded, &child); + tspan.to_chunks( + &child, + acquired_nodes, + &cascaded, + layout_context, + chunks, + dx, + dy, + depth + 1, + link.clone(), + ); + } + + ElementData::Link(ref link) => { + // TSpan::default sets all offsets to 0, + // which is what we want in links. + // + // FIXME: This is the only place in the code where an element's method (TSpan::to_chunks) + // is called with a node that is not the element itself: here, `child` is a Link, not a TSpan. + // + // The code works because the `tspan` is dropped immediately after calling to_chunks and no + // references are retained for it. + let tspan = TSpan::default(); + let cascaded = CascadedValues::clone_with_node(cascaded, &child); + tspan.to_chunks( + &child, + acquired_nodes, + &cascaded, + layout_context, + chunks, + dx, + dy, + depth + 1, + link.link.clone(), + ); + } + + ElementData::TRef(ref tref) => { + let cascaded = CascadedValues::clone_with_node(cascaded, &child); + tref.to_chunks( + &child, + acquired_nodes, + &cascaded, + chunks, + depth + 1, + layout_context, + ); + } + + _ => (), + } + } + + // After the first span, we don't need to carry over the parent's dx/dy. + dx = 0.0; + dy = 0.0; + } +} + +/// In SVG text elements, we use `Chars` to store character data. For example, +/// an element like `<text>Foo Bar</text>` will be a `Text` with a single child, +/// and the child will be a `Chars` with "Foo Bar" for its contents. +/// +/// Text elements can contain `<tspan>` sub-elements. In this case, +/// those `tspan` nodes will also contain `Chars` children. +/// +/// A text or tspan element can contain more than one `Chars` child, for example, +/// if there is an XML comment that splits the character contents in two: +/// +/// ```xml +/// <text> +/// This sentence will create a Chars. +/// <!-- this comment is ignored --> +/// This sentence will cretea another Chars. +/// </text> +/// ``` +/// +/// When rendering a text element, it will take care of concatenating the strings +/// in its `Chars` children as appropriate, depending on the +/// `xml:space="preserve"` attribute. A `Chars` stores the characters verbatim +/// as they come out of the XML parser, after ensuring that they are valid UTF-8. + +#[derive(Default)] +pub struct Chars { + string: RefCell<String>, + space_normalized: RefCell<Option<String>>, +} + +impl Chars { + pub fn new(initial_text: &str) -> Chars { + Chars { + string: RefCell::new(String::from(initial_text)), + space_normalized: RefCell::new(None), + } + } + + pub fn is_empty(&self) -> bool { + self.string.borrow().is_empty() + } + + pub fn append(&self, s: &str) { + self.string.borrow_mut().push_str(s); + *self.space_normalized.borrow_mut() = None; + } + + fn ensure_normalized_string(&self, node: &Node, values: &ComputedValues) { + let mut normalized = self.space_normalized.borrow_mut(); + + if (*normalized).is_none() { + let mode = match values.xml_space() { + XmlSpace::Default => XmlSpaceNormalize::Default(NormalizeDefault { + has_element_before: node.previous_sibling().is_some(), + has_element_after: node.next_sibling().is_some(), + }), + + XmlSpace::Preserve => XmlSpaceNormalize::Preserve, + }; + + *normalized = Some(xml_space_normalize(mode, &self.string.borrow())); + } + } + + fn make_span( + &self, + node: &Node, + values: Rc<ComputedValues>, + dx: f64, + dy: f64, + depth: usize, + link_target: Option<String>, + ) -> Span { + self.ensure_normalized_string(node, &values); + + Span::new( + self.space_normalized.borrow().as_ref().unwrap(), + values, + dx, + dy, + depth, + link_target, + ) + } + + fn to_chunks( + &self, + node: &Node, + values: Rc<ComputedValues>, + chunks: &mut [Chunk], + dx: f64, + dy: f64, + depth: usize, + link_target: Option<String>, + ) { + let span = self.make_span(node, values, dx, dy, depth, link_target); + let num_chunks = chunks.len(); + assert!(num_chunks > 0); + + chunks[num_chunks - 1].spans.push(span); + } + + pub fn get_string(&self) -> String { + self.string.borrow().clone() + } +} + +#[derive(Default)] +pub struct Text { + x: Length<Horizontal>, + y: Length<Vertical>, + dx: Length<Horizontal>, + dy: Length<Vertical>, +} + +impl Text { + fn make_chunks( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + layout_context: &LayoutContext, + x: f64, + y: f64, + ) -> Vec<Chunk> { + let mut chunks = Vec::new(); + + let values = cascaded.get(); + let params = NormalizeParams::new(values, &layout_context.viewport); + + chunks.push(Chunk::new(values, Some(x), Some(y))); + + let dx = self.dx.to_user(¶ms); + let dy = self.dy.to_user(¶ms); + + children_to_chunks( + &mut chunks, + node, + acquired_nodes, + cascaded, + layout_context, + dx, + dy, + 0, + None, + ); + chunks + } +} + +impl ElementTrait for Text { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), + expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), + expanded_name!("", "dx") => set_attribute(&mut self.dx, attr.parse(value), session), + expanded_name!("", "dy") => set_attribute(&mut self.dy, attr.parse(value), session), + _ => (), + } + } + } + + fn draw( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + viewport: &Viewport, + draw_ctx: &mut DrawingCtx, + clipping: bool, + ) -> Result<BoundingBox, RenderingError> { + let values = cascaded.get(); + let params = NormalizeParams::new(values, viewport); + + let elt = node.borrow_element(); + + let stacking_ctx = StackingContext::new( + draw_ctx.session(), + acquired_nodes, + &elt, + values.transform(), + values, + ); + + let layout_text = { + let transform = draw_ctx.get_transform_for_stacking_ctx(&stacking_ctx, clipping)?; + + let layout_context = LayoutContext { + writing_mode: values.writing_mode(), + transform, + font_options: draw_ctx.get_font_options(), + viewport: viewport.clone(), + session: draw_ctx.session().clone(), + }; + + let mut x = self.x.to_user(¶ms); + let mut y = self.y.to_user(¶ms); + + let chunks = self.make_chunks(node, acquired_nodes, cascaded, &layout_context, x, y); + + let mut measured_chunks = Vec::new(); + for chunk in &chunks { + measured_chunks.push(MeasuredChunk::from_chunk(&layout_context, chunk)); + } + + let mut positioned_chunks = Vec::new(); + for chunk in &measured_chunks { + let chunk_x = chunk.x.unwrap_or(x); + let chunk_y = chunk.y.unwrap_or(y); + + let positioned = + PositionedChunk::from_measured(&layout_context, chunk, chunk_x, chunk_y); + + x = positioned.next_chunk_x; + y = positioned.next_chunk_y; + + positioned_chunks.push(positioned); + } + + let mut layout_spans = Vec::new(); + for chunk in &positioned_chunks { + for span in &chunk.spans { + layout_spans.push(span.layout(&layout_context, acquired_nodes)); + } + } + + let empty_bbox = BoundingBox::new().with_transform(*transform); + + let text_bbox = layout_spans.iter().fold(empty_bbox, |mut bbox, span| { + if let Some(ref span_bbox) = span.bbox { + bbox.insert(span_bbox); + } + + bbox + }); + + let mut text_spans = Vec::new(); + for span in layout_spans { + let normalize_values = NormalizeValues::new(&span.values); + + let stroke_paint = span.stroke_paint.to_user_space( + &text_bbox.rect, + &layout_context.viewport, + &normalize_values, + ); + let fill_paint = span.fill_paint.to_user_space( + &text_bbox.rect, + &layout_context.viewport, + &normalize_values, + ); + + let text_span = TextSpan { + layout: span.layout, + gravity: span.gravity, + bbox: span.bbox, + is_visible: span.is_visible, + x: span.x, + y: span.y, + paint_order: span.paint_order, + stroke: span.stroke, + stroke_paint, + fill_paint, + text_rendering: span.text_rendering, + link_target: span.link_target, + }; + + text_spans.push(text_span); + } + + layout::Text { spans: text_spans } + }; + + let layer = Layer { + kind: LayerKind::Text(Box::new(layout_text)), + stacking_ctx, + }; + + draw_ctx.draw_layer(&layer, acquired_nodes, clipping, viewport) + } +} + +#[derive(Default)] +pub struct TRef { + link: Option<NodeId>, +} + +impl TRef { + fn to_chunks( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + chunks: &mut Vec<Chunk>, + depth: usize, + layout_context: &LayoutContext, + ) { + if self.link.is_none() { + return; + } + + let link = self.link.as_ref().unwrap(); + + let values = cascaded.get(); + if !values.is_displayed() { + return; + } + + if let Ok(acquired) = acquired_nodes.acquire(link) { + let c = acquired.get(); + extract_chars_children_to_chunks_recursively(chunks, c, Rc::new(values.clone()), depth); + } else { + rsvg_log!( + layout_context.session, + "element {} references a nonexistent text source \"{}\"", + node, + link, + ); + } + } +} + +fn extract_chars_children_to_chunks_recursively( + chunks: &mut Vec<Chunk>, + node: &Node, + values: Rc<ComputedValues>, + depth: usize, +) { + for child in node.children() { + let values = values.clone(); + + if child.is_chars() { + child + .borrow_chars() + .to_chunks(&child, values, chunks, 0.0, 0.0, depth, None) + } else { + extract_chars_children_to_chunks_recursively(chunks, &child, values, depth + 1) + } + } +} + +impl ElementTrait for TRef { + fn set_attributes(&mut self, attrs: &Attributes, _session: &Session) { + self.link = attrs + .iter() + .find(|(attr, _)| attr.expanded() == expanded_name!(xlink "href")) + // Unlike other elements which use `href` in SVG2 versus `xlink:href` in SVG1.1, + // the <tref> element got removed in SVG2. So, here we still use a match + // against the full namespaced version of the attribute. + .and_then(|(attr, value)| NodeId::parse(value).attribute(attr).ok()); + } +} + +#[derive(Default)] +pub struct TSpan { + x: Option<Length<Horizontal>>, + y: Option<Length<Vertical>>, + dx: Length<Horizontal>, + dy: Length<Vertical>, +} + +impl TSpan { + fn to_chunks( + &self, + node: &Node, + acquired_nodes: &mut AcquiredNodes<'_>, + cascaded: &CascadedValues<'_>, + layout_context: &LayoutContext, + chunks: &mut Vec<Chunk>, + dx: f64, + dy: f64, + depth: usize, + link: Option<String>, + ) { + let values = cascaded.get(); + if !values.is_displayed() { + return; + } + + let params = NormalizeParams::new(values, &layout_context.viewport); + + let x = self.x.map(|l| l.to_user(¶ms)); + let y = self.y.map(|l| l.to_user(¶ms)); + + let span_dx = dx + self.dx.to_user(¶ms); + let span_dy = dy + self.dy.to_user(¶ms); + + if x.is_some() || y.is_some() { + chunks.push(Chunk::new(values, x, y)); + } + + children_to_chunks( + chunks, + node, + acquired_nodes, + cascaded, + layout_context, + span_dx, + span_dy, + depth, + link, + ); + } +} + +impl ElementTrait for TSpan { + fn set_attributes(&mut self, attrs: &Attributes, session: &Session) { + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session), + expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session), + expanded_name!("", "dx") => set_attribute(&mut self.dx, attr.parse(value), session), + expanded_name!("", "dy") => set_attribute(&mut self.dy, attr.parse(value), session), + _ => (), + } + } + } +} + +impl From<FontStyle> for pango::Style { + fn from(s: FontStyle) -> pango::Style { + match s { + FontStyle::Normal => pango::Style::Normal, + FontStyle::Italic => pango::Style::Italic, + FontStyle::Oblique => pango::Style::Oblique, + } + } +} + +impl From<FontVariant> for pango::Variant { + fn from(v: FontVariant) -> pango::Variant { + match v { + FontVariant::Normal => pango::Variant::Normal, + FontVariant::SmallCaps => pango::Variant::SmallCaps, + } + } +} + +impl From<FontStretch> for pango::Stretch { + fn from(s: FontStretch) -> pango::Stretch { + match s { + FontStretch::Normal => pango::Stretch::Normal, + FontStretch::Wider => pango::Stretch::Expanded, // not quite correct + FontStretch::Narrower => pango::Stretch::Condensed, // not quite correct + FontStretch::UltraCondensed => pango::Stretch::UltraCondensed, + FontStretch::ExtraCondensed => pango::Stretch::ExtraCondensed, + FontStretch::Condensed => pango::Stretch::Condensed, + FontStretch::SemiCondensed => pango::Stretch::SemiCondensed, + FontStretch::SemiExpanded => pango::Stretch::SemiExpanded, + FontStretch::Expanded => pango::Stretch::Expanded, + FontStretch::ExtraExpanded => pango::Stretch::ExtraExpanded, + FontStretch::UltraExpanded => pango::Stretch::UltraExpanded, + } + } +} + +impl From<FontWeight> for pango::Weight { + fn from(w: FontWeight) -> pango::Weight { + pango::Weight::__Unknown(w.numeric_weight().into()) + } +} + +impl From<Direction> for pango::Direction { + fn from(d: Direction) -> pango::Direction { + match d { + Direction::Ltr => pango::Direction::Ltr, + Direction::Rtl => pango::Direction::Rtl, + } + } +} + +impl From<WritingMode> for pango::Direction { + fn from(m: WritingMode) -> pango::Direction { + use WritingMode::*; + match m { + HorizontalTb | VerticalRl | VerticalLr | LrTb | Lr | Tb | TbRl => pango::Direction::Ltr, + RlTb | Rl => pango::Direction::Rtl, + } + } +} + +impl From<WritingMode> for pango::Gravity { + fn from(m: WritingMode) -> pango::Gravity { + use WritingMode::*; + match m { + HorizontalTb | LrTb | Lr | RlTb | Rl => pango::Gravity::South, + VerticalRl | Tb | TbRl => pango::Gravity::East, + VerticalLr => pango::Gravity::West, + } + } +} + +/// Constants with Unicode's directional formatting characters +/// +/// <https://unicode.org/reports/tr9/#Directional_Formatting_Characters> +mod directional_formatting_characters { + /// Left-to-Right Embedding + /// + /// Treat the following text as embedded left-to-right. + pub const LRE: char = '\u{202a}'; + + /// Right-to-Left Embedding + /// + /// Treat the following text as embedded right-to-left. + pub const RLE: char = '\u{202b}'; + + /// Left-to-Right Override + /// + /// Force following characters to be treated as strong left-to-right characters. + pub const LRO: char = '\u{202d}'; + + /// Right-to-Left Override + /// + /// Force following characters to be treated as strong right-to-left characters. + pub const RLO: char = '\u{202e}'; + + /// Pop Directional Formatting + /// + /// End the scope of the last LRE, RLE, RLO, or LRO. + pub const PDF: char = '\u{202c}'; + + /// Left-to-Right Isolate + /// + /// Treat the following text as isolated and left-to-right. + pub const LRI: char = '\u{2066}'; + + /// Right-to-Left Isolate + /// + /// Treat the following text as isolated and right-to-left. + pub const RLI: char = '\u{2067}'; + + /// First Strong Isolate + /// + /// Treat the following text as isolated and in the direction of its first strong + /// directional character that is not inside a nested isolate. + pub const FSI: char = '\u{2068}'; + + /// Pop Directional Isolate + /// + /// End the scope of the last LRI, RLI, or FSI. + pub const PDI: char = '\u{2069}'; +} + +/// Unicode control characters to be inserted when `unicode-bidi` is specified. +/// +/// The `unicode-bidi` property is used to change the embedding of a text span within +/// another. This struct contains slices with the control characters that must be +/// inserted into the text stream at the span's limits so that the bidi/shaping engine +/// will know what to do. +struct BidiControl { + start: &'static [char], + end: &'static [char], +} + +impl BidiControl { + /// Creates a `BidiControl` from the properties that determine it. + /// + /// See the table titled "Bidi control codes injected..." in + /// <https://www.w3.org/TR/css-writing-modes-3/#unicode-bidi> + #[rustfmt::skip] + fn from_unicode_bidi_and_direction(unicode_bidi: UnicodeBidi, direction: Direction) -> BidiControl { + use UnicodeBidi::*; + use Direction::*; + use directional_formatting_characters::*; + + let (start, end) = match (unicode_bidi, direction) { + (Normal, _) => (&[][..], &[][..]), + (Embed, Ltr) => (&[LRE][..], &[PDF][..]), + (Embed, Rtl) => (&[RLE][..], &[PDF][..]), + (Isolate, Ltr) => (&[LRI][..], &[PDI][..]), + (Isolate, Rtl) => (&[RLI][..], &[PDI][..]), + (BidiOverride, Ltr) => (&[LRO][..], &[PDF][..]), + (BidiOverride, Rtl) => (&[RLO][..], &[PDF][..]), + (IsolateOverride, Ltr) => (&[FSI, LRO][..], &[PDF, PDI][..]), + (IsolateOverride, Rtl) => (&[FSI, RLO][..], &[PDF, PDI][..]), + (Plaintext, Ltr) => (&[FSI][..], &[PDI][..]), + (Plaintext, Rtl) => (&[FSI][..], &[PDI][..]), + }; + + BidiControl { start, end } + } +} + +/// Prepends and appends Unicode directional formatting characters. +fn wrap_with_direction_control_chars(s: &str, bidi_control: &BidiControl) -> String { + let mut res = + String::with_capacity(s.len() + bidi_control.start.len() + bidi_control.end.len()); + + for &ch in bidi_control.start { + res.push(ch); + } + + res.push_str(s); + + for &ch in bidi_control.end { + res.push(ch); + } + + res +} + +/// Returns `None` if the layout would be invalid due to, for example, out-of-bounds font sizes. +fn create_pango_layout( + layout_context: &LayoutContext, + props: &FontProperties, + text: &str, +) -> Option<pango::Layout> { + let pango_context = + create_pango_context(&layout_context.font_options, &layout_context.transform); + + if let XmlLang(Some(ref lang)) = props.xml_lang { + pango_context.set_language(Some(&pango::Language::from_string(lang.as_str()))); + } + + pango_context.set_base_gravity(pango::Gravity::from(layout_context.writing_mode)); + + match (props.unicode_bidi, props.direction) { + (UnicodeBidi::BidiOverride, _) | (UnicodeBidi::Embed, _) => { + pango_context.set_base_dir(pango::Direction::from(props.direction)); + } + + (_, direction) if direction != Direction::Ltr => { + pango_context.set_base_dir(pango::Direction::from(direction)); + } + + (_, _) => { + pango_context.set_base_dir(pango::Direction::from(layout_context.writing_mode)); + } + } + + let layout = pango::Layout::new(&pango_context); + + let font_size = PangoUnits::from_pixels(props.font_size); + let letter_spacing = PangoUnits::from_pixels(props.letter_spacing); + + if font_size.is_none() { + rsvg_log!( + &layout_context.session, + "font-size {} is out of bounds; ignoring span", + props.font_size + ); + } + + if letter_spacing.is_none() { + rsvg_log!( + &layout_context.session, + "letter-spacing {} is out of bounds; ignoring span", + props.letter_spacing + ); + } + + if let (Some(font_size), Some(letter_spacing)) = (font_size, letter_spacing) { + let attr_list = pango::AttrList::new(); + add_pango_attributes(&attr_list, props, 0, text.len(), font_size, letter_spacing); + + layout.set_attributes(Some(&attr_list)); + layout.set_text(text); + layout.set_auto_dir(false); + + Some(layout) + } else { + None + } +} + +/// Adds Pango attributes, suitable for a span of text, to an `AttrList`. +fn add_pango_attributes( + attr_list: &pango::AttrList, + props: &FontProperties, + start_index: usize, + end_index: usize, + font_size: PangoUnits, + letter_spacing: PangoUnits, +) { + let start_index = u32::try_from(start_index).expect("Pango attribute index must fit in u32"); + let end_index = u32::try_from(end_index).expect("Pango attribute index must fit in u32"); + assert!(start_index <= end_index); + + let mut attributes = Vec::new(); + + let mut font_desc = pango::FontDescription::new(); + font_desc.set_family(props.font_family.as_str()); + font_desc.set_style(pango::Style::from(props.font_style)); + + // PANGO_VARIANT_SMALL_CAPS does nothing: https://gitlab.gnome.org/GNOME/pango/-/issues/566 + // see below for using the "smcp" OpenType feature for fonts that support it. + // font_desc.set_variant(pango::Variant::from(props.font_variant)); + + font_desc.set_weight(pango::Weight::from(props.font_weight)); + font_desc.set_stretch(pango::Stretch::from(props.font_stretch)); + + font_desc.set_size(font_size.0); + + attributes.push(pango::AttrFontDesc::new(&font_desc).upcast()); + + attributes.push(pango::AttrInt::new_letter_spacing(letter_spacing.0).upcast()); + + if props.text_decoration.overline { + attributes.push(pango::AttrInt::new_overline(pango::Overline::Single).upcast()); + } + + if props.text_decoration.underline { + attributes.push(pango::AttrInt::new_underline(pango::Underline::Single).upcast()); + } + + if props.text_decoration.strike { + attributes.push(pango::AttrInt::new_strikethrough(true).upcast()); + } + + // FIXME: Using the "smcp" OpenType feature only works for fonts that support it. We + // should query if the font supports small caps, and synthesize them if it doesn't. + if props.font_variant == FontVariant::SmallCaps { + // smcp - small capitals - https://docs.microsoft.com/en-ca/typography/opentype/spec/features_pt#smcp + attributes.push(pango::AttrFontFeatures::new("'smcp' 1").upcast()); + } + + // Set the range in each attribute + + for attr in &mut attributes { + attr.set_start_index(start_index); + attr.set_end_index(end_index); + } + + // Add the attributes to the attr_list + + for attr in attributes { + attr_list.insert(attr); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn chars_default() { + let c = Chars::default(); + assert!(c.is_empty()); + assert!(c.space_normalized.borrow().is_none()); + } + + #[test] + fn chars_new() { + let example = "Test 123"; + let c = Chars::new(example); + assert_eq!(c.get_string(), example); + assert!(c.space_normalized.borrow().is_none()); + } + + // This is called _horizontal because the property value in "CSS Writing Modes 3" + // is `horizontal-tb`. Eventually we will support that and this will make more sense. + #[test] + fn adjusted_advance_horizontal_ltr() { + use Direction::*; + use TextAnchor::*; + + assert_eq!( + text_anchor_offset( + Start, + Ltr, + WritingMode::Lr, + Rect::from_size(1.0, 2.0).translate((5.0, 6.0)) + ), + (-5.0, 0.0) + ); + + assert_eq!( + text_anchor_offset( + Middle, + Ltr, + WritingMode::Lr, + Rect::from_size(1.0, 2.0).translate((5.0, 6.0)) + ), + (-5.5, 0.0) + ); + + assert_eq!( + text_anchor_offset( + End, + Ltr, + WritingMode::Lr, + Rect::from_size(1.0, 2.0).translate((5.0, 6.0)) + ), + (-6.0, 0.0) + ); + } + + #[test] + fn adjusted_advance_horizontal_rtl() { + use Direction::*; + use TextAnchor::*; + + assert_eq!( + text_anchor_offset( + Start, + Rtl, + WritingMode::Rl, + Rect::from_size(1.0, 2.0).translate((5.0, 6.0)) + ), + (-6.0, 0.0) + ); + assert_eq!( + text_anchor_offset( + Middle, + Rtl, + WritingMode::Rl, + Rect::from_size(1.0, 2.0).translate((5.0, 6.0)) + ), + (-5.5, 0.0) + ); + assert_eq!( + text_anchor_offset( + TextAnchor::End, + Direction::Rtl, + WritingMode::Rl, + Rect::from_size(1.0, 2.0).translate((5.0, 6.0)) + ), + (-5.0, 0.0) + ); + } + + // This is called _vertical because "CSS Writing Modes 3" has both `vertical-rl` (East + // Asia), and `vertical-lr` (Manchu, Mongolian), but librsvg does not support block + // flow direction properly yet. Eventually we will support that and this will make + // more sense. + #[test] + fn adjusted_advance_vertical() { + use Direction::*; + use TextAnchor::*; + + assert_eq!( + text_anchor_offset(Start, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)), + (0.0, 0.0) + ); + + assert_eq!( + text_anchor_offset(Middle, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)), + (0.0, -2.0) + ); + + assert_eq!( + text_anchor_offset(End, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)), + (0.0, -4.0) + ); + } + + #[test] + fn pango_units_works() { + assert_eq!(PangoUnits::from_pixels(10.0).unwrap().0, pango::SCALE * 10); + } + + #[test] + fn pango_units_detects_overflow() { + assert!(PangoUnits::from_pixels(1e7).is_none()); + } +} diff --git a/rsvg/src/transform.rs b/rsvg/src/transform.rs new file mode 100644 index 00000000..9f6c6789 --- /dev/null +++ b/rsvg/src/transform.rs @@ -0,0 +1,1127 @@ +//! Handling of transform values. +//! +//! This module contains the following: +//! +//! * [`Transform`] to represent 2D transforms in general; it's just a matrix. +//! +//! * [`TransformProperty`] for the [`transform` property][prop] in SVG2/CSS3. +//! +//! * [`Transform`] also handles the [`transform` attribute][attr] in SVG1.1, which has a different +//! grammar than the `transform` property from SVG2. +//! +//! [prop]: https://www.w3.org/TR/css-transforms-1/#transform-property +//! [attr]: https://www.w3.org/TR/SVG11/coords.html#TransformAttribute + +use cssparser::{Parser, Token}; +use std::ops::Deref; + +use crate::angle::Angle; +use crate::error::*; +use crate::length::*; +use crate::parsers::{optional_comma, Parse}; +use crate::properties::ComputedValues; +use crate::property_macros::Property; +use crate::rect::Rect; + +/// A transform that has been checked to be invertible. +/// +/// We need to validate user-supplied transforms before setting them on Cairo, +/// so we use this type for that. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct ValidTransform(Transform); + +impl TryFrom<Transform> for ValidTransform { + type Error = InvalidTransform; + + /// Validates a [`Transform`] before converting it to a [`ValidTransform`]. + /// + /// A transform is valid if it is invertible. For example, a + /// matrix with all-zeros is not invertible, and it is invalid. + fn try_from(t: Transform) -> Result<ValidTransform, InvalidTransform> { + if t.is_invertible() { + Ok(ValidTransform(t)) + } else { + Err(InvalidTransform) + } + } +} + +impl Deref for ValidTransform { + type Target = Transform; + + fn deref(&self) -> &Transform { + &self.0 + } +} + +/// A 2D transformation matrix. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Transform { + pub xx: f64, + pub yx: f64, + pub xy: f64, + pub yy: f64, + pub x0: f64, + pub y0: f64, +} + +/// The `transform` property from the CSS Transforms Module Level 1. +/// +/// CSS Transforms 1: <https://www.w3.org/TR/css-transforms-1/#transform-property> +#[derive(Debug, Default, Clone, PartialEq)] +pub enum TransformProperty { + #[default] + None, + List(Vec<TransformFunction>), +} + +/// The `transform` attribute from SVG1.1 +/// +/// SVG1.1: <https://www.w3.org/TR/SVG11/coords.html#TransformAttribute> +#[derive(Copy, Clone, Default, Debug, PartialEq)] +pub struct TransformAttribute(Transform); + +impl Property for TransformProperty { + fn inherits_automatically() -> bool { + false + } + + fn compute(&self, _v: &ComputedValues) -> Self { + self.clone() + } +} + +impl TransformProperty { + pub fn to_transform(&self) -> Transform { + // From the spec (https://www.w3.org/TR/css-transforms-1/#current-transformation-matrix): + // Start with the identity matrix. + // TODO: implement (#685) - Translate by the computed X and Y of transform-origin + // Multiply by each of the transform functions in transform property from left to right + // TODO: implement - Translate by the negated computed X and Y values of transform-origin + + match self { + TransformProperty::None => Transform::identity(), + + TransformProperty::List(l) => { + let mut final_transform = Transform::identity(); + + for f in l.iter() { + use TransformFunction::*; + + let transform_matrix = match f { + Matrix(trans_matrix) => *trans_matrix, + Translate(h, v) => Transform::new_translate(h.length, v.length), + TranslateX(h) => Transform::new_translate(h.length, 0.0), + TranslateY(v) => Transform::new_translate(0.0, v.length), + Scale(x, y) => Transform::new_scale(*x, *y), + ScaleX(x) => Transform::new_scale(*x, 1.0), + ScaleY(y) => Transform::new_scale(1.0, *y), + Rotate(a) => Transform::new_rotate(*a), + Skew(ax, ay) => Transform::new_skew(*ax, *ay), + SkewX(ax) => Transform::new_skew(*ax, Angle::new(0.0)), + SkewY(ay) => Transform::new_skew(Angle::new(0.0), *ay), + }; + final_transform = transform_matrix.post_transform(&final_transform); + } + + final_transform + } + } + } +} + +// https://www.w3.org/TR/css-transforms-1/#typedef-transform-function +#[derive(Debug, Clone, PartialEq)] +pub enum TransformFunction { + Matrix(Transform), + Translate(Length<Horizontal>, Length<Vertical>), + TranslateX(Length<Horizontal>), + TranslateY(Length<Vertical>), + Scale(f64, f64), + ScaleX(f64), + ScaleY(f64), + Rotate(Angle), + Skew(Angle, Angle), + SkewX(Angle), + SkewY(Angle), +} + +impl Parse for TransformProperty { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<TransformProperty, ParseError<'i>> { + if parser + .try_parse(|p| p.expect_ident_matching("none")) + .is_ok() + { + Ok(TransformProperty::None) + } else { + let t = parse_transform_prop_function_list(parser)?; + + Ok(TransformProperty::List(t)) + } + } +} + +fn parse_transform_prop_function_list<'i>( + parser: &mut Parser<'i, '_>, +) -> Result<Vec<TransformFunction>, ParseError<'i>> { + let mut v = Vec::<TransformFunction>::new(); + + loop { + v.push(parse_transform_prop_function_command(parser)?); + + if parser.is_exhausted() { + break; + } + } + + Ok(v) +} + +fn parse_transform_prop_function_command<'i>( + parser: &mut Parser<'i, '_>, +) -> Result<TransformFunction, ParseError<'i>> { + let loc = parser.current_source_location(); + + match parser.next()?.clone() { + Token::Function(ref name) => parse_transform_prop_function_internal(name, parser), + tok => Err(loc.new_unexpected_token_error(tok.clone())), + } +} + +fn parse_transform_prop_function_internal<'i>( + name: &str, + parser: &mut Parser<'i, '_>, +) -> Result<TransformFunction, ParseError<'i>> { + let loc = parser.current_source_location(); + + match name { + "matrix" => parse_prop_matrix_args(parser), + "translate" => parse_prop_translate_args(parser), + "translateX" => parse_prop_translate_x_args(parser), + "translateY" => parse_prop_translate_y_args(parser), + "scale" => parse_prop_scale_args(parser), + "scaleX" => parse_prop_scale_x_args(parser), + "scaleY" => parse_prop_scale_y_args(parser), + "rotate" => parse_prop_rotate_args(parser), + "skew" => parse_prop_skew_args(parser), + "skewX" => parse_prop_skew_x_args(parser), + "skewY" => parse_prop_skew_y_args(parser), + _ => Err(loc.new_custom_error(ValueErrorKind::parse_error( + "expected matrix|translate|translateX|translateY|scale|scaleX|scaleY|rotate|skewX|skewY", + ))), + } +} + +fn parse_prop_matrix_args<'i>( + parser: &mut Parser<'i, '_>, +) -> Result<TransformFunction, ParseError<'i>> { + parser.parse_nested_block(|p| { + let xx = f64::parse(p)?; + p.expect_comma()?; + let yx = f64::parse(p)?; + p.expect_comma()?; + let xy = f64::parse(p)?; + p.expect_comma()?; + let yy = f64::parse(p)?; + p.expect_comma()?; + let x0 = f64::parse(p)?; + p.expect_comma()?; + let y0 = f64::parse(p)?; + + Ok(TransformFunction::Matrix(Transform::new_unchecked( + xx, yx, xy, yy, x0, y0, + ))) + }) +} + +fn length_is_in_pixels<N: Normalize>(l: &Length<N>) -> bool { + l.unit == LengthUnit::Px +} + +fn only_pixels_error<'i>(loc: cssparser::SourceLocation) -> ParseError<'i> { + loc.new_custom_error(ValueErrorKind::parse_error( + "only translations in pixels are supported for now", + )) +} + +fn parse_prop_translate_args<'i>( + parser: &mut Parser<'i, '_>, +) -> Result<TransformFunction, ParseError<'i>> { + parser.parse_nested_block(|p| { + let loc = p.current_source_location(); + + let tx: Length<Horizontal> = Length::parse(p)?; + + let ty: Length<Vertical> = if p.try_parse(|p| p.expect_comma()).is_ok() { + Length::parse(p)? + } else { + Length::new(0.0, LengthUnit::Px) + }; + + if !(length_is_in_pixels(&tx) && length_is_in_pixels(&ty)) { + return Err(only_pixels_error(loc)); + } + + Ok(TransformFunction::Translate(tx, ty)) + }) +} + +fn parse_prop_translate_x_args<'i>( + parser: &mut Parser<'i, '_>, +) -> Result<TransformFunction, ParseError<'i>> { + parser.parse_nested_block(|p| { + let loc = p.current_source_location(); + + let tx: Length<Horizontal> = Length::parse(p)?; + + if !length_is_in_pixels(&tx) { + return Err(only_pixels_error(loc)); + } + + Ok(TransformFunction::TranslateX(tx)) + }) +} + +fn parse_prop_translate_y_args<'i>( + parser: &mut Parser<'i, '_>, +) -> Result<TransformFunction, ParseError<'i>> { + parser.parse_nested_block(|p| { + let loc = p.current_source_location(); + + let ty: Length<Vertical> = Length::parse(p)?; + + if !length_is_in_pixels(&ty) { + return Err(only_pixels_error(loc)); + } + + Ok(TransformFunction::TranslateY(ty)) + }) +} + +fn parse_prop_scale_args<'i>( + parser: &mut Parser<'i, '_>, +) -> Result<TransformFunction, ParseError<'i>> { + parser.parse_nested_block(|p| { + let x = f64::parse(p)?; + + let y = if p.try_parse(|p| p.expect_comma()).is_ok() { + f64::parse(p)? + } else { + x + }; + + Ok(TransformFunction::Scale(x, y)) + }) +} + +fn parse_prop_scale_x_args<'i>( + parser: &mut Parser<'i, '_>, +) -> Result<TransformFunction, ParseError<'i>> { + parser.parse_nested_block(|p| { + let x = f64::parse(p)?; + + Ok(TransformFunction::ScaleX(x)) + }) +} + +fn parse_prop_scale_y_args<'i>( + parser: &mut Parser<'i, '_>, +) -> Result<TransformFunction, ParseError<'i>> { + parser.parse_nested_block(|p| { + let y = f64::parse(p)?; + + Ok(TransformFunction::ScaleY(y)) + }) +} + +fn parse_prop_rotate_args<'i>( + parser: &mut Parser<'i, '_>, +) -> Result<TransformFunction, ParseError<'i>> { + parser.parse_nested_block(|p| { + let angle = Angle::parse(p)?; + + Ok(TransformFunction::Rotate(angle)) + }) +} + +fn parse_prop_skew_args<'i>( + parser: &mut Parser<'i, '_>, +) -> Result<TransformFunction, ParseError<'i>> { + parser.parse_nested_block(|p| { + let ax = Angle::parse(p)?; + + let ay = if p.try_parse(|p| p.expect_comma()).is_ok() { + Angle::parse(p)? + } else { + Angle::from_degrees(0.0) + }; + + Ok(TransformFunction::Skew(ax, ay)) + }) +} + +fn parse_prop_skew_x_args<'i>( + parser: &mut Parser<'i, '_>, +) -> Result<TransformFunction, ParseError<'i>> { + parser.parse_nested_block(|p| { + let angle = Angle::parse(p)?; + Ok(TransformFunction::SkewX(angle)) + }) +} + +fn parse_prop_skew_y_args<'i>( + parser: &mut Parser<'i, '_>, +) -> Result<TransformFunction, ParseError<'i>> { + parser.parse_nested_block(|p| { + let angle = Angle::parse(p)?; + Ok(TransformFunction::SkewY(angle)) + }) +} + +impl Transform { + #[inline] + pub fn new_unchecked(xx: f64, yx: f64, xy: f64, yy: f64, x0: f64, y0: f64) -> Self { + Self { + xx, + yx, + xy, + yy, + x0, + y0, + } + } + + #[inline] + pub fn identity() -> Self { + Self::new_unchecked(1.0, 0.0, 0.0, 1.0, 0.0, 0.0) + } + + #[inline] + pub fn new_translate(tx: f64, ty: f64) -> Self { + Self::new_unchecked(1.0, 0.0, 0.0, 1.0, tx, ty) + } + + #[inline] + pub fn new_scale(sx: f64, sy: f64) -> Self { + Self::new_unchecked(sx, 0.0, 0.0, sy, 0.0, 0.0) + } + + #[inline] + pub fn new_rotate(a: Angle) -> Self { + let (s, c) = a.radians().sin_cos(); + Self::new_unchecked(c, s, -s, c, 0.0, 0.0) + } + + #[inline] + pub fn new_skew(ax: Angle, ay: Angle) -> Self { + Self::new_unchecked(1.0, ay.radians().tan(), ax.radians().tan(), 1.0, 0.0, 0.0) + } + + #[must_use] + pub fn multiply(t1: &Transform, t2: &Transform) -> Self { + #[allow(clippy::suspicious_operation_groupings)] + Transform { + xx: t1.xx * t2.xx + t1.yx * t2.xy, + yx: t1.xx * t2.yx + t1.yx * t2.yy, + xy: t1.xy * t2.xx + t1.yy * t2.xy, + yy: t1.xy * t2.yx + t1.yy * t2.yy, + x0: t1.x0 * t2.xx + t1.y0 * t2.xy + t2.x0, + y0: t1.x0 * t2.yx + t1.y0 * t2.yy + t2.y0, + } + } + + #[inline] + pub fn pre_transform(&self, t: &Transform) -> Self { + Self::multiply(t, self) + } + + #[inline] + pub fn post_transform(&self, t: &Transform) -> Self { + Self::multiply(self, t) + } + + #[inline] + pub fn pre_translate(&self, x: f64, y: f64) -> Self { + self.pre_transform(&Transform::new_translate(x, y)) + } + + #[inline] + pub fn pre_scale(&self, sx: f64, sy: f64) -> Self { + self.pre_transform(&Transform::new_scale(sx, sy)) + } + + #[inline] + pub fn pre_rotate(&self, angle: Angle) -> Self { + self.pre_transform(&Transform::new_rotate(angle)) + } + + #[inline] + pub fn post_translate(&self, x: f64, y: f64) -> Self { + self.post_transform(&Transform::new_translate(x, y)) + } + + #[inline] + pub fn post_scale(&self, sx: f64, sy: f64) -> Self { + self.post_transform(&Transform::new_scale(sx, sy)) + } + + #[inline] + pub fn post_rotate(&self, angle: Angle) -> Self { + self.post_transform(&Transform::new_rotate(angle)) + } + + #[inline] + fn determinant(&self) -> f64 { + self.xx * self.yy - self.xy * self.yx + } + + #[inline] + pub fn is_invertible(&self) -> bool { + let det = self.determinant(); + + det != 0.0 && det.is_finite() + } + + #[must_use] + pub fn invert(&self) -> Option<Self> { + let det = self.determinant(); + + if det == 0.0 || !det.is_finite() { + return None; + } + + let inv_det = 1.0 / det; + + Some(Transform::new_unchecked( + inv_det * self.yy, + inv_det * (-self.yx), + inv_det * (-self.xy), + inv_det * self.xx, + inv_det * (self.xy * self.y0 - self.yy * self.x0), + inv_det * (self.yx * self.x0 - self.xx * self.y0), + )) + } + + #[inline] + pub fn transform_distance(&self, dx: f64, dy: f64) -> (f64, f64) { + (dx * self.xx + dy * self.xy, dx * self.yx + dy * self.yy) + } + + #[inline] + pub fn transform_point(&self, px: f64, py: f64) -> (f64, f64) { + let (x, y) = self.transform_distance(px, py); + (x + self.x0, y + self.y0) + } + + pub fn transform_rect(&self, rect: &Rect) -> Rect { + let points = [ + self.transform_point(rect.x0, rect.y0), + self.transform_point(rect.x1, rect.y0), + self.transform_point(rect.x0, rect.y1), + self.transform_point(rect.x1, rect.y1), + ]; + + let (mut xmin, mut ymin, mut xmax, mut ymax) = { + let (x, y) = points[0]; + + (x, y, x, y) + }; + + for &(x, y) in points.iter().skip(1) { + if x < xmin { + xmin = x; + } + + if x > xmax { + xmax = x; + } + + if y < ymin { + ymin = y; + } + + if y > ymax { + ymax = y; + } + } + + Rect { + x0: xmin, + y0: ymin, + x1: xmax, + y1: ymax, + } + } +} + +impl Default for Transform { + #[inline] + fn default() -> Transform { + Transform::identity() + } +} + +impl TransformAttribute { + pub fn to_transform(self) -> Transform { + self.0 + } +} + +impl Parse for TransformAttribute { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<TransformAttribute, ParseError<'i>> { + Ok(TransformAttribute(parse_transform_list(parser)?)) + } +} + +fn parse_transform_list<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> { + let mut t = Transform::identity(); + + loop { + if parser.is_exhausted() { + break; + } + + t = parse_transform_command(parser)?.post_transform(&t); + optional_comma(parser); + } + + Ok(t) +} + +fn parse_transform_command<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> { + let loc = parser.current_source_location(); + + match parser.next()?.clone() { + Token::Function(ref name) => parse_transform_function(name, parser), + + Token::Ident(ref name) => { + parser.expect_parenthesis_block()?; + parse_transform_function(name, parser) + } + + tok => Err(loc.new_unexpected_token_error(tok.clone())), + } +} + +fn parse_transform_function<'i>( + name: &str, + parser: &mut Parser<'i, '_>, +) -> Result<Transform, ParseError<'i>> { + let loc = parser.current_source_location(); + + match name { + "matrix" => parse_matrix_args(parser), + "translate" => parse_translate_args(parser), + "scale" => parse_scale_args(parser), + "rotate" => parse_rotate_args(parser), + "skewX" => parse_skew_x_args(parser), + "skewY" => parse_skew_y_args(parser), + _ => Err(loc.new_custom_error(ValueErrorKind::parse_error( + "expected matrix|translate|scale|rotate|skewX|skewY", + ))), + } +} + +fn parse_matrix_args<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> { + parser.parse_nested_block(|p| { + let xx = f64::parse(p)?; + optional_comma(p); + + let yx = f64::parse(p)?; + optional_comma(p); + + let xy = f64::parse(p)?; + optional_comma(p); + + let yy = f64::parse(p)?; + optional_comma(p); + + let x0 = f64::parse(p)?; + optional_comma(p); + + let y0 = f64::parse(p)?; + + Ok(Transform::new_unchecked(xx, yx, xy, yy, x0, y0)) + }) +} + +fn parse_translate_args<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> { + parser.parse_nested_block(|p| { + let tx = f64::parse(p)?; + + let ty = p + .try_parse(|p| { + optional_comma(p); + f64::parse(p) + }) + .unwrap_or(0.0); + + Ok(Transform::new_translate(tx, ty)) + }) +} + +fn parse_scale_args<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> { + parser.parse_nested_block(|p| { + let x = f64::parse(p)?; + + let y = p + .try_parse(|p| { + optional_comma(p); + f64::parse(p) + }) + .unwrap_or(x); + + Ok(Transform::new_scale(x, y)) + }) +} + +fn parse_rotate_args<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> { + parser.parse_nested_block(|p| { + let angle = Angle::from_degrees(f64::parse(p)?); + + let (tx, ty) = p + .try_parse(|p| -> Result<_, ParseError<'_>> { + optional_comma(p); + let tx = f64::parse(p)?; + + optional_comma(p); + let ty = f64::parse(p)?; + + Ok((tx, ty)) + }) + .unwrap_or((0.0, 0.0)); + + Ok(Transform::new_translate(tx, ty) + .pre_rotate(angle) + .pre_translate(-tx, -ty)) + }) +} + +fn parse_skew_x_args<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> { + parser.parse_nested_block(|p| { + let angle = Angle::from_degrees(f64::parse(p)?); + Ok(Transform::new_skew(angle, Angle::new(0.0))) + }) +} + +fn parse_skew_y_args<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> { + parser.parse_nested_block(|p| { + let angle = Angle::from_degrees(f64::parse(p)?); + Ok(Transform::new_skew(Angle::new(0.0), angle)) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use float_cmp::ApproxEq; + use std::f64; + + fn rotation_transform(deg: f64, tx: f64, ty: f64) -> Transform { + Transform::new_translate(tx, ty) + .pre_rotate(Angle::from_degrees(deg)) + .pre_translate(-tx, -ty) + } + + fn parse_transform(s: &str) -> Result<Transform, ParseError<'_>> { + let transform_attr = TransformAttribute::parse_str(s)?; + Ok(transform_attr.to_transform()) + } + + fn parse_transform_prop(s: &str) -> Result<TransformProperty, ParseError<'_>> { + TransformProperty::parse_str(s) + } + + fn assert_transform_eq(t1: &Transform, t2: &Transform) { + let epsilon = 8.0 * f64::EPSILON; // kind of arbitrary, but allow for some sloppiness + + assert!(t1.xx.approx_eq(t2.xx, (epsilon, 1))); + assert!(t1.yx.approx_eq(t2.yx, (epsilon, 1))); + assert!(t1.xy.approx_eq(t2.xy, (epsilon, 1))); + assert!(t1.yy.approx_eq(t2.yy, (epsilon, 1))); + assert!(t1.x0.approx_eq(t2.x0, (epsilon, 1))); + assert!(t1.y0.approx_eq(t2.y0, (epsilon, 1))); + } + + #[test] + fn test_multiply() { + let t1 = Transform::identity(); + let t2 = Transform::new_unchecked(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); + assert_transform_eq(&Transform::multiply(&t1, &t2), &t2); + assert_transform_eq(&Transform::multiply(&t2, &t1), &t2); + + let t1 = Transform::new_unchecked(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); + let t2 = Transform::new_unchecked(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + let r = Transform::new_unchecked(0.0, 0.0, 0.0, 0.0, 5.0, 6.0); + assert_transform_eq(&Transform::multiply(&t1, &t2), &t2); + assert_transform_eq(&Transform::multiply(&t2, &t1), &r); + + let t1 = Transform::new_unchecked(0.5, 0.0, 0.0, 0.5, 10.0, 10.0); + let t2 = Transform::new_unchecked(1.0, 0.0, 0.0, 1.0, -10.0, -10.0); + let r1 = Transform::new_unchecked(0.5, 0.0, 0.0, 0.5, 0.0, 0.0); + let r2 = Transform::new_unchecked(0.5, 0.0, 0.0, 0.5, 5.0, 5.0); + assert_transform_eq(&Transform::multiply(&t1, &t2), &r1); + assert_transform_eq(&Transform::multiply(&t2, &t1), &r2); + } + + #[test] + fn test_invert() { + let t = Transform::new_unchecked(2.0, 0.0, 0.0, 0.0, 0.0, 0.0); + assert!(!t.is_invertible()); + assert!(t.invert().is_none()); + + let t = Transform::identity(); + assert!(t.is_invertible()); + assert!(t.invert().is_some()); + let i = t.invert().unwrap(); + assert_transform_eq(&i, &Transform::identity()); + + let t = Transform::new_unchecked(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); + assert!(t.is_invertible()); + assert!(t.invert().is_some()); + let i = t.invert().unwrap(); + assert_transform_eq(&t.pre_transform(&i), &Transform::identity()); + assert_transform_eq(&t.post_transform(&i), &Transform::identity()); + } + + #[test] + pub fn test_transform_point() { + let t = Transform::new_translate(10.0, 10.0); + assert_eq!((11.0, 11.0), t.transform_point(1.0, 1.0)); + } + + #[test] + pub fn test_transform_distance() { + let t = Transform::new_translate(10.0, 10.0).pre_scale(2.0, 1.0); + assert_eq!((2.0, 1.0), t.transform_distance(1.0, 1.0)); + } + + #[test] + fn parses_valid_transform() { + let t = Transform::new_unchecked(1.0, 0.0, 0.0, 1.0, 20.0, 30.0); + let s = Transform::new_unchecked(10.0, 0.0, 0.0, 10.0, 0.0, 0.0); + let r = rotation_transform(30.0, 10.0, 10.0); + + let a = Transform::multiply(&s, &t); + assert_transform_eq( + &parse_transform("translate(20, 30), scale (10) rotate (30 10 10)").unwrap(), + &Transform::multiply(&r, &a), + ); + } + + fn assert_parse_error(s: &str) { + assert!(parse_transform(s).is_err()); + } + + #[test] + fn syntax_error_yields_parse_error() { + assert_parse_error("foo"); + assert_parse_error("matrix (1 2 3 4 5)"); + assert_parse_error("translate(1 2 3 4 5)"); + assert_parse_error("translate (1,)"); + assert_parse_error("scale (1,)"); + assert_parse_error("skewX (1,2)"); + assert_parse_error("skewY ()"); + assert_parse_error("skewY"); + } + + #[test] + fn parses_matrix() { + assert_transform_eq( + &parse_transform("matrix (1 2 3 4 5 6)").unwrap(), + &Transform::new_unchecked(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), + ); + + assert_transform_eq( + &parse_transform("matrix(1,2,3,4 5 6)").unwrap(), + &Transform::new_unchecked(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), + ); + + assert_transform_eq( + &parse_transform("matrix (1,2.25,-3.25e2,4 5 6)").unwrap(), + &Transform::new_unchecked(1.0, 2.25, -325.0, 4.0, 5.0, 6.0), + ); + } + + #[test] + fn parses_translate() { + assert_transform_eq( + &parse_transform("translate(-1 -2)").unwrap(), + &Transform::new_unchecked(1.0, 0.0, 0.0, 1.0, -1.0, -2.0), + ); + + assert_transform_eq( + &parse_transform("translate(-1, -2)").unwrap(), + &Transform::new_unchecked(1.0, 0.0, 0.0, 1.0, -1.0, -2.0), + ); + + assert_transform_eq( + &parse_transform("translate(-1)").unwrap(), + &Transform::new_unchecked(1.0, 0.0, 0.0, 1.0, -1.0, 0.0), + ); + } + + #[test] + fn parses_scale() { + assert_transform_eq( + &parse_transform("scale (-1)").unwrap(), + &Transform::new_unchecked(-1.0, 0.0, 0.0, -1.0, 0.0, 0.0), + ); + + assert_transform_eq( + &parse_transform("scale(-1 -2)").unwrap(), + &Transform::new_unchecked(-1.0, 0.0, 0.0, -2.0, 0.0, 0.0), + ); + + assert_transform_eq( + &parse_transform("scale(-1, -2)").unwrap(), + &Transform::new_unchecked(-1.0, 0.0, 0.0, -2.0, 0.0, 0.0), + ); + } + + #[test] + fn parses_rotate() { + assert_transform_eq( + &parse_transform("rotate (30)").unwrap(), + &rotation_transform(30.0, 0.0, 0.0), + ); + assert_transform_eq( + &parse_transform("rotate (30,-1,-2)").unwrap(), + &rotation_transform(30.0, -1.0, -2.0), + ); + assert_transform_eq( + &parse_transform("rotate(30, -1, -2)").unwrap(), + &rotation_transform(30.0, -1.0, -2.0), + ); + } + + #[test] + fn parses_skew_x() { + assert_transform_eq( + &parse_transform("skewX (30)").unwrap(), + &Transform::new_skew(Angle::from_degrees(30.0), Angle::new(0.0)), + ); + } + + #[test] + fn parses_skew_y() { + assert_transform_eq( + &parse_transform("skewY (30)").unwrap(), + &Transform::new_skew(Angle::new(0.0), Angle::from_degrees(30.0)), + ); + } + + #[test] + fn parses_transform_list() { + let t = Transform::new_unchecked(1.0, 0.0, 0.0, 1.0, 20.0, 30.0); + let s = Transform::new_unchecked(10.0, 0.0, 0.0, 10.0, 0.0, 0.0); + let r = rotation_transform(30.0, 10.0, 10.0); + + assert_transform_eq( + &parse_transform("scale(10)rotate(30, 10, 10)").unwrap(), + &Transform::multiply(&r, &s), + ); + + assert_transform_eq( + &parse_transform("translate(20, 30), scale (10)").unwrap(), + &Transform::multiply(&s, &t), + ); + + let a = Transform::multiply(&s, &t); + assert_transform_eq( + &parse_transform("translate(20, 30), scale (10) rotate (30 10 10)").unwrap(), + &Transform::multiply(&r, &a), + ); + } + + #[test] + fn parses_empty() { + assert_transform_eq(&parse_transform("").unwrap(), &Transform::identity()); + } + + #[test] + fn test_parse_transform_property_none() { + assert_eq!( + parse_transform_prop("none").unwrap(), + TransformProperty::None + ); + } + + #[test] + fn none_transform_is_identity() { + assert_eq!( + parse_transform_prop("none").unwrap().to_transform(), + Transform::identity() + ); + } + + #[test] + fn empty_transform_property_is_error() { + // https://www.w3.org/TR/css-transforms-1/#transform-property + // + // <transform-list> = <transform-function>+ + // ^ one or more required + assert!(parse_transform_prop("").is_err()); + } + + #[test] + fn test_parse_transform_property_matrix() { + let tp = TransformProperty::List(vec![TransformFunction::Matrix( + Transform::new_unchecked(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), + )]); + + assert_eq!(&tp, &parse_transform_prop("matrix(1,2,3,4,5,6)").unwrap()); + assert!(parse_transform_prop("matrix(1 2 3 4 5 6)").is_err()); + assert!(parse_transform_prop("Matrix(1,2,3,4,5,6)").is_err()); + } + + #[test] + fn test_parse_transform_property_translate() { + let tpt = TransformProperty::List(vec![TransformFunction::Translate( + Length::<Horizontal>::new(100.0, LengthUnit::Px), + Length::<Vertical>::new(200.0, LengthUnit::Px), + )]); + + assert_eq!( + &tpt, + &parse_transform_prop("translate(100px,200px)").unwrap() + ); + + assert_eq!( + parse_transform_prop("translate(1)").unwrap(), + parse_transform_prop("translate(1, 0)").unwrap() + ); + + assert!(parse_transform_prop("translate(100, foo)").is_err()); + assert!(parse_transform_prop("translate(100, )").is_err()); + assert!(parse_transform_prop("translate(100 200)").is_err()); + assert!(parse_transform_prop("translate(1px,2px,3px,4px)").is_err()); + } + + #[test] + fn test_parse_transform_property_translate_x() { + let tptx = TransformProperty::List(vec![TransformFunction::TranslateX( + Length::<Horizontal>::new(100.0, LengthUnit::Px), + )]); + + assert_eq!(&tptx, &parse_transform_prop("translateX(100px)").unwrap()); + assert!(parse_transform_prop("translateX(1)").is_ok()); + assert!(parse_transform_prop("translateX(100 100)").is_err()); + assert!(parse_transform_prop("translatex(1px)").is_err()); + assert!(parse_transform_prop("translatex(1rad)").is_err()); + } + + #[test] + fn test_parse_transform_property_translate_y() { + let tpty = TransformProperty::List(vec![TransformFunction::TranslateY( + Length::<Vertical>::new(100.0, LengthUnit::Px), + )]); + + assert_eq!(&tpty, &parse_transform_prop("translateY(100px)").unwrap()); + assert!(parse_transform_prop("translateY(1)").is_ok()); + assert!(parse_transform_prop("translateY(100 100)").is_err()); + assert!(parse_transform_prop("translatey(1px)").is_err()); + } + + #[test] + fn test_translate_only_supports_pixel_units() { + assert!(parse_transform_prop("translate(1in, 2)").is_err()); + assert!(parse_transform_prop("translate(1, 2in)").is_err()); + assert!(parse_transform_prop("translateX(1cm)").is_err()); + assert!(parse_transform_prop("translateY(1cm)").is_err()); + } + + #[test] + fn test_parse_transform_property_scale() { + let tps = TransformProperty::List(vec![TransformFunction::Scale(1.0, 10.0)]); + + assert_eq!(&tps, &parse_transform_prop("scale(1,10)").unwrap()); + + assert_eq!( + parse_transform_prop("scale(2)").unwrap(), + parse_transform_prop("scale(2, 2)").unwrap() + ); + + assert!(parse_transform_prop("scale(100, foo)").is_err()); + assert!(parse_transform_prop("scale(100, )").is_err()); + assert!(parse_transform_prop("scale(1 10)").is_err()); + assert!(parse_transform_prop("scale(1px,10px)").is_err()); + assert!(parse_transform_prop("scale(1%)").is_err()); + } + + #[test] + fn test_parse_transform_property_scale_x() { + let tpsx = TransformProperty::List(vec![TransformFunction::ScaleX(10.0)]); + + assert_eq!(&tpsx, &parse_transform_prop("scaleX(10)").unwrap()); + + assert!(parse_transform_prop("scaleX(100 100)").is_err()); + assert!(parse_transform_prop("scalex(10)").is_err()); + assert!(parse_transform_prop("scaleX(10px)").is_err()); + } + + #[test] + fn test_parse_transform_property_scale_y() { + let tpsy = TransformProperty::List(vec![TransformFunction::ScaleY(10.0)]); + + assert_eq!(&tpsy, &parse_transform_prop("scaleY(10)").unwrap()); + assert!(parse_transform_prop("scaleY(10 1)").is_err()); + assert!(parse_transform_prop("scaleY(1px)").is_err()); + } + + #[test] + fn test_parse_transform_property_rotate() { + let tpr = + TransformProperty::List(vec![TransformFunction::Rotate(Angle::from_degrees(100.0))]); + assert_eq!(&tpr, &parse_transform_prop("rotate(100deg)").unwrap()); + assert!(parse_transform_prop("rotate(100deg 100)").is_err()); + assert!(parse_transform_prop("rotate(3px)").is_err()); + } + + #[test] + fn test_parse_transform_property_skew() { + let tpsk = TransformProperty::List(vec![TransformFunction::Skew( + Angle::from_degrees(90.0), + Angle::from_degrees(120.0), + )]); + + assert_eq!(&tpsk, &parse_transform_prop("skew(90deg,120deg)").unwrap()); + + assert_eq!( + parse_transform_prop("skew(45deg)").unwrap(), + parse_transform_prop("skew(45deg, 0)").unwrap() + ); + + assert!(parse_transform_prop("skew(1.0,1.0)").is_ok()); + assert!(parse_transform_prop("skew(1rad,1rad)").is_ok()); + + assert!(parse_transform_prop("skew(100, foo)").is_err()); + assert!(parse_transform_prop("skew(100, )").is_err()); + assert!(parse_transform_prop("skew(1.0px)").is_err()); + assert!(parse_transform_prop("skew(1.0,1.0,1deg)").is_err()); + } + + #[test] + fn test_parse_transform_property_skew_x() { + let tpskx = + TransformProperty::List(vec![TransformFunction::SkewX(Angle::from_degrees(90.0))]); + + assert_eq!(&tpskx, &parse_transform_prop("skewX(90deg)").unwrap()); + assert!(parse_transform_prop("skewX(1.0)").is_ok()); + assert!(parse_transform_prop("skewX(1rad)").is_ok()); + assert!(parse_transform_prop("skewx(1.0)").is_err()); + assert!(parse_transform_prop("skewX(1.0,1.0)").is_err()); + } + + #[test] + fn test_parse_transform_property_skew_y() { + let tpsky = + TransformProperty::List(vec![TransformFunction::SkewY(Angle::from_degrees(90.0))]); + + assert_eq!(&tpsky, &parse_transform_prop("skewY(90deg)").unwrap()); + assert!(parse_transform_prop("skewY(1.0)").is_ok()); + assert!(parse_transform_prop("skewY(1rad)").is_ok()); + assert!(parse_transform_prop("skewy(1.0)").is_err()); + assert!(parse_transform_prop("skewY(1.0,1.0)").is_err()); + } +} diff --git a/rsvg/src/ua.css b/rsvg/src/ua.css new file mode 100644 index 00000000..3292f8ac --- /dev/null +++ b/rsvg/src/ua.css @@ -0,0 +1,41 @@ +/* See https://www.w3.org/TR/SVG/styling.html#UAStyleSheet + * + * Commented out rules cannot yet be parsed. + */ + +/* +@namespace url(http://www.w3.org/2000/svg); +@namespace xml url(http://www.w3.org/XML/1998/namespace); +*/ + +svg:not(:root), image, marker, pattern, symbol { overflow: hidden; } + +/* +*:not(svg), +*:not(foreignObject) > svg { + transform-origin: 0 0; +} + +*[xml|space=preserve] { + text-space-collapse: preserve-spaces; +} +*/ + +defs, +clipPath, mask, marker, +desc, title, metadata, +pattern, linearGradient, radialGradient, +script, style, +symbol { + display: none !important; +} + +:host(use) > symbol { + display: inline !important; +} + +/* +:link, :visited { + cursor: pointer; +} +* diff --git a/rsvg/src/unit_interval.rs b/rsvg/src/unit_interval.rs new file mode 100644 index 00000000..65cc3d94 --- /dev/null +++ b/rsvg/src/unit_interval.rs @@ -0,0 +1,94 @@ +//! Type for values in the [0.0, 1.0] range. + +use cssparser::Parser; + +use crate::error::*; +use crate::length::*; +use crate::parsers::Parse; +use crate::util; + +#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)] +pub struct UnitInterval(pub f64); + +impl UnitInterval { + pub fn clamp(val: f64) -> UnitInterval { + UnitInterval(util::clamp(val, 0.0, 1.0)) + } +} + +impl Parse for UnitInterval { + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<UnitInterval, ParseError<'i>> { + let loc = parser.current_source_location(); + let l: Length<Both> = Parse::parse(parser)?; + match l.unit { + LengthUnit::Px | LengthUnit::Percent => Ok(UnitInterval::clamp(l.length)), + _ => Err(loc.new_custom_error(ValueErrorKind::value_error( + "<unit-interval> must be in default or percent units", + ))), + } + } +} + +impl From<UnitInterval> for u8 { + fn from(val: UnitInterval) -> u8 { + let UnitInterval(x) = val; + (x * 255.0 + 0.5).floor() as u8 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn clamps() { + assert_eq!(UnitInterval::clamp(-1.0), UnitInterval(0.0)); + assert_eq!(UnitInterval::clamp(0.0), UnitInterval(0.0)); + assert_eq!(UnitInterval::clamp(0.5), UnitInterval(0.5)); + assert_eq!(UnitInterval::clamp(1.0), UnitInterval(1.0)); + assert_eq!(UnitInterval::clamp(2.0), UnitInterval(1.0)); + } + + #[test] + fn parses_percentages() { + assert_eq!(UnitInterval::parse_str("-100%").unwrap(), UnitInterval(0.0)); + assert_eq!(UnitInterval::parse_str("0%").unwrap(), UnitInterval(0.0)); + assert_eq!(UnitInterval::parse_str("50%").unwrap(), UnitInterval(0.5)); + assert_eq!(UnitInterval::parse_str("100%").unwrap(), UnitInterval(1.0)); + assert_eq!( + UnitInterval::parse_str("100.1%").unwrap(), + UnitInterval(1.0) + ); + assert_eq!(UnitInterval::parse_str("200%").unwrap(), UnitInterval(1.0)); + } + + #[test] + fn parses_number() { + assert_eq!(UnitInterval::parse_str("0").unwrap(), UnitInterval(0.0)); + assert_eq!(UnitInterval::parse_str("1").unwrap(), UnitInterval(1.0)); + assert_eq!(UnitInterval::parse_str("0.5").unwrap(), UnitInterval(0.5)); + } + + #[test] + fn parses_out_of_range_number() { + assert_eq!(UnitInterval::parse_str("-10").unwrap(), UnitInterval(0.0)); + assert_eq!(UnitInterval::parse_str("10").unwrap(), UnitInterval(1.0)); + } + + #[test] + fn errors_on_invalid_input() { + assert!(UnitInterval::parse_str("").is_err()); + assert!(UnitInterval::parse_str("foo").is_err()); + assert!(UnitInterval::parse_str("-x").is_err()); + assert!(UnitInterval::parse_str("0.0foo").is_err()); + assert!(UnitInterval::parse_str("0.0%%").is_err()); + assert!(UnitInterval::parse_str("%").is_err()); + } + + #[test] + fn convert() { + assert_eq!(u8::from(UnitInterval(0.0)), 0); + assert_eq!(u8::from(UnitInterval(0.5)), 128); + assert_eq!(u8::from(UnitInterval(1.0)), 255); + } +} diff --git a/rsvg/src/url_resolver.rs b/rsvg/src/url_resolver.rs new file mode 100644 index 00000000..d62c4483 --- /dev/null +++ b/rsvg/src/url_resolver.rs @@ -0,0 +1,256 @@ +//! Determine which URLs are allowed for loading. + +use std::fmt; +use std::io; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use url::Url; + +use crate::error::AllowedUrlError; + +/// Currently only contains the base URL. +/// +/// The plan is to add: +/// base_only: Only allow to load content from the same base URL. By default +// this restriction is enabled and requires to provide base_url. +/// include_xml: Allows to use xi:include with XML. Enabled by default. +/// include_text: Allows to use xi:include with text. Enabled by default. +/// local_only: Only allow to load content from the local filesystem. +/// Enabled by default. +#[derive(Clone)] +pub struct UrlResolver { + /// Base URL; all relative references will be resolved with respect to this. + pub base_url: Option<Url>, +} + +impl UrlResolver { + /// Creates a `UrlResolver` with defaults, and sets the `base_url`. + pub fn new(base_url: Option<Url>) -> Self { + UrlResolver { base_url } + } + + pub fn resolve_href(&self, href: &str) -> Result<AllowedUrl, AllowedUrlError> { + let url = Url::options() + .base_url(self.base_url.as_ref()) + .parse(href) + .map_err(AllowedUrlError::UrlParseError)?; + + // Allow loads of data: from any location + if url.scheme() == "data" { + return Ok(AllowedUrl(url)); + } + + // All other sources require a base url + if self.base_url.is_none() { + return Err(AllowedUrlError::BaseRequired); + } + + let base_url = self.base_url.as_ref().unwrap(); + + // Deny loads from differing URI schemes + if url.scheme() != base_url.scheme() { + return Err(AllowedUrlError::DifferentUriSchemes); + } + + // resource: is allowed to load anything from other resources + if url.scheme() == "resource" { + return Ok(AllowedUrl(url)); + } + + // Non-file: isn't allowed to load anything + if url.scheme() != "file" { + return Err(AllowedUrlError::DisallowedScheme); + } + + // We have two file: URIs. Now canonicalize them (remove .. and symlinks, etc.) + // and see if the directories match + + let url_path = url + .to_file_path() + .map_err(|_| AllowedUrlError::InvalidPath)?; + let base_path = base_url + .to_file_path() + .map_err(|_| AllowedUrlError::InvalidPath)?; + + let base_parent = base_path.parent(); + if base_parent.is_none() { + return Err(AllowedUrlError::BaseIsRoot); + } + + let base_parent = base_parent.unwrap(); + + let url_canon = + canonicalize(url_path).map_err(|_| AllowedUrlError::CanonicalizationError)?; + let parent_canon = + canonicalize(base_parent).map_err(|_| AllowedUrlError::CanonicalizationError)?; + + if url_canon.starts_with(parent_canon) { + Ok(AllowedUrl(url)) + } else { + Err(AllowedUrlError::NotSiblingOrChildOfBaseFile) + } + } +} + +/// Wrapper for URLs which are allowed to be loaded +/// +/// SVG files can reference other files (PNG/JPEG images, other SVGs, +/// CSS files, etc.). This object is constructed by checking whether +/// a specified `href` (a possibly-relative filename, for example) +/// should be allowed to be loaded, given the base URL of the SVG +/// being loaded. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct AllowedUrl(Url); + +impl Deref for AllowedUrl { + type Target = Url; + + fn deref(&self) -> &Url { + &self.0 + } +} + +impl fmt::Display for AllowedUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +// For tests, we don't want to touch the filesystem. In that case, +// assume that we are being passed canonical file names. +#[cfg(not(test))] +fn canonicalize<P: AsRef<Path>>(path: P) -> Result<PathBuf, io::Error> { + path.as_ref().canonicalize() +} +#[cfg(test)] +fn canonicalize<P: AsRef<Path>>(path: P) -> Result<PathBuf, io::Error> { + Ok(path.as_ref().to_path_buf()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn disallows_relative_file_with_no_base_file() { + let url_resolver = UrlResolver::new(None); + assert!(matches!( + url_resolver.resolve_href("foo.svg"), + Err(AllowedUrlError::UrlParseError( + url::ParseError::RelativeUrlWithoutBase + )) + )); + } + + #[test] + fn disallows_different_schemes() { + let url_resolver = UrlResolver::new(Some( + Url::parse("http://example.com/malicious.svg").unwrap(), + )); + assert!(matches!( + url_resolver.resolve_href("file:///etc/passwd"), + Err(AllowedUrlError::DifferentUriSchemes) + )); + } + + fn make_file_uri(p: &str) -> String { + if cfg!(windows) { + format!("file:///c:{}", p) + } else { + format!("file://{}", p) + } + } + + #[test] + fn disallows_base_is_root() { + let url_resolver = UrlResolver::new(Some(Url::parse(&make_file_uri("/")).unwrap())); + assert!(matches!( + url_resolver.resolve_href("foo.svg"), + Err(AllowedUrlError::BaseIsRoot) + )); + } + + #[test] + fn disallows_non_file_scheme() { + let url_resolver = UrlResolver::new(Some(Url::parse("http://foo.bar/baz.svg").unwrap())); + assert!(matches!( + url_resolver.resolve_href("foo.svg"), + Err(AllowedUrlError::DisallowedScheme) + )); + } + + #[test] + fn allows_data_url_with_no_base_file() { + let url_resolver = UrlResolver::new(None); + assert_eq!( + url_resolver + .resolve_href("data:image/jpeg;base64,xxyyzz") + .unwrap() + .as_ref(), + "data:image/jpeg;base64,xxyyzz", + ); + } + + #[test] + fn allows_relative() { + let url_resolver = UrlResolver::new(Some( + Url::parse(&make_file_uri("/example/bar.svg")).unwrap(), + )); + let resolved = url_resolver.resolve_href("foo.svg").unwrap(); + let expected = make_file_uri("/example/foo.svg"); + assert_eq!(resolved.as_ref(), expected); + } + + #[test] + fn allows_sibling() { + let url_resolver = UrlResolver::new(Some( + Url::parse(&make_file_uri("/example/bar.svg")).unwrap(), + )); + let resolved = url_resolver + .resolve_href(&make_file_uri("/example/foo.svg")) + .unwrap(); + let expected = make_file_uri("/example/foo.svg"); + assert_eq!(resolved.as_ref(), expected); + } + + #[test] + fn allows_child_of_sibling() { + let url_resolver = UrlResolver::new(Some( + Url::parse(&make_file_uri("/example/bar.svg")).unwrap(), + )); + let resolved = url_resolver + .resolve_href(&make_file_uri("/example/subdir/foo.svg")) + .unwrap(); + let expected = make_file_uri("/example/subdir/foo.svg"); + assert_eq!(resolved.as_ref(), expected); + } + + #[test] + fn disallows_non_sibling() { + let url_resolver = UrlResolver::new(Some( + Url::parse(&make_file_uri("/example/bar.svg")).unwrap(), + )); + assert!(matches!( + url_resolver.resolve_href(&make_file_uri("/etc/passwd")), + Err(AllowedUrlError::NotSiblingOrChildOfBaseFile) + )); + } + + #[cfg(windows)] + #[test] + fn invalid_url_from_test_suite() { + // This is required for Url to panic. + let resolver = + UrlResolver::new(Some(Url::parse("file:///c:/foo.svg").expect("initial url"))); + // With this, it doesn't panic: + // let resolver = UrlResolver::new(None); + + // The following panics, when using a base URL + // match resolver.resolve_href("file://invalid.css") { + // so, use a less problematic case, hopefully + match resolver.resolve_href("file://") { + Ok(_) => println!("yay!"), + Err(e) => println!("err: {}", e), + } + } +} diff --git a/rsvg/src/util.rs b/rsvg/src/util.rs new file mode 100644 index 00000000..1375b860 --- /dev/null +++ b/rsvg/src/util.rs @@ -0,0 +1,75 @@ +//! Miscellaneous utilities. + +use std::borrow::Cow; +use std::ffi::CStr; +use std::mem::transmute; +use std::str; + +/// Converts a `char *` which is known to be valid UTF-8 into a `&str` +/// +/// The usual `from_glib_none(s)` allocates an owned String. The +/// purpose of `utf8_cstr()` is to get a temporary string slice into a +/// C string which is already known to be valid UTF-8; for example, +/// as for strings which come from `libxml2`. +pub unsafe fn utf8_cstr<'a>(s: *const libc::c_char) -> &'a str { + assert!(!s.is_null()); + + str::from_utf8_unchecked(CStr::from_ptr(s).to_bytes()) +} + +pub unsafe fn opt_utf8_cstr<'a>(s: *const libc::c_char) -> Option<&'a str> { + if s.is_null() { + None + } else { + Some(utf8_cstr(s)) + } +} + +/// Error-tolerant C string import +pub unsafe fn cstr<'a>(s: *const libc::c_char) -> Cow<'a, str> { + if s.is_null() { + return Cow::Borrowed("(null)"); + } + CStr::from_ptr(s).to_string_lossy() +} + +/// Casts a pointer to `c_char` to a pointer to `u8`. +/// +/// The obvious `p as *const u8` or `p as *const _` produces a +/// trivial_casts warning when compiled on aarch64, where `c_char` is +/// unsigned (on Intel, it is signed, so the cast works). +/// +/// We do this here with a `transmute`, which is awkward to type, +/// so wrap it in a function. +pub unsafe fn c_char_as_u8_ptr(p: *const libc::c_char) -> *const u8 { + transmute::<_, *const u8>(p) +} + +/// Casts a pointer to `c_char` to a pointer to mutable `u8`. +/// +/// See [`c_char_as_u8_ptr`] for the reason for this. +pub unsafe fn c_char_as_u8_ptr_mut(p: *mut libc::c_char) -> *mut u8 { + transmute::<_, *mut u8>(p) +} + +pub fn clamp<T: PartialOrd>(val: T, low: T, high: T) -> T { + if val < low { + low + } else if val > high { + high + } else { + val + } +} + +#[macro_export] +macro_rules! enum_default { + ($name:ident, $default:expr) => { + impl Default for $name { + #[inline] + fn default() -> $name { + $default + } + } + }; +} diff --git a/rsvg/src/viewbox.rs b/rsvg/src/viewbox.rs new file mode 100644 index 00000000..e6ad39a9 --- /dev/null +++ b/rsvg/src/viewbox.rs @@ -0,0 +1,87 @@ +//! Parser for the `viewBox` attribute. + +use cssparser::Parser; +use std::ops::Deref; + +use crate::error::*; +use crate::parsers::{NumberList, Parse}; +use crate::rect::Rect; + +/// Newtype around a [`Rect`], used to represent the `viewBox` attribute. +/// +/// A `ViewBox` is a new user-space coordinate system mapped onto the rectangle defined by +/// the current viewport. See <https://www.w3.org/TR/SVG2/coords.html#ViewBoxAttribute> +/// +/// `ViewBox` derefs to [`Rect`], so you can use [`Rect`]'s methods and fields directly like +/// `vbox.x0` or `vbox.width()`. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct ViewBox(Rect); + +impl Deref for ViewBox { + type Target = Rect; + + fn deref(&self) -> &Rect { + &self.0 + } +} + +impl From<Rect> for ViewBox { + fn from(r: Rect) -> ViewBox { + ViewBox(r) + } +} + +impl Parse for ViewBox { + // Parse a viewBox attribute + // https://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute + // + // viewBox: double [,] double [,] double [,] double [,] + // + // x, y, w, h + // + // Where w and h must be nonnegative. + fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<ViewBox, ParseError<'i>> { + let loc = parser.current_source_location(); + + let NumberList::<4, 4>(v) = NumberList::parse(parser)?; + let (x, y, width, height) = (v[0], v[1], v[2], v[3]); + + if width >= 0.0 && height >= 0.0 { + Ok(ViewBox(Rect::new(x, y, x + width, y + height))) + } else { + Err(loc.new_custom_error(ValueErrorKind::value_error( + "width and height must not be negative", + ))) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_valid_viewboxes() { + assert_eq!( + ViewBox::parse_str(" 1 2 3 4").unwrap(), + ViewBox(Rect::new(1.0, 2.0, 4.0, 6.0)) + ); + + assert_eq!( + ViewBox::parse_str(" -1.5 -2.5e1,34,56e2 ").unwrap(), + ViewBox(Rect::new(-1.5, -25.0, 32.5, 5575.0)) + ); + } + + #[test] + fn parsing_invalid_viewboxes_yields_error() { + assert!(ViewBox::parse_str("").is_err()); + assert!(ViewBox::parse_str(" 1,2,-3,-4 ").is_err()); + assert!(ViewBox::parse_str("qwerasdfzxcv").is_err()); + assert!(ViewBox::parse_str(" 1 2 3 4 5").is_err()); + assert!(ViewBox::parse_str(" 1 2 foo 3 4").is_err()); + + // https://gitlab.gnome.org/GNOME/librsvg/issues/344 + assert!(ViewBox::parse_str("0 0 9E80.7").is_err()); + } +} diff --git a/rsvg/src/xml/attributes.rs b/rsvg/src/xml/attributes.rs new file mode 100644 index 00000000..844066cd --- /dev/null +++ b/rsvg/src/xml/attributes.rs @@ -0,0 +1,257 @@ +//! Store XML element attributes and their values. + +use std::slice; +use std::str; + +use markup5ever::{ + expanded_name, local_name, namespace_url, ns, LocalName, Namespace, Prefix, QualName, +}; +use string_cache::DefaultAtom; + +use crate::error::{ImplementationLimit, LoadingError}; +use crate::limits; +use crate::util::{c_char_as_u8_ptr, opt_utf8_cstr, utf8_cstr}; + +/// Type used to store attribute values. +/// +/// Attribute values are often repeated in an SVG file, so we intern them using the +/// string_cache crate. +pub type AttributeValue = DefaultAtom; + +/// Iterable wrapper for libxml2's representation of attribute/value. +/// +/// See the [`new_from_xml2_attributes`] function for information. +/// +/// [`new_from_xml2_attributes`]: #method.new_from_xml2_attributes +#[derive(Clone)] +pub struct Attributes { + attrs: Box<[(QualName, AttributeValue)]>, + id_idx: Option<u16>, + class_idx: Option<u16>, +} + +/// Iterator from `Attributes.iter`. +pub struct AttributesIter<'a>(slice::Iter<'a, (QualName, AttributeValue)>); + +impl Attributes { + #[cfg(test)] + pub fn new() -> Attributes { + Attributes { + attrs: [].into(), + id_idx: None, + class_idx: None, + } + } + + /// Creates an iterable `Attributes` from the C array of borrowed C strings. + /// + /// With libxml2's SAX parser, the caller's startElementNsSAX2Func + /// callback gets passed a `xmlChar **` for attributes, which + /// comes in groups of (localname/prefix/URI/value_start/value_end). + /// In those, localname/prefix/URI are NUL-terminated strings; + /// value_start and value_end point to the start-inclusive and + /// end-exclusive bytes in the attribute's value. + /// + /// # Safety + /// + /// This function is unsafe because the caller must guarantee the following: + /// + /// * `attrs` is a valid pointer, with (n_attributes * 5) elements. + /// + /// * All strings are valid UTF-8. + pub unsafe fn new_from_xml2_attributes( + n_attributes: usize, + attrs: *const *const libc::c_char, + ) -> Result<Attributes, LoadingError> { + let mut array = Vec::with_capacity(n_attributes); + let mut id_idx = None; + let mut class_idx = None; + + if n_attributes > limits::MAX_LOADED_ATTRIBUTES { + return Err(LoadingError::LimitExceeded( + ImplementationLimit::TooManyAttributes, + )); + } + + if n_attributes > 0 && !attrs.is_null() { + for attr in slice::from_raw_parts(attrs, n_attributes * 5).chunks_exact(5) { + let localname = attr[0]; + let prefix = attr[1]; + let uri = attr[2]; + let value_start = attr[3]; + let value_end = attr[4]; + + assert!(!localname.is_null()); + + let localname = utf8_cstr(localname); + + let prefix = opt_utf8_cstr(prefix); + let uri = opt_utf8_cstr(uri); + let qual_name = QualName::new( + prefix.map(Prefix::from), + uri.map(Namespace::from) + .unwrap_or_else(|| namespace_url!("")), + LocalName::from(localname), + ); + + if !value_start.is_null() && !value_end.is_null() { + assert!(value_end >= value_start); + + // FIXME: ptr::offset_from() is nightly-only. + // We'll do the computation of the length by hand. + let start = value_start as usize; + let end = value_end as usize; + let len = end - start; + + let value_slice = slice::from_raw_parts(c_char_as_u8_ptr(value_start), len); + let value_str = str::from_utf8_unchecked(value_slice); + let value_atom = DefaultAtom::from(value_str); + + let idx = array.len() as u16; + match qual_name.expanded() { + expanded_name!("", "id") => id_idx = Some(idx), + expanded_name!("", "class") => class_idx = Some(idx), + _ => (), + } + + array.push((qual_name, value_atom)); + } + } + } + + Ok(Attributes { + attrs: array.into(), + id_idx, + class_idx, + }) + } + + /// Returns the number of attributes. + pub fn len(&self) -> usize { + self.attrs.len() + } + + /// Creates an iterator that yields `(QualName, &'a str)` tuples. + pub fn iter(&self) -> AttributesIter<'_> { + AttributesIter(self.attrs.iter()) + } + + pub fn get_id(&self) -> Option<&str> { + self.id_idx.and_then(|idx| { + self.attrs + .get(usize::from(idx)) + .map(|(_name, value)| &value[..]) + }) + } + + pub fn get_class(&self) -> Option<&str> { + self.class_idx.and_then(|idx| { + self.attrs + .get(usize::from(idx)) + .map(|(_name, value)| &value[..]) + }) + } + + pub fn clear_class(&mut self) { + self.class_idx = None; + } +} + +impl<'a> Iterator for AttributesIter<'a> { + type Item = (QualName, &'a str); + + fn next(&mut self) -> Option<Self::Item> { + self.0.next().map(|(a, v)| (a.clone(), v.as_ref())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use markup5ever::{expanded_name, local_name, namespace_url, ns}; + use std::ffi::CString; + use std::ptr; + + #[test] + fn empty_attributes() { + let map = unsafe { Attributes::new_from_xml2_attributes(0, ptr::null()).unwrap() }; + assert_eq!(map.len(), 0); + } + + #[test] + fn attributes_with_namespaces() { + let attrs = [ + ( + CString::new("href").unwrap(), + Some(CString::new("xlink").unwrap()), + Some(CString::new("http://www.w3.org/1999/xlink").unwrap()), + CString::new("1").unwrap(), + ), + ( + CString::new("ry").unwrap(), + None, + None, + CString::new("2").unwrap(), + ), + ( + CString::new("d").unwrap(), + None, + None, + CString::new("").unwrap(), + ), + ]; + + let mut v: Vec<*const libc::c_char> = Vec::new(); + + for (localname, prefix, uri, val) in &attrs { + v.push(localname.as_ptr()); + v.push( + prefix + .as_ref() + .map(|p: &CString| p.as_ptr()) + .unwrap_or_else(|| ptr::null()), + ); + v.push( + uri.as_ref() + .map(|p: &CString| p.as_ptr()) + .unwrap_or_else(|| ptr::null()), + ); + + let val_start = val.as_ptr(); + let val_end = unsafe { val_start.offset(val.as_bytes().len() as isize) }; + v.push(val_start); // value_start + v.push(val_end); // value_end + } + + let attrs = unsafe { Attributes::new_from_xml2_attributes(3, v.as_ptr()).unwrap() }; + + let mut had_href: bool = false; + let mut had_ry: bool = false; + let mut had_d: bool = false; + + for (a, v) in attrs.iter() { + match a.expanded() { + expanded_name!(xlink "href") => { + assert!(v == "1"); + had_href = true; + } + + expanded_name!("", "ry") => { + assert!(v == "2"); + had_ry = true; + } + + expanded_name!("", "d") => { + assert!(v == ""); + had_d = true; + } + + _ => unreachable!(), + } + } + + assert!(had_href); + assert!(had_ry); + assert!(had_d); + } +} diff --git a/rsvg/src/xml/mod.rs b/rsvg/src/xml/mod.rs new file mode 100644 index 00000000..0c75f8a8 --- /dev/null +++ b/rsvg/src/xml/mod.rs @@ -0,0 +1,787 @@ +//! The main XML parser. + +use encoding_rs::Encoding; +use gio::{ + prelude::BufferedInputStreamExt, BufferedInputStream, Cancellable, ConverterInputStream, + InputStream, ZlibCompressorFormat, ZlibDecompressor, +}; +use glib::Cast; +use markup5ever::{ + expanded_name, local_name, namespace_url, ns, ExpandedName, LocalName, Namespace, QualName, +}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +use std::str; +use std::string::ToString; +use std::sync::Arc; +use xml5ever::buffer_queue::BufferQueue; +use xml5ever::tendril::format_tendril; +use xml5ever::tokenizer::{TagKind, Token, TokenSink, XmlTokenizer, XmlTokenizerOpts}; + +use crate::css::{Origin, Stylesheet}; +use crate::document::{Document, DocumentBuilder}; +use crate::error::{ImplementationLimit, LoadingError}; +use crate::handle::LoadOptions; +use crate::io::{self, IoError}; +use crate::limits::{MAX_LOADED_ELEMENTS, MAX_XINCLUDE_DEPTH}; +use crate::node::{Node, NodeBorrow}; +use crate::session::Session; +use crate::style::StyleType; +use crate::url_resolver::AllowedUrl; + +use xml2_load::Xml2Parser; + +mod attributes; +mod xml2; +mod xml2_load; + +pub use attributes::Attributes; + +#[derive(Clone)] +enum Context { + // Starting state + Start, + + // Creating nodes for elements under the current node + ElementCreation, + + // Inside <style>; accumulate text to include in a stylesheet + Style, + + // An unsupported element inside a `<style>` element, to be ignored + UnsupportedStyleChild, + + // Inside <xi:include> + XInclude(XIncludeContext), + + // An unsupported element inside a <xi:include> context, to be ignored + UnsupportedXIncludeChild, + + // Insie <xi::fallback> + XIncludeFallback(XIncludeContext), + + // An XML parsing error was found. We will no-op upon any further XML events. + FatalError(LoadingError), +} + +#[derive(Clone)] +struct XIncludeContext { + need_fallback: bool, +} + +// This is to hold an xmlEntityPtr from libxml2; we just hold an opaque pointer +// that is freed in impl Drop for XmlState +type XmlEntityPtr = *mut libc::c_void; + +extern "C" { + // The original function takes an xmlNodePtr, but that is compatible + // with xmlEntityPtr for the purposes of this function. + fn xmlFreeNode(node: XmlEntityPtr); +} + +// Creates an ExpandedName from the XInclude namespace and a local_name +// +// The markup5ever crate doesn't have built-in namespaces for XInclude, +// so we make our own. +macro_rules! xinclude_name { + ($local_name:expr) => { + ExpandedName { + ns: &Namespace::from("http://www.w3.org/2001/XInclude"), + local: &LocalName::from($local_name), + } + }; +} + +/// Holds the state used for XML processing +/// +/// These methods are called when an XML event is parsed out of the XML stream: `start_element`, +/// `end_element`, `characters`. +/// +/// When an element starts, we push a corresponding `Context` into the `context_stack`. Within +/// that context, all XML events will be forwarded to it, and processed in one of the `XmlHandler` +/// trait objects. Normally the context refers to a `NodeCreationContext` implementation which is +/// what creates normal graphical elements. +struct XmlStateInner { + document_builder: DocumentBuilder, + num_loaded_elements: usize, + xinclude_depth: usize, + context_stack: Vec<Context>, + current_node: Option<Node>, + + // Note that neither XmlStateInner nor Xmlstate implement Drop. + // + // An XmlState is finally consumed in XmlState::build_document(), and that + // function is responsible for freeing all the XmlEntityPtr from this field. + // + // (The structs cannot impl Drop because build_document() + // destructures and consumes them at the same time.) + entities: HashMap<String, XmlEntityPtr>, +} + +pub struct XmlState { + inner: RefCell<XmlStateInner>, + + session: Session, + load_options: Arc<LoadOptions>, +} + +/// Errors returned from XmlState::acquire() +/// +/// These follow the terminology from <https://www.w3.org/TR/xinclude/#terminology> +enum AcquireError { + /// Resource could not be acquired (file not found), or I/O error. + /// In this case, the `xi:fallback` can be used if present. + ResourceError, + + /// Resource could not be parsed/decoded + FatalError(String), +} + +impl XmlStateInner { + fn context(&self) -> Context { + // We can unwrap since the stack is never empty + self.context_stack.last().unwrap().clone() + } +} + +impl XmlState { + fn new( + session: Session, + document_builder: DocumentBuilder, + load_options: Arc<LoadOptions>, + ) -> XmlState { + XmlState { + inner: RefCell::new(XmlStateInner { + document_builder, + num_loaded_elements: 0, + xinclude_depth: 0, + context_stack: vec![Context::Start], + current_node: None, + entities: HashMap::new(), + }), + + session, + load_options, + } + } + + fn check_last_error(&self) -> Result<(), LoadingError> { + let inner = self.inner.borrow(); + + match inner.context() { + Context::FatalError(e) => Err(e), + _ => Ok(()), + } + } + + fn check_limits(&self) -> Result<(), ()> { + if self.inner.borrow().num_loaded_elements > MAX_LOADED_ELEMENTS { + self.error(LoadingError::LimitExceeded( + ImplementationLimit::TooManyLoadedElements, + )); + Err(()) + } else { + Ok(()) + } + } + + pub fn start_element(&self, name: QualName, attrs: Attributes) -> Result<(), ()> { + self.check_limits()?; + + let context = self.inner.borrow().context(); + + if let Context::FatalError(_) = context { + return Err(()); + } + + self.inner.borrow_mut().num_loaded_elements += 1; + + let new_context = match context { + Context::Start => self.element_creation_start_element(&name, attrs), + Context::ElementCreation => self.element_creation_start_element(&name, attrs), + + Context::Style => self.inside_style_start_element(&name), + Context::UnsupportedStyleChild => self.unsupported_style_start_element(&name), + + Context::XInclude(ref ctx) => self.inside_xinclude_start_element(ctx, &name), + Context::UnsupportedXIncludeChild => self.unsupported_xinclude_start_element(&name), + Context::XIncludeFallback(ref ctx) => { + self.xinclude_fallback_start_element(ctx, &name, attrs) + } + + Context::FatalError(_) => unreachable!(), + }; + + self.inner.borrow_mut().context_stack.push(new_context); + + Ok(()) + } + + pub fn end_element(&self, _name: QualName) { + let context = self.inner.borrow().context(); + + match context { + Context::Start => panic!("end_element: XML handler stack is empty!?"), + Context::ElementCreation => self.element_creation_end_element(), + + Context::Style => self.style_end_element(), + Context::UnsupportedStyleChild => (), + + Context::XInclude(_) => (), + Context::UnsupportedXIncludeChild => (), + Context::XIncludeFallback(_) => (), + + Context::FatalError(_) => return, + } + + // We can unwrap since start_element() always adds a context to the stack + self.inner.borrow_mut().context_stack.pop().unwrap(); + } + + pub fn characters(&self, text: &str) { + let context = self.inner.borrow().context(); + + match context { + Context::Start => { + // This is character data before the first element, i.e. something like + // <?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg"/> + // ^ note the space here + // libxml2 is not finished reading the file yet; it will emit an error + // on its own when it finishes. So, ignore this condition. + } + + Context::ElementCreation => self.element_creation_characters(text), + + Context::Style => self.element_creation_characters(text), + Context::UnsupportedStyleChild => (), + + Context::XInclude(_) => (), + Context::UnsupportedXIncludeChild => (), + Context::XIncludeFallback(ref ctx) => self.xinclude_fallback_characters(ctx, text), + Context::FatalError(_) => (), + } + } + + pub fn processing_instruction(&self, target: &str, data: &str) { + if target != "xml-stylesheet" { + return; + } + + if let Ok(pairs) = parse_xml_stylesheet_processing_instruction(data) { + let mut alternate = None; + let mut type_ = None; + let mut href = None; + + for (att, value) in pairs { + match att.as_str() { + "alternate" => alternate = Some(value), + "type" => type_ = Some(value), + "href" => href = Some(value), + _ => (), + } + } + + let mut inner = self.inner.borrow_mut(); + + if type_.as_deref() != Some("text/css") + || (alternate.is_some() && alternate.as_deref() != Some("no")) + { + rsvg_log!( + self.session, + "invalid parameters in XML processing instruction for stylesheet", + ); + return; + } + + if let Some(href) = href { + if let Ok(aurl) = self.load_options.url_resolver.resolve_href(&href) { + if let Ok(stylesheet) = + Stylesheet::from_href(&aurl, Origin::Author, self.session.clone()) + { + inner.document_builder.append_stylesheet(stylesheet); + } else { + // FIXME: https://www.w3.org/TR/xml-stylesheet/ does not seem to specify + // what to do if the stylesheet cannot be loaded, so here we ignore the error. + rsvg_log!( + self.session, + "could not create stylesheet from {} in XML processing instruction", + href + ); + } + } else { + rsvg_log!( + self.session, + "{} not allowed for xml-stylesheet in XML processing instruction", + href + ); + } + } else { + rsvg_log!( + self.session, + "xml-stylesheet processing instruction does not have href; ignoring" + ); + } + } else { + self.error(LoadingError::XmlParseError(String::from( + "invalid processing instruction data in xml-stylesheet", + ))); + } + } + + pub fn error(&self, e: LoadingError) { + self.inner + .borrow_mut() + .context_stack + .push(Context::FatalError(e)); + } + + pub fn entity_lookup(&self, entity_name: &str) -> Option<XmlEntityPtr> { + self.inner.borrow().entities.get(entity_name).copied() + } + + pub fn entity_insert(&self, entity_name: &str, entity: XmlEntityPtr) { + let mut inner = self.inner.borrow_mut(); + + let old_value = inner.entities.insert(entity_name.to_string(), entity); + + if let Some(v) = old_value { + unsafe { + xmlFreeNode(v); + } + } + } + + fn element_creation_start_element(&self, name: &QualName, attrs: Attributes) -> Context { + if name.expanded() == xinclude_name!("include") { + self.xinclude_start_element(name, attrs) + } else { + let mut inner = self.inner.borrow_mut(); + + let parent = inner.current_node.clone(); + let node = inner.document_builder.append_element(name, attrs, parent); + inner.current_node = Some(node); + + if name.expanded() == expanded_name!(svg "style") { + Context::Style + } else { + Context::ElementCreation + } + } + } + + fn element_creation_end_element(&self) { + let mut inner = self.inner.borrow_mut(); + let node = inner.current_node.take().unwrap(); + inner.current_node = node.parent(); + } + + fn element_creation_characters(&self, text: &str) { + let mut inner = self.inner.borrow_mut(); + + let mut parent = inner.current_node.clone().unwrap(); + inner.document_builder.append_characters(text, &mut parent); + } + + fn style_end_element(&self) { + self.add_inline_stylesheet(); + self.element_creation_end_element() + } + + fn add_inline_stylesheet(&self) { + let mut inner = self.inner.borrow_mut(); + let current_node = inner.current_node.as_ref().unwrap(); + + let style_type = borrow_element_as!(current_node, Style).style_type(); + + if style_type == StyleType::TextCss { + let stylesheet_text = current_node + .children() + .map(|child| { + // Note that here we assume that the only children of <style> + // are indeed text nodes. + let child_borrow = child.borrow_chars(); + child_borrow.get_string() + }) + .collect::<String>(); + + if let Ok(stylesheet) = Stylesheet::from_data( + &stylesheet_text, + &self.load_options.url_resolver, + Origin::Author, + self.session.clone(), + ) { + inner.document_builder.append_stylesheet(stylesheet); + } else { + rsvg_log!(self.session, "invalid inline stylesheet"); + } + } + } + + fn inside_style_start_element(&self, name: &QualName) -> Context { + self.unsupported_style_start_element(name) + } + + fn unsupported_style_start_element(&self, _name: &QualName) -> Context { + Context::UnsupportedStyleChild + } + + fn xinclude_start_element(&self, _name: &QualName, attrs: Attributes) -> Context { + let mut href = None; + let mut parse = None; + let mut encoding = None; + + let ln_parse = LocalName::from("parse"); + + for (attr, value) in attrs.iter() { + match attr.expanded() { + expanded_name!("", "href") => href = Some(value), + ref v + if *v + == ExpandedName { + ns: &ns!(), + local: &ln_parse, + } => + { + parse = Some(value) + } + expanded_name!("", "encoding") => encoding = Some(value), + _ => (), + } + } + + let need_fallback = match self.acquire(href, parse, encoding) { + Ok(()) => false, + Err(AcquireError::ResourceError) => true, + Err(AcquireError::FatalError(s)) => { + return Context::FatalError(LoadingError::XmlParseError(s)) + } + }; + + Context::XInclude(XIncludeContext { need_fallback }) + } + + fn inside_xinclude_start_element(&self, ctx: &XIncludeContext, name: &QualName) -> Context { + if name.expanded() == xinclude_name!("fallback") { + Context::XIncludeFallback(ctx.clone()) + } else { + // https://www.w3.org/TR/xinclude/#include_element + // + // "Other content (text, processing instructions, + // comments, elements not in the XInclude namespace, + // descendants of child elements) is not constrained by + // this specification and is ignored by the XInclude + // processor" + + self.unsupported_xinclude_start_element(name) + } + } + + fn xinclude_fallback_start_element( + &self, + ctx: &XIncludeContext, + name: &QualName, + attrs: Attributes, + ) -> Context { + if ctx.need_fallback { + if name.expanded() == xinclude_name!("include") { + self.xinclude_start_element(name, attrs) + } else { + self.element_creation_start_element(name, attrs) + } + } else { + Context::UnsupportedXIncludeChild + } + } + + fn xinclude_fallback_characters(&self, ctx: &XIncludeContext, text: &str) { + if ctx.need_fallback && self.inner.borrow().current_node.is_some() { + // We test for is_some() because with a bad "SVG" file like this: + // + // <xi:include href="blah"><xi:fallback>foo</xi:fallback></xi:include> + // + // at the point we get "foo" here, there is no current_node because + // no nodes have been created before the xi:include. + self.element_creation_characters(text); + } + } + + fn acquire( + &self, + href: Option<&str>, + parse: Option<&str>, + encoding: Option<&str>, + ) -> Result<(), AcquireError> { + if let Some(href) = href { + let aurl = self + .load_options + .url_resolver + .resolve_href(href) + .map_err(|e| { + // FIXME: should AlloweUrlError::UrlParseError be a fatal error, + // not a resource error? + rsvg_log!(self.session, "could not acquire \"{}\": {}", href, e); + AcquireError::ResourceError + })?; + + // https://www.w3.org/TR/xinclude/#include_element + // + // "When omitted, the value of "xml" is implied (even in + // the absence of a default value declaration). Values + // other than "xml" and "text" are a fatal error." + match parse { + None | Some("xml") => self.include_xml(&aurl), + + Some("text") => self.acquire_text(&aurl, encoding), + + Some(v) => Err(AcquireError::FatalError(format!( + "unknown 'parse' attribute value: \"{v}\"" + ))), + } + } else { + // The href attribute is not present. Per + // https://www.w3.org/TR/xinclude/#include_element we + // should use the xpointer attribute, but we do not + // support that yet. So, we'll just say, "OK" and not + // actually include anything. + Ok(()) + } + } + + fn include_xml(&self, aurl: &AllowedUrl) -> Result<(), AcquireError> { + self.increase_xinclude_depth(aurl)?; + + let result = self.acquire_xml(aurl); + + self.decrease_xinclude_depth(); + + result + } + + fn increase_xinclude_depth(&self, aurl: &AllowedUrl) -> Result<(), AcquireError> { + let mut inner = self.inner.borrow_mut(); + + if inner.xinclude_depth == MAX_XINCLUDE_DEPTH { + Err(AcquireError::FatalError(format!( + "exceeded maximum level of nested xinclude in {aurl}" + ))) + } else { + inner.xinclude_depth += 1; + Ok(()) + } + } + + fn decrease_xinclude_depth(&self) { + let mut inner = self.inner.borrow_mut(); + inner.xinclude_depth -= 1; + } + + fn acquire_text(&self, aurl: &AllowedUrl, encoding: Option<&str>) -> Result<(), AcquireError> { + let binary = io::acquire_data(aurl, None).map_err(|e| { + rsvg_log!(self.session, "could not acquire \"{}\": {}", aurl, e); + AcquireError::ResourceError + })?; + + let encoding = encoding.unwrap_or("utf-8"); + + let encoder = Encoding::for_label_no_replacement(encoding.as_bytes()).ok_or_else(|| { + AcquireError::FatalError(format!("unknown encoding \"{encoding}\" for \"{aurl}\"")) + })?; + + let utf8_data = encoder + .decode_without_bom_handling_and_without_replacement(&binary.data) + .ok_or_else(|| { + AcquireError::FatalError(format!("could not convert contents of \"{aurl}\" from character encoding \"{encoding}\"")) + })?; + + self.element_creation_characters(&utf8_data); + Ok(()) + } + + fn acquire_xml(&self, aurl: &AllowedUrl) -> Result<(), AcquireError> { + // FIXME: distinguish between "file not found" and "invalid XML" + + let stream = io::acquire_stream(aurl, None).map_err(|e| match e { + IoError::BadDataUrl => AcquireError::FatalError(String::from("malformed data: URL")), + _ => AcquireError::ResourceError, + })?; + + // FIXME: pass a cancellable + self.parse_from_stream(&stream, None).map_err(|e| match e { + LoadingError::Io(_) => AcquireError::ResourceError, + LoadingError::XmlParseError(s) => AcquireError::FatalError(s), + _ => AcquireError::FatalError(String::from("unknown error")), + }) + } + + // Parses XML from a stream into an XmlState. + // + // This can be called "in the middle" of an XmlState's processing status, + // for example, when including another XML file via xi:include. + fn parse_from_stream( + &self, + stream: &gio::InputStream, + cancellable: Option<&gio::Cancellable>, + ) -> Result<(), LoadingError> { + Xml2Parser::from_stream(self, self.load_options.unlimited_size, stream, cancellable) + .and_then(|parser| parser.parse()) + .and_then(|_: ()| self.check_last_error()) + } + + fn unsupported_xinclude_start_element(&self, _name: &QualName) -> Context { + Context::UnsupportedXIncludeChild + } + + fn build_document( + self, + stream: &gio::InputStream, + cancellable: Option<&gio::Cancellable>, + ) -> Result<Document, LoadingError> { + self.parse_from_stream(stream, cancellable)?; + + // consume self, then consume inner, then consume document_builder by calling .build() + + let XmlState { inner, .. } = self; + let mut inner = inner.into_inner(); + + // Free the hash of XmlEntityPtr. We cannot do this in Drop because we will + // consume inner by destructuring it after the for() loop. + for (_key, entity) in inner.entities.drain() { + unsafe { + xmlFreeNode(entity); + } + } + + let XmlStateInner { + document_builder, .. + } = inner; + document_builder.build() + } +} + +/// Temporary holding space for data in an XML processing instruction +#[derive(Default)] +struct ProcessingInstructionData { + attributes: Vec<(String, String)>, + error: bool, +} + +struct ProcessingInstructionSink(Rc<RefCell<ProcessingInstructionData>>); + +impl TokenSink for ProcessingInstructionSink { + fn process_token(&mut self, token: Token) { + let mut data = self.0.borrow_mut(); + + match token { + Token::TagToken(tag) if tag.kind == TagKind::EmptyTag => { + for a in &tag.attrs { + let name = a.name.local.as_ref().to_string(); + let value = a.value.to_string(); + + data.attributes.push((name, value)); + } + } + + Token::ParseError(_) => data.error = true, + + _ => (), + } + } +} + +// https://www.w3.org/TR/xml-stylesheet/ +// +// The syntax for the xml-stylesheet processing instruction we support +// is this: +// +// <?xml-stylesheet href="uri" alternate="no" type="text/css"?> +// +// XML parsers just feed us the raw data after the target name +// ("xml-stylesheet"), so we'll create a mini-parser with a hackish +// element just to extract the data as attributes. +fn parse_xml_stylesheet_processing_instruction(data: &str) -> Result<Vec<(String, String)>, ()> { + let pi_data = Rc::new(RefCell::new(ProcessingInstructionData { + attributes: Vec::new(), + error: false, + })); + + let mut queue = BufferQueue::new(); + queue.push_back(format_tendril!("<rsvg-hack {} />", data)); + + let sink = ProcessingInstructionSink(pi_data.clone()); + + let mut tokenizer = XmlTokenizer::new(sink, XmlTokenizerOpts::default()); + tokenizer.run(&mut queue); + + let pi_data = pi_data.borrow(); + + if pi_data.error { + Err(()) + } else { + Ok(pi_data.attributes.clone()) + } +} + +pub fn xml_load_from_possibly_compressed_stream( + session: Session, + document_builder: DocumentBuilder, + load_options: Arc<LoadOptions>, + stream: &gio::InputStream, + cancellable: Option<&gio::Cancellable>, +) -> Result<Document, LoadingError> { + let state = XmlState::new(session, document_builder, load_options); + + let stream = get_input_stream_for_loading(stream, cancellable)?; + + state.build_document(&stream, cancellable) +} + +// Header of a gzip data stream +const GZ_MAGIC_0: u8 = 0x1f; +const GZ_MAGIC_1: u8 = 0x8b; + +fn get_input_stream_for_loading( + stream: &InputStream, + cancellable: Option<&Cancellable>, +) -> Result<InputStream, LoadingError> { + // detect gzipped streams (svgz) + + let buffered = BufferedInputStream::new(stream); + let num_read = buffered.fill(2, cancellable)?; + if num_read < 2 { + // FIXME: this string was localized in the original; localize it + return Err(LoadingError::XmlParseError(String::from( + "Input file is too short", + ))); + } + + let buf = buffered.peek_buffer(); + assert!(buf.len() >= 2); + if buf[0..2] == [GZ_MAGIC_0, GZ_MAGIC_1] { + let decomp = ZlibDecompressor::new(ZlibCompressorFormat::Gzip); + let converter = ConverterInputStream::new(&buffered, &decomp); + Ok(converter.upcast::<InputStream>()) + } else { + Ok(buffered.upcast::<InputStream>()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_processing_instruction_data() { + let mut r = + parse_xml_stylesheet_processing_instruction("foo=\"bar\" baz=\"beep\"").unwrap(); + r.sort_by(|a, b| a.0.cmp(&b.0)); + + assert_eq!( + r, + vec![ + ("baz".to_string(), "beep".to_string()), + ("foo".to_string(), "bar".to_string()) + ] + ); + } +} diff --git a/rsvg/src/xml/xml2.rs b/rsvg/src/xml/xml2.rs new file mode 100644 index 00000000..b2f9785f --- /dev/null +++ b/rsvg/src/xml/xml2.rs @@ -0,0 +1,212 @@ +//! Hand-written binding to the very minimal part of libxml2 that we need. + +#![allow(clippy::upper_case_acronyms)] +#![allow(non_snake_case, non_camel_case_types)] + +use glib::ffi::gpointer; + +pub const XML_CHAR_ENCODING_NONE: libc::c_int = 0; + +pub const XML_INTERNAL_GENERAL_ENTITY: libc::c_int = 1; + +pub const XML_PARSE_NONET: libc::c_int = 1 << 11; +pub const XML_PARSE_HUGE: libc::c_int = 1 << 19; +pub const XML_PARSE_BIG_LINES: libc::c_int = 1 << 22; + +pub const XML_SAX2_MAGIC: libc::c_uint = 0xDEEDBEAF; + +pub type xmlDocPtr = gpointer; + +pub type xmlEntityPtr = gpointer; + +pub type UnusedFn = Option<unsafe extern "C" fn()>; + +#[rustfmt::skip] +#[repr(C)] +pub struct xmlSAXHandler { + pub internalSubset: UnusedFn, + pub isStandalone: UnusedFn, + pub hasInternalSubset: UnusedFn, + pub hasExternalSubset: UnusedFn, + pub resolveEntity: UnusedFn, + + pub getEntity: Option<unsafe extern "C" fn( + ctx: *mut libc::c_void, + name: *const libc::c_char, + ) -> xmlEntityPtr>, + + pub entityDecl: Option<unsafe extern "C" fn( + ctx: *mut libc::c_void, + name: *const libc::c_char, + type_: libc::c_int, + public_id: *const libc::c_char, + system_id: *const libc::c_char, + content: *const libc::c_char, + )>, + + pub notationDecl: UnusedFn, + pub attributeDecl: UnusedFn, + pub elementDecl: UnusedFn, + + pub unparsedEntityDecl: Option<unsafe extern "C" fn( + ctx: *mut libc::c_void, + name: *const libc::c_char, + public_id: *const libc::c_char, + system_id: *const libc::c_char, + notation_name: *const libc::c_char, + )>, + + pub setDocumentLocator: UnusedFn, + pub startDocument: UnusedFn, + pub endDocument: UnusedFn, + pub startElement: UnusedFn, + pub endElement: UnusedFn, + + pub reference: UnusedFn, + + pub characters: Option<unsafe extern "C" fn( + ctx: *mut libc::c_void, + ch: *const libc::c_char, + len: libc::c_int, + )>, + + pub ignorableWhitespace: UnusedFn, + + pub processingInstruction: Option<unsafe extern "C" fn( + ctx: *mut libc::c_void, + target: *const libc::c_char, + data: *const libc::c_char, + )>, + + pub comment: UnusedFn, + pub warning: UnusedFn, + + pub error: UnusedFn, + + pub fatalError: UnusedFn, + + pub getParameterEntity: Option<unsafe extern "C" fn( + ctx: *mut libc::c_void, + name: *const libc::c_char, + ) -> xmlEntityPtr>, + + pub cdataBlock: Option<unsafe extern "C" fn( + ctx: *mut libc::c_void, + value: *const libc::c_char, + len: libc::c_int, + )>, + + pub externalSubset: UnusedFn, + + pub initialized: libc::c_uint, + + pub _private: gpointer, + + pub startElementNs: Option<unsafe extern "C" fn( + ctx: *mut libc::c_void, + localname: *mut libc::c_char, + prefix: *mut libc::c_char, + uri: *mut libc::c_char, + nb_namespaces: libc::c_int, + namespaces: *mut *mut libc::c_char, + nb_attributes: libc::c_int, + nb_defaulted: libc::c_int, + attributes: *mut *mut libc::c_char, + )>, + + pub endElementNs: Option<unsafe extern "C" fn( + ctx: *mut libc::c_void, + localname: *mut libc::c_char, + prefix: *mut libc::c_char, + uri: *mut libc::c_char, + )>, + + pub serror: Option<unsafe extern "C" fn(user_data: *mut libc::c_void, error: xmlErrorPtr)>, +} + +pub type xmlSAXHandlerPtr = *mut xmlSAXHandler; + +// The original struct _xmlParserCtxt in libxml2 has a *ton* of +// fields; mostly are implementation details. We only require access +// to fields up to replaceEntities, so we'll represent up to that +// field, and ignore subsequent ones. This struct is used just to +// cast the xmlParserCtxtPtr that we get out of libxml2 into a +// Rust-visible structure; we don't need a complete representation of the +// original struct. +#[repr(C)] +pub struct xmlParserCtxt { + pub sax: gpointer, + pub userData: gpointer, + pub myDoc: xmlDocPtr, + pub wellFormed: libc::c_int, + pub replaceEntities: libc::c_int, + // ... libxml2 has more fields here; we don't use them +} + +pub type xmlParserCtxtPtr = *mut xmlParserCtxt; + +#[repr(C)] +pub struct xmlError { + pub domain: libc::c_int, + pub code: libc::c_int, + pub message: *const libc::c_char, + pub level: libc::c_int, + pub file: *const libc::c_char, + pub line: libc::c_int, + pub str1: *const libc::c_char, + pub str2: *const libc::c_char, + pub str3: *const libc::c_char, + pub int1: libc::c_int, + pub int2: libc::c_int, + pub ctxt: gpointer, + pub node: gpointer, +} + +pub type xmlErrorPtr = *mut xmlError; + +pub type xmlInputReadCallback = Option< + unsafe extern "C" fn( + context: *mut libc::c_void, + buffer: *mut libc::c_char, + len: libc::c_int, + ) -> libc::c_int, +>; + +pub type xmlInputCloseCallback = + Option<unsafe extern "C" fn(context: *mut libc::c_void) -> libc::c_int>; + +pub type xmlCharEncoding = libc::c_int; + +extern "C" { + pub fn xmlInitParser(); + + pub fn xmlCreateIOParserCtxt( + sax: xmlSAXHandlerPtr, + user_data: *mut libc::c_void, + ioread: xmlInputReadCallback, + ioclose: xmlInputCloseCallback, + ioctx: *mut libc::c_void, + enc: xmlCharEncoding, + ) -> xmlParserCtxtPtr; + + pub fn xmlStopParser(ctxt: xmlParserCtxtPtr); + + pub fn xmlParseDocument(ctxt: xmlParserCtxtPtr) -> libc::c_int; + + pub fn xmlFreeDoc(doc: xmlDocPtr); + + pub fn xmlFreeParserCtxt(ctxt: xmlParserCtxtPtr); + + pub fn xmlCtxtGetLastError(ctxt: *mut libc::c_void) -> xmlErrorPtr; + + pub fn xmlCtxtUseOptions(ctxt: xmlParserCtxtPtr, options: libc::c_int) -> libc::c_int; + + pub fn xmlNewEntity( + doc: xmlDocPtr, + name: *const libc::c_char, + type_: libc::c_int, + external_id: *const libc::c_char, + system_id: *const libc::c_char, + content: *const libc::c_char, + ) -> xmlEntityPtr; +} diff --git a/rsvg/src/xml/xml2_load.rs b/rsvg/src/xml/xml2_load.rs new file mode 100644 index 00000000..9dcdac4e --- /dev/null +++ b/rsvg/src/xml/xml2_load.rs @@ -0,0 +1,517 @@ +//! Glue between the libxml2 API and our xml parser module. +//! +//! This file provides functions to create a libxml2 xmlParserCtxtPtr, configured +//! to read from a gio::InputStream, and to maintain its loading data in an XmlState. + +use gio::prelude::*; +use std::borrow::Cow; +use std::cell::{Cell, RefCell}; +use std::ptr; +use std::rc::Rc; +use std::slice; +use std::str; +use std::sync::Once; + +use glib::translate::*; +use markup5ever::{namespace_url, ns, LocalName, Namespace, Prefix, QualName}; + +use crate::error::LoadingError; +use crate::util::{c_char_as_u8_ptr, c_char_as_u8_ptr_mut, cstr, opt_utf8_cstr, utf8_cstr}; + +use super::xml2::*; +use super::Attributes; +use super::XmlState; + +#[rustfmt::skip] +fn get_xml2_sax_handler() -> xmlSAXHandler { + xmlSAXHandler { + // first the unused callbacks + internalSubset: None, + isStandalone: None, + hasInternalSubset: None, + hasExternalSubset: None, + resolveEntity: None, + notationDecl: None, + attributeDecl: None, + elementDecl: None, + setDocumentLocator: None, + startDocument: None, + endDocument: None, + reference: None, + ignorableWhitespace: None, + comment: None, + warning: None, + error: None, + fatalError: None, + externalSubset: None, + + _private: ptr::null_mut(), + + // then the used callbacks + getEntity: Some(sax_get_entity_cb), + entityDecl: Some(sax_entity_decl_cb), + unparsedEntityDecl: Some(sax_unparsed_entity_decl_cb), + getParameterEntity: Some(sax_get_parameter_entity_cb), + characters: Some(sax_characters_cb), + cdataBlock: Some(sax_characters_cb), + startElement: None, + endElement: None, + processingInstruction: Some(sax_processing_instruction_cb), + startElementNs: Some(sax_start_element_ns_cb), + endElementNs: Some(sax_end_element_ns_cb), + serror: Some(rsvg_sax_serror_cb), + + initialized: XML_SAX2_MAGIC, + } +} + +unsafe extern "C" fn rsvg_sax_serror_cb(user_data: *mut libc::c_void, error: xmlErrorPtr) { + let xml2_parser = &*(user_data as *mut Xml2Parser<'_>); + let error = error.as_ref().unwrap(); + + let level_name = match error.level { + 1 => "warning", + 2 => "error", + 3 => "fatal error", + _ => "unknown error", + }; + + // "int2" is the column number + let column = if error.int2 > 0 { + Cow::Owned(format!(":{}", error.int2)) + } else { + Cow::Borrowed("") + }; + + let full_error_message = format!( + "{} code={} ({}) in {}:{}{}: {}", + level_name, + error.code, + error.domain, + cstr(error.file), + error.line, + column, + cstr(error.message) + ); + xml2_parser + .state + .error(LoadingError::XmlParseError(full_error_message)); +} + +fn free_xml_parser_and_doc(parser: xmlParserCtxtPtr) { + // Free the ctxt and its ctxt->myDoc - libxml2 doesn't free them together + // http://xmlsoft.org/html/libxml-parser.html#xmlFreeParserCtxt + unsafe { + if !parser.is_null() { + let rparser = &mut *parser; + + if !rparser.myDoc.is_null() { + xmlFreeDoc(rparser.myDoc); + rparser.myDoc = ptr::null_mut(); + } + + xmlFreeParserCtxt(parser); + } + } +} + +unsafe extern "C" fn sax_get_entity_cb( + user_data: *mut libc::c_void, + name: *const libc::c_char, +) -> xmlEntityPtr { + let xml2_parser = &*(user_data as *mut Xml2Parser<'_>); + + assert!(!name.is_null()); + let name = utf8_cstr(name); + + xml2_parser + .state + .entity_lookup(name) + .unwrap_or(ptr::null_mut()) +} + +unsafe extern "C" fn sax_entity_decl_cb( + user_data: *mut libc::c_void, + name: *const libc::c_char, + type_: libc::c_int, + _public_id: *const libc::c_char, + _system_id: *const libc::c_char, + content: *const libc::c_char, +) { + let xml2_parser = &*(user_data as *mut Xml2Parser<'_>); + + assert!(!name.is_null()); + + if type_ != XML_INTERNAL_GENERAL_ENTITY { + // We don't allow loading external entities; we don't support + // defining parameter entities in the DTD, and libxml2 should + // handle internal predefined entities by itself (e.g. "&"). + return; + } + + let entity = xmlNewEntity( + ptr::null_mut(), + name, + type_, + ptr::null(), + ptr::null(), + content, + ); + assert!(!entity.is_null()); + + let name = utf8_cstr(name); + xml2_parser.state.entity_insert(name, entity); +} + +unsafe extern "C" fn sax_unparsed_entity_decl_cb( + user_data: *mut libc::c_void, + name: *const libc::c_char, + public_id: *const libc::c_char, + system_id: *const libc::c_char, + _notation_name: *const libc::c_char, +) { + sax_entity_decl_cb( + user_data, + name, + XML_INTERNAL_GENERAL_ENTITY, + public_id, + system_id, + ptr::null(), + ); +} + +fn make_qual_name(prefix: Option<&str>, uri: Option<&str>, localname: &str) -> QualName { + // FIXME: If the element doesn't have a namespace URI, we are falling back + // to the SVG namespace. In reality we need to take namespace scoping into account, + // i.e. handle the "default namespace" active at that point in the XML stack. + let element_ns = uri.map_or_else(|| ns!(svg), Namespace::from); + + QualName::new( + prefix.map(Prefix::from), + element_ns, + LocalName::from(localname), + ) +} + +unsafe extern "C" fn sax_start_element_ns_cb( + user_data: *mut libc::c_void, + localname: *mut libc::c_char, + prefix: *mut libc::c_char, + uri: *mut libc::c_char, + _nb_namespaces: libc::c_int, + _namespaces: *mut *mut libc::c_char, + nb_attributes: libc::c_int, + _nb_defaulted: libc::c_int, + attributes: *mut *mut libc::c_char, +) { + let xml2_parser = &*(user_data as *mut Xml2Parser<'_>); + + assert!(!localname.is_null()); + + let prefix = opt_utf8_cstr(prefix); + let uri = opt_utf8_cstr(uri); + let localname = utf8_cstr(localname); + + let qual_name = make_qual_name(prefix, uri, localname); + + let nb_attributes = nb_attributes as usize; + let attrs = + match Attributes::new_from_xml2_attributes(nb_attributes, attributes as *const *const _) { + Ok(attrs) => attrs, + Err(e) => { + xml2_parser.state.error(e); + let parser = xml2_parser.parser.get(); + xmlStopParser(parser); + return; + } + }; + + // This clippy::let_unit_value is for the "let _: () = e" guard below. + #[allow(clippy::let_unit_value)] + if let Err(e) = xml2_parser.state.start_element(qual_name, attrs) { + let _: () = e; // guard in case we change the error type later + + let parser = xml2_parser.parser.get(); + xmlStopParser(parser); + } +} + +unsafe extern "C" fn sax_end_element_ns_cb( + user_data: *mut libc::c_void, + localname: *mut libc::c_char, + prefix: *mut libc::c_char, + uri: *mut libc::c_char, +) { + let xml2_parser = &*(user_data as *mut Xml2Parser<'_>); + + assert!(!localname.is_null()); + + let prefix = opt_utf8_cstr(prefix); + let uri = opt_utf8_cstr(uri); + let localname = utf8_cstr(localname); + + let qual_name = make_qual_name(prefix, uri, localname); + + xml2_parser.state.end_element(qual_name); +} + +unsafe extern "C" fn sax_characters_cb( + user_data: *mut libc::c_void, + unterminated_text: *const libc::c_char, + len: libc::c_int, +) { + let xml2_parser = &*(user_data as *mut Xml2Parser<'_>); + + assert!(!unterminated_text.is_null()); + assert!(len >= 0); + + // libxml2 already validated the incoming string as UTF-8. Note that + // it is *not* nul-terminated; this is why we create a byte slice first. + let bytes = std::slice::from_raw_parts(c_char_as_u8_ptr(unterminated_text), len as usize); + let utf8 = str::from_utf8_unchecked(bytes); + + xml2_parser.state.characters(utf8); +} + +unsafe extern "C" fn sax_processing_instruction_cb( + user_data: *mut libc::c_void, + target: *const libc::c_char, + data: *const libc::c_char, +) { + let xml2_parser = &*(user_data as *mut Xml2Parser<'_>); + + assert!(!target.is_null()); + let target = utf8_cstr(target); + + let data = if data.is_null() { "" } else { utf8_cstr(data) }; + + xml2_parser.state.processing_instruction(target, data); +} + +unsafe extern "C" fn sax_get_parameter_entity_cb( + user_data: *mut libc::c_void, + name: *const libc::c_char, +) -> xmlEntityPtr { + sax_get_entity_cb(user_data, name) +} + +fn set_xml_parse_options(parser: xmlParserCtxtPtr, unlimited_size: bool) { + let mut options: libc::c_int = XML_PARSE_NONET | XML_PARSE_BIG_LINES; + + if unlimited_size { + options |= XML_PARSE_HUGE; + } + + unsafe { + xmlCtxtUseOptions(parser, options); + + // If false, external entities work, but internal ones don't. if + // true, internal entities work, but external ones don't. favor + // internal entities, in order to not cause a regression + (*parser).replaceEntities = 1; + } +} + +// Struct used as closure data for xmlCreateIOParserCtxt(). In conjunction +// with stream_ctx_read() and stream_ctx_close(), this struct provides the +// I/O callbacks and their context for libxml2. +// +// We call I/O methods on the stream, and as soon as we get an error +// we store it in the gio_error field. Libxml2 just allows us to +// return -1 from the I/O callbacks in that case; it doesn't actually +// see the error code. +// +// The gio_error field comes from the place that constructs the +// StreamCtx. That place is later responsible for seeing if the error +// is set; if it is, it means that there was an I/O error. Otherwise, +// there were no I/O errors but the caller must then ask libxml2 for +// XML parsing errors. +struct StreamCtx { + stream: gio::InputStream, + cancellable: Option<gio::Cancellable>, + gio_error: Rc<RefCell<Option<glib::Error>>>, +} + +// read() callback from xmlCreateIOParserCtxt() +unsafe extern "C" fn stream_ctx_read( + context: *mut libc::c_void, + buffer: *mut libc::c_char, + len: libc::c_int, +) -> libc::c_int { + let ctx = &mut *(context as *mut StreamCtx); + + let mut err_ref = ctx.gio_error.borrow_mut(); + + // has the error been set already? + if err_ref.is_some() { + return -1; + } + + let buf: &mut [u8] = slice::from_raw_parts_mut(c_char_as_u8_ptr_mut(buffer), len as usize); + + match ctx.stream.read(buf, ctx.cancellable.as_ref()) { + Ok(size) => size as libc::c_int, + + Err(e) => { + // Just store the first I/O error we get; ignore subsequent ones. + *err_ref = Some(e); + -1 + } + } +} + +// close() callback from xmlCreateIOParserCtxt() +unsafe extern "C" fn stream_ctx_close(context: *mut libc::c_void) -> libc::c_int { + let ctx = &mut *(context as *mut StreamCtx); + + let ret = match ctx.stream.close(ctx.cancellable.as_ref()) { + Ok(()) => 0, + + Err(e) => { + let mut err_ref = ctx.gio_error.borrow_mut(); + + // don't overwrite a previous error + if err_ref.is_none() { + *err_ref = Some(e); + } + + -1 + } + }; + + drop(Box::from_raw(ctx)); + + ret +} + +fn init_libxml2() { + static ONCE: Once = Once::new(); + + ONCE.call_once(|| unsafe { + xmlInitParser(); + }); +} + +pub struct Xml2Parser<'a> { + parser: Cell<xmlParserCtxtPtr>, + state: &'a XmlState, + gio_error: Rc<RefCell<Option<glib::Error>>>, +} + +impl<'a> Xml2Parser<'a> { + pub fn from_stream( + state: &'a XmlState, + unlimited_size: bool, + stream: &gio::InputStream, + cancellable: Option<&gio::Cancellable>, + ) -> Result<Box<Xml2Parser<'a>>, LoadingError> { + init_libxml2(); + + // The Xml2Parser we end up creating, if + // xmlCreateIOParserCtxt() is successful, needs to hold a + // location to place a GError from within the I/O callbacks + // stream_ctx_read() and stream_ctx_close(). We put this + // location in an Rc so that it can outlive the call to + // xmlCreateIOParserCtxt() in case that fails, since on + // failure that function frees the StreamCtx. + let gio_error = Rc::new(RefCell::new(None)); + + let ctx = Box::new(StreamCtx { + stream: stream.clone(), + cancellable: cancellable.cloned(), + gio_error: gio_error.clone(), + }); + + let mut sax_handler = get_xml2_sax_handler(); + + let mut xml2_parser = Box::new(Xml2Parser { + parser: Cell::new(ptr::null_mut()), + state, + gio_error, + }); + + unsafe { + let xml2_parser_ptr: *mut Xml2Parser<'a> = xml2_parser.as_mut(); + let parser = xmlCreateIOParserCtxt( + &mut sax_handler, + xml2_parser_ptr as *mut _, + Some(stream_ctx_read), + Some(stream_ctx_close), + Box::into_raw(ctx) as *mut _, + XML_CHAR_ENCODING_NONE, + ); + + if parser.is_null() { + // on error, xmlCreateIOParserCtxt() frees our ctx via the + // stream_ctx_close function + Err(LoadingError::OutOfMemory(String::from( + "could not create XML parser", + ))) + } else { + xml2_parser.parser.set(parser); + + set_xml_parse_options(parser, unlimited_size); + + Ok(xml2_parser) + } + } + } + + pub fn parse(&self) -> Result<(), LoadingError> { + unsafe { + let parser = self.parser.get(); + + let xml_parse_success = xmlParseDocument(parser) == 0; + + let mut err_ref = self.gio_error.borrow_mut(); + + let io_error = err_ref.take(); + + if let Some(io_error) = io_error { + Err(LoadingError::from(io_error)) + } else if !xml_parse_success { + let xerr = xmlCtxtGetLastError(parser as *mut _); + let msg = xml2_error_to_string(xerr); + Err(LoadingError::XmlParseError(msg)) + } else { + Ok(()) + } + } + } +} + +impl<'a> Drop for Xml2Parser<'a> { + fn drop(&mut self) { + let parser = self.parser.get(); + free_xml_parser_and_doc(parser); + self.parser.set(ptr::null_mut()); + } +} + +fn xml2_error_to_string(xerr: xmlErrorPtr) -> String { + unsafe { + if !xerr.is_null() { + let xerr = &*xerr; + + let file = if xerr.file.is_null() { + "data".to_string() + } else { + from_glib_none(xerr.file) + }; + + let message = if xerr.message.is_null() { + "-".to_string() + } else { + from_glib_none(xerr.message) + }; + + format!( + "Error domain {} code {} on line {} column {} of {}: {}", + xerr.domain, xerr.code, xerr.line, xerr.int2, file, message + ) + } else { + // The error is not set? Return a generic message :( + "Error parsing XML data".to_string() + } + } +} |