ntk 8.13.1 → 8.14.1
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/lib/app.js +24 -0
- package/lib/cliprect.js +13 -2
- package/lib/color.js +21 -0
- package/lib/fontconfig.js +273 -55
- package/lib/renderingcontext_2d.js +41 -16
- package/lib/text/anchors.js +81 -0
- package/lib/text/font.js +81 -3
- package/lib/text/fontmanager.js +97 -12
- package/lib/text/fontsource.js +25 -0
- package/lib/text/layout.js +67 -23
- package/lib/text/marks.js +212 -0
- package/lib/text/shape.js +17 -8
- package/package.json +1 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// The glyphs a face's mark positioning can move, read from the GPOS table's
|
|
2
|
+
// own bytes, so that its lookups need not be decoded until a run holds one.
|
|
3
|
+
//
|
|
4
|
+
// fontkit decodes a lookup whole the first time a run asks for its feature,
|
|
5
|
+
// and `mark` is the one that costs: Noto Sans' mark-to-base subtable holds an
|
|
6
|
+
// anchor for every base glyph in every mark class, and decoding it took 14 ms
|
|
7
|
+
// of each face's first shaping (21 ms in all), inside the first frame of
|
|
8
|
+
// every app whose sans-serif is Noto. A line of Latin UI text has no glyph
|
|
9
|
+
// any of those lookups acts on.
|
|
10
|
+
//
|
|
11
|
+
// So each `mark` and `mkmk` lookup is stood in for, in fontkit's cache of the
|
|
12
|
+
// face's lookups, by an empty one (`standInMarks`): a run applies the stand-in
|
|
13
|
+
// and decodes nothing, and fontkit is handed the caller's features as they
|
|
14
|
+
// are. A run that turns out to hold a glyph one of the real lookups acts at
|
|
15
|
+
// puts them back and is shaped again (`Font#_layout`).
|
|
16
|
+
//
|
|
17
|
+
// That is exact rather than a guess about scripts. Every GPOS lookup acts
|
|
18
|
+
// only at a glyph in its first coverage — the mark coverage of a mark
|
|
19
|
+
// attachment, the first glyph's of a pair, the first input's of a context —
|
|
20
|
+
// wherever it is reached from: its feature, another feature that names it,
|
|
21
|
+
// the feature table a variation swaps in, a context that nests it. A run
|
|
22
|
+
// holding no glyph from the coverage of any stood-in lookup is one the real
|
|
23
|
+
// lookups would have left as it is.
|
|
24
|
+
//
|
|
25
|
+
// An earlier cut left the two features out through fontkit's own feature
|
|
26
|
+
// overrides instead. That was exact too, and cost every shaping a pass over
|
|
27
|
+
// the overrides that deletes the tags from the plan: 1-3 us a word, 6% of a
|
|
28
|
+
// frame scrolling a long document into text it had not shaped.
|
|
29
|
+
//
|
|
30
|
+
// Null — shape as fontkit shapes — for a face that has nothing to stand in
|
|
31
|
+
// for, that fontkit shapes through AAT's morx rather than GPOS, or whose
|
|
32
|
+
// tables this cannot read to the end.
|
|
33
|
+
|
|
34
|
+
const TAGS = new Set(['mark', 'mkmk']);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {object} fk a fontkit font
|
|
38
|
+
* @returns {{ bits: Uint8Array, lookups: Set<number> }|null} the `mark` and
|
|
39
|
+
* `mkmk` lookups, and a bit per glyph id, set for every glyph one of them
|
|
40
|
+
* can act at; null where none can be stood in for
|
|
41
|
+
*/
|
|
42
|
+
export function markGlyphs(fk) {
|
|
43
|
+
try {
|
|
44
|
+
if (fk.directory?.tables?.morx) return null;
|
|
45
|
+
const gpos = table(fk, 'GPOS');
|
|
46
|
+
if (!gpos) return null;
|
|
47
|
+
const lookups = markLookups(gpos);
|
|
48
|
+
if (lookups.size === 0) return null;
|
|
49
|
+
const bits = new Uint8Array(8192);
|
|
50
|
+
const lookupList = gpos.offset(8, 0);
|
|
51
|
+
for (const index of lookups) {
|
|
52
|
+
if (!coverLookup(gpos, lookupList, index, bits)) return null;
|
|
53
|
+
}
|
|
54
|
+
return { bits, lookups };
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Stand an empty lookup in for each `mark` and `mkmk` lookup of a face, in
|
|
62
|
+
* fontkit's cache of its GPOS lookups (restructure's lazy array, which
|
|
63
|
+
* decodes an index the first time it is asked and keeps it in `items`).
|
|
64
|
+
* Applied, a stand-in has no subtables and moves nothing.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} fk a fontkit font, before its first layout
|
|
67
|
+
* @returns {{ bits: Uint8Array, restore: () => void }|null} `restore` puts the
|
|
68
|
+
* real lookups back, to be decoded by the next run that asks; null where
|
|
69
|
+
* there is nothing to stand in for, where fontkit's cache is not the shape
|
|
70
|
+
* this knows, or where a lookup was decoded already
|
|
71
|
+
*/
|
|
72
|
+
export function standInMarks(fk) {
|
|
73
|
+
const marks = markGlyphs(fk);
|
|
74
|
+
if (marks === null) return null;
|
|
75
|
+
let list;
|
|
76
|
+
try {
|
|
77
|
+
list = fk.GPOS?.lookupList;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (!list || typeof list.get !== 'function' || !Array.isArray(list.items)) return null;
|
|
82
|
+
for (const index of marks.lookups) if (list.items[index] != null) return null;
|
|
83
|
+
const standIn = { lookupType: 0, flags: {}, subTableCount: 0, subTables: [] };
|
|
84
|
+
for (const index of marks.lookups) list.items[index] = standIn;
|
|
85
|
+
return {
|
|
86
|
+
bits: marks.bits,
|
|
87
|
+
restore() {
|
|
88
|
+
for (const index of marks.lookups) {
|
|
89
|
+
if (list.items[index] === standIn) list.items[index] = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether `bits` (from `markGlyphs`) has glyph `id`. */
|
|
96
|
+
export function marksCover(bits, id) {
|
|
97
|
+
return (bits[id >> 3] & (1 << (id & 7))) !== 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A table's bytes, read big-endian at positions from its start. `offset(pos,
|
|
102
|
+
* base)` reads the 16-bit offset stored at `pos` and answers where it
|
|
103
|
+
* points, `base` being what the format measures it from.
|
|
104
|
+
*/
|
|
105
|
+
function table(fk, tag) {
|
|
106
|
+
const entry = fk.directory?.tables?.[tag];
|
|
107
|
+
if (!entry) return null;
|
|
108
|
+
const stream = fk._getTableStream(tag);
|
|
109
|
+
if (!stream) return null;
|
|
110
|
+
const buf = stream.buffer;
|
|
111
|
+
const start = stream.pos;
|
|
112
|
+
const end = Math.min(start + entry.length, buf.length);
|
|
113
|
+
const u16 = (pos) => {
|
|
114
|
+
const p = start + pos;
|
|
115
|
+
if (pos < 0 || p + 2 > end) throw new RangeError(`${tag} read past its end`);
|
|
116
|
+
return (buf[p] << 8) | buf[p + 1];
|
|
117
|
+
};
|
|
118
|
+
const u32 = (pos) => u16(pos) * 65536 + u16(pos + 2);
|
|
119
|
+
const offset = (pos, base) => base + u16(pos);
|
|
120
|
+
return { u16, u32, offset };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The lookup indices of every `mark` and `mkmk` feature record, in any
|
|
125
|
+
* script: a superset of what fontkit applies, which only widens what
|
|
126
|
+
* counts as a mark.
|
|
127
|
+
*/
|
|
128
|
+
function markLookups(t) {
|
|
129
|
+
const list = t.offset(6, 0);
|
|
130
|
+
const lookups = new Set();
|
|
131
|
+
for (let i = 0, n = t.u16(list); i < n; i++) {
|
|
132
|
+
const rec = list + 2 + i * 6;
|
|
133
|
+
const tag = String.fromCharCode(t.u16(rec) >> 8, t.u16(rec) & 255, t.u16(rec + 2) >> 8, t.u16(rec + 2) & 255);
|
|
134
|
+
if (!TAGS.has(tag)) continue;
|
|
135
|
+
const feature = t.offset(rec + 4, list);
|
|
136
|
+
for (let j = 0, m = t.u16(feature + 2); j < m; j++) lookups.add(t.u16(feature + 4 + j * 2));
|
|
137
|
+
}
|
|
138
|
+
return lookups;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Adds the first coverage of each of a lookup's subtables; false for one it cannot read. */
|
|
142
|
+
function coverLookup(t, lookupList, index, bits) {
|
|
143
|
+
if (index >= t.u16(lookupList)) return false;
|
|
144
|
+
const lookup = t.offset(lookupList + 2 + index * 2, lookupList);
|
|
145
|
+
const type = t.u16(lookup);
|
|
146
|
+
for (let i = 0, n = t.u16(lookup + 4); i < n; i++) {
|
|
147
|
+
let sub = t.offset(lookup + 6 + i * 2, lookup);
|
|
148
|
+
let subType = type;
|
|
149
|
+
if (subType === 9) {
|
|
150
|
+
// an extension: the real subtable, at a 32-bit offset from here
|
|
151
|
+
if (t.u16(sub) !== 1) return false;
|
|
152
|
+
subType = t.u16(sub + 2);
|
|
153
|
+
sub += t.u32(sub + 4);
|
|
154
|
+
}
|
|
155
|
+
const coverage = firstCoverage(t, subType, sub);
|
|
156
|
+
if (coverage < 0) return false;
|
|
157
|
+
addCoverage(t, coverage, bits);
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Where the coverage a subtable is entered from starts, or -1 for a
|
|
164
|
+
* subtable this does not know.
|
|
165
|
+
*/
|
|
166
|
+
function firstCoverage(t, type, sub) {
|
|
167
|
+
const format = t.u16(sub);
|
|
168
|
+
switch (type) {
|
|
169
|
+
case 1: // single adjustment
|
|
170
|
+
case 2: // pair adjustment: the pair's first glyph
|
|
171
|
+
return format === 1 || format === 2 ? t.offset(sub + 2, sub) : -1;
|
|
172
|
+
case 3: // cursive attachment
|
|
173
|
+
case 4: // mark to base: the mark
|
|
174
|
+
case 5: // mark to ligature: the mark
|
|
175
|
+
case 6: // mark to mark: the mark that attaches
|
|
176
|
+
return format === 1 ? t.offset(sub + 2, sub) : -1;
|
|
177
|
+
case 7: // context
|
|
178
|
+
if (format === 1 || format === 2) return t.offset(sub + 2, sub);
|
|
179
|
+
// the first input glyph's, after the glyph and lookup counts
|
|
180
|
+
return format === 3 && t.u16(sub + 2) > 0 ? t.offset(sub + 6, sub) : -1;
|
|
181
|
+
case 8: {
|
|
182
|
+
// chained context
|
|
183
|
+
if (format === 1 || format === 2) return t.offset(sub + 2, sub);
|
|
184
|
+
if (format !== 3) return -1;
|
|
185
|
+
// past the backtrack coverages to the input's count, then its first
|
|
186
|
+
const input = sub + 4 + t.u16(sub + 2) * 2;
|
|
187
|
+
return t.u16(input) > 0 ? t.offset(input + 2, sub) : -1;
|
|
188
|
+
}
|
|
189
|
+
default:
|
|
190
|
+
return -1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function addCoverage(t, coverage, bits) {
|
|
195
|
+
const format = t.u16(coverage);
|
|
196
|
+
const count = t.u16(coverage + 2);
|
|
197
|
+
if (format === 1) {
|
|
198
|
+
for (let i = 0; i < count; i++) set(bits, t.u16(coverage + 4 + i * 2));
|
|
199
|
+
} else if (format === 2) {
|
|
200
|
+
for (let i = 0; i < count; i++) {
|
|
201
|
+
const rec = coverage + 4 + i * 6;
|
|
202
|
+
const last = t.u16(rec + 2);
|
|
203
|
+
for (let id = t.u16(rec); id <= last; id++) set(bits, id);
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
throw new RangeError(`coverage format ${format}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function set(bits, id) {
|
|
211
|
+
bits[id >> 3] |= 1 << (id & 7);
|
|
212
|
+
}
|
package/lib/text/shape.js
CHANGED
|
@@ -74,23 +74,32 @@ export function normalizedLevels(levels, start, end) {
|
|
|
74
74
|
*/
|
|
75
75
|
const OPTIONAL_LIGATURES = ['liga', 'clig', 'dlig', 'hlig'];
|
|
76
76
|
|
|
77
|
-
/**
|
|
78
|
-
* The features a letter-spaced run shapes with: the optional ligatures off,
|
|
79
|
-
* underneath whatever the caller asked for — a style that names `liga`
|
|
80
|
-
* itself still gets it. `features` is either form fontkit takes, an array of
|
|
81
|
-
* tags to turn on or an object of tag → on/off.
|
|
82
|
-
*/
|
|
83
77
|
/**
|
|
84
78
|
* A copy of `features` for fontkit, which **adds to the object it is handed**
|
|
85
79
|
* — `rvrn` and the like, as it plans the shaping. Handed a caller's own
|
|
86
80
|
* object that is a style quietly growing keys, which changes the memo key it
|
|
87
81
|
* is filed under next time, and a frozen one throws.
|
|
82
|
+
*
|
|
83
|
+
* A feature a 0 turns off, as CSS writes one (`font-feature-settings: "kern"
|
|
84
|
+
* 0`, and `font-kerning: none`), goes to fontkit as `false`. It reads a 0 as
|
|
85
|
+
* off where it plans the OpenType features, but where a face keeps its
|
|
86
|
+
* kerning in the older `kern` table — Times New Roman's, and DejaVu's beside
|
|
87
|
+
* its GPOS pairs — it asks `kern !== false`, and a 0 kerned all the same.
|
|
88
88
|
*/
|
|
89
89
|
function ownFeatures(features) {
|
|
90
90
|
if (!features) return features;
|
|
91
|
-
|
|
91
|
+
if (Array.isArray(features)) return [...features];
|
|
92
|
+
const out = {};
|
|
93
|
+
for (const tag in features) out[tag] = features[tag] === 0 ? false : features[tag];
|
|
94
|
+
return out;
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The features a letter-spaced run shapes with: the optional ligatures off,
|
|
99
|
+
* underneath whatever the caller asked for — a style that names `liga`
|
|
100
|
+
* itself still gets it. `features` is either form fontkit takes, an array of
|
|
101
|
+
* tags to turn on or an object of tag → on/off.
|
|
102
|
+
*/
|
|
94
103
|
function spacedFeatures(features) {
|
|
95
104
|
const off = Object.fromEntries(OPTIONAL_LIGATURES.map((tag) => [tag, false]));
|
|
96
105
|
if (!features) return off;
|
|
@@ -98,7 +107,7 @@ function spacedFeatures(features) {
|
|
|
98
107
|
for (const tag of features) off[tag] = true;
|
|
99
108
|
return off;
|
|
100
109
|
}
|
|
101
|
-
return { ...off, ...features };
|
|
110
|
+
return { ...off, ...ownFeatures(features) };
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
/**
|