Skip to content

Commit

Permalink
feat: add napi-rs and wasm_bindgen bindings to bbox
Browse files Browse the repository at this point in the history
  • Loading branch information
yisibl committed May 7, 2022
1 parent 92bb14d commit 9933961
Show file tree
Hide file tree
Showing 18 changed files with 312 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ jobs:
run: cargo fmt -- --check

- name: Clippy
run: cargo clippy
run: cargo clippy
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Although we support the use of Wasm packages in Node.js, this is not recommended
<script>
;(async function () {
// The Wasm must be initialized first
await resvg.initWasm(fetch('https://unpkg.com/@resvg/resvg-wasm@2.0.0-beta.0/index_bg.wasm'))
await resvg.initWasm(fetch('https://unpkg.com/@resvg/resvg-wasm/index_bg.wasm'))
const opts = {
fitTo: {
mode: 'width', // If you need to change the size
Expand Down
40 changes: 40 additions & 0 deletions __test__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,46 @@ test('should render `<use xlink:href>` to an `<defs>` element', async (t) => {
t.is(result.getHeight(), 900)
})

test('should get svg bbox', (t) => {
const svg = `<svg viewBox="-40 0 150 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g fill="red" transform="rotate(-10 50 100) translate(-36 45.5) skewX(40) scale(1 0.5)">
<path id="heart" d="M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 z" />
</g>
</svg>`

const resvg = new Resvg(svg, {
fitTo: {
mode: 'width',
value: 500,
},
})
const bbox = resvg.innerBBox()

t.is(bbox.width, 120)
t.is(bbox.height, 49)
t.is(bbox.x, -30)
t.is(bbox.y, 49)
})

test('should get svg bbox(rect)', (t) => {
const svg = `<svg width="300px" height="300px" viewBox="0 0 300 300" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect fill="#5283E8" x="50" y="60.8" width="200" height="100"></rect>
</svg>`

const resvg = new Resvg(svg, {
fitTo: {
mode: 'width',
value: 380,
},
})
const bbox = resvg.innerBBox()

t.is(bbox.width, 200)
t.is(bbox.height, 101) // Here the expected value is actually 100, and the calculation of the bbox needs to be fixed.
t.is(bbox.x, 50)
t.is(bbox.y, 60)
})

test('should throw because invalid SVG attribute (width attribute is 0)', (t) => {
const error = t.throws(
() => {
Expand Down
19 changes: 19 additions & 0 deletions __test__/wasm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,25 @@ test('should render `<use xlink:href>` to an `<defs>` element', async (t) => {
t.is(result.getHeight(), 900)
})

test('should get svg bbox', (t) => {
const svg = `<svg width="300px" height="300px" viewBox="0 0 300 300" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect fill="#5283E8" x="50" y="60.8" width="200" height="100"></rect>
</svg>`

const resvg = new Resvg(svg, {
fitTo: {
mode: 'width',
value: 500,
},
})
const bbox = resvg.innerBBox()

t.is(bbox.width, 200)
t.is(bbox.height, 101) // Here the expected value is actually 100, and the calculation of the bbox needs to be fixed.
t.is(bbox.x, 50)
t.is(bbox.y, 60)
})

// throws
test('should throw because invalid SVG (blank string)', (t) => {
const error = t.throws(
Expand Down
Binary file added example/bbox-out.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions example/bbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const { promises } = require('fs')
const { join } = require('path')
const { performance } = require('perf_hooks')

const { Resvg } = require('../index')

async function main() {
const svg = await promises.readFile(join(__dirname, './bbox.svg'))

const opts = {
fitTo: {
mode: 'width',
value: 500,
},
font: {
loadSystemFonts: false,
},
}

const t = performance.now()
const resvg = new Resvg(svg, opts)
const bbox = resvg.innerBBox()
const pngData = resvg.render()
const pngBuffer = pngData.asPng()

console.info('SVG BBox:', `${bbox.width} x ${bbox.height}`)
console.info('Original SVG Size:', `${resvg.width} x ${resvg.height}`)
console.info('Output PNG Size :', `${pngData.width} x ${pngData.height}`)
console.info('✨ Done in', performance.now() - t, 'ms')

await promises.writeFile(join(__dirname, './bbox-out.png'), pngBuffer)
}

main()
6 changes: 6 additions & 0 deletions example/bbox.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion example/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async function main() {
},
// imageRendering: 1,
// shapeRendering: 2,
logLevel: 'debug',
logLevel: 'debug', // Default Value: error
}

const t = performance.now()
Expand Down
12 changes: 12 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ export type ResvgRenderOptions = {
}
logLevel?: 'off' | 'error' | 'warn' | 'info' | 'debug' | 'trace'
}
export class BBox {
x: number
y: number
width: number
height: number
}

export function renderAsync(
svg: string | Buffer,
Expand All @@ -50,6 +56,12 @@ export class Resvg {
constructor(svg: Buffer | string, options?: ResvgRenderOptions | null)
toString(): string
render(): RenderedImage
/**
* Calculate a maximum bounding box of all visible elements in this SVG.
*
* Note: path bounding box are approx values.
*/
innerBBox(): BBox

/** Get the SVG width */
get width(): number
Expand Down
12 changes: 12 additions & 0 deletions js-binding.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,24 @@
/* auto-generated by NAPI-RS */

export function renderAsync(svg: string | Buffer, options?: string | undefined | null, signal?: AbortSignal | undefined | null): Promise<RenderedImage>
export class BBox {
x: number
y: number
width: number
height: number
}
export class Resvg {
constructor(svg: string | Buffer, options?: string | undefined | null)
/** Renders an SVG in Node.js */
render(): RenderedImage
/** Output usvg-simplified SVG string */
toString(): string
/**
* Calculate a maximum bounding box of all visible elements in this SVG.
*
* Note: path bounding box are approx values.
*/
innerBBox(): BBox
/** Get the SVG width */
get width(): number
/** Get the SVG height */
Expand Down
3 changes: 2 additions & 1 deletion js-binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,9 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}

const { Resvg, RenderedImage, renderAsync } = nativeBinding
const { BBox, Resvg, RenderedImage, renderAsync } = nativeBinding

module.exports.BBox = BBox
module.exports.Resvg = Resvg
module.exports.RenderedImage = RenderedImage
module.exports.renderAsync = renderAsync
52 changes: 42 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ extern "C" {
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[cfg_attr(not(target_arch = "wasm32"), napi)]
#[derive(Debug)]
pub struct BBox {
pub x: f64,
Expand Down Expand Up @@ -141,8 +142,8 @@ impl Resvg {
Ok(Resvg { tree, js_options })
}

/// Renders an SVG in Node.js
#[napi]
/// Renders an SVG in Node.js
pub fn render(&self) -> Result<RenderedImage, NapiError> {
Ok(self.render_inner()?)
}
Expand All @@ -153,10 +154,10 @@ impl Resvg {
self.tree.to_string(&usvg::XmlOptions::default())
}

/// Calculate a maximum bounding box of all visible elements in this
/// SVG.
#[napi(js_name = innerBBox)]
/// Calculate a maximum bounding box of all visible elements in this SVG.
///
/// Note: path bounding box are approx. values
/// 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(
Expand Down Expand Up @@ -220,12 +221,6 @@ impl Resvg {
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
impl Resvg {
#[wasm_bindgen(js_name = toString)]
/// Output usvg-simplified SVG string
pub fn to_string(&self) -> String {
self.tree.to_string(&usvg::XmlOptions::default())
}

#[wasm_bindgen(constructor)]
pub fn new(svg: IStringOrBuffer, options: Option<String>) -> Result<Resvg, JsValue> {
let js_options: JsOptions = options
Expand All @@ -251,6 +246,43 @@ impl Resvg {
pub fn render(&self) -> Result<RenderedImage, JsValue> {
Ok(self.render_inner()?)
}

#[wasm_bindgen(js_name = toString)]
/// Output usvg-simplified SVG string
pub fn to_string(&self) -> String {
self.tree.to_string(&usvg::XmlOptions::default())
}

#[wasm_bindgen(js_name = innerBBox)]
/// 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,
}
}
}

impl Resvg {
Expand Down
20 changes: 18 additions & 2 deletions wasm/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
// Generated by dts-bundle-generator v6.9.0
// Generated by dts-bundle-generator v6.5.0

declare class BBox {
free(): void;
/**
*/
height: number;
/**
*/
width: number;
/**
*/
x: number;
/**
*/
y: number;
}
declare class RenderedImage {
free(): void;
/**
Expand Down Expand Up @@ -59,8 +74,9 @@ export declare const initWasm: (module_or_path: Promise<InitInput> | InitInput)
export declare const Resvg: {
new (svg: Uint8Array | string, options?: ResvgRenderOptions | undefined): {
free(): void;
toString(): string;
render(): RenderedImage;
toString(): string;
innerBBox(): BBox;
readonly height: number;
readonly width: number;
};
Expand Down
6 changes: 3 additions & 3 deletions wasm/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>resvg-js playground</title>
<link rel="preload" as="fetch" type="application/wasm" href="./index_bg.wasm" crossorigin>
<script src="./index.min.js"></script>
<link rel="preload" as="fetch" type="application/wasm" href="https://unpkg.com/@resvg/resvg-wasm@2.0.0-beta.0/index_bg.wasm" crossorigin>
<script src="https://unpkg.com/@resvg/resvg-wasm@2.0.0-beta.0/index.min.js"></script>
<style>
html {
box-sizing: border-box;
Expand Down Expand Up @@ -149,7 +149,7 @@

<script>
(async function () {
await resvg.initWasm(fetch('./index_bg.wasm'))
await resvg.initWasm(fetch('https://unpkg.com/@resvg/resvg-wasm@2.0.0-beta.0/index_bg.wasm'))
const output = document.getElementById('output')
const svgElement = document.querySelector('#input-svg')
const inputSVG = svgElement.value.trim()
Expand Down
Loading

0 comments on commit 9933961

Please sign in to comment.