text-svg
Version:
Convert text to SVG for node.js
147 lines (146 loc) • 5.95 kB
JavaScript
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