svg-bbox
Version:
A set of tools to compute and to use a SVG bounding box you can trust (as opposed to the unreliable .getBBox() scourge)
359 lines (301 loc) • 10.3 kB
JavaScript
#!/usr/bin/env node
/**
* sbb-chrome-getbbox.cjs - Get bounding box using Chrome's native .get BBox()
*
* This tool demonstrates the standard SVG .getBBox() method's behavior for comparison
* with SvgVisualBBox algorithm. It returns bbox information without extraction.
*/
const puppeteer = require('puppeteer');
const fs = require('fs');
const path = require('path');
const { getVersion } = require('./version.cjs');
const { printError, printSuccess, printInfo, runCLI } = require('./lib/cli-utils.cjs');
/**
* Get bbox using native .getBBox() method
*/
async function getBBoxWithChrome(options) {
const { inputFile, elementIds, margin } = options;
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
try {
const page = await browser.newPage();
// Read the SVG file
const svgContent = fs.readFileSync(inputFile, 'utf-8');
// Load it into the page
await page.setContent(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { margin: 0; padding: 0; }
svg { display: block; }
</style>
</head>
<body>${svgContent}</body>
</html>
`);
// Get bbox for all requested elements
const results = await page.evaluate(
(elementIds, marginValue) => {
/* eslint-disable no-undef */
const svg = document.querySelector('svg');
if (!svg) {
return { error: 'No SVG element found' };
}
const output = {};
// If no element IDs specified, compute whole content bbox
if (elementIds.length === 0) {
try {
/** @type {SVGGraphicsElement} */
const svgEl = /** @type {any} */ (svg);
const bbox = svgEl.getBBox();
const bboxWithMargin = {
x: bbox.x - marginValue,
y: bbox.y - marginValue,
width: bbox.width + 2 * marginValue,
height: bbox.height + 2 * marginValue
};
output['WHOLE CONTENT'] = {
bbox: bboxWithMargin,
originalBbox: {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height
},
svgViewBox: svg.getAttribute('viewBox')
};
} catch (err) {
output['WHOLE CONTENT'] = { error: err.message };
}
} else {
// Get bbox for each element ID
for (const id of elementIds) {
const element = /** @type {SVGGraphicsElement} */ (
/** @type {unknown} */ (document.getElementById(id))
);
if (!element) {
output[id] = { error: 'Element not found' };
continue;
}
try {
// Get the standard SVG .getBBox()
const bbox = element.getBBox();
// Apply margin
const bboxWithMargin = {
x: bbox.x - marginValue,
y: bbox.y - marginValue,
width: bbox.width + 2 * marginValue,
height: bbox.height + 2 * marginValue
};
output[id] = {
bbox: bboxWithMargin,
originalBbox: {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height
},
element: {
tagName: element.tagName,
id: element.id
}
};
} catch (err) {
output[id] = { error: err.message };
}
}
}
return output;
/* eslint-enable no-undef */
},
elementIds,
margin
);
return {
filename: path.basename(inputFile),
path: inputFile,
results
};
} finally {
await browser.close();
}
}
/**
* Format bbox for console output
*/
function formatBBox(bbox) {
if (!bbox) {
return 'null';
}
if (bbox.error) {
return `ERROR: ${bbox.error}`;
}
const orig = bbox.originalBbox;
const withMargin = bbox.bbox;
return `{x: ${orig.x.toFixed(2)}, y: ${orig.y.toFixed(2)}, width: ${orig.width.toFixed(2)}, height: ${orig.height.toFixed(2)}} (with margin: ${withMargin.width.toFixed(2)} × ${withMargin.height.toFixed(2)})`;
}
/**
* Print results to console
*/
function printResults(result) {
console.log(`\nSVG: ${result.path}`);
const keys = Object.keys(result.results);
keys.forEach((key, idx) => {
const isLast = idx === keys.length - 1;
const prefix = isLast ? '└─' : '├─';
console.log(`${prefix} ${key}: ${formatBBox(result.results[key])}`);
});
}
/**
* Save results as JSON
*/
function saveJSON(result, outputPath) {
const json = {};
json[result.path] = result.results;
fs.writeFileSync(outputPath, JSON.stringify(json, null, 2), 'utf8');
printSuccess(`JSON saved to: ${outputPath}`);
}
/**
* Print help message
*/
function printHelp() {
const version = getVersion();
console.log(`
╔════════════════════════════════════════════════════════════════════════════╗
║ sbb-chrome-getbbox - Get bbox using Chrome .getBBox() ║
╚════════════════════════════════════════════════════════════════════════════╝
ℹ Version ${version}
DESCRIPTION:
Get bounding box information using Chrome's native .getBBox() method.
This tool is for comparison with SvgVisualBBox algorithm.
USAGE:
sbb-chrome-getbbox <input.svg> [element-ids...] [options]
REQUIRED ARGUMENTS:
input.svg Input SVG file path
OPTIONAL ARGUMENTS:
element-ids... Element IDs to get bbox for (if omitted, gets whole content)
OPTIONS:
--margin <number> Margin around bbox in SVG units (default: 5)
--json <path> Save results as JSON to specified file
--help, -h Show this help message
--version, -v Show version number
═══════════════════════════════════════════════════════════════════════════════
EXAMPLES:
# Get bbox for whole content
sbb-chrome-getbbox drawing.svg
# Get bbox for specific elements
sbb-chrome-getbbox drawing.svg text39 rect42 path55
# Get bbox with custom margin
sbb-chrome-getbbox drawing.svg logo --margin 10
# Save results as JSON
sbb-chrome-getbbox drawing.svg --json results.json
═══════════════════════════════════════════════════════════════════════════════
COMPARISON NOTES:
This tool uses Chrome's native .getBBox() method, which:
• Uses geometric calculations based on element bounds
• Often OVERSIZES vertically due to font metrics (ascender/descender)
• Ignores visual effects like filters, shadows, glows
• May not accurately reflect actual rendered pixels
Compare with:
• sbb-getbbox: Uses SvgVisualBBox (pixel-accurate canvas rasterization)
• sbb-inkscape-extract: Uses Inkscape (often UNDERSIZES due to font issues)
USE CASES:
• Demonstrate .getBBox() limitations vs SvgVisualBBox
• Create comparison test cases
• Benchmark against other bbox methods
• Educational purposes showing why accurate bbox matters
`);
}
/**
* Parse command line arguments
*/
function parseArgs(argv) {
const args = argv.slice(2);
// Check for --help
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
printHelp();
process.exit(0);
}
// Check for --version
if (args.includes('--version') || args.includes('-v')) {
console.log(getVersion());
process.exit(0);
}
const positional = [];
const options = {
margin: 5,
json: null
};
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a.startsWith('--')) {
const [key, val] = a.split('=');
const name = key.replace(/^--/, '');
const next = typeof val === 'undefined' ? args[i + 1] : val;
function useNext() {
if (typeof val === 'undefined') {
i++;
}
}
switch (name) {
case 'margin':
options.margin = parseFloat(next);
if (!isFinite(options.margin) || options.margin < 0) {
printError('Margin must be a non-negative number');
process.exit(1);
}
useNext();
break;
case 'json':
options.json = next || null;
useNext();
break;
default:
printError(`Unknown option: ${key}`);
process.exit(1);
}
} else {
positional.push(a);
}
}
// Validate required arguments
if (positional.length < 1) {
printError('Missing required argument: input.svg');
console.log('\nUsage: sbb-chrome-getbbox <input.svg> [element-ids...] [options]');
process.exit(1);
}
options.input = positional[0];
options.elementIds = positional.slice(1);
// Check input file exists
if (!fs.existsSync(options.input)) {
printError(`Input file not found: ${options.input}`);
process.exit(1);
}
return options;
}
/**
* Main CLI entry point
*/
async function main() {
printInfo(`sbb-chrome-getbbox v${getVersion()} | svg-bbox toolkit\n`);
const options = parseArgs(process.argv);
// Get bbox using Chrome .getBBox()
const result = await getBBoxWithChrome({
inputFile: options.input,
elementIds: options.elementIds,
margin: options.margin
});
// Output results
if (options.json) {
saveJSON(result, options.json);
} else {
printResults(result);
}
}
// Run CLI
runCLI(main);
module.exports = { getBBoxWithChrome };