Last active
January 25, 2026 15:27
-
-
Save MrPaXe/232c9db06bcbcb9b85eaf10a23364b2d to your computer and use it in GitHub Desktop.
SVG Converter to make svgs compatible with PixiJs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const fs = require("fs"); | |
| const path = require("path"); | |
| /** | |
| * Converts CSS declarations to SVG attributes | |
| * @param {string} decls | |
| * @returns {string} | |
| */ | |
| function declsToAttrs(decls) { | |
| return decls | |
| .split(";") | |
| .map((d) => { | |
| const [prop, val] = d.split(":").map((s) => s?.trim()); | |
| if (prop && val) { | |
| // Remove px from values as Pixi prefers raw numbers | |
| const cleanVal = val.replace(/px/g, ""); | |
| return `${prop}="${cleanVal}"`; | |
| } | |
| return ""; | |
| }) | |
| .filter((s) => s !== "") | |
| .join(" "); | |
| } | |
| /** | |
| * Processes a single SVG file to make it compatible with PixiJS v8 | |
| * @param {string} filePath | |
| */ | |
| function fixSVGFile(filePath) { | |
| if (!filePath.endsWith(".svg")) { | |
| return; | |
| } | |
| const fileName = path.basename(filePath); | |
| console.log(`Processing ${fileName}...`); | |
| let content; | |
| try { | |
| content = fs.readFileSync(filePath, "utf8"); | |
| } catch (err) { | |
| console.error(` Error reading file: ${err.message}`); | |
| return; | |
| } | |
| const originalSize = content.length; | |
| // 1. Extract styles from <style> blocks | |
| const styleMatch = content.match(/<style[^>]*>([\s\S]*?)<\/style>/); | |
| const styles = {}; | |
| if (styleMatch) { | |
| const styleContent = styleMatch[1]; | |
| // Simple parser for Illustrator-style CSS | |
| const rules = styleContent.split("}"); | |
| rules.forEach((rule) => { | |
| const parts = rule.split("{"); | |
| if (parts.length === 2) { | |
| const selectors = parts[0].trim().split(","); | |
| const decls = parts[1].trim(); | |
| selectors.forEach((sel) => { | |
| const className = sel.trim().replace(/^\./, ""); | |
| if (className) { | |
| if (!styles[className]) { | |
| styles[className] = ""; | |
| } | |
| styles[className] += decls + ";"; | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| // 2. Inline styles by replacing class="..." with actual attributes | |
| content = content.replace(/class="([^"]+)"/g, (match, className) => { | |
| const classes = className.split(/\s+/); | |
| let attrs = ""; | |
| classes.forEach((c) => { | |
| if (styles[c]) { | |
| attrs += " " + declsToAttrs(styles[c]); | |
| } | |
| }); | |
| return attrs || match; | |
| }); | |
| // 3. Clean up style blocks and other junk | |
| content = content.replace(/<style[^>]*>[\s\S]*?<\/style>/g, ""); | |
| content = content.replace(/<defs>\s*<\/defs>/g, ""); // Remove empty defs | |
| // 4. Remove Adobe Illustrator metadata and junk | |
| content = content.replace(/xmlns:i="[^"]+"/g, ""); | |
| content = content.replace(/<i:pgf>[\s\S]*?<\/i:pgf>/g, ""); | |
| content = content.replace(/<i:view[\s\S]*?\/>/g, ""); | |
| content = content.replace(/<!-- Generator: Adobe Illustrator.*? -->/g, ""); | |
| content = content.replace(/i:[a-z]+="[^"]*"/g, ""); | |
| content = content.replace(/version="2\.1\.1"/g, 'version="1.1"'); | |
| // 5. Ensure viewBox exists (critical for PixiJS) | |
| if (!content.includes("viewBox")) { | |
| const widthMatch = content.match(/width="([^"]+)"/); | |
| const heightMatch = content.match(/height="([^"]+)"/); | |
| if (widthMatch && heightMatch) { | |
| const width = widthMatch[1].replace("px", ""); | |
| const height = heightMatch[1].replace("px", ""); | |
| content = content.replace("<svg", `<svg viewBox="0 0 ${width} ${height}"`); | |
| } | |
| } | |
| // 6. Fix dasharray (sometimes it's semicolon separated or has "px") | |
| content = content.replace(/stroke-dasharray="([^"]+)"/g, (match, val) => { | |
| const cleanVal = val.replace(/px/g, "").replace(/;/g, " ").trim(); | |
| return `stroke-dasharray="${cleanVal}"`; | |
| }); | |
| // 7. Handle special case: isolation: isolate which Pixi doesn't support | |
| content = content.replace(/isolation="isolate"/g, ""); | |
| if (content.length !== originalSize || content !== fs.readFileSync(filePath, "utf8")) { | |
| fs.writeFileSync(filePath, content); | |
| console.log(` Fixed ${fileName} (Size: ${originalSize} -> ${content.length})`); | |
| } else { | |
| console.log(` No changes needed for ${fileName}`); | |
| } | |
| } | |
| // Main execution | |
| const target = process.argv[2]; | |
| let filesToProcess = []; | |
| if (target) { | |
| const fullPath = path.resolve(target); | |
| if (!fs.existsSync(fullPath)) { | |
| console.error(`Error: Path not found: ${fullPath}`); | |
| process.exit(1); | |
| } | |
| const stats = fs.statSync(fullPath); | |
| if (stats.isFile()) { | |
| filesToProcess.push(fullPath); | |
| } else if (stats.isDirectory()) { | |
| filesToProcess = fs.readdirSync(fullPath) | |
| .filter((f) => f.endsWith(".svg")) | |
| .map((f) => path.join(fullPath, f)); | |
| if (filesToProcess.length === 0) { | |
| console.log(`No SVG files found in directory: ${fullPath}`); | |
| } | |
| } else { | |
| console.error(`Error: Path is neither a file nor a directory: ${fullPath}`); | |
| process.exit(1); | |
| } | |
| } else { | |
| // Default behavior: process the maps directory | |
| const mapsDir = __dirname; | |
| if (fs.existsSync(mapsDir)) { | |
| filesToProcess = fs.readdirSync(mapsDir) | |
| .filter((f) => f.endsWith(".svg")) | |
| .map((f) => path.join(mapsDir, f)); | |
| if (filesToProcess.length === 0) { | |
| console.log(`No SVG files found in default maps directory: ${mapsDir}`); | |
| } | |
| } else { | |
| console.error(`Error: Default maps directory not found: ${mapsDir}`); | |
| console.log("Usage: node fixsvg.cjs [file-or-directory]"); | |
| process.exit(1); | |
| } | |
| } | |
| filesToProcess.forEach(fixSVGFile); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment