From a064679ddfab59d5f4b55d8bb64a08d111c9d302 Mon Sep 17 00:00:00 2001 From: yutent Date: Sun, 4 Feb 2024 16:45:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90svg2ttf=E7=9A=84=E8=AF=AD?= =?UTF-8?q?=E6=B3=95=E7=A7=BB=E6=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- src/index.js | 2 + src/lib/core.js | 8 +- src/lib/svg2ttf/index.js | 216 +++++++++++++ src/lib/svg2ttf/lib/math.js | 59 ++++ src/lib/svg2ttf/lib/microbuffer.js | 188 ++++++++++++ src/lib/svg2ttf/lib/sfnt.js | 379 +++++++++++++++++++++++ src/lib/svg2ttf/lib/str.js | 42 +++ src/lib/svg2ttf/lib/svg.js | 263 ++++++++++++++++ src/lib/svg2ttf/lib/ttf.js | 156 ++++++++++ src/lib/svg2ttf/lib/ttf/tables/cmap.js | 298 ++++++++++++++++++ src/lib/svg2ttf/lib/ttf/tables/glyf.js | 193 ++++++++++++ src/lib/svg2ttf/lib/ttf/tables/gsub.js | 400 +++++++++++++++++++++++++ src/lib/svg2ttf/lib/ttf/tables/head.js | 39 +++ src/lib/svg2ttf/lib/ttf/tables/hhea.js | 28 ++ src/lib/svg2ttf/lib/ttf/tables/hmtx.js | 13 + src/lib/svg2ttf/lib/ttf/tables/loca.js | 37 +++ src/lib/svg2ttf/lib/ttf/tables/maxp.js | 40 +++ src/lib/svg2ttf/lib/ttf/tables/name.js | 115 +++++++ src/lib/svg2ttf/lib/ttf/tables/os2.js | 94 ++++++ src/lib/svg2ttf/lib/ttf/tables/post.js | 67 +++++ src/lib/svg2ttf/lib/ttf/utils.js | 127 ++++++++ src/lib/svg2ttf/lib/ucs2.js | 50 ++++ test/index.js | 9 + 24 files changed, 2823 insertions(+), 5 deletions(-) create mode 100644 src/lib/svg2ttf/index.js create mode 100644 src/lib/svg2ttf/lib/math.js create mode 100644 src/lib/svg2ttf/lib/microbuffer.js create mode 100644 src/lib/svg2ttf/lib/sfnt.js create mode 100644 src/lib/svg2ttf/lib/str.js create mode 100644 src/lib/svg2ttf/lib/svg.js create mode 100644 src/lib/svg2ttf/lib/ttf.js create mode 100644 src/lib/svg2ttf/lib/ttf/tables/cmap.js create mode 100644 src/lib/svg2ttf/lib/ttf/tables/glyf.js create mode 100644 src/lib/svg2ttf/lib/ttf/tables/gsub.js create mode 100644 src/lib/svg2ttf/lib/ttf/tables/head.js create mode 100644 src/lib/svg2ttf/lib/ttf/tables/hhea.js create mode 100644 src/lib/svg2ttf/lib/ttf/tables/hmtx.js create mode 100644 src/lib/svg2ttf/lib/ttf/tables/loca.js create mode 100644 src/lib/svg2ttf/lib/ttf/tables/maxp.js create mode 100644 src/lib/svg2ttf/lib/ttf/tables/name.js create mode 100644 src/lib/svg2ttf/lib/ttf/tables/os2.js create mode 100644 src/lib/svg2ttf/lib/ttf/tables/post.js create mode 100644 src/lib/svg2ttf/lib/ttf/utils.js create mode 100644 src/lib/svg2ttf/lib/ucs2.js create mode 100644 test/index.js diff --git a/package.json b/package.json index 18ff41c..72f02b6 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/index.js b/src/index.js index ea35255..ee9024e 100644 --- a/src/index.js +++ b/src/index.js @@ -3,3 +3,5 @@ * @author yutent * @date 2024/01/31 18:38:35 */ + +export { svg2ttf } from './lib/core.js' diff --git a/src/lib/core.js b/src/lib/core.js index 5b02524..3b2f4e8 100644 --- a/src/lib/core.js +++ b/src/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 } diff --git a/src/lib/svg2ttf/index.js b/src/lib/svg2ttf/index.js new file mode 100644 index 0000000..26d3e84 --- /dev/null +++ b/src/lib/svg2ttf/index.js @@ -0,0 +1,216 @@ +/* + * Copyright: Vitaly Puzrin + * Author: Sergey Batishchev + * + * 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 +} diff --git a/src/lib/svg2ttf/lib/math.js b/src/lib/svg2ttf/lib/math.js new file mode 100644 index 0000000..0299ace --- /dev/null +++ b/src/lib/svg2ttf/lib/math.js @@ -0,0 +1,59 @@ +/** + * {Point} + * @author yutent + * @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 +} diff --git a/src/lib/svg2ttf/lib/microbuffer.js b/src/lib/svg2ttf/lib/microbuffer.js new file mode 100644 index 0000000..400173b --- /dev/null +++ b/src/lib/svg2ttf/lib/microbuffer.js @@ -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) + } +} diff --git a/src/lib/svg2ttf/lib/sfnt.js b/src/lib/svg2ttf/lib/sfnt.js new file mode 100644 index 0000000..e6bdc56 --- /dev/null +++ b/src/lib/svg2ttf/lib/sfnt.js @@ -0,0 +1,379 @@ +/** + * {description of this file} + * @author yutent + * @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 = '' +} diff --git a/src/lib/svg2ttf/lib/str.js b/src/lib/svg2ttf/lib/str.js new file mode 100644 index 0000000..6f3e394 --- /dev/null +++ b/src/lib/svg2ttf/lib/str.js @@ -0,0 +1,42 @@ +/** + * {} + * @author yutent + * @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 + } +} diff --git a/src/lib/svg2ttf/lib/svg.js b/src/lib/svg2ttf/lib/svg.js new file mode 100644 index 0000000..695d768 --- /dev/null +++ b/src/lib/svg2ttf/lib/svg.js @@ -0,0 +1,263 @@ +/** + * {} + * @author yutent + * @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 + let pathElem = glyphElem.getElementsByTagName('path')[0] + + if (pathElem.hasAttribute('d')) { + // 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 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 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 Copyright:>Fontello + if (metadata && metadata.textContent) { + font.metadata = metadata.textContent + } + + // Get 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 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 +} diff --git a/src/lib/svg2ttf/lib/ttf.js b/src/lib/svg2ttf/lib/ttf.js new file mode 100644 index 0000000..c0874b5 --- /dev/null +++ b/src/lib/svg2ttf/lib/ttf.js @@ -0,0 +1,156 @@ +/** + * {} + * @author yutent + * @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 +} diff --git a/src/lib/svg2ttf/lib/ttf/tables/cmap.js b/src/lib/svg2ttf/lib/ttf/tables/cmap.js new file mode 100644 index 0000000..f441075 --- /dev/null +++ b/src/lib/svg2ttf/lib/ttf/tables/cmap.js @@ -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 +} diff --git a/src/lib/svg2ttf/lib/ttf/tables/glyf.js b/src/lib/svg2ttf/lib/ttf/tables/glyf.js new file mode 100644 index 0000000..b9c8b1e --- /dev/null +++ b/src/lib/svg2ttf/lib/ttf/tables/glyf.js @@ -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 +} diff --git a/src/lib/svg2ttf/lib/ttf/tables/gsub.js b/src/lib/svg2ttf/lib/ttf/tables/gsub.js new file mode 100644 index 0000000..48dcb75 --- /dev/null +++ b/src/lib/svg2ttf/lib/ttf/tables/gsub.js @@ -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 +} diff --git a/src/lib/svg2ttf/lib/ttf/tables/head.js b/src/lib/svg2ttf/lib/ttf/tables/head.js new file mode 100644 index 0000000..11b0f99 --- /dev/null +++ b/src/lib/svg2ttf/lib/ttf/tables/head.js @@ -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 +} diff --git a/src/lib/svg2ttf/lib/ttf/tables/hhea.js b/src/lib/svg2ttf/lib/ttf/tables/hhea.js new file mode 100644 index 0000000..ece4313 --- /dev/null +++ b/src/lib/svg2ttf/lib/ttf/tables/hhea.js @@ -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 +} diff --git a/src/lib/svg2ttf/lib/ttf/tables/hmtx.js b/src/lib/svg2ttf/lib/ttf/tables/hmtx.js new file mode 100644 index 0000000..64f147d --- /dev/null +++ b/src/lib/svg2ttf/lib/ttf/tables/hmtx.js @@ -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 +} diff --git a/src/lib/svg2ttf/lib/ttf/tables/loca.js b/src/lib/svg2ttf/lib/ttf/tables/loca.js new file mode 100644 index 0000000..b0a993e --- /dev/null +++ b/src/lib/svg2ttf/lib/ttf/tables/loca.js @@ -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 +} diff --git a/src/lib/svg2ttf/lib/ttf/tables/maxp.js b/src/lib/svg2ttf/lib/ttf/tables/maxp.js new file mode 100644 index 0000000..8b98f3a --- /dev/null +++ b/src/lib/svg2ttf/lib/ttf/tables/maxp.js @@ -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 +} diff --git a/src/lib/svg2ttf/lib/ttf/tables/name.js b/src/lib/svg2ttf/lib/ttf/tables/name.js new file mode 100644 index 0000000..b3bffbd --- /dev/null +++ b/src/lib/svg2ttf/lib/ttf/tables/name.js @@ -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 +} diff --git a/src/lib/svg2ttf/lib/ttf/tables/os2.js b/src/lib/svg2ttf/lib/ttf/tables/os2.js new file mode 100644 index 0000000..210d520 --- /dev/null +++ b/src/lib/svg2ttf/lib/ttf/tables/os2.js @@ -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 +} diff --git a/src/lib/svg2ttf/lib/ttf/tables/post.js b/src/lib/svg2ttf/lib/ttf/tables/post.js new file mode 100644 index 0000000..a9ac0b9 --- /dev/null +++ b/src/lib/svg2ttf/lib/ttf/tables/post.js @@ -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 +} diff --git a/src/lib/svg2ttf/lib/ttf/utils.js b/src/lib/svg2ttf/lib/ttf/utils.js new file mode 100644 index 0000000..f6f0ac2 --- /dev/null +++ b/src/lib/svg2ttf/lib/ttf/utils.js @@ -0,0 +1,127 @@ +/** + * {} + * @author yutent + * @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 +} diff --git a/src/lib/svg2ttf/lib/ucs2.js b/src/lib/svg2ttf/lib/ucs2.js new file mode 100644 index 0000000..f8796a2 --- /dev/null +++ b/src/lib/svg2ttf/lib/ucs2.js @@ -0,0 +1,50 @@ +/** + * {} + * @author yutent + * @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 } diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..fa5c619 --- /dev/null +++ b/test/index.js @@ -0,0 +1,9 @@ +/** + * {test unit} + * @author yutent + * @date 2024/02/04 11:48:14 + */ + +import { svg2ttf } from '../src/index.js' + +console.log(svg2ttf)