UNPKG

text-svg

Version:

Convert text to SVG for node.js

147 lines (146 loc) 5.95 kB
import { Readable } from "node:stream"; import { createCanvas, registerFont } from "canvas"; //#region index.ts const text2svg = (text, inputOptions = {}) => { const options = parseOptions(inputOptions); if (options.localFontPath && options.localFontName) registerFont(options.localFontPath, { family: options.localFontName }); const canvas = createCanvas(0, 0, "svg"); const ctx = canvas.getContext("2d"); const max = { left: 0, right: 0, ascent: 0, descent: 0 }; let lastDescent = 0; const lineProps = []; ctx.font = options.font; text.split("\n").forEach((line) => { const words = line.split(" "); let newLine = ""; let left, right, ascent, descent; for (let n = 0; n < words.length; n++) { let testLine; if (n === 0) testLine = words[n]; else testLine = `${newLine} ${words[n]}`; const metrics = ctx.measureText(testLine); if (options.wrap && options.maxWidth && metrics.width > options.maxWidth && n > 0) { const metrics = ctx.measureText(newLine); left = -1 * metrics.actualBoundingBoxLeft; right = metrics.actualBoundingBoxRight; ascent = metrics.actualBoundingBoxAscent; descent = metrics.actualBoundingBoxDescent; max.left = Math.max(max.left, left); max.right = Math.max(max.right, right); max.ascent = Math.max(max.ascent, ascent); max.descent = Math.max(max.descent, descent); lastDescent = descent; lineProps.push({ line: newLine, left, right, ascent, descent }); newLine = words[n]; } else newLine = testLine; } const metrics = ctx.measureText(newLine); left = -1 * metrics.actualBoundingBoxLeft; right = metrics.actualBoundingBoxRight; ascent = metrics.actualBoundingBoxAscent; descent = metrics.actualBoundingBoxDescent; max.left = Math.max(max.left, left); max.right = Math.max(max.right, right); max.ascent = Math.max(max.ascent, ascent); max.descent = Math.max(max.descent, descent); lastDescent = descent; lineProps.push({ line: newLine, left, right, ascent, descent }); }); const lineHeight = max.ascent + max.descent + options.lineSpacing; const contentWidth = options.width ? options.width : max.left + max.right; const contentHeight = lineHeight * lineProps.length - options.lineSpacing - (max.descent - lastDescent); if (options.width) canvas.width = options.width; else canvas.width = Math.ceil(contentWidth) + options.borderLeftWidth + options.borderRightWidth + options.paddingLeft + options.paddingRight; if (options.height) canvas.height = options.height; else canvas.height = Math.ceil(contentHeight) + options.borderTopWidth + options.borderBottomWidth + options.paddingTop + options.paddingBottom; const hasBorder = options.borderLeftWidth > 0 || options.borderTopWidth > 0 || options.borderRightWidth > 0 || options.borderBottomWidth > 0; if (hasBorder) { ctx.fillStyle = options.borderColor; ctx.fillRect(0, 0, canvas.width, canvas.height); } if (options.backgroundColor) { ctx.fillStyle = options.backgroundColor; ctx.fillRect(options.borderLeftWidth, options.borderTopWidth, canvas.width - (options.borderLeftWidth + options.borderRightWidth), canvas.height - (options.borderTopWidth + options.borderBottomWidth)); } else if (hasBorder) ctx.clearRect(options.borderLeftWidth, options.borderTopWidth, canvas.width - (options.borderLeftWidth + options.borderRightWidth), canvas.height - (options.borderTopWidth + options.borderBottomWidth)); ctx.font = options.font; ctx.fillStyle = options.textColor; ctx.antialias = "gray"; ctx.textAlign = options.textAlign; ctx.lineWidth = options.strokeWidth; ctx.strokeStyle = options.strokeColor; let offsetY = options.borderTopWidth + options.paddingTop; lineProps.forEach((lineProp) => { let x = 0; const y = max.ascent + offsetY; switch (options.textAlign) { case "start": case "left": x = lineProp.left + options.borderLeftWidth + options.paddingLeft; break; case "end": case "right": x = canvas.width - lineProp.left - options.borderRightWidth - options.paddingRight; break; case "center": x = contentWidth / 2 + options.borderLeftWidth + options.paddingLeft; break; } ctx.fillText(lineProp.line, x, y); if (options.strokeWidth > 0) ctx.strokeText(lineProp.line, x, y); offsetY += lineHeight; }); switch (options.output) { case "buffer": return canvas.toBuffer(); case "stream": return Readable.from(canvas.toBuffer()); case "dataURL": return canvas.toDataURL(); case "canvas": return canvas; default: throw new Error(`output type:${options.output} is not supported.`); } }; function parseOptions(options) { return { font: options.font || "30px sans-serif", textAlign: options.textAlign || "left", textColor: options.textColor || options.color || "black", backgroundColor: options.bgColor || options.backgroundColor || null, lineSpacing: options.lineSpacing || 0, strokeWidth: options.strokeWidth || 0, strokeColor: options.strokeColor || "white", paddingLeft: options.paddingLeft || options.padding || 0, paddingTop: options.paddingTop || options.padding || 0, paddingRight: options.paddingRight || options.padding || 0, paddingBottom: options.paddingBottom || options.padding || 0, borderLeftWidth: options.borderLeftWidth || options.borderWidth || 0, borderTopWidth: options.borderTopWidth || options.borderWidth || 0, borderBottomWidth: options.borderBottomWidth || options.borderWidth || 0, borderRightWidth: options.borderRightWidth || options.borderWidth || 0, borderColor: options.borderColor ?? "black", localFontName: options.localFontName || null, localFontPath: options.localFontPath || null, wrap: options.wrap || false, maxWidth: options.maxWidth || void 0, output: options.output || "buffer", width: options.width || void 0, height: options.height || void 0 }; } //#endregion export { text2svg as default, text2svg }; //# sourceMappingURL=index.mjs.map