UNPKG

text-svg

Version:

Convert text to SVG for node.js

284 lines (243 loc) 8.08 kB
const { registerFont, createCanvas } = require('canvas') const stream = require('stream') /** * Convert text to PNG image. * @param text * @param [options] * @param [options.font="30px sans-serif"] css style font * @param [options.textAlign="left"] text alignment (left, center, right) * @param [options.color="black"] (or options.textColor) text color * @param [options.backgroundColor] (or options.bgColor) background color * @param [options.lineSpacing=0] * @param [options.strokeWidth=0] * @param [options.strokeColor='white'] * @param [options.padding=0] width of the padding area (left, top, right, bottom) * @param [options.paddingLeft] * @param [options.paddingTop] * @param [options.paddingRight] * @param [options.paddingBottom] * @param [options.borderWidth=0] width of border (left, top, right, bottom) * @param [options.borderLeftWidth=0] * @param [options.borderTopWidth=0] * @param [options.borderRightWidth=0] * @param [options.borderBottomWidth=0] * @param [options.borderColor="black"] border color * @param [options.localFontPath] path to local font (e.g. fonts/Lobster-Regular.ttf) * @param [options.localFontName] name of local font (e.g. Lobster) * @param [options.maxWidth] maximum width of text * @param [options.wrap] whether text should be wrapped when it exceeds max width * @param [options.output="buffer"] 'buffer', 'stream', 'dataURL', 'canvas's * @param [options.width] * @param [options.height] * @returns {string} svg image buffer */ const text2svg = (text, options = {}) => { // Options options = parseOptions(options) // Register a custom font 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 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 metrics = ctx.measureText(testLine) if (options.wrap && 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 = false || 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 => { // Calculate Y let x = 0 const y = max.ascent + offsetY // Calculate X 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 stream.Readable() readable._read = () => { readable.push(canvas.toBuffer()) readable.push(null) } return readable } case 'dataURL': return canvas.toDataURL('image/svg') 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 || undefined, output: options.output || 'buffer', width: options.width || undefined, height: options.height || undefined } } module.exports = text2svg