UNPKG

text-svg

Version:

Convert text to SVG for node.js

189 lines (188 loc) 6.58 kB
// index.ts import { registerFont, createCanvas } from "canvas"; import { Readable } from "stream"; var 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 = []; text.split("\n").forEach((line) => { ctx.font = options.font; 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 metrics2 = ctx.measureText(testLine); if (options.wrap && options.maxWidth && metrics2.width > options.maxWidth && n > 0) { const metrics3 = ctx.measureText(newLine); left = -1 * metrics3.actualBoundingBoxLeft; right = metrics3.actualBoundingBoxRight; ascent = metrics3.actualBoundingBoxAscent; descent = metrics3.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 || options.borderTopWidth || options.borderRightWidth || options.borderBottomWidth; 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": { const readable = new Readable(); readable._read = () => { readable.push(canvas.toBuffer()); readable.push(null); }; return readable; } 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 }; } var index_default = text2svg; export { index_default as default, text2svg };