UNPKG

opentype.js

Version:
587 lines (532 loc) 20.8 kB
// The Font object import Path from './path'; import sfnt from './tables/sfnt'; import { DefaultEncoding } from './encoding'; import glyphset from './glyphset'; import Position from './position'; import Substitution from './substitution'; import { isBrowser, checkArgument, arrayBufferToNodeBuffer } 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.'); // OS X will complain if the names are empty, so we put a single space everywhere by default. this.names = { fontFamily: {en: options.familyName || ' '}, fontSubfamily: {en: options.styleName || ' '}, fullName: {en: options.fullName || options.familyName + ' ' + options.styleName}, // postScriptName may not contain any whitespace postScriptName: {en: options.postScriptName || (options.familyName + options.styleName).replace(/\s/g, '')}, designer: {en: options.designer || ' '}, designerURL: {en: options.designerURL || ' '}, manufacturer: {en: options.manufacturer || ' '}, manufacturerURL: {en: options.manufacturerURL || ' '}, license: {en: options.license || ' '}, licenseURL: {en: options.licenseURL || ' '}, version: {en: options.version || 'Version 0.1'}, description: {en: options.description || ' '}, copyright: {en: options.copyright || ' '}, trademark: {en: options.trademark || ' '} }; 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; }; /** * @param {string} * @return {Number} */ Font.prototype.nameToGlyphIndex = function(name) { return this.glyphNames.nameToGlyphIndex(name); }; /** * @param {string} * @return {opentype.Glyph} */ Font.prototype.nameToGlyph = function(name) { const glyphIndex = this.nameToGlyphIndex(name); let glyph = this.glyphs.get(glyphIndex); if (!glyph) { // .notdef glyph = this.glyphs.get(0); } return glyph; }; /** * @param {Number} * @return {String} */ Font.prototype.glyphIndexToName = function(gid) { if (!this.glyphNames.glyphIndexToName) { return ''; } return this.glyphNames.glyphIndexToName(gid); }; /** * 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() {}); }; /** * Draw the text on the given drawing context. * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. * @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 */ Font.prototype.draw = function(ctx, text, x, y, fontSize, options) { this.getPath(text, x, y, fontSize, options).draw(ctx); }; /** * Draw the points of all glyphs in the text. * On-curve points will be drawn in blue, off-curve points will be drawn in red. * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. * @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 */ Font.prototype.drawPoints = function(ctx, text, x, y, fontSize, options) { this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { glyph.drawPoints(ctx, gX, gY, gFontSize); }); }; /** * Draw lines indicating important font measurements for all glyphs in the text. * Black lines indicate the origin of the coordinate system (point 0,0). * Blue lines indicate the glyph bounding box. * Green line indicates the advance width of the glyph. * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. * @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 */ Font.prototype.drawMetrics = function(ctx, text, x, y, fontSize, options) { this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { glyph.drawMetrics(ctx, gX, gY, gFontSize); }); }; /** * @param {string} * @return {string} */ Font.prototype.getEnglishName = function(name) { const translations = this.names[name]; if (translations) { return translations.en; } }; /** * Validate */ Font.prototype.validate = function() { const warnings = []; const _this = this; function assert(predicate, message) { if (!predicate) { warnings.push(message); } } function assertNamePresent(name) { const englishName = _this.getEnglishName(name); assert(englishName && englishName.trim().length > 0, 'No English ' + name + ' specified.'); } // Identification information assertNamePresent('fontFamily'); assertNamePresent('weightName'); assertNamePresent('manufacturer'); assertNamePresent('copyright'); assertNamePresent('version'); // Dimension information assert(this.unitsPerEm > 0, 'No unitsPerEm specified.'); }; /** * Convert the font object to a SFNT data structure. * This structure contains all the necessary tables and metadata to create a binary OTF file. * @return {opentype.Table} */ Font.prototype.toTables = function() { return sfnt.fontToTable(this); }; /** * @deprecated Font.toBuffer is deprecated. Use Font.toArrayBuffer instead. */ Font.prototype.toBuffer = function() { console.warn('Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.'); return this.toArrayBuffer(); }; /** * Converts a `opentype.Font` into an `ArrayBuffer` * @return {ArrayBuffer} */ Font.prototype.toArrayBuffer = function() { const sfntTable = this.toTables(); const bytes = sfntTable.encode(); const buffer = new ArrayBuffer(bytes.length); const intArray = new Uint8Array(buffer); for (let i = 0; i < bytes.length; i++) { intArray[i] = bytes[i]; } return buffer; }; /** * Initiate a download of the OpenType font. */ Font.prototype.download = function(fileName) { const familyName = this.getEnglishName('fontFamily'); const styleName = this.getEnglishName('fontSubfamily'); fileName = fileName || familyName.replace(/\s/g, '') + '-' + styleName + '.otf'; const arrayBuffer = this.toArrayBuffer(); if (isBrowser()) { window.URL = window.URL || window.webkitURL; if (window.URL) { const dataView = new DataView(arrayBuffer); const blob = new Blob([dataView], {type: 'font/opentype'}); let link = document.createElement('a'); link.href = window.URL.createObjectURL(blob); link.download = fileName; let event = document.createEvent('MouseEvents'); event.initEvent('click', true, false); link.dispatchEvent(event); } else { console.warn('Font file could not be downloaded. Try using a different browser.'); } } else { const fs = require('fs'); const buffer = arrayBufferToNodeBuffer(arrayBuffer); fs.writeFileSync(fileName, buffer); } }; /** * @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;