Skip to content

Instantly share code, notes, and snippets.

@folknor
Created August 8, 2023 07:07
Show Gist options
  • Save folknor/da97a716bd77b1d2f12f404e19601e10 to your computer and use it in GitHub Desktop.
Save folknor/da97a716bd77b1d2f12f404e19601e10 to your computer and use it in GitHub Desktop.
/**
* Copyright (c) 2016 Hideki Shiro
* Manually adapted for private project use by https://github.com/folknor because
* the original repository hasn't been updated in a while and there are
* pending pull requests and other issues that I needed.
*/
import opentype from "opentype.js"
// 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 }
}
class TextToSVG {
constructor(font) {
this.font = font
}
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 = {}) {
return this.getDAndMetrics(text, options).d
}
getDAndMetrics(text, options = {}) {
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, options.fontSize || 72, Object.assign({}, options, { kerning, letterSpacing, tracking }))
const d = path.toPathData()
return { d, metrics }
}
getPath(text, options) {
return this.getPathAndMetrics(text, options).path
}
getPathAndMetrics(text, options = {}) {
const attributes = Object.keys(options.attributes || {})
.map(key => `${key}="${options.attributes[key]}"`)
.join(" ")
const { d, metrics } = this.getDAndMetrics(text, options)
let path
if (attributes) {
path = `<path ${attributes} d="${d}"/>`
} else {
path = `<path d="${d}"/>`
}
return { path, metrics }
}
getSVG(text, options) {
return this.getSVGAndMetrics(text, options).svg
}
getSVGAndMetrics(text, options) {
const { path, metrics } = this.getPathAndMetrics(text, options)
let svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 ${metrics.width.toFixed(2)} ${metrics.height.toFixed(2)}">`
svg += path
svg += "</svg>"
return { svg, metrics }
}
getDebugSVG(text, options = {}) {
options = JSON.parse(JSON.stringify(options))
options.x = options.x || 0
options.y = options.y || 0
const { path, metrics } = this.getPathAndMetrics(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" viewBox="0 0 "${metrics.width}" "${metrics.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 += path
svg += "</svg>"
return svg
}
}
export default TextToSVG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment