完成svg2ttf的语法移植

master
yutent 2024-02-04 16:45:25 +08:00
parent 9e1bb413ff
commit a064679ddf
24 changed files with 2823 additions and 5 deletions

View File

@ -15,9 +15,10 @@
"build": "esbuild src/index.js --minify --bundle --format=esm --target=esnext --outfile=dist/index.js"
},
"dependencies": {
"@xmldom/xmldom": "^0.8.10",
"cubic2quad": "^1.2.1",
"iofs": "^1.5.3",
"svg2ttf": "^6.0.3",
"ttf2svg": "^1.2.0",
"svgpath": "^2.6.0",
"ttf2woff2": "^5.0.0"
}
}

View File

@ -3,3 +3,5 @@
* @author yutent<yutent.io@gmail.com>
* @date 2024/01/31 18:38:35
*/
export { svg2ttf } from './lib/core.js'

View File

@ -5,8 +5,10 @@
*/
import fs from 'iofs'
import svg2ttf from 'svg2ttf'
import ttf2svg from 'ttf2svg'
import ttf2woff2 from 'ttf2woff2'
import { svg2ttf } from './svg2ttf/index.js'
// import ttf2svg from 'ttf2svg'
// import ttf2woff2 from 'ttf2woff2'
export default {}
export { svg2ttf }

216
src/lib/svg2ttf/index.js Normal file
View File

@ -0,0 +1,216 @@
/*
* Copyright: Vitaly Puzrin
* Author: Sergey Batishchev <snb2003@rambler.ru>
*
* Written for fontello.com project.
*/
import { encode, decode } from './lib/ucs2.js'
import SvgPath from 'svgpath'
import { load, cubicToQuad, toSfntCoutours } from './lib/svg.js'
import { Font, Glyph, Contour, Point } from './lib/sfnt.js'
import { generateTTF } from './lib/ttf.js'
let VERSION_RE = /^(Version )?(\d+[.]\d+)$/i
export function svg2ttf(svgString, options) {
let font = new Font()
let svgFont = load(svgString)
options = options || {}
font.id = options.id || svgFont.id
font.familyName = options.familyname || svgFont.familyName || svgFont.id
font.copyright = options.copyright || svgFont.metadata
font.description = options.description || ''
font.url = options.url || ''
font.sfntNames.push({
id: 2,
value: options.subfamilyname || svgFont.subfamilyName || 'Regular'
}) // subfamily name
font.sfntNames.push({ id: 4, value: options.fullname || svgFont.id }) // full name
let versionString = options.version || 'Version 1.0'
if (!VERSION_RE.test(versionString)) {
throw new Error(
'svg2ttf: invalid option, version - "' + options.version + '"'
)
}
versionString = 'Version ' + versionString.match(VERSION_RE)[2]
font.sfntNames.push({ id: 5, value: versionString }) // version ID for TTF name table
font.sfntNames.push({
id: 6,
value: (options.fullname || svgFont.id)
.replace(/[\s\(\)\[\]<>%\/]/g, '')
.substr(0, 62)
}) // Postscript name for the font, required for OSX Font Book
if (typeof options.ts !== 'undefined') {
font.createdDate = font.modifiedDate = new Date(
parseInt(options.ts, 10) * 1000
)
}
// Try to fill font metrics or guess defaults
//
font.unitsPerEm = svgFont.unitsPerEm || 1000
font.horizOriginX = svgFont.horizOriginX || 0
font.horizOriginY = svgFont.horizOriginY || 0
font.vertOriginX = svgFont.vertOriginX || 0
font.vertOriginY = svgFont.vertOriginY || 0
font.width = svgFont.width || svgFont.unitsPerEm
font.height = svgFont.height || svgFont.unitsPerEm
font.descent = !isNaN(svgFont.descent) ? svgFont.descent : -font.vertOriginY
font.ascent = svgFont.ascent || font.unitsPerEm - font.vertOriginY
// Values for font substitution. We're mostly working with icon fonts, so they aren't expected to be substituted.
// https://docs.microsoft.com/en-us/typography/opentype/spec/os2#sxheight
font.capHeight = svgFont.capHeight || 0 // 0 is a valid value if "H" glyph doesn't exist
font.xHeight = svgFont.xHeight || 0 // 0 is a valid value if "x" glyph doesn't exist
if (typeof svgFont.weightClass !== 'undefined') {
let wght = parseInt(svgFont.weightClass, 10)
if (!isNaN(wght)) font.weightClass = wght
else {
// Unknown names are silently ignored
if (svgFont.weightClass === 'normal') font.weightClass = 400
if (svgFont.weightClass === 'bold') font.weightClass = 700
}
}
if (typeof svgFont.underlinePosition !== 'undefined') {
font.underlinePosition = svgFont.underlinePosition
}
if (typeof svgFont.underlineThickness !== 'undefined') {
font.underlineThickness = svgFont.underlineThickness
}
let glyphs = font.glyphs
let codePoints = font.codePoints
let ligatures = font.ligatures
function addCodePoint(codePoint, glyph) {
if (codePoints[codePoint]) {
// Ignore code points already defined
return false
}
codePoints[codePoint] = glyph
return true
}
// add SVG glyphs to SFNT font
svgFont.glyphs.forEach(function (svgGlyph) {
let glyph = new Glyph()
glyph.name = svgGlyph.name
glyph.codes = svgGlyph.ligatureCodes || svgGlyph.unicode // needed for nice validator error output
glyph.d = svgGlyph.d
glyph.height = !isNaN(svgGlyph.height) ? svgGlyph.height : font.height
glyph.width = !isNaN(svgGlyph.width) ? svgGlyph.width : font.width
glyphs.push(glyph)
svgGlyph.sfntGlyph = glyph
svgGlyph.unicode.forEach(function (codePoint) {
addCodePoint(codePoint, glyph)
})
})
let missingGlyph
// add missing glyph to SFNT font
// also, check missing glyph existance and single instance
if (svgFont.missingGlyph) {
missingGlyph = new Glyph()
missingGlyph.d = svgFont.missingGlyph.d
missingGlyph.height = !isNaN(svgFont.missingGlyph.height)
? svgFont.missingGlyph.height
: font.height
missingGlyph.width = !isNaN(svgFont.missingGlyph.width)
? svgFont.missingGlyph.width
: font.width
} else {
missingGlyph = glyphs.find(function (glyph) {
return glyph.name === '.notdef'
})
}
if (!missingGlyph) {
// no missing glyph and .notdef glyph, we need to create missing glyph
missingGlyph = new Glyph()
}
// Create glyphs for all characters used in ligatures
svgFont.ligatures.forEach(function (svgLigature) {
let ligature = {
ligature: svgLigature.ligature,
unicode: svgLigature.unicode,
glyph: svgLigature.glyph.sfntGlyph
}
ligature.unicode.forEach(function (charPoint) {
// We need to have a distinct glyph for each code point so we can reference it in GSUB
let glyph = new Glyph()
let added = addCodePoint(charPoint, glyph)
if (added) {
glyph.name = encode([charPoint])
glyphs.push(glyph)
}
})
ligatures.push(ligature)
})
// Missing Glyph needs to have index 0
if (glyphs.indexOf(missingGlyph) !== -1) {
glyphs.splice(glyphs.indexOf(missingGlyph), 1)
}
glyphs.unshift(missingGlyph)
let nextID = 0
//add IDs
glyphs.forEach(function (glyph) {
glyph.id = nextID
nextID++
// Calculate accuracy for cubicToQuad transformation
// For glyphs with height and width smaller than 500 use relative 0.06% accuracy,
// for larger glyphs use fixed accuracy 0.3.
let glyphSize = Math.max(glyph.width, glyph.height)
let accuracy = glyphSize > 500 ? 0.3 : glyphSize * 0.0006
//SVG transformations
let svgPath = new SvgPath(glyph.d)
.abs()
.unshort()
.unarc()
.iterate(function (segment, index, x, y) {
return cubicToQuad(segment, index, x, y, accuracy)
})
let sfntContours = toSfntCoutours(svgPath)
// Add contours to SFNT font
glyph.contours = sfntContours.map(function (sfntContour) {
let contour = new Contour()
contour.points = sfntContour.map(function (sfntPoint) {
let point = new Point()
point.x = sfntPoint.x
point.y = sfntPoint.y
point.onCurve = sfntPoint.onCurve
return point
})
return contour
})
})
let ttf = generateTTF(font)
return ttf
}

View File

@ -0,0 +1,59 @@
/**
* {Point}
* @author yutent<yutent.io@gmail.com>
* @date 2024/02/04 11:53:48
*/
export class Point {
constructor(x, y) {
this.x = x
this.y = y
}
add(point) {
return new Point(this.x + point.x, this.y + point.y)
}
sub(point) {
return new Point(this.x - point.x, this.y - point.y)
}
mul(value) {
return new Point(this.x * value, this.y * value)
}
div(value) {
return new Point(this.x / value, this.y / value)
}
dist() {
return Math.sqrt(this.x * this.x + this.y * this.y)
}
sqr() {
return this.x * this.x + this.y * this.y
}
}
/*
* Check if 3 points are in line, and second in the midle.
* Used to replace quad curves with lines or join lines
*
*/
export function isInLine(p1, m, p2, accuracy) {
let a = p1.sub(m).sqr()
let b = p2.sub(m).sqr()
let c = p1.sub(p2).sqr()
// control point not between anchors
if (a > b + c || b > a + c) {
return false
}
// count distance via scalar multiplication
let distance = Math.sqrt(
Math.pow((p1.x - m.x) * (p2.y - m.y) - (p2.x - m.x) * (p1.y - m.y), 2) / c
)
return distance < accuracy ? true : false
}

View File

