@mark1russell7/typeset 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/build/chunks/engines.d.ts +32 -0
- package/build/chunks/engines.d.ts.map +1 -0
- package/build/chunks/engines.js +35 -0
- package/build/chunks/engines.js.map +1 -0
- package/build/chunks/font.d.ts +203 -0
- package/build/chunks/font.d.ts.map +1 -0
- package/build/chunks/font.js +3550 -0
- package/build/chunks/font.js.map +1 -0
- package/build/chunks/fonts.d.ts +8 -0
- package/build/chunks/fonts.d.ts.map +1 -0
- package/build/chunks/fonts.js +13 -0
- package/build/chunks/fonts.js.map +1 -0
- package/build/chunks/measurer.d.ts +67 -0
- package/build/chunks/measurer.d.ts.map +1 -0
- package/build/chunks/measurer.js +277 -0
- package/build/chunks/measurer.js.map +1 -0
- package/build/chunks/sfnt.d.ts +22 -0
- package/build/chunks/sfnt.d.ts.map +1 -0
- package/build/chunks/text.js.map +1 -1
- package/build/fonts-inspect.d.ts +10 -0
- package/build/fonts-inspect.d.ts.map +1 -0
- package/build/fonts-inspect.js +1 -0
- package/build/fonts.d.ts +5 -0
- package/build/fonts.js +4 -0
- package/build/index.d.ts +3 -1
- package/build/index.js +3 -1
- package/build/measurer.d.ts +2 -0
- package/build/measurer.js +1 -0
- package/package.json +17 -3
- package/src/fonts/bidi.ts +125 -0
- package/src/fonts/binary.ts +41 -0
- package/src/fonts/engines.ts +38 -0
- package/src/fonts/font.ts +442 -0
- package/src/fonts/gvar.ts +220 -0
- package/src/fonts/index.ts +10 -0
- package/src/fonts/indic.ts +585 -0
- package/src/fonts/joining.ts +141 -0
- package/src/fonts/kerning.ts +217 -0
- package/src/fonts/sfnt.ts +102 -0
- package/src/fonts/substitution.ts +586 -0
- package/src/fonts/tables.ts +140 -0
- package/src/fonts/variations.ts +175 -0
- package/src/fonts/words.ts +119 -0
- package/src/fonts-inspect.ts +8 -0
- package/src/fonts.ts +8 -0
- package/src/index.ts +2 -0
- package/src/measurer/advance.ts +307 -0
- package/src/measurer.ts +5 -0
- package/src/text/rope.ts +1 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small model of the Unicode Bidirectional Algorithm (UAX #9) for one paragraph with the base level 0. Blink adds the
|
|
3
|
+
* widths of the words of a canvas in the visual order, so the single-precision sum needs this order. The model has no
|
|
4
|
+
* explicit embeddings, no isolates and no brackets (rule N0). It gets the bidi class of a character from its script and
|
|
5
|
+
* its general category, so it knows the classes of the common characters only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A bidi class of UAX #9. */
|
|
9
|
+
type BidiClass = "L" | "R" | "AL" | "EN" | "ES" | "ET" | "AN" | "CS" | "NSM" | "BN" | "B" | "S" | "WS" | "ON";
|
|
10
|
+
|
|
11
|
+
const nonSpacingMark = /[\p{Mn}\p{Me}]/u;
|
|
12
|
+
const currency = /\p{Sc}/u;
|
|
13
|
+
const format = /\p{Cf}/u;
|
|
14
|
+
const hebrew = /\p{Script_Extensions=Hebrew}/u;
|
|
15
|
+
const arabicLetter = /[\p{Script_Extensions=Arabic}\p{Script_Extensions=Syriac}\p{Script_Extensions=Thaana}]/u;
|
|
16
|
+
const letter = /[\p{L}\p{Mc}]/u;
|
|
17
|
+
const commonSeparators: ReadonlySet<number> = new Set([0x2c, 0x2e, 0x2f, 0x3a, 0xa0, 0x60c, 0x202f, 0x2044]);
|
|
18
|
+
|
|
19
|
+
/** This function gives the bidi class of a character, from its code point, its script and its general category. */
|
|
20
|
+
function bidiClass(character: string): BidiClass {
|
|
21
|
+
const c = character.codePointAt(0)!;
|
|
22
|
+
if (nonSpacingMark.test(character)) return "NSM";
|
|
23
|
+
if ((c >= 0x30 && c <= 0x39) || (c >= 0x6f0 && c <= 0x6f9) || c === 0xb2 || c === 0xb3 || c === 0xb9 || (c >= 0xff10 && c <= 0xff19)) return "EN";
|
|
24
|
+
if ((c >= 0x660 && c <= 0x669) || c === 0x66b || c === 0x66c) return "AN";
|
|
25
|
+
if (c === 0x2b || c === 0x2d || c === 0x2212) return "ES";
|
|
26
|
+
if (c === 0x23 || c === 0x25 || c === 0xb0 || c === 0x2030 || currency.test(character)) return "ET";
|
|
27
|
+
if (commonSeparators.has(c)) return "CS";
|
|
28
|
+
if (c === 0x9 || c === 0xb || c === 0x1f) return "S";
|
|
29
|
+
if (c === 0xa || c === 0xd || (c >= 0x1c && c <= 0x1e) || c === 0x85 || c === 0x2029) return "B";
|
|
30
|
+
if (c === 0x20 || c === 0xc || (c >= 0x2000 && c <= 0x200a) || c === 0x2028 || c === 0x3000) return "WS";
|
|
31
|
+
if (format.test(character)) return "BN";
|
|
32
|
+
if (hebrew.test(character)) return "R";
|
|
33
|
+
if (arabicLetter.test(character)) return "AL";
|
|
34
|
+
return letter.test(character) ? "L" : "ON";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** This function gives the direction of a strong class for the rule N1: `EN` and `AN` count as `R`. */
|
|
38
|
+
function strongDirection(type: BidiClass): "L" | "R" | undefined {
|
|
39
|
+
if (type === "L") return "L";
|
|
40
|
+
return type === "R" || type === "EN" || type === "AN" ? "R" : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* This function gives the bidi level of each code point of a text, in a paragraph with the base level 0. It applies the
|
|
45
|
+
* rules W1 to W7, N1, N2, I1 and L1 of UAX #9.
|
|
46
|
+
*/
|
|
47
|
+
export function bidiLevels(text: string): number[] {
|
|
48
|
+
const characters = [...text];
|
|
49
|
+
const original = characters.map(bidiClass);
|
|
50
|
+
const types = [...original];
|
|
51
|
+
const n = types.length;
|
|
52
|
+
// W1: a mark gets the class of the character before it
|
|
53
|
+
for (let i = 0; i < n; i++) if (types[i] === "NSM") types[i] = i === 0 || types[i - 1] === "BN" ? "L" : types[i - 1]!;
|
|
54
|
+
// W2 and W3: a European digit after an Arabic letter is an Arabic digit, and an Arabic letter is R
|
|
55
|
+
let strong: BidiClass = "L";
|
|
56
|
+
for (let i = 0; i < n; i++) {
|
|
57
|
+
const type = types[i]!;
|
|
58
|
+
if (type === "L" || type === "R" || type === "AL") strong = type;
|
|
59
|
+
else if (type === "EN" && strong === "AL") types[i] = "AN";
|
|
60
|
+
}
|
|
61
|
+
for (let i = 0; i < n; i++) if (types[i] === "AL") types[i] = "R";
|
|
62
|
+
// W4: one separator between two digits of the same class gets their class
|
|
63
|
+
for (let i = 1; i + 1 < n; i++) {
|
|
64
|
+
const before = types[i - 1]!;
|
|
65
|
+
if (types[i] === "ES" && before === "EN" && types[i + 1] === "EN") types[i] = "EN";
|
|
66
|
+
else if (types[i] === "CS" && (before === "EN" || before === "AN") && types[i + 1] === before) types[i] = before;
|
|
67
|
+
}
|
|
68
|
+
// W5: a sequence of terminators next to a European digit is EN
|
|
69
|
+
for (let i = 0; i < n; i++) {
|
|
70
|
+
if (types[i] !== "ET") continue;
|
|
71
|
+
let end = i;
|
|
72
|
+
while (end < n && types[end] === "ET") end++;
|
|
73
|
+
if ((i > 0 && types[i - 1] === "EN") || (end < n && types[end] === "EN")) types.fill("EN", i, end);
|
|
74
|
+
i = end - 1;
|
|
75
|
+
}
|
|
76
|
+
// W6 and W7: the other separators and terminators are ON, and a European digit after L is L
|
|
77
|
+
for (let i = 0; i < n; i++) if (types[i] === "ES" || types[i] === "ET" || types[i] === "CS") types[i] = "ON";
|
|
78
|
+
strong = "L";
|
|
79
|
+
for (let i = 0; i < n; i++) {
|
|
80
|
+
const type = types[i]!;
|
|
81
|
+
if (type === "L" || type === "R") strong = type;
|
|
82
|
+
else if (type === "EN" && strong === "L") types[i] = "L";
|
|
83
|
+
}
|
|
84
|
+
// N1 and N2: a sequence of neutrals gets the direction of the strong characters on its two sides, or else L
|
|
85
|
+
for (let i = 0; i < n; i++) {
|
|
86
|
+
if (strongDirection(types[i]!) !== undefined) continue;
|
|
87
|
+
let end = i;
|
|
88
|
+
while (end < n && strongDirection(types[end]!) === undefined) end++;
|
|
89
|
+
const before = i === 0 ? "L" : strongDirection(types[i - 1]!)!;
|
|
90
|
+
const after = end === n ? "L" : strongDirection(types[end]!)!;
|
|
91
|
+
types.fill(before === after ? before : "L", i, end);
|
|
92
|
+
i = end - 1;
|
|
93
|
+
}
|
|
94
|
+
// I1: R goes up one level, and the digits go up two levels
|
|
95
|
+
const levels = types.map((type) => (type === "R" ? 1 : type === "AN" || type === "EN" ? 2 : 0));
|
|
96
|
+
// L1: the white space at the end and before each separator goes back to the base level
|
|
97
|
+
const isSpace = (i: number) => original[i] === "WS" || original[i] === "BN";
|
|
98
|
+
for (let i = n - 1; i >= 0 && isSpace(i); i--) levels[i] = 0;
|
|
99
|
+
for (let i = 0; i < n; i++) {
|
|
100
|
+
if (original[i] !== "S" && original[i] !== "B") continue;
|
|
101
|
+
levels[i] = 0;
|
|
102
|
+
for (let k = i - 1; k >= 0 && isSpace(k); k--) levels[k] = 0;
|
|
103
|
+
}
|
|
104
|
+
return levels;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* This function puts items in the visual order of the rule L2 of UAX #9. Each item has the level `levels[i]`. From the
|
|
109
|
+
* highest level to the level 1, it reverses each sequence of items at that level or higher.
|
|
110
|
+
*/
|
|
111
|
+
export function visualOrder<T>(items: readonly T[], levels: readonly number[]): T[] {
|
|
112
|
+
const order = items.map((_, i) => i);
|
|
113
|
+
const at = [...levels];
|
|
114
|
+
for (let level = Math.max(0, ...levels); level >= 1; level--) {
|
|
115
|
+
for (let i = 0; i < order.length; i++) {
|
|
116
|
+
if (at[i]! < level) continue;
|
|
117
|
+
let end = i;
|
|
118
|
+
while (end < order.length && at[end]! >= level) end++;
|
|
119
|
+
order.splice(i, end - i, ...order.slice(i, end).reverse());
|
|
120
|
+
at.splice(i, end - i, ...at.slice(i, end).reverse());
|
|
121
|
+
i = end;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return order.map((i) => items[i]!);
|
|
125
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** A big-endian reader over a part of a font file. The offsets are relative to the start of the part. */
|
|
2
|
+
export class Reader {
|
|
3
|
+
readonly bytes: Uint8Array;
|
|
4
|
+
private readonly view: DataView;
|
|
5
|
+
|
|
6
|
+
constructor(bytes: Uint8Array) {
|
|
7
|
+
this.bytes = bytes;
|
|
8
|
+
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get length(): number {
|
|
12
|
+
return this.bytes.byteLength;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
u8(offset: number): number {
|
|
16
|
+
return this.view.getUint8(offset);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
u16(offset: number): number {
|
|
20
|
+
return this.view.getUint16(offset);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
i16(offset: number): number {
|
|
24
|
+
return this.view.getInt16(offset);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
u32(offset: number): number {
|
|
28
|
+
return this.view.getUint32(offset);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
tag(offset: number): string {
|
|
32
|
+
return String.fromCharCode(this.u8(offset), this.u8(offset + 1), this.u8(offset + 2), this.u8(offset + 3));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** This method gives a reader over the bytes from `offset`, with the length `length` or to the end. */
|
|
36
|
+
part(offset: number, length?: number): Reader {
|
|
37
|
+
const end = length === undefined ? this.bytes.byteLength : offset + length;
|
|
38
|
+
if (offset < 0 || end > this.bytes.byteLength || offset > end) throw new RangeError(`the part ${offset}..${end} is outside ${this.bytes.byteLength} bytes`);
|
|
39
|
+
return new Reader(this.bytes.subarray(offset, end));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules of one text engine for the widths of a font. The fields `deltas` and `metrics` are options of `readFont`, and
|
|
3
|
+
* the fields `rounding` and `sum` are options of `measureWidth`. Thus a caller spreads the same row into the two calls.
|
|
4
|
+
*/
|
|
5
|
+
export interface EngineRules {
|
|
6
|
+
/** The rounding of the `HVAR` and `gvar` deltas of an instance. */
|
|
7
|
+
readonly deltas: "round" | "exact";
|
|
8
|
+
/** The advance of a composite glyph: its own advance, or the advance of its component with the flag USE_MY_METRICS. */
|
|
9
|
+
readonly metrics: "own" | "component";
|
|
10
|
+
/** The rounding of each advance: `pixel` rounds it to whole px, and `exact` keeps the fraction. */
|
|
11
|
+
readonly rounding: "exact" | "pixel";
|
|
12
|
+
/**
|
|
13
|
+
* The sum of the advances: `exact`, or `float32` as Blink adds the widths of the words of a canvas in single precision.
|
|
14
|
+
* A sum of whole px is exact in single precision, so the rounding `pixel` needs no `float32` sum.
|
|
15
|
+
*/
|
|
16
|
+
readonly sum: "exact" | "float32";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The rules of the text engines of Chromium, one row for each engine. The fonts runs of typeset#27 measured each row
|
|
21
|
+
* against the canvas, and the T1d diagnosis of the sum measured the field `sum`. A new engine is a new row.
|
|
22
|
+
*/
|
|
23
|
+
export const engines: { readonly freetype: EngineRules; readonly coretext: EngineRules; readonly directwrite: EngineRules } = {
|
|
24
|
+
/** FreeType, as Chromium on Linux uses it: rounded deltas, and each advance in whole px. */
|
|
25
|
+
freetype: { deltas: "round", metrics: "own", rounding: "pixel", sum: "exact" },
|
|
26
|
+
/** CoreText on macOS: it keeps the fraction of the deltas. */
|
|
27
|
+
coretext: { deltas: "exact", metrics: "own", rounding: "exact", sum: "float32" },
|
|
28
|
+
/** DirectWrite on Windows: a composite glyph has the advance of its USE_MY_METRICS component. */
|
|
29
|
+
directwrite: { deltas: "round", metrics: "component", rounding: "exact", sum: "float32" },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** The name of a text engine of the table `engines`. */
|
|
33
|
+
export type EngineName = keyof typeof engines;
|
|
34
|
+
|
|
35
|
+
/** This function tells if a text is the name of an engine of the table `engines`. */
|
|
36
|
+
export function isEngineName(name: string): name is EngineName {
|
|
37
|
+
return Object.hasOwn(engines, name);
|
|
38
|
+
}
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { fnv1a32 } from "@mark1russell7/rigor-generators";
|
|
2
|
+
import type { Reader } from "./binary.ts";
|
|
3
|
+
import { readGposKerning, readKernTable, IGNORE_BASE, IGNORE_LIGATURES, IGNORE_MARKS, type PairLookup } from "./kerning.ts";
|
|
4
|
+
import { readTables, type Decompress, type TableMap } from "./sfnt.ts";
|
|
5
|
+
import { defaultFeatures, readGlyphFilter, readSubstitutions, type GlyphFilter, type SubstitutionLookup } from "./substitution.ts";
|
|
6
|
+
import { readAdvances, readCharacterMap, readGlyphClasses, readHeader } from "./tables.ts";
|
|
7
|
+
import { gvarAdvanceDeltas, readPointCounts, readTransformedPointCounts, type GlyphPoints } from "./gvar.ts";
|
|
8
|
+
import { instanceAdvances, normalize, readAxes, readVariationStore, type Axis, type Instance } from "./variations.ts";
|
|
9
|
+
import { indicScripts, indicShaper } from "./indic.ts";
|
|
10
|
+
import { joiningFeatures, joiningForms, joiningScripts, joiningStages } from "./joining.ts";
|
|
11
|
+
import { canvasSegments, canvasWords } from "./words.ts";
|
|
12
|
+
|
|
13
|
+
/** The options of `readFont`. */
|
|
14
|
+
export interface ReadOptions {
|
|
15
|
+
/** The Brotli decompress function. A WOFF2 file needs it. */
|
|
16
|
+
readonly decompress?: Decompress;
|
|
17
|
+
/**
|
|
18
|
+
* The instance of a variable font, in the user units of its axes, for example `{ wght: 400 }`. An absent axis has its
|
|
19
|
+
* default value. A browser uses the CSS weight, so a page with the weight `normal` needs `{ wght: 400 }`.
|
|
20
|
+
*/
|
|
21
|
+
readonly instance?: Instance;
|
|
22
|
+
/**
|
|
23
|
+
* The rounding of the `HVAR` deltas. `round` (the default) rounds each delta to whole font units, as FreeType does.
|
|
24
|
+
* `exact` keeps the fraction. The epoch tells the two modes apart.
|
|
25
|
+
*/
|
|
26
|
+
readonly deltas?: "round" | "exact";
|
|
27
|
+
/**
|
|
28
|
+
* The source of the advances of an instance. `auto` (the default) uses `HVAR` when the font has it, else the phantom
|
|
29
|
+
* points of `gvar`. `gvar` uses the phantom points also when the font has `HVAR`. The epoch tells the two modes apart.
|
|
30
|
+
*/
|
|
31
|
+
readonly advances?: "auto" | "gvar";
|
|
32
|
+
/**
|
|
33
|
+
* The advance of a composite glyph. `own` (the default) uses the advance of the glyph, as FreeType and CoreText do.
|
|
34
|
+
* `component` uses the advance of its component with the flag USE_MY_METRICS, as DirectWrite does.
|
|
35
|
+
*/
|
|
36
|
+
readonly metrics?: "own" | "component";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The source of the advances of an instance: the default advances of `hmtx`, the deltas of `HVAR`, or the phantom points of `gvar`. */
|
|
40
|
+
export type AdvanceSource = "hmtx" | "HVAR" | "gvar";
|
|
41
|
+
|
|
42
|
+
/** The measured tables of one font file. */
|
|
43
|
+
export interface Font {
|
|
44
|
+
/**
|
|
45
|
+
* The font epoch: the revision of `head` and an FNV-1a hash of the data that gives the widths. A cache of widths can use
|
|
46
|
+
* it as a key. The TrueType file and the WOFF2 file of one font give the same epoch.
|
|
47
|
+
*/
|
|
48
|
+
readonly epoch: string;
|
|
49
|
+
readonly unitsPerEm: number;
|
|
50
|
+
/** The axes of `fvar`. A font that is not variable has no axes. */
|
|
51
|
+
readonly axes: readonly Axis[];
|
|
52
|
+
/** The instance of the font: the value of each axis in user units, after the clamp to the range of the axis. */
|
|
53
|
+
readonly instance: Instance;
|
|
54
|
+
/** The normalized coordinates of the instance, one for each axis, after `avar`. */
|
|
55
|
+
readonly coordinates: readonly number[];
|
|
56
|
+
/** The source of the advances of this instance. The default instance and a font that is not variable use `hmtx`. */
|
|
57
|
+
readonly advanceSource: AdvanceSource;
|
|
58
|
+
readonly numGlyphs: number;
|
|
59
|
+
/** This function gives the glyph of a code point, or 0 when the font has no glyph for it. */
|
|
60
|
+
glyph(codePoint: number): number;
|
|
61
|
+
/** This function gives the advance of a glyph in font units, as the shaper uses it. A mark has the advance 0. */
|
|
62
|
+
advance(glyph: number): number;
|
|
63
|
+
/** This function gives the GDEF class of a glyph: 1 base, 2 ligature, 3 mark, 4 component, 0 none. */
|
|
64
|
+
glyphClass(glyph: number): number;
|
|
65
|
+
/** This function gives the pair lookups of the kerning for an OpenType script tag, for example `latn`. */
|
|
66
|
+
kerning(script: string): readonly PairLookup[];
|
|
67
|
+
/** This function gives the GSUB lookups of a list of feature tags for an OpenType script tag, in the order of the lookup list. */
|
|
68
|
+
substitutions(script: string, features: readonly string[]): readonly SubstitutionLookup[];
|
|
69
|
+
/** This function gives the same font at another instance. It uses the tables that are already read. */
|
|
70
|
+
at(instance: Instance): Font;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** This function reads a TrueType, OpenType or WOFF2 file. */
|
|
74
|
+
export function readFont(bytes: Uint8Array, options: ReadOptions = {}): Font {
|
|
75
|
+
const tables = readTables(bytes, options.decompress);
|
|
76
|
+
const need = (tag: string) => {
|
|
77
|
+
const table = tables.get(tag);
|
|
78
|
+
if (table === undefined) throw new Error(`the font has no ${tag} table`);
|
|
79
|
+
return table;
|
|
80
|
+
};
|
|
81
|
+
const header = readHeader(need("head"), need("hhea"), need("maxp"));
|
|
82
|
+
const parsed: Parsed = {
|
|
83
|
+
tables,
|
|
84
|
+
header,
|
|
85
|
+
advances: readAdvances(need("hmtx"), header),
|
|
86
|
+
characters: readCharacterMap(need("cmap")),
|
|
87
|
+
glyphClass: readGlyphClasses(tables.get("GDEF")),
|
|
88
|
+
filter: readGlyphFilter(tables.get("GDEF"), readGlyphClasses(tables.get("GDEF"))),
|
|
89
|
+
axes: tables.has("fvar") ? readAxes(tables.get("fvar")!) : [],
|
|
90
|
+
points: lazy(() => pointsOf(tables, need("head"), header.numGlyphs)),
|
|
91
|
+
};
|
|
92
|
+
const modes: Modes = { deltas: options.deltas ?? "round", advances: options.advances ?? "auto", metrics: options.metrics ?? "own" };
|
|
93
|
+
return build(parsed, options.instance ?? {}, modes);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** This function gives a function that computes a value at its first call and then keeps it. */
|
|
97
|
+
function lazy<T>(compute: () => T): () => T {
|
|
98
|
+
let done = false;
|
|
99
|
+
let value: T | undefined;
|
|
100
|
+
return () => {
|
|
101
|
+
if (!done) {
|
|
102
|
+
value = compute();
|
|
103
|
+
done = true;
|
|
104
|
+
}
|
|
105
|
+
return value as T;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* This function gives the points of each glyph, or `undefined` when the font has no `glyf` table. A WOFF2 file keeps
|
|
111
|
+
* `glyf` in its transformed form, and its `loca` table is then empty.
|
|
112
|
+
*/
|
|
113
|
+
function pointsOf(tables: TableMap, head: Reader, numGlyphs: number): GlyphPoints | undefined {
|
|
114
|
+
const glyf = tables.get("glyf");
|
|
115
|
+
const loca = tables.get("loca");
|
|
116
|
+
if (glyf === undefined || loca === undefined) return undefined;
|
|
117
|
+
return loca.length === 0 ? readTransformedPointCounts(glyf, numGlyphs) : readPointCounts(glyf, loca, head.i16(50), numGlyphs);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The modes of `readFont` that stay the same for each instance of a font. */
|
|
121
|
+
interface Modes {
|
|
122
|
+
readonly deltas: "round" | "exact";
|
|
123
|
+
readonly advances: "auto" | "gvar";
|
|
124
|
+
readonly metrics: "own" | "component";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The tables of a font that do not change with the instance. */
|
|
128
|
+
interface Parsed {
|
|
129
|
+
readonly tables: TableMap;
|
|
130
|
+
readonly header: ReturnType<typeof readHeader>;
|
|
131
|
+
readonly advances: Uint16Array;
|
|
132
|
+
readonly characters: Map<number, number>;
|
|
133
|
+
readonly glyphClass: (glyph: number) => number;
|
|
134
|
+
readonly filter: GlyphFilter;
|
|
135
|
+
readonly axes: readonly Axis[];
|
|
136
|
+
/** The points of each glyph, for the phantom points of `gvar` and the flag USE_MY_METRICS. */
|
|
137
|
+
readonly points: () => GlyphPoints | undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** This function gives the font at one instance. */
|
|
141
|
+
function build(parsed: Parsed, requested: Instance, modes: Modes): Font {
|
|
142
|
+
const { tables, header, characters, glyphClass, axes } = parsed;
|
|
143
|
+
const { deltas } = modes;
|
|
144
|
+
const instance = Object.fromEntries(axes.map((axis) => [axis.tag, Math.min(axis.max, Math.max(axis.min, requested[axis.tag] ?? axis.default))]));
|
|
145
|
+
const coordinates = normalize(axes, tables.get("avar"), instance);
|
|
146
|
+
const variable = coordinates.some((value) => value !== 0);
|
|
147
|
+
const hvar = tables.get("HVAR");
|
|
148
|
+
const gvar = tables.get("gvar");
|
|
149
|
+
const phantoms = gvar !== undefined && (modes.advances === "gvar" || hvar === undefined) && parsed.points() !== undefined;
|
|
150
|
+
const advanceSource: AdvanceSource = !variable ? "hmtx" : phantoms ? "gvar" : hvar !== undefined ? "HVAR" : "hmtx";
|
|
151
|
+
const advances = advanceSource === "gvar" ? phantomAdvances(parsed.advances, gvarAdvanceDeltas(gvar!, parsed.points()!.counts, coordinates, deltas)) : instanceAdvances(parsed.advances, advanceSource === "HVAR" ? hvar : undefined, coordinates, deltas);
|
|
152
|
+
const metrics = modes.metrics === "component" ? parsed.points()?.metrics : undefined;
|
|
153
|
+
if (metrics !== undefined) componentAdvances(advances, metrics);
|
|
154
|
+
const gdef = tables.get("GDEF");
|
|
155
|
+
// The ItemVariationStore of GDEF 1.3 gives the deltas of the GPOS values
|
|
156
|
+
const storeOffset = variable && gdef !== undefined && gdef.u16(2) >= 3 ? gdef.u32(14) : 0;
|
|
157
|
+
const store = storeOffset === 0 ? undefined : readVariationStore(gdef!.part(storeOffset), coordinates);
|
|
158
|
+
const gpos = tables.get("GPOS");
|
|
159
|
+
const kern = tables.get("kern");
|
|
160
|
+
const gsub = tables.get("GSUB");
|
|
161
|
+
const lookups = new Map<string, readonly PairLookup[]>();
|
|
162
|
+
const substitutions = new Map<string, readonly SubstitutionLookup[]>();
|
|
163
|
+
const hashed = [tables.get("cmap"), gdef, gpos, kern, tables.get("fvar"), tables.get("avar"), tables.get("HVAR"), gsub];
|
|
164
|
+
return {
|
|
165
|
+
epoch: `${header.revision.toString(16).padStart(8, "0")}-${widthHash(header.unitsPerEm, coordinates, advances, hashed)}${deltas === "exact" ? "-exact" : ""}${advanceSource === "gvar" && hvar !== undefined ? "-gvar" : ""}${metrics !== undefined ? "-metrics" : ""}`,
|
|
166
|
+
unitsPerEm: header.unitsPerEm,
|
|
167
|
+
numGlyphs: header.numGlyphs,
|
|
168
|
+
axes,
|
|
169
|
+
instance,
|
|
170
|
+
coordinates,
|
|
171
|
+
advanceSource,
|
|
172
|
+
glyph: (codePoint) => characters.get(codePoint) ?? 0,
|
|
173
|
+
advance: (glyph) => (glyphClass(glyph) === 3 ? 0 : advances[glyph] ?? 0),
|
|
174
|
+
glyphClass,
|
|
175
|
+
kerning(script) {
|
|
176
|
+
let found = lookups.get(script);
|
|
177
|
+
if (found === undefined) {
|
|
178
|
+
// HarfBuzz uses the kern table only when the font has no GPOS table
|
|
179
|
+
found = gpos !== undefined ? readGposKerning(gpos, script, store, parsed.filter) : kern !== undefined ? readKernTable(kern) : [];
|
|
180
|
+
lookups.set(script, found);
|
|
181
|
+
}
|
|
182
|
+
return found;
|
|
183
|
+
},
|
|
184
|
+
substitutions(script, features) {
|
|
185
|
+
const key = `${script} ${features.join(",")}`;
|
|
186
|
+
let found = substitutions.get(key);
|
|
187
|
+
if (found === undefined) {
|
|
188
|
+
found = gsub === undefined ? [] : readSubstitutions(gsub, script, features, parsed.filter);
|
|
189
|
+
substitutions.set(key, found);
|
|
190
|
+
}
|
|
191
|
+
return found;
|
|
192
|
+
},
|
|
193
|
+
at: (next) => build(parsed, next, modes),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* This function gives each composite glyph the advance of its component with the flag USE_MY_METRICS. A component can
|
|
199
|
+
* have that flag in its own components, so the function follows the chain. A chain of more than 8 steps stops.
|
|
200
|
+
*/
|
|
201
|
+
function componentAdvances(advances: Float64Array, metrics: Int32Array): void {
|
|
202
|
+
const own = Float64Array.from(advances);
|
|
203
|
+
for (let glyph = 0; glyph < metrics.length; glyph++) {
|
|
204
|
+
let source = glyph;
|
|
205
|
+
for (let step = 0; step < 8 && (metrics[source] ?? -1) >= 0; step++) source = metrics[source]!;
|
|
206
|
+
if (source !== glyph && source < own.length) advances[glyph] = own[source]!;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** This function adds the advance delta of each glyph to its default advance. A negative sum becomes 0. */
|
|
211
|
+
function phantomAdvances(advances: Uint16Array, deltas: Float64Array): Float64Array {
|
|
212
|
+
return Float64Array.from(advances, (advance, glyph) => Math.max(0, advance + (deltas[glyph] ?? 0)));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* This function gives 8 hexadecimal digits of FNV-1a. The hash covers the units per em, the coordinates of the instance,
|
|
217
|
+
* the advances and the tables that change a width.
|
|
218
|
+
*/
|
|
219
|
+
function widthHash(unitsPerEm: number, coordinates: readonly number[], advances: Float64Array, tables: readonly (Reader | undefined)[]): string {
|
|
220
|
+
const parts: string[] = [String(unitsPerEm), coordinates.join(",")];
|
|
221
|
+
const units = Uint16Array.from(advances, (value) => value & 0xffff);
|
|
222
|
+
for (let i = 0; i < units.length; i += 4096) parts.push(String.fromCharCode(...units.subarray(i, i + 4096)));
|
|
223
|
+
for (const table of tables) {
|
|
224
|
+
parts.push(table === undefined ? "-" : "+");
|
|
225
|
+
if (table === undefined) continue;
|
|
226
|
+
for (let i = 0; i < table.length; i += 8192) parts.push(String.fromCharCode(...table.bytes.subarray(i, i + 8192)));
|
|
227
|
+
}
|
|
228
|
+
return fnv1a32(parts.join("")).toString(16).padStart(8, "0");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The options of `measureWidth`. */
|
|
232
|
+
export interface MeasureOptions {
|
|
233
|
+
/** The OpenType script tag of the text, for example `latn` or `cyrl`. The default is `DFLT`. */
|
|
234
|
+
readonly script?: string;
|
|
235
|
+
/**
|
|
236
|
+
* The rounding of the width. `exact` (the default) adds the advances with no rounding. `pixel` rounds each advance to
|
|
237
|
+
* 1/64 px and then to a whole px, and each kerning value to a whole px. Chromium on Linux does this at the device scale 1.
|
|
238
|
+
*/
|
|
239
|
+
readonly rounding?: "exact" | "pixel";
|
|
240
|
+
/** The pair kerning. When it is `false`, the width is the sum of the advances only. The default is `true`. */
|
|
241
|
+
readonly kerning?: boolean;
|
|
242
|
+
/**
|
|
243
|
+
* The GSUB features to apply before the kerning. The default is `defaultFeatures`: the features that a browser applies
|
|
244
|
+
* when the page does not change them. An empty list applies no feature of the list. The joining forms are not in the
|
|
245
|
+
* list. The option `joining` controls them.
|
|
246
|
+
*/
|
|
247
|
+
readonly features?: readonly string[];
|
|
248
|
+
/**
|
|
249
|
+
* The joining of the Arabic shaper of HarfBuzz. When it is `true` (the default), the scripts of `joiningScripts` get the
|
|
250
|
+
* joining forms, for example `init` and `fina`. Their features then go in the stages of `joiningStages`. The other
|
|
251
|
+
* scripts do not change. When it is `false`, all the scripts use the one stage of the other scripts.
|
|
252
|
+
*/
|
|
253
|
+
readonly joining?: boolean;
|
|
254
|
+
/**
|
|
255
|
+
* The Indic shaper of HarfBuzz. When it is `true` (the default), the scripts of `indicScripts` get the syllables, the
|
|
256
|
+
* reordering and the stages of `indicShaper`. When it is `false`, they use the one stage of the other scripts.
|
|
257
|
+
*/
|
|
258
|
+
readonly indic?: boolean;
|
|
259
|
+
/**
|
|
260
|
+
* The shaping of the browser to model. `dom` (the default) shapes the words of `shapingWords` and then kerns all the
|
|
261
|
+
* text. `canvas` shapes and kerns each word of `canvasWords` alone, as Blink does for the text of a canvas.
|
|
262
|
+
*/
|
|
263
|
+
readonly shaping?: "dom" | "canvas";
|
|
264
|
+
/**
|
|
265
|
+
* The sum of the advances. `exact` (the default) adds them with no rounding. `float32` models the canvas of Blink with
|
|
266
|
+
* CoreText and DirectWrite. Then the option `shaping` has no effect. The rounding `pixel` ignores this option.
|
|
267
|
+
*/
|
|
268
|
+
readonly sum?: "exact" | "float32";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** This function tells if a lookup with the flags `flags` skips a glyph of the GDEF class `glyphClass`. */
|
|
272
|
+
function skips(flags: number, glyphClass: number): boolean {
|
|
273
|
+
return ((flags & IGNORE_MARKS) !== 0 && glyphClass === 3) || ((flags & IGNORE_BASE) !== 0 && glyphClass === 1) || ((flags & IGNORE_LIGATURES) !== 0 && glyphClass === 2);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* This function divides a text into the words that the GSUB pass shapes one at a time. Each space and each tab is a word
|
|
278
|
+
* alone. Blink shapes the text of a canvas in the same words, so no GSUB context goes across a space.
|
|
279
|
+
*/
|
|
280
|
+
export function shapingWords(text: string): string[] {
|
|
281
|
+
const words: string[] = [];
|
|
282
|
+
let word = "";
|
|
283
|
+
for (const character of text) {
|
|
284
|
+
if (character !== " " && character !== " ") {
|
|
285
|
+
word += character;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (word !== "") words.push(word);
|
|
289
|
+
words.push(character);
|
|
290
|
+
word = "";
|
|
291
|
+
}
|
|
292
|
+
if (word !== "") words.push(word);
|
|
293
|
+
return words;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** The GSUB pass of one set of options: a function that gives the glyphs of one word, and the kerning lookups. */
|
|
297
|
+
function shaperOf(font: Font, options: MeasureOptions): { substitute: (word: string) => number[]; kerning: readonly PairLookup[] } {
|
|
298
|
+
const script = options.script ?? "DFLT";
|
|
299
|
+
const features = options.features ?? defaultFeatures;
|
|
300
|
+
const kerning = options.kerning === false ? [] : font.kerning(script);
|
|
301
|
+
if (options.indic !== false && indicScripts.includes(script)) return { substitute: indicShaper(font, script, features), kerning };
|
|
302
|
+
const joins = options.joining !== false && joiningScripts.includes(script);
|
|
303
|
+
const lookups = joins ? [] : font.substitutions(script, features);
|
|
304
|
+
const stages = joins ? joiningStages(features, features.includes("rclt") && font.substitutions(script, ["rclt"]).length > 0).map((stage) => ({ stage, lookups: font.substitutions(script, stage.features) })) : [];
|
|
305
|
+
const substitute = (word: string): number[] => {
|
|
306
|
+
const codePoints = [...word].map((character) => character.codePointAt(0)!);
|
|
307
|
+
const wordGlyphs = codePoints.map((codePoint) => font.glyph(codePoint));
|
|
308
|
+
for (const lookup of lookups) lookup.apply(wordGlyphs);
|
|
309
|
+
if (joins) {
|
|
310
|
+
// Each glyph has the bit of its joining form, and the bit 7 for the features of all the glyphs
|
|
311
|
+
const masks = joiningForms(codePoints).map((form) => 0x80 | (form === undefined ? 0 : 1 << joiningFeatures.indexOf(form)));
|
|
312
|
+
for (const { stage, lookups: stageLookups } of stages) {
|
|
313
|
+
const mask = stage.form === undefined ? 0x80 : 1 << joiningFeatures.indexOf(stage.form);
|
|
314
|
+
for (const lookup of stageLookups) lookup.apply(wordGlyphs, masks, mask);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return wordGlyphs;
|
|
318
|
+
};
|
|
319
|
+
return { substitute, kerning };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* This function gives the glyphs of a text and their advances in font units. With the shaping `dom`, it applies the GSUB
|
|
324
|
+
* substitutions to each word of `shapingWords`, and then the pair kerning to all the text. With the shaping `canvas`, it
|
|
325
|
+
* applies the substitutions and the kerning to each word of `canvasWords` alone. A script of `joiningScripts` also gets
|
|
326
|
+
* the joining forms of each word, in the stages of `joiningStages`. A script of `indicScripts` uses `indicShaper`.
|
|
327
|
+
*/
|
|
328
|
+
export function glyphAdvances(font: Font, text: string, options: MeasureOptions = {}): { glyphs: number[]; advances: number[] } {
|
|
329
|
+
const { substitute, kerning } = shaperOf(font, options);
|
|
330
|
+
const canvas = options.shaping === "canvas";
|
|
331
|
+
const glyphs: number[] = [];
|
|
332
|
+
const advances: number[] = [];
|
|
333
|
+
for (const word of canvas ? canvasWords(text) : shapingWords(text)) {
|
|
334
|
+
const wordGlyphs = substitute(word);
|
|
335
|
+
const wordAdvances = wordGlyphs.map((glyph) => font.advance(glyph));
|
|
336
|
+
if (canvas) kern(font, kerning, wordGlyphs, wordAdvances);
|
|
337
|
+
glyphs.push(...wordGlyphs);
|
|
338
|
+
advances.push(...wordAdvances);
|
|
339
|
+
}
|
|
340
|
+
if (!canvas) kern(font, kerning, glyphs, advances);
|
|
341
|
+
return { glyphs, advances };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The scale of HarfBuzz at one size. Blink gives HarfBuzz the scale of the size times 65536, so a position is in 1/65536 px.
|
|
346
|
+
* The field `multiplier` is the factor of `em_mult` in `hb-font.hh`.
|
|
347
|
+
*/
|
|
348
|
+
interface PositionScale {
|
|
349
|
+
readonly xScale: number;
|
|
350
|
+
readonly multiplier: number;
|
|
351
|
+
readonly unitsPerEm: number;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** This function rounds a value to the nearest whole number, and a half goes away from 0, as `roundf` does. */
|
|
355
|
+
function roundHalfAway(value: number): number {
|
|
356
|
+
return Math.sign(value) * Math.floor(Math.abs(value) + 0.5);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* This function gives a kerning value in 1/65536 px, as HarfBuzz scales it. HarfBuzz scales the value of the ValueRecord
|
|
361
|
+
* with `em_mult`, and the delta of the VariationIndex table in single precision with `em_scalef`.
|
|
362
|
+
*/
|
|
363
|
+
function kerningPosition(value: number, delta: number, scale: PositionScale): number {
|
|
364
|
+
const base = Math.round(value - delta);
|
|
365
|
+
const scaledDelta = delta === 0 ? 0 : roundHalfAway(Math.fround(Math.fround(Math.fround(delta) * scale.xScale) / scale.unitsPerEm));
|
|
366
|
+
return Math.floor((base * scale.multiplier + 32768) / 65536) + scaledDelta;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* This function adds the values of the pair kerning lookups `lookups` to the advances `advances` of the glyphs `glyphs`.
|
|
371
|
+
* When `positions` is given, it also adds each value in 1/65536 px, as HarfBuzz scales it at the scale `scale`.
|
|
372
|
+
*/
|
|
373
|
+
function kern(font: Font, lookups: readonly PairLookup[], glyphs: readonly number[], advances: number[], positions?: number[], scale?: PositionScale): void {
|
|
374
|
+
for (const lookup of lookups) {
|
|
375
|
+
const next = (from: number): number => {
|
|
376
|
+
for (let j = from; j < glyphs.length; j++) if (!(lookup.skip?.(glyphs[j]!) ?? skips(lookup.flags, font.glyphClass(glyphs[j]!)))) return j;
|
|
377
|
+
return -1;
|
|
378
|
+
};
|
|
379
|
+
let i = next(0);
|
|
380
|
+
while (i >= 0) {
|
|
381
|
+
const j = next(i + 1);
|
|
382
|
+
if (j < 0) break;
|
|
383
|
+
const value = lookup.pair(glyphs[i]!, glyphs[j]!);
|
|
384
|
+
if (value === undefined) {
|
|
385
|
+
i = j;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
advances[i]! += value.first;
|
|
389
|
+
advances[j]! += value.second;
|
|
390
|
+
if (positions !== undefined && scale !== undefined) {
|
|
391
|
+
positions[i]! += kerningPosition(value.first, value.firstDelta ?? 0, scale);
|
|
392
|
+
positions[j]! += kerningPosition(value.second, value.secondDelta ?? 0, scale);
|
|
393
|
+
}
|
|
394
|
+
i = value.skipSecond ? next(j + 1) : j;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* This function gives the width of a text with the sum `float32`, as Blink adds the widths of a canvas. Each advance in
|
|
401
|
+
* single precision is cut to 1/65536 px, and HarfBuzz scales each kerning value. The sum of a segment of `canvasSegments`
|
|
402
|
+
* is exact, and the sum of the segments is in single precision.
|
|
403
|
+
*/
|
|
404
|
+
function float32Width(font: Font, text: string, size: number, options: MeasureOptions): number {
|
|
405
|
+
const { substitute, kerning } = shaperOf(font, options);
|
|
406
|
+
const xScale = Math.trunc(Math.fround(size) * 65536);
|
|
407
|
+
const scale: PositionScale = { xScale, multiplier: Math.floor((xScale * 65536) / font.unitsPerEm), unitsPerEm: font.unitsPerEm };
|
|
408
|
+
const pxPerUnit = size / font.unitsPerEm;
|
|
409
|
+
let total = 0;
|
|
410
|
+
for (const segment of canvasSegments(text)) {
|
|
411
|
+
const glyphs = substitute(segment);
|
|
412
|
+
// Skia gives each advance in single precision, and Blink cuts it to the 16.16 fixed point of HarfBuzz
|
|
413
|
+
const positions = glyphs.map((glyph) => Math.trunc(Math.fround(font.advance(glyph) * pxPerUnit) * 65536));
|
|
414
|
+
kern(font, kerning, glyphs, glyphs.map((glyph) => font.advance(glyph)), positions, scale);
|
|
415
|
+
let units = 0;
|
|
416
|
+
for (const position of positions) units += position;
|
|
417
|
+
total = Math.fround(total + Math.fround(units / 65536));
|
|
418
|
+
}
|
|
419
|
+
return total;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* This function gives the width of a text in px at the size `size` in px. The width is the sum of the advances after the
|
|
424
|
+
* GSUB substitutions and the pair kerning.
|
|
425
|
+
*/
|
|
426
|
+
export function measureWidth(font: Font, text: string, size: number, options: MeasureOptions = {}): number {
|
|
427
|
+
if (options.sum === "float32" && options.rounding !== "pixel") return float32Width(font, text, size, options);
|
|
428
|
+
const { glyphs, advances } = glyphAdvances(font, text, options);
|
|
429
|
+
if (options.rounding === "pixel") {
|
|
430
|
+
const scale = size / font.unitsPerEm;
|
|
431
|
+
let px = 0;
|
|
432
|
+
glyphs.forEach((glyph, i) => {
|
|
433
|
+
const base = font.advance(glyph);
|
|
434
|
+
// FreeType gives the advance in 26.6 fixed point, and Skia rounds it to whole px
|
|
435
|
+
px += Math.round(Math.round(base * scale * 64) / 64) + Math.round((advances[i]! - base) * scale);
|
|
436
|
+
});
|
|
437
|
+
return px;
|
|
438
|
+
}
|
|
439
|
+
let units = 0;
|
|
440
|
+
for (const advance of advances) units += advance;
|
|
441
|
+
return (units * size) / font.unitsPerEm;
|
|
442
|
+
}
|