@mark1russell7/typeset 0.1.2 → 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.
@@ -25,8 +25,22 @@ export interface PairLookup {
25
25
  readonly skip?: (glyph: number) => boolean;
26
26
  /** This function gives the value for a pair, or `undefined` when no subtable applies. */
27
27
  pair(first: number, second: number): PairValue | undefined;
28
+ /**
29
+ * This function gives the largest positive value of each glyph of a pair, over the pairs with two glyphs of `glyphs`.
30
+ * A value is at least 0. It uses the values of the instance, so it includes each variation delta.
31
+ */
32
+ largest(glyphs: ReadonlySet<number>): LargestKerning;
33
+ }
34
+
35
+ /** The largest positive kerning of the first glyph and of the second glyph of a pair, in font units. */
36
+ export interface LargestKerning {
37
+ readonly first: number;
38
+ readonly second: number;
28
39
  }
29
40
 
41
+ /** One PairPos subtable. */
42
+ type PairSubtable = Pick<PairLookup, "pair" | "largest">;
43
+
30
44
  /** The lookup flags of OpenType. */
31
45
  export const IGNORE_BASE = 0x2;
32
46
  export const IGNORE_LIGATURES = 0x4;
@@ -63,8 +77,8 @@ function pairValue(first: { value: number; delta: number }, second: { value: num
63
77
  return { first: first.value, second: second.value, firstDelta: first.delta, secondDelta: second.delta, skipSecond };
64
78
  }
65
79
 
66
- /** This function makes the pair function of one PairPos subtable. */
67
- function pairSubtable(table: Reader, store: VariationStore | undefined): (first: number, second: number) => PairValue | undefined {
80
+ /** This function makes the functions of one PairPos subtable. */
81
+ function pairSubtable(table: Reader, store: VariationStore | undefined): PairSubtable {
68
82
  const format = table.u16(0);
69
83
  const coverage = table.part(table.u16(2));
70
84
  const format1 = table.u16(4);
@@ -73,11 +87,11 @@ function pairSubtable(table: Reader, store: VariationStore | undefined): (first:
73
87
  const size2 = valueSize(format2);
74
88
  const skipSecond = format2 !== 0;
75
89
  if (format === 1) {
76
- return (first, second) => {
90
+ const recordSize = 2 + size1 + size2;
91
+ const pair = (first: number, second: number): PairValue | undefined => {
77
92
  const index = coverageIndex(coverage, first);
78
93
  if (index < 0) return undefined;
79
94
  const set = table.part(table.u16(10 + index * 2));
80
- const recordSize = 2 + size1 + size2;
81
95
  let low = 0;
82
96
  let high = set.u16(0) - 1;
83
97
  while (low <= high) {
@@ -90,13 +104,30 @@ function pairSubtable(table: Reader, store: VariationStore | undefined): (first:
90
104
  }
91
105
  return undefined;
92
106
  };
107
+ const largest = (glyphs: ReadonlySet<number>): LargestKerning => {
108
+ let first = 0;
109
+ let second = 0;
110
+ for (const glyph of glyphs) {
111
+ const index = coverageIndex(coverage, glyph);
112
+ if (index < 0) continue;
113
+ const set = table.part(table.u16(10 + index * 2));
114
+ for (let record = 0; record < set.u16(0); record++) {
115
+ const at = 2 + record * recordSize;
116
+ if (!glyphs.has(set.u16(at))) continue;
117
+ first = Math.max(first, xAdvance(set, at + 2, format1, set, store).value);
118
+ second = Math.max(second, xAdvance(set, at + 2 + size1, format2, set, store).value);
119
+ }
120
+ }
121
+ return { first, second };
122
+ };
123
+ return { pair, largest };
93
124
  }
94
125
  if (format === 2) {
95
126
  const classes1 = table.part(table.u16(8));
96
127
  const classes2 = table.part(table.u16(10));
97
128
  const count1 = table.u16(12);
98
129
  const count2 = table.u16(14);
99
- return (first, second) => {
130
+ const pair = (first: number, second: number): PairValue | undefined => {
100
131
  if (coverageIndex(coverage, first) < 0) return undefined;
101
132
  const class1 = classOf(classes1, first);
102
133
  const class2 = classOf(classes2, second);
@@ -104,6 +135,26 @@ function pairSubtable(table: Reader, store: VariationStore | undefined): (first:
104
135
  const at = 16 + (class1 * count2 + class2) * (size1 + size2);
105
136
  return pairValue(xAdvance(table, at, format1, table, store), xAdvance(table, at + size1, format2, table, store), skipSecond);
106
137
  };
138
+ const largest = (glyphs: ReadonlySet<number>): LargestKerning => {
139
+ const firsts = new Set<number>();
140
+ const seconds = new Set<number>();
141
+ for (const glyph of glyphs) {
142
+ if (coverageIndex(coverage, glyph) >= 0) firsts.add(classOf(classes1, glyph));
143
+ seconds.add(classOf(classes2, glyph));
144
+ }
145
+ let first = 0;
146
+ let second = 0;
147
+ for (const class1 of firsts) {
148
+ for (const class2 of seconds) {
149
+ if (class1 >= count1 || class2 >= count2) continue;
150
+ const at = 16 + (class1 * count2 + class2) * (size1 + size2);
151
+ first = Math.max(first, xAdvance(table, at, format1, table, store).value);
152
+ second = Math.max(second, xAdvance(table, at + size1, format2, table, store).value);
153
+ }
154
+ }
155
+ return { first, second };
156
+ };
157
+ return { pair, largest };
107
158
  }
108
159
  throw new Error(`the PairPos format ${format} is unknown`);
109
160
  }
@@ -150,7 +201,7 @@ export function readGposKerning(gpos: Reader, script: string, store?: VariationS
150
201
  }
151
202
  return [...indices].sort((a, b) => a - b).flatMap((index) => {
152
203
  const lookup = lookups.part(lookups.u16(2 + index * 2));
153
- const subtables: ((first: number, second: number) => PairValue | undefined)[] = [];
204
+ const subtables: PairSubtable[] = [];
154
205
  const type = lookup.u16(0);
155
206
  for (let s = 0; s < lookup.u16(4); s++) {
156
207
  let subtable = lookup.part(lookup.u16(6 + s * 2));
@@ -176,11 +227,16 @@ export function readGposKerning(gpos: Reader, script: string, store?: VariationS
176
227
  ...(skip === undefined ? {} : { skip }),
177
228
  pair(first: number, second: number): PairValue | undefined {
178
229
  for (const subtable of subtables) {
179
- const value = subtable(first, second);
230
+ const value = subtable.pair(first, second);
180
231
  if (value !== undefined) return value;
181
232
  }
182
233
  return undefined;
183
234
  },
235
+ // A pair gets the value of one subtable, so the largest value of a lookup is the largest value of its subtables
236
+ largest(glyphs: ReadonlySet<number>): LargestKerning {
237
+ const values = subtables.map((subtable) => subtable.largest(glyphs));
238
+ return { first: Math.max(...values.map((value) => value.first)), second: Math.max(...values.map((value) => value.second)) };
239
+ },
184
240
  }];
185
241
  });
186
242
  }
@@ -213,5 +269,10 @@ export function readKernTable(kern: Reader): PairLookup[] {
213
269
  const value = pairs.get(first * 0x10000 + second);
214
270
  return value === undefined ? undefined : { first: value, second: 0, skipSecond: false };
215
271
  },
272
+ largest(glyphs: ReadonlySet<number>): LargestKerning {
273
+ let first = 0;
274
+ for (const [key, value] of pairs) if (glyphs.has(Math.floor(key / 0x10000)) && glyphs.has(key % 0x10000)) first = Math.max(first, value);
275
+ return { first, second: 0 };
276
+ },
216
277
  }];
217
278
  }
@@ -1,6 +1,6 @@
1
1
  import type { Reader } from "./binary.ts";
2
2
  import { IGNORE_BASE, IGNORE_LIGATURES, IGNORE_MARKS, scriptTags } from "./kerning.ts";
3
- import { classOf, coverageIndex } from "./tables.ts";
3
+ import { classOf, coverageGlyphs, coverageIndex } from "./tables.ts";
4
4
 
5
5
  /**
6
6
  * The GSUB features that the default model applies: the features that HarfBuzz and Chromium apply to horizontal text
@@ -66,6 +66,16 @@ export interface GlyphInfo {
66
66
  readonly codePoint: number;
67
67
  }
68
68
 
69
+ /** One substitution that a lookup can do, with no context. */
70
+ export interface SubstitutionRule {
71
+ /** The glyph that the rule replaces. The first glyph of a ligature is this glyph. */
72
+ readonly from: number;
73
+ /** The glyphs that replace it. A multiple substitution can give no glyph. */
74
+ readonly to: readonly number[];
75
+ /** The other glyphs of a ligature, in order. Another rule has no such glyphs. */
76
+ readonly components?: readonly number[];
77
+ }
78
+
69
79
  /** A lookup of GSUB for one script and one set of features. */
70
80
  export interface SubstitutionLookup {
71
81
  /** The index of the lookup in the lookup list of GSUB. */
@@ -93,6 +103,12 @@ export interface SubstitutionLookup {
93
103
  * `hb_ot_layout_lookup_would_substitute` with zero context tells it. A context rule with backtrack or lookahead does not apply.
94
104
  */
95
105
  wouldApply(glyphs: readonly number[]): boolean;
106
+ /**
107
+ * This function gives the rules of the lookup, and the rules of each lookup that it nests in a context rule. Each lookup
108
+ * has one list, and the list of this lookup is the first. The lists do not tell a context, so each nested rule counts.
109
+ * An alternate substitution gives one rule for each alternate.
110
+ */
111
+ rules(): readonly (readonly SubstitutionRule[])[];
96
112
  }
97
113
 
98
114
  /** One lookup of the lookup list, with the subtables of an extension lookup resolved. */
@@ -113,6 +129,71 @@ function records(table: Reader, at: number, count: number): SequenceRecord[] {
113
129
  return Array.from({ length: count }, (_, k) => ({ sequenceIndex: table.u16(at + k * 4), lookupIndex: table.u16(at + k * 4 + 2) }));
114
130
  }
115
131
 
132
+ /** This function gives each rule table of the rule sets at `at` of a context subtable of format 1 or 2. An offset 0 is an empty set. */
133
+ function ruleTables(t: Reader, at: number): Reader[] {
134
+ const tables: Reader[] = [];
135
+ for (let s = 0; s < t.u16(at); s++) {
136
+ const offset = t.u16(at + 2 + s * 2);
137
+ if (offset === 0) continue;
138
+ const set = t.part(offset);
139
+ for (let r = 0; r < set.u16(0); r++) tables.push(set.part(set.u16(2 + r * 2)));
140
+ }
141
+ return tables;
142
+ }
143
+
144
+ /** This function gives the records of each rule of a context subtable (lookup type 5). */
145
+ function contextRecords(t: Reader): SequenceRecord[] {
146
+ const format = t.u16(0);
147
+ if (format === 3) return records(t, 6 + t.u16(2) * 2, t.u16(4));
148
+ return ruleTables(t, format === 2 ? 6 : 4).flatMap((rule) => records(rule, 4 + (rule.u16(0) - 1) * 2, rule.u16(2)));
149
+ }
150
+
151
+ /** This function gives the records of each rule of a chained context subtable (lookup type 6). */
152
+ function chainRecords(t: Reader): SequenceRecord[] {
153
+ const format = t.u16(0);
154
+ if (format === 3) {
155
+ const inputAt = 4 + t.u16(2) * 2;
156
+ const aheadAt = inputAt + 2 + t.u16(inputAt) * 2;
157
+ const recordsAt = aheadAt + 2 + t.u16(aheadAt) * 2;
158
+ return records(t, recordsAt + 2, t.u16(recordsAt));
159
+ }
160
+ return ruleTables(t, format === 2 ? 10 : 4).flatMap((rule) => {
161
+ const inputAt = 2 + rule.u16(0) * 2;
162
+ const aheadAt = inputAt + 2 + Math.max(0, rule.u16(inputAt) - 1) * 2;
163
+ const recordsAt = aheadAt + 2 + rule.u16(aheadAt) * 2;
164
+ return records(rule, recordsAt + 2, rule.u16(recordsAt));
165
+ });
166
+ }
167
+
168
+ /** This function adds the rules of one subtable of a lookup type to `rules`, and each nested lookup to `nested`. */
169
+ function readRules(type: number, t: Reader, rules: SubstitutionRule[], nested: number[]): void {
170
+ if (type === 5 || type === 6) {
171
+ for (const record of type === 5 ? contextRecords(t) : chainRecords(t)) nested.push(record.lookupIndex);
172
+ return;
173
+ }
174
+ if (type < 1 || type > 8 || type === 7) return;
175
+ const format = t.u16(0);
176
+ for (const [from, index] of coverageGlyphs(t.part(t.u16(2)))) {
177
+ if (type === 1) rules.push({ from, to: [format === 1 ? (from + t.i16(4)) & 0xffff : t.u16(6 + index * 2)] });
178
+ else if (type === 2) {
179
+ const sequence = t.part(t.u16(6 + index * 2));
180
+ rules.push({ from, to: Array.from({ length: sequence.u16(0) }, (_, k) => sequence.u16(2 + k * 2)) });
181
+ } else if (type === 3) {
182
+ const set = t.part(t.u16(6 + index * 2));
183
+ for (let k = 0; k < set.u16(0); k++) rules.push({ from, to: [set.u16(2 + k * 2)] });
184
+ } else if (type === 4) {
185
+ const set = t.part(t.u16(6 + index * 2));
186
+ for (let l = 0; l < set.u16(0); l++) {
187
+ const ligature = set.part(set.u16(2 + l * 2));
188
+ rules.push({ from, to: [ligature.u16(0)], components: Array.from({ length: ligature.u16(2) - 1 }, (_, k) => ligature.u16(4 + k * 2)) });
189
+ }
190
+ } else {
191
+ const aheadAt = 6 + t.u16(4) * 2;
192
+ rules.push({ from, to: [t.u16(aheadAt + 2 + t.u16(aheadAt) * 2 + 2 + index * 2)] });
193
+ }
194
+ }
195
+ }
196
+
116
197
  /** The lookups of one GSUB table. It reads each lookup at its first use. */
117
198
  class Gsub {
118
199
  private readonly lookups: Reader;
@@ -392,6 +473,24 @@ class Gsub {
392
473
  });
393
474
  }
394
475
 
476
+ /** This function gives the rules of a lookup and of each lookup that it nests, as `rules` of `SubstitutionLookup` tells. */
477
+ rulesOf(index: number): SubstitutionRule[][] {
478
+ const lists: SubstitutionRule[][] = [];
479
+ const seen = new Set<number>();
480
+ const visit = (at: number): void => {
481
+ const lookup = this.lookup(at);
482
+ if (seen.has(at) || lookup === undefined) return;
483
+ seen.add(at);
484
+ const rules: SubstitutionRule[] = [];
485
+ const nested: number[] = [];
486
+ for (const subtable of lookup.subtables) readRules(lookup.type, subtable, rules, nested);
487
+ lists.push(rules);
488
+ nested.forEach(visit);
489
+ };
490
+ visit(index);
491
+ return lists;
492
+ }
493
+
395
494
  /** This function applies one subtable at `i`. It gives the next position, or -1 when the subtable does not match. */
396
495
  private subtable(lookup: Lookup, t: Reader, glyphs: number[], i: number, depth: number): number {
397
496
  const glyph = glyphs[i]!;
@@ -580,6 +679,7 @@ export function readSubstitutions(gsub: Reader, script: string, features: readon
580
679
  apply: (glyphs: number[], masks?: number[], mask?: number) => table.applyAll(index, glyphs, masks, mask),
581
680
  applyInfos: (glyphs: number[], infos: GlyphInfo[], mask: number, perSyllable: boolean) => table.applyRun(index, glyphs, infos, mask, perSyllable),
582
681
  wouldApply: (glyphs: readonly number[]) => table.wouldApply(index, glyphs),
682
+ rules: () => table.rulesOf(index),
583
683
  });
584
684
  }
585
685
  return result;
@@ -130,6 +130,28 @@ export function coverageIndex(table: Reader, glyph: number): number {
130
130
  throw new Error(`the Coverage format ${format} is unknown`);
131
131
  }
132
132
 
133
+ /** This function gives each glyph of a Coverage table with its coverage index, in the order of the table. */
134
+ export function coverageGlyphs(table: Reader): (readonly [glyph: number, index: number])[] {
135
+ const format = table.u16(0);
136
+ const count = table.u16(2);
137
+ const glyphs: (readonly [number, number])[] = [];
138
+ if (format === 1) {
139
+ for (let k = 0; k < count; k++) glyphs.push([table.u16(4 + k * 2), k]);
140
+ return glyphs;
141
+ }
142
+ if (format === 2) {
143
+ for (let k = 0; k < count; k++) {
144
+ const at = 4 + k * 6;
145
+ const start = table.u16(at);
146
+ const end = table.u16(at + 2);
147
+ const index = table.u16(at + 4);
148
+ for (let glyph = start; glyph <= end; glyph++) glyphs.push([glyph, index + glyph - start]);
149
+ }
150
+ return glyphs;
151
+ }
152
+ throw new Error(`the Coverage format ${format} is unknown`);
153
+ }
154
+
133
155
  /** The GDEF glyph classes: 1 base, 2 ligature, 3 mark, 4 component. */
134
156
  export function readGlyphClasses(gdef: Reader | undefined): ((glyph: number) => number) {
135
157
  if (gdef === undefined) return () => 0;
package/src/fonts.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * the width of a text as the text engine of a browser gives it. The host gives the bytes of the font files.
4
4
  */
5
5
  export { measureWidth, readFont, type Font, type MeasureOptions, type ReadOptions } from "./fonts/font.ts";
6
+ export { widthBound, type WidthBound } from "./fonts/bound.ts";
6
7
  export { engines, isEngineName, type EngineName, type EngineRules } from "./fonts/engines.ts";
7
8
  export type { Decompress } from "./fonts/sfnt.ts";
8
9
  export type { Axis, Instance } from "./fonts/variations.ts";