[librsvg: 6/7] transform: implement our own Transform type



commit 6b8b5c2df21091c83188fc2e1770297e954e5e25
Author: Paolo Borelli <pborelli gnome org>
Date:   Fri Jan 17 16:14:41 2020 +0100

    transform: implement our own Transform type
    
    Use our own Transform type, rather than cairo::Matrix.
    Beside paving the way to other rendering backends, using our own
    type has the benefit of not having to cross the ffi boundary for
    every transformation and also allows us to have a nicer api.
    The code matches the conventions of cairo::Matrix (eg the
    internal representation), but for the API I also checked
    euclid::Transform2D and resvg's Transform type, and where possible
    makes things more clear and ergonomic (explicit pre_ and post_
    variants for each method, chainable methods).
    
    Note: the filters-composite-02-b reftest differs by one pixel,
    I am assuming this is a rounding issue and hence I am updating
    the reference image.

 rsvg_internals/src/aspect_ratio.rs                 |  16 +-
 rsvg_internals/src/bbox.rs                         |  22 +-
 rsvg_internals/src/drawing_ctx.rs                  | 109 +++--
 rsvg_internals/src/filter.rs                       |   3 +-
 rsvg_internals/src/filters/context.rs              |  50 +--
 rsvg_internals/src/filters/mod.rs                  |   2 +-
 rsvg_internals/src/filters/turbulence.rs           |   3 +-
 rsvg_internals/src/gradient.rs                     |  47 +-
 rsvg_internals/src/node.rs                         |   9 +-
 rsvg_internals/src/pattern.rs                      |  63 +--
 rsvg_internals/src/rect.rs                         |  46 --
 rsvg_internals/src/text.rs                         |   3 +-
 rsvg_internals/src/transform.rs                    | 485 +++++++++++++++------
 .../reftests/svg1.1/filters-composite-02-b-ref.png | Bin 15917 -> 15922 bytes
 14 files changed, 512 insertions(+), 346 deletions(-)
---
diff --git a/rsvg_internals/src/aspect_ratio.rs b/rsvg_internals/src/aspect_ratio.rs
index c8d441ef..a8c9a82c 100644
--- a/rsvg_internals/src/aspect_ratio.rs
+++ b/rsvg_internals/src/aspect_ratio.rs
@@ -25,6 +25,7 @@ use std::ops::Deref;
 use crate::error::*;
 use crate::parsers::Parse;
 use crate::rect::Rect;
+use crate::transform::Transform;
 use crate::viewbox::ViewBox;
 use cssparser::{BasicParseError, Parser};
 
@@ -155,7 +156,7 @@ impl AspectRatio {
         &self,
         vbox: Option<ViewBox>,
         viewport: &Rect,
-    ) -> Option<cairo::Matrix> {
+    ) -> Option<Transform> {
         // 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
@@ -175,16 +176,13 @@ impl AspectRatio {
                 None
             } else {
                 let r = self.compute(&vbox, viewport);
-                let mut matrix = cairo::Matrix::identity();
-                matrix.translate(r.x0, r.y0);
-                matrix.scale(r.width() / vbox.0.width(), r.height() / vbox.0.height());
-                matrix.translate(-vbox.0.x0, -vbox.0.y0);
-                Some(matrix)
+                let t = Transform::new_translate(r.x0, r.y0)
+                    .pre_scale(r.width() / vbox.0.width(), r.height() / vbox.0.height())
+                    .pre_translate(-vbox.0.x0, -vbox.0.y0);
+                Some(t)
             }
         } else {
-            let mut matrix = cairo::Matrix::identity();
-            matrix.translate(viewport.x0, viewport.y0);
-            Some(matrix)
+            Some(Transform::new_translate(viewport.x0, viewport.y0))
         }
     }
 }
diff --git a/rsvg_internals/src/bbox.rs b/rsvg_internals/src/bbox.rs
index d7a918d7..39113a5b 100644
--- a/rsvg_internals/src/bbox.rs
+++ b/rsvg_internals/src/bbox.rs
@@ -1,10 +1,11 @@
 //! Bounding boxes that know their coordinate space.
 
-use crate::rect::{Rect, TransformRect};
+use crate::rect::Rect;
+use crate::transform::Transform;
 
 #[derive(Debug, Copy, Clone)]
 pub struct BoundingBox {
-    pub transform: cairo::Matrix,
+    pub transform: Transform,
     pub rect: Option<Rect>,     // without stroke
     pub ink_rect: Option<Rect>, // with stroke
 }
