|
/* |
|
* Bespoke SVG exporter |
|
* |
|
* Copyright (c) 2023 OBONO |
|
* |
|
* Released under the MIT license. |
|
* see https://opensource.org/licenses/MIT |
|
*/ |
|
|
|
const { XMLParser, XMLBuilder } = require('fast-xml-parser'); |
|
const { createCanvas, Image } = require('canvas'); |
|
const { format } = require('util'); |
|
const fs = require('fs'); |
|
|
|
const mimeTypePng = 'image/png'; |
|
const mimeTypeJpeg = 'image/jpeg'; |
|
|
|
function interpretColor(color) { |
|
const re = /^[0-9A-Za-z]{3,20}$/; |
|
if (color != null && re.test(color)) { |
|
if (color === 'random') { |
|
return '#' + Math.floor((1 + Math.random()) * 0x1000000).toString(16).slice(-6); |
|
} |
|
const re2 = /^[0-9A-Fa-f]+$/; |
|
const len = color.length; |
|
if (re2.test(color) && (len == 3 || len == 6)) { |
|
return '#' + color; |
|
} |
|
return color; |
|
} |
|
return undefined; |
|
} |
|
|
|
function applyStyle(style, id, attr, params) { |
|
const color = interpretColor(params[id + '.' + attr]); |
|
if (color != null) { |
|
style = style.replace(new RegExp(attr + ':[^;]+;'), attr + ':' + color + ';'); |
|
} |
|
return style; |
|
} |
|
|
|
function doProcedure(filePath, params, mimeType, callback) { |
|
|
|
// Load the base SVG file |
|
const xmlOption = { ignoreAttributes: false }; |
|
const parser = new XMLParser(xmlOption); |
|
const svgStr = fs.readFileSync(filePath, 'utf-8').toString(); |
|
const svgObj = parser.parse(svgStr); |
|
|
|
// Transform |
|
const size = Number(params.size); |
|
let width = Number(svgObj.svg['@_width']); |
|
let height = Number(svgObj.svg['@_height']); |
|
if (size > 0 && size <= 512) { |
|
if (width >= height) { |
|
height *= size / width; |
|
width = size; |
|
} else { |
|
width *= size / height; |
|
height = size; |
|
} |
|
svgObj.svg['@_width'] = width; |
|
svgObj.svg['@_height'] = height; |
|
} |
|
let rotate = (params.rotate === 'random') ? Math.random() * 360 : Number(params.rotate); |
|
if (rotate) { |
|
const centerX = width / 2; |
|
const centerY = height / 2; |
|
svgObj.svg['@_transform'] = format('translate(%f,%f) rotate(%f) translate(-%f,-%f)', |
|
centerX, centerY, rotate, centerX, centerY); |
|
} |
|
|
|
// Change colors |
|
for (let i = 0; i < svgObj.svg.g.length; i++) { |
|
const g = svgObj.svg.g[i]; |
|
const id = g['@_id']; |
|
let style = g['@_style']; |
|
style = applyStyle(style, id, 'fill', params); |
|
style = applyStyle(style, id, 'stroke', params); |
|
g['@_style'] = style; |
|
} |
|
|
|
// Draw & Export |
|
const canvas = createCanvas(width, height); |
|
const ctx = canvas.getContext('2d'); |
|
const image = new Image(); |
|
image.onload = () => { |
|
let color = interpretColor(params.bg); |
|
if (color == null && mimeType === mimeTypeJpeg) { |
|
color = 'white'; |
|
} |
|
if (color != null) { |
|
ctx.fillStyle = color; |
|
ctx.fillRect(0, 0, width, height); |
|
} |
|
ctx.drawImage(image, 0, 0); |
|
const exportOption = { |
|
compressionLevel: 9, |
|
filters: canvas.PNG_FILTER_NONE, |
|
quality: 0.95 |
|
}; |
|
callback(canvas.toBuffer(mimeType, exportOption)); |
|
} |
|
image.onerror = (err) => { |
|
console.log(err); |
|
callback(null); |
|
} |
|
const builder = new XMLBuilder(xmlOption); |
|
image.src = Buffer.from(builder.build(svgObj)); |
|
} |
|
|
|
exports.doProcedure = doProcedure; |
|
exports.mimeTypePng = mimeTypePng; |
|
exports.mimeTypeJpeg = mimeTypeJpeg; |