astro-ghostcms/.pnpm-store/v3/files/58/cdfe550ec011858706e4c390c9c...

434 lines
14 KiB
Plaintext
Raw Permalink Normal View History

2024-02-14 14:10:47 +00:00
// 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;