@@ -12,13 +13,13 @@ pub struct BoundingBox {
 impl BoundingBox {
     pub fn new() -> BoundingBox {
         BoundingBox {
-            transform: cairo::Matrix::identity(),
+            transform: Default::default(),
             rect: None,
             ink_rect: None,
         }
     }
 
-    pub fn with_transform(self, transform: cairo::Matrix) -> BoundingBox {
+    pub fn with_transform(self, transform: Transform) -> BoundingBox {
         BoundingBox { transform, ..self }
     }
 
@@ -46,11 +47,12 @@ impl BoundingBox {
             return;
         }
 
-        let mut transform = self.transform;
-
         // this will panic!() if it's not invertible... should we check on our own?
-        transform.invert();
-        transform = cairo::Matrix::multiply(&src.transform, &transform);
+        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);
@@ -68,7 +70,7 @@ impl BoundingBox {
 fn combine_rects(
     r1: Option<Rect>,
     r2: Option<Rect>,
-    transform: &cairo::Matrix,
+    transform: &Transform,
     clip: bool,
 ) -> Option<Rect> {
     match (r1, r2, clip) {
@@ -91,7 +93,7 @@ mod tests {
         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 = cairo::Matrix::new(1.0, 0.0, 0.0, 1.0, 0.5, 0.5);
+        let t = Transform::new(1.0, 0.0, 0.0, 1.0, 0.5, 0.5);
 
         let res = combine_rects(None, None, &t, true);
         assert_eq!(res, None);
diff --git a/rsvg_internals/src/drawing_ctx.rs b/rsvg_internals/src/drawing_ctx.rs
index 30387355..64cfbbb8 100644
--- a/rsvg_internals/src/drawing_ctx.rs
+++ b/rsvg_internals/src/drawing_ctx.rs
@@ -28,13 +28,14 @@ use crate::property_defs::{
     ClipRule, FillRule, Opacity, Overflow, ShapeRendering, StrokeDasharray, StrokeLinecap,
     StrokeLinejoin,
 };
-use crate::rect::{Rect, TransformRect};
+use crate::rect::Rect;
 use crate::shapes::Markers;
 use crate::structure::{ClipPath, Mask, Symbol, Use};
 use crate::surface_utils::{
     shared_surface::ExclusiveImageSurface, shared_surface::SharedImageSurface,
     shared_surface::SurfaceType,
 };
+use crate::transform::Transform;
 use crate::unit_interval::UnitInterval;
 use crate::viewbox::ViewBox;
 
@@ -87,7 +88,7 @@ pub enum ClipMode {
 pub struct DrawingCtx {
     document: Rc<Document>,
 
-    initial_affine: cairo::Matrix,
+    initial_transform: Transform,
 
     rect: Rect,
     dpi: Dpi,
@@ -115,7 +116,7 @@ impl DrawingCtx {
         measuring: bool,
         testing: bool,
     ) -> DrawingCtx {
-        let initial_affine = cr.get_matrix();
+        let initial_transform = Transform::from(cr.get_matrix());
 
         // This is more or less a hack to make measuring geometries possible,
         // while the code gets refactored not to need special cases for that.
@@ -148,7 +149,7 @@ impl DrawingCtx {
 
         let mut draw_ctx = DrawingCtx {
             document,
-            initial_affine,
+            initial_transform,
             rect,
             dpi,
             cr_stack: Vec::new(),
@@ -185,8 +186,8 @@ impl DrawingCtx {
         self.cr.clone()
     }
 
-    pub fn get_transform(&self) -> cairo::Matrix {
-        self.cr.get_matrix()
+    pub fn get_transform(&self) -> Transform {
+        Transform::from(self.cr.get_matrix())
     }
 
     pub fn empty_bbox(&self) -> BoundingBox {
@@ -217,7 +218,7 @@ impl DrawingCtx {
         let (viewport_width, viewport_height) = (self.rect.width(), self.rect.height());
 
         let (width, height) = self
-            .initial_affine
+            .initial_transform
             .transform_distance(viewport_width, viewport_height);
 
         // We need a size in whole pixels, so use ceil() to ensure the whole viewport fits
@@ -302,7 +303,7 @@ impl DrawingCtx {
         preserve_aspect_ratio
             .viewport_to_viewbox_transform(vbox, &viewport)
             .and_then(|t| {
-                self.cr.transform(t);
+                self.cr.transform(t.into());
 
                 if let Some(vbox) = vbox {
                     if let Some(ClipMode::ClipToVbox) = clip_mode {
@@ -340,10 +341,10 @@ impl DrawingCtx {
 
             let cascaded = CascadedValues::new_from_node(node);
 
-            let matrix = if units == CoordUnits::ObjectBoundingBox {
+            let transform = if units == CoordUnits::ObjectBoundingBox {
                 let bbox_rect = bbox.rect.as_ref().unwrap();
 
-                Some(cairo::Matrix::new(
+                Some(Transform::new(
                     bbox_rect.width(),
                     0.0,
                     0.0,
@@ -355,7 +356,7 @@ impl DrawingCtx {
                 None
             };
 
-            self.with_saved_transform(matrix, &mut |dc| {
+            self.with_saved_transform(transform, &mut |dc| {
                 let cr = dc.get_cairo_context();
 
                 // here we don't push a layer because we are clipping
@@ -379,7 +380,7 @@ impl DrawingCtx {
         &mut self,
         mask: &Mask,
         mask_node: &RsvgNode,
-        affine: cairo::Matrix,
+        transform: Transform,
         bbox: &BoundingBox,
     ) -> Result<Option<cairo::ImageSurface>, RenderingError> {
         if bbox.rect.is_none() {
@@ -407,7 +408,10 @@ impl DrawingCtx {
             mask.get_rect(&values, &params)
         };
 
-        let mask_affine = cairo::Matrix::multiply(&mask_node.borrow().get_transform(), &affine);
+        let mask_transform = mask_node
+            .borrow()
+            .get_transform()
+            .post_transform(&transform);
 
         let mask_content_surface = self.create_surface_for_toplevel_viewport()?;
 
@@ -415,9 +419,9 @@ impl DrawingCtx {
         // reference to the surface before we access the pixels
         {
             let mask_cr = cairo::Context::new(&mask_content_surface);
-            mask_cr.set_matrix(mask_affine);
+            mask_cr.set_matrix(mask_transform.into());
 
-            let bbtransform = cairo::Matrix::new(bb_w, 0.0, 0.0, bb_h, bb_x, bb_y);
+            let bbtransform = Transform::new(bb_w, 0.0, 0.0, bb_h, bb_x, bb_y);
 
             self.push_cairo_context(mask_cr);
 
@@ -428,7 +432,7 @@ impl DrawingCtx {
             }
 
             let _params = if mask.get_content_units() == CoordUnits::ObjectBoundingBox {
-                self.get_cairo_context().transform(bbtransform);
+                self.get_cairo_context().transform(bbtransform.into());
                 self.push_view_box(1.0, 1.0)
             } else {
                 self.get_view_params()
@@ -493,7 +497,7 @@ impl DrawingCtx {
 
                     let affines = CompositingAffines::new(
                         affine_at_start,
-                        dc.initial_affine_with_offset(),
+                        dc.initial_transform_with_offset(),
                         dc.cr_stack.len(),
                     );
 
@@ -507,7 +511,7 @@ impl DrawingCtx {
                         )
                     };
 
-                    cr.set_matrix(affines.for_temporary_surface);
+                    cr.set_matrix(affines.for_temporary_surface.into());
 
                     dc.push_cairo_context(cr);
 
@@ -545,12 +549,12 @@ impl DrawingCtx {
 
                     // Set temporary surface as source
 
-                    dc.cr.set_matrix(affines.compositing);
+                    dc.cr.set_matrix(affines.compositing.into());
                     dc.cr.set_source_surface(&source_surface, 0.0, 0.0);
 
                     // Clip
 
-                    dc.cr.set_matrix(affines.outside_temporary_surface);
+                    dc.cr.set_matrix(affines.outside_temporary_surface.into());
                     let _: () = dc.clip_to_node(&clip_in_object_space, &bbox)?;
 
                     // Mask
@@ -568,7 +572,7 @@ impl DrawingCtx {
                                 )
                                 .and_then(|mask_surf| {
                                     if let Some(surf) = mask_surf {
-                                        dc.cr.set_matrix(affines.compositing);
+                                        dc.cr.set_matrix(affines.compositing.into());
                                         dc.cr.mask_surface(&surf, 0.0, 0.0);
                                     }
                                     Ok(())
@@ -585,7 +589,7 @@ impl DrawingCtx {
                     } else {
                         // No mask, so composite the temporary surface
 
-                        dc.cr.set_matrix(affines.compositing);
+                        dc.cr.set_matrix(affines.compositing.into());
 
                         if opacity < 1.0 {
                             dc.cr.paint_with_alpha(opacity);
@@ -594,7 +598,7 @@ impl DrawingCtx {
                         }
                     }
 
-                    dc.cr.set_matrix(affine_at_start);
+                    dc.cr.set_matrix(affine_at_start.into());
 
                     res
                 } else {
@@ -604,10 +608,9 @@ impl DrawingCtx {
         }
     }
 
-    fn initial_affine_with_offset(&self) -> cairo::Matrix {
-        let mut initial_with_offset = self.initial_affine;
-        initial_with_offset.translate(self.rect.x0, self.rect.y0);
-        initial_with_offset
+    fn initial_transform_with_offset(&self) -> Transform {
+        self.initial_transform
+            .pre_translate(self.rect.x0, self.rect.y0)
     }
 
     /// Saves the current transform, applies a new transform if specified,
@@ -619,18 +622,18 @@ impl DrawingCtx {
     /// was set by the `draw_fn`.
     pub fn with_saved_transform(
         &mut self,
-        transform: Option<cairo::Matrix>,
+        transform: Option<Transform>,
         draw_fn: &mut dyn FnMut(&mut DrawingCtx) -> Result<BoundingBox, RenderingError>,
     ) -> Result<BoundingBox, RenderingError> {
         let orig_transform = self.get_transform();
 
         if let Some(t) = transform {
-            self.cr.transform(t);
+            self.cr.transform(t.into());
         }
 
         let res = draw_fn(self);
 
-        self.cr.set_matrix(orig_transform);
+        self.cr.set_matrix(orig_transform.into());
 
         if let Ok(bbox) = res {
             let mut res_bbox = BoundingBox::new().with_transform(orig_transform);
@@ -935,12 +938,12 @@ impl DrawingCtx {
         surface.draw(&mut |cr| {
             for (depth, draw) in self.cr_stack.iter().enumerate() {
                 let affines = CompositingAffines::new(
-                    draw.get_matrix(),
-                    self.initial_affine_with_offset(),
+                    Transform::from(draw.get_matrix()),
+                    self.initial_transform_with_offset(),
                     depth,
                 );
 
-                cr.set_matrix(affines.for_snapshot);
+                cr.set_matrix(affines.for_snapshot.into());
                 cr.set_source_surface(&draw.get_target(), 0.0, 0.0);
                 cr.paint();
             }
@@ -961,7 +964,7 @@ impl DrawingCtx {
         &mut self,
         node: &RsvgNode,
         cascaded: &CascadedValues<'_>,
-        affine: cairo::Matrix,
+        affine: Transform,
         width: i32,
         height: i32,
     ) -> Result<SharedImageSurface, RenderingError> {
@@ -972,7 +975,7 @@ impl DrawingCtx {
 
         {
             let cr = cairo::Context::new(&surface);
-            cr.set_matrix(affine);
+            cr.set_matrix(affine.into());
 
             self.cr = cr;
 
@@ -1120,48 +1123,42 @@ impl DrawingCtx {
 
 #[derive(Debug)]
 struct CompositingAffines {
-    pub outside_temporary_surface: cairo::Matrix,
-    pub initial: cairo::Matrix,
-    pub for_temporary_surface: cairo::Matrix,
-    pub compositing: cairo::Matrix,
-    pub for_snapshot: cairo::Matrix,
+    pub outside_temporary_surface: Transform,
+    pub initial: Transform,
+    pub for_temporary_surface: Transform,
+    pub compositing: Transform,
+    pub for_snapshot: Transform,
 }
 
 impl CompositingAffines {
-    fn new(
-        current: cairo::Matrix,
-        initial: cairo::Matrix,
-        cr_stack_depth: usize,
-    ) -> 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.try_invert().unwrap();
+        let initial_inverse = initial.invert().unwrap();
 
         let outside_temporary_surface = if is_topmost_temporary_surface {
             current
         } else {
-            cairo::Matrix::multiply(&current, &initial_inverse)
+            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 {
-            let untransformed = cairo::Matrix::multiply(&current, &initial_inverse);
-            let scale = cairo::Matrix::new(scale_x, 0.0, 0.0, scale_y, 0.0, 0.0);
-            cairo::Matrix::multiply(&untransformed, &scale)
+            current
+                .post_transform(&initial_inverse)
+                .post_scale(scale_x, scale_y)
         } else {
             current
         };
 
         let compositing = if is_topmost_temporary_surface {
-            let mut scaled = initial;
-            scaled.scale(1.0 / scale_x, 1.0 / scale_y);
-            scaled
+            initial.pre_scale(1.0 / scale_x, 1.0 / scale_y)
         } else {
-            cairo::Matrix::identity()
+            Transform::identity()
         };
 
-        let for_snapshot = compositing.try_invert().unwrap();
+        let for_snapshot = compositing.invert().unwrap();
 
         CompositingAffines {
             outside_temporary_surface,
@@ -1208,7 +1205,7 @@ fn acquire_paint_server(
 }
 
 fn compute_stroke_and_fill_box(cr: &cairo::Context, values: &ComputedValues) -> BoundingBox {
-    let affine = cr.get_matrix();
+    let affine = Transform::from(cr.get_matrix());
 
     let mut bbox = BoundingBox::new().with_transform(affine);
 
diff --git a/rsvg_internals/src/filter.rs b/rsvg_internals/src/filter.rs
index 05196da9..10c692b0 100644
--- a/rsvg_internals/src/filter.rs
+++ b/rsvg_internals/src/filter.rs
@@ -12,6 +12,7 @@ use crate::parsers::{Parse, ParseValue};
 use crate::properties::ComputedValues;
 use crate::property_bag::PropertyBag;
 use crate::rect::Rect;
+use crate::transform::Transform;
 
 /// The <filter> node.
 pub struct Filter {
@@ -51,7 +52,7 @@ impl Filter {
         &self,
         computed_from_target_node: &ComputedValues,
         draw_ctx: &mut DrawingCtx,
-        transform: cairo::Matrix,
+        transform: Transform,
         width: f64,
         height: f64,
     ) -> BoundingBox {
diff --git a/rsvg_internals/src/filters/context.rs b/rsvg_internals/src/filters/context.rs
index 359291ed..e4d31ced 100644
--- a/rsvg_internals/src/filters/context.rs
+++ b/rsvg_internals/src/filters/context.rs
@@ -13,6 +13,7 @@ use crate::rect::IRect;
 use crate::surface_utils::shared_surface::{
     ExclusiveImageSurface, SharedImageSurface, SurfaceType,
 };
+use crate::transform::Transform;
 use crate::unit_interval::UnitInterval;
 
 use super::error::FilterError;
@@ -86,12 +87,12 @@ pub struct FilterContext {
     /// 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: cairo::Matrix,
+    _affine: Transform,
 
     /// The filter primitive affine matrix.
     ///
     /// See the comments for `_affine`, they largely apply here.
-    paffine: cairo::Matrix,
+    paffine: Transform,
 }
 
 impl FilterContext {
@@ -114,36 +115,31 @@ impl FilterContext {
 
         let affine = match filter.get_filter_units() {
             CoordUnits::UserSpaceOnUse => draw_transform,
-            CoordUnits::ObjectBoundingBox => {
-                let affine = cairo::Matrix::new(
-                    bbox_rect.width(),
-                    0.0,
-                    0.0,
-                    bbox_rect.height(),
-                    bbox_rect.x0,
-                    bbox_rect.y0,
-                );
-                cairo::Matrix::multiply(&affine, &draw_transform)
-            }
+            CoordUnits::ObjectBoundingBox => Transform::new(
+                bbox_rect.width(),
+                0.0,
+                0.0,
+                bbox_rect.height(),
+                bbox_rect.x0,
+                bbox_rect.y0,
+            )
+            .post_transform(&draw_transform),
         };
 
         let paffine = match filter.get_primitive_units() {
             CoordUnits::UserSpaceOnUse => draw_transform,
-            CoordUnits::ObjectBoundingBox => {
-                let affine = cairo::Matrix::new(
-                    bbox_rect.width(),
-                    0.0,
-                    0.0,
-                    bbox_rect.height(),
-                    bbox_rect.x0,
-                    bbox_rect.y0,
-                );
-                cairo::Matrix::multiply(&affine, &draw_transform)
-            }
+            CoordUnits::ObjectBoundingBox => Transform::new(
+                bbox_rect.width(),
+                0.0,
+                0.0,
+                bbox_rect.height(),
+                bbox_rect.x0,
+                bbox_rect.y0,
+            )
+            .post_transform(&draw_transform),
         };
 
-        let width = source_surface.width();
-        let height = source_surface.height();
+        let (width, height) = (source_surface.width(), source_surface.height());
 
         Self {
             node: filter_node.clone(),
@@ -243,7 +239,7 @@ impl FilterContext {
 
     /// Returns the paffine matrix.
     #[inline]
-    pub fn paffine(&self) -> cairo::Matrix {
+    pub fn paffine(&self) -> Transform {
         self.paffine
     }
 
diff --git a/rsvg_internals/src/filters/mod.rs b/rsvg_internals/src/filters/mod.rs
index b627fb27..07e4c5bd 100644
--- a/rsvg_internals/src/filters/mod.rs
+++ b/rsvg_internals/src/filters/mod.rs
@@ -257,7 +257,7 @@ pub fn render(
 
     // If paffine is non-invertible, we won't draw anything. Also bbox combining in bounds
     // computations will panic due to non-invertible martrix.
-    if filter_ctx.paffine().try_invert().is_err() {
+    if !filter_ctx.paffine().is_invertible() {
         return Ok(filter_ctx.into_output()?);
     }
 
diff --git a/rsvg_internals/src/filters/turbulence.rs b/rsvg_internals/src/filters/turbulence.rs
index b8046a22..b3828c5c 100644
--- a/rsvg_internals/src/filters/turbulence.rs
+++ b/rsvg_internals/src/filters/turbulence.rs
@@ -339,8 +339,7 @@ impl FilterEffect for FeTurbulence {
     ) -> Result<FilterResult, FilterError> {
         let bounds = self.base.get_bounds(ctx).into_irect(draw_ctx);
 
-        let mut affine = ctx.paffine();
-        affine.invert();
+        let affine = ctx.paffine().invert().unwrap();
 
         let noise_generator = NoiseGenerator::new(
             self.seed,
diff --git a/rsvg_internals/src/gradient.rs b/rsvg_internals/src/gradient.rs
index 89bfbf9e..782c7c5e 100644
--- a/rsvg_internals/src/gradient.rs
+++ b/rsvg_internals/src/gradient.rs
@@ -16,6 +16,7 @@ use crate::parsers::{Parse, ParseValue};
 use crate::properties::ComputedValues;
 use crate::property_bag::PropertyBag;
 use crate::property_defs::StopColor;
+use crate::transform::Transform;
 use crate::unit_interval::UnitInterval;
 
 /// Contents of a <stop> element for gradient color stops
@@ -317,7 +318,7 @@ impl Variant {
 #[derive(Default)]
 struct Common {
     units: Option<GradientUnits>,
-    affine: Option<cairo::Matrix>,
+    transform: Option<Transform>,
     spread: Option<SpreadMethod>,
 
     fallback: Option<Fragment>,
@@ -354,7 +355,7 @@ pub struct RadialGradient {
 /// field was specified.
 struct UnresolvedGradient {
     units: Option<GradientUnits>,
-    affine: Option<cairo::Matrix>,
+    transform: Option<Transform>,
     spread: Option<SpreadMethod>,
     stops: Option<Vec<ColorStop>>,
 
@@ -365,7 +366,7 @@ struct UnresolvedGradient {
 #[derive(Clone)]
 pub struct Gradient {
     units: GradientUnits,
-    affine: cairo::Matrix,
+    transform: Transform,
     spread: SpreadMethod,
     stops: Vec<ColorStop>,
 
@@ -378,7 +379,7 @@ impl UnresolvedGradient {
 
         let UnresolvedGradient {
             units,
-            affine,
+            transform,
             spread,
             stops,
             variant,
@@ -387,7 +388,7 @@ impl UnresolvedGradient {
         match variant {
             UnresolvedVariant::Linear { .. } => Gradient {
                 units: units.unwrap(),
-                affine: affine.unwrap(),
+                transform: transform.unwrap(),
                 spread: spread.unwrap(),
                 stops: stops.unwrap(),
 
@@ -396,7 +397,7 @@ impl UnresolvedGradient {
 
             UnresolvedVariant::Radial { .. } => Gradient {
                 units: units.unwrap(),
-                affine: affine.unwrap(),
+                transform: transform.unwrap(),
                 spread: spread.unwrap(),
                 stops: stops.unwrap(),
 
@@ -472,7 +473,7 @@ impl UnresolvedGradient {
 
     fn is_resolved(&self) -> bool {
         self.units.is_some()
-            && self.affine.is_some()
+            && self.transform.is_some()
             && self.spread.is_some()
             && self.stops.is_some()
             && self.variant.is_resolved()
@@ -480,14 +481,14 @@ impl UnresolvedGradient {
 
     fn resolve_from_fallback(&self, fallback: &UnresolvedGradient) -> UnresolvedGradient {
         let units = self.units.or(fallback.units);
-        let affine = self.affine.or(fallback.affine);
+        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,
-            affine,
+            transform,
             spread,
             stops,
             variant,
@@ -496,14 +497,14 @@ impl UnresolvedGradient {
 
     fn resolve_from_defaults(&self) -> UnresolvedGradient {
         let units = self.units.or_else(|| Some(GradientUnits::default()));
-        let affine = self.affine.or_else(|| Some(cairo::Matrix::identity()));
+        let transform = self.transform.or_else(|| Some(Transform::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,
-            affine,
+            transform,
             spread,
             stops,
             variant,
@@ -550,7 +551,7 @@ macro_rules! impl_get_unresolved {
             fn get_unresolved(&self, node: &RsvgNode) -> Unresolved {
                 let mut gradient = UnresolvedGradient {
                     units: self.common.units,
-                    affine: self.common.affine,
+                    transform: self.common.transform,
                     spread: self.common.spread,
                     stops: None,
                     variant: self.get_unresolved_variant(),
@@ -574,7 +575,9 @@ impl Common {
         for (attr, value) in pbag.iter() {
             match attr.expanded() {
                 expanded_name!("", "gradientUnits") => self.units = Some(attr.parse(value)?),
-                expanded_name!("", "gradientTransform") => self.affine = Some(attr.parse(value)?),
+                expanded_name!("", "gradientTransform") => {
+                    self.transform = Some(attr.parse(value)?)
+                }
                 expanded_name!("", "spreadMethod") => self.spread = Some(attr.parse(value)?),
                 expanded_name!(xlink "href") => {
                     self.fallback = Some(Fragment::parse(value).attribute(attr)?)
@@ -753,25 +756,23 @@ impl Gradient {
         bbox: &BoundingBox,
         opacity: UnitInterval,
     ) {
-        let mut affine = self.affine;
-
-        if self.units == GradientUnits(CoordUnits::ObjectBoundingBox) {
+        let transform = if self.units == GradientUnits(CoordUnits::ObjectBoundingBox) {
             let bbox_rect = bbox.rect.unwrap();
-            let bbox_matrix = cairo::Matrix::new(
+            Transform::new(
                 bbox_rect.width(),
                 0.0,
                 0.0,
                 bbox_rect.height(),
                 bbox_rect.x0,
                 bbox_rect.y0,
-            );
-            affine = cairo::Matrix::multiply(&affine, &bbox_matrix);
-        }
+            )
+            .pre_transform(&self.transform)
+        } else {
+            self.transform
+        };
 
-        affine.invert();
-        pattern.set_matrix(affine);
+        transform.invert().map(|m| pattern.set_matrix(m.into()));
         pattern.set_extend(cairo::Extend::from(self.spread));
-
         self.add_color_stops_to_pattern(pattern, opacity);
     }
 
diff --git a/rsvg_internals/src/node.rs b/rsvg_internals/src/node.rs
index fa9f25e0..07f932e2 100644
--- a/rsvg_internals/src/node.rs
+++ b/rsvg_internals/src/node.rs
@@ -29,6 +29,7 @@ use crate::parsers::Parse;
 use crate::properties::{ComputedValues, SpecifiedValue, SpecifiedValues};
 use crate::property_bag::PropertyBag;
 use crate::property_defs::Overflow;
+use crate::transform::Transform;
 
 /// Strong reference to an element in the SVG tree.
 ///
@@ -49,7 +50,7 @@ pub struct NodeData {
     specified_values: SpecifiedValues,
     important_styles: HashSet<QualName>,
     result: NodeResult,
-    transform: cairo::Matrix,
+    transform: Transform,
     values: ComputedValues,
     cond: bool,
     style_attr: String,
@@ -71,7 +72,7 @@ impl NodeData {
             class: class.map(str::to_string),
             specified_values: Default::default(),
             important_styles: Default::default(),
-            transform: cairo::Matrix::identity(),
+            transform: Default::default(),
             result: Ok(()),
             values: ComputedValues::default(),
             cond: true,
@@ -112,7 +113,7 @@ impl NodeData {
         self.cond
     }
 
-    pub fn get_transform(&self) -> cairo::Matrix {
+    pub fn get_transform(&self) -> Transform {
         self.transform
     }
 
@@ -153,7 +154,7 @@ impl NodeData {
         for (attr, value) in pbag.iter() {
             match attr.expanded() {
                 expanded_name!("", "transform") => {
-                    return cairo::Matrix::parse_str(value)
+                    return Transform::parse_str(value)
                         .attribute(attr)
                         .and_then(|affine| {
                             self.transform = affine;
diff --git a/rsvg_internals/src/pattern.rs b/rsvg_internals/src/pattern.rs
index 6d49ab4b..f6069158 100644
--- a/rsvg_internals/src/pattern.rs
+++ b/rsvg_internals/src/pattern.rs
@@ -18,6 +18,7 @@ use crate::parsers::ParseValue;
 use crate::properties::ComputedValues;
 use crate::property_bag::PropertyBag;
 use crate::rect::Rect;
+use crate::transform::Transform;
 use crate::unit_interval::UnitInterval;
 use crate::viewbox::*;
 
@@ -34,7 +35,7 @@ struct Common {
     // In that case, the fully resolved pattern will have a .vbox=Some(None) value.
     vbox: Option<Option<ViewBox>>,
     preserve_aspect_ratio: Option<AspectRatio>,
-    affine: Option<cairo::Matrix>,
+    affine: Option<Transform>,
     x: Option<Length<Horizontal>>,
     y: Option<Length<Vertical>>,
     width: Option<Length<Horizontal>>,
@@ -98,7 +99,7 @@ pub struct ResolvedPattern {
     // In that case, the fully resolved pattern will have a .vbox=Some(None) value.
     vbox: Option<ViewBox>,
     preserve_aspect_ratio: AspectRatio,
-    affine: cairo::Matrix,
+    affine: Transform,
     x: Length<Horizontal>,
     y: Length<Vertical>,
     width: Length<Horizontal>,
@@ -257,7 +258,7 @@ impl AsPaintSource for ResolvedPattern {
             PatternUnits(CoordUnits::UserSpaceOnUse) => (1.0, 1.0),
         };
 
-        let taffine = cairo::Matrix::multiply(&pattern_affine, &draw_ctx.get_transform());
+        let taffine = draw_ctx.get_transform().pre_transform(&pattern_affine);
 
         let mut scwscale = (taffine.xx.powi(2) + taffine.xy.powi(2)).sqrt();
         let mut schscale = (taffine.yx.powi(2) + taffine.yy.powi(2)).sqrt();
@@ -279,27 +280,25 @@ impl AsPaintSource for ResolvedPattern {
         scwscale = f64::from(pw) / scaled_width;
         schscale = f64::from(ph) / scaled_height;
 
-        let mut affine = cairo::Matrix::identity();
-
         // Create the pattern coordinate system
-        match units {
+        let mut affine = match units {
             PatternUnits(CoordUnits::ObjectBoundingBox) => {
                 let bbrect = bbox.rect.unwrap();
-                affine.translate(
+                Transform::new_translate(
                     bbrect.x0 + pattern_rect.x0 * bbrect.width(),
                     bbrect.y0 + pattern_rect.y0 * bbrect.height(),
-                );
+                )
             }
 
             PatternUnits(CoordUnits::UserSpaceOnUse) => {
-                affine.translate(pattern_rect.x0, pattern_rect.y0);
+                Transform::new_translate(pattern_rect.x0, pattern_rect.y0)
             }
-        }
+        };
 
         // Apply the pattern transform
-        affine = cairo::Matrix::multiply(&affine, &pattern_affine);
+        affine = affine.post_transform(&pattern_affine);
 
-        let mut caffine: cairo::Matrix;
+        let mut caffine: Transform;
 
         // Create the pattern contents coordinate system
         let _params = if let Some(vbox) = vbox {
@@ -312,31 +311,24 @@ impl AsPaintSource for ResolvedPattern {
             let x = r.x0 - vbox.0.x0 * sw;
             let y = r.y0 - vbox.0.y0 * sh;
 
-            caffine = cairo::Matrix::new(sw, 0.0, 0.0, sh, x, y);
+            caffine = Transform::new(sw, 0.0, 0.0, sh, x, y);
 
             draw_ctx.push_view_box(vbox.0.width(), vbox.0.height())
         } else if content_units == PatternContentUnits(CoordUnits::ObjectBoundingBox) {
             // If coords are in terms of the bounding box, use them
             let (bbw, bbh) = bbox.rect.unwrap().size();
 
-            caffine = cairo::Matrix::identity();
-            caffine.scale(bbw, bbh);
+            caffine = Transform::new_scale(bbw, bbh);
 
             draw_ctx.push_view_box(1.0, 1.0)
         } else {
-            caffine = cairo::Matrix::identity();
+            caffine = Transform::identity();
             draw_ctx.get_view_params()
         };
 
         if !scwscale.approx_eq_cairo(1.0) || !schscale.approx_eq_cairo(1.0) {
-            let mut scalematrix = cairo::Matrix::identity();
-            scalematrix.scale(scwscale, schscale);
-            caffine = cairo::Matrix::multiply(&caffine, &scalematrix);
-
-            scalematrix = cairo::Matrix::identity();
-            scalematrix.scale(1.0 / scwscale, 1.0 / schscale);
-
-            affine = cairo::Matrix::multiply(&scalematrix, &affine);
+            caffine = caffine.post_scale(scwscale, schscale);
+            affine = affine.pre_scale(1.0 / scwscale, 1.0 / schscale);
         }
 
         // Draw to another surface
@@ -363,7 +355,7 @@ impl AsPaintSource for ResolvedPattern {
         let pattern_cascaded = CascadedValues::new_from_node(&node_with_children);
         let pattern_values = pattern_cascaded.get();
 
-        cr_pattern.set_matrix(caffine);
+        cr_pattern.set_matrix(caffine.into());
 
         let res =
             draw_ctx.with_discrete_layer(&node_with_children, pattern_values, false, &mut |dc| {
@@ -371,21 +363,15 @@ impl AsPaintSource for ResolvedPattern {
             });
 
         // Return to the original coordinate system and rendering context
-
         draw_ctx.set_cairo_context(&cr_save);
 
         // Set the final surface as a Cairo pattern into the Cairo context
+        let pattern = cairo::SurfacePattern::create(&surface);
 
-        let surface_pattern = cairo::SurfacePattern::create(&surface);
-        surface_pattern.set_extend(cairo::Extend::Repeat);
-
-        let mut matrix = affine;
-        matrix.invert();
-
-        surface_pattern.set_matrix(matrix);
-        surface_pattern.set_filter(cairo::Filter::Best);
-
-        cr_save.set_source(&surface_pattern);
+        affine.invert().map(|m| pattern.set_matrix(m.into()));
+        pattern.set_extend(cairo::Extend::Repeat);
+        pattern.set_filter(cairo::Filter::Best);
+        cr_save.set_source(&pattern);
 
         res.and_then(|_| Ok(true))
     }
@@ -465,10 +451,7 @@ impl UnresolvedPattern {
             .common
             .preserve_aspect_ratio
             .or_else(|| Some(AspectRatio::default()));
-        let affine = self
-            .common
-            .affine
-            .or_else(|| Some(cairo::Matrix::identity()));
+        let affine = self.common.affine.or_else(|| Some(Transform::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()));
diff --git a/rsvg_internals/src/rect.rs b/rsvg_internals/src/rect.rs
index ea6e7675..acb522d0 100644
--- a/rsvg_internals/src/rect.rs
+++ b/rsvg_internals/src/rect.rs
@@ -229,49 +229,3 @@ impl From<IRect> for cairo::Rectangle {
         }
     }
 }
-
-pub trait TransformRect {
-    fn transform_rect(&self, rect: &Rect) -> Rect;
-}
-
-impl TransformRect for cairo::Matrix {
-    fn transform_rect(&self, rect: &Rect) -> Rect {
-        let points = vec![
-            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().take(4).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,
-        }
-    }
-}
diff --git a/rsvg_internals/src/text.rs b/rsvg_internals/src/text.rs
index e5fcc426..a1ff2524 100644
--- a/rsvg_internals/src/text.rs
+++ b/rsvg_internals/src/text.rs
@@ -22,6 +22,7 @@ use crate::property_defs::{
 };
 use crate::rect::Rect;
 use crate::space::{xml_space_normalize, NormalizeDefault, XmlSpaceNormalize};
+use crate::transform::Transform;
 
 /// An absolutely-positioned array of `Span`s
 ///
@@ -357,7 +358,7 @@ impl PositionedSpan {
 
     fn compute_text_bbox(
         &self,
-        transform: cairo::Matrix,
+        transform: Transform,
         gravity: pango::Gravity,
     ) -> Option<BoundingBox> {
         let (ink, _) = self.layout.get_extents();
diff --git a/rsvg_internals/src/transform.rs b/rsvg_internals/src/transform.rs
index a8715951..4898f1fc 100644
--- a/rsvg_internals/src/transform.rs
+++ b/rsvg_internals/src/transform.rs
@@ -1,50 +1,254 @@
-//! CSS transform values.
-
-use std::f64::consts::*;
+//! Handling of `transform` values.
+//!
+//! This module handles `transform` values [per the SVG specification][spec].
+//!
+//! [spec]:  https://www.w3.org/TR/SVG11/coords.html#TransformAttribute
 
 use cssparser::{Parser, Token};
 
+use crate::angle::Angle;
 use crate::error::*;
 use crate::parsers::{optional_comma, Parse};
+use crate::rect::Rect;
+
+#[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,
+}
+
+impl Transform {
+    #[inline]
+    pub fn new(xx: f64, yx: f64, xy: f64, yy: f64, x0: f64, y0: f64) -> Self {
+        Self {
+            xx,
+            xy,
+            x0,
+            yx,
+            yy,
+            y0,
+        }
+    }
+
+    #[inline]
+    pub fn identity() -> Self {
+        Self::new(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
+    }
+
+    #[inline]
+    pub fn new_translate(tx: f64, ty: f64) -> Self {
+        Self::new(1.0, 0.0, 0.0, 1.0, tx, ty)
+    }
+
+    #[inline]
+    pub fn new_scale(sx: f64, sy: f64) -> Self {
+        Self::new(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(c, s, -s, c, 0.0, 0.0)
+    }
+
+    #[inline]
+    pub fn new_skew(ax: Angle, ay: Angle) -> Self {
+        Self::new(1.0, ay.radians().tan(), ax.radians().tan(), 1.0, 0.0, 0.0)
+    }
+
+    #[must_use]
+    pub fn multiply(t1: &Transform, t2: &Transform) -> Self {
+        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(
+            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 = vec![
+            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),
+        ];
 
-impl Parse for cairo::Matrix {
-    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<cairo::Matrix, ParseError<'i>> {
+        let (mut xmin, mut ymin, mut xmax, mut ymax) = {
+            let (x, y) = points[0];
+
+            (x, y, x, y)
+        };
+
+        for &(x, y) in points.iter().take(4).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 Parse for Transform {
+    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> {
         let loc = parser.current_source_location();
 
-        let matrix = parse_transform_list(parser)?;
+        let t = parse_transform_list(parser)?;
 
-        matrix.try_invert().map(|_| matrix).map_err(|_| {
-            loc.new_custom_error(ValueErrorKind::Value(
+        if !t.is_invertible() {
+            return Err(loc.new_custom_error(ValueErrorKind::Value(
                 "invalid transformation matrix".to_string(),
-            ))
-        })
+            )));
+        }
+
+        Ok(t)
+    }
+}
+
+impl From<cairo::Matrix> for Transform {
+    #[inline]
+    fn from(m: cairo::Matrix) -> Self {
+        Self::new(m.xx, m.yx, m.xy, m.yy, m.x0, m.y0)
     }
 }
 
-// This parser is for the "transform" attribute in SVG.
-// Its operataion and grammar are described here:
-// https://www.w3.org/TR/SVG/coords.html#TransformAttribute
+impl From<Transform> for cairo::Matrix {
+    #[inline]
+    fn from(t: Transform) -> Self {
+        Self::new(t.xx, t.yx, t.xy, t.yy, t.x0, t.y0)
+    }
+}
 
-fn parse_transform_list<'i>(parser: &mut Parser<'i, '_>) -> Result<cairo::Matrix, ParseError<'i>> {
-    let mut matrix = cairo::Matrix::identity();
+fn parse_transform_list<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> {
+    let mut t = Transform::identity();
 
     loop {
         if parser.is_exhausted() {
             break;
         }
 
-        let m = parse_transform_command(parser)?;
-        matrix = cairo::Matrix::multiply(&m, &matrix);
-
+        t = parse_transform_command(parser)?.post_transform(&t);
         optional_comma(parser);
     }
 
-    Ok(matrix)
+    Ok(t)
 }
 
-fn parse_transform_command<'i>(
-    parser: &mut Parser<'i, '_>,
-) -> Result<cairo::Matrix, ParseError<'i>> {
+fn parse_transform_command<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> {
     let loc = parser.current_source_location();
 
     match parser.next()?.clone() {
@@ -62,7 +266,7 @@ fn parse_transform_command<'i>(
 fn parse_transform_function<'i>(
     name: &str,
     parser: &mut Parser<'i, '_>,
-) -> Result<cairo::Matrix, ParseError<'i>> {
+) -> Result<Transform, ParseError<'i>> {
     let loc = parser.current_source_location();
 
     match name {
@@ -70,15 +274,15 @@ fn parse_transform_function<'i>(
         "translate" => parse_translate_args(parser),
         "scale" => parse_scale_args(parser),
         "rotate" => parse_rotate_args(parser),
-        "skewX" => parse_skewx_args(parser),
-        "skewY" => parse_skewy_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<cairo::Matrix, ParseError<'i>> {
+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);
@@ -97,11 +301,11 @@ fn parse_matrix_args<'i>(parser: &mut Parser<'i, '_>) -> Result<cairo::Matrix, P
 
         let y0 = f64::parse(p)?;
 
-        Ok(cairo::Matrix::new(xx, yx, xy, yy, x0, y0))
+        Ok(Transform::new(xx, yx, xy, yy, x0, y0))
     })
 }
 
-fn parse_translate_args<'i>(parser: &mut Parser<'i, '_>) -> Result<cairo::Matrix, ParseError<'i>> {
+fn parse_translate_args<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> {
     parser.parse_nested_block(|p| {
         let tx = f64::parse(p)?;
 
@@ -112,11 +316,11 @@ fn parse_translate_args<'i>(parser: &mut Parser<'i, '_>) -> Result<cairo::Matrix
             })
             .unwrap_or(0.0);
 
-        Ok(cairo::Matrix::new(1.0, 0.0, 0.0, 1.0, tx, ty))
+        Ok(Transform::new_translate(tx, ty))
     })
 }
 
-fn parse_scale_args<'i>(parser: &mut Parser<'i, '_>) -> Result<cairo::Matrix, ParseError<'i>> {
+fn parse_scale_args<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> {
     parser.parse_nested_block(|p| {
         let x = f64::parse(p)?;
 
@@ -127,13 +331,13 @@ fn parse_scale_args<'i>(parser: &mut Parser<'i, '_>) -> Result<cairo::Matrix, Pa
             })
             .unwrap_or(x);
 
-        Ok(cairo::Matrix::new(x, 0.0, 0.0, y, 0.0, 0.0))
+        Ok(Transform::new_scale(x, y))
     })
 }
 
-fn parse_rotate_args<'i>(parser: &mut Parser<'i, '_>) -> Result<cairo::Matrix, ParseError<'i>> {
+fn parse_rotate_args<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> {
     parser.parse_nested_block(|p| {
-        let angle = f64::parse(p)? * PI / 180.0;
+        let angle = Angle::from_degrees(f64::parse(p)?);
 
         let (tx, ty) = p
             .try_parse(|p| -> Result<_, ParseError> {
@@ -147,75 +351,116 @@ fn parse_rotate_args<'i>(parser: &mut Parser<'i, '_>) -> Result<cairo::Matrix, P
             })
             .unwrap_or((0.0, 0.0));
 
-        let (s, c) = angle.sin_cos();
-
-        let mut m = cairo::Matrix::new(1.0, 0.0, 0.0, 1.0, tx, ty);
-
-        m = cairo::Matrix::multiply(&cairo::Matrix::new(c, s, -s, c, 0.0, 0.0), &m);
-        m = cairo::Matrix::multiply(&cairo::Matrix::new(1.0, 0.0, 0.0, 1.0, -tx, -ty), &m);
-        Ok(m)
+        Ok(Transform::new_translate(tx, ty)
+            .pre_rotate(angle)
+            .pre_translate(-tx, -ty))
     })
 }
 
-fn parse_skewx_args<'i>(parser: &mut Parser<'i, '_>) -> Result<cairo::Matrix, ParseError<'i>> {
+fn parse_skew_x_args<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> {
     parser.parse_nested_block(|p| {
-        let a = f64::parse(p)? * PI / 180.0;
-        Ok(cairo::Matrix::new(1.0, 0.0, a.tan(), 1.0, 0.0, 0.0))
+        let angle = Angle::from_degrees(f64::parse(p)?);
+        Ok(Transform::new_skew(angle, Angle::new(0.0)))
     })
 }
 
-fn parse_skewy_args<'i>(parser: &mut Parser<'i, '_>) -> Result<cairo::Matrix, ParseError<'i>> {
+fn parse_skew_y_args<'i>(parser: &mut Parser<'i, '_>) -> Result<Transform, ParseError<'i>> {
     parser.parse_nested_block(|p| {
-        let a = f64::parse(p)? * PI / 180.0;
-        Ok(cairo::Matrix::new(1.0, a.tan(), 0.0, 1.0, 0.0, 0.0))
+        let angle = Angle::from_degrees(f64::parse(p)?);
+        Ok(Transform::new_skew(Angle::new(0.0), angle))
     })
 }
 
-#[cfg(test)]
-fn make_rotation_matrix(angle_degrees: f64, tx: f64, ty: f64) -> cairo::Matrix {
-    let angle = angle_degrees * PI / 180.0;
-
-    let mut m = cairo::Matrix::new(1.0, 0.0, 0.0, 1.0, tx, ty);
-
-    let mut r = cairo::Matrix::identity();
-    r.rotate(angle);
-    m = cairo::Matrix::multiply(&r, &m);
-
-    m = cairo::Matrix::multiply(&cairo::Matrix::new(1.0, 0.0, 0.0, 1.0, -tx, -ty), &m);
-    m
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
     use float_cmp::ApproxEq;
     use std::f64;
 
-    fn parse_transform(s: &str) -> Result<cairo::Matrix, ParseError> {
-        cairo::Matrix::parse_str(s)
+    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 assert_matrix_eq(a: &cairo::Matrix, b: &cairo::Matrix) {
+    fn parse_transform(s: &str) -> Result<Transform, ParseError> {
+        Transform::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!(a.xx.approx_eq(b.xx, (epsilon, 1)));
-        assert!(a.yx.approx_eq(b.yx, (epsilon, 1)));
-        assert!(a.xy.approx_eq(b.xy, (epsilon, 1)));
-        assert!(a.yy.approx_eq(b.yy, (epsilon, 1)));
-        assert!(a.x0.approx_eq(b.x0, (epsilon, 1)));
-        assert!(a.y0.approx_eq(b.y0, (epsilon, 1)));
+        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(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(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
+        let t2 = Transform::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
+        let r = Transform::new(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(0.5, 0.0, 0.0, 0.5, 10.0, 10.0);
+        let t2 = Transform::new(1.0, 0.0, 0.0, 1.0, -10.0, -10.0);
+        let r1 = Transform::new(0.5, 0.0, 0.0, 0.5, 0.0, 0.0);
+        let r2 = Transform::new(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(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(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 = cairo::Matrix::new(1.0, 0.0, 0.0, 1.0, 20.0, 30.0);
-        let s = cairo::Matrix::new(10.0, 0.0, 0.0, 10.0, 0.0, 0.0);
-        let r = make_rotation_matrix(30.0, 10.0, 10.0);
+        let t = Transform::new(1.0, 0.0, 0.0, 1.0, 20.0, 30.0);
+        let s = Transform::new(10.0, 0.0, 0.0, 10.0, 0.0, 0.0);
+        let r = rotation_transform(30.0, 10.0, 10.0);
 
-        let a = cairo::Matrix::multiply(&s, &t);
-        assert_matrix_eq(
+        let a = Transform::multiply(&s, &t);
+        assert_transform_eq(
             &parse_transform("translate(20, 30), scale (10) rotate (30 10 10)").unwrap(),
-            &cairo::Matrix::multiply(&r, &a),
+            &Transform::multiply(&r, &a),
         );
     }
 
@@ -244,127 +489,115 @@ mod tests {
 
     #[test]
     fn parses_matrix() {
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("matrix (1 2 3 4 5 6)").unwrap(),
-            &cairo::Matrix::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0),
+            &Transform::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0),
         );
 
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("matrix(1,2,3,4 5 6)").unwrap(),
-            &cairo::Matrix::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0),
+            &Transform::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0),
         );
 
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("matrix (1,2.25,-3.25e2,4 5 6)").unwrap(),
-            &cairo::Matrix::new(1.0, 2.25, -325.0, 4.0, 5.0, 6.0),
+            &Transform::new(1.0, 2.25, -325.0, 4.0, 5.0, 6.0),
         );
     }
 
     #[test]
     fn parses_translate() {
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("translate(-1 -2)").unwrap(),
-            &cairo::Matrix::new(1.0, 0.0, 0.0, 1.0, -1.0, -2.0),
+            &Transform::new(1.0, 0.0, 0.0, 1.0, -1.0, -2.0),
         );
 
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("translate(-1, -2)").unwrap(),
-            &cairo::Matrix::new(1.0, 0.0, 0.0, 1.0, -1.0, -2.0),
+            &Transform::new(1.0, 0.0, 0.0, 1.0, -1.0, -2.0),
         );
 
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("translate(-1)").unwrap(),
-            &cairo::Matrix::new(1.0, 0.0, 0.0, 1.0, -1.0, 0.0),
+            &Transform::new(1.0, 0.0, 0.0, 1.0, -1.0, 0.0),
         );
     }
 
     #[test]
     fn parses_scale() {
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("scale (-1)").unwrap(),
-            &cairo::Matrix::new(-1.0, 0.0, 0.0, -1.0, 0.0, 0.0),
+            &Transform::new(-1.0, 0.0, 0.0, -1.0, 0.0, 0.0),
         );
 
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("scale(-1 -2)").unwrap(),
-            &cairo::Matrix::new(-1.0, 0.0, 0.0, -2.0, 0.0, 0.0),
+            &Transform::new(-1.0, 0.0, 0.0, -2.0, 0.0, 0.0),
         );
 
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("scale(-1, -2)").unwrap(),
-            &cairo::Matrix::new(-1.0, 0.0, 0.0, -2.0, 0.0, 0.0),
+            &Transform::new(-1.0, 0.0, 0.0, -2.0, 0.0, 0.0),
         );
     }
 
     #[test]
     fn parses_rotate() {
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("rotate (30)").unwrap(),
-            &make_rotation_matrix(30.0, 0.0, 0.0),
+            &rotation_transform(30.0, 0.0, 0.0),
         );
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("rotate (30,-1,-2)").unwrap(),
-            &make_rotation_matrix(30.0, -1.0, -2.0),
+            &rotation_transform(30.0, -1.0, -2.0),
         );
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("rotate(30, -1, -2)").unwrap(),
-            &make_rotation_matrix(30.0, -1.0, -2.0),
+            &rotation_transform(30.0, -1.0, -2.0),
         );
     }
 
-    fn make_skew_x_matrix(angle_degrees: f64) -> cairo::Matrix {
-        let a = angle_degrees * PI / 180.0;
-        cairo::Matrix::new(1.0, 0.0, a.tan(), 1.0, 0.0, 0.0)
-    }
-
-    fn make_skew_y_matrix(angle_degrees: f64) -> cairo::Matrix {
-        let mut m = make_skew_x_matrix(angle_degrees);
-        m.yx = m.xy;
-        m.xy = 0.0;
-        m
-    }
-
     #[test]
     fn parses_skew_x() {
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("skewX (30)").unwrap(),
-            &make_skew_x_matrix(30.0),
+            &Transform::new_skew(Angle::from_degrees(30.0), Angle::new(0.0)),
         );
     }
 
     #[test]
     fn parses_skew_y() {
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("skewY (30)").unwrap(),
-            &make_skew_y_matrix(30.0),
+            &Transform::new_skew(Angle::new(0.0), Angle::from_degrees(30.0)),
         );
     }
 
     #[test]
     fn parses_transform_list() {
-        let t = cairo::Matrix::new(1.0, 0.0, 0.0, 1.0, 20.0, 30.0);
-        let s = cairo::Matrix::new(10.0, 0.0, 0.0, 10.0, 0.0, 0.0);
-        let r = make_rotation_matrix(30.0, 10.0, 10.0);
+        let t = Transform::new(1.0, 0.0, 0.0, 1.0, 20.0, 30.0);
+        let s = Transform::new(10.0, 0.0, 0.0, 10.0, 0.0, 0.0);
+        let r = rotation_transform(30.0, 10.0, 10.0);
 
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("scale(10)rotate(30, 10, 10)").unwrap(),
-            &cairo::Matrix::multiply(&r, &s),
+            &Transform::multiply(&r, &s),
         );
 
-        assert_matrix_eq(
+        assert_transform_eq(
             &parse_transform("translate(20, 30), scale (10)").unwrap(),
-            &cairo::Matrix::multiply(&s, &t),
+            &Transform::multiply(&s, &t),
         );
 
-        let a = cairo::Matrix::multiply(&s, &t);
-        assert_matrix_eq(
+        let a = Transform::multiply(&s, &t);
+        assert_transform_eq(
             &parse_transform("translate(20, 30), scale (10) rotate (30 10 10)").unwrap(),
-            &cairo::Matrix::multiply(&r, &a),
+            &Transform::multiply(&r, &a),
         );
     }
 
     #[test]
     fn parses_empty() {
-        assert_matrix_eq(&parse_transform("").unwrap(), &cairo::Matrix::identity());
+        assert_transform_eq(&parse_transform("").unwrap(), &Transform::identity());
     }
 }
diff --git a/tests/fixtures/reftests/svg1.1/filters-composite-02-b-ref.png 
b/tests/fixtures/reftests/svg1.1/filters-composite-02-b-ref.png
index 43492334..4d43318f 100644
Binary files a/tests/fixtures/reftests/svg1.1/filters-composite-02-b-ref.png and 
b/tests/fixtures/reftests/svg1.1/filters-composite-02-b-ref.png differ


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]