Skip to content

Commit

Permalink
Fix absolute bounding box calculation for paths.
Browse files Browse the repository at this point in the history
  • Loading branch information
RazrFalcon committed Feb 4, 2024
1 parent b241d35 commit e6e1d20
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 18 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/resvg/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
7 changes: 5 additions & 2 deletions crates/resvg/examples/bboxes.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions crates/usvg-parser/src/converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions crates/usvg-parser/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
5 changes: 5 additions & 0 deletions crates/usvg-text-layout/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Expand Down Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion crates/usvg-tree/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
107 changes: 98 additions & 9 deletions crates/usvg-tree/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,12 @@ impl Node {
///
/// This method is cheap since bounding boxes are already calculated.
pub fn abs_bounding_box(&self) -> Option<Rect> {
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.
Expand All @@ -809,7 +814,13 @@ impl Node {
///
/// This method is cheap since bounding boxes are already calculated.
pub fn abs_stroke_bounding_box(&self) -> Option<NonZeroRect> {
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.
Expand Down Expand Up @@ -922,11 +933,23 @@ pub struct Group {
/// Will be set after calling `usvg::Tree::postprocess`.
pub bounding_box: Option<Rect>,

/// Element's bounding box in canvas coordinates.
///
/// `userSpaceOnUse` in SVG terms.
///
/// Will be set after calling `usvg::Tree::postprocess`.
pub abs_bounding_box: Option<Rect>,

/// Element's object bounding box including stroke.
///
/// Similar to `bounding_box`, but includes stroke.
pub stroke_bounding_box: Option<NonZeroRect>,

/// Element's bounding box including stroke in user coordinates.
///
/// Similar to `abs_bounding_box`, but includes stroke.
pub abs_stroke_bounding_box: Option<NonZeroRect>,

/// Element's "layer" bounding box in object units.
///
/// Conceptually, this is `stroke_bounding_box` expanded and/or clipped
Expand Down Expand Up @@ -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(),
}
Expand Down Expand Up @@ -1154,12 +1179,26 @@ pub struct Path {
/// Will be set after calling `usvg::Tree::postprocess`.
pub bounding_box: Option<Rect>,

/// Element's bounding box in canvas coordinates.
///
/// `userSpaceOnUse` in SVG terms.
///
/// Will be set after calling `usvg::Tree::postprocess`.
pub abs_bounding_box: Option<Rect>,

/// 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<NonZeroRect>,

/// 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<NonZeroRect>,
}

impl Path {
Expand All @@ -1175,21 +1214,31 @@ impl Path {
data,
abs_transform: Transform::default(),
bounding_box: None,
abs_bounding_box: None,
stroke_bounding_box: None,
abs_stroke_bounding_box: None,
}
}

/// Calculates and sets path's stroke bounding box.
///
/// This operation is expensive.
pub fn calculate_stroke_bounding_box(&self) -> Option<NonZeroRect> {
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<NonZeroRect> {
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
Expand Down Expand Up @@ -1291,6 +1340,11 @@ pub struct Image {
}

impl Image {
fn abs_bounding_box(&self) -> Option<NonZeroRect> {
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)
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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());
Expand All @@ -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() {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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() {
Expand Down
10 changes: 9 additions & 1 deletion crates/usvg-tree/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -354,13 +354,21 @@ pub struct Text {
/// This is because we have to perform a text layout before calculating a bounding box.
pub bounding_box: Option<NonZeroRect>,

/// Element's text bounding box in canvas coordinates.
///
/// `userSpaceOnUse` in SVG terms.
pub abs_bounding_box: Option<NonZeroRect>,

/// 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<NonZeroRect>,

/// Element's bounding box including stroke in canvas coordinates.
pub abs_stroke_bounding_box: Option<NonZeroRect>,

/// Text converted into paths, ready to render.
///
/// Will be set only after calling `usvg::Tree::postprocess` with
Expand Down
Loading

0 comments on commit e6e1d20

Please sign in to comment.