@ -0,0 +1,188 @@
// Light implementation of binary buffer with helpers for easy access.
// https://github.com/fontello/microbuffer
function createArray(size) {
return new Uint8Array(size)
}
export class ByteBuffer {
constructor(buffer, start, length) {
let isInherited = buffer instanceof MicroBuffer
this.buffer = isInherited
? buffer.buffer
: typeof buffer === 'number'
? createArray(buffer)
: buffer
this.start = (start || 0) + (isInherited ? buffer.start : 0)
this.length = length || this.buffer.length - this.start
this.offset = 0
this.isTyped = !Array.isArray(this.buffer)
}
getUint8(pos) {
return this.buffer[pos + this.start]
}
getUint16(pos, littleEndian) {
let val
if (littleEndian) {
throw new Error('not implemented')
} else {
val = this.buffer[pos + 1 + this.start]
val += (this.buffer[pos + this.start] << 8) >>> 0
}
return val
}
getUint32(pos, littleEndian) {
let val
if (littleEndian) {
throw new Error('not implemented')
} else {
val = this.buffer[pos + 1 + this.start] << 16
val |= this.buffer[pos + 2 + this.start] << 8
val |= this.buffer[pos + 3 + this.start]
val += (this.buffer[pos + this.start] << 24) >>> 0
}
return val
}
setUint8(pos, value) {
this.buffer[pos + this.start] = value & 0xff
}
setUint16(pos, value, littleEndian) {
let offset = pos + this.start
let buf = this.buffer
if (littleEndian) {
buf[offset] = value & 0xff
buf[offset + 1] = (value >>> 8) & 0xff
} else {
buf[offset] = (value >>> 8) & 0xff
buf[offset + 1] = value & 0xff
}
}
setUint32(pos, value, littleEndian) {
let offset = pos + this.start
let buf = this.buffer
if (littleEndian) {
buf[offset] = value & 0xff
buf[offset + 1] = (value >>> 8) & 0xff
buf[offset + 2] = (value >>> 16) & 0xff
buf[offset + 3] = (value >>> 24) & 0xff
} else {
buf[offset] = (value >>> 24) & 0xff
buf[offset + 1] = (value >>> 16) & 0xff
buf[offset + 2] = (value >>> 8) & 0xff
buf[offset + 3] = value & 0xff
}
}
writeUint8(value) {
this.buffer[this.offset + this.start] = value & 0xff
this.offset++
}
writeInt8(value) {
this.setUint8(this.offset, value < 0 ? 0xff + value + 1 : value)
this.offset++
}
writeUint16(value, littleEndian) {
this.setUint16(this.offset, value, littleEndian)
this.offset += 2
}
writeInt16(value, littleEndian) {
this.setUint16(
this.offset,
value < 0 ? 0xffff + value + 1 : value,
littleEndian
)
this.offset += 2
}
writeUint32(value, littleEndian) {
this.setUint32(this.offset, value, littleEndian)
this.offset += 4
}
writeInt32(value, littleEndian) {
this.setUint32(
this.offset,
value < 0 ? 0xffffffff + value + 1 : value,
littleEndian
)
this.offset += 4
}
// get current position
//
tell() {
return this.offset
}
// set current position
//
seek(pos) {
this.offset = pos
}
fill(value) {
let index = this.length - 1
while (index >= 0) {
this.buffer[index + this.start] = value
index--
}
}
writeUint64(value) {
// we canot use bitwise operations for 64bit values because of JavaScript limitations,
// instead we should divide it to 2 Int32 numbers
// 2^32 = 4294967296
let hi = Math.floor(value / 4294967296)
let lo = value - hi * 4294967296
this.writeUint32(hi)
this.writeUint32(lo)
}
writeBytes(data) {
let buffer = this.buffer
let offset = this.offset + this.start
if (this.isTyped) {
buffer.set(data, offset)
} else {
for (let i = 0; i < data.length; i++) {
buffer[i + offset] = data[i]
}
}
this.offset += data.length
}
toString(offset, length) {
// default values if not set
offset = offset || 0
length = length || this.length - offset
// add buffer shift
let start = offset + this.start
let end = start + length
let string = ''
for (let i = start; i < end; i++) {
string += String.fromCharCode(this.buffer[i])
}
return string
}
toArray() {
if (this.isTyped) {
return this.buffer.subarray(this.start, this.start + this.length)
}
return this.buffer.slice(this.start, this.start + this.length)
}
}

379
src/lib/svg2ttf/lib/sfnt.js Normal file
View File

@ -0,0 +1,379 @@
/**
* {description of this file}
* @author yutent<yutent.io@gmail.com>
* @date 2024/02/04 12:16:26
*/
export class Font {
ascent = 850
copyright = ''
createdDate = new Date()
glyphs = []
ligatures = []
// Maping of code points to glyphs.
// Keys are actually numeric, thus should be `parseInt`ed.
codePoints = {}
isFixedPitch = 0
italicAngle = 0
familyClass = 0 // No Classification
familyName = ''
// 0x40 - REGULAR - Characters are in the standard weight/style for the font
// 0x80 - USE_TYPO_METRICS - use OS/2.sTypoAscender - OS/2.sTypoDescender + OS/2.sTypoLineGap as the default line spacing
// https://docs.microsoft.com/en-us/typography/opentype/spec/os2#fsselection
// https://github.com/fontello/svg2ttf/issues/95
fsSelection = 0x40 | 0x80
// Non zero value can cause issues in IE, https://github.com/fontello/svg2ttf/issues/45
fsType = 0
lowestRecPPEM = 8
macStyle = 0
modifiedDate = new Date()
panose = {
familyType: 2, // Latin Text
serifStyle: 0, // any
weight: 5, // book
proportion: 3, //modern
contrast: 0, //any
strokeVariation: 0, //any,
armStyle: 0, //any,
letterform: 0, //any,
midline: 0, //any,
xHeight: 0 //any,
}
revision = 1
sfntNames = []
underlineThickness = 0
unitsPerEm = 1000
weightClass = 400 // normal
width = 1000
widthClass = 5 // Medium (normal)
ySubscriptXOffset = 0
ySuperscriptXOffset = 0
int_descent = -150
xHeight = 0
capHeight = 0
/* */
get descent() {
return this.int_descent
}
set descent(value) {
this.int_descent = parseInt(Math.round(-Math.abs(value)), 10)
}
get avgCharWidth() {
if (this.glyphs.length === 0) {
return 0
}
let widths = this.glyphs.map(it => it.width)
return ~~(
widths.reduce(function (prev, cur) {
return prev + cur
}) / widths.length
)
}
get ySubscriptXSize() {
return ~~(void 0 === this.int_ySubscriptXSize
? this.width * 0.6347
: this.int_ySubscriptXSize)
}
set ySubscriptXSize(value) {
this.int_ySubscriptXSize = value
}
get ySubscriptYSize() {
return ~~(void 0 === this.int_ySubscriptYSize
? (this.ascent - this.descent) * 0.7
: this.int_ySubscriptYSize)
}
set ySubscriptYSize(value) {
this.int_ySubscriptYSize = value
}
get ySubscriptYOffset() {
return ~~(void 0 === this.int_ySubscriptYOffset
? (this.ascent - this.descent) * 0.14
: this.int_ySubscriptYOffset)
}
set ySubscriptYOffset(value) {
this.int_ySubscriptYOffset = value
}
get ySuperscriptXSize() {
return ~~(void 0 === this.int_ySuperscriptXSize
? this.width * 0.6347
: this.int_ySuperscriptXSize)
}
set ySuperscriptXSize(value) {
this.int_ySuperscriptXSize = value
}
get ySuperscriptYSize() {
return ~~(void 0 === this.int_ySuperscriptYSize
? (this.ascent - this.descent) * 0.7
: this.int_ySuperscriptYSize)
}
set ySuperscriptYSize(value) {
this.int_ySuperscriptYSize = value
}
get ySuperscriptYOffset() {
return ~~(void 0 === this.int_ySuperscriptYOffset
? (this.ascent - this.descent) * 0.48
: this.int_ySuperscriptYOffset)
}
set ySuperscriptYOffset(value) {
this.int_ySuperscriptYOffset = value
}
get yStrikeoutSize() {
return ~~(void 0 === this.int_yStrikeoutSize
? (this.ascent - this.descent) * 0.049
: this.int_yStrikeoutSize)
}
set yStrikeoutSize(value) {
this.int_yStrikeoutSize = value
}
get yStrikeoutPosition() {
return ~~(void 0 === this.int_yStrikeoutPosition
? (this.ascent - this.descent) * 0.258
: this.int_yStrikeoutPosition)
}
set yStrikeoutPosition(value) {
this.int_yStrikeoutPosition = value
}
get minLsb() {
return ~~Math.min(...this.glyphs.map(it => it.xMin))
}
get minRsb() {
if (!this.glyphs.length) return ~~this.width
return ~~this.glyphs.reduce(function (minRsb, glyph) {
return Math.min(minRsb, glyph.width - glyph.xMax)
}, 0)
}
get xMin() {
if (!this.glyphs.length) return this.width
return this.glyphs.reduce(function (xMin, glyph) {
return Math.min(xMin, glyph.xMin)
}, 0)
}
get yMin() {
if (!this.glyphs.length) return this.width
return this.glyphs.reduce(function (yMin, glyph) {
return Math.min(yMin, glyph.yMin)
}, 0)
}
get xMax() {
if (!this.glyphs.length) return this.width
return this.glyphs.reduce(function (xMax, glyph) {
return Math.max(xMax, glyph.xMax)
}, 0)
}
get yMax() {
if (!this.glyphs.length) return this.width
return this.glyphs.reduce(function (yMax, glyph) {
return Math.max(yMax, glyph.yMax)
}, 0)
}
get avgWidth() {
let len = this.glyphs.length
if (len === 0) {
return this.width
}
let sumWidth = this.glyphs.reduce(function (sumWidth, glyph) {
return sumWidth + glyph.width
}, 0)
return Math.round(sumWidth / len)
}
get maxWidth() {
if (!this.glyphs.length) return this.width
return this.glyphs.reduce(function (maxWidth, glyph) {
return Math.max(maxWidth, glyph.width)
}, 0)
}
get maxExtent() {
if (!this.glyphs.length) return this.width
return this.glyphs.reduce(function (maxExtent, glyph) {
return Math.max(maxExtent, glyph.xMax /*- glyph.xMin*/)
}, 0)
}
// Property used for `sTypoLineGap` in OS/2 and not used for `lineGap` in HHEA, because
// non zero lineGap causes bad offset in IE, https://github.com/fontello/svg2ttf/issues/37
get lineGap() {
return ~~(void 0 === this.int_lineGap
? (this.ascent - this.descent) * 0.09
: this.int_lineGap)
}
set lineGap(value) {
this.int_lineGap = value
}
get underlinePosition() {
return ~~(void 0 === this.int_underlinePosition
? (this.ascent - this.descent) * 0.01
: this.int_underlinePosition)
}
set underlinePosition(value) {
this.int_underlinePosition = value
}
}
export function Glyph() {
this.contours = []
this.d = ''
this.id = ''
this.codes = [] // needed for nice validator error output
this.height = 0
this.name = ''
this.width = 0
}
Object.defineProperty(Glyph.prototype, 'xMin', {
get: function () {
let xMin = 0
let hasPoints = false
this.contours.forEach(function (contour) {
contour.points.forEach(function (point) {
xMin = Math.min(xMin, Math.floor(point.x))
hasPoints = true
})
})
if (xMin < -32768) {
throw new Error(
'xMin value for glyph ' +
(this.name ? '"' + this.name + '"' : JSON.stringify(this.codes)) +
' is out of bounds (actual ' +
xMin +
', expected -32768..32767, d="' +
this.d +
'")'
)
}
return hasPoints ? xMin : 0
}
})
Object.defineProperty(Glyph.prototype, 'xMax', {
get: function () {
let xMax = 0
let hasPoints = false
this.contours.forEach(function (contour) {
contour.points.forEach(function (point) {
xMax = Math.max(xMax, -Math.floor(-point.x))
hasPoints = true
})
})
if (xMax > 32767) {
throw new Error(
'xMax value for glyph ' +
(this.name ? '"' + this.name + '"' : JSON.stringify(this.codes)) +
' is out of bounds (actual ' +
xMax +
', expected -32768..32767, d="' +
this.d +
'")'
)
}
return hasPoints ? xMax : this.width
}
})
Object.defineProperty(Glyph.prototype, 'yMin', {
get: function () {
let yMin = 0
let hasPoints = false
this.contours.forEach(function (contour) {
contour.points.forEach(function (point) {
yMin = Math.min(yMin, Math.floor(point.y))
hasPoints = true
})
})
if (yMin < -32768) {
throw new Error(
'yMin value for glyph ' +
(this.name ? '"' + this.name + '"' : JSON.stringify(this.codes)) +
' is out of bounds (actual ' +
yMin +
', expected -32768..32767, d="' +
this.d +
'")'
)
}
return hasPoints ? yMin : 0
}
})
Object.defineProperty(Glyph.prototype, 'yMax', {
get: function () {
let yMax = 0
let hasPoints = false
this.contours.forEach(function (contour) {
contour.points.forEach(function (point) {
yMax = Math.max(yMax, -Math.floor(-point.y))
hasPoints = true
})
})
if (yMax > 32767) {
throw new Error(
'yMax value for glyph ' +
(this.name ? '"' + this.name + '"' : JSON.stringify(this.codes)) +
' is out of bounds (actual ' +
yMax +
', expected -32768..32767, d="' +
this.d +
'")'
)
}
return hasPoints ? yMax : 0
}
})
export function Contour() {
this.points = []
}
export function Point() {
this.onCurve = true
this.x = 0
this.y = 0
}
export function SfntName() {
this.id = 0
this.value = ''
}

