ntk 8.14.5 → 8.14.6
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/text/font.js +14 -0
- package/lib/text/fontmanager.js +24 -0
- package/lib/text/layout.js +62 -8
- package/lib/text/marks.js +3 -3
- package/lib/text/pairs.js +88 -0
- package/package.json +1 -1
package/lib/text/font.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as fontkit from 'fontkit';
|
|
|
3
3
|
import { flatten, rasterizePath } from '../rasterize.js';
|
|
4
4
|
import { NO_ANCHOR, tolerateAnchors, watchAnchors } from './anchors.js';
|
|
5
5
|
import { marksCover, standInMarks } from './marks.js';
|
|
6
|
+
import { pairGlyphs, pairsCover } from './pairs.js';
|
|
6
7
|
|
|
7
8
|
// Axis coordinates are rounded to this many decimals before anything is
|
|
8
9
|
// instantiated or cached. Every distinct coordinate is a font in its own
|
|
@@ -154,6 +155,8 @@ export default class Font {
|
|
|
154
155
|
// whether the face's mark attachment is watched for a NULL anchor
|
|
155
156
|
// (./anchors.js): set at its first shaping, false once one has been met
|
|
156
157
|
this._anchors = false;
|
|
158
|
+
// the glyphs its pairs can move (./pairs.js), read when first asked
|
|
159
|
+
this._pairs = undefined;
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
static loadSync(path, postscriptName) {
|
|
@@ -356,6 +359,17 @@ export default class Font {
|
|
|
356
359
|
return this._drawable;
|
|
357
360
|
}
|
|
358
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Whether two glyphs side by side can be set otherwise than apart: the
|
|
364
|
+
* left one moved against what follows it, or the right one against what
|
|
365
|
+
* comes before it (./pairs.js). True where the tables cannot tell.
|
|
366
|
+
*/
|
|
367
|
+
mayKern(left, right) {
|
|
368
|
+
if (this._pairs === undefined) this._pairs = pairGlyphs(this.fk);
|
|
369
|
+
const pairs = this._pairs;
|
|
370
|
+
return pairs === null || pairsCover(pairs.left, left) || pairsCover(pairs.right, right);
|
|
371
|
+
}
|
|
372
|
+
|
|
359
373
|
hasGlyph(codepoint) {
|
|
360
374
|
return this.drawable && this.fk.hasGlyphForCodePoint(codepoint);
|
|
361
375
|
}
|
package/lib/text/fontmanager.js
CHANGED
|
@@ -235,6 +235,8 @@ export default class FontManager {
|
|
|
235
235
|
this._shapeCount = 0;
|
|
236
236
|
// whether two letters shape otherwise together than apart (`_meets`)
|
|
237
237
|
this._meetings = new Map();
|
|
238
|
+
// how much two letters kern across a break opportunity (`_kernAcross`)
|
|
239
|
+
this._kerns = new Map();
|
|
238
240
|
// a paragraph's layout up to the line fill, for its next width
|
|
239
241
|
// (text/paragraphs.js, TextLayout)
|
|
240
242
|
this._paragraphs = new ParagraphCache();
|
|
@@ -580,6 +582,28 @@ export default class FontManager {
|
|
|
580
582
|
return meets;
|
|
581
583
|
}
|
|
582
584
|
|
|
585
|
+
/**
|
|
586
|
+
* How much wider the two letters of `pair`, cut at `cut`, are set
|
|
587
|
+
* together than apart in this style: their kerning, negative for a pair
|
|
588
|
+
* set closer. 0 where they do not kern, and where shaping them together
|
|
589
|
+
* changes a glyph — a ligature, a contextual form — which no offset
|
|
590
|
+
* between the two stands for. Kept, as `_meets` keeps its answers.
|
|
591
|
+
*/
|
|
592
|
+
_kernAcross(pair, cut, style, levelsKey = '0') {
|
|
593
|
+
const key = `${shapeGroupOf(style, levelsKey)}|${cut}|${pair}`;
|
|
594
|
+
let kern = this._kerns.get(key);
|
|
595
|
+
if (kern !== undefined) return kern;
|
|
596
|
+
const whole = this._shapeCached(pair, style, levelsKey);
|
|
597
|
+
const left = this._shapeCached(pair.slice(0, cut), style, sliceKey(levelsKey, 0, cut));
|
|
598
|
+
const right = this._shapeCached(pair.slice(cut), style, sliceKey(levelsKey, cut, pair.length));
|
|
599
|
+
const ids = (...shaped) => shaped.flatMap((s) => s.runs.flatMap((r) => r.glyphs.map((g) => g.id))).join(',');
|
|
600
|
+
kern = whole.width - left.width - right.width;
|
|
601
|
+
if (Math.abs(kern) <= 1e-6 || ids(whole) !== ids(left, right)) kern = 0;
|
|
602
|
+
if (this._kerns.size >= SHAPE_GENERATION) this._kerns.clear();
|
|
603
|
+
this._kerns.set(key, kern);
|
|
604
|
+
return kern;
|
|
605
|
+
}
|
|
606
|
+
|
|
583
607
|
_shapeCached(text, style, levelsKey = '0') {
|
|
584
608
|
// A hit in this generation is the whole cost of a word a paragraph has
|
|
585
609
|
// shaped before — a relayout at a new width asks for every word again,
|
package/lib/text/layout.js
CHANGED
|
@@ -21,10 +21,16 @@ const UNSPACED =
|
|
|
21
21
|
/** How wide a line's tokens are, the white space it ends on left out. */
|
|
22
22
|
function tokensWidth(toks) {
|
|
23
23
|
let width = 0;
|
|
24
|
-
for (
|
|
24
|
+
for (let i = 0; i < toks.length; i++) width += toks[i].width + (i ? kernBefore(toks[i]) : 0);
|
|
25
25
|
return toks.length ? width - toks[toks.length - 1].wsWidth : 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** The kerning between a token and the one before it on its line
|
|
29
|
+
* (`TextLayout._kernBefore`); none for a token a line starts with. */
|
|
30
|
+
function kernBefore(token) {
|
|
31
|
+
return token.kernBefore ?? 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
function isWsGlyph(g) {
|
|
29
35
|
return g.codePoints.length > 0 && g.codePoints.every((cp) => WS.has(cp));
|
|
30
36
|
}
|
|
@@ -121,7 +127,7 @@ export class TextLayout {
|
|
|
121
127
|
curWidth = 0;
|
|
122
128
|
};
|
|
123
129
|
for (let token of tokens) {
|
|
124
|
-
while (wraps && curWidth + token.width - token.wsWidth > maxWidth) {
|
|
130
|
+
while (wraps && curWidth + (cur.length ? kernBefore(token) : 0) + token.width - token.wsWidth > maxWidth) {
|
|
125
131
|
if (cur.length > 0) {
|
|
126
132
|
flush();
|
|
127
133
|
} else if (whole && !UNSPACED.test(text.slice(token.start, token.end))) {
|
|
@@ -135,8 +141,8 @@ export class TextLayout {
|
|
|
135
141
|
token = rest;
|
|
136
142
|
}
|
|
137
143
|
}
|
|
144
|
+
curWidth += token.width + (cur.length ? kernBefore(token) : 0);
|
|
138
145
|
cur.push(token);
|
|
139
|
-
curWidth += token.width;
|
|
140
146
|
if (token.required) flush();
|
|
141
147
|
}
|
|
142
148
|
if (cur.length) lineTokens.push(cur);
|
|
@@ -185,16 +191,24 @@ export class TextLayout {
|
|
|
185
191
|
// .start/.end are absolute code-unit ranges into the full text, kept
|
|
186
192
|
// for caret positioning (caretPosition / indexAt)
|
|
187
193
|
let entries = [];
|
|
188
|
-
for (
|
|
194
|
+
for (let t = 0; t < toks.length; t++) {
|
|
195
|
+
const token = toks[t];
|
|
196
|
+
// the kerning with the token before it goes before its first run
|
|
197
|
+
let kern = t ? kernBefore(token) : 0;
|
|
189
198
|
for (const frag of token.fragments) {
|
|
190
199
|
for (const run of frag.shaped.runs) {
|
|
191
|
-
|
|
200
|
+
const entry = {
|
|
192
201
|
run,
|
|
193
202
|
span: frag.span,
|
|
194
203
|
level: run.level,
|
|
195
204
|
start: frag.start + run.start,
|
|
196
205
|
end: frag.start + run.end
|
|
197
|
-
}
|
|
206
|
+
};
|
|
207
|
+
if (kern) {
|
|
208
|
+
entry.kern = kern;
|
|
209
|
+
kern = 0;
|
|
210
|
+
}
|
|
211
|
+
entries.push(entry);
|
|
198
212
|
}
|
|
199
213
|
}
|
|
200
214
|
}
|
|
@@ -234,6 +248,7 @@ export class TextLayout {
|
|
|
234
248
|
const runs = [];
|
|
235
249
|
let x = 0;
|
|
236
250
|
for (const e of entries) {
|
|
251
|
+
if (e.kern) x += e.kern;
|
|
237
252
|
const positioned = { x, width: e.run.width, run: e.run, span: e.span, start: e.start, end: e.end };
|
|
238
253
|
if (e.ellipsis) positioned.ellipsis = true;
|
|
239
254
|
runs.push(positioned);
|
|
@@ -396,10 +411,48 @@ export class TextLayout {
|
|
|
396
411
|
tokens.push(this._makeToken(text, prev, text.length, levels, spanAt, false));
|
|
397
412
|
}
|
|
398
413
|
}
|
|
414
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
415
|
+
const kern = this._kernBefore(text, tokens[i - 1], tokens[i], levels);
|
|
416
|
+
if (kern) tokens[i].kernBefore = kern;
|
|
417
|
+
}
|
|
399
418
|
|
|
400
419
|
return { spans, text, baseLevel, tokens };
|
|
401
420
|
}
|
|
402
421
|
|
|
422
|
+
/**
|
|
423
|
+
* The kerning between the last letter of one token and the first of the
|
|
424
|
+
* next, where the two are on one line. Each token is shaped apart, so a
|
|
425
|
+
* pair either side of a break opportunity was never kerned — Trebuchet MS
|
|
426
|
+
* sets a space closer to an A, a T or a Y — and a line of it came out
|
|
427
|
+
* wider than a browser's, which shapes a line whole: shaping breaks only
|
|
428
|
+
* at an inline box's margin, border or padding (CSS Text 3, 7.3). Only
|
|
429
|
+
* between letters shaped alike at one left-to-right level; a line that
|
|
430
|
+
* breaks there never meets it.
|
|
431
|
+
*/
|
|
432
|
+
_kernBefore(text, prev, next, levels) {
|
|
433
|
+
if (prev.required) return 0;
|
|
434
|
+
const a = prev.fragments[prev.fragments.length - 1];
|
|
435
|
+
const b = next.fragments[0];
|
|
436
|
+
if (!a || !b || a.shaping !== b.shaping || a.span.shapeApart || b.span.shapeApart) return 0;
|
|
437
|
+
const at = a.start + a.text.length;
|
|
438
|
+
if (at !== b.start) return 0;
|
|
439
|
+
// two glyphs the face's tables cannot move against each other are set
|
|
440
|
+
// together as apart, and asked about no further: most faces kern no
|
|
441
|
+
// space, and a paragraph meets one at every word
|
|
442
|
+
const l = a.shaped.runs[a.shaped.runs.length - 1];
|
|
443
|
+
const r = b.shaped.runs[0];
|
|
444
|
+
if (!l || !r || l.font !== r.font) return 0;
|
|
445
|
+
const lg = l.glyphs[l.glyphs.length - 1];
|
|
446
|
+
const rg = r.glyphs[0];
|
|
447
|
+
if (!lg || !rg || (l.font.mayKern && !l.font.mayKern(lg.id, rg.id))) return 0;
|
|
448
|
+
const low = text.charCodeAt(at - 1);
|
|
449
|
+
const before = low >= 0xdc00 && low < 0xe000 && at >= 2 ? 2 : 1;
|
|
450
|
+
const after = text.codePointAt(at) > 0xffff ? 2 : 1;
|
|
451
|
+
const key = normalizedLevels(levels, at - before, at + after);
|
|
452
|
+
if (key.includes(',') || Number(key) & 1) return 0;
|
|
453
|
+
return this.fonts._kernAcross(text.slice(at - before, at + after), before, a.shaping, key);
|
|
454
|
+
}
|
|
455
|
+
|
|
403
456
|
/**
|
|
404
457
|
* Whether the letters that meet at an edge between two spans of a group
|
|
405
458
|
* shape otherwise together than apart: their glyphs or their advances
|
|
@@ -537,9 +590,10 @@ export class TextLayout {
|
|
|
537
590
|
for (const token of toks) {
|
|
538
591
|
// trailing whitespace does not count against the budget, exactly as it
|
|
539
592
|
// does not count during the greedy fill
|
|
540
|
-
|
|
593
|
+
const kern = kept.length ? kernBefore(token) : 0;
|
|
594
|
+
if (used + kern + token.width - token.wsWidth <= budget) {
|
|
541
595
|
kept.push(token);
|
|
542
|
-
used += token.width;
|
|
596
|
+
used += kern + token.width;
|
|
543
597
|
continue;
|
|
544
598
|
}
|
|
545
599
|
const [head] = this._forceBreak(token, budget - used);
|
package/lib/text/marks.js
CHANGED
|
@@ -102,7 +102,7 @@ export function marksCover(bits, id) {
|
|
|
102
102
|
* base)` reads the 16-bit offset stored at `pos` and answers where it
|
|
103
103
|
* points, `base` being what the format measures it from.
|
|
104
104
|
*/
|
|
105
|
-
function table(fk, tag) {
|
|
105
|
+
export function table(fk, tag) {
|
|
106
106
|
const entry = fk.directory?.tables?.[tag];
|
|
107
107
|
if (!entry) return null;
|
|
108
108
|
const stream = fk._getTableStream(tag);
|
|
@@ -163,7 +163,7 @@ function coverLookup(t, lookupList, index, bits) {
|
|
|
163
163
|
* Where the coverage a subtable is entered from starts, or -1 for a
|
|
164
164
|
* subtable this does not know.
|
|
165
165
|
*/
|
|
166
|
-
function firstCoverage(t, type, sub) {
|
|
166
|
+
export function firstCoverage(t, type, sub) {
|
|
167
167
|
const format = t.u16(sub);
|
|
168
168
|
switch (type) {
|
|
169
169
|
case 1: // single adjustment
|
|
@@ -191,7 +191,7 @@ function firstCoverage(t, type, sub) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
function addCoverage(t, coverage, bits) {
|
|
194
|
+
export function addCoverage(t, coverage, bits) {
|
|
195
195
|
const format = t.u16(coverage);
|
|
196
196
|
const count = t.u16(coverage + 2);
|
|
197
197
|
if (format === 1) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The glyphs a face can set closer or further from the glyph beside them,
|
|
2
|
+
// read from its tables' bytes the way ./marks.js reads its marks: a pair of
|
|
3
|
+
// letters that neither can be is set together as it is set apart, and the
|
|
4
|
+
// layout need not shape the two to find that out (`TextLayout._kernBefore`).
|
|
5
|
+
//
|
|
6
|
+
// A pair's left glyph can be moved by a pair adjustment it enters, by a
|
|
7
|
+
// cursive attachment, by a context it is the first input of, or by the
|
|
8
|
+
// pairs of a legacy `kern` table; its right glyph only by a context it is
|
|
9
|
+
// the first input of, with the left glyph behind it. Every GPOS lookup is
|
|
10
|
+
// read, whatever feature names it — a superset of what fontkit applies,
|
|
11
|
+
// which only widens what counts.
|
|
12
|
+
//
|
|
13
|
+
// Null — ask the shaper — for a face shaped through AAT's morx, a `kern`
|
|
14
|
+
// table in a format this does not read, or tables it cannot read to the end.
|
|
15
|
+
|
|
16
|
+
import { addCoverage, firstCoverage, table } from './marks.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} fk a fontkit font
|
|
20
|
+
* @returns {{ left: Uint8Array, right: Uint8Array }|null} a bit per glyph id,
|
|
21
|
+
* set for every glyph that can be moved against the glyph after it
|
|
22
|
+
* (`left`) or before it (`right`); null where this cannot tell
|
|
23
|
+
*/
|
|
24
|
+
export function pairGlyphs(fk) {
|
|
25
|
+
try {
|
|
26
|
+
if (fk.directory?.tables?.morx) return null;
|
|
27
|
+
const left = new Uint8Array(8192);
|
|
28
|
+
const right = new Uint8Array(8192);
|
|
29
|
+
const gpos = table(fk, 'GPOS');
|
|
30
|
+
if (gpos) {
|
|
31
|
+
const lookupList = gpos.offset(8, 0);
|
|
32
|
+
for (let index = 0, n = gpos.u16(lookupList); index < n; index++) {
|
|
33
|
+
if (!coverPairs(gpos, lookupList, index, left, right)) return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const kern = table(fk, 'kern');
|
|
37
|
+
if (kern && !kernLefts(kern, left)) return null;
|
|
38
|
+
return { left, right };
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether `bits` (from `pairGlyphs`) has glyph `id`. */
|
|
45
|
+
export function pairsCover(bits, id) {
|
|
46
|
+
return (bits[id >> 3] & (1 << (id & 7))) !== 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Adds what a lookup can move against a neighbour; false for one it cannot read. */
|
|
50
|
+
function coverPairs(t, lookupList, index, left, right) {
|
|
51
|
+
const lookup = t.offset(lookupList + 2 + index * 2, lookupList);
|
|
52
|
+
const type = t.u16(lookup);
|
|
53
|
+
for (let i = 0, n = t.u16(lookup + 4); i < n; i++) {
|
|
54
|
+
let sub = t.offset(lookup + 6 + i * 2, lookup);
|
|
55
|
+
let subType = type;
|
|
56
|
+
if (subType === 9) {
|
|
57
|
+
if (t.u16(sub) !== 1) return false;
|
|
58
|
+
subType = t.u16(sub + 2);
|
|
59
|
+
sub += t.u32(sub + 4);
|
|
60
|
+
}
|
|
61
|
+
// a single adjustment moves a glyph whatever is beside it, and a mark
|
|
62
|
+
// attaches with no advance of its own
|
|
63
|
+
if (subType !== 2 && subType !== 3 && subType !== 7 && subType !== 8) continue;
|
|
64
|
+
const coverage = firstCoverage(t, subType, sub);
|
|
65
|
+
if (coverage < 0) return false;
|
|
66
|
+
addCoverage(t, coverage, left);
|
|
67
|
+
if (subType === 7 || subType === 8) addCoverage(t, coverage, right);
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Adds the left glyphs of a Microsoft `kern` table's pairs; false for a
|
|
73
|
+
* table or subtable format this does not read. */
|
|
74
|
+
function kernLefts(t, left) {
|
|
75
|
+
if (t.u16(0) !== 0) return false;
|
|
76
|
+
let at = 4;
|
|
77
|
+
for (let i = 0, n = t.u16(2); i < n; i++) {
|
|
78
|
+
const length = t.u16(at + 2);
|
|
79
|
+
const format = t.u16(at + 4) >> 8;
|
|
80
|
+
if (format !== 0) return false;
|
|
81
|
+
for (let j = 0, pairs = t.u16(at + 6); j < pairs; j++) {
|
|
82
|
+
const id = t.u16(at + 14 + j * 6);
|
|
83
|
+
left[id >> 3] |= 1 << (id & 7);
|
|
84
|
+
}
|
|
85
|
+
at += length;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|