Complete API reference for using SvgVisualBBox.js in web browsers.
- Installation
- Quick Start
- Core Functions
- FBF.SVG support
- Options Reference
- Return Types
- Error Handling
- Performance Tips
- Examples
- Coordinate Systems
The right entry point is picked automatically by package.json exports — see
the Choose your runtime cheatsheet in the
README for the full per-scenario table. Quick reference:
<!-- Production: minified UMD, ~29 KB -->
<script src="https://unpkg.com/svg-bbox@latest/SvgVisualBBox.min.js"></script>
<!-- Or via jsDelivr (same file, second mirror) -->
<script src="https://cdn.jsdelivr.net/npm/svg-bbox@latest/SvgVisualBBox.min.js"></script>
<!-- Debug: unminified UMD, ~111 KB -->
<script src="https://unpkg.com/svg-bbox@latest/SvgVisualBBox.js"></script>After loading, every API documented on this page is available under
window.SvgVisualBBox.*.
npm install svg-bbox
bun add svg-bbox # alternatively// Browser bundler (Webpack, Vite, esbuild, Rollup, Parcel) — full library
import {
extractFbfFrame,
getSvgElementVisualBBoxTwoPassAggressive
} from 'svg-bbox';
// Bun (CJS or ESM) — full library
const { extractFbfFrame } = require('svg-bbox');
// Node.js (CJS or ESM) — FBF helpers + actionable stubs for DOM-bound fns
const { extractFbfFrame } = require('svg-bbox'); // CJS
import { extractFbfFrame } from 'svg-bbox'; // ESM (named or default)
// Slim FBF-only entry — works in every runtime, smallest dependency surface
const { extractFbfFrame } = require('svg-bbox/fbf'); // CJS
import { extractFbfFrame } from 'svg-bbox/fbf'; // ESMIn Node, the bbox functions (getSvgElementVisualBBoxTwoPassAggressive,
getSvgElementsUnionVisualBBox, showTrueBBoxBorder, …) need a real DOM.
Calling them directly throws an actionable error pointing at the
Puppeteer-injection pattern — see the
Pattern C
example in the README, or any of the shipped CLI tools.
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/svg-bbox@latest/SvgVisualBBox.min.js"></script>
</head>
<body>
<svg id="mySvg" viewBox="0 0 200 100" width="400">
<text
id="greeting"
x="100"
y="50"
text-anchor="middle"
font-size="24"
fill="black"
>
Hello SVG!
</text>
</svg>
<script>
(async () => {
// Wait for fonts to load
await SvgVisualBBox.waitForDocumentFonts();
// Get accurate bounding box
const bbox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive(
'#greeting'
);
console.log('BBox:', bbox);
// Output: {x: 45.2, y: 32.1, width: 109.6, height: 25.8}
// Show visual debugging border
const border = await SvgVisualBBox.showTrueBBoxBorder('#greeting');
// Remove border after 3 seconds
setTimeout(() => border.remove(), 3000);
})();
</script>
</body>
</html>Waits for all web fonts in the document to finish loading before measuring text.
await SvgVisualBBox.waitForDocumentFonts(document, timeoutMs);document(Document) - The document object. Default:window.documenttimeoutMs(number) - Maximum time to wait in milliseconds. Default:5000
Promise<void>- Resolves when fonts are ready or timeout is reached
// Wait for fonts with default 5-second timeout
await SvgVisualBBox.waitForDocumentFonts();
// Wait with custom timeout (10 seconds)
await SvgVisualBBox.waitForDocumentFonts(document, 10000);Text bounding boxes depend on loaded fonts. If you measure before fonts load, you'll get incorrect results. Always call this before measuring text elements.
Computes the visual bounding box for an SVG element, including stroke width, filters, shadows, and all visual effects.
const bbox = await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive(
element,
options
);element(string | Element) - CSS selector string or DOM elementoptions(Object) - Configuration options (see Options Reference)
Promise<BBox | null>- Bounding box object ornullif element is invisible
{
x: number, // Left edge in SVG user units
y: number, // Top edge in SVG user units
width: number, // Width in SVG user units
height: number // Height in SVG user units
}// Basic usage
const bbox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive('#myElement');
// With options
const bbox = await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive(
document.getElementById('myPath'),
{
mode: 'unclipped', // Ignore viewBox clipping
coarseFactor: 3, // Faster coarse pass
fineFactor: 24, // More accurate fine pass
useLayoutScale: true // Use element's actual rendered size
}
);Measure text with custom fonts:
await SvgVisualBBox.waitForDocumentFonts();
const textBBox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive('#myText');Measure element with filters:
const blurredBBox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive(
'#blurredElement'
);
// Includes filter effects like blur, shadow, glowMeasure transformed element:
const rotatedBBox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive(
'#rotatedElement'
);
// Accounts for rotation, scale, translateComputes the union bounding box for multiple SVG elements. The union bbox is the smallest rectangle that contains all specified elements.
const bbox = await SvgVisualBBox.getSvgElementsUnionVisualBBox(
targets,
options
);targets(Array<string | Element>) - Array of CSS selectors, element IDs, or DOM elements. Must be non-empty.options(Object) - Configuration options (same asgetSvgElementVisualBBoxTwoPassAggressive)
Promise<BBox | null>- Union bounding box object ornullif all elements are invisible
Compute union of multiple elements:
const unionBBox = await SvgVisualBBox.getSvgElementsUnionVisualBBox([
'#header',
'#logo',
'#title'
]);
console.log('Union bbox:', unionBBox);
// {x: 10, y: 5, width: 380, height: 95}With options:
const unionBBox = await SvgVisualBBox.getSvgElementsUnionVisualBBox(
['#elem1', '#elem2', '#elem3'],
{
mode: 'unclipped',
coarseFactor: 3,
fineFactor: 24
}
);// ❌ Throws error if targets is empty
await SvgVisualBBox.getSvgElementsUnionVisualBBox([]);
// ❌ Throws error if elements are in different SVG roots
await SvgVisualBBox.getSvgElementsUnionVisualBBox([
'#elemInSvg1',
'#elemInSvg2'
]);- Compute bounding box for a group of elements
- Determine the extent of a selection
- Calculate the area needed to contain multiple objects
Computes two bounding boxes:
- visible - Content inside the current viewBox (respects clipping)
- full - Entire drawing extent (ignores viewBox clipping)
const result = await SvgVisualBBox.getSvgElementVisibleAndFullBBoxes(
svgElement,
options
);svgElement(Element) - The<svg>element (must be an Element, not a selector)options(Object) - Configuration options
{
visible: BBox | null, // What's visible inside viewBox
full: BBox | null // Complete drawing extent
}const svg = document.getElementById('mySvg');
const boxes = await SvgVisualBBox.getSvgElementVisibleAndFullBBoxes(svg);
console.log('Visible area:', boxes.visible);
// {x: 0, y: 0, width: 200, height: 100}
console.log('Full drawing:', boxes.full);
// {x: -50, y: -20, width: 300, height: 140}Fix missing viewBox:
const { full } = await SvgVisualBBox.getSvgElementVisibleAndFullBBoxes(svg);
if (full) {
svg.setAttribute(
'viewBox',
`${full.x} ${full.y} ${full.width} ${full.height}`
);
}Decide render mode:
const { visible, full } =
await SvgVisualBBox.getSvgElementVisibleAndFullBBoxes(svg);
if (visible && full) {
const hasContentOutsideViewBox =
full.width > visible.width || full.height > visible.height;
if (hasContentOutsideViewBox) {
console.log('⚠️ Some content is clipped by viewBox');
}
}Computes the viewBox expansion needed to include all content that extends beyond the current viewBox. Useful for determining how much to expand the viewBox to show the complete drawing.
const expansion = await SvgVisualBBox.getSvgRootViewBoxExpansionForFullDrawing(
svgRootOrId,
options
);svgRootOrId(string | SVGSVGElement) - The root<svg>element or its ID. Must have a valid viewBox attribute.options(Object) - Configuration options (same asgetSvgElementVisualBBoxTwoPassAggressive)
{
top: number, // Expansion needed above current viewBox
right: number, // Expansion needed to the right
bottom: number, // Expansion needed below
left: number, // Expansion needed to the left
fullBBox: BBox // The full drawing bounding box
}Check if content extends beyond viewBox:
const svg = document.getElementById('mySvg');
const expansion =
await SvgVisualBBox.getSvgRootViewBoxExpansionForFullDrawing(svg);
if (
expansion.top > 0 ||
expansion.right > 0 ||
expansion.bottom > 0 ||
expansion.left > 0
) {
console.log('Content extends beyond viewBox by:', expansion);
}Expand viewBox to fit all content:
const svg = document.getElementById('mySvg');
const expansion =
await SvgVisualBBox.getSvgRootViewBoxExpansionForFullDrawing(svg);
const vb = svg.viewBox.baseVal;
// Apply expansion
svg.setAttribute(
'viewBox',
`${vb.x - expansion.left} ${vb.y - expansion.top} ` +
`${vb.width + expansion.left + expansion.right} ` +
`${vb.height + expansion.top + expansion.bottom}`
);// ❌ Throws error if not an SVG element
await SvgVisualBBox.getSvgRootViewBoxExpansionForFullDrawing('#myDiv');
// ❌ Throws error if SVG has no viewBox
await SvgVisualBBox.getSvgRootViewBoxExpansionForFullDrawing(
'#svgWithoutViewBox'
);- Detect clipped content in SVG files
- Auto-expand viewBox to show complete drawing
- Validate SVG viewBox coverage
Displays a visual debugging border around an element's true bounding box. Perfect for debugging layout issues or verifying measurements.
const result = await SvgVisualBBox.showTrueBBoxBorder(selector, options);selector(string) - CSS selector for the target elementoptions(Object) - Border styling options
{
theme: 'light' | 'dark' | 'auto', // Color theme (default: 'auto')
borderColor: string, // Custom border color (CSS color)
borderWidth: string, // Border width (default: '2px')
borderStyle: string, // Border style (default: 'dashed')
padding: number, // Extra padding in SVG units
opacity: number // Border opacity 0-1 (default: 1)
}{
remove: () => void, // Function to remove the border
element: HTMLElement // The border overlay element
}Auto-detected theme:
const result = await SvgVisualBBox.showTrueBBoxBorder('#myText');
// Auto-detects system dark/light mode
// Remove after 5 seconds
setTimeout(() => result.remove(), 5000);Force dark theme:
const result = await SvgVisualBBox.showTrueBBoxBorder('#myElement', {
theme: 'dark' // Force dark border for light backgrounds
});Custom styling:
const result = await SvgVisualBBox.showTrueBBoxBorder('#myPath', {
borderColor: 'red',
borderWidth: '3px',
borderStyle: 'solid',
padding: 5,
opacity: 0.8
});Debugging multiple elements:
const borders = [];
for (const id of ['elem1', 'elem2', 'elem3']) {
const border = await SvgVisualBBox.showTrueBBoxBorder(`#${id}`, {
theme: 'dark'
});
borders.push(border);
}
// Remove all borders
borders.forEach((b) => b.remove());- ✅ Auto-detects system dark/light theme
- ✅ Works with inline SVG,
<object>,<iframe>, sprites - ✅ Non-intrusive overlay (doesn't modify SVG)
- ✅ Follows SVG on scroll/resize
- ✅ Easy cleanup with
remove()
Reframes the SVG's viewBox to focus on specific objects, with options for aspect ratio and margins.
await SvgVisualBBox.setViewBoxOnObjects(svgElement, objectIds, options);svgElement(string | Element) - SVG element or selectorobjectIds(string | string[]) - Object ID(s) to focus onoptions(Object) - Reframing options
{
aspect: 'stretch' | 'meet' | 'slice', // Legacy option for fitting (default: 'stretch')
aspectRatioMode: 'meet' | 'slice', // preserveAspectRatio mode (default: 'meet')
align: string, // preserveAspectRatio alignment (default: 'xMidYMid')
margin: number, // Margin around objects in SVG units (default: 0)
visibility: 'unchanged' | 'show' | 'hide', // How to handle non-target visibility (default: 'unchanged')
visibilityList: Object | null, // Custom visibility map {id: 'visible'|'hidden'} (default: null)
saveVisibilityList: boolean, // Return visibility states for restoration (default: false)
dryRun: boolean, // Compute but don't apply changes (default: false)
bboxOptions: Object // Options passed to bbox computation (default: {})
}{
newViewBox: string, // The new viewBox value
oldViewBox: string, // The previous viewBox value
bbox: BBox, // Computed bounding box
visibilityList: Object | null, // Saved visibility states (if saveVisibilityList: true)
restore: () => void // Function to restore original state
}Focus on a single element:
await SvgVisualBBox.setViewBoxOnObjects('svg', 'importantElement', {
aspectRatioMode: 'meet',
margin: 10
});Focus on multiple elements:
await SvgVisualBBox.setViewBoxOnObjects('svg', ['elem1', 'elem2', 'elem3'], {
aspectRatioMode: 'meet',
align: 'xMidYMid',
margin: 20
});Dry run to preview changes:
const result = await SvgVisualBBox.setViewBoxOnObjects(
'#mySvg',
'targetElement',
{
margin: 15,
dryRun: true
}
);
console.log('Would set viewBox to:', result.newViewBox);Hide other elements while focusing:
const result = await SvgVisualBBox.setViewBoxOnObjects(
'#mySvg',
'focusElement',
{
visibility: 'hide',
saveVisibilityList: true
}
);
// Later, restore original visibility
result.restore();svg-bbox natively understands the
FBF.SVG (Frame-By-Frame SVG) format
produced by svg2fbf: a single
self-contained SVG where every animation frame lives in <defs> as a
<g id="FRAME0000N"> and a single <use id="PROSKENION"> swaps its
xlink:href across the frames via a discrete-mode <animate>.
The same FBF helpers are exposed from every entry point — browser CDN,
browser bundler, Bun, Node CJS, Node ESM, and the svg-bbox/fbf subpath. They
are pure string manipulation (no DOM, no canvas, no fs), so they run in any
JavaScript runtime.
Pin a specific frame of an FBF.SVG and return a renderable SVG string ready for
any normal renderer (canvas, <img>, <object>, sharp, inkscape, sbb-svg2png,
…).
const result = extractFbfFrame(svgContent, frameNumber);svgContent(string, required) — the full FBF.SVG markup.frameNumber(number, required) — 1-based frame number to extract.
{
svg: string; // pinned-frame SVG, ready to render
frameId: string; // canonical FRAMEnnnnn id selected
frameNumber: number; // echoes the requested number
totalFrames: number; // count of frames in the source FBF.SVG
}Throws an Error with an actionable message when:
svgContentis not a non-empty stringframeNumberis not a positive integer- the markup is not FBF.SVG (no
<use id="PROSKENION">or no<g id="FRAMEnnnnn">defs) — error namessvg2fbf's URL frameNumberis out of range — error lists the available range, e.g."frame 99 not found. Available frames: 1..30 (30 total)."
const { extractFbfFrame } = require('svg-bbox');
const fs = require('fs');
const fbf = fs.readFileSync('animation.fbf.svg', 'utf8');
const { svg, frameId, frameNumber, totalFrames } = extractFbfFrame(fbf, 7);
fs.writeFileSync('frame-7.svg', svg);
console.log(`Pinned ${frameId} (${frameNumber}/${totalFrames})`);Inspect an SVG string and report whether it is FBF.SVG and what frames it contains. Useful for previewing batch operations or surfacing a "this isn't FBF.SVG" hint in a UI.
const desc = describeFbf(svgContent);
// {
// isFbf: boolean,
// frames: Array<{ id: string, number: number, padWidth: number }>,
// padWidth: number, // dominant zero-pad width used by the file
// minFrame: number, // lowest frame number (0 if none)
// maxFrame: number, // highest frame number (0 if none)
// hasProskenion: boolean
// }Boolean-only form of describeFbf() for callers that only need a yes/no:
if (isFbfSvg(content)) { … }Every shipped CLI tool that takes a single SVG input accepts --fbf-frame N,
which calls extractFbfFrame() internally before any other processing:
| Tool | What --fbf-frame N does |
|---|---|
sbb-getbbox |
Compute the bbox of frame N |
sbb-extract |
List / extract / export / rename objects as they appear at frame N |
sbb-fix-viewbox |
Repair the viewBox using frame N as the visual reference |
sbb-svg2png |
Render frame N (also accepts list / range) |
sbb-compare |
Pin a frame on either side of the diff (--fbf-frame-a/-b) |
sbb-test |
Test bbox accuracy on frame N |
sbb-chrome-getbbox |
Same as sbb-getbbox via Chrome's .getBBox() |
sbb-chrome-extract |
Same as sbb-extract via Chrome's .getBBox() |
sbb-inkscape-getbbox |
Same as sbb-getbbox via Inkscape (writes the pinned frame to a temp file) |
sbb-inkscape-extract |
Same as sbb-extract via Inkscape |
sbb-inkscape-text2path |
Convert text → path on frame N |
sbb-inkscape-svg2png |
Inkscape PNG render of frame N |
sbb-svg2png and sbb-compare extend the syntax to accept lists and ranges
(--fbf-frame 7,23,87, --fbf-frame 1-30); every other tool takes a single
frame number.
No size limit applies to FBF.SVG inputs. A multi-hour 60 fps vector
animation packs millions of frames into a single file, and svg-bbox reads it
without rejection (file-size checks in lib/security-utils.cjs are Infinity
by design).
'unclipped'- Measure entire element, ignoring viewBox clipping'clipped'- Only measure what's visible inside viewBox
coarseFactor(number) - Pixels per SVG unit for initial pass. Default:3- Lower = faster but less accurate
- Higher = slower but more precise
fineFactor(number) - Pixels per SVG unit for refinement. Default:24- Used to refine edges found in coarse pass
useLayoutScale(boolean) - Use element's actual rendered size. Default:truetrue: Respects CSS transforms and layoutfalse: Uses only SVG coordinate system
interface BBox {
x: number; // Left edge in SVG user units (GLOBAL coordinates)
y: number; // Top edge in SVG user units (GLOBAL coordinates)
width: number; // Width in SVG user units
height: number; // Height in SVG user units
}All bounding box functions return coordinates in the root SVG coordinate system after all transforms have been applied. If an element has transforms (translate, rotate, scale, etc.), the returned coordinates reflect the transformed position.
This is correct for visual rendering but causes issues when you need untransformed local coordinates (e.g., for text-to-path conversion).
See Coordinate Systems below for details and workarounds.
interface BorderResult {
remove: () => void; // Removes the border overlay
element: HTMLElement; // The overlay DOM element
}All async functions may reject with errors. Always use try-catch:
try {
const bbox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive('#myElement');
if (bbox) {
console.log('BBox:', bbox);
} else {
console.log('Element is invisible or fully clipped');
}
} catch (error) {
console.error('Failed to compute bbox:', error);
}Element not found:
// ❌ Throws error if element doesn't exist
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive('#nonexistent');
// ✅ Check element exists first
const element = document.querySelector('#myElement');
if (element) {
const bbox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive(element);
}Font not loaded:
// ❌ May give wrong bbox if fonts not loaded
const bbox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive('#textElement');
// ✅ Always wait for fonts first
await SvgVisualBBox.waitForDocumentFonts();
const bbox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive('#textElement');Don't query the DOM repeatedly:
// ❌ Slow - queries DOM multiple times
for (let i = 0; i < 100; i++) {
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive('#myElement');
}
// ✅ Fast - query once, reuse element
const element = document.getElementById('myElement');
for (let i = 0; i < 100; i++) {
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive(element);
}For faster (but less accurate) measurements:
const fastBBox = await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive(
'#myElement',
{
coarseFactor: 2, // Lower = faster
fineFactor: 12 // Lower = faster
}
);Wait for fonts once, not per element:
// ✅ Wait once
await SvgVisualBBox.waitForDocumentFonts();
// Then measure all text elements
const bboxes = await Promise.all(
['text1', 'text2', 'text3'].map((id) =>
SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive(`#${id}`)
)
);If you only care about visible content:
const visibleBBox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive('#myElement', {
mode: 'clipped' // Faster, ignores content outside viewBox
});<svg viewBox="0 0 400 100">
<text id="fancyText" x="200" y="50" font-family="CustomFont" font-size="32">
Fancy Text
</text>
</svg>
<script>
(async () => {
// Wait for CustomFont to load
await SvgVisualBBox.waitForDocumentFonts(document, 10000);
// Get accurate bbox
const bbox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive(
'#fancyText'
);
console.log('Text bbox:', bbox);
})();
</script>// Show borders for all elements with a specific class
const elements = document.querySelectorAll('.debug-me');
const borders = [];
for (const el of elements) {
const border = await SvgVisualBBox.showTrueBBoxBorder(`#${el.id}`, {
theme: 'dark',
borderColor: 'red'
});
borders.push(border);
}
// Remove all borders after 10 seconds
setTimeout(() => borders.forEach((b) => b.remove()), 10000);const svg = document.getElementById('brokenSvg');
// Get full drawing extent
const { full } = await SvgVisualBBox.getSvgElementVisibleAndFullBBoxes(svg);
if (full) {
// Fix viewBox
svg.setAttribute(
'viewBox',
`${full.x} ${full.y} ${full.width} ${full.height}`
);
// Set reasonable dimensions
svg.setAttribute('width', Math.round(full.width));
svg.setAttribute('height', Math.round(full.height));
console.log('✅ ViewBox fixed!');
}document.querySelectorAll('.zoomable').forEach((element) => {
element.addEventListener('click', async () => {
const svg = element.closest('svg');
await SvgVisualBBox.setViewBoxOnObjects(svg, element.id, {
aspectRatioMode: 'meet',
margin: 20
});
});
});const ids = ['icon1', 'icon2', 'icon3', 'icon4'];
// Measure all in parallel
const bboxes = await Promise.all(
ids.map((id) =>
SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive(`#${id}`)
)
);
// Create lookup table
const bboxMap = Object.fromEntries(ids.map((id, i) => [id, bboxes[i]]));
console.log('All bboxes:', bboxMap);const element = document.getElementById('complexElement');
// Standard getBBox() (often wrong)
const standardBBox = element.getBBox();
// Accurate visual bbox
const visualBBox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive(element);
console.log('Standard getBBox():', standardBBox);
console.log('Visual BBox:', visualBBox);
// Show how much getBBox() was off
const widthDiff = Math.abs(visualBBox.width - standardBBox.width);
const heightDiff = Math.abs(visualBBox.height - standardBBox.height);
console.log(`Width difference: ${widthDiff.toFixed(2)} units`);
console.log(`Height difference: ${heightDiff.toFixed(2)} units`);Global coordinates are measured in the root SVG coordinate system after all parent transforms have been applied.
<svg viewBox="0 0 200 200">
<g transform="translate(50, 30) rotate(45)">
<text id="myText" x="10" y="20">Hello</text>
</g>
</svg>For the text element above:
- Local coordinates:
x="10" y="20"(before transforms) - Global coordinates: After applying
translate(50, 30)androtate(45), the text appears at a different position in the root SVG space
All bbox functions return GLOBAL coordinates:
const bbox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive('#myText');
// bbox.x and bbox.y are in GLOBAL coordinates (after transforms)This is correct for:
- ✅ Visual rendering
- ✅ Positioning overlays
- ✅ Measuring screen space
- ✅ Layout calculations
This is WRONG for:
- ❌ Text-to-path conversion (causes double-transform bug)
- ❌ Operations requiring untransformed coordinates
- ❌ Extracting element's original position/size
When converting text to paths, you typically:
- Get text bbox
- Create path at bbox position
- Apply same transforms as original text
Problem: If you use global bbox coordinates, transforms apply twice:
// ❌ WRONG - Transforms apply twice!
const globalBBox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive('#text');
// globalBBox.x/y already include parent transforms
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', pathData);
path.setAttribute('transform', textElement.getAttribute('transform'));
// BUG: transform applies on top of already-transformed coordinatesSee GitHub Issue #1 for details.
Until a native getLocalBBox() function is added, use inverse matrix
transformation:
// Get Current Transformation Matrix (CTM)
const ctm = element.getCTM();
// Invert the matrix
function invertMatrix(m) {
const det = m.a * m.d - m.b * m.c;
if (Math.abs(det) < 1e-10) {
throw new Error('Matrix is not invertible');
}
return {
a: m.d / det,
b: -m.b / det,
c: -m.c / det,
d: m.a / det,
e: (m.c * m.f - m.d * m.e) / det,
f: (m.b * m.e - m.a * m.f) / det
};
}
// Get global bbox
const globalBBox =
await SvgVisualBBox.getSvgElementVisualBBoxTwoPassAggressive(element);
// Convert to local coordinates
const inv = invertMatrix(ctm);
const corners = [
{ x: globalBBox.x, y: globalBBox.y },
{ x: globalBBox.x + globalBBox.width, y: globalBBox.y },
{ x: globalBBox.x, y: globalBBox.y + globalBBox.height },
{
x: globalBBox.x + globalBBox.width,
y: globalBBox.y + globalBBox.height
}
];
const localCorners = corners.map((c) => ({
x: inv.a * c.x + inv.c * c.y + inv.e,
y: inv.b * c.x + inv.d * c.y + inv.f
}));
const xs = localCorners.map((c) => c.x);
const ys = localCorners.map((c) => c.y);
const localBBox = {
x: Math.min(...xs),
y: Math.min(...ys),
width: Math.max(...xs) - Math.min(...xs),
height: Math.max(...ys) - Math.min(...ys)
};
console.log('Local bbox (untransformed):', localBBox);See
examples/local-vs-global-coordinates.cjs
for a complete working demonstration with:
- Multiple transform scenarios (translate, rotate, complex)
- Matrix inversion implementation
- Visualization with colored rectangles
- Side-by-side comparison of global vs local
A native getLocalBBox() function is under consideration that would return
coordinates in the element's local coordinate system (before transforms):
// Proposed future API (not yet implemented)
const localBBox = await SvgVisualBBox.getSvgElementLocalBBox(
'#transformedElement'
);
// Returns coordinates BEFORE parent transformsUntil this is implemented, use the matrix inversion workaround above.
| Coordinate System | When to Use | Current API | Future API |
|---|---|---|---|
| Global | Visual rendering, overlays | ✅ Available | (same functions) |
| Local | Text-to-path, untransformed state | getLocalBBox() (TBD) |
- Chrome/Chromium: ✅ Fully supported (recommended)
- Firefox:
⚠️ May have minor SVG rendering differences - Safari:
⚠️ May have minor SVG rendering differences - Edge: ✅ Chromium-based version fully supported
| Runtime | Bbox functions | FBF helpers (extractFbfFrame, etc.) |
|---|---|---|
Browser (<script> or bundler) |
✅ | ✅ |
Bun (require or import) |
✅ (full UMD) | ✅ |
| Node.js (CJS or ESM) | ❌ direct call throws actionable error — use Puppeteer injection (see README Pattern C) | ✅ pure-string helpers, no DOM needed |
Node.js requires Node 24+ (for npm 11.5.1+ trusted publishing during the release pipeline; the runtime itself works on Node 20+ for FBF helpers).
MIT License - see LICENSE file for details.