View File

@ -0,0 +1,42 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2024/02/04 12:34:50
*/
export default class Str {
constructor(str) {
this.str = str
}
toUTF8Bytes() {
let byteArray = []
for (let i = 0; i < str.length; i++) {
if (str.charCodeAt(i) <= 0x7f) {
byteArray.push(str.charCodeAt(i))
} else {
let h = encodeURIComponent(str.charAt(i)).substr(1).split('%')
for (let j = 0; j < h.length; j++) {
byteArray.push(parseInt(h[j], 16))
}
}
}
return byteArray
}
toUCS2Bytes() {
// Code is taken here:
// http://stackoverflow.com/questions/6226189/how-to-convert-a-string-to-bytearray
let byteArray = []
let ch
for (let i = 0; i < str.length; ++i) {
ch = str.charCodeAt(i) // get char
byteArray.push(ch >> 8)
byteArray.push(ch & 0xff)
}
return byteArray
}
}

263
src/lib/svg2ttf/lib/svg.js Normal file
View File

@ -0,0 +1,263 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2024/02/04 10:50:22
*/
import { decode } from './ucs2.js'
import cubic2quad from 'cubic2quad'
import svgpath from 'svgpath'
import { DOMParser } from '@xmldom/xmldom'
function getGlyph(glyphElem, fontInfo) {
let glyph = {}
if (glyphElem.hasAttribute('d')) {
glyph.d = glyphElem.getAttribute('d').trim()
} else {
// try nested <path>
let pathElem = glyphElem.getElementsByTagName('path')[0]
if (pathElem.hasAttribute('d')) {
// <path> has reversed Y axis
glyph.d = svgpath(pathElem.getAttribute('d'))
.scale(1, -1)
.translate(0, fontInfo.ascent)
.toString()
} else {
throw new Error("Can't find 'd' attribute of <glyph> tag.")
}
}
glyph.unicode = []
if (glyphElem.getAttribute('unicode')) {
glyph.character = glyphElem.getAttribute('unicode')
let unicode = decode(glyph.character)
// If more than one code point is involved, the glyph is a ligature glyph
if (unicode.length > 1) {
glyph.ligature = glyph.character
glyph.ligatureCodes = unicode
} else {
glyph.unicode.push(unicode[0])
}
}
glyph.name = glyphElem.getAttribute('glyph-name')
if (glyphElem.getAttribute('horiz-adv-x')) {
glyph.width = parseInt(glyphElem.getAttribute('horiz-adv-x'), 10)
}
return glyph
}
function deduplicateGlyps(glyphs, ligatures) {
let result = []
glyphs.forEach(function (glyph) {
let canonical = result.find(
it => it.width === glyph.width && it.d === glyph.d
)
if (canonical) {
canonical.unicode = canonical.unicode.concat(glyph.unicode)
glyph.canonical = canonical
} else {
result.push(glyph)
}
})
// Update ligatures to point to the canonical version
ligatures.forEach(function (ligature) {
while (ligature.glyph.canonical) {
ligature.glyph = ligature.glyph.canonical
}
})
return result
}
export function load(str) {
let attrs
let doc = new DOMParser().parseFromString(str, 'application/xml')
let metadata, fontElem, fontFaceElem
metadata = doc.getElementsByTagName('metadata')[0]
fontElem = doc.getElementsByTagName('font')[0]
if (!fontElem) {
throw new Error(
"Can't find <font> tag. Make sure you SVG file is font, not image."
)
}
fontFaceElem = fontElem.getElementsByTagName('font-face')[0]
let familyName = fontFaceElem.getAttribute('font-family') || 'fontello'
let subfamilyName = fontFaceElem.getAttribute('font-style') || 'Regular'
let id =
fontElem.getAttribute('id') ||
(familyName + '-' + subfamilyName)
.replace(/[\s\(\)\[\]<>%\/]/g, '')
.substr(0, 62)
let font = {
id: id,
familyName: familyName,
subfamilyName: subfamilyName,
stretch: fontFaceElem.getAttribute('font-stretch') || 'normal'
}
// Doesn't work with complex content like <strong>Copyright:></strong><em>Fontello</em>
if (metadata && metadata.textContent) {
font.metadata = metadata.textContent
}
// Get <font> numeric attributes
attrs = {
width: 'horiz-adv-x',
//height: 'vert-adv-y',
horizOriginX: 'horiz-origin-x',
horizOriginY: 'horiz-origin-y',
vertOriginX: 'vert-origin-x',
vertOriginY: 'vert-origin-y'
}
attrs.forEach(function (val, key) {
if (fontElem.hasAttribute(val)) {
font[key] = parseInt(fontElem.getAttribute(val), 10)
}
})
// Get <font-face> numeric attributes
attrs = {
ascent: 'ascent',
descent: 'descent',
unitsPerEm: 'units-per-em',
capHeight: 'cap-height',
xHeight: 'x-height',
underlineThickness: 'underline-thickness',
underlinePosition: 'underline-position'
}
attrs.forEach(function (val, key) {
if (fontFaceElem.hasAttribute(val)) {
font[key] = parseInt(fontFaceElem.getAttribute(val), 10)
}
})
if (fontFaceElem.hasAttribute('font-weight')) {
font.weightClass = fontFaceElem.getAttribute('font-weight')
}
let missingGlyphElem = fontElem.getElementsByTagName('missing-glyph')[0]
if (missingGlyphElem) {
font.missingGlyph = {}
font.missingGlyph.d = missingGlyphElem.getAttribute('d') || ''
if (missingGlyphElem.getAttribute('horiz-adv-x')) {
font.missingGlyph.width = parseInt(
missingGlyphElem.getAttribute('horiz-adv-x'),
10
)
}
}
let glyphs = []
let ligatures = []
fontElem.getElementsByTagName('glyph').forEach(function (glyphElem) {
let glyph = getGlyph(glyphElem, font)
if (glyph.ligature) {
ligatures.push({
ligature: glyph.ligature,
unicode: glyph.ligatureCodes,
glyph: glyph
})
}
glyphs.push(glyph)
})
glyphs = deduplicateGlyps(glyphs, ligatures)
font.glyphs = glyphs
font.ligatures = ligatures
return font
}
export function cubicToQuad(segment, index, x, y, accuracy) {
if (segment[0] === 'C') {
let quadCurves = cubic2quad(
x,
y,
segment[1],
segment[2],
segment[3],
segment[4],
segment[5],
segment[6],
accuracy
)
let res = []
for (let i = 2; i < quadCurves.length; i += 4) {
res.push([
'Q',
quadCurves[i],
quadCurves[i + 1],
quadCurves[i + 2],
quadCurves[i + 3]
])
}
return res
}
}
// Converts svg points to contours. All points must be converted
// to relative ones, smooth curves must be converted to generic ones
// before this conversion.
//
export function toSfntCoutours(svgPath) {
let resContours = []
let resContour = []
svgPath.iterate(function (segment, index, x, y) {
//start new contour
if (index === 0 || segment[0] === 'M') {
resContour = []
resContours.push(resContour)
}
let name = segment[0]
if (name === 'Q') {
//add control point of quad spline, it is not on curve
resContour.push({ x: segment[1], y: segment[2], onCurve: false })
}
// add on-curve point
if (name === 'H') {
// vertical line has Y coordinate only, X remains the same
resContour.push({ x: segment[1], y: y, onCurve: true })
} else if (name === 'V') {
// horizontal line has X coordinate only, Y remains the same
resContour.push({ x: x, y: segment[1], onCurve: true })
} else if (name !== 'Z') {
// for all commands (except H and V) X and Y are placed in the end of the segment
resContour.push({
x: segment[segment.length - 2],
y: segment[segment.length - 1],
onCurve: true
})
}
})
return resContours
}

