Skip to content

Commit

Permalink
feat: add bbox related api
Browse files Browse the repository at this point in the history
  • Loading branch information
zimond authored and yisibl committed May 5, 2022
1 parent ecc7a56 commit a3004bb
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 2 deletions.
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ svgtypes = "0.8.0"
tiny-skia = "0.6.3"
thiserror = "1.0.30"
png = "0.17.3"
pathfinder_geometry = "0.5.1"
pathfinder_content = { version = "0.5.0", default-features = false }
pathfinder_simd = { version = "0.5.1", features = ["pf-no-simd"] }

[target.'cfg(all(not(all(target_os = "linux", target_arch = "aarch64", target_env = "musl")), not(all(target_os = "windows", target_arch = "aarch64")), not(target_arch = "wasm32")))'.dependencies]
mimalloc-rust = { version = "0.1" }
Expand All @@ -33,7 +36,7 @@ js-sys = "0.3.56"
usvg = { version = "0.22.0", default-features = false, features = [
"export",
"filter",
]}
] }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
napi = { version = "2.2.0", features = ["serde-json"] }
Expand All @@ -42,7 +45,7 @@ usvg = { version = "0.22.0", default-features = false, features = [
"export",
"filter",
"text",
]}
] }

[build-dependencies]
napi-build = "1"
Expand Down
206 changes: 206 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#[cfg(not(target_arch = "wasm32"))]
use napi::bindgen_prelude::{AbortSignal, AsyncTask, Buffer, Either, Error as NapiError, Task};
use pathfinder_content::{
outline::{Contour, Outline},
stroke::{LineCap, LineJoin, OutlineStrokeToFill, StrokeStyle},
};
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::Vector2F;

