import r from 'restructure'; import { cache } from './decorators'; import fontkit from './base'; import Directory from './tables/directory'; import tables from './tables'; import CmapProcessor from './CmapProcessor'; import LayoutEngine from './layout/LayoutEngine'; import TTFGlyph from './glyph/TTFGlyph'; import CFFGlyph from './glyph/CFFGlyph'; import SBIXGlyph from './glyph/SBIXGlyph'; import COLRGlyph from './glyph/COLRGlyph'; import GlyphVariationProcessor from './glyph/GlyphVariationProcessor'; import TTFSubset from './subset/TTFSubset'; import CFFSubset from './subset/CFFSubset'; import BBox from './glyph/BBox'; /** * This is the base class for all SFNT-based font formats in fontkit. * It supports TrueType, and PostScript glyphs, and several color glyph formats. */ export default class TTFFont { static probe(buffer) { let format = buffer.toString('ascii', 0, 4); return format === 'true' || format === 'OTTO' || format === String.fromCharCode(0, 1, 0, 0); } constructor(stream, variationCoords = null) { this.defaultLanguage = null; this.stream = stream; this.variationCoords = variationCoords; this._directoryPos = this.stream.pos; this._tables = {}; this._glyphs = {}; this._decodeDirectory(); // define properties for each table to lazily parse for (let tag in this.directory.tables) { let table = this.directory.tables[tag]; if (tables[tag] && table.length > 0) { Object.defineProperty(this, tag, { get: this._getTable.bind(this, table) }); } } } setDefaultLanguage(lang = null) { this.defaultLanguage = lang; } _getTable(table) { if (!(table.tag in this._tables)) { try { this._tables[table.tag] = this._decodeTable(table); } catch (e) { if (fontkit.logErrors) { console.error(`Error decoding table ${table.tag}`); console.error(e.stack); } } } return this._tables[table.tag]; } _getTableStream(tag) { let table = this.directory.tables[tag]; if (table) { this.stream.pos = table.offset; return this.stream; } return null; } _decodeDirectory() { return this.directory = Directory.decode(this.stream, {_startOffset: 0}); } _decodeTable(table) { let pos = this.stream.pos; let stream = this._getTableStream(table.tag); let result = tables[table.tag].decode(stream, this, table.length); this.stream.pos = pos; return result; } /** * Gets a string from the font's `name` table * `lang` is a BCP-47 language code. * @return {string} */ getName(key, lang = this.defaultLanguage || fontkit.defaultLanguage) { let record = this.name && this.name.records[key]; if (record) { // Attempt to retrieve the entry, depending on which translation is available: return ( record[lang] || record[this.defaultLanguage] || record[fontkit.defaultLanguage] || record['en'] || record[Object.keys(record)[0]] // Seriously, ANY language would be fine || null ); } return null; } /** * The unique PostScript name for this font, e.g. "Helvetica-Bold" * @type {string} */ get postscriptName() { return this.getName('postscriptName'); } /** * The font's full name, e.g. "Helvetica Bold" * @type {string} */ get fullName() { return this.getName('fullName'); } /** * The font's family name, e.g. "Helvetica" * @type {string} */ get familyName() { return this.getName('fontFamily'); } /** * The font's sub-family, e.g. "Bold". * @type {string} */ get subfamilyName() { return this.getName('fontSubfamily'); } /** * The font's copyright information * @type {string} */ get copyright() { return this.getName('copyright'); } /** * The font's version number * @type {string} */ get version() { return this.getName('version'); } /** * The font’s [ascender](https://en.wikipedia.org/wiki/Ascender_(typography)) * @type {number} */ get ascent() { return this.hhea.ascent; } /** * The font’s [descender](https://en.wikipedia.org/wiki/Descender) * @type {number} */ get descent() { return this.hhea.descent; } /** * The amount of space that should be included between lines * @type {number} */ get lineGap() { return this.hhea.lineGap; } /** * The offset from the normal underline position that should be used * @type {number} */ get underlinePosition() { return this.post.underlinePosition; } /** * The weight of the underline that should be used * @type {number} */ get underlineThickness() { return this.post.underlineThickness; } /** * If this is an italic font, the angle the cursor should be drawn at to match the font design * @type {number} */ get italicAngle() { return this.post.italicAngle; } /** * The height of capital letters above the baseline. * See [here](https://en.wikipedia.org/wiki/Cap_height) for more details. * @type {number} */ get capHeight() { let os2 = this['OS/2']; return os2 ? os2.capHeight : this.ascent; } /** * The height of lower case letters in the font. * See [here](https://en.wikipedia.org/wiki/X-height) for more details. * @type {number} */ get xHeight() { let os2 = this['OS/2']; return os2 ? os2.xHeight : 0; } /** * The number of glyphs in the font. * @type {number} */ get numGlyphs() { return this.maxp.numGlyphs; } /** * The size of the font’s internal coordinate grid * @type {number} */ get unitsPerEm() { return this.head.unitsPerEm; } /** * The font’s bounding box, i.e. the box that encloses all glyphs in the font. * @type {BBox} */ @cache get bbox() { return Object.freeze(new BBox(this.head.xMin, this.head.yMin, this.head.xMax, this.head.yMax)); } @cache get _cmapProcessor() { return new CmapProcessor(this.cmap); } /** * An array of all of the unicode code points supported by the font. * @type {number[]} */ @cache get characterSet() { return this._cmapProcessor.getCharacterSet(); } /** * Returns whether there is glyph in the font for the given unicode code point. * * @param {number} codePoint * @return {boolean} */ hasGlyphForCodePoint(codePoint) { return !!this._cmapProcessor.lookup(codePoint); } /** * Maps a single unicode code point to a Glyph object. * Does not perform any advanced substitutions (there is no context to do so). * * @param {number} codePoint * @return {Glyph} */ glyphForCodePoint(codePoint) { return this.getGlyph(this._cmapProcessor.lookup(codePoint), [codePoint]); } /** * Returns an array of Glyph objects for the given string. * This is only a one-to-one mapping from characters to glyphs. * For most uses, you should use font.layout (described below), which * provides a much more advanced mapping supporting AAT and OpenType shaping. * * @param {string} string * @return {Glyph[]} */ glyphsForString(string) { let glyphs = []; let len = string.length; let idx = 0; let last = -1; let state = -1; while (idx <= len) { let code = 0; let nextState = 0; if (idx < len) { // Decode the next codepoint from UTF 16 code = string.charCodeAt(idx++); if (0xd800 <= code && code <= 0xdbff && idx < len) { let next = string.charCodeAt(idx); if (0xdc00 <= next && next <= 0xdfff) { idx++; code = ((code & 0x3ff) << 10) + (next & 0x3ff) + 0x10000; } } // Compute the next state: 1 if the next codepoint is a variation selector, 0 otherwise. nextState = ((0xfe00 <= code && code <= 0xfe0f) || (0xe0100 <= code && code <= 0xe01ef)) ? 1 : 0; } else { idx++; } if (state === 0 && nextState === 1) { // Variation selector following normal codepoint. glyphs.push(this.getGlyph(this._cmapProcessor.lookup(last, code), [last, code])); } else if (state === 0 && nextState === 0) { // Normal codepoint following normal codepoint. glyphs.push(this.glyphForCodePoint(last)); } last = code; state = nextState; } return glyphs; } @cache get _layoutEngine() { return new LayoutEngine(this); } /** * Returns a GlyphRun object, which includes an array of Glyphs and GlyphPositions for the given string. * * @param {string} string * @param {string[]} [userFeatures] * @param {string} [script] * @param {string} [language] * @param {string} [direction] * @return {GlyphRun} */ layout(string, userFeatures, script, language, direction) { return this._layoutEngine.layout(string, userFeatures, script, language, direction); } /** * Returns an array of strings that map to the given glyph id. * @param {number} gid - glyph id */ stringsForGlyph(gid) { return this._layoutEngine.stringsForGlyph(gid); } /** * An array of all [OpenType feature tags](https://www.microsoft.com/typography/otspec/featuretags.htm) * (or mapped AAT tags) supported by the font. * The features parameter is an array of OpenType feature tags to be applied in addition to the default set. * If this is an AAT font, the OpenType feature tags are mapped to AAT features. * * @type {string[]} */ get availableFeatures() { return this._layoutEngine.getAvailableFeatures(); } getAvailableFeatures(script, language) { return this._layoutEngine.getAvailableFeatures(script, language); } _getBaseGlyph(glyph, characters = []) { if (!this._glyphs[glyph]) { if (this.directory.tables.glyf) { this._glyphs[glyph] = new TTFGlyph(glyph, characters, this); } else if (this.directory.tables['CFF '] || this.directory.tables.CFF2) { this._glyphs[glyph] = new CFFGlyph(glyph, characters, this); } } return this._glyphs[glyph] || null; } /** * Returns a glyph object for the given glyph id. * You can pass the array of code points this glyph represents for * your use later, and it will be stored in the glyph object. * * @param {number} glyph * @param {number[]} characters * @return {Glyph} */ getGlyph(glyph, characters = []) { if (!this._glyphs[glyph]) { if (this.directory.tables.sbix) { this._glyphs[glyph] = new SBIXGlyph(glyph, characters, this); } else if ((this.directory.tables.COLR) && (this.directory.tables.CPAL)) { this._glyphs[glyph] = new COLRGlyph(glyph, characters, this); } else { this._getBaseGlyph(glyph, characters); } } return this._glyphs[glyph] || null; } /** * Returns a Subset for this font. * @return {Subset} */ createSubset() { if (this.directory.tables['CFF ']) { return new CFFSubset(this); } return new TTFSubset(this); } /** * Returns an object describing the available variation axes * that this font supports. Keys are setting tags, and values * contain the axis name, range, and default value. * * @type {object} */ @cache get variationAxes() { let res = {}; if (!this.fvar) { return res; } for (let axis of this.fvar.axis) { res[axis.axisTag.trim()] = { name: axis.name.en, min: axis.minValue, default: axis.defaultValue, max: axis.maxValue }; } return res; } /** * Returns an object describing the named variation instances * that the font designer has specified. Keys are variation names * and values are the variation settings for this instance. * * @type {object} */ @cache get namedVariations() { let res = {}; if (!this.fvar) { return res; } for (let instance of this.fvar.instance) { let settings = {}; for (let i = 0; i < this.fvar.axis.length; i++) { let axis = this.fvar.axis[i]; settings[axis.axisTag.trim()] = instance.coord[i]; } res[instance.name.en] = settings; } return res; } /** * Returns a new font with the given variation settings applied. * Settings can either be an instance name, or an object containing * variation tags as specified by the `variationAxes` property. * * @param {object} settings * @return {TTFFont} */ getVariation(settings) { if (!(this.directory.tables.fvar && ((this.directory.tables.gvar && this.directory.tables.glyf) || this.directory.tables.CFF2))) { throw new Error('Variations require a font with the fvar, gvar and glyf, or CFF2 tables.'); } if (typeof settings === 'string') { settings = this.namedVariations[settings]; } if (typeof settings !== 'object') { throw new Error('Variation settings must be either a variation name or settings object.'); } // normalize the coordinates let coords = this.fvar.axis.map((axis, i) => { let axisTag = axis.axisTag.trim(); if (axisTag in settings) { return Math.max(axis.minValue, Math.min(axis.maxValue, settings[axisTag])); } else { return axis.defaultValue; } }); let stream = new r.DecodeStream(this.stream.buffer); stream.pos = this._directoryPos; let font = new TTFFont(stream, coords); font._tables = this._tables; return font; } @cache get _variationProcessor() { if (!this.fvar) { return null; } let variationCoords = this.variationCoords; // Ignore if no variation coords and not CFF2 if (!variationCoords && !this.CFF2) { return null; } if (!variationCoords) { variationCoords = this.fvar.axis.map(axis => axis.defaultValue); } return new GlyphVariationProcessor(this, variationCoords); } // Standardized format plugin API getFont(name) { return this.getVariation(name); } }