summaryrefslogtreecommitdiff
path: root/rsvg/src
diff options
context:
space:
mode:
Diffstat (limited to 'rsvg/src')
-rw-r--r--rsvg/src/accept_language.rs458
-rw-r--r--rsvg/src/angle.rs187
-rw-r--r--rsvg/src/api.rs638
-rw-r--r--rsvg/src/aspect_ratio.rs456
-rw-r--r--rsvg/src/bbox.rs121
-rw-r--r--rsvg/src/color.rs27
-rw-r--r--rsvg/src/cond.rs243
-rw-r--r--rsvg/src/coord_units.rs99
-rw-r--r--rsvg/src/css.rs1110
-rw-r--r--rsvg/src/dasharray.rs114
-rw-r--r--rsvg/src/document.rs667
-rw-r--r--rsvg/src/dpi.rs13
-rw-r--r--rsvg/src/drawing_ctx.rs2412
-rw-r--r--rsvg/src/element.rs651
-rw-r--r--rsvg/src/error.rs526
-rw-r--r--rsvg/src/filter.rs332
-rw-r--r--rsvg/src/filter_func.rs959
-rw-r--r--rsvg/src/filters/blend.rs178
-rw-r--r--rsvg/src/filters/bounds.rs121
-rw-r--r--rsvg/src/filters/color_matrix.rs342
-rw-r--r--rsvg/src/filters/component_transfer.rs458
-rw-r--r--rsvg/src/filters/composite.rs179
-rw-r--r--rsvg/src/filters/context.rs405
-rw-r--r--rsvg/src/filters/convolve_matrix.rs354
-rw-r--r--rsvg/src/filters/displacement_map.rs195
-rw-r--r--rsvg/src/filters/drop_shadow.rs88
-rw-r--r--rsvg/src/filters/error.rs78
-rw-r--r--rsvg/src/filters/flood.rs70
-rw-r--r--rsvg/src/filters/gaussian_blur.rs282
-rw-r--r--rsvg/src/filters/image.rs211
-rw-r--r--rsvg/src/filters/lighting.rs1090
-rw-r--r--rsvg/src/filters/merge.rs217
-rw-r--r--rsvg/src/filters/mod.rs381
-rw-r--r--rsvg/src/filters/morphology.rs200
-rw-r--r--rsvg/src/filters/offset.rs100
-rw-r--r--rsvg/src/filters/tile.rs109
-rw-r--r--rsvg/src/filters/turbulence.rs484
-rw-r--r--rsvg/src/float_eq_cairo.rs154
-rw-r--r--rsvg/src/font_props.rs878
-rw-r--r--rsvg/src/gradient.rs748
-rw-r--r--rsvg/src/handle.rs388
-rw-r--r--rsvg/src/href.rs50
-rw-r--r--rsvg/src/image.rs119
-rw-r--r--rsvg/src/io.rs122
-rw-r--r--rsvg/src/iri.rs90
-rw-r--r--rsvg/src/layout.rs387
-rw-r--r--rsvg/src/length.rs736
-rw-r--r--rsvg/src/lib.rs261
-rw-r--r--rsvg/src/limits.rs52
-rw-r--r--rsvg/src/log.rs75
-rw-r--r--rsvg/src/marker.rs1215
-rw-r--r--rsvg/src/node.rs377
-rw-r--r--rsvg/src/paint_server.rs413
-rw-r--r--rsvg/src/parsers.rs424
-rw-r--r--rsvg/src/path_builder.rs875
-rw-r--r--rsvg/src/path_parser.rs2223
-rw-r--r--rsvg/src/pattern.rs525
-rw-r--r--rsvg/src/properties.rs1131
-rw-r--r--rsvg/src/property_defs.rs1328
-rw-r--r--rsvg/src/property_macros.rs288
-rw-r--r--rsvg/src/rect.rs274
-rw-r--r--rsvg/src/session.rs44
-rw-r--r--rsvg/src/shapes.rs743
-rw-r--r--rsvg/src/space.rs184
-rw-r--r--rsvg/src/structure.rs632
-rw-r--r--rsvg/src/style.rs84
-rw-r--r--rsvg/src/surface_utils/iterators.rs247
-rw-r--r--rsvg/src/surface_utils/mod.rs338
-rw-r--r--rsvg/src/surface_utils/shared_surface.rs1477
-rw-r--r--rsvg/src/surface_utils/srgb.rs95
-rw-r--r--rsvg/src/test_utils/compare_surfaces.rs112
-rw-r--r--rsvg/src/test_utils/mod.rs119
-rw-r--r--rsvg/src/test_utils/reference_utils.rs309
-rw-r--r--rsvg/src/text.rs1456
-rw-r--r--rsvg/src/transform.rs1127
-rw-r--r--rsvg/src/ua.css41
-rw-r--r--rsvg/src/unit_interval.rs94
-rw-r--r--rsvg/src/url_resolver.rs256
-rw-r--r--rsvg/src/util.rs75
-rw-r--r--rsvg/src/viewbox.rs87
-rw-r--r--rsvg/src/xml/attributes.rs257
-rw-r--r--rsvg/src/xml/mod.rs787
-rw-r--r--rsvg/src/xml/xml2.rs212
-rw-r--r--rsvg/src/xml/xml2_load.rs517
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(&params)
+ };
+
+ 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(&params));
+ }
+ }
+
+ 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(&params, 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(&params_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!(
+ &params.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(&params),
+ y1: y1.to_user(&params),
+ x2: x2.to_user(&params),
+ y2: y2.to_user(&params),
+ },
+
+ ResolvedGradientVariant::Radial {
+ cx,
+ cy,
+ r,
+ fx,
+ fy,
+ fr,
+ } => GradientVariant::Radial {
+ cx: cx.to_user(&params),
+ cy: cy.to_user(&params),
+ r: r.to_user(&params),
+ fx: fx.to_user(&params),
+ fy: fy.to_user(&params),
+ fr: fr.to_user(&params),
+ },
+ };
+
+ 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(&params), height.to_user(&params))
+ }
+
+ 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(&params);
+ let y = values.y().0.to_user(&params);
+
+ let w = match values.width().0 {
+ LengthOrAuto::Length(l) => l.to_user(&params),
+ LengthOrAuto::Auto => surface.width() as f64,
+ };
+ let h = match values.height().0 {
+ LengthOrAuto::Length(l) => l.to_user(&params),
+ 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(&params),
+ 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(&params),
+ 400.0
+ );
+ assert_approx_eq_cairo!(
+ Length::<Vertical>::new(10.0, LengthUnit::In).to_user(&params),
+ 500.0
+ );
+
+ assert_approx_eq_cairo!(
+ Length::<Horizontal>::new(10.0, LengthUnit::Cm).to_user(&params),
+ 400.0 / CM_PER_INCH
+ );
+ assert_approx_eq_cairo!(
+ Length::<Horizontal>::new(10.0, LengthUnit::Mm).to_user(&params),
+ 400.0 / MM_PER_INCH
+ );
+ assert_approx_eq_cairo!(
+ Length::<Horizontal>::new(10.0, LengthUnit::Pt).to_user(&params),
+ 400.0 / POINTS_PER_INCH
+ );
+ assert_approx_eq_cairo!(
+ Length::<Horizontal>::new(10.0, LengthUnit::Pc).to_user(&params),
+ 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(&params),
+ 5.0
+ );
+ assert_approx_eq_cairo!(
+ Length::<Vertical>::new(0.05, LengthUnit::Percent).to_user(&params),
+ 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(&params),
+ 12.0
+ );
+
+ assert_approx_eq_cairo!(
+ Length::<Vertical>::new(1.0, LengthUnit::Ex).to_user(&params),
+ 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(&params),
+ 2.0 * 72.0
+ );
+ assert_approx_eq_cairo!(
+ Length::<Vertical>::new(192.0, LengthUnit::Px).to_points(&params),
+ 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(&params);
+ let marker_height = self.height.to_user(&params);
+
+ 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(&params);
+
+ // 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(&params, values);
+
+ let is_visible = values.is_visible();
+ let paint_order = values.paint_order();
+
+ let stroke = Stroke::new(values, &params);
+
+ 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(&params, 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(&params);
+
+ 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(&copy)?;
+ 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, &params);
+
+ 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, &params);
+
+ 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, &params);
+
+ 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(&params);
+ let dy = self.dy.to_user(&params);
+
+ 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(&params);
+ let mut y = self.y.to_user(&params);
+
+ 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(&params));
+ let y = self.y.map(|l| l.to_user(&params));
+
+ let span_dx = dx + self.dx.to_user(&params);
+ let span_dy = dy + self.dy.to_user(&params);
+
+ 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. "&amp;").
+ 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()
+ }
+ }
+}