156
src/lib/svg2ttf/lib/ttf.js Normal file
View File

@ -0,0 +1,156 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2024/02/04 11:34:19
*/
import { ByteBuffer } from './microbuffer.js'
import createGSUBTable from './ttf/tables/gsub.js'
import createOS2Table from './ttf/tables/os2.js'
import createCMapTable from './ttf/tables/cmap.js'
import createGlyfTable from './ttf/tables/glyf.js'
import createHeadTable from './ttf/tables/head.js'
import createHHeadTable from './ttf/tables/hhea.js'
import createHtmxTable from './ttf/tables/hmtx.js'
import createLocaTable from './ttf/tables/loca.js'
import createMaxpTable from './ttf/tables/maxp.js'
import createNameTable from './ttf/tables/name.js'
import createPostTable from './ttf/tables/post.js'
import * as utils from './ttf/utils.js'
// Tables
let TABLES = [
{ innerName: 'hhea', order: 1, create: createHHeadTable }, // hhea
{ innerName: 'head', order: 2, create: createHeadTable }, // head
{ innerName: 'maxp', order: 3, create: createMaxpTable }, // maxp
{ innerName: 'GSUB', order: 4, create: createGSUBTable }, // GSUB
{ innerName: 'OS/2', order: 4, create: createOS2Table }, // OS/2
{ innerName: 'hmtx', order: 5, create: createHtmxTable }, // hmtx
{ innerName: 'cmap', order: 6, create: createCMapTable }, // cmap
{ innerName: 'loca', order: 7, create: createLocaTable }, // loca
{ innerName: 'glyf', order: 8, create: createGlyfTable }, // glyf
{ innerName: 'name', order: 9, create: createNameTable }, // name
{ innerName: 'post', order: 10, create: createPostTable } // post
]
// Various constants
let CONST = {
VERSION: 0x10000,
CHECKSUM_ADJUSTMENT: 0xb1b0afba
}
function ulong(t) {
t &= 0xffffffff
if (t < 0) {
t += 0x100000000
}
return t
}
function calc_checksum(buf) {
let sum = 0
let nlongs = Math.floor(buf.length / 4)
for (let i = 0; i < nlongs; ++i) {
let t = buf.getUint32(i * 4)
sum = ulong(sum + t)
}
let leftBytes = buf.length - nlongs * 4 //extra 1..3 bytes found, because table is not aligned. Need to include them in checksum too.
if (leftBytes > 0) {
let leftRes = 0
for (let i = 0; i < 4; i++) {
leftRes =
(leftRes << 8) + (i < leftBytes ? buf.getUint8(nlongs * 4 + i) : 0)
}
sum = ulong(sum + leftRes)
}
return sum
}
export function generateTTF(font) {
// Prepare TTF contours objects. Note, that while sfnt countours are classes,
// ttf contours are just plain arrays of points
font.glyphs.forEach(function (glyph) {
glyph.ttfContours = glyph.contours.map(function (contour) {
return contour.points
})
// 0.3px accuracy is ok. fo 1000x1000.
glyph.ttfContours = utils.simplify(glyph.ttfContours, 0.3)
glyph.ttfContours = utils.simplify(glyph.ttfContours, 0.3) // one pass is not enougth
// Interpolated points can be removed. 1.1px is acceptable
// measure - it will give us 1px error after coordinates rounding.
glyph.ttfContours = utils.interpolate(glyph.ttfContours, 1.1)
glyph.ttfContours = utils.roundPoints(glyph.ttfContours)
glyph.ttfContours = utils.removeClosingReturnPoints(glyph.ttfContours)
glyph.ttfContours = utils.toRelative(glyph.ttfContours)
})
// Add tables
let headerSize = 12 + 16 * TABLES.length // TTF header plus table headers
let bufSize = headerSize
//calculate offsets
let offset = headerSize
TABLES.forEach(function (table) {
//store each table in its own buffer
table.buffer = table.create(font)
table.length = table.buffer.length
table.corLength = table.length + ((4 - (table.length % 4)) % 4) // table size should be divisible to 4
table.checkSum = calc_checksum(table.buffer)
bufSize += table.corLength
table.offset = offset
offset += table.corLength
})
//create TTF buffer
let buf = new ByteBuffer(bufSize)
//special constants
let entrySelector = Math.floor(Math.log(TABLES.length) / Math.LN2)
let searchRange = Math.pow(2, entrySelector) * 16
let rangeShift = TABLES.length * 16 - searchRange
// Add TTF header
buf.writeUint32(CONST.VERSION)
buf.writeUint16(TABLES.length)
buf.writeUint16(searchRange)
buf.writeUint16(entrySelector)
buf.writeUint16(rangeShift)
let headOffset = 0
TABLES.forEach(function (table) {
buf.writeUint32(utils.identifier(table.innerName)) //inner name
buf.writeUint32(table.checkSum) //checksum
buf.writeUint32(table.offset) //offset
buf.writeUint32(table.length) //length
if (table.innerName === 'head') {
//we must store head offset to write font checksum
headOffset = buf.tell()
}
buf.writeBytes(table.buffer.buffer)
for (let i = table.length; i < table.corLength; i++) {
//align table to be divisible to 4
buf.writeUint8(0)
}
})
// Write font checksum (corrected by magic value) into HEAD table
buf.setUint32(
headOffset + 8,
ulong(CONST.CHECKSUM_ADJUSTMENT - calc_checksum(buf))
)
return buf
}

View File

