diff --git a/CHANGELOG.md b/CHANGELOG.md index 47e8884b4..88a9fe6e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This changelog also contains important changes in dependencies. ## [Unreleased] ### Fixed - `font-family` parsing. +- Absolute bounding box calculation for paths. ## [0.38.0] - 2024-01-21 ### Added diff --git a/Cargo.lock b/Cargo.lock index 37209a54d..016e9c775 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -327,9 +327,9 @@ dependencies = [ [[package]] name = "tiny-skia" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a067b809476893fce6a254cf285850ff69c847e6cfbade6a20b655b6c7e80d" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" dependencies = [ "arrayref", "arrayvec", @@ -342,9 +342,9 @@ dependencies = [ [[package]] name = "tiny-skia-path" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de35e8a90052baaaf61f171680ac2f8e925a1e43ea9d2e3a00514772250e541" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" dependencies = [ "arrayref", "bytemuck", diff --git a/crates/resvg/Cargo.toml b/crates/resvg/Cargo.toml index 3bdad9071..b39ab3772 100644 --- a/crates/resvg/Cargo.toml +++ b/crates/resvg/Cargo.toml @@ -22,7 +22,7 @@ pico-args = { version = "0.5", features = ["eq-separator"] } png = { version = "0.17", optional = true } rgb = "0.8" svgtypes = "0.13" -tiny-skia = "0.11.3" +tiny-skia = "0.11.4" usvg = { path = "../usvg", version = "0.38.0", default-features = false } [dev-dependencies] diff --git a/crates/resvg/examples/bboxes.svg b/crates/resvg/examples/bboxes.svg index b88a255ac..30c9622b0 100644 --- a/crates/resvg/examples/bboxes.svg +++ b/crates/resvg/examples/bboxes.svg @@ -1,4 +1,4 @@ - + Simple rect @@ -14,7 +14,10 @@ Rect with transform - THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG the quick brown fox jumps over the lazy dog + Circle with transform + + + THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG the quick brown fox jumps over the lazy dog Simple text Text diff --git a/crates/usvg-parser/src/converter.rs b/crates/usvg-parser/src/converter.rs index 3f15b5810..628c050d2 100644 --- a/crates/usvg-parser/src/converter.rs +++ b/crates/usvg-parser/src/converter.rs @@ -577,7 +577,9 @@ pub(crate) fn convert_group( mask, filters, bounding_box: None, + abs_bounding_box: None, stroke_bounding_box: None, + abs_stroke_bounding_box: None, layer_bounding_box: None, children: Vec::new(), }; @@ -643,7 +645,9 @@ fn convert_path( data: path, abs_transform: Transform::default(), bounding_box: None, + abs_bounding_box: None, stroke_bounding_box: None, + abs_stroke_bounding_box: None, }; match raw_paint_order.order { diff --git a/crates/usvg-parser/src/text.rs b/crates/usvg-parser/src/text.rs index 08ea2a54d..359f39958 100644 --- a/crates/usvg-parser/src/text.rs +++ b/crates/usvg-parser/src/text.rs @@ -131,7 +131,9 @@ pub(crate) fn convert( chunks, abs_transform: Transform::default(), bounding_box: None, + abs_bounding_box: None, stroke_bounding_box: None, + abs_stroke_bounding_box: None, flattened: None, }; parent.children.push(Node::Text(Box::new(text))); diff --git a/crates/usvg-text-layout/src/lib.rs b/crates/usvg-text-layout/src/lib.rs index a8c05e800..bb37b399d 100644 --- a/crates/usvg-text-layout/src/lib.rs +++ b/crates/usvg-text-layout/src/lib.rs @@ -41,8 +41,11 @@ pub fn convert_text(root: &mut Group, fontdb: &fontdb::Database) { if let Node::Text(ref mut text) = node { if let Some((node, bbox, stroke_bbox)) = convert_node(text, fontdb) { text.bounding_box = Some(bbox); + text.abs_bounding_box = bbox.transform(text.abs_transform); // TODO: test + // TODO: should we stroke transformed paths? text.stroke_bounding_box = Some(stroke_bbox); + text.abs_stroke_bounding_box = stroke_bbox.transform(text.abs_transform); text.flattened = Some(Box::new(node)); } } @@ -692,7 +695,9 @@ fn convert_span( data: Rc::new(path), abs_transform: Transform::default(), bounding_box: None, + abs_bounding_box: None, stroke_bounding_box: None, + abs_stroke_bounding_box: None, }; Some((path, bbox)) diff --git a/crates/usvg-tree/Cargo.toml b/crates/usvg-tree/Cargo.toml index 25eeb7b5a..75ad53ea7 100644 --- a/crates/usvg-tree/Cargo.toml +++ b/crates/usvg-tree/Cargo.toml @@ -15,4 +15,4 @@ workspace = "../.." [dependencies] strict-num = "0.1.1" svgtypes = "0.13" -tiny-skia-path = "0.11.3" +tiny-skia-path = "0.11.4" diff --git a/crates/usvg-tree/src/lib.rs b/crates/usvg-tree/src/lib.rs index 73a27e457..8a5e5ba45 100644 --- a/crates/usvg-tree/src/lib.rs +++ b/crates/usvg-tree/src/lib.rs @@ -789,7 +789,12 @@ impl Node { /// /// This method is cheap since bounding boxes are already calculated. pub fn abs_bounding_box(&self) -> Option { - self.bounding_box()?.transform(self.abs_transform()) + match self { + Node::Group(ref group) => group.abs_bounding_box, + Node::Path(ref path) => path.abs_bounding_box, + Node::Image(ref image) => image.abs_bounding_box().map(|r| r.to_rect()), + Node::Text(ref text) => text.abs_bounding_box.map(|r| r.to_rect()), + } } /// Returns node's bounding box, including stroke, in object coordinates, if any. @@ -809,7 +814,13 @@ impl Node { /// /// This method is cheap since bounding boxes are already calculated. pub fn abs_stroke_bounding_box(&self) -> Option { - self.stroke_bounding_box()?.transform(self.abs_transform()) + match self { + Node::Group(ref group) => group.abs_stroke_bounding_box, + Node::Path(ref path) => path.abs_stroke_bounding_box, + // Image cannot be stroked. + Node::Image(ref image) => image.abs_bounding_box(), + Node::Text(ref text) => text.abs_stroke_bounding_box, + } } /// Calls a closure for each subroot this `Node` has. @@ -922,11 +933,23 @@ pub struct Group { /// Will be set after calling `usvg::Tree::postprocess`. pub bounding_box: Option, + /// Element's bounding box in canvas coordinates. + /// + /// `userSpaceOnUse` in SVG terms. + /// + /// Will be set after calling `usvg::Tree::postprocess`. + pub abs_bounding_box: Option, + /// Element's object bounding box including stroke. /// /// Similar to `bounding_box`, but includes stroke. pub stroke_bounding_box: Option, + /// Element's bounding box including stroke in user coordinates. + /// + /// Similar to `abs_bounding_box`, but includes stroke. + pub abs_stroke_bounding_box: Option, + /// Element's "layer" bounding box in object units. /// /// Conceptually, this is `stroke_bounding_box` expanded and/or clipped @@ -959,7 +982,9 @@ impl Default for Group { mask: None, filters: Vec::new(), bounding_box: None, + abs_bounding_box: None, stroke_bounding_box: None, + abs_stroke_bounding_box: None, layer_bounding_box: None, children: Vec::new(), } @@ -1154,12 +1179,26 @@ pub struct Path { /// Will be set after calling `usvg::Tree::postprocess`. pub bounding_box: Option, + /// Element's bounding box in canvas coordinates. + /// + /// `userSpaceOnUse` in SVG terms. + /// + /// Will be set after calling `usvg::Tree::postprocess`. + pub abs_bounding_box: Option, + /// Element's object bounding box including stroke. /// /// Similar to `bounding_box`, but includes stroke. /// /// Will have the same value as `bounding_box` when path has no stroke. pub stroke_bounding_box: Option, + + /// Element's bounding box including stroke in canvas coordinates. + /// + /// Similar to `abs_bounding_box`, but includes stroke. + /// + /// Will have the same value as `abs_bounding_box` when path has no stroke. + pub abs_stroke_bounding_box: Option, } impl Path { @@ -1175,7 +1214,9 @@ impl Path { data, abs_transform: Transform::default(), bounding_box: None, + abs_bounding_box: None, stroke_bounding_box: None, + abs_stroke_bounding_box: None, } } @@ -1183,13 +1224,21 @@ impl Path { /// /// This operation is expensive. pub fn calculate_stroke_bounding_box(&self) -> Option { - let stroke = self.stroke.as_ref()?; - let mut stroke = stroke.to_tiny_skia(); + Self::calculate_stroke_bounding_box_inner(self.stroke.as_ref(), &self.data) + } + + fn calculate_stroke_bounding_box_inner( + stroke: Option<&Stroke>, + path: &tiny_skia_path::Path, + ) -> Option { + let mut stroke = stroke?.to_tiny_skia(); // According to the spec, dash should not be accounted during bbox calculation. stroke.dash = None; + // TODO: avoid for round and bevel caps + // Expensive, but there is not much we can do about it. - if let Some(stroked_path) = self.data.stroke(&stroke, 1.0) { + if let Some(stroked_path) = path.stroke(&stroke, 1.0) { // A stroked path cannot have zero width or height, // therefore we use `NonZeroRect` here. return stroked_path @@ -1291,6 +1340,11 @@ pub struct Image { } impl Image { + fn abs_bounding_box(&self) -> Option { + self.bounding_box + .and_then(|r| r.transform(self.abs_transform)) + } + fn subroots(&self, f: &mut dyn FnMut(&Group)) { if let ImageKind::SVG(ref tree) = self.kind { f(&tree.root) @@ -1373,14 +1427,14 @@ impl Tree { /// Calculates absolute transforms for all nodes in the tree. /// - /// A low-level methods. Prefer `usvg::Tree::postprocess` instead. + /// A low-level method. Prefer `usvg::Tree::postprocess` instead. pub fn calculate_abs_transforms(&mut self) { self.root.calculate_abs_transforms(Transform::identity()); } /// Calculates bounding boxes for all nodes in the tree. /// - /// A low-level methods. Prefer `usvg::Tree::postprocess` instead. + /// A low-level method. Prefer `usvg::Tree::postprocess` instead. pub fn calculate_bounding_boxes(&mut self) { self.root.calculate_bounding_boxes(); } @@ -1536,7 +1590,7 @@ fn loop_over_filters(parent: &Group, f: &mut dyn FnMut(filter::SharedFilter)) { impl Group { /// Calculates absolute transforms for all children of this group. /// - /// A low-level methods. Prefer `usvg::Tree::postprocess` instead. + /// A low-level method. Prefer `usvg::Tree::postprocess` instead. pub fn calculate_abs_transforms(&mut self, transform: Transform) { for node in &mut self.children { match node { @@ -1557,13 +1611,36 @@ impl Group { /// Calculates bounding boxes for all children of this group. /// - /// A low-level methods. Prefer `usvg::Tree::postprocess` instead. + /// A low-level method. Prefer `usvg::Tree::postprocess` instead. pub fn calculate_bounding_boxes(&mut self) { for node in &mut self.children { match node { Node::Path(ref mut path) => { path.bounding_box = path.data.compute_tight_bounds(); path.stroke_bounding_box = path.calculate_stroke_bounding_box(); + + if path.abs_transform.has_skew() { + // TODO: avoid re-alloc + let path2 = path.data.as_ref().clone(); + if let Some(path2) = path2.transform(path.abs_transform) { + path.abs_bounding_box = path2.compute_tight_bounds(); + path.abs_stroke_bounding_box = + Path::calculate_stroke_bounding_box_inner( + path.stroke.as_ref(), + &path2, + ); + } + } else { + // A transform without a skew can be performed just on a bbox. + path.abs_bounding_box = path + .bounding_box + .and_then(|r| r.transform(path.abs_transform)); + + path.abs_stroke_bounding_box = path + .stroke_bounding_box + .and_then(|r| r.transform(path.abs_transform)); + } + if path.stroke_bounding_box.is_none() { path.stroke_bounding_box = path.bounding_box.and_then(|r| r.to_non_zero_rect()); @@ -1584,7 +1661,9 @@ impl Group { } let mut bbox = BBox::default(); + let mut abs_bbox = BBox::default(); let mut stroke_bbox = BBox::default(); + let mut abs_stroke_bbox = BBox::default(); let mut layer_bbox = BBox::default(); for child in &self.children { if let Some(mut c_bbox) = child.bounding_box() { @@ -1597,6 +1676,10 @@ impl Group { bbox = bbox.expand(c_bbox); } + if let Some(c_bbox) = child.abs_bounding_box() { + abs_bbox = abs_bbox.expand(c_bbox); + } + if let Some(mut c_bbox) = child.stroke_bounding_box() { if let Node::Group(ref group) = child { if let Some(r) = c_bbox.transform(group.transform) { @@ -1607,6 +1690,10 @@ impl Group { stroke_bbox = stroke_bbox.expand(c_bbox); } + if let Some(c_bbox) = child.abs_stroke_bounding_box() { + abs_stroke_bbox = abs_stroke_bbox.expand(c_bbox); + } + if let Node::Group(ref group) = child { if let Some(r) = group.layer_bounding_box { if let Some(r) = r.transform(group.transform) { @@ -1620,7 +1707,9 @@ impl Group { } self.bounding_box = bbox.to_rect(); + self.abs_bounding_box = abs_bbox.to_rect(); self.stroke_bounding_box = stroke_bbox.to_non_zero_rect(); + self.abs_stroke_bounding_box = abs_stroke_bbox.to_non_zero_rect(); // Filter bbox has a higher priority than layers bbox. if let Some(filter_bbox) = self.filters_bounding_box() { diff --git a/crates/usvg-tree/src/text.rs b/crates/usvg-tree/src/text.rs index 628996241..019ea7ac3 100644 --- a/crates/usvg-tree/src/text.rs +++ b/crates/usvg-tree/src/text.rs @@ -339,7 +339,7 @@ pub struct Text { /// The SVG one would be set only on groups. pub abs_transform: Transform, - /// Contains a text bounding box. + /// Element's text bounding box. /// /// Text bounding box is special in SVG and doesn't represent /// tight bounds of the element's content. @@ -354,6 +354,11 @@ pub struct Text { /// This is because we have to perform a text layout before calculating a bounding box. pub bounding_box: Option, + /// Element's text bounding box in canvas coordinates. + /// + /// `userSpaceOnUse` in SVG terms. + pub abs_bounding_box: Option, + /// Element's object bounding box including stroke. /// /// Similar to `bounding_box`, but includes stroke. @@ -361,6 +366,9 @@ pub struct Text { /// Will have the same value as `bounding_box` when path has no stroke. pub stroke_bounding_box: Option, + /// Element's bounding box including stroke in canvas coordinates. + pub abs_stroke_bounding_box: Option, + /// Text converted into paths, ready to render. /// /// Will be set only after calling `usvg::Tree::postprocess` with diff --git a/crates/usvg/src/writer.rs b/crates/usvg/src/writer.rs index 652a47bc0..8985847c9 100644 --- a/crates/usvg/src/writer.rs +++ b/crates/usvg/src/writer.rs @@ -777,7 +777,9 @@ fn write_text_path_paths(parent: &Group, ctx: &mut WriterContext, xml: &mut XmlW paint_order: PaintOrder::default(), abs_transform: Transform::default(), bounding_box: None, + abs_bounding_box: None, stroke_bounding_box: None, + abs_stroke_bounding_box: None, }; write_path(&path, false, Transform::default(), None, ctx, xml); ctx.text_path_map