// The Font object import Path from './path'; import { DefaultEncoding } from './encoding'; import glyphset from './glyphset'; import Position from './position'; import Substitution from './substitution'; import { checkArgument } from './util'; import HintingTrueType from './hintingtt'; import Bidi from './bidi'; /** * @typedef FontOptions * @type Object * @property {Boolean} empty - whether to create a new empty font * @property {string} familyName * @property {string} styleName * @property {string=} fullName * @property {string=} postScriptName * @property {string=} designer * @property {string=} designerURL * @property {string=} manufacturer * @property {string=} manufacturerURL * @property {string=} license * @property {string=} licenseURL * @property {string=} version * @property {string=} description * @property {string=} copyright * @property {string=} trademark * @property {Number} unitsPerEm * @property {Number} ascender * @property {Number} descender * @property {Number} createdTimestamp * @property {string=} weightClass * @property {string=} widthClass * @property {string=} fsSelection */ /** * A Font represents a loaded OpenType font file. * It contains a set of glyphs and methods to draw text on a drawing context, * or to get a path representing the text. * @exports opentype.Font * @class * @param {FontOptions} * @constructor */ function Font(options) { options = options || {}; options.tables = options.tables || {}; if (!options.empty) { // Check that we've provided the minimum set of names. checkArgument( options.familyName, 'When creating a new Font object, familyName is required.' ); checkArgument( options.styleName, 'When creating a new Font object, styleName is required.' ); checkArgument( options.unitsPerEm, 'When creating a new Font object, unitsPerEm is required.' ); checkArgument( options.ascender, 'When creating a new Font object, ascender is required.' ); checkArgument( options.descender <= 0, 'When creating a new Font object, negative descender value is required.' ); this.unitsPerEm = options.unitsPerEm || 1000; this.ascender = options.ascender; this.descender = options.descender; this.createdTimestamp = options.createdTimestamp; this.tables = Object.assign(options.tables, { os2: Object.assign( { usWeightClass: options.weightClass || this.usWeightClasses.MEDIUM, usWidthClass: options.widthClass || this.usWidthClasses.MEDIUM, fsSelection: options.fsSelection || this.fsSelectionValues.REGULAR, }, options.tables.os2 ), }); } this.supported = true; // Deprecated: parseBuffer will throw an error if font is not supported. this.glyphs = new glyphset.GlyphSet(this, options.glyphs || []); this.encoding = new DefaultEncoding(this); this.position = new Position(this); this.substitution = new Substitution(this); this.tables = this.tables || {}; // needed for low memory mode only. this._push = null; this._hmtxTableData = {}; Object.defineProperty(this, 'hinting', { get: function () { if (this._hinting) return this._hinting; if (this.outlinesFormat === 'truetype') { return (this._hinting = new HintingTrueType(this)); } }, }); } /** * Check if the font has a glyph for the given character. * @param {string} * @return {Boolean} */ Font.prototype.hasChar = function (c) { return this.encoding.charToGlyphIndex(c) !== null; }; /** * Convert the given character to a single glyph index. * Note that this function assumes that there is a one-to-one mapping between * the given character and a glyph; for complex scripts this might not be the case. * @param {string} * @return {Number} */ Font.prototype.charToGlyphIndex = function (s) { return this.encoding.charToGlyphIndex(s); }; /** * Convert the given character to a single Glyph object. * Note that this function assumes that there is a one-to-one mapping between * the given character and a glyph; for complex scripts this might not be the case. * @param {string} * @return {opentype.Glyph} */ Font.prototype.charToGlyph = function (c) { const glyphIndex = this.charToGlyphIndex(c); let glyph = this.glyphs.get(glyphIndex); if (!glyph) { // .notdef glyph = this.glyphs.get(0); } return glyph; }; /** * Update features * @param {any} options features options */ Font.prototype.updateFeatures = function (options) { // TODO: update all features options not only 'latn'. return this.defaultRenderOptions.features.map((feature) => { if (feature.script === 'latn') { return { script: 'latn', tags: feature.tags.filter((tag) => options[tag]), }; } else { return feature; } }); }; /** * Convert the given text to a list of Glyph objects. * Note that there is no strict one-to-one mapping between characters and * glyphs, so the list of returned glyphs can be larger or smaller than the * length of the given string. * @param {string} * @param {GlyphRenderOptions} [options] * @return {opentype.Glyph[]} */ Font.prototype.stringToGlyphs = function (s, options) { const bidi = new Bidi(); // Create and register 'glyphIndex' state modifier const charToGlyphIndexMod = (token) => this.charToGlyphIndex(token.char); bidi.registerModifier('glyphIndex', null, charToGlyphIndexMod); // roll-back to default features let features = options ? this.updateFeatures(options.features) : this.defaultRenderOptions.features; bidi.applyFeatures(this, features); const indexes = bidi.getTextGlyphs(s); let length = indexes.length; // convert glyph indexes to glyph objects const glyphs = new Array(length); const notdef = this.glyphs.get(0); for (let i = 0; i < length; i += 1) { glyphs[i] = this.glyphs.get(indexes[i]) || notdef; } return glyphs; }; /** * Retrieve the value of the kerning pair between the left glyph (or its index) * and the right glyph (or its index). If no kerning pair is found, return 0. * The kerning value gets added to the advance width when calculating the spacing * between glyphs. * For GPOS kerning, this method uses the default script and language, which covers * most use cases. To have greater control, use font.position.getKerningValue . * @param {opentype.Glyph} leftGlyph * @param {opentype.Glyph} rightGlyph * @return {Number} */ Font.prototype.getKerningValue = function (leftGlyph, rightGlyph) { leftGlyph = leftGlyph.index || leftGlyph; rightGlyph = rightGlyph.index || rightGlyph; const gposKerning = this.position.defaultKerningTables; if (gposKerning) { return this.position.getKerningValue( gposKerning, leftGlyph, rightGlyph ); } // "kern" table return this.kerningPairs[leftGlyph + ',' + rightGlyph] || 0; }; /** * @typedef GlyphRenderOptions * @type Object * @property {string} [script] - script used to determine which features to apply. By default, 'DFLT' or 'latn' is used. * See https://www.microsoft.com/typography/otspec/scripttags.htm * @property {string} [language='dflt'] - language system used to determine which features to apply. * See https://www.microsoft.com/typography/developers/opentype/languagetags.aspx * @property {boolean} [kerning=true] - whether to include kerning values * @property {object} [features] - OpenType Layout feature tags. Used to enable or disable the features of the given script/language system. * See https://www.microsoft.com/typography/otspec/featuretags.htm */ Font.prototype.defaultRenderOptions = { kerning: true, features: [ /** * these 4 features are required to render Arabic text properly * and shouldn't be turned off when rendering arabic text. */ { script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] }, { script: 'latn', tags: ['liga', 'rlig'] }, ], }; /** * Helper function that invokes the given callback for each glyph in the given text. * The callback gets `(glyph, x, y, fontSize, options)`.* @param {string} text * @param {string} text - The text to apply. * @param {number} [x=0] - Horizontal position of the beginning of the text. * @param {number} [y=0] - Vertical position of the *baseline* of the text. * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. * @param {GlyphRenderOptions=} options * @param {Function} callback */ Font.prototype.forEachGlyph = function ( text, x, y, fontSize, options, callback ) { x = x !== undefined ? x : 0; y = y !== undefined ? y : 0; fontSize = fontSize !== undefined ? fontSize : 72; options = Object.assign({}, this.defaultRenderOptions, options); const fontScale = (1 / this.unitsPerEm) * fontSize; const glyphs = this.stringToGlyphs(text, options); let kerningLookups; if (options.kerning) { const script = options.script || this.position.getDefaultScriptName(); kerningLookups = this.position.getKerningTables( script, options.language ); } for (let i = 0; i < glyphs.length; i += 1) { const glyph = glyphs[i]; callback.call(this, glyph, x, y, fontSize, options); if (glyph.advanceWidth) { x += glyph.advanceWidth * fontScale; } if (options.kerning && i < glyphs.length - 1) { // We should apply position adjustment lookups in a more generic way. // Here we only use the xAdvance value. const kerningValue = kerningLookups ? this.position.getKerningValue( kerningLookups, glyph.index, glyphs[i + 1].index ) : this.getKerningValue(glyph, glyphs[i + 1]); x += kerningValue * fontScale; } if (options.letterSpacing) { x += options.letterSpacing * fontSize; } else if (options.tracking) { x += (options.tracking / 1000) * fontSize; } } return x; }; /** * Create a Path object that represents the given text. * @param {string} text - The text to create. * @param {number} [x=0] - Horizontal position of the beginning of the text. * @param {number} [y=0] - Vertical position of the *baseline* of the text. * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. * @param {GlyphRenderOptions=} options * @return {opentype.Path} */ Font.prototype.getPath = function (text, x, y, fontSize, options) { const fullPath = new Path(); this.forEachGlyph( text, x, y, fontSize, options, function (glyph, gX, gY, gFontSize) { const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this); fullPath.extend(glyphPath); } ); return fullPath; }; /** * Create an array of Path objects that represent the glyphs of a given text. * @param {string} text - The text to create. * @param {number} [x=0] - Horizontal position of the beginning of the text. * @param {number} [y=0] - Vertical position of the *baseline* of the text. * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. * @param {GlyphRenderOptions=} options * @return {opentype.Path[]} */ Font.prototype.getPaths = function (text, x, y, fontSize, options) { const glyphPaths = []; this.forEachGlyph( text, x, y, fontSize, options, function (glyph, gX, gY, gFontSize) { const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this); glyphPaths.push(glyphPath); } ); return glyphPaths; }; /** * Returns the advance width of a text. * * This is something different than Path.getBoundingBox() as for example a * suffixed whitespace increases the advanceWidth but not the bounding box * or an overhanging letter like a calligraphic 'f' might have a quite larger * bounding box than its advance width. * * This corresponds to canvas2dContext.measureText(text).width * * @param {string} text - The text to create. * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. * @param {GlyphRenderOptions=} options * @return advance width */ Font.prototype.getAdvanceWidth = function (text, fontSize, options) { return this.forEachGlyph(text, 0, 0, fontSize, options, function () {}); }; /** * @private */ Font.prototype.fsSelectionValues = { ITALIC: 0x001, //1 UNDERSCORE: 0x002, //2 NEGATIVE: 0x004, //4 OUTLINED: 0x008, //8 STRIKEOUT: 0x010, //16 BOLD: 0x020, //32 REGULAR: 0x040, //64 USER_TYPO_METRICS: 0x080, //128 WWS: 0x100, //256 OBLIQUE: 0x200, //512 }; /** * @private */ Font.prototype.usWidthClasses = { ULTRA_CONDENSED: 1, EXTRA_CONDENSED: 2, CONDENSED: 3, SEMI_CONDENSED: 4, MEDIUM: 5, SEMI_EXPANDED: 6, EXPANDED: 7, EXTRA_EXPANDED: 8, ULTRA_EXPANDED: 9, }; /** * @private */ Font.prototype.usWeightClasses = { THIN: 100, EXTRA_LIGHT: 200, LIGHT: 300, NORMAL: 400, MEDIUM: 500, SEMI_BOLD: 600, BOLD: 700, EXTRA_BOLD: 800, BLACK: 900, }; export default Font;