@ -0,0 +1,298 @@
// See documentation here: http://www.microsoft.com/typography/otspec/cmap.htm
import { ByteBuffer } from '../../microbuffer.js'
function getIDByUnicode(font, unicode) {
return font.codePoints[unicode] ? font.codePoints[unicode].id : 0
}
// Calculate character segments with non-interruptable chains of unicodes
function getSegments(font, bounds) {
bounds = bounds || Number.MAX_VALUE
let result = []
let segment
// prevEndCode only changes when a segment closes
font.codePoints.forEach(function (glyph, unicode) {
unicode = parseInt(unicode, 10)
if (unicode >= bounds) {
return false
}
// Initialize first segment or add new segment if code "hole" is found
if (!segment || unicode !== segment.end + 1) {
if (segment) {
result.push(segment)
}
segment = {
start: unicode
}
}
segment.end = unicode
})
// Need to finish the last segment
if (segment) {
result.push(segment)
}
result.forEach(function (segment) {
segment.length = segment.end - segment.start + 1
})
return result
}
// Returns an array of {unicode, glyph} sets for all valid code points up to bounds
function getCodePoints(codePoints, bounds) {
bounds = bounds || Number.MAX_VALUE
let result = []
codePoints.forEach(function (glyph, unicode) {
unicode = parseInt(unicode, 10)
// Since this is a sparse array, iterating will only yield the valid code points
if (unicode > bounds) {
return false
}
result.push({
unicode: unicode,
glyph: glyph
})
})
return result
}
function bufferForTable(format, length) {
let fieldWidth =
format === 8 || format === 10 || format === 12 || format === 13 ? 4 : 2
length +=
0 +
fieldWidth + // Format
fieldWidth + // Length
fieldWidth // Language
let LANGUAGE = 0
let buffer = new ByteBuffer(length)
let writer = fieldWidth === 4 ? buffer.writeUint32 : buffer.writeUint16
// Format specifier
buffer.writeUint16(format)
if (fieldWidth === 4) {
// In case of formats 8.…, 10.…, 12.… and 13.…, this is the decimal part of the format number
// But since have not been any point releases, this can be zero in that case as well
buffer.writeUint16(0)
}
// Length
writer.call(buffer, length)
// Language code (0, only used for legacy quickdraw tables)
writer.call(buffer, LANGUAGE)
return buffer
}
function createFormat0Table(font) {
let FORMAT = 0
let i
let length = 0xff + 1 //Format 0 maps only single-byte code points
let buffer = bufferForTable(FORMAT, length)
for (i = 0; i < length; i++) {
buffer.writeUint8(getIDByUnicode(font, i)) // existing char in table 0..255
}
return buffer
}
function createFormat4Table(font) {
let FORMAT = 4
let segments = getSegments(font, 0xffff)
let glyphIndexArrays = []
segments.forEach(function (segment) {
let glyphIndexArray = []
for (let unicode = segment.start; unicode <= segment.end; unicode++) {
glyphIndexArray.push(getIDByUnicode(font, unicode))
}
glyphIndexArrays.push(glyphIndexArray)
})
let segCount = segments.length + 1 // + 1 for the 0xFFFF section
let glyphIndexArrayLength = glyphIndexArrays
.map(it => it.length)
.reduce(function (result, count) {
return result + count
}, 0)
let length =
0 +
2 + // segCountX2
2 + // searchRange
2 + // entrySelector
2 + // rangeShift
2 * segCount + // endCodes
2 + // Padding
2 * segCount + //startCodes
2 * segCount + //idDeltas
2 * segCount + //idRangeOffsets
2 * glyphIndexArrayLength
let buffer = bufferForTable(FORMAT, length)
buffer.writeUint16(segCount * 2) // segCountX2
let maxExponent = Math.floor(Math.log(segCount) / Math.LN2)
let searchRange = 2 * Math.pow(2, maxExponent)
buffer.writeUint16(searchRange) // searchRange
buffer.writeUint16(maxExponent) // entrySelector
buffer.writeUint16(2 * segCount - searchRange) // rangeShift
// Array of end counts
segments.forEach(function (segment) {
buffer.writeUint16(segment.end)
})
buffer.writeUint16(0xffff) // endCountArray should be finished with 0xFFFF
buffer.writeUint16(0) // reservedPad
// Array of start counts
segments.forEach(function (segment) {
buffer.writeUint16(segment.start) //startCountArray
})
buffer.writeUint16(0xffff) // startCountArray should be finished with 0xFFFF
// Array of deltas. Leave it zero to not complicate things when using the glyph index array
for (let i = 0; i < segments.length; i++) {
buffer.writeUint16(0) // delta is always zero because we use the glyph array
}
buffer.writeUint16(1) // idDeltaArray should be finished with 1
// Array of range offsets
let offset = 0
for (let i = 0; i < segments.length; i++) {
buffer.writeUint16(2 * (segments.length - i + 1 + offset))
offset += glyphIndexArrays[i].length
}
buffer.writeUint16(0) // rangeOffsetArray should be finished with 0
glyphIndexArrays.forEach(function (glyphIndexArray) {
glyphIndexArray.forEach(function (glyphId) {
buffer.writeUint16(glyphId)
})
})
return buffer
}
function createFormat12Table(font) {
let FORMAT = 12
let codePoints = getCodePoints(font.codePoints)
let length =
0 +
4 + // nGroups
4 * codePoints.length + // startCharCode
4 * codePoints.length + // endCharCode
4 * codePoints.length // startGlyphCode
let buffer = bufferForTable(FORMAT, length)
buffer.writeUint32(codePoints.length) // nGroups
codePoints.forEach(function (codePoint) {
buffer.writeUint32(codePoint.unicode) // startCharCode
buffer.writeUint32(codePoint.unicode) // endCharCode
buffer.writeUint32(codePoint.glyph.id) // startGlyphCode
})
return buffer
}
export default function createCMapTable(font) {
let TABLE_HEAD =
0 +
2 + // platform
2 + // encoding
4 // offset
let singleByteTable = createFormat0Table(font)
let twoByteTable = createFormat4Table(font)
let fourByteTable = createFormat12Table(font)
// Subtable headers must be sorted by platformID, encodingID
let tableHeaders = [
// subtable 4, unicode
{
platformID: 0,
encodingID: 3,
table: twoByteTable
},
// subtable 12, unicode
{
platformID: 0,
encodingID: 4,
table: fourByteTable
},
// subtable 0, mac standard
{
platformID: 1,
encodingID: 0,
table: singleByteTable
},
// subtable 4, windows standard, identical to the unicode table
{
platformID: 3,
encodingID: 1,
table: twoByteTable
},
// subtable 12, windows ucs4
{
platformID: 3,
encodingID: 10,
table: fourByteTable
}
]
let tables = [twoByteTable, singleByteTable, fourByteTable]
let tableOffset =
0 +
2 + // version
2 + // number of subtable headers
tableHeaders.length * TABLE_HEAD
// Calculate offsets for each table
tables.forEach(function (table) {
table._tableOffset = tableOffset
tableOffset += table.length
})
let length = tableOffset
let buffer = new ByteBuffer(length)
// Write table header.
buffer.writeUint16(0) // version
buffer.writeUint16(tableHeaders.length) // count
// Write subtable headers
tableHeaders.forEach(function (header) {
buffer.writeUint16(header.platformID) // platform
buffer.writeUint16(header.encodingID) // encoding
buffer.writeUint32(header.table._tableOffset) // offset
})
// Write subtables
tables.forEach(function (table) {
buffer.writeBytes(table.buffer)
})
return buffer
}

View File

@ -0,0 +1,193 @@
// See documentation here: http://www.microsoft.com/typography/otspec/glyf.htm
import { ByteBuffer } from '../../microbuffer.js'
function getFlags(glyph) {
let result = []
glyph.ttfContours.forEach(function (contour) {
contour.forEach(function (point) {
let flag = point.onCurve ? 1 : 0
if (point.x === 0) {
flag += 16
} else {
if (-0xff <= point.x && point.x <= 0xff) {
flag += 2 // the corresponding x-coordinate is 1 byte long
}
if (point.x > 0 && point.x <= 0xff) {
flag += 16 // If x-Short Vector is set, this bit describes the sign of the value, with 1 equalling positive and 0 negative
}
}
if (point.y === 0) {
flag += 32
} else {
if (-0xff <= point.y && point.y <= 0xff) {
flag += 4 // the corresponding y-coordinate is 1 byte long
}
if (point.y > 0 && point.y <= 0xff) {
flag += 32 // If y-Short Vector is set, this bit describes the sign of the value, with 1 equalling positive and 0 negative.
}
}
result.push(flag)
})
})
return result
}
//repeating flags can be packed
function compactFlags(flags) {
let result = []
let prevFlag = -1
let firstRepeat = false
flags.forEach(function (flag) {
if (prevFlag === flag) {
if (firstRepeat) {
result[result.length - 1] += 8 //current flag repeats previous one, need to set 3rd bit of previous flag and set 1 to the current one
result.push(1)
firstRepeat = false
} else {
result[result.length - 1]++ //when flag is repeating second or more times, we need to increase the last flag value
}
} else {
firstRepeat = true
prevFlag = flag
result.push(flag)
}
})
return result
}
function getCoords(glyph, coordName) {
let result = []
glyph.ttfContours.forEach(function (contour) {
result.push.apply(
result,
contour.map(it => it[coordName])
)
})
return result
}
function compactCoords(coords) {
return coords.filter(function (coord) {
return coord !== 0
})
}
//calculates length of glyph data in GLYF table
function glyphDataSize(glyph) {
// Ignore glyphs without outlines. These will get a length of zero in the “loca” table
if (!glyph.contours.length) {
return 0
}
let result = 12 //glyph fixed properties
result += glyph.contours.length * 2 //add contours
glyph.ttf_x.forEach(function (x) {
//add 1 or 2 bytes for each coordinate depending of its size
result += -0xff <= x && x <= 0xff ? 1 : 2
})
glyph.ttf_y.forEach(function (y) {
//add 1 or 2 bytes for each coordinate depending of its size
result += -0xff <= y && y <= 0xff ? 1 : 2
})
// Add flags length to glyph size.
result += glyph.ttf_flags.length
if (result % 4 !== 0) {
// glyph size must be divisible by 4.
result += 4 - (result % 4)
}
return result
}
function tableSize(font) {
let result = 0
font.glyphs.forEach(function (glyph) {
glyph.ttf_size = glyphDataSize(glyph)
result += glyph.ttf_size
})
font.ttf_glyph_size = result //sum of all glyph lengths
return result
}
export default function createGlyfTable(font) {
font.glyphs.forEach(function (glyph) {
glyph.ttf_flags = getFlags(glyph)
glyph.ttf_flags = compactFlags(glyph.ttf_flags)
glyph.ttf_x = getCoords(glyph, 'x')
glyph.ttf_x = compactCoords(glyph.ttf_x)
glyph.ttf_y = getCoords(glyph, 'y')
glyph.ttf_y = compactCoords(glyph.ttf_y)
})
let buf = new ByteBuffer(tableSize(font))
font.glyphs.forEach(function (glyph) {
// Ignore glyphs without outlines. These will get a length of zero in the “loca” table
if (!glyph.contours.length) {
return
}
let offset = buf.tell()
buf.writeInt16(glyph.contours.length) // numberOfContours
buf.writeInt16(glyph.xMin) // xMin
buf.writeInt16(glyph.yMin) // yMin
buf.writeInt16(glyph.xMax) // xMax
buf.writeInt16(glyph.yMax) // yMax
// Array of end points
let endPtsOfContours = -1
let ttfContours = glyph.ttfContours
ttfContours.forEach(function (contour) {
endPtsOfContours += contour.length
buf.writeInt16(endPtsOfContours)
})
buf.writeInt16(0) // instructionLength, is not used here
// Array of flags
glyph.ttf_flags.forEach(function (flag) {
buf.writeInt8(flag)
})
// Array of X relative coordinates
glyph.ttf_x.forEach(function (x) {
if (-0xff <= x && x <= 0xff) {
buf.writeUint8(Math.abs(x))
} else {
buf.writeInt16(x)
}
})
// Array of Y relative coordinates
glyph.ttf_y.forEach(function (y) {
if (-0xff <= y && y <= 0xff) {
buf.writeUint8(Math.abs(y))
} else {
buf.writeInt16(y)
}
})
let tail = (buf.tell() - offset) % 4
if (tail !== 0) {
// glyph size must be divisible by 4.
for (; tail < 4; tail++) {
buf.writeUint8(0)
}
}
})
return buf
}