#[cfg(not(target_arch = "wasm32"))]
use napi_derive::napi;
Expand Down Expand Up @@ -36,6 +42,15 @@ extern "C" {
pub type IStringOrBuffer;
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[derive(Debug)]
pub struct BBox {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[cfg_attr(not(target_arch = "wasm32"), napi)]
pub struct Resvg {
Expand Down Expand Up @@ -137,6 +152,37 @@ impl Resvg {
pub fn to_string(&self) -> String {
self.tree.to_string(&usvg::XmlOptions::default())
}

/// Calculate a maximum bounding box of all visible elements in this
/// SVG.
///
/// Note: path bounding box are approx. values
pub fn inner_bbox(&self) -> BBox {
let rect = self.tree.svg_node().view_box.rect;
let rect = points_to_rect(
usvg::Point::new(rect.x(), rect.y()),
usvg::Point::new(rect.right(), rect.bottom()),
);
let mut v = None;
for child in self.tree.root().children().skip(1) {
let child_viewbox = match self.node_bbox(child).and_then(|v| v.intersection(rect)) {
Some(v) => v,
None => continue,
};
if let Some(v) = v.as_mut() {
*v = child_viewbox.union_rect(*v);
} else {
v = Some(child_viewbox)
};
}
let v = v.unwrap();
BBox {
x: v.min_x().floor() as f64,
y: v.min_y().floor() as f64,
width: (v.max_x().ceil() - v.min_x().floor()) as f64,
height: (v.max_y().ceil() - v.min_y().floor()) as f64,
}
}
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
Expand Down Expand Up @@ -208,6 +254,160 @@ impl Resvg {
}

impl Resvg {
fn node_by_id(&self, id: &str) -> Option<usvg::Node> {
for node in self.tree.root().descendants() {
if id == node.borrow().id() {
return Some(node);
}
}
None
}

fn node_bbox(&self, node: usvg::Node) -> Option<RectF> {
let transform = node.borrow().transform();
let bbox = match &*node.borrow() {
usvg::NodeKind::Path(p) => {
let no_fill = p.fill.is_none()
|| p
.fill
.as_ref()
.map(|f| f.opacity.value() == 0.0)
.unwrap_or_default();
let no_stroke = p.stroke.is_none()
|| p
.stroke
.as_ref()
.map(|f| f.opacity.value() == 0.0)
.unwrap_or_default();
if no_fill && no_stroke {
return None;
}
let mut outline = Outline::new();
let mut contour = Contour::new();
let mut iter = p.data.0.iter().peekable();
while let Some(seg) = iter.next() {
match seg {
usvg::PathSegment::MoveTo { x, y } => {
if !contour.is_empty() {
outline.push_contour(std::mem::replace(&mut contour, Contour::new()));
}
contour.push_endpoint(Vector2F::new(*x as f32, *y as f32));
}
usvg::PathSegment::LineTo { x, y } => {
let v = Vector2F::new(*x as f32, *y as f32);
if let Some(usvg::PathSegment::ClosePath) = iter.peek() {
let first = contour.position_of(0);
if (first - v).square_length() < 1.0 {
continue;
}
}
contour.push_endpoint(v);
}
usvg::PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x,
y,
} => {
contour.push_cubic(
Vector2F::new(*x1 as f32, *y1 as f32),
Vector2F::new(*x2 as f32, *y2 as f32),
Vector2F::new(*x as f32, *y as f32),
);
}
usvg::PathSegment::ClosePath => {
contour.close();
outline.push_contour(std::mem::replace(&mut contour, Contour::new()));
}
}
}
if !contour.is_empty() {
outline.push_contour(std::mem::replace(&mut contour, Contour::new()));
}
if let Some(stroke) = p.stroke.as_ref() {
if !no_stroke {
let mut style = StrokeStyle::default();
style.line_width = stroke.width.value() as f32;
style.line_join = LineJoin::Miter(style.line_width);
style.line_cap = match stroke.linecap {
usvg::LineCap::Butt => LineCap::Butt,
usvg::LineCap::Round => LineCap::Round,
usvg::LineCap::Square => LineCap::Square,
};
let mut filler = OutlineStrokeToFill::new(&outline, style);
filler.offset();
outline = filler.into_outline();
}
}
Some(outline.bounds())
}
usvg::NodeKind::Group(g) => {
let clippath = if let Some(clippath) = g
.clip_path
.as_ref()
.and_then(|cp| self.node_by_id(cp))
.and_then(|n| n.first_child())
{
self.node_bbox(clippath)
} else if let Some(mask) = g.mask.as_ref().and_then(|cp| self.node_by_id(cp)) {
self.node_bbox(mask)
} else {
Some(self.viewbox())
}?;
let mut v = None;
for child in node.children() {
let child_viewbox = match self.node_bbox(child).and_then(|v| v.intersection(clippath)) {
Some(v) => v,
None => continue,
};
if let Some(v) = v.as_mut() {
*v = child_viewbox.union_rect(*v);
} else {
v = Some(child_viewbox)
};
}
v.and_then(|v| v.intersection(self.viewbox()))
}
usvg::NodeKind::Image(image) => {
let rect = image.view_box.rect;
Some(points_to_rect(
usvg::Point::new(rect.x(), rect.y()),
usvg::Point::new(rect.right(), rect.bottom()),
))
}
usvg::NodeKind::ClipPath(_) | usvg::NodeKind::Mask(_) => {
if let Some(child) = node.first_child() {
self.node_bbox(child)
} else {
None
}
}
_ => None,
}?;
let (x1, y1) = transform.apply(bbox.min_x() as f64, bbox.min_y() as f64);
let (x2, y2) = transform.apply(bbox.max_x() as f64, bbox.max_y() as f64);
let (x3, y3) = transform.apply(bbox.min_x() as f64, bbox.max_y() as f64);
let (x4, y4) = transform.apply(bbox.max_x() as f64, bbox.min_y() as f64);
let x_min = x1.min(x2).min(x3).min(x4);
let x_max = x1.max(x2).max(x3).max(x4);
let y_min = y1.min(y2).min(y3).min(y4);
let y_max = y1.max(y2).max(y3).max(y4);
let r = points_to_rect(
usvg::Point::new(x_min, y_min),
usvg::Point::new(x_max, y_max),
);
Some(r)
}

fn viewbox(&self) -> RectF {
RectF::new(
Vector2F::new(0.0, 0.0),
Vector2F::new(self.width() as f32, self.height() as f32),
)
}

fn render_inner(&self) -> Result<RenderedImage, Error> {
let pixmap_size = self
.js_options
Expand Down Expand Up @@ -281,3 +481,9 @@ pub fn render_async(
None => AsyncTask::new(AsyncRenderer { options, svg }),
}
}

fn points_to_rect(min: usvg::Point<f64>, max: usvg::Point<f64>) -> RectF {
let min = Vector2F::new(min.x as f32, min.y as f32);
let max = Vector2F::new(max.x as f32, max.y as f32);
RectF::new(min, max - min)
}

1 comment on commit a3004bb

@vercel
Copy link

@vercel vercel bot commented on a3004bb May 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

resvg-js – ./

resvg-js-yisibl.vercel.app
resvg-js.vercel.app
resvg-js-git-main-yisibl.vercel.app

Please sign in to comment.