完成svg2ttf的语法移植
parent
9e1bb413ff
commit
a064679ddf
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,3 +3,5 @@
|
|||
* @author yutent<yutent.io@gmail.com>
|
||||
* @date 2024/01/31 18:38:35
|
||||
*/
|
||||
|
||||
export { svg2ttf } from './lib/core.js'
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 = ''
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 it’s 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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 }
|
|
@ -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)
|
Loading…
Reference in New Issue