235 lines
6.7 KiB
JavaScript
235 lines
6.7 KiB
JavaScript
#!/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 };
|