View File

@ -0,0 +1,400 @@
// See documentation here: http://www.microsoft.com/typography/otspec/GSUB.htm
import { identifier } from '../utils.js'
import { ByteBuffer } from '../../microbuffer.js'
function createScript() {
let scriptRecord =
0 +
2 + // Script DefaultLangSys Offset
2 // Script[0] LangSysCount (0)
let langSys =
0 +
2 + // Script DefaultLangSys LookupOrder
2 + // Script DefaultLangSys ReqFeatureIndex
2 + // Script DefaultLangSys FeatureCount (0?)
2 // Script Optional Feature Index[0]
let length = 0 + scriptRecord + langSys
let buffer = new ByteBuffer(length)
// Script Record
// Offset to the start of langSys from the start of scriptRecord
buffer.writeUint16(scriptRecord) // DefaultLangSys
// Number of LangSys entries other than the default (none)
buffer.writeUint16(0)
// LangSys record (DefaultLangSys)
// LookupOrder
buffer.writeUint16(0)
// ReqFeatureIndex -> only one required feature: all ligatures
buffer.writeUint16(0)
// Number of FeatureIndex values for this language system (excludes the required feature)
buffer.writeUint16(1)
// FeatureIndex for the first optional feature
// Note: Adding the same feature to both the optional
// and the required features is a clear violation of the spec
// but it fixes IE not displaying the ligatures.
// See http://partners.adobe.com/public/developer/opentype/index_table_formats.html, Section “Language System Table”
// “FeatureCount: Number of FeatureIndex values for this language system-*excludes the required feature*” (emphasis added)
buffer.writeUint16(0)
return buffer
}
function createScriptList() {
let scriptSize =
0 +
4 + // Tag
2 // Offset
// tags should be arranged alphabetically
let scripts = [
['DFLT', createScript()],
['latn', createScript()]
]
let header =
0 +
2 + // Script count
scripts.length * scriptSize
let tableLengths = scripts
.map(function (script) {
return script[1].length
})
.reduce(function (result, count) {
return result + count
}, 0)
let length = 0 + header + tableLengths
let buffer = new ByteBuffer(length)
// Script count
buffer.writeUint16(scripts.length)
// Write all ScriptRecords
let offset = header
scripts.forEach(function (script) {
let name = script[0],
table = script[1]
// Script identifier (DFLT/latn)
buffer.writeUint32(identifier(name))
// Offset to the ScriptRecord from start of the script list
buffer.writeUint16(offset)
// Increment offset by script table length
offset += table.length
})
// Write all ScriptTables
scripts.forEach(function (script) {
let table = script[1]
buffer.writeBytes(table.buffer)
})
return buffer
}
// Write one feature containing all ligatures
function createFeatureList() {
let header =
0 +
2 + // FeatureCount
4 + // FeatureTag[0]
2 // Feature Offset[0]
let length =
0 +
header +
2 + // FeatureParams[0]
2 + // LookupCount[0]
2 // Lookup[0] LookupListIndex[0]
let buffer = new ByteBuffer(length)
// FeatureCount
buffer.writeUint16(1)
// FeatureTag[0]
buffer.writeUint32(identifier('liga'))
// Feature Offset[0]
buffer.writeUint16(header)
// FeatureParams[0]
buffer.writeUint16(0)
// LookupCount[0]
buffer.writeUint16(1)
// Index into lookup table. Since we only have ligatures, the index is always 0
buffer.writeUint16(0)
return buffer
}
function createLigatureCoverage(font, ligatureGroups) {
let glyphCount = ligatureGroups.length
let length =
0 +
2 + // CoverageFormat
2 + // GlyphCount
2 * glyphCount // GlyphID[i]
let buffer = new ByteBuffer(length)
// CoverageFormat
buffer.writeUint16(1)
// Length
buffer.writeUint16(glyphCount)
ligatureGroups.forEach(function (group) {
buffer.writeUint16(group.startGlyph.id)
})
return buffer
}
function createLigatureTable(font, ligature) {
let allCodePoints = font.codePoints
let unicode = ligature.unicode
let length =
0 +
2 + // LigGlyph
2 + // CompCount
2 * (unicode.length - 1)
let buffer = new ByteBuffer(length)
// LigGlyph
let glyph = ligature.glyph
buffer.writeUint16(glyph.id)
// CompCount
buffer.writeUint16(unicode.length)
// Compound glyphs (excluding first as its already in the coverage table)
for (let i = 1; i < unicode.length; i++) {
glyph = allCodePoints[unicode[i]]
buffer.writeUint16(glyph.id)
}
return buffer
}
function createLigatureSet(font, codePoint, ligatures) {
let ligatureTables = []
ligatures.forEach(function (ligature) {
ligatureTables.push(createLigatureTable(font, ligature))
})
let tableLengths = ligatureTables
.map(it => it.length)
.reduce(function (result, count) {
return result + count
}, 0)
let offset =
0 +
2 + // LigatureCount
2 * ligatures.length
let length = 0 + offset + tableLengths
let buffer = new ByteBuffer(length)
// LigatureCount
buffer.writeUint16(ligatures.length)
// Ligature offsets
ligatureTables.forEach(function (table) {
// The offset to the current set, from SubstFormat
buffer.writeUint16(offset)
offset += table.length
})
// Ligatures
ligatureTables.forEach(function (table) {
buffer.writeBytes(table.buffer)
})
return buffer
}
function createLigatureList(font, ligatureGroups) {
let sets = []
ligatureGroups.forEach(function (group) {
let set = createLigatureSet(font, group.codePoint, group.ligatures)
sets.push(set)
})
let setLengths = sets
.map(it => it.length)
.reduce(function (result, count) {
return result + count
}, 0)
let coverage = createLigatureCoverage(font, ligatureGroups)
let tableOffset =
0 +
2 + // Lookup type
2 + // Lokup flag
2 + // SubTableCount
2 // SubTable[0] Offset
let setOffset =
0 +
2 + // SubstFormat
2 + // Coverage offset
2 + // LigSetCount
2 * sets.length // LigSet Offsets
let coverageOffset = setOffset + setLengths
let length = 0 + tableOffset + coverageOffset + coverage.length
let buffer = new ByteBuffer(length)
// Lookup type 4 ligatures
buffer.writeUint16(4)
// Lookup flag empty
buffer.writeUint16(0)
// Subtable count
buffer.writeUint16(1)
// Subtable[0] offset
buffer.writeUint16(tableOffset)
// SubstFormat
buffer.writeUint16(1)
// Coverage
buffer.writeUint16(coverageOffset)
// LigSetCount
buffer.writeUint16(sets.length)
sets.forEach(function (set) {
// The offset to the current set, from SubstFormat
buffer.writeUint16(setOffset)
setOffset += set.length
})
sets.forEach(function (set) {
buffer.writeBytes(set.buffer)
})
buffer.writeBytes(coverage.buffer)
return buffer
}
// Add a lookup for each ligature
function createLookupList(font) {
let ligatures = font.ligatures
let groupedLigatures = {}
// Group ligatures by first code point
ligatures.forEach(function (ligature) {
let first = ligature.unicode[0]
if (!groupedLigatures[first]) {
groupedLigatures[first] = []
}
groupedLigatures[first].push(ligature)
})
let ligatureGroups = []
groupedLigatures.forEach(function (ligatures, codePoint) {
codePoint = parseInt(codePoint, 10)
// Order ligatures by length, descending
// “Ligatures with more components must be stored ahead of those with fewer components in order to be found”
// From: http://partners.adobe.com/public/developer/opentype/index_tag7.html#liga
ligatures.sort(function (ligA, ligB) {
return ligB.unicode.length - ligA.unicode.length
})
ligatureGroups.push({
codePoint: codePoint,
ligatures: ligatures,
startGlyph: font.codePoints[codePoint]
})
})
ligatureGroups.sort(function (a, b) {
return a.startGlyph.id - b.startGlyph.id
})
let offset =
0 +
2 + // Lookup count
2 // Lookup[0] offset
let set = createLigatureList(font, ligatureGroups)
let length = 0 + offset + set.length
let buffer = new ByteBuffer(length)
// Lookup count
buffer.writeUint16(1)
// Lookup[0] offset
buffer.writeUint16(offset)
// Lookup[0]
buffer.writeBytes(set.buffer)
return buffer
}
export default function createGSUB(font) {
let scriptList = createScriptList()
let featureList = createFeatureList()
let lookupList = createLookupList(font)
let lists = [scriptList, featureList, lookupList]
let offset =
0 +
4 + // Version
2 * lists.length // List offsets
// Calculate offsets
lists.forEach(function (list) {
list._listOffset = offset
offset += list.length
})
let length = offset
let buffer = new ByteBuffer(length)
// Version
buffer.writeUint32(0x00010000)
// Offsets
lists.forEach(function (list) {
buffer.writeUint16(list._listOffset)
})
// List contents
lists.forEach(function (list) {
buffer.writeBytes(list.buffer)
})
return buffer
}

View File

