@mark1russell7/typeset 0.1.1 → 0.1.3
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 +228 -0
- package/build/chunks/font.d.ts.map +1 -0
- package/build/chunks/font.js +3779 -0
- package/build/chunks/font.js.map +1 -0
- package/build/chunks/fonts.d.ts +31 -0
- package/build/chunks/fonts.d.ts.map +1 -0
- package/build/chunks/fonts.js +150 -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 +18 -4
- package/src/fonts/bidi.ts +125 -0
- package/src/fonts/binary.ts +41 -0
- package/src/fonts/bound.ts +167 -0
- package/src/fonts/engines.ts +38 -0
- package/src/fonts/font.ts +458 -0
- package/src/fonts/gvar.ts +220 -0
- package/src/fonts/index.ts +11 -0
- package/src/fonts/indic.ts +626 -0
- package/src/fonts/joining.ts +141 -0
- package/src/fonts/kerning.ts +278 -0
- package/src/fonts/sfnt.ts +102 -0
- package/src/fonts/substitution.ts +686 -0
- package/src/fonts/tables.ts +162 -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 +9 -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,458 @@
|
|
|
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 choices that a set of options makes for the GSUB pass and for the kerning. */
|
|
297
|
+
export interface Shaping {
|
|
298
|
+
readonly script: string;
|
|
299
|
+
/** The features of the option `features`, or `defaultFeatures`. */
|
|
300
|
+
readonly features: readonly string[];
|
|
301
|
+
readonly shaper: "indic" | "joining" | "plain";
|
|
302
|
+
readonly kerning: readonly PairLookup[];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** This function gives the choices of a set of options: the script, the features, the shaper and the kerning lookups. */
|
|
306
|
+
export function shapingOf(font: Font, options: MeasureOptions): Shaping {
|
|
307
|
+
const script = options.script ?? "DFLT";
|
|
308
|
+
const features = options.features ?? defaultFeatures;
|
|
309
|
+
const kerning = options.kerning === false ? [] : font.kerning(script);
|
|
310
|
+
if (options.indic !== false && indicScripts.includes(script)) return { script, features, shaper: "indic", kerning };
|
|
311
|
+
return { script, features, shaper: options.joining !== false && joiningScripts.includes(script) ? "joining" : "plain", kerning };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** The GSUB pass of one set of options: a function that gives the glyphs of one word, and the kerning lookups. */
|
|
315
|
+
function shaperOf(font: Font, options: MeasureOptions): { substitute: (word: string) => number[]; kerning: readonly PairLookup[] } {
|
|
316
|
+
const { script, features, shaper, kerning } = shapingOf(font, options);
|
|
317
|
+
if (shaper === "indic") return { substitute: indicShaper(font, script, features), kerning };
|
|
318
|
+
const joins = shaper === "joining";
|
|
319
|
+
const lookups = joins ? [] : font.substitutions(script, features);
|
|
320
|
+
const stages = joins ? joiningStages(features, features.includes("rclt") && font.substitutions(script, ["rclt"]).length > 0).map((stage) => ({ stage, lookups: font.substitutions(script, stage.features) })) : [];
|
|
321
|
+
const substitute = (word: string): number[] => {
|
|
322
|
+
const codePoints = [...word].map((character) => character.codePointAt(0)!);
|
|
323
|
+
const wordGlyphs = codePoints.map((codePoint) => font.glyph(codePoint));
|
|
324
|
+
for (const lookup of lookups) lookup.apply(wordGlyphs);
|
|
325
|
+
if (joins) {
|
|
326
|
+
// Each glyph has the bit of its joining form, and the bit 7 for the features of all the glyphs
|
|
327
|
+
const masks = joiningForms(codePoints).map((form) => 0x80 | (form === undefined ? 0 : 1 << joiningFeatures.indexOf(form)));
|
|
328
|
+
for (const { stage, lookups: stageLookups } of stages) {
|
|
329
|
+
const mask = stage.form === undefined ? 0x80 : 1 << joiningFeatures.indexOf(stage.form);
|
|
330
|
+
for (const lookup of stageLookups) lookup.apply(wordGlyphs, masks, mask);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return wordGlyphs;
|
|
334
|
+
};
|
|
335
|
+
return { substitute, kerning };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* This function gives the glyphs of a text and their advances in font units. With the shaping `dom`, it applies the GSUB
|
|
340
|
+
* substitutions to each word of `shapingWords`, and then the pair kerning to all the text. With the shaping `canvas`, it
|
|
341
|
+
* applies the substitutions and the kerning to each word of `canvasWords` alone. A script of `joiningScripts` also gets
|
|
342
|
+
* the joining forms of each word, in the stages of `joiningStages`. A script of `indicScripts` uses `indicShaper`.
|
|
343
|
+
*/
|
|
344
|
+
export function glyphAdvances(font: Font, text: string, options: MeasureOptions = {}): { glyphs: number[]; advances: number[] } {
|
|
345
|
+
const { substitute, kerning } = shaperOf(font, options);
|
|
346
|
+
const canvas = options.shaping === "canvas";
|
|
347
|
+
const glyphs: number[] = [];
|
|
348
|
+
const advances: number[] = [];
|
|
349
|
+
for (const word of canvas ? canvasWords(text) : shapingWords(text)) {
|
|
350
|
+
const wordGlyphs = substitute(word);
|
|
351
|
+
const wordAdvances = wordGlyphs.map((glyph) => font.advance(glyph));
|
|
352
|
+
if (canvas) kern(font, kerning, wordGlyphs, wordAdvances);
|
|
353
|
+
glyphs.push(...wordGlyphs);
|
|
354
|
+
advances.push(...wordAdvances);
|
|
355
|
+
}
|
|
356
|
+
if (!canvas) kern(font, kerning, glyphs, advances);
|
|
357
|
+
return { glyphs, advances };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 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.
|
|
362
|
+
* The field `multiplier` is the factor of `em_mult` in `hb-font.hh`.
|
|
363
|
+
*/
|
|
364
|
+
interface PositionScale {
|
|
365
|
+
readonly xScale: number;
|
|
366
|
+
readonly multiplier: number;
|
|
367
|
+
readonly unitsPerEm: number;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** This function rounds a value to the nearest whole number, and a half goes away from 0, as `roundf` does. */
|
|
371
|
+
function roundHalfAway(value: number): number {
|
|
372
|
+
return Math.sign(value) * Math.floor(Math.abs(value) + 0.5);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* This function gives a kerning value in 1/65536 px, as HarfBuzz scales it. HarfBuzz scales the value of the ValueRecord
|
|
377
|
+
* with `em_mult`, and the delta of the VariationIndex table in single precision with `em_scalef`.
|
|
378
|
+
*/
|
|
379
|
+
function kerningPosition(value: number, delta: number, scale: PositionScale): number {
|
|
380
|
+
const base = Math.round(value - delta);
|
|
381
|
+
const scaledDelta = delta === 0 ? 0 : roundHalfAway(Math.fround(Math.fround(Math.fround(delta) * scale.xScale) / scale.unitsPerEm));
|
|
382
|
+
return Math.floor((base * scale.multiplier + 32768) / 65536) + scaledDelta;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* This function adds the values of the pair kerning lookups `lookups` to the advances `advances` of the glyphs `glyphs`.
|
|
387
|
+
* When `positions` is given, it also adds each value in 1/65536 px, as HarfBuzz scales it at the scale `scale`.
|
|
388
|
+
*/
|
|
389
|
+
function kern(font: Font, lookups: readonly PairLookup[], glyphs: readonly number[], advances: number[], positions?: number[], scale?: PositionScale): void {
|
|
390
|
+
for (const lookup of lookups) {
|
|
391
|
+
const next = (from: number): number => {
|
|
392
|
+
for (let j = from; j < glyphs.length; j++) if (!(lookup.skip?.(glyphs[j]!) ?? skips(lookup.flags, font.glyphClass(glyphs[j]!)))) return j;
|
|
393
|
+
return -1;
|
|
394
|
+
};
|
|
395
|
+
let i = next(0);
|
|
396
|
+
while (i >= 0) {
|
|
397
|
+
const j = next(i + 1);
|
|
398
|
+
if (j < 0) break;
|
|
399
|
+
const value = lookup.pair(glyphs[i]!, glyphs[j]!);
|
|
400
|
+
if (value === undefined) {
|
|
401
|
+
i = j;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
advances[i]! += value.first;
|
|
405
|
+
advances[j]! += value.second;
|
|
406
|
+
if (positions !== undefined && scale !== undefined) {
|
|
407
|
+
positions[i]! += kerningPosition(value.first, value.firstDelta ?? 0, scale);
|
|
408
|
+
positions[j]! += kerningPosition(value.second, value.secondDelta ?? 0, scale);
|
|
409
|
+
}
|
|
410
|
+
i = value.skipSecond ? next(j + 1) : j;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* This function gives the width of a text with the sum `float32`, as Blink adds the widths of a canvas. Each advance in
|
|
417
|
+
* single precision is cut to 1/65536 px, and HarfBuzz scales each kerning value. The sum of a segment of `canvasSegments`
|
|
418
|
+
* is exact, and the sum of the segments is in single precision.
|
|
419
|
+
*/
|
|
420
|
+
function float32Width(font: Font, text: string, size: number, options: MeasureOptions): number {
|
|
421
|
+
const { substitute, kerning } = shaperOf(font, options);
|
|
422
|
+
const xScale = Math.trunc(Math.fround(size) * 65536);
|
|
423
|
+
const scale: PositionScale = { xScale, multiplier: Math.floor((xScale * 65536) / font.unitsPerEm), unitsPerEm: font.unitsPerEm };
|
|
424
|
+
const pxPerUnit = size / font.unitsPerEm;
|
|
425
|
+
let total = 0;
|
|
426
|
+
for (const segment of canvasSegments(text)) {
|
|
427
|
+
const glyphs = substitute(segment);
|
|
428
|
+
// Skia gives each advance in single precision, and Blink cuts it to the 16.16 fixed point of HarfBuzz
|
|
429
|
+
const positions = glyphs.map((glyph) => Math.trunc(Math.fround(font.advance(glyph) * pxPerUnit) * 65536));
|
|
430
|
+
kern(font, kerning, glyphs, glyphs.map((glyph) => font.advance(glyph)), positions, scale);
|
|
431
|
+
let units = 0;
|
|
432
|
+
for (const position of positions) units += position;
|
|
433
|
+
total = Math.fround(total + Math.fround(units / 65536));
|
|
434
|
+
}
|
|
435
|
+
return total;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* 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
|
|
440
|
+
* GSUB substitutions and the pair kerning.
|
|
441
|
+
*/
|
|
442
|
+
export function measureWidth(font: Font, text: string, size: number, options: MeasureOptions = {}): number {
|
|
443
|
+
if (options.sum === "float32" && options.rounding !== "pixel") return float32Width(font, text, size, options);
|
|
444
|
+
const { glyphs, advances } = glyphAdvances(font, text, options);
|
|
445
|
+
if (options.rounding === "pixel") {
|
|
446
|
+
const scale = size / font.unitsPerEm;
|
|
447
|
+
let px = 0;
|
|
448
|
+
glyphs.forEach((glyph, i) => {
|
|
449
|
+
const base = font.advance(glyph);
|
|
450
|
+
// FreeType gives the advance in 26.6 fixed point, and Skia rounds it to whole px
|
|
451
|
+
px += Math.round(Math.round(base * scale * 64) / 64) + Math.round((advances[i]! - base) * scale);
|
|
452
|
+
});
|
|
453
|
+
return px;
|
|
454
|
+
}
|
|
455
|
+
let units = 0;
|
|
456
|
+
for (const advance of advances) units += advance;
|
|
457
|
+
return (units * size) / font.unitsPerEm;
|
|
458
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { Reader } from "./binary.ts";
|
|
2
|
+
import { axisScalar } from "./variations.ts";
|
|
3
|
+
|
|
4
|
+
/** The composite flags of `glyf` that change the length of a component record. */
|
|
5
|
+
const ARG_1_AND_2_ARE_WORDS = 0x1;
|
|
6
|
+
const WE_HAVE_A_SCALE = 0x8;
|
|
7
|
+
const MORE_COMPONENTS = 0x20;
|
|
8
|
+
const WE_HAVE_AN_X_AND_Y_SCALE = 0x40;
|
|
9
|
+
const WE_HAVE_A_TWO_BY_TWO = 0x80;
|
|
10
|
+
/** The composite flag that gives the metrics of the component to the composite glyph. */
|
|
11
|
+
const USE_MY_METRICS = 0x200;
|
|
12
|
+
|
|
13
|
+
/** The points of the glyphs of a `glyf` table. */
|
|
14
|
+
export interface GlyphPoints {
|
|
15
|
+
/** The count of points of each glyph, as `gvar` counts them. The four phantom points are not in the count. */
|
|
16
|
+
readonly counts: Uint32Array;
|
|
17
|
+
/** The component with the flag USE_MY_METRICS of each composite glyph, or -1. */
|
|
18
|
+
readonly metrics: Int32Array;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* This function reads the composite records that start at `at`. It gives their length in bytes and the count of
|
|
23
|
+
* components. It also gives the first component with the flag USE_MY_METRICS, or -1.
|
|
24
|
+
*/
|
|
25
|
+
function components(glyph: Reader, at: number): { count: number; length: number; metrics: number } {
|
|
26
|
+
let count = 0;
|
|
27
|
+
let offset = at;
|
|
28
|
+
let metrics = -1;
|
|
29
|
+
for (;;) {
|
|
30
|
+
const flags = glyph.u16(offset);
|
|
31
|
+
if ((flags & USE_MY_METRICS) !== 0 && metrics < 0) metrics = glyph.u16(offset + 2);
|
|
32
|
+
offset += 4 + ((flags & ARG_1_AND_2_ARE_WORDS) !== 0 ? 4 : 2);
|
|
33
|
+
if ((flags & WE_HAVE_A_SCALE) !== 0) offset += 2;
|
|
34
|
+
else if ((flags & WE_HAVE_AN_X_AND_Y_SCALE) !== 0) offset += 4;
|
|
35
|
+
else if ((flags & WE_HAVE_A_TWO_BY_TWO) !== 0) offset += 8;
|
|
36
|
+
count++;
|
|
37
|
+
if ((flags & MORE_COMPONENTS) === 0) return { count, length: offset - at, metrics };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* This function gives the points of each glyph. A glyph of contours has the points of its contours. A composite glyph
|
|
43
|
+
* has one point for each component.
|
|
44
|
+
*/
|
|
45
|
+
export function readPointCounts(glyf: Reader, loca: Reader, indexToLocFormat: number, numGlyphs: number): GlyphPoints {
|
|
46
|
+
const counts = new Uint32Array(numGlyphs);
|
|
47
|
+
const metrics = new Int32Array(numGlyphs).fill(-1);
|
|
48
|
+
const offsetOf = (glyph: number) => (indexToLocFormat === 0 ? loca.u16(glyph * 2) * 2 : loca.u32(glyph * 4));
|
|
49
|
+
for (let glyph = 0; glyph < numGlyphs; glyph++) {
|
|
50
|
+
const start = offsetOf(glyph);
|
|
51
|
+
if (offsetOf(glyph + 1) <= start) continue;
|
|
52
|
+
const contours = glyf.i16(start);
|
|
53
|
+
if (contours > 0) counts[glyph] = glyf.u16(start + 10 + (contours - 1) * 2) + 1;
|
|
54
|
+
else if (contours < 0) {
|
|
55
|
+
const found = components(glyf, start + 10);
|
|
56
|
+
counts[glyph] = found.count;
|
|
57
|
+
metrics[glyph] = found.metrics;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { counts, metrics };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** This function reads a 255UInt16 of WOFF2. It gives the value and the next offset. */
|
|
64
|
+
function read255(stream: Reader, at: number): [number, number] {
|
|
65
|
+
const code = stream.u8(at);
|
|
66
|
+
if (code === 253) return [stream.u16(at + 1), at + 3];
|
|
67
|
+
if (code === 254) return [stream.u8(at + 1) + 506, at + 2];
|
|
68
|
+
if (code === 255) return [stream.u8(at + 1) + 253, at + 2];
|
|
69
|
+
return [code, at + 1];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** This function gives the points of each glyph from the transformed `glyf` table of WOFF2. */
|
|
73
|
+
export function readTransformedPointCounts(glyf: Reader, numGlyphs: number): GlyphPoints {
|
|
74
|
+
const counts = new Uint32Array(numGlyphs);
|
|
75
|
+
const metrics = new Int32Array(numGlyphs).fill(-1);
|
|
76
|
+
const sizes = Array.from({ length: 7 }, (_, i) => glyf.u32(8 + i * 4));
|
|
77
|
+
const contourStream = glyf.part(36, sizes[0]);
|
|
78
|
+
const pointStream = glyf.part(36 + sizes[0]!, sizes[1]);
|
|
79
|
+
// The composite stream comes after the streams of the contours, the points, the flags and the glyphs
|
|
80
|
+
const compositeStream = glyf.part(36 + sizes[0]! + sizes[1]! + sizes[2]! + sizes[3]!, sizes[4]);
|
|
81
|
+
let pointAt = 0;
|
|
82
|
+
let compositeAt = 0;
|
|
83
|
+
const glyphs = Math.min(numGlyphs, glyf.u16(4));
|
|
84
|
+
for (let glyph = 0; glyph < glyphs; glyph++) {
|
|
85
|
+
const contours = contourStream.i16(glyph * 2);
|
|
86
|
+
if (contours > 0) {
|
|
87
|
+
let total = 0;
|
|
88
|
+
for (let c = 0; c < contours; c++) {
|
|
89
|
+
const [points, next] = read255(pointStream, pointAt);
|
|
90
|
+
total += points;
|
|
91
|
+
pointAt = next;
|
|
92
|
+
}
|
|
93
|
+
counts[glyph] = total;
|
|
94
|
+
} else if (contours < 0) {
|
|
95
|
+
const found = components(compositeStream, compositeAt);
|
|
96
|
+
counts[glyph] = found.count;
|
|
97
|
+
metrics[glyph] = found.metrics;
|
|
98
|
+
compositeAt += found.length;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { counts, metrics };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** This function reads packed point numbers. It gives the point numbers, or `undefined` for all the points, and the next offset. */
|
|
105
|
+
function readPoints(data: Reader, at: number): [number[] | undefined, number] {
|
|
106
|
+
let count = data.u8(at++);
|
|
107
|
+
if ((count & 0x80) !== 0) count = ((count & 0x7f) << 8) | data.u8(at++);
|
|
108
|
+
if (count === 0) return [undefined, at];
|
|
109
|
+
const points: number[] = [];
|
|
110
|
+
let last = 0;
|
|
111
|
+
while (points.length < count) {
|
|
112
|
+
const control = data.u8(at++);
|
|
113
|
+
const words = (control & 0x80) !== 0;
|
|
114
|
+
const run = (control & 0x7f) + 1;
|
|
115
|
+
for (let k = 0; k < run && points.length < count; k++) {
|
|
116
|
+
last += words ? data.u16(at) : data.u8(at);
|
|
117
|
+
at += words ? 2 : 1;
|
|
118
|
+
points.push(last);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return [points, at];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** This function reads `count` packed deltas. It gives the deltas and the next offset. */
|
|
125
|
+
function readDeltas(data: Reader, at: number, count: number): [number[], number] {
|
|
126
|
+
const deltas: number[] = [];
|
|
127
|
+
while (deltas.length < count) {
|
|
128
|
+
const control = data.u8(at++);
|
|
129
|
+
const run = (control & 0x3f) + 1;
|
|
130
|
+
const kind = control & 0xc0;
|
|
131
|
+
for (let k = 0; k < run && deltas.length < count; k++) {
|
|
132
|
+
if (kind === 0x80) deltas.push(0);
|
|
133
|
+
else if (kind === 0x40) {
|
|
134
|
+
deltas.push(data.i16(at));
|
|
135
|
+
at += 2;
|
|
136
|
+
} else if (kind === 0xc0) {
|
|
137
|
+
deltas.push(data.u32(at) | 0);
|
|
138
|
+
at += 4;
|
|
139
|
+
} else deltas.push((data.u8(at++) << 24) >> 24);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return [deltas, at];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* This function gives the advance delta of each glyph at the coordinates, from the phantom points of `gvar`. The delta
|
|
147
|
+
* is the x delta of the second phantom point minus the x delta of the first. With `round`, the difference is rounded to
|
|
148
|
+
* whole font units, and a half goes away from 0, as HarfBuzz rounds it.
|
|
149
|
+
*/
|
|
150
|
+
export function gvarAdvanceDeltas(gvar: Reader, pointCounts: Uint32Array, coordinates: readonly number[], deltas: "round" | "exact"): Float64Array {
|
|
151
|
+
const axisCount = gvar.u16(4);
|
|
152
|
+
const sharedCount = gvar.u16(6);
|
|
153
|
+
const sharedAt = gvar.u32(8);
|
|
154
|
+
const glyphCount = Math.min(gvar.u16(12), pointCounts.length);
|
|
155
|
+
const longOffsets = (gvar.u16(14) & 1) !== 0;
|
|
156
|
+
const dataAt = gvar.u32(16);
|
|
157
|
+
const offsetOf = (glyph: number) => (longOffsets ? gvar.u32(20 + glyph * 4) : gvar.u16(20 + glyph * 2) * 2);
|
|
158
|
+
const peakOf = (at: number) => Array.from({ length: axisCount }, (_, a) => gvar.i16(at + a * 2) / 16384);
|
|
159
|
+
const shared = Array.from({ length: sharedCount }, (_, t) => peakOf(sharedAt + t * axisCount * 2));
|
|
160
|
+
const result = new Float64Array(pointCounts.length);
|
|
161
|
+
for (let glyph = 0; glyph < glyphCount; glyph++) {
|
|
162
|
+
const start = offsetOf(glyph);
|
|
163
|
+
const end = offsetOf(glyph + 1);
|
|
164
|
+
if (end <= start) continue;
|
|
165
|
+
const data = gvar.part(dataAt + start, end - start);
|
|
166
|
+
const tupleCount = data.u16(0) & 0x0fff;
|
|
167
|
+
const sharedPoints = (data.u16(0) & 0x8000) !== 0;
|
|
168
|
+
let serialized = data.u16(2);
|
|
169
|
+
let header = 4;
|
|
170
|
+
const allPoints = pointCounts[glyph]! + 4;
|
|
171
|
+
let sharedList: number[] | undefined;
|
|
172
|
+
if (sharedPoints) [sharedList, serialized] = readPoints(data, serialized);
|
|
173
|
+
const first = pointCounts[glyph]!;
|
|
174
|
+
let left = 0;
|
|
175
|
+
let right = 0;
|
|
176
|
+
for (let t = 0; t < tupleCount; t++) {
|
|
177
|
+
const size = data.u16(header);
|
|
178
|
+
const index = data.u16(header + 2);
|
|
179
|
+
header += 4;
|
|
180
|
+
let peak: number[];
|
|
181
|
+
if ((index & 0x8000) !== 0) {
|
|
182
|
+
peak = Array.from({ length: axisCount }, (_, a) => data.i16(header + a * 2) / 16384);
|
|
183
|
+
header += axisCount * 2;
|
|
184
|
+
} else peak = shared[index & 0x0fff] ?? [];
|
|
185
|
+
let startTuple: number[] | undefined;
|
|
186
|
+
let endTuple: number[] | undefined;
|
|
187
|
+
if ((index & 0x4000) !== 0) {
|
|
188
|
+
startTuple = Array.from({ length: axisCount }, (_, a) => data.i16(header + a * 2) / 16384);
|
|
189
|
+
endTuple = Array.from({ length: axisCount }, (_, a) => data.i16(header + axisCount * 2 + a * 2) / 16384);
|
|
190
|
+
header += axisCount * 4;
|
|
191
|
+
}
|
|
192
|
+
let scalar = 1;
|
|
193
|
+
for (let a = 0; a < axisCount && scalar !== 0; a++) {
|
|
194
|
+
const p = peak[a] ?? 0;
|
|
195
|
+
const s = startTuple?.[a] ?? Math.min(p, 0);
|
|
196
|
+
const e = endTuple?.[a] ?? Math.max(p, 0);
|
|
197
|
+
scalar *= axisScalar(s, p, e, coordinates[a] ?? 0);
|
|
198
|
+
}
|
|
199
|
+
const tupleEnd = serialized + size;
|
|
200
|
+
if (scalar !== 0) {
|
|
201
|
+
let at = serialized;
|
|
202
|
+
let points = sharedList;
|
|
203
|
+
if ((index & 0x2000) !== 0) [points, at] = readPoints(data, at);
|
|
204
|
+
const count = points === undefined ? allPoints : points.length;
|
|
205
|
+
const [xs] = readDeltas(data, at, count);
|
|
206
|
+
const deltaOf = (point: number): number => {
|
|
207
|
+
if (points === undefined) return xs[point] ?? 0;
|
|
208
|
+
const k = points.indexOf(point);
|
|
209
|
+
return k < 0 ? 0 : xs[k]!;
|
|
210
|
+
};
|
|
211
|
+
left += scalar * deltaOf(first);
|
|
212
|
+
right += scalar * deltaOf(first + 1);
|
|
213
|
+
}
|
|
214
|
+
serialized = tupleEnd;
|
|
215
|
+
}
|
|
216
|
+
const delta = right - left;
|
|
217
|
+
result[glyph] = deltas === "round" ? Math.sign(delta) * Math.round(Math.abs(delta)) : delta;
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|