Skip to content

Instantly share code, notes, and snippets.

@tsmd
Created September 25, 2023 23:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tsmd/671966737baecfe5b9c430273e28c2a2 to your computer and use it in GitHub Desktop.
Save tsmd/671966737baecfe5b9c430273e28c2a2 to your computer and use it in GitHub Desktop.
Optimize SVG Icons
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,
}
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>'));
});
});
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