@ -0,0 +1,39 @@
'use strict'
// See documentation here: http://www.microsoft.com/typography/otspec/head.htm
import { ByteBuffer } from '../../microbuffer.js'
function dateToUInt64(date) {
let startDate = new Date('1904-01-01T00:00:00.000Z')
return Math.floor((date - startDate) / 1000)
}
export default function createHeadTable(font) {
let buf = new ByteBuffer(54) // fixed table length
buf.writeInt32(0x10000) // version
buf.writeInt32(font.revision * 0x10000) // fontRevision
buf.writeUint32(0) // checkSumAdjustment
buf.writeUint32(0x5f0f3cf5) // magicNumber
// FLag meanings:
// Bit 0: Baseline for font at y=0;
// Bit 1: Left sidebearing point at x=0;
// Bit 3: Force ppem to integer values for all internal scaler math; may use fractional ppem sizes if this bit is clear;
buf.writeUint16(0x000b) // flags
buf.writeUint16(font.unitsPerEm) // unitsPerEm
buf.writeUint64(dateToUInt64(font.createdDate)) // created
buf.writeUint64(dateToUInt64(font.modifiedDate)) // modified
buf.writeInt16(font.xMin) // xMin
buf.writeInt16(font.yMin) // yMin
buf.writeInt16(font.xMax) // xMax
buf.writeInt16(font.yMax) // yMax
buf.writeUint16(font.macStyle) //macStyle
buf.writeUint16(font.lowestRecPPEM) // lowestRecPPEM
buf.writeInt16(2) // fontDirectionHint
buf.writeInt16(font.ttf_glyph_size < 0x20000 ? 0 : 1) // indexToLocFormat, 0 for short offsets, 1 for long offsets
buf.writeInt16(0) // glyphDataFormat
return buf
}

View File

@ -0,0 +1,28 @@
'use strict'
// See documentation here: http://www.microsoft.com/typography/otspec/hhea.htm
import { ByteBuffer } from '../../microbuffer.js'
export default function createHHeadTable(font) {
let buf = new ByteBuffer(36) // fixed table length
buf.writeInt32(0x10000) // version
buf.writeInt16(font.ascent) // ascent
buf.writeInt16(font.descent) // descend
// Non zero lineGap causes offset in IE, https://github.com/fontello/svg2ttf/issues/37
buf.writeInt16(0) // lineGap
buf.writeUint16(font.maxWidth) // advanceWidthMax
buf.writeInt16(font.minLsb) // minLeftSideBearing
buf.writeInt16(font.minRsb) // minRightSideBearing
buf.writeInt16(font.maxExtent) // xMaxExtent
buf.writeInt16(1) // caretSlopeRise
buf.writeInt16(0) // caretSlopeRun
buf.writeUint32(0) // reserved1
buf.writeUint32(0) // reserved2
buf.writeUint16(0) // reserved3
buf.writeInt16(0) // metricDataFormat
buf.writeUint16(font.glyphs.length) // numberOfHMetrics
return buf
}

View File

@ -0,0 +1,13 @@
// See documentation here: http://www.microsoft.com/typography/otspec/hmtx.htm
import { ByteBuffer } from '../../microbuffer.js'
export default function createHtmxTable(font) {
let buf = new ByteBuffer(font.glyphs.length * 4)
font.glyphs.forEach(function (glyph) {
buf.writeUint16(glyph.width) //advanceWidth
buf.writeInt16(glyph.xMin) //lsb
})
return buf
}

View File

@ -0,0 +1,37 @@
// See documentation here: http://www.microsoft.com/typography/otspec/loca.htm
import { ByteBuffer } from '../../microbuffer.js'
function tableSize(font, isShortFormat) {
let result = (font.glyphs.length + 1) * (isShortFormat ? 2 : 4) // by glyph count + tail
return result
}
export default function createLocaTable(font) {
let isShortFormat = font.ttf_glyph_size < 0x20000
let buf = new ByteBuffer(tableSize(font, isShortFormat))
let location = 0
// Array of offsets in GLYF table for each glyph
font.glyphs.forEach(function (glyph) {
if (isShortFormat) {
buf.writeUint16(location)
location += glyph.ttf_size / 2 // actual location must be divided to 2 in short format
} else {
buf.writeUint32(location)
location += glyph.ttf_size //actual location is stored as is in long format
}
})
// The last glyph location is stored to get last glyph length
if (isShortFormat) {
buf.writeUint16(location)
} else {
buf.writeUint32(location)
}
return buf
}

View File

@ -0,0 +1,40 @@
// See documentation here: http://www.microsoft.com/typography/otspec/maxp.htm
import { ByteBuffer } from '../../microbuffer.js'
// Find max points in glyph TTF contours.
function getMaxPoints(font) {
return Math.max(
...font.glyphs.map(glyph =>
glyph.ttfContours.reduce((sum, ctr) => sum + ctr.length, 0)
)
)
}
function getMaxContours(font) {
return Math.max(...font.glyphs.map(g => g.ttfContours.length))
}
export default function createMaxpTable(font) {
let buf = new ByteBuffer(32)
buf.writeInt32(0x10000) // version
buf.writeUint16(font.glyphs.length) // numGlyphs
buf.writeUint16(getMaxPoints(font)) // maxPoints
buf.writeUint16(getMaxContours(font)) // maxContours
buf.writeUint16(0) // maxCompositePoints
buf.writeUint16(0) // maxCompositeContours
buf.writeUint16(2) // maxZones
buf.writeUint16(0) // maxTwilightPoints
// It is unclear how to calculate maxStorage, maxFunctionDefs and maxInstructionDefs.
// These are magic constants now, with values exceeding values from FontForge
buf.writeUint16(10) // maxStorage
buf.writeUint16(10) // maxFunctionDefs
buf.writeUint16(0) // maxInstructionDefs
buf.writeUint16(255) // maxStackElements
buf.writeUint16(0) // maxSizeOfInstructions
buf.writeUint16(0) // maxComponentElements
buf.writeUint16(0) // maxComponentDepth
return buf
}

View File

@ -0,0 +1,115 @@
// See documentation here: http://www.microsoft.com/typography/otspec/name.htm
import { ByteBuffer } from '../../microbuffer.js'
import Str from '../../str.js'
let TTF_NAMES = {
COPYRIGHT: 0,
FONT_FAMILY: 1,
ID: 3,
DESCRIPTION: 10,
URL_VENDOR: 11
}
function tableSize(names) {
let result = 6 // table header
names.forEach(function (name) {
result += 12 + name.data.length //name header and data
})
return result
}
function getStrings(name, id) {
let result = []
let str = new Str(name)
result.push({
data: str.toUTF8Bytes(),
id: id,
platformID: 1,
encodingID: 0,
languageID: 0
}) //mac standard
result.push({
data: str.toUCS2Bytes(),
id: id,
platformID: 3,
encodingID: 1,
languageID: 0x409
}) //windows standard
return result
}
// Collect font names
function getNames(font) {
let result = []
if (font.copyright) {
result.push.apply(result, getStrings(font.copyright, TTF_NAMES.COPYRIGHT))
}
if (font.familyName) {
result.push.apply(
result,
getStrings(font.familyName, TTF_NAMES.FONT_FAMILY)
)
}
if (font.id) {
result.push.apply(result, getStrings(font.id, TTF_NAMES.ID))
}
result.push.apply(result, getStrings(font.description, TTF_NAMES.DESCRIPTION))
result.push.apply(result, getStrings(font.url, TTF_NAMES.URL_VENDOR))
font.sfntNames.forEach(function (sfntName) {
result.push.apply(result, getStrings(sfntName.value, sfntName.id))
})
result.sort(function (a, b) {
let orderFields = ['platformID', 'encodingID', 'languageID', 'id']
let i
for (i = 0; i < orderFields.length; i++) {
if (a[orderFields[i]] !== b[orderFields[i]]) {
return a[orderFields[i]] < b[orderFields[i]] ? -1 : 1
}
}
return 0
})
return result
}
export default function createNameTable(font) {
let names = getNames(font)
let buf = new ByteBuffer(tableSize(names))
buf.writeUint16(0) // formatSelector
buf.writeUint16(names.length) // nameRecordsCount
let offsetPosition = buf.tell()
buf.writeUint16(0) // offset, will be filled later
let nameOffset = 0
names.forEach(function (name) {
buf.writeUint16(name.platformID) // platformID
buf.writeUint16(name.encodingID) // platEncID
buf.writeUint16(name.languageID) // languageID, English (USA)
buf.writeUint16(name.id) // nameID
buf.writeUint16(name.data.length) // reclength
buf.writeUint16(nameOffset) // offset
nameOffset += name.data.length
})
let actualStringDataOffset = buf.tell()
//Array of bytes with actual string data
names.forEach(function (name) {
buf.writeBytes(name.data)
})
//write actual string data offset
buf.seek(offsetPosition)
buf.writeUint16(actualStringDataOffset) // offset
return buf
}

View File

