Created
September 25, 2023 23:10
-
-
Save tsmd/671966737baecfe5b9c430273e28c2a2 to your computer and use it in GitHub Desktop.
Optimize SVG Icons
This file contains 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 { XMLParser, XMLBuilder } = require('fast-xml-parser'); | |
const presentationAttrs = [ | |
'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-rendering', | |
'cursor', 'display', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', | |
'opacity', 'pointer-events', 'shape-rendering', 'stroke', 'stroke-dasharray', | |
'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', | |
'stroke-opacity', 'stroke-width', 'transform', 'vector-effect', 'visibility', | |
]; | |
const processSVG = (svgString) => { | |
const parser = new XMLParser({ | |
preserveOrder: true, | |
ignoreAttributes: false, | |
ignoreDeclaration: true, | |
attributeNamePrefix: '', | |
}); | |
const builder = new XMLBuilder({ | |
preserveOrder: true, | |
ignoreAttributes: false, | |
attributeNamePrefix: '', | |
suppressEmptyNode: true, | |
format: true, | |
indentBy: '\t', | |
}); | |
const parsed = parser.parse(svgString); | |
// id と data-name 属性を削除 | |
function removeIdAndDataNameAttributes(obj) { | |
delete obj[':@']?.['id']; | |
delete obj[':@']?.['data-name']; | |
obj.svg?.forEach(removeIdAndDataNameAttributes); | |
obj.g?.forEach(removeIdAndDataNameAttributes); | |
} | |
parsed.forEach(removeIdAndDataNameAttributes) | |
// stroke-width 属性を削除 | |
function removeStrokeWidthAttribute(obj) { | |
if (obj[':@']) { | |
if (obj[':@']['stroke-width'] === '0' && !obj[':@']['stroke']) { | |
delete obj[':@']['stroke-width']; | |
} | |
} | |
obj.svg?.forEach(removeStrokeWidthAttribute); | |
obj.g?.forEach(removeStrokeWidthAttribute); | |
} | |
parsed.forEach(removeStrokeWidthAttribute); | |
// <rect width="56" height="56" fill="none"/> を削除 | |
function removeEmptyRect(obj) { | |
const children = obj.svg || obj.g; | |
if (!children) return; | |
for (let i = children.length - 1; i >= 0; i--) { | |
if ('rect' in children[i] && children[i][':@'].width === '56' && children[i][':@'].height === '56' && children[i][':@'].fill === 'none') { | |
children.splice(i, 1); | |
} | |
} | |
children.forEach(removeEmptyRect); | |
} | |
removeEmptyRect(parsed[0]); | |
function processGroupElement(node) { | |
const children = node.svg || node.g; | |
children?.forEach(processGroupElement); | |
if (children?.length > 0) { | |
presentationAttrs.forEach((attr) => { | |
const attrValue = children[0][':@']?.[attr]; | |
const isCommon = children.every((child) => child[':@']?.[attr] === attrValue); | |
if (attrValue && isCommon) { | |
node[':@'] = node[':@'] || {}; | |
node[':@'][attr] = attrValue; | |
} | |
children.forEach((child) => { | |
if (isCommon && child[':@']) delete child[':@'][attr]; | |
}); | |
}); | |
} | |
} | |
processGroupElement(parsed[0]); | |
// 属性のない g 要素を削除し、子要素を親要素に移動 | |
function removeEmptyGroup(children) { | |
for (let i = 0; i < children.length; i++) { | |
let obj = children[i]; | |
if (obj.g) { | |
if (!obj[':@'] || Object.keys(obj[':@']).length === 0) { | |
children.splice(i, 1, ...obj.g); | |
i -= 1; | |
} | |
} | |
} | |
for (let i = 0; i < children.length; i++) { | |
let obj = children[i]; | |
if (obj.g) { | |
removeEmptyGroup(obj.g); | |
} | |
} | |
} | |
removeEmptyGroup(parsed[0].svg) | |
return builder.build(parsed).trim(); | |
}; | |
module.exports = { | |
processSVG, | |
} |
This file contains 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 { processSVG } = require('./lib'); // processSVG関数が含まれるモジュールへのパスを指定 | |
const { XMLParser } = require('fast-xml-parser'); | |
const parser = new XMLParser({ | |
preserveOrder: true, | |
ignoreAttributes: false, | |
ignoreDeclaration: true, | |
attributeNamePrefix: '', | |
}); | |
describe('processSVG function', () => { | |
const parseXML = (xmlString) => { | |
return parser.parse(xmlString); | |
}; | |
it('should remove XML declaration', () => { | |
const input = '<?xml version="1.0" encoding="UTF-8"?><svg></svg>'; | |
const output = processSVG(input); | |
console.log(output); | |
expect(parseXML(output)).toEqual(parseXML('<svg></svg>')); | |
}); | |
it('should remove id and data-name attributes', () => { | |
const input = '<svg><circle id="circle1" data-name="circle" r="50" cx="50" cy="50"/></svg>'; | |
const output = processSVG(input); | |
expect(parseXML(output)).toEqual(parseXML('<svg><circle r="50" cx="50" cy="50"/></svg>')); | |
}); | |
it('should move common fill and stroke attributes to g element', () => { | |
const input = '<svg><g><path fill="red" stroke="blue"/><circle fill="red" stroke="blue"/></g></svg>'; | |
const output = processSVG(input); | |
expect(parseXML(output)).toEqual(parseXML('<svg fill="red" stroke="blue"><path/><circle/></svg>')); | |
}); | |
it('should handle nested g elements', () => { | |
const input = '<svg><g><g><path fill="red"/><path fill="red"/></g></g></svg>'; | |
const output = processSVG(input); | |
expect(parseXML(output)).toEqual(parseXML('<svg fill="red"><path/><path/></svg>')); | |
}); | |
it('should not move attributes if they are not common', () => { | |
const input = '<svg><g><path fill="red"/><path fill="blue"/></g></svg>'; | |
const output = processSVG(input); | |
expect(parseXML(output)).toEqual(parseXML('<svg><path fill="red"/><path fill="blue"/></svg>')); | |
}); | |
it('should not move attributes if they are not common', () => { | |
const input = '<svg><g><path fill="red"/><circle fill="blue"/></g></svg>'; | |
const output = processSVG(input); | |
expect(parseXML(output)).toEqual(parseXML('<svg><path fill="red"/><circle fill="blue"/></svg>')); | |
}); | |
it('should not remove fill attribute from g element', () => { | |
const input = '<svg><g fill="#fff"><rect y="4.8" width="18" height="14.4" rx="3"/><path d=""/></g></svg>'; | |
const output = processSVG(input); | |
expect(parseXML(output)).toEqual(parseXML('<svg fill="#fff"><rect y="4.8" width="18" height="14.4" rx="3"/><path d=""/></svg>')); | |
}); | |
// 属性のない g 要素を削除するテスト | |
it('should remove g element without attributes', () => { | |
const input = '<svg><g><path/><path/><g><g><path/><path/></g></g></g></svg>'; | |
const output = processSVG(input); | |
expect(parseXML(output)).toEqual(parseXML('<svg><path/><path/><path/><path/></svg>')); | |
}); | |
}); |
This file contains 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'); | |
const { processSVG } = require('./lib'); | |
const targetDir = process.argv[2].replace(/([\/\\])$/, ''); // コマンドライン引数からディレクトリを取得 | |
if (!targetDir) { | |
console.error('Please provide a directory as an argument.'); | |
process.exit(1); | |
} | |
const optimizedDir = path.join(targetDir, 'optimized'); | |
// optimized ディレクトリが存在しない場合は作成 | |
if (!fs.existsSync(optimizedDir)) { | |
fs.mkdirSync(optimizedDir); | |
} | |
fs.readdir(targetDir, (err, files) => { | |
if (err) { | |
console.error('Error reading the directory:', err); | |
return; | |
} | |
for (const file of files) { | |
if (path.extname(file) === '.svg') { | |
const filePath = path.join(targetDir, file); | |
const fileContent = fs.readFileSync(filePath, 'utf8'); | |
const optimizedContent = processSVG(fileContent) + '\n'; | |
const optimizedFilePath = path.join(optimizedDir, file).normalize(); | |
fs.writeFileSync(optimizedFilePath, optimizedContent, 'utf8'); | |
console.log(`Optimized ${file} and saved to optimized directory.`); | |
} | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment