Skip to content

Commit

Permalink
Precalculate node stroke bounding boxes as well.
Browse files Browse the repository at this point in the history
Closes #662
  • Loading branch information
RazrFalcon committed Dec 17, 2023
1 parent d503481 commit b882871
Show file tree
Hide file tree
Showing 11 changed files with 227 additions and 64 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ This changelog also contains important changes in dependencies.
- `usvg::Tree::calculate_bounding_boxes` to calculate all bounding boxes beforehand.
- `usvg::Node::bounding_box` which returns a precalculated node's bounding box in object coordinates.
- `usvg::Node::abs_bounding_box` which returns a precalculated node's bounding box in canvas coordinates.
- `usvg::Node::stroke_bounding_box` which returns a precalculated node's bounding box,
including stroke, in object coordinates.
- `usvg::Node::abs_stroke_bounding_box` which returns a precalculated node's bounding box,
including stroke, in canvas coordinates.
- (c-api) `resvg_get_node_stroke_bbox`

### Removed
- `usvg::Node::calculate_bbox`. Use `usvg::Node::abs_bounding_box`.
Expand Down
30 changes: 28 additions & 2 deletions crates/c-api/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,7 @@ pub extern "C" fn resvg_get_node_transform(
false
}

/// @brief Returns node's bounding box by ID.
/// @brief Returns node's bounding box in canvas coordinates by ID.
///
/// @param tree Render tree.
/// @param id Node's ID. Must not be NULL.
Expand All @@ -774,6 +774,32 @@ pub extern "C" fn resvg_get_node_bbox(
tree: *const resvg_render_tree,
id: *const c_char,
bbox: *mut resvg_rect,
) -> bool {
get_node_bbox(tree, id, bbox, &|node| node.abs_bounding_box())
}

/// @brief Returns node's bounding box, including stroke, in canvas coordinates by ID.
///
/// @param tree Render tree.
/// @param id Node's ID. Must not be NULL.
/// @param bbox Node's bounding box.
/// @return `false` if a node with such an ID does not exist
/// @return `false` if ID isn't a UTF-8 string.
/// @return `false` if ID is an empty string
#[no_mangle]
pub extern "C" fn resvg_get_node_stroke_bbox(
tree: *const resvg_render_tree,
id: *const c_char,
bbox: *mut resvg_rect,
) -> bool {
get_node_bbox(tree, id, bbox, &|node| node.abs_stroke_bounding_box())
}

