UNPKG

text-svg

Version:

Convert text to SVG for node.js

1 lines 10.9 kB
{"version":3,"file":"index.mjs","names":[],"sources":["../index.ts"],"sourcesContent":["import { Readable } from 'node:stream'\nimport { type Canvas, createCanvas, registerFont } from 'canvas'\nimport type {\n LineProperty,\n MaxMetrics,\n ParsedOptions,\n TextSvgOptions\n} from './types/index.ts'\n\nconst text2svg = (\n text: string,\n inputOptions: TextSvgOptions = {}\n): Buffer | Readable | string | Canvas => {\n // Options\n const options: ParsedOptions = parseOptions(inputOptions)\n\n // Register a custom font\n if (options.localFontPath && options.localFontName) {\n registerFont(options.localFontPath, {\n family: options.localFontName\n })\n }\n\n const canvas = createCanvas(0, 0, 'svg')\n const ctx = canvas.getContext('2d')\n\n const max: MaxMetrics = {\n left: 0,\n right: 0,\n ascent: 0,\n descent: 0\n }\n\n let lastDescent: number = 0\n const lineProps: LineProperty[] = []\n ctx.font = options.font\n text.split('\\n').forEach((line) => {\n const words = line.split(' ')\n let newLine = ''\n let left: number, right: number, ascent: number, descent: number\n\n for (let n = 0; n < words.length; n++) {\n let testLine: string\n if (n === 0) {\n testLine = words[n]\n } else {\n testLine = `${newLine} ${words[n]}`\n }\n\n const metrics = ctx.measureText(testLine)\n\n if (\n options.wrap &&\n options.maxWidth &&\n metrics.width > options.maxWidth &&\n n > 0\n ) {\n const metrics = ctx.measureText(newLine)\n left = -1 * metrics.actualBoundingBoxLeft\n right = metrics.actualBoundingBoxRight\n ascent = metrics.actualBoundingBoxAscent\n descent = metrics.actualBoundingBoxDescent\n\n max.left = Math.max(max.left, left)\n max.right = Math.max(max.right, right)\n max.ascent = Math.max(max.ascent, ascent)\n max.descent = Math.max(max.descent, descent)\n lastDescent = descent\n lineProps.push({\n line: newLine,\n left,\n right,\n ascent,\n descent\n })\n newLine = words[n]\n } else {\n newLine = testLine\n }\n }\n\n const metrics = ctx.measureText(newLine)\n\n left = -1 * metrics.actualBoundingBoxLeft\n right = metrics.actualBoundingBoxRight\n ascent = metrics.actualBoundingBoxAscent\n descent = metrics.actualBoundingBoxDescent\n\n max.left = Math.max(max.left, left)\n max.right = Math.max(max.right, right)\n max.ascent = Math.max(max.ascent, ascent)\n max.descent = Math.max(max.descent, descent)\n lastDescent = descent\n\n lineProps.push({\n line: newLine,\n left,\n right,\n ascent,\n descent\n })\n })\n\n const lineHeight = max.ascent + max.descent + options.lineSpacing\n\n const contentWidth = options.width ? options.width : max.left + max.right\n const contentHeight =\n lineHeight * lineProps.length -\n options.lineSpacing -\n (max.descent - lastDescent)\n\n if (options.width) {\n canvas.width = options.width\n } else {\n canvas.width =\n Math.ceil(contentWidth) +\n options.borderLeftWidth +\n options.borderRightWidth +\n options.paddingLeft +\n options.paddingRight\n }\n\n if (options.height) {\n canvas.height = options.height\n } else {\n canvas.height =\n Math.ceil(contentHeight) +\n options.borderTopWidth +\n options.borderBottomWidth +\n options.paddingTop +\n options.paddingBottom\n }\n\n const hasBorder =\n options.borderLeftWidth > 0 ||\n options.borderTopWidth > 0 ||\n options.borderRightWidth > 0 ||\n options.borderBottomWidth > 0\n\n if (hasBorder) {\n ctx.fillStyle = options.borderColor\n ctx.fillRect(0, 0, canvas.width, canvas.height)\n }\n\n if (options.backgroundColor) {\n ctx.fillStyle = options.backgroundColor\n ctx.fillRect(\n options.borderLeftWidth,\n options.borderTopWidth,\n canvas.width - (options.borderLeftWidth + options.borderRightWidth),\n canvas.height - (options.borderTopWidth + options.borderBottomWidth)\n )\n } else if (hasBorder) {\n ctx.clearRect(\n options.borderLeftWidth,\n options.borderTopWidth,\n canvas.width - (options.borderLeftWidth + options.borderRightWidth),\n canvas.height - (options.borderTopWidth + options.borderBottomWidth)\n )\n }\n\n ctx.font = options.font\n ctx.fillStyle = options.textColor\n ctx.antialias = 'gray'\n ctx.textAlign = options.textAlign\n ctx.lineWidth = options.strokeWidth\n ctx.strokeStyle = options.strokeColor\n\n let offsetY = options.borderTopWidth + options.paddingTop\n lineProps.forEach((lineProp) => {\n // Calculate Y\n let x = 0\n const y = max.ascent + offsetY\n\n // Calculate X\n switch (options.textAlign) {\n case 'start':\n case 'left':\n x = lineProp.left + options.borderLeftWidth + options.paddingLeft\n break\n\n case 'end':\n case 'right':\n x =\n canvas.width -\n lineProp.left -\n options.borderRightWidth -\n options.paddingRight\n break\n\n case 'center':\n x = contentWidth / 2 + options.borderLeftWidth + options.paddingLeft\n break\n }\n\n ctx.fillText(lineProp.line, x, y)\n\n if (options.strokeWidth > 0) {\n ctx.strokeText(lineProp.line, x, y)\n }\n\n offsetY += lineHeight\n })\n\n switch (options.output) {\n case 'buffer':\n return canvas.toBuffer()\n case 'stream': {\n return Readable.from(canvas.toBuffer())\n }\n case 'dataURL':\n return canvas.toDataURL()\n case 'canvas':\n return canvas\n default:\n throw new Error(`output type:${options.output} is not supported.`)\n }\n}\n\nfunction parseOptions(options: TextSvgOptions): ParsedOptions {\n return {\n font: options.font || '30px sans-serif',\n textAlign: options.textAlign || 'left',\n textColor: options.textColor || options.color || 'black',\n backgroundColor: options.bgColor || options.backgroundColor || null,\n lineSpacing: options.lineSpacing || 0,\n\n strokeWidth: options.strokeWidth || 0,\n strokeColor: options.strokeColor || 'white',\n\n paddingLeft: options.paddingLeft || options.padding || 0,\n paddingTop: options.paddingTop || options.padding || 0,\n paddingRight: options.paddingRight || options.padding || 0,\n paddingBottom: options.paddingBottom || options.padding || 0,\n\n borderLeftWidth: options.borderLeftWidth || options.borderWidth || 0,\n borderTopWidth: options.borderTopWidth || options.borderWidth || 0,\n borderBottomWidth: options.borderBottomWidth || options.borderWidth || 0,\n borderRightWidth: options.borderRightWidth || options.borderWidth || 0,\n borderColor: options.borderColor ?? 'black',\n\n localFontName: options.localFontName || null,\n localFontPath: options.localFontPath || null,\n\n wrap: options.wrap || false,\n maxWidth: options.maxWidth || undefined,\n\n output: options.output || 'buffer',\n\n width: options.width || undefined,\n height: options.height || undefined\n }\n}\n\nexport default text2svg\nexport { type ParsedOptions, type TextSvgOptions, text2svg }\n"],"mappings":"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"}