@ -0,0 +1,94 @@
// See documentation here: http://www.microsoft.com/typography/otspec/os2.htm
import { identifier } from '../utils.js'
import { ByteBuffer } from '../../microbuffer.js'
//get first glyph unicode
function getFirstCharIndex(font) {
return Math.max(
0,
Math.min(
0xffff,
Math.abs(Math.min(...Object.keys(font.codePoints).map(p => ~~p)))
)
)
}
//get last glyph unicode
function getLastCharIndex(font) {
return Math.max(
0,
Math.min(
0xffff,
Math.abs(Math.max(...Object.keys(font.codePoints).map(p => ~~p)))
)
)
}
// OpenType spec: https://docs.microsoft.com/en-us/typography/opentype/spec/os2
export default function createOS2Table(font) {
// use at least 2 for ligatures and kerning
let maxContext = font.ligatures
.map(function (l) {
return l.unicode.length
})
.reduce(function (a, b) {
return Math.max(a, b)
}, 2)
let buf = new ByteBuffer(96)
// Version 5 is not supported in the Android 5 browser.
buf.writeUint16(4) // version
buf.writeInt16(font.avgWidth) // xAvgCharWidth
buf.writeUint16(font.weightClass) // usWeightClass
buf.writeUint16(font.widthClass) // usWidthClass
buf.writeInt16(font.fsType) // fsType
buf.writeInt16(font.ySubscriptXSize) // ySubscriptXSize
buf.writeInt16(font.ySubscriptYSize) //ySubscriptYSize
buf.writeInt16(font.ySubscriptXOffset) // ySubscriptXOffset
buf.writeInt16(font.ySubscriptYOffset) // ySubscriptYOffset
buf.writeInt16(font.ySuperscriptXSize) // ySuperscriptXSize
buf.writeInt16(font.ySuperscriptYSize) // ySuperscriptYSize
buf.writeInt16(font.ySuperscriptXOffset) // ySuperscriptXOffset
buf.writeInt16(font.ySuperscriptYOffset) // ySuperscriptYOffset
buf.writeInt16(font.yStrikeoutSize) // yStrikeoutSize
buf.writeInt16(font.yStrikeoutPosition) // yStrikeoutPosition
buf.writeInt16(font.familyClass) // sFamilyClass
buf.writeUint8(font.panose.familyType) // panose.bFamilyType
buf.writeUint8(font.panose.serifStyle) // panose.bSerifStyle
buf.writeUint8(font.panose.weight) // panose.bWeight
buf.writeUint8(font.panose.proportion) // panose.bProportion
buf.writeUint8(font.panose.contrast) // panose.bContrast
buf.writeUint8(font.panose.strokeVariation) // panose.bStrokeVariation
buf.writeUint8(font.panose.armStyle) // panose.bArmStyle
buf.writeUint8(font.panose.letterform) // panose.bLetterform
buf.writeUint8(font.panose.midline) // panose.bMidline
buf.writeUint8(font.panose.xHeight) // panose.bXHeight
// TODO: This field is used to specify the Unicode blocks or ranges based on the 'cmap' table.
buf.writeUint32(0) // ulUnicodeRange1
buf.writeUint32(0) // ulUnicodeRange2
buf.writeUint32(0) // ulUnicodeRange3
buf.writeUint32(0) // ulUnicodeRange4
buf.writeUint32(identifier('PfEd')) // achVendID, equal to PfEd
buf.writeUint16(font.fsSelection) // fsSelection
buf.writeUint16(getFirstCharIndex(font)) // usFirstCharIndex
buf.writeUint16(getLastCharIndex(font)) // usLastCharIndex
buf.writeInt16(font.ascent) // sTypoAscender
buf.writeInt16(font.descent) // sTypoDescender
buf.writeInt16(font.lineGap) // lineGap
// Enlarge win acscent/descent to avoid clipping
// WinAscent - WinDecent should at least be equal to TypoAscender - TypoDescender + TypoLineGap:
// https://www.high-logic.com/font-editor/fontcreator/tutorials/font-metrics-vertical-line-spacing
buf.writeInt16(Math.max(font.yMax, font.ascent + font.lineGap)) // usWinAscent
buf.writeInt16(-Math.min(font.yMin, font.descent)) // usWinDescent
buf.writeInt32(1) // ulCodePageRange1, Latin 1
buf.writeInt32(0) // ulCodePageRange2
buf.writeInt16(font.xHeight) // sxHeight
buf.writeInt16(font.capHeight) // sCapHeight
buf.writeUint16(0) // usDefaultChar, pointing to missing glyph (always id=0)
buf.writeUint16(0) // usBreakChar, code=32 isn't guaranteed to be a space in icon fonts
buf.writeUint16(maxContext) // usMaxContext, use at least 2 for ligatures and kerning
return buf
}

View File

@ -0,0 +1,67 @@
// See documentation here: http://www.microsoft.com/typography/otspec/post.htm
import { ByteBuffer } from '../../microbuffer.js'
function tableSize(font, names) {
let result = 36 // table header
result += font.glyphs.length * 2 // name declarations
names.forEach(function (name) {
result += name.length
})
return result
}
function pascalString(str) {
let bytes = []
let len = str ? (str.length < 256 ? str.length : 255) : 0 //length in Pascal string is limited with 255
bytes.push(len)
for (let i = 0; i < len; i++) {
let char = str.charCodeAt(i)
bytes.push(char < 128 ? char : 95) //non-ASCII characters are substituted with '_'
}
return bytes
}
export default function createPostTable(font) {
let names = []
font.glyphs.forEach(function (glyph) {
if (glyph.unicode !== 0) {
names.push(pascalString(glyph.name))
}
})
let buf = new ByteBuffer(tableSize(font, names))
buf.writeInt32(0x20000) // formatType, version 2.0
buf.writeInt32(font.italicAngle) // italicAngle
buf.writeInt16(font.underlinePosition) // underlinePosition
buf.writeInt16(font.underlineThickness) // underlineThickness
buf.writeUint32(font.isFixedPitch) // isFixedPitch
buf.writeUint32(0) // minMemType42
buf.writeUint32(0) // maxMemType42
buf.writeUint32(0) // minMemType1
buf.writeUint32(0) // maxMemType1
buf.writeUint16(font.glyphs.length) // numberOfGlyphs
// Array of glyph name indexes
let index = 258 // first index of custom glyph name, it is calculated as glyph name index + 258
font.glyphs.forEach(function (glyph) {
if (glyph.unicode === 0) {
buf.writeUint16(0) // missed element should have .notDef name in the Macintosh standard order.
} else {
buf.writeUint16(index++)
}
})
// Array of glyph name indexes
names.forEach(function (name) {
buf.writeBytes(name)
})
return buf
}

View File

@ -0,0 +1,127 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2024/02/04 11:56:58
*/
import { Point, isInLine } from '../math.js'
// Remove points, that looks like straight line
export function simplify(contours, accuracy) {
return contours.map(function (contour) {
let i, curr, prev, next
let p, pPrev, pNext
// run from the end, to simplify array elements removal
for (i = contour.length - 2; i > 1; i--) {
prev = contour[i - 1]
next = contour[i + 1]
curr = contour[i]
// skip point (both oncurve & offcurve),
// if [prev,next] is straight line
if (prev.onCurve && next.onCurve) {
p = new Point(curr.x, curr.y)
pPrev = new Point(prev.x, prev.y)
pNext = new Point(next.x, next.y)
if (isInLine(pPrev, p, pNext, accuracy)) {
contour.splice(i, 1)
}
}
}
return contour
})
}
// Remove interpolateable oncurve points
// Those should be in the middle of nebor offcurve points
export function interpolate(contours, accuracy) {
return contours.map(function (contour) {
let resContour = []
contour.forEach(function (point, idx) {
// Never skip first and last points
if (idx === 0 || idx === contour.length - 1) {
resContour.push(point)
return
}
let prev = contour[idx - 1]
let next = contour[idx + 1]
let p, pPrev, pNext
// skip interpolateable oncurve points (if exactly between previous and next offcurves)
if (!prev.onCurve && point.onCurve && !next.onCurve) {
p = new Point(point.x, point.y)
pPrev = new Point(prev.x, prev.y)
pNext = new Point(next.x, next.y)
if (pPrev.add(pNext).div(2).sub(p).dist() < accuracy) {
return
}
}
// keep the rest
resContour.push(point)
})
return resContour
})
}
export function roundPoints(contours) {
return contours.map(contour =>
contour.map(p => ({
x: Math.round(p.x),
y: Math.round(p.y),
onCurve: p.onCurve
}))
)
}
// Remove closing point if it is the same as first point of contour.
// TTF doesn't need this point when drawing contours.
export function removeClosingReturnPoints(contours) {
return contours.map(function (contour) {
let length = contour.length
if (
length > 1 &&
contour[0].x === contour[length - 1].x &&
contour[0].y === contour[length - 1].y
) {
contour.splice(length - 1)
}
return contour
})
}
export function toRelative(contours) {
let prevPoint = { x: 0, y: 0 }
let resContours = []
contours.forEach(function (contour) {
let resContour = []
resContours.push(resContour)
contour.forEach(function (point) {
resContour.push({
x: point.x - prevPoint.x,
y: point.y - prevPoint.y,
onCurve: point.onCurve
})
prevPoint = point
})
})
return resContours
}
export function identifier(string, littleEndian) {
let result = 0
for (let i = 0; i < string.length; i++) {
result = result << 8
let index = littleEndian ? string.length - i - 1 : i
result += string.charCodeAt(index)
}
return result
}

View File

@ -0,0 +1,50 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2024/02/04 10:45:51
*/
// Taken from the punycode library
function ucs2encode(array) {
return _.map(array, function (value) {
let output = ''
if (value > 0xffff) {
value -= 0x10000
output += String.fromCharCode(((value >>> 10) & 0x3ff) | 0xd800)
value = 0xdc00 | (value & 0x3ff)
}
output += String.fromCharCode(value)
return output
}).join('')
}
function ucs2decode(string) {
let output = [],
counter = 0,
length = string.length,
value,
extra
while (counter < length) {
value = string.charCodeAt(counter++)
if (value >= 0xd800 && value <= 0xdbff && counter < length) {
// high surrogate, and there is a next character
extra = string.charCodeAt(counter++)
if ((extra & 0xfc00) === 0xdc00) {
// low surrogate
output.push(((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000)
} else {
// unmatched surrogate; only append this code unit, in case the next
// code unit is the high surrogate of a surrogate pair
output.push(value)
counter--
}
} else {
output.push(value)
}
}
return output
}
export { ucs2encode as encode, ucs2decode as decode }

9
test/index.js Normal file
View File

@ -0,0 +1,9 @@
/**
* {test unit}
* @author yutent<yutent.io@gmail.com>
* @date 2024/02/04 11:48:14
*/
import { svg2ttf } from '../src/index.js'
console.log(svg2ttf)