fn get_node_bbox(
tree: *const resvg_render_tree,
id: *const c_char,
bbox: *mut resvg_rect,
f: &dyn Fn(usvg::Node) -> Option<usvg::Rect>,
) -> bool {
let id = match cstr_to_str(id) {
Some(v) => v,
Expand All @@ -795,7 +821,7 @@ pub extern "C" fn resvg_get_node_bbox(

match tree.0.node_by_id(id) {
Some(node) => {
if let Some(r) = node.abs_bounding_box() {
if let Some(r) = f(node) {
unsafe {
*bbox = resvg_rect {
x: r.x(),
Expand Down
14 changes: 13 additions & 1 deletion crates/c-api/resvg.h
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ bool resvg_get_node_transform(const resvg_render_tree *tree,
resvg_transform *transform);

/**
* @brief Returns node's bounding box by ID.
* @brief Returns node's bounding box in canvas coordinates by ID.
*
* @param tree Render tree.
* @param id Node's ID. Must not be NULL.
Expand All @@ -442,6 +442,18 @@ bool resvg_get_node_transform(const resvg_render_tree *tree,
*/
bool resvg_get_node_bbox(const resvg_render_tree *tree, const char *id, resvg_rect *bbox);

/**
* @brief Returns node's bounding box, including stroke, in canvas coordinates by ID.
*
* @param tree Render tree.
* @param id Node's ID. Must not be NULL.
* @param bbox Node's bounding box.
* @return `false` if a node with such an ID does not exist
* @return `false` if ID isn't a UTF-8 string.
* @return `false` if ID is an empty string
*/
bool resvg_get_node_stroke_bbox(const resvg_render_tree *tree, const char *id, resvg_rect *bbox);

/**
* @brief Destroys the #resvg_render_tree.
*/
Expand Down
21 changes: 20 additions & 1 deletion crates/resvg/examples/draw_bboxes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,17 @@ fn main() {
tree.calculate_bounding_boxes();

let mut bboxes = Vec::new();
let mut stroke_bboxes = Vec::new();
let mut text_bboxes = Vec::new();
for node in tree.root.descendants() {
if let Some(bbox) = node.abs_bounding_box() {
bboxes.push(bbox);

if let Some(stroke_bbox) = node.abs_stroke_bounding_box() {
if bbox != stroke_bbox {
stroke_bboxes.push(stroke_bbox);
}
}
}

// Text bboxes are different from path bboxes.
Expand All @@ -55,6 +62,12 @@ fn main() {
});

let stroke2 = Some(usvg::Stroke {
paint: usvg::Paint::Color(usvg::Color::new_rgb(0, 200, 0)),
opacity: usvg::Opacity::new_clamped(0.5),
..usvg::Stroke::default()
});

let stroke3 = Some(usvg::Stroke {
paint: usvg::Paint::Color(usvg::Color::new_rgb(0, 0, 200)),
opacity: usvg::Opacity::new_clamped(0.5),
..usvg::Stroke::default()
Expand All @@ -66,12 +79,18 @@ fn main() {
tree.root.append_kind(usvg::NodeKind::Path(path));
}

for bbox in text_bboxes {
for bbox in stroke_bboxes {
let mut path = usvg::Path::new(Rc::new(tiny_skia::PathBuilder::from_rect(bbox)));
path.stroke = stroke2.clone();
tree.root.append_kind(usvg::NodeKind::Path(path));
}

for bbox in text_bboxes {
let mut path = usvg::Path::new(Rc::new(tiny_skia::PathBuilder::from_rect(bbox)));
path.stroke = stroke3.clone();
tree.root.append_kind(usvg::NodeKind::Path(path));
}

// Calculate bboxes of newly added path.
tree.calculate_bounding_boxes();

Expand Down
64 changes: 11 additions & 53 deletions crates/resvg/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,19 @@ pub fn convert(
.and_then(|ufill| convert_fill_path(ufill, upath.data.clone(), bounding_box, anti_alias));

let stroke_path = upath.stroke.as_ref().and_then(|ustroke| {
convert_stroke_path(
ustroke,
upath.data.clone(),
bounding_box,
text_bbox,
anti_alias,
)
convert_stroke_path(ustroke, upath.data.clone(), bounding_box, anti_alias)
});

if fill_path.is_none() && stroke_path.is_none() {
return None;
}

let mut layer_bbox = usvg::BBox::default();
let mut layer_bbox = usvg::BBox::from(bounding_box);

if let Some(_) = fill_path {
layer_bbox = layer_bbox.expand(bounding_box);
}
if let Some((_, l_bbox)) = stroke_path {
layer_bbox = layer_bbox.expand(l_bbox);
if stroke_path.is_some() {
if let Some(stroke_bbox) = upath.stroke_bounding_box {
layer_bbox = layer_bbox.expand(stroke_bbox);
}
}

// Do not add hidden paths, but preserve the bbox.
Expand All @@ -78,11 +71,11 @@ pub fn convert(
children.push(Node::FillPath(path));
}

if let Some((path, _)) = stroke_path {
if let Some(path) = stroke_path {
children.push(Node::StrokePath(path));
}
} else {
if let Some((path, _)) = stroke_path {
if let Some(path) = stroke_path {
children.push(Node::StrokePath(path));
}

Expand Down Expand Up @@ -127,26 +120,8 @@ fn convert_stroke_path(
ustroke: &usvg::Stroke,
path: Rc<tiny_skia::Path>,
object_bbox: tiny_skia::Rect,
text_bbox: Option<tiny_skia::NonZeroRect>,
anti_alias: bool,
) -> Option<(StrokePath, usvg::BBox)> {
let mut stroke = tiny_skia::Stroke {
width: ustroke.width.get(),
miter_limit: ustroke.miterlimit.get(),
line_cap: match ustroke.linecap {
usvg::LineCap::Butt => tiny_skia::LineCap::Butt,
usvg::LineCap::Round => tiny_skia::LineCap::Round,
usvg::LineCap::Square => tiny_skia::LineCap::Square,
},
line_join: match ustroke.linejoin {
usvg::LineJoin::Miter => tiny_skia::LineJoin::Miter,
usvg::LineJoin::MiterClip => tiny_skia::LineJoin::MiterClip,
usvg::LineJoin::Round => tiny_skia::LineJoin::Round,
usvg::LineJoin::Bevel => tiny_skia::LineJoin::Bevel,
},
dash: None,
};

) -> Option<StrokePath> {
// Zero-sized stroke path is not an error, because linecap round or square
// would produce the shape either way.
// TODO: Find a better way to handle it.
Expand All @@ -157,31 +132,14 @@ fn convert_stroke_path(
object_bbox.to_non_zero_rect(),
)?;

// TODO: seems like stroke shouldn't be dashed for bbox
if let Some(ref list) = ustroke.dasharray {
stroke.dash = tiny_skia::StrokeDash::new(list.clone(), ustroke.dashoffset);
}

// TODO: explain
// TODO: expand by stroke width for round/bevel joins
let stroked_path = path.stroke(&stroke, 1.0)?;

let mut layer_bbox = usvg::BBox::from(stroked_path.compute_tight_bounds()?);
if let Some(text_bbox) = text_bbox {
layer_bbox = layer_bbox.expand(usvg::BBox::from(text_bbox));
}

// TODO: dash beforehand
// TODO: preserve stroked path

let path = StrokePath {
paint,
stroke,
stroke: ustroke.to_tiny_skia(),
anti_alias,
path,
};

Some((path, layer_bbox))
Some(path)
}

pub fn render_fill_path(
Expand Down
2 changes: 2 additions & 0 deletions crates/usvg-parser/src/converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ pub(crate) fn convert_group(
mask,
filters,
bounding_box: None,
stroke_bounding_box: None,
}));

GroupKind::Create(g)
Expand Down Expand Up @@ -613,6 +614,7 @@ fn convert_path(
data: path,
abs_transform: Transform::default(),
bounding_box: None,
stroke_bounding_box: None,
};

let append_marker = || {
Expand Down
1 change: 1 addition & 0 deletions crates/usvg-parser/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ pub(crate) fn convert(
chunks,
abs_transform: Transform::default(),
bounding_box: None,
stroke_bounding_box: None,
flattened: None,
};
parent.append_kind(NodeKind::Text(text));
Expand Down
32 changes: 26 additions & 6 deletions crates/usvg-text-layout/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,21 @@ fn convert_text(root: Node, fontdb: &fontdb::Database) {

for node in &text_nodes {
if let NodeKind::Text(ref mut text) = *node.borrow_mut() {
if let Some((node, bbox)) = convert_node(text, fontdb) {
if let Some((node, bbox, stroke_bbox)) = convert_node(text, fontdb) {
text.bounding_box = Some(bbox);
// TODO: test
text.stroke_bounding_box = Some(stroke_bbox.unwrap_or(bbox.to_rect()));
text.flattened = Some(node);
}
}
}
}

fn convert_node(text: &Text, fontdb: &fontdb::Database) -> Option<(Node, NonZeroRect)> {
let (new_paths, bbox) = text_to_paths(text, fontdb)?;
fn convert_node(
text: &Text,
fontdb: &fontdb::Database,
) -> Option<(Node, NonZeroRect, Option<Rect>)> {
let (new_paths, bbox, stroke_bbox) = text_to_paths(text, fontdb)?;

let group = Node::new(NodeKind::Group(Group {
id: text.id.clone(),
Expand All @@ -95,7 +100,7 @@ fn convert_node(text: &Text, fontdb: &fontdb::Database) -> Option<(Node, NonZero
group.append_kind(NodeKind::Path(path));
}

Some((group, bbox))
Some((group, bbox, stroke_bbox))
}

trait DatabaseExt {
Expand Down Expand Up @@ -445,7 +450,10 @@ fn resolve_baseline(span: &TextSpan, font: &ResolvedFont, writing_mode: WritingM

type FontsCache = HashMap<Font, Rc<ResolvedFont>>;

fn text_to_paths(text_node: &Text, fontdb: &fontdb::Database) -> Option<(Vec<Path>, NonZeroRect)> {
fn text_to_paths(
text_node: &Text,
fontdb: &fontdb::Database,
) -> Option<(Vec<Path>, NonZeroRect, Option<Rect>)> {
let mut fonts_cache: FontsCache = HashMap::new();
for chunk in &text_node.chunks {
for span in &chunk.spans {
Expand All @@ -458,6 +466,7 @@ fn text_to_paths(text_node: &Text, fontdb: &fontdb::Database) -> Option<(Vec<Pat
}

let mut bbox = BBox::default();
let mut stroke_bbox = BBox::default();
let mut char_offset = 0;
let mut last_x = 0.0;
let mut last_y = 0.0;
Expand Down Expand Up @@ -527,6 +536,7 @@ fn text_to_paths(text_node: &Text, fontdb: &fontdb::Database) -> Option<(Vec<Pat
convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
{
bbox = bbox.expand(path.data.bounds());
stroke_bbox = stroke_bbox.expand(path.data.bounds());
new_paths.push(path);
}
}
Expand All @@ -541,12 +551,19 @@ fn text_to_paths(text_node: &Text, fontdb: &fontdb::Database) -> Option<(Vec<Pat
convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
{
bbox = bbox.expand(path.data.bounds());
stroke_bbox = stroke_bbox.expand(path.data.bounds());
new_paths.push(path);
}
}

if let Some((path, span_bbox)) = convert_span(span, &mut clusters, span_ts) {
bbox = bbox.expand(span_bbox);

// TODO: find a way to cache it
if let Some(s_bbox) = path.calculate_stroke_bounding_box() {
stroke_bbox = stroke_bbox.expand(s_bbox)
}

new_paths.push(path);
}

Expand All @@ -560,6 +577,7 @@ fn text_to_paths(text_node: &Text, fontdb: &fontdb::Database) -> Option<(Vec<Pat
convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
{
bbox = bbox.expand(path.data.bounds());
stroke_bbox = stroke_bbox.expand(path.data.bounds());
new_paths.push(path);
}
}
Expand All @@ -578,7 +596,8 @@ fn text_to_paths(text_node: &Text, fontdb: &fontdb::Database) -> Option<(Vec<Pat
}

let bbox = bbox.to_non_zero_rect()?;
Some((new_paths, bbox))
let stroke_bbox = stroke_bbox.to_rect();
Some((new_paths, bbox, stroke_bbox))
}

fn resolve_font(font: &Font, fontdb: &fontdb::Database) -> Option<ResolvedFont> {
Expand Down Expand Up @@ -697,6 +716,7 @@ fn convert_span(
data: Rc::new(path),
abs_transform: Transform::default(),
bounding_box: None,
stroke_bounding_box: None,
};

Some((path, bbox))
Expand Down
Loading

0 comments on commit b882871

Please sign in to comment.