UNPKG

@nmmty/lazycanvas

Version:

A simple way to interact with @napi-rs/canvas in an advanced way!

578 lines (577 loc) 28.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SVGReader = void 0; const svgson_1 = require("svgson"); const fs_1 = require("fs"); const index_1 = require("../../../index"); class SVGReader { static async parseSVG(svg) { let loadedSVG = await fs_1.promises.readFile(svg); return (0, svgson_1.parse)(loadedSVG.toString()); } static async createImageLayer(svg) { const imageLayer = new index_1.ImageLayer(); const src = svg.attributes['xlink:href'] || svg.attributes.href; if (svg.attributes.id) imageLayer.id = svg.attributes.id; if (svg.attributes['fill-opacity']) imageLayer.props.opacity = Number(svg.attributes['fill-opacity']); if (src.startsWith('data:image')) { const base64Data = src.replace(/^data:image\/\w+;base64,/, ""); const buffer = Buffer.from(base64Data, 'base64'); imageLayer.props.src = buffer; } else { imageLayer.props.src = src; } imageLayer.props.x = Number(svg.attributes.x) || 0; imageLayer.props.y = Number(svg.attributes.y) || 0; imageLayer.props.centring = 'start-top'; imageLayer.props.size.width = Number(svg.attributes.width); imageLayer.props.size.height = Number(svg.attributes.height); if (svg.attributes.transform) { SVGReader.applyTransform(svg.attributes.transform, imageLayer); } return imageLayer; } static async createEllipseImageLayer(svg, path) { const ellipseImageLayer = new index_1.ImageLayer(); const src = svg.attributes['xlink:href'] || svg.attributes.href; ellipseImageLayer.data.id = svg.attributes.id; if (svg.attributes['fill-opacity']) ellipseImageLayer.data.alpha = Number(svg.attributes['fill-opacity']); if (src.startsWith('data:image')) { const base64Data = src.replace(/^data:image\/\w+;base64,/, ""); const buffer = Buffer.from(base64Data, 'base64'); ellipseImageLayer.data.image = buffer; } else { ellipseImageLayer.data.image = src; } ellipseImageLayer.data.x = Number(svg.attributes.x) || 0; ellipseImageLayer.data.y = Number(svg.attributes.y) || 0; ellipseImageLayer.data.centering = 'legacy'; ellipseImageLayer.data.width = Number(svg.attributes.width); ellipseImageLayer.data.height = Number(svg.attributes.height); ellipseImageLayer.data.path = path; if (svg.attributes.transform) { SVGReader.applyTransform(svg.attributes.transform, ellipseImageLayer); } ellipseImageLayer.data.fromSVG = true; return ellipseImageLayer; } static async createRectLayer(svg, svgObject) { let rectLayer = new index_1.MorphLayer(); if (svg.attributes.rx || svg.attributes.ry) { rectLayer.props.size.radius = Math.min(Number(svg.attributes.rx) || 0, Number(svg.attributes.ry) || 0); } if (svg.attributes.id) rectLayer.id = svg.attributes.id; if (svg.attributes.fill && svg.attributes.fill !== 'none') { rectLayer.props.fillStyle = await this.fillStyle(svg, svgObject); } else { rectLayer.props.fillStyle = svg.attributes.fill === 'none' ? svg.attributes.stroke : svg.attributes.fill || 'black'; } rectLayer.props.filled = svg.attributes.fill !== 'none'; if (svg.attributes['fill-opacity']) rectLayer.props.opacity = Number(svg.attributes['fill-opacity']); rectLayer.props.x = Number(svg.attributes.x) || 0; rectLayer.props.y = Number(svg.attributes.y) || 0; rectLayer.props.centring = 'start-top'; rectLayer.props.size.width = Number(svg.attributes.width); rectLayer.props.size.height = Number(svg.attributes.height); if (svg.attributes.transform) { SVGReader.applyTransform(svg.attributes.transform, rectLayer); } return rectLayer; } static async createEllipseLayer(svg, svgObject) { const ellipseLayer = new index_1.MorphLayer(); if (svg.attributes.id) ellipseLayer.id = svg.attributes.id; if (svg.attributes.fill && svg.attributes.fill !== 'none') { let id = SVGReader.extractIdFromUrl(svg.attributes.fill); if (id) { let element = SVGReader.findElementById(svgObject, id); if (element.name === "pattern") { ellipseLayer.props.fillStyle = await SVGReader.createPattern(element, svgObject, svg); } else if (['linearGradient', 'radialGradient'].includes(element.name)) { ellipseLayer.props.fillStyle = await SVGReader.createGradient(element, svgObject, svg); } } else { ellipseLayer.props.fillStyle = svg.attributes.fill === 'none' ? svg.attributes.stroke : svg.attributes.fill || 'black'; } } else { ellipseLayer.data.color = svg.attributes.fill === 'none' ? svg.attributes.stroke : svg.attributes.fill || 'black'; } ellipseLayer.data.fill = svg.attributes.fill !== 'none'; if (svg.attributes['fill-opacity']) ellipseLayer.data.alpha = Number(svg.attributes['fill-opacity']); ellipseLayer.data.x = Number(svg.attributes.x) || 0; ellipseLayer.data.y = Number(svg.attributes.y) || 0; ellipseLayer.data.centering = 'legacy'; ellipseLayer.data.width = Number(svg.attributes.width); ellipseLayer.data.height = Number(svg.attributes.height); if (svg.attributes.transform) { SVGReader.applyTransform(svg.attributes.transform, ellipseLayer); } return ellipseLayer; } static async createCircleLayer(svg, svgObject) { const circleLayer = new index_1.MorphLayer(); circleLayer.data.id = svg.attributes.id || `CircleLayer-${Math.random().toString(36).substring(2, 15)}`; if (svg.attributes.fill && svg.attributes.fill !== 'none') { let id = SVGReader.extractIdFromUrl(svg.attributes.fill); if (id) { let element = SVGReader.findElementById(svgObject, id); if (element.name === "pattern") { circleLayer.data.color = await SVGReader.createPattern(element, svgObject, svg); } else if (['linearGradient', 'radialGradient'].includes(element.name)) { circleLayer.data.color = await SVGReader.createGradient(element, svgObject, svg); } } else { circleLayer.data.color = svg.attributes.fill === 'none' ? svg.attributes.stroke : svg.attributes.fill || 'black'; } } else { circleLayer.data.color = svg.attributes.fill === 'none' ? svg.attributes.stroke : svg.attributes.fill || 'black'; } circleLayer.data.fill = svg.attributes.fill !== 'none'; if (svg.attributes['fill-opacity']) circleLayer.data.alpha = Number(svg.attributes['fill-opacity']); circleLayer.data.x = Number(svg.attributes.cx) || 0; circleLayer.data.y = Number(svg.attributes.cy) || 0; circleLayer.data.radius = Number(svg.attributes.r); if (svg.attributes.transform) { SVGReader.applyTransform(svg.attributes.transform, circleLayer); } return circleLayer; } static async createLineLayer(svg, svgObject) { const lineLayer = new index_1.LineLayer(); lineLayer.data.id = svg.attributes.id || `LineLayer-${Math.random().toString(36).substring(2, 15)}`; if (svg.attributes.fill && svg.attributes.fill !== 'none') { let id = SVGReader.extractIdFromUrl(svg.attributes.fill); if (id) { let element = SVGReader.findElementById(svgObject, id); if (element.name === "pattern") { lineLayer.data.color = await SVGReader.createPattern(element, svgObject, svg); } else if (['linearGradient', 'radialGradient'].includes(element.name)) { lineLayer.data.color = await SVGReader.createGradient(element, svgObject, svg); } } else { lineLayer.data.color = svg.attributes.fill === 'none' ? svg.attributes.stroke : svg.attributes.fill || 'black'; } } else { lineLayer.data.color = svg.attributes.fill === 'none' ? svg.attributes.stroke : svg.attributes.fill || 'black'; } lineLayer.data.fill = svg.attributes.fill !== 'none'; if (svg.attributes['fill-opacity']) lineLayer.data.alpha = Number(svg.attributes['fill-opacity']); for (let i = 0; i < 2; i++) { lineLayer.data.points.push({ x: Number(svg.attributes[`x${i + 1}`]), y: Number(svg.attributes[`y${i + 1}`]) }); } if (svg.attributes.transform) { SVGReader.applyTransform(svg.attributes.transform, lineLayer); } return lineLayer; } static async createGradient(svg, svgObject, parent) { const gradient = new index_1.Gradient(); gradient.data.id = svg.attributes.id || `Gradient-${Math.random().toString(36).substring(2, 15)}`; gradient.data.gradientType = svg.name === 'linearGradient' ? 'linear' : 'radial'; let baseXOne = (parent.attributes.x !== undefined ? Number(parent.attributes.x) : (parent.attributes.x1 !== undefined ? Number(parent.attributes.x1) : Number(parent.attributes.cx) - Number(parent.attributes.r))); let baseYOne = (parent.attributes.y !== undefined ? Number(parent.attributes.y) : (parent.attributes.y1 !== undefined ? Number(parent.attributes.y1) : Number(parent.attributes.cy) - Number(parent.attributes.r))); let baseXTwo = (parent.attributes.x !== undefined ? Number(parent.attributes.x) : (parent.attributes.x2 !== undefined ? Number(parent.attributes.x2) : Number(parent.attributes.cx) - Number(parent.attributes.r))); let baseYTwo = (parent.attributes.y !== undefined ? Number(parent.attributes.y) : (parent.attributes.y2 !== undefined ? Number(parent.attributes.y2) : Number(parent.attributes.cy) - Number(parent.attributes.r))); gradient.data.points = [ { x: baseXOne + (svg.attributes.x1 !== undefined ? (parent.attributes.width !== undefined ? parent.attributes.width * Number(svg.attributes.x1) : baseXOne * Number(svg.attributes.x1)) : Number(svg.attributes.fx) * (parent.attributes.width !== undefined ? Number(parent.attributes.width) : Number(parent.attributes.r) * 2)), y: baseYOne + (svg.attributes.y1 !== undefined ? (parent.attributes.height !== undefined ? parent.attributes.height * Number(svg.attributes.y1) : baseYOne * Number(svg.attributes.y1)) : Number(svg.attributes.fy) * (parent.attributes.height !== undefined ? Number(parent.attributes.height) : Number(parent.attributes.r) * 2)) }, { x: baseXTwo + (svg.attributes.x2 !== undefined ? (parent.attributes.width !== undefined ? parent.attributes.width * Number(svg.attributes.x2) : baseXTwo * Number(svg.attributes.x2)) : Number(svg.attributes.cx) * (parent.attributes.width !== undefined ? Number(parent.attributes.width) : Number(parent.attributes.r) * 2)), y: baseYTwo + (svg.attributes.y2 !== undefined ? (parent.attributes.height !== undefined ? parent.attributes.height * Number(svg.attributes.y2) : baseYTwo * Number(svg.attributes.y2)) : Number(svg.attributes.cy) * (parent.attributes.height !== undefined ? Number(parent.attributes.height) : Number(parent.attributes.r) * 2)) } ]; if (svg.name === "radialGradient") { gradient.data.radius = Number(parent.attributes.r) ? (Number(parent.attributes.r) * Number(svg.attributes.r)) * 2 : Math.min((Number(parent.attributes.width) * Number(svg.attributes.r)), (Number(parent.attributes.height) * Number(svg.attributes.r))); } for (const colorStop of svg.children) { let color = colorStop.attributes['stop-color']; let offset = colorStop.attributes['offset'].replace('%', ''); gradient.data.colorPoints.push({ color, position: Number(offset) / 100 }); } return gradient; } static async createPattern(svg, svgObject, parent) { const pattern = new index_1.Pattern(); pattern.data.id = svg.attributes.id || `Pattern-${Math.random().toString(36).substring(2, 15)}`; pattern.data.patternType = index_1.PatternType.repeat; let layers = []; for (const child of svg.children) { let layer; switch (child.name) { case 'image': layer = await SVGReader.createImageLayer(child); break; case 'path': layer = await SVGReader.createPath2DLayer(child, svgObject); break; case 'g': const group = await SVGReader.createGroupLayer(child, svgObject); layers.push(...group); break; case 'clipPath': layer = await SVGReader.createClipPathLayer(child); break; case 'use': layer = await SVGReader.handleUseElement(child, svgObject, svg); break; case 'rect': layer = await SVGReader.createRectLayer(child, svgObject); break; case 'ellipse': layer = await SVGReader.createEllipseLayer(child, svgObject); break; case 'circle': layer = await SVGReader.createCircleLayer(child, svgObject); break; case 'line': layer = await SVGReader.createLineLayer(child, svgObject); break; default: console.warn(`Unsupported SVG element: ${child.name}`); } if (layer) { layers.push(layer); } } let width = Number(svg.attributes['width']); let height = Number(svg.attributes['height']); width = width < 1 ? width * (Number(parent.attributes['width']) ? Number(parent.attributes['width']) : (Number(parent.attributes['r']) * 2)) : width; height = height < 1 ? height * (Number(parent.attributes['height']) ? Number(parent.attributes['height']) : (Number(parent.attributes['r']) * 2)) : height; pattern.data.pattern = { data: new index_1.LazyCanvas() .createNewCanvas(width, height) .addLayers(...layers), type: "canvas" }; return pattern; } static async createPath2DLayer(svg, svgObject) { const path2DLayer = new index_1.Path2DLayer(svg.attributes.d); path2DLayer.data.id = svg.attributes.id || `Path2DLayer-${Math.random().toString(36).substring(2, 15)}`; if (svg.attributes.fill && svg.attributes.fill !== 'none') { let id = SVGReader.extractIdFromUrl(svg.attributes.fill); if (id) { let element = SVGReader.findElementById(svgObject, id); if (element.name === "pattern") { path2DLayer.data.color = await SVGReader.createPattern(element, svgObject, svg); } else if (['linearGradient', 'radialGradient'].includes(element.name)) { path2DLayer.data.color = await SVGReader.createGradient(element, svgObject, svg); } } else { path2DLayer.data.fillStyle = svg.attributes.fill === 'none' ? svg.attributes.stroke : svg.attributes.fill || 'black'; } } else { path2DLayer.data.fillStyle = svg.attributes.fill === 'none' ? svg.attributes.stroke : svg.attributes.fill || 'black'; } path2DLayer.data.filled = svg.attributes.fill !== 'none'; if (svg.attributes['fill-opacity']) path2DLayer.data.alpha = Number(svg.attributes['fill-opacity']); path2DLayer.data.lineWidth = svg.attributes['stroke-width'] ? Number(svg.attributes['stroke-width']) : 1; if (svg.attributes.transform) { SVGReader.applyTransform(svg.attributes.transform, path2DLayer); } return path2DLayer; } static async createClipPathLayer(svg) { const clipPathLayer = new index_1.Path2DLayer(svg.children.map((child) => child.attributes.d).join(' ')); clipPathLayer.data.clipPath = true; clipPathLayer.data.id = svg.attributes.id || `Path2DLayer-${Math.random().toString(36).substring(2, 15)}`; if (svg.attributes.transform) { SVGReader.applyTransform(svg.attributes.transform, clipPathLayer); } return clipPathLayer; } //private static async createTextLayer(svg: any, parent: any): Promise<any[] | TextLayer> { // if (svg.children.length !== 0) { // const textGroup = [] // for (const child of svg.children) { // let layer; // switch (child.name) { // case 'text': // layer = await SVGReader.createTextLayer(child, svg); // break; // case 'tspan': // console.log(child) // layer = await SVGReader.createTextLayer(child, svg); // break; // default: // console.warn(`Unsupported SVG element: ${child.name}`); // } // if (layer) { // // @ts-ignore // textGroup.push(...layer); // } // } // return textGroup // } else { // const textLayer = new TextLayer() // textLayer.data.id = svg.attributes.id || `TextLayer-${Math.random().toString(36).substring(2, 15)}`; // textLayer.data.x = svg.attributes.x || parent.attributes.x || 0 // textLayer.data.y = svg.attributes.y || parent.attributes.y || 0 // textLayer.data.size = svg.attributes['font-size'] || parent.attributes['font-size'] || 20 // textLayer.data.align = svg.attributes['anchor'] || parent.attributes['anchor'] || 'left' // // return textLayer // } //} static async createGroupLayer(svg, svgObject) { const groupLayers = []; for (const child of svg.children) { let layer; switch (child.name) { case 'image': layer = await SVGReader.createImageLayer(child); break; case 'path': layer = await SVGReader.createPath2DLayer(child, svgObject); break; case 'g': const group = await SVGReader.createGroupLayer(child, svgObject); groupLayers.push(...group); break; case 'clipPath': layer = await SVGReader.createClipPathLayer(child); break; case 'use': layer = await SVGReader.handleUseElement(child, svgObject, svg); break; case 'rect': layer = await SVGReader.createRectLayer(child, svgObject); break; case 'ellipse': layer = await SVGReader.createEllipseLayer(child, svgObject); break; case 'circle': layer = await SVGReader.createCircleLayer(child, svgObject); break; case 'line': layer = await SVGReader.createLineLayer(child, svgObject); break; default: console.warn(`Unsupported SVG element: ${child.name}`); } if (layer) { groupLayers.push(layer); } } return groupLayers; } static applyTransform(transform, layer) { const matrixMatch = transform.match(/matrix\(([^)]+)\)/); const scaleMatch = transform.match(/scale\(([^)]+)\)/); const translateMatch = transform.match(/translate\(([^)]+)\)/); const rotateMatch = transform.match(/rotate\(([^)]+)\)/); if (matrixMatch) { const [a, b, c, d, e, f] = matrixMatch[1].split(' ').map(Number); layer.data.transform.matrix = { a, b, c, d, e, f }; layer.data.centering = 'legacy'; } else if (scaleMatch) { const [sx, sy] = scaleMatch[1].split(' ').map(Number); layer.data.transform.scale = { x: sx, y: sy }; } else if (translateMatch) { const [tx, ty] = translateMatch[1].split(' ').map(Number); layer.data.transform.translate = { x: tx, y: ty }; } else if (rotateMatch) { const [angle, cx, cy] = rotateMatch[1].split(' ').map(Number); layer.data.transform.rotate = angle; } } static findElementById(svgObject, id) { const queue = [svgObject]; while (queue.length > 0) { const current = queue.shift(); if (current.attributes && current.attributes.id === id) { return current; } if (current.children) { queue.push(...current.children); } } throw new Error(`Element with id ${id} not found`); } static extractIdFromUrl(url) { const regex = /url\(#([^)]+)\)/; const match = url.match(regex); return match ? match[1] : null; } async fillStyle(svg, svgObject) { let fill, style; let id = SVGReader.extractIdFromUrl(svg.attributes.fill); if (id) { let element = SVGReader.findElementById(svgObject, id); if (element.name === "pattern") { fill = await SVGReader.createPattern(element, svgObject, svg); } else if (['linearGradient', 'radialGradient'].includes(element.name)) { fill = await SVGReader.createGradient(element, svgObject, svg); } } else { style = svg.attributes.fill === 'none' ? svg.attributes.stroke : svg.attributes.fill || 'black'; } return { fill, style }; } static async handleUseElement(svg, svgObject, from) { const href = svg.attributes['xlink:href'] || svg.attributes.href; const clipPath = from.attributes['clip-path']; const id = href.replace('#', ''); const referencedElement = SVGReader.findElementById(svgObject, id); let layer; switch (referencedElement.name) { case 'image': if (clipPath) { let id = SVGReader.extractIdFromUrl(from.attributes['clip-path']); if (!id) return console.warn(`Could not extract id from clipPath: ${from.attributes['clip-path']}`); let element = SVGReader.findElementById(svgObject, id); layer = await SVGReader.createEllipseImageLayer(referencedElement, element.children[0].attributes.d); } else { layer = await SVGReader.createImageLayer(referencedElement); } break; case 'path': layer = await SVGReader.createPath2DLayer(referencedElement, svgObject); break; default: throw new Error(`Unsupported element referenced by <use>: ${referencedElement.name}`); } if (svg.attributes.transform) { SVGReader.applyTransform(svg.attributes.transform, layer); } return layer; } static async readSVG(svg) { const svgObject = await SVGReader.parseSVG(svg); const layers = []; for (const svgElement of svgObject.children) { let layer; switch (svgElement.name) { case 'defs': for (const child of svgElement.children) { if (child.name === 'image') { layer = await SVGReader.createImageLayer(child); } else if (child.name === 'clipPath') { layer = await SVGReader.createClipPathLayer(child); } } break; case 'path': layer = await SVGReader.createPath2DLayer(svgElement, svgObject); break; case 'g': const group = await SVGReader.createGroupLayer(svgElement, svgObject); for (const element of group) { let search = layers.find((l) => l.data.id === element.data.id); if (!search) layers.push(element); else { layers.splice(layers.indexOf(search), 1, element); } } break; case 'clipPath': layer = await SVGReader.createClipPathLayer(svgElement); break; case 'use': layer = await SVGReader.handleUseElement(svgElement, svgObject, svgObject); break; case 'rect': layer = await SVGReader.createRectLayer(svgElement, svgObject); break; case 'ellipse': layer = await SVGReader.createEllipseLayer(svgElement, svgObject); break; case 'circle': layer = await SVGReader.createCircleLayer(svgElement, svgObject); break; case 'line': layer = await SVGReader.createLineLayer(svgElement, svgObject); break; //case 'text': // const textGroup = await SVGReader.createTextLayer(svgElement, svgObject); // if (Array.isArray(textGroup)) { // for (const element of textGroup) { // let search = layers.find((l) => l.data.id === element.data.id); // if (!search) layers.push(element); // else { // layers.splice(layers.indexOf(search), 1, element); // } // } // } else { // layer = textGroup; // } // break; default: console.warn(`Unsupported SVG element: ${svgElement.name}`); } if (layer) { let search = layers.find((l) => l.data.id === layer.data.id); if (!search) layers.push(layer); else { layers.splice(layers.indexOf(search), 1, layer); } } } return layers; } } exports.SVGReader = SVGReader;