UNPKG

text-to-svg

Version:

Convert text to SVG path without native dependence.

193 lines (156 loc) 5.4 kB
/** * Copyright (c) 2016 Hideki Shiro */ import * as opentype from 'opentype.js'; const DEFAULT_FONT = require('path').join(__dirname, '../fonts/ipag.ttf'); // Private method function parseAnchorOption(anchor) { let horizontal = anchor.match(/left|center|right/gi) || []; horizontal = horizontal.length === 0 ? 'left' : horizontal[0]; let vertical = anchor.match(/baseline|top|bottom|middle/gi) || []; vertical = vertical.length === 0 ? 'baseline' : vertical[0]; return { horizontal, vertical }; } export default class TextToSVG { constructor(font) { this.font = font; } static loadSync(file = DEFAULT_FONT) { return new TextToSVG(opentype.loadSync(file)); } static load(url, cb) { opentype.load(url, (err, font) => { if (err !== null) { return cb(err, null); } return cb(null, new TextToSVG(font)); }); } getWidth(text, options) { const fontSize = options.fontSize || 72; const kerning = 'kerning' in options ? options.kerning : true; const fontScale = 1 / this.font.unitsPerEm * fontSize; let width = 0; const glyphs = this.font.stringToGlyphs(text); for (let i = 0; i < glyphs.length; i++) { const glyph = glyphs[i]; if (glyph.advanceWidth) { width += glyph.advanceWidth * fontScale; } if (kerning && i < glyphs.length - 1) { const kerningValue = this.font.getKerningValue(glyph, glyphs[i + 1]); width += kerningValue * fontScale; } if (options.letterSpacing) { width += options.letterSpacing * fontSize; } else if (options.tracking) { width += (options.tracking / 1000) * fontSize; } } return width; } getHeight(fontSize) { const fontScale = 1 / this.font.unitsPerEm * fontSize; return (this.font.ascender - this.font.descender) * fontScale; } getMetrics(text, options = {}) { const fontSize = options.fontSize || 72; const anchor = parseAnchorOption(options.anchor || ''); const width = this.getWidth(text, options); const height = this.getHeight(fontSize); const fontScale = 1 / this.font.unitsPerEm * fontSize; const ascender = this.font.ascender * fontScale; const descender = this.font.descender * fontScale; let x = options.x || 0; switch (anchor.horizontal) { case 'left': x -= 0; break; case 'center': x -= width / 2; break; case 'right': x -= width; break; default: throw new Error(`Unknown anchor option: ${anchor.horizontal}`); } let y = options.y || 0; switch (anchor.vertical) { case 'baseline': y -= ascender; break; case 'top': y -= 0; break; case 'middle': y -= height / 2; break; case 'bottom': y -= height; break; default: throw new Error(`Unknown anchor option: ${anchor.vertical}`); } const baseline = y + ascender; return { x, y, baseline, width, height, ascender, descender, }; } getD(text, options = {}) { const fontSize = options.fontSize || 72; const kerning = 'kerning' in options ? options.kerning : true; const letterSpacing = 'letterSpacing' in options ? options.letterSpacing : false; const tracking = 'tracking' in options ? options.tracking : false; const metrics = this.getMetrics(text, options); const path = this.font.getPath(text, metrics.x, metrics.baseline, fontSize, { kerning, letterSpacing, tracking }); return path.toPathData(); } getPath(text, options = {}) { const attributes = Object.keys(options.attributes || {}) .map(key => `${key}="${options.attributes[key]}"`) .join(' '); const d = this.getD(text, options); if (attributes) { return `<path ${attributes} d="${d}"/>`; } return `<path d="${d}"/>`; } getSVG(text, options = {}) { const metrics = this.getMetrics(text, options); let svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${metrics.width}" height="${metrics.height}">`; svg += this.getPath(text, options); svg += '</svg>'; return svg; } getDebugSVG(text, options = {}) { options = JSON.parse(JSON.stringify(options)); options.x = options.x || 0; options.y = options.y || 0; const metrics = this.getMetrics(text, options); const box = { width: Math.max(metrics.x + metrics.width, 0) - Math.min(metrics.x, 0), height: Math.max(metrics.y + metrics.height, 0) - Math.min(metrics.y, 0), }; const origin = { x: box.width - Math.max(metrics.x + metrics.width, 0), y: box.height - Math.max(metrics.y + metrics.height, 0), }; // Shift text based on origin options.x += origin.x; options.y += origin.y; let svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${box.width}" height="${box.height}">`; svg += `<path fill="none" stroke="red" stroke-width="1" d="M0,${origin.y}L${box.width},${origin.y}"/>`; // X Axis svg += `<path fill="none" stroke="red" stroke-width="1" d="M${origin.x},0L${origin.x},${box.height}"/>`; // Y Axis svg += this.getPath(text, options); svg += '</svg>'; return svg; } } module.exports = exports.default;