Skip to content

Commit 7fa7341

Browse files
committed
feat: the padding of cropByBBox() no longer scales with image dimensions
Previously, padding scaled proportionally with changes to the output image dimensions. Now, regardless of the width/height values set in fitTo, the padding remains constant. When the padding exceeds half of the set dimensions, the output image will become transparent.
1 parent 400c7e1 commit 7fa7341

File tree

7 files changed

+306
-29
lines changed

7 files changed

+306
-29
lines changed

__test__/index.spec.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,95 @@ test('should get svg bbox(rect) and cropByBBox', async (t) => {
579579
}
580580
})
581581

582+
test('should cropByBBox only with padding', async (t) => {
583+
const bbox_width = 300
584+
const padding = 10
585+
let expectedWidth = bbox_width - padding * 2
586+
expectedWidth = expectedWidth < 0 ? 0 : expectedWidth
587+
const origin_svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 1024 1024">
588+
<rect width="300" height="300" x="100" y="150" fill="green"/></svg>`
589+
const extened_svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${bbox_width}" height="${bbox_width}" viewBox="0 0 ${bbox_width} ${bbox_width}">
590+
<rect width="${expectedWidth}" height="${expectedWidth}" x="${padding}" y="${padding}" fill="green"/></svg>`
591+
console.info('extened_svg \n', extened_svg)
592+
593+
const resvg = new Resvg(origin_svg)
594+
const bbox = resvg.getBBox()
595+
t.not(bbox, undefined)
596+
597+
let result: jimp
598+
if (bbox) {
599+
resvg.cropByBBox(bbox, padding) // Add padding
600+
const pngData = resvg.render()
601+
const pngBuffer = pngData.asPng()
602+
result = await jimp.read(pngBuffer)
603+
604+
t.is(bbox.width, bbox_width)
605+
t.is(bbox.height, bbox_width)
606+
607+
// Must be have Alpha
608+
t.is(result.hasAlpha(), true)
609+
t.is(result.getWidth(), bbox_width)
610+
t.is(result.getHeight(), bbox_width)
611+
}
612+
613+
const extened_render = new Resvg(extened_svg)
614+
const extened_pngData = extened_render.render()
615+
const extened_pngBuffer = extened_pngData.asPng()
616+
const extened_result = await jimp.read(extened_pngBuffer)
617+
618+
t.is(extened_result.getWidth(), bbox_width)
619+
t.is(extened_result.getHeight(), bbox_width)
620+
// Compare the two images
621+
t.is(jimp.diff(result, extened_result, 0.01).percent, 0) // 0 means similar, 1 means not similar
622+
})
623+
624+
test('should cropByBBox with fitTo and padding', async (t) => {
625+
const w = 500
626+
const padding = 50
627+
let expectedWidth = w - padding * 2
628+
expectedWidth = expectedWidth < 0 ? 0 : expectedWidth
629+
const origin_svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 1024 1024">
630+
<rect width="300" height="300" x="100" y="150" fill="red"/></svg>`
631+
const extened_svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${w}" height="${w}" viewBox="0 0 ${w} ${w}">
632+
<rect width="${expectedWidth}" height="${expectedWidth}" x="${padding}" y="${padding}" fill="red"/></svg>`
633+
console.info('extened_svg \n', extened_svg)
634+
635+
const resvg = new Resvg(origin_svg, {
636+
fitTo: {
637+
mode: 'width', // TODO: test mode 'zoom'
638+
value: w,
639+
},
640+
})
641+
const bbox = resvg.getBBox()
642+
t.not(bbox, undefined)
643+
644+
let result: jimp
645+
if (bbox) {
646+
resvg.cropByBBox(bbox, padding) // Add padding
647+
const pngData = resvg.render()
648+
const pngBuffer = pngData.asPng()
649+
result = await jimp.read(pngBuffer)
650+
651+
t.is(bbox.width, 300)
652+
t.is(bbox.height, 300)
653+
654+
// Must be have Alpha
655+
t.is(result.hasAlpha(), true)
656+
t.is(result.getWidth(), w)
657+
t.is(result.getHeight(), w)
658+
}
659+
660+
const extened_render = new Resvg(extened_svg)
661+
const extened_pngData = extened_render.render()
662+
const extened_pngBuffer = extened_pngData.asPng()
663+
const extened_result = await jimp.read(extened_pngBuffer)
664+
665+
t.is(extened_result.getWidth(), w)
666+
t.is(extened_result.getHeight(), w)
667+
// Compare the two images
668+
t.is(jimp.diff(result, extened_result, 0.01).percent, 0) // 0 means similar, 1 means not similar
669+
})
670+
582671
test('should return undefined if bbox is invalid', (t) => {
583672
const svg = `<svg width="300px" height="300px" viewBox="0 0 300 300" version="1.1" xmlns="http://www.w3.org/2000/svg"></svg>`
584673
const resvg = new Resvg(svg)

example/bbox-out.png

6 Bytes
Loading

src/lib.rs

Lines changed: 208 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ impl Resvg {
244244
if !bbox.width.is_finite() || !bbox.height.is_finite() {
245245
return;
246246
}
247-
let padding = padding.unwrap_or(0.0) as f32;
247+
let pixel_padding = padding.unwrap_or(0.0) as f32;
248248
let square = square.unwrap_or(false);
249249

250250
let mut x = bbox.x as f32;
@@ -265,15 +265,109 @@ impl Resvg {
265265
height = max_dimension;
266266
}
267267

268-
// Apply padding
269-
let final_x = x - padding;
270-
let final_y = y - padding;
271-
let final_width = width + (padding * 2.0);
272-
let final_height = height + (padding * 2.0);
273-
274-
self.tree.view_box.rect =
275-
usvg::NonZeroRect::from_xywh(final_x, final_y, final_width, final_height).unwrap();
276-
self.tree.size = usvg::Size::from_wh(final_width, final_height).unwrap();
268+
// Get current tree size before any modifications
269+
let current_tree_size = self.tree.size;
270+
271+
// Check if fitTo is being used (not Original)
272+
match &self.js_options.fit_to {
273+
options::FitToDef::Original => {
274+
// Case 1: No fitTo - crop to bbox size, then center and scale content to fit (bbox_size - padding*2)
275+
// Calculate the content size (bbox size minus padding), clamp to 0 for negative values
276+
let content_width = (width - pixel_padding * 2.0).max(0.0);
277+
let content_height = (height - pixel_padding * 2.0).max(0.0);
278+
279+
// The final SVG size should be the bbox size
280+
let final_svg_width = width;
281+
let final_svg_height = height;
282+
283+
// Handle edge case: if content size is 0, create viewBox outside visible area for transparent result
284+
if content_width == 0.0 || content_height == 0.0 {
285+
// Create a viewBox that's positioned outside the visible area to produce transparent result
286+
self.tree.view_box.rect =
287+
usvg::NonZeroRect::from_xywh(x + width, y + height, 1.0, 1.0).unwrap();
288+
self.tree.size =
289+
usvg::Size::from_wh(final_svg_width, final_svg_height).unwrap();
290+
} else {
291+
// Calculate the scale factor to fit the content within the padding
292+
let scale_x = content_width / width;
293+
let scale_y = content_height / height;
294+
let scale = scale_x.min(scale_y);
295+
296+
// Create a viewBox that shows the original bbox area, scaled and centered
297+
let bbox_center_x = x + width / 2.0;
298+
let bbox_center_y = y + height / 2.0;
299+
300+
let viewbox_width = width / scale;
301+
let viewbox_height = height / scale;
302+
303+
let viewbox_x = bbox_center_x - viewbox_width / 2.0;
304+
let viewbox_y = bbox_center_y - viewbox_height / 2.0;
305+
306+
self.tree.view_box.rect = usvg::NonZeroRect::from_xywh(
307+
viewbox_x,
308+
viewbox_y,
309+
viewbox_width,
310+
viewbox_height,
311+
)
312+
.unwrap();
313+
self.tree.size =
314+
usvg::Size::from_wh(final_svg_width, final_svg_height).unwrap();
315+
}
316+
}
317+
_ => {
318+
// Case 2: fitTo is used - padding is absolute pixels within the target size
319+
match self.js_options.fit_to.fit_to(current_tree_size) {
320+
Ok((target_width, target_height, _)) => {
321+
// Calculate the content size in the final render (target size minus padding), clamp to 0 for negative values
322+
let content_target_width =
323+
(target_width as f32 - pixel_padding * 2.0).max(0.0);
324+
let content_target_height =
325+
(target_height as f32 - pixel_padding * 2.0).max(0.0);
326+
327+
// Handle edge case: if content size is 0, create viewBox outside visible area for transparent result
328+
if content_target_width == 0.0 || content_target_height == 0.0 {
329+
// Create a viewBox that's positioned outside the visible area to produce transparent result
330+
self.tree.view_box.rect =
331+
usvg::NonZeroRect::from_xywh(x + width, y + height, 1.0, 1.0)
332+
.unwrap();
333+
self.tree.size =
334+
usvg::Size::from_wh(target_width as f32, target_height as f32)
335+
.unwrap();
336+
} else {
337+
// Calculate what SVG size we need to achieve the target final size after fitTo
338+
let required_svg_width =
339+
width * target_width as f32 / content_target_width;
340+
let required_svg_height =
341+
height * target_height as f32 / content_target_height;
342+
343+
// Create a viewBox that centers the original bbox within the required SVG size
344+
let bbox_center_x = x + width / 2.0;
345+
let bbox_center_y = y + height / 2.0;
346+
347+
let viewbox_x = bbox_center_x - required_svg_width / 2.0;
348+
let viewbox_y = bbox_center_y - required_svg_height / 2.0;
349+
350+
self.tree.view_box.rect = usvg::NonZeroRect::from_xywh(
351+
viewbox_x,
352+
viewbox_y,
353+
required_svg_width,
354+
required_svg_height,
355+
)
356+
.unwrap();
357+
self.tree.size =
358+
usvg::Size::from_wh(required_svg_width, required_svg_height)
359+
.unwrap();
360+
}
361+
}
362+
Err(_) => {
363+
// Fallback to no padding
364+
self.tree.view_box.rect =
365+
usvg::NonZeroRect::from_xywh(x, y, width, height).unwrap();
366+
self.tree.size = usvg::Size::from_wh(width, height).unwrap();
367+
}
368+
}
369+
}
370+
}
277371
}
278372

279373
#[napi]
@@ -412,7 +506,7 @@ impl Resvg {
412506
if !bbox.width.is_finite() || !bbox.height.is_finite() {
413507
return;
414508
}
415-
let padding = padding.unwrap_or(0.0) as f32;
509+
let pixel_padding = padding.unwrap_or(0.0) as f32;
416510
let square = square.unwrap_or(false);
417511

418512
let mut x = bbox.x as f32;
@@ -433,15 +527,109 @@ impl Resvg {
433527
height = max_dimension;
434528
}
435529

436-
// Apply padding
437-
let final_x = x - padding;
438-
let final_y = y - padding;
439-
let final_width = width + (padding * 2.0);
440-
let final_height = height + (padding * 2.0);
441-
442-
self.tree.view_box.rect =
443-
usvg::NonZeroRect::from_xywh(final_x, final_y, final_width, final_height).unwrap();
444-
self.tree.size = usvg::Size::from_wh(final_width, final_height).unwrap();
530+
// Get current tree size before any modifications
531+
let current_tree_size = self.tree.size;
532+
533+
// Check if fitTo is being used (not Original)
534+
match &self.js_options.fit_to {
535+
options::FitToDef::Original => {
536+
// Case 1: No fitTo - crop to bbox size, then center and scale content to fit (bbox_size - padding*2)
537+
// Calculate the content size (bbox size minus padding), clamp to 0 for negative values
538+
let content_width = (width - pixel_padding * 2.0).max(0.0);
539+
let content_height = (height - pixel_padding * 2.0).max(0.0);
540+
541+
// The final SVG size should be the bbox size
542+
let final_svg_width = width;
543+
let final_svg_height = height;
544+
545+
// Handle edge case: if content size is 0, create viewBox outside visible area for transparent result
546+
if content_width == 0.0 || content_height == 0.0 {
547+
// Create a viewBox that's positioned outside the visible area to produce transparent result
548+
self.tree.view_box.rect =
549+
usvg::NonZeroRect::from_xywh(x + width, y + height, 1.0, 1.0).unwrap();
550+
self.tree.size =
551+
usvg::Size::from_wh(final_svg_width, final_svg_height).unwrap();
552+
} else {
553+
// Calculate the scale factor to fit the content within the padding
554+
let scale_x = content_width / width;
555+
let scale_y = content_height / height;
556+
let scale = scale_x.min(scale_y);
557+
558+
// Create a viewBox that shows the original bbox area, scaled and centered
559+
let bbox_center_x = x + width / 2.0;
560+
let bbox_center_y = y + height / 2.0;
561+
562+
let viewbox_width = width / scale;
563+
let viewbox_height = height / scale;
564+
565+
let viewbox_x = bbox_center_x - viewbox_width / 2.0;
566+
let viewbox_y = bbox_center_y - viewbox_height / 2.0;
567+
568+
self.tree.view_box.rect = usvg::NonZeroRect::from_xywh(
569+
viewbox_x,
570+
viewbox_y,
571+
viewbox_width,
572+
viewbox_height,
573+
)
574+
.unwrap();
575+
self.tree.size =
576+
usvg::Size::from_wh(final_svg_width, final_svg_height).unwrap();
577+
}
578+
}
579+
_ => {
580+
// Case 2: fitTo is used - padding is absolute pixels within the target size
581+
match self.js_options.fit_to.fit_to(current_tree_size) {
582+
Ok((target_width, target_height, _)) => {
583+
// Calculate the content size in the final render (target size minus padding), clamp to 0 for negative values
584+
let content_target_width =
585+
(target_width as f32 - pixel_padding * 2.0).max(0.0);
586+
let content_target_height =
587+
(target_height as f32 - pixel_padding * 2.0).max(0.0);
588+
589+
// Handle edge case: if content size is 0, create viewBox outside visible area for transparent result
590+
if content_target_width == 0.0 || content_target_height == 0.0 {
591+
// Create a viewBox that's positioned outside the visible area to produce transparent result
592+
self.tree.view_box.rect =
593+
usvg::NonZeroRect::from_xywh(x + width, y + height, 1.0, 1.0)
594+
.unwrap();
595+
self.tree.size =
596+
usvg::Size::from_wh(target_width as f32, target_height as f32)
597+
.unwrap();
598+
} else {
599+
// Calculate what SVG size we need to achieve the target final size after fitTo
600+
let required_svg_width =
601+
width * target_width as f32 / content_target_width;
602+
let required_svg_height =
603+
height * target_height as f32 / content_target_height;
604+
605+
// Create a viewBox that centers the original bbox within the required SVG size
606+
let bbox_center_x = x + width / 2.0;
607+
let bbox_center_y = y + height / 2.0;
608+
609+
let viewbox_x = bbox_center_x - required_svg_width / 2.0;
610+
let viewbox_y = bbox_center_y - required_svg_height / 2.0;
611+
612+
self.tree.view_box.rect = usvg::NonZeroRect::from_xywh(
613+
viewbox_x,
614+
viewbox_y,
615+
required_svg_width,
616+
required_svg_height,
617+
)
618+
.unwrap();
619+
self.tree.size =
620+
usvg::Size::from_wh(required_svg_width, required_svg_height)
621+
.unwrap();
622+
}
623+
}
624+
Err(_) => {
625+
// Fallback to no padding
626+
self.tree.view_box.rect =
627+
usvg::NonZeroRect::from_xywh(x, y, width, height).unwrap();
628+
self.tree.size = usvg::Size::from_wh(width, height).unwrap();
629+
}
630+
}
631+
}
632+
}
445633
}
446634

447635
#[wasm_bindgen(js_name = imagesToResolve)]

wasm/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,10 +466,6 @@ async function __wbg_load(module2, imports) {
466466
function __wbg_get_imports() {
467467
const imports = {};
468468
imports.wbg = {};
469-
imports.wbg.__wbg_new_28c511d9baebfa89 = function(arg0, arg1) {
470-
const ret = new Error(getStringFromWasm0(arg0, arg1));
471-
return addHeapObject(ret);
472-
};
473469
imports.wbg.__wbindgen_memory = function() {
474470
const ret = wasm.memory;
475471
return addHeapObject(ret);
@@ -489,6 +485,10 @@ function __wbg_get_imports() {
489485
const ret = new Uint8Array(getObject(arg0));
490486
return addHeapObject(ret);
491487
};
488+
imports.wbg.__wbg_new_28c511d9baebfa89 = function(arg0, arg1) {
489+
const ret = new Error(getStringFromWasm0(arg0, arg1));
490+
return addHeapObject(ret);
491+
};
492492
imports.wbg.__wbg_values_839f3396d5aac002 = function(arg0) {
493493
const ret = getObject(arg0).values();
494494
return addHeapObject(ret);

0 commit comments

Comments
 (0)