555 lines
14 KiB
JavaScript
555 lines
14 KiB
JavaScript
import * as r from 'restructure';
|
||
import { cache } from './decorators';
|
||
import * as 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';
|
||
import { asciiDecoder } from './utils';
|
||
|
||
/**
|
||
* 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 {
|
||
type = 'TTF';
|
||
|
||
static probe(buffer) {
|
||
let format = asciiDecoder.decode(buffer.slice(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);
|
||
}
|
||
}
|