import r from 'restructure'; import brotli from 'brotli/decompress'; import TTFFont from './TTFFont'; import TTFGlyph, { Point } from './glyph/TTFGlyph'; import WOFF2Glyph from './glyph/WOFF2Glyph'; import WOFF2Directory from './tables/WOFF2Directory'; /** * Subclass of TTFFont that represents a TTF/OTF font compressed by WOFF2 * See spec here: http://www.w3.org/TR/WOFF2/ */ export default class WOFF2Font extends TTFFont { static probe(buffer) { return buffer.toString('ascii', 0, 4) === 'wOF2'; } _decodeDirectory() { this.directory = WOFF2Directory.decode(this.stream); this._dataPos = this.stream.pos; } _decompress() { // decompress data and setup table offsets if we haven't already if (!this._decompressed) { this.stream.pos = this._dataPos; let buffer = this.stream.readBuffer(this.directory.totalCompressedSize); let decompressedSize = 0; for (let tag in this.directory.tables) { let entry = this.directory.tables[tag]; entry.offset = decompressedSize; decompressedSize += (entry.transformLength != null) ? entry.transformLength : entry.length; } let decompressed = brotli(buffer, decompressedSize); if (!decompressed) { throw new Error('Error decoding compressed data in WOFF2'); } this.stream = new r.DecodeStream(new Buffer(decompressed)); this._decompressed = true; } } _decodeTable(table) { this._decompress(); return super._decodeTable(table); } // Override this method to get a glyph and return our // custom subclass if there is a glyf table. _getBaseGlyph(glyph, characters = []) { if (!this._glyphs[glyph]) { if (this.directory.tables.glyf && this.directory.tables.glyf.transformed) { if (!this._transformedGlyphs) { this._transformGlyfTable(); } return this._glyphs[glyph] = new WOFF2Glyph(glyph, characters, this); } else { return super._getBaseGlyph(glyph, characters); } } } _transformGlyfTable() { this._decompress(); this.stream.pos = this.directory.tables.glyf.offset; let table = GlyfTable.decode(this.stream); let glyphs = []; for (let index = 0; index < table.numGlyphs; index++) { let glyph = {}; let nContours = table.nContours.readInt16BE(); glyph.numberOfContours = nContours; if (nContours > 0) { // simple glyph let nPoints = []; let totalPoints = 0; for (let i = 0; i < nContours; i++) { let r = read255UInt16(table.nPoints); totalPoints += r; nPoints.push(totalPoints); } glyph.points = decodeTriplet(table.flags, table.glyphs, totalPoints); for (let i = 0; i < nContours; i++) { glyph.points[nPoints[i] - 1].endContour = true; } var instructionSize = read255UInt16(table.glyphs); } else if (nContours < 0) { // composite glyph let haveInstructions = TTFGlyph.prototype._decodeComposite.call({ _font: this }, glyph, table.composites); if (haveInstructions) { var instructionSize = read255UInt16(table.glyphs); } } glyphs.push(glyph); } this._transformedGlyphs = glyphs; } } // Special class that accepts a length and returns a sub-stream for that data class Substream { constructor(length) { this.length = length; this._buf = new r.Buffer(length); } decode(stream, parent) { return new r.DecodeStream(this._buf.decode(stream, parent)); } } // This struct represents the entire glyf table let GlyfTable = new r.Struct({ version: r.uint32, numGlyphs: r.uint16, indexFormat: r.uint16, nContourStreamSize: r.uint32, nPointsStreamSize: r.uint32, flagStreamSize: r.uint32, glyphStreamSize: r.uint32, compositeStreamSize: r.uint32, bboxStreamSize: r.uint32, instructionStreamSize: r.uint32, nContours: new Substream('nContourStreamSize'), nPoints: new Substream('nPointsStreamSize'), flags: new Substream('flagStreamSize'), glyphs: new Substream('glyphStreamSize'), composites: new Substream('compositeStreamSize'), bboxes: new Substream('bboxStreamSize'), instructions: new Substream('instructionStreamSize') }); const WORD_CODE = 253; const ONE_MORE_BYTE_CODE2 = 254; const ONE_MORE_BYTE_CODE1 = 255; const LOWEST_U_CODE = 253; function read255UInt16(stream) { let code = stream.readUInt8(); if (code === WORD_CODE) { return stream.readUInt16BE(); } if (code === ONE_MORE_BYTE_CODE1) { return stream.readUInt8() + LOWEST_U_CODE; } if (code === ONE_MORE_BYTE_CODE2) { return stream.readUInt8() + LOWEST_U_CODE * 2; } return code; } function withSign(flag, baseval) { return flag & 1 ? baseval : -baseval; } function decodeTriplet(flags, glyphs, nPoints) { let y; let x = y = 0; let res = []; for (let i = 0; i < nPoints; i++) { let dx = 0, dy = 0; let flag = flags.readUInt8(); let onCurve = !(flag >> 7); flag &= 0x7f; if (flag < 10) { dx = 0; dy = withSign(flag, ((flag & 14) << 7) + glyphs.readUInt8()); } else if (flag < 20) { dx = withSign(flag, (((flag - 10) & 14) << 7) + glyphs.readUInt8()); dy = 0; } else if (flag < 84) { var b0 = flag - 20; var b1 = glyphs.readUInt8(); dx = withSign(flag, 1 + (b0 & 0x30) + (b1 >> 4)); dy = withSign(flag >> 1, 1 + ((b0 & 0x0c) << 2) + (b1 & 0x0f)); } else if (flag < 120) { var b0 = flag - 84; dx = withSign(flag, 1 + ((b0 / 12) << 8) + glyphs.readUInt8()); dy = withSign(flag >> 1, 1 + (((b0 % 12) >> 2) << 8) + glyphs.readUInt8()); } else if (flag < 124) { var b1 = glyphs.readUInt8(); let b2 = glyphs.readUInt8(); dx = withSign(flag, (b1 << 4) + (b2 >> 4)); dy = withSign(flag >> 1, ((b2 & 0x0f) << 8) + glyphs.readUInt8()); } else { dx = withSign(flag, glyphs.readUInt16BE()); dy = withSign(flag >> 1, glyphs.readUInt16BE()); } x += dx; y += dy; res.push(new Point(onCurve, false, x, y)); } return res; }