diff --git a/CHANGELOG.md b/CHANGELOG.md
index 285a9e95c..d3544a595 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ This changelog also contains important changes in dependencies.
### Added
- `usvg_tree::Text::flattened` that will contain an flattened/outlined text.
- `usvg_tree::Text::bounding_box`. Will be set only after text flattening.
+- Optimize `usvg_tree::NodeExt::abs_transform` by storing absolute transforms in the tree
+ instead of calculating them each time.
### Changed
- `usvg_tree::Text::positions` was replaced with `usvg_tree::Text::dx` and `usvg_tree::Text::dy`.
diff --git a/crates/usvg-parser/src/converter.rs b/crates/usvg-parser/src/converter.rs
index b5a85b171..7fc503d1b 100644
--- a/crates/usvg-parser/src/converter.rs
+++ b/crates/usvg-parser/src/converter.rs
@@ -176,6 +176,7 @@ pub(crate) fn convert_doc(svg_doc: &svgtree::Document, opt: &Options) -> Result<
convert_children(svg_doc.root(), &state, &mut cache, &mut tree.root);
remove_empty_groups(&mut tree);
+ tree.calculate_abs_transforms();
if restore_viewbox {
calculate_svg_bbox(&mut tree);
@@ -541,6 +542,7 @@ pub(crate) fn convert_group(
let g = parent.append_kind(NodeKind::Group(Group {
id,
transform,
+ abs_transform: Transform::identity(),
opacity,
blend_mode,
isolate,
diff --git a/crates/usvg-text-layout/src/lib.rs b/crates/usvg-text-layout/src/lib.rs
index fd5278dba..e991e7e21 100644
--- a/crates/usvg-text-layout/src/lib.rs
+++ b/crates/usvg-text-layout/src/lib.rs
@@ -44,6 +44,7 @@ pub trait TreeTextToPath {
impl TreeTextToPath for usvg_tree::Tree {
fn convert_text(&mut self, fontdb: &fontdb::Database) {
convert_text(self.root.clone(), fontdb);
+ self.calculate_abs_transforms();
}
}
diff --git a/crates/usvg-tree/src/lib.rs b/crates/usvg-tree/src/lib.rs
index 91f27872c..f744460b4 100644
--- a/crates/usvg-tree/src/lib.rs
+++ b/crates/usvg-tree/src/lib.rs
@@ -743,9 +743,19 @@ pub struct Group {
/// Can be empty.
pub id: String,
- /// Element transform.
+ /// Element's transform.
pub transform: Transform,
+ /// Element's absolute transform.
+ ///
+ /// Contains all ancestors transforms.
+ /// Will be set automatically by the parser or can be recalculated manually using
+ /// [`Tree::calculate_abs_transforms`].
+ ///
+ /// Note that subroots, like clipPaths, masks and patterns, have their own root transform,
+ /// which isn't affected by the node that references this subroot.
+ pub abs_transform: Transform,
+
/// Group opacity.
///
/// After the group is rendered we should combine
@@ -777,6 +787,7 @@ impl Default for Group {
Group {
id: String::new(),
transform: Transform::default(),
+ abs_transform: Transform::default(),
opacity: Opacity::ONE,
blend_mode: BlendMode::Normal,
isolate: false,
@@ -999,6 +1010,16 @@ impl Tree {
pub fn filters)>(&self, mut f: F) {
loop_over_filters(&self.root, &mut f)
}
+
+ /// Calculates absolute transforms for all nodes in the tree.
+ ///
+ /// As of now, sets [`Group::abs_transform`].
+ ///
+ /// Automatically called by the parser
+ /// and ideally should be called manually after each tree modification.
+ pub fn calculate_abs_transforms(&mut self) {
+ calculate_abs_transform(&self.root, Transform::identity());
+ }
}
fn has_text_nodes(root: &Node) -> bool {
@@ -1203,17 +1224,17 @@ pub trait NodeExt {
///
/// If a current node doesn't support transformation - a default
/// transform will be returned.
+ ///
+ /// This method is cheap, since an absolute transform is already stored in
+ /// [`Group::abs_transform`].
fn abs_transform(&self) -> Transform;
/// Appends `kind` as a node child.
- ///
- /// Shorthand for `Node::append(Node::new(Box::new(kind)))`.
fn append_kind(&self, kind: NodeKind) -> Node;
/// Calculates node's absolute bounding box.
///
- /// Always returns `None` for `NodeKind::Text` since we cannot calculate its bbox
- /// without converting it into paths first.
+ /// Returns `None` for `NodeKind::Text` unless it was flattened already.
fn calculate_bbox(&self) -> Option;
/// Calls a closure for each subroot this `Node` has.
@@ -1248,19 +1269,13 @@ impl NodeExt for Node {
}
fn abs_transform(&self) -> Transform {
- let mut ts_list = Vec::new();
- for p in self.ancestors() {
- if let NodeKind::Group(ref group) = *p.borrow() {
- ts_list.push(group.transform);
- }
- }
-
- let mut abs_ts = Transform::default();
- for ts in ts_list.iter().rev() {
- abs_ts = abs_ts.pre_concat(*ts);
+ if let NodeKind::Group(ref g) = *self.borrow() {
+ g.abs_transform
+ } else {
+ // Only groups can have a transform, therefore for paths, images and text
+ // we simply use the parent transform.
+ self.parent().map(|n| n.abs_transform()).unwrap_or_default()
}
-
- abs_ts
}
#[inline]
@@ -1314,3 +1329,21 @@ fn calc_node_bbox(node: &Node, ts: Transform) -> Option {
}
}
}
+
+// TODO: test somehow
+fn calculate_abs_transform(node: &Node, ts: Transform) {
+ if matches!(*node.borrow(), NodeKind::Group(_)) {
+ let mut abs_ts = ts;
+ if let NodeKind::Group(ref mut group) = *node.borrow_mut() {
+ group.abs_transform = ts.pre_concat(group.transform);
+ abs_ts = group.abs_transform;
+ }
+
+ for child in node.children() {
+ calculate_abs_transform(&child, abs_ts);
+ }
+ }
+
+ // Yes, subroots are not affected by the node's transform.
+ node.subroots(|root| calculate_abs_transform(&root, Transform::identity()));
+}