434 lines
14 KiB
Plaintext
434 lines
14 KiB
Plaintext
|
// 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;
|