UNPKG

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)

471 lines (402 loc) 14 kB
#!/usr/bin/env node /** * sbb-chrome-extract.cjs - Extract SVG elements using Chrome's native .getBBox() * * This tool demonstrates the standard SVG .getBBox() method's behavior for comparison * with SvgVisualBBox and Inkscape extraction methods. */ const puppeteer = require('puppeteer'); const fs = require('fs'); // const path = require('path'); // Reserved for future use const { getVersion } = require('./version.cjs'); const { printError, printSuccess, printInfo, runCLI } = require('./lib/cli-utils.cjs'); /** * Extract SVG element using native .getBBox() method */ async function extractWithGetBBox(options) { const { inputFile, elementId, outputSvg, outputPng, margin, background, scale, width, height } = 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 using standard .getBBox() const result = await page.evaluate( (id, marginValue) => { const element = /** @type {SVGGraphicsElement} */ ( /* eslint-disable-next-line no-undef */ /** @type {unknown} */ (document.getElementById(id)) ); if (!element) { throw new Error(`Element with id "${id}" not found`); } // Get the standard SVG .getBBox() const bbox = element.getBBox(); // Get SVG root and its viewBox const svg = element.ownerSVGElement; // Apply margin const bboxWithMargin = { x: bbox.x - marginValue, y: bbox.y - marginValue, width: bbox.width + 2 * marginValue, height: bbox.height + 2 * marginValue }; return { bbox: bboxWithMargin, originalBbox: { x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height }, svgViewBox: svg.getAttribute('viewBox'), element: { tagName: element.tagName, id: element.id } }; }, elementId, margin ); printInfo( `Standard .getBBox() result: ${result.originalBbox.width.toFixed(2)} × ${result.originalBbox.height.toFixed(2)}` ); printInfo( `With margin (${margin}): ${result.bbox.width.toFixed(2)} × ${result.bbox.height.toFixed(2)}` ); // Create a new SVG with just this element and the getBBox dimensions const extractedSvg = await page.evaluate( (id, bbox) => { const element = /** @type {SVGGraphicsElement} */ ( /* eslint-disable-next-line no-undef */ /** @type {unknown} */ (document.getElementById(id)) ); const svg = element.ownerSVGElement; // Clone the element const clone = element.cloneNode(true); // Get defs if any const defs = svg.querySelectorAll('defs'); let defsContent = ''; defs.forEach((def) => { defsContent += /** @type {Element} */ (def).outerHTML + '\n'; }); // Create new SVG with viewBox set to getBBox result const newViewBox = `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`; return `<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg id="getbbox_extraction" version="1.1" x="0px" y="0px" width="${bbox.width}" height="${bbox.height}" viewBox="${newViewBox}" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> ${defsContent}${/** @type {Element} */ (clone).outerHTML} </svg>`; }, elementId, result.bbox ); // Write the extracted SVG fs.writeFileSync(outputSvg, extractedSvg); printSuccess(`SVG extracted to: ${outputSvg}`); // Render PNG if requested if (outputPng) { await renderToPng(page, extractedSvg, outputPng, { width, height, scale, background, viewBox: result.bbox }); printSuccess(`PNG rendered to: ${outputPng}`); } } finally { await browser.close(); } } /** * Render SVG to PNG using Puppeteer */ async function renderToPng(page, svgContent, outputPath, options) { const { width, height, scale, background, viewBox } = options; // Calculate dimensions let pngWidth, pngHeight; if (width && height) { pngWidth = width; pngHeight = height; } else if (width) { pngWidth = width; pngHeight = Math.round((width / viewBox.width) * viewBox.height); } else if (height) { pngHeight = height; pngWidth = Math.round((height / viewBox.height) * viewBox.width); } else { // Use scale factor pngWidth = Math.round(viewBox.width * scale); pngHeight = Math.round(viewBox.height * scale); } // Set page size await page.setViewport({ width: pngWidth, height: pngHeight, deviceScaleFactor: 1 }); // Determine background style let bgStyle = ''; if (background === 'transparent') { bgStyle = 'background: transparent;'; } else { bgStyle = `background: ${background};`; } // Render the SVG await page.setContent(` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: ${pngWidth}px; height: ${pngHeight}px; ${bgStyle} overflow: hidden; } svg { display: block; width: 100%; height: 100%; } </style> </head> <body>${svgContent}</body> </html> `); // Take screenshot await page.screenshot({ path: outputPath, type: 'png', omitBackground: background === 'transparent' }); printInfo(`PNG size: ${pngWidth}×${pngHeight}px (scale: ${scale}x, background: ${background})`); } /** * Print help message */ function printHelp() { const version = getVersion(); console.log(` ╔════════════════════════════════════════════════════════════════════════════╗ ║ sbb-chrome-extract - Extract using Chrome .getBBox() ║ ╚════════════════════════════════════════════════════════════════════════════╝ ℹ Version ${version} DESCRIPTION: Extract SVG elements using Chrome's native .getBBox() method. This tool is for comparison with SvgVisualBBox and Inkscape extraction. USAGE: sbb-chrome-extract input.svg --id <element-id> --output <output.svg> [options] REQUIRED ARGUMENTS: input.svg Input SVG file path --id <element-id> ID of the element to extract OUTPUT OPTIONS: --output <path> Output SVG file path (required) --png <path> Also render PNG to this path (optional) BBOX OPTIONS: --margin <number> Margin around bbox in SVG units (default: 5) PNG RENDERING OPTIONS: --scale <number> Resolution multiplier (default: 4) Higher = better quality but larger file --width <pixels> Exact PNG width in pixels --height <pixels> Exact PNG height in pixels If only one dimension specified, other is computed If both omitted, uses scale factor --background <color> Background color (default: transparent) Options: - transparent (PNG transparency) - white, black, red, etc. (CSS colors) - #RRGGBB (hex colors) - rgba(r,g,b,a) (CSS rgba format) GENERAL OPTIONS: --help, -h Show this help message --version, -v Show version number ═══════════════════════════════════════════════════════════════════════════════ EXAMPLES: # Extract element with default margin sbb-chrome-extract drawing.svg --id text39 --output text39.svg # Extract and render PNG with transparent background sbb-chrome-extract drawing.svg --id text39 \\ --output text39.svg --png text39.png # Extract with custom margin and white background PNG sbb-chrome-extract drawing.svg --id logo \\ --output logo.svg --png logo.png \\ --margin 10 --background white # Extract with exact PNG dimensions at high resolution sbb-chrome-extract chart.svg --id graph \\ --output graph.svg --png graph.png \\ --width 1920 --height 1080 --background "#f0f0f0" # Extract with custom scale and colored background sbb-chrome-extract icon.svg --id main_icon \\ --output icon.svg --png icon.png \\ --scale 8 --background "rgba(255, 255, 255, 0.9)" ═══════════════════════════════════════════════════════════════════════════════ 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-extract: 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 extraction 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 = { id: null, output: null, png: null, margin: 5, scale: 4, width: null, height: null, background: 'transparent' }; 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 'id': options.id = next || null; useNext(); break; case 'output': options.output = next || null; useNext(); break; case 'png': options.png = next || null; useNext(); break; 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 'scale': options.scale = parseFloat(next); if (!isFinite(options.scale) || options.scale <= 0 || options.scale > 20) { printError('Scale must be between 0 and 20'); process.exit(1); } useNext(); break; case 'width': options.width = parseInt(next, 10); useNext(); break; case 'height': options.height = parseInt(next, 10); useNext(); break; case 'background': options.background = next || 'transparent'; 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-extract input.svg --id <element-id> --output <output.svg>'); process.exit(1); } if (!options.id) { printError('Missing required option: --id <element-id>'); process.exit(1); } if (!options.output) { printError('Missing required option: --output <output.svg>'); process.exit(1); } options.input = positional[0]; // 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-extract v${getVersion()} | svg-bbox toolkit\n`); const options = parseArgs(process.argv); // Extract options for the extraction function const extractOptions = { inputFile: options.input, elementId: options.id, outputSvg: options.output, outputPng: options.png, margin: options.margin, background: options.background, scale: options.scale, width: options.width, height: options.height }; // Run extraction await extractWithGetBBox(extractOptions); } // Run CLI runCLI(main); module.exports = { extractWithGetBBox };