soroban-abacus-flashcards/packages/templates/extract-viewbox.js

235 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// Extract viewBox from crop marks with proper transform accumulation
// This correctly walks up the SVG hierarchy to calculate final coordinates
const fs = require("fs");
function parseSVGWithTransforms(svgContent) {
// Parse SVG structure to find crop marks in both CLI and Node.js formats
const cropMarks = [];
// Method 1: Look for crop-mark:// links (Node.js format)
const linkMatches = svgContent.matchAll(
/xlink:href="crop-mark:\/\/(left|right|top|bottom)"/g,
);
const transformRegex = /transform="translate\(([^)]+)\)"/g;
// Convert content to searchable text for easier parsing
const searchableContent = svgContent.replace(/\n/g, " ").replace(/\s+/g, " ");
for (const linkMatch of linkMatches) {
const direction = linkMatch[1];
const beforeLink = searchableContent.substring(0, linkMatch.index);
// Find all transforms before this link
let totalX = 0,
totalY = 0;
let transformMatch;
transformRegex.lastIndex = 0; // Reset regex
while ((transformMatch = transformRegex.exec(beforeLink)) !== null) {
const coords = transformMatch[1].split(/[,\s]+/).map(Number);
totalX += coords[0] || 0;
totalY += coords[1] || 0;
}
cropMarks.push({
type: "crop-mark",
direction,
finalTransform: { x: totalX, y: totalY },
});
}
// Method 2: Fallback to CLI format (multi-line with path elements)
if (cropMarks.length === 0) {
const lines = svgContent.split("\n");
const stack = []; // Track parent transforms
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const indent = line.match(/^(\s*)/)[1].length;
// Track opening/closing tags to maintain hierarchy
if (line.includes("<g ") && line.includes("transform=")) {
const transformMatch = line.match(/transform="translate\(([^)]+)\)"/);
if (transformMatch) {
const coords = transformMatch[1].split(/[,\s]+/).map(Number);
const transform = { x: coords[0] || 0, y: coords[1] || 0 };
// Maintain stack based on indentation level
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
stack.pop();
}
// Calculate accumulated transform
const accumulated = stack.reduce(
(acc, parent) => ({
x: acc.x + parent.transform.x,
y: acc.y + parent.transform.y,
}),
{ x: 0, y: 0 },
);
const finalTransform = {
x: accumulated.x + transform.x,
y: accumulated.y + transform.y,
};
stack.push({ indent, transform, finalTransform });
// Check if this contains a tiny crop mark point (not the dashed lines)
if (
i + 1 < lines.length &&
lines[i + 1].includes('fill="#ff4136"') &&
lines[i + 1].includes("0.1 0.1")
) {
cropMarks.push({
type: "crop-mark",
finalTransform,
line: i,
});
}
}
}
}
}
return cropMarks;
}
function extractViewBoxFromCropMarks(svgPath) {
console.log(
`📐 Analyzing crop marks with transform accumulation in ${svgPath}...`,
);
if (!fs.existsSync(svgPath)) {
throw new Error(`SVG file not found: ${svgPath}`);
}
const svgContent = fs.readFileSync(svgPath, "utf8");
// Use proper SVG parsing with transform accumulation
const elements = parseSVGWithTransforms(svgContent);
const cropMarkElements = elements.filter((e) => e.type === "crop-mark");
if (cropMarkElements.length === 0) {
console.log(" ⚠️ No crop marks found in this SVG");
return null;
}
// Extract positions from accumulated transforms
const cropMarks = {};
cropMarkElements.forEach((element, index) => {
let markType;
if (element.direction) {
// Node.js format: has explicit direction
markType = element.direction;
} else {
// CLI format: use order-based mapping
const markTypes = ["left", "right", "top", "bottom"];
markType = markTypes[index];
}
if (markType) {
const { x, y } = element.finalTransform;
cropMarks[markType] = { x, y };
console.log(` ✅ Found ${markType} at (${x}, ${y}) [accumulated]`);
}
});
// Calculate viewBox from accumulated positions
const positions = Object.values(cropMarks);
const minX = Math.min(...positions.map((p) => p.x));
const maxX = Math.max(...positions.map((p) => p.x));
const minY = Math.min(...positions.map((p) => p.y));
const maxY = Math.max(...positions.map((p) => p.y));
const width = maxX - minX;
const height = maxY - minY;
const viewBox = `${minX} ${minY} ${width} ${height}`;
console.log(`📏 Calculated viewBox: "${viewBox}"`);
console.log(` 📊 Dimensions: ${width} × ${height}`);
console.log(` 📍 Origin: (${minX}, ${minY})`);
return {
viewBox,
width,
height,
minX,
minY,
maxX,
maxY,
cropMarks,
};
}
function updateSVGViewBox(inputPath, outputPath = null) {
const result = extractViewBoxFromCropMarks(inputPath);
if (!result) {
console.log("❌ Cannot update viewBox - no crop marks found");
return false;
}
const svgContent = fs.readFileSync(inputPath, "utf8");
// Update the viewBox attribute and SVG dimensions to match aspect ratio
let updatedSVG = svgContent.replace(
/viewBox="[^"]*"/,
`viewBox="${result.viewBox}"`,
);
// Update width and height to match the viewBox dimensions for correct aspect ratio
updatedSVG = updatedSVG.replace(/width="[^"]*"/, `width="${result.width}pt"`);
updatedSVG = updatedSVG.replace(
/height="[^"]*"/,
`height="${result.height}pt"`,
);
const output =
outputPath || inputPath.replace(".svg", "-cropped-correct.svg");
fs.writeFileSync(output, updatedSVG);
console.log(`✅ Updated SVG saved to ${output}`);
return true;
}
// CLI usage
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log(
"Usage: node extract-viewbox-correct.js <svg-file> [output-file]",
);
console.log("");
console.log("Examples:");
console.log(
" node extract-viewbox-correct.js gallery/debug-crop-marks-89.svg",
);
console.log(
" node extract-viewbox-correct.js gallery/crop-single-1.svg cropped.svg",
);
process.exit(1);
}
const [inputFile, outputFile] = args;
try {
if (outputFile) {
updateSVGViewBox(inputFile, outputFile);
} else {
extractViewBoxFromCropMarks(inputFile);
}
} catch (error) {
console.error("❌ Error:", error.message);
process.exit(1);
}
}
module.exports = { extractViewBoxFromCropMarks, updateSVGViewBox };