ntk 8.2.0 → 8.3.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 +346 -7
- package/lib/cleanup.js +23 -7
- package/lib/drawable.js +10 -0
- package/lib/events_map.js +100 -1
- package/lib/glyphdirectory.js +275 -0
- package/lib/glyphdwire.js +142 -0
- package/lib/glyphset.js +32 -5
- package/lib/index.js +25 -2
- package/lib/pictformat.js +196 -0
- package/lib/pixmap.js +166 -7
- package/lib/region.js +148 -0
- package/lib/renderingcontext_2d.js +717 -235
- package/lib/shapeglyphs.js +94 -26
- package/lib/sharedglyphs.js +433 -0
- package/lib/text/glyphs.js +143 -26
- package/lib/widgets/svgview.js +7 -5
- package/lib/window.js +308 -40
- package/lib/xembed.js +3 -1
- package/package.json +2 -2
package/lib/text/glyphs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import GlyphSet from '../glyphset.js';
|
|
2
2
|
import Picture from '../picture.js';
|
|
3
3
|
import Pixmap from '../pixmap.js';
|
|
4
|
+
import { fontPageToken, sharedGlyphsFor } from '../sharedglyphs.js';
|
|
4
5
|
import { trapezoidize } from '../trapezoid.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -37,6 +38,25 @@ function policyOf(app) {
|
|
|
37
38
|
return app.textPolicy ? { ...DEFAULT_TEXT_POLICY, ...app.textPolicy } : DEFAULT_TEXT_POLICY;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/** a fresh AddGlyphs object — fresh per call because node-x11 mutates what
|
|
42
|
+
* it is given (pads rows, divides offX by 64); the source bitmap survives */
|
|
43
|
+
function uploadGlyph(lid, bitmap, adv) {
|
|
44
|
+
return {
|
|
45
|
+
id: lid,
|
|
46
|
+
width: bitmap ? bitmap.width : 0,
|
|
47
|
+
height: bitmap ? bitmap.height : 0,
|
|
48
|
+
// XRender GLYPHINFO places the image at origin - (x, y); node-x11
|
|
49
|
+
// packs -x and +y, so x here is the bitmap's left bearing and y its
|
|
50
|
+
// (negated, y-down) top — i.e. the ascent above the baseline
|
|
51
|
+
x: bitmap ? bitmap.left : 0,
|
|
52
|
+
y: bitmap ? -bitmap.top : 0,
|
|
53
|
+
// node-x11 AddGlyphs expects 26.6 fixed point and divides by 64
|
|
54
|
+
offX: adv * 64,
|
|
55
|
+
offY: 0,
|
|
56
|
+
image: bitmap ? bitmap.data : Buffer.alloc(0)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
/**
|
|
41
61
|
* Server-side glyph cache for one (font face, pixel size) pair.
|
|
42
62
|
*
|
|
@@ -51,32 +71,52 @@ function policyOf(app) {
|
|
|
51
71
|
* (`offX`), so the server advances the pen automatically and runs of
|
|
52
72
|
* unkerned text need no per-glyph position data at all.
|
|
53
73
|
* - Bitmaps upload lazily, once per glyph, batched into one AddGlyphs
|
|
54
|
-
* request per draw that introduces new glyphs
|
|
74
|
+
* request per draw that introduces new glyphs — and, when the display's
|
|
75
|
+
* shared glyph directory is live (docs/shared-glyphs.md), re-bind to the
|
|
76
|
+
* display-wide set so other ntk processes skip the work entirely.
|
|
55
77
|
*/
|
|
56
78
|
export class GlyphPage {
|
|
57
79
|
constructor(app, font, size) {
|
|
58
80
|
this.app = app;
|
|
59
81
|
this.font = font;
|
|
60
82
|
this.size = size;
|
|
61
|
-
this
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
// With shared glyphs off this page is exactly what it always was: a
|
|
84
|
+
// private set from birth. With them on, the set is minted lazily — a
|
|
85
|
+
// page that resolves entirely from the directory never creates one.
|
|
86
|
+
this.glyphset = sharedGlyphsFor(app) ? null : new GlyphSet(app);
|
|
87
|
+
this.entries = new Map(); // font glyph id -> { lid, adv, gs }
|
|
88
|
+
this.bytes = 0; // privately uploaded bitmap bytes, for the LRU budget
|
|
89
|
+
this._lids = 0; // next private lid (dense, upload order)
|
|
90
|
+
this._maxLid = -1; // widest id this page composites, private or shared
|
|
91
|
+
this._privateCount = 0; // entries still bound to the private set
|
|
92
|
+
this._shared = undefined; // SharedPage binding, resolved on first ensure
|
|
64
93
|
}
|
|
65
94
|
|
|
66
95
|
/** number of bits per glyph needed to address this page's ids */
|
|
67
96
|
get bits() {
|
|
68
|
-
return this.
|
|
97
|
+
return this._maxLid > 255 ? 16 : 8;
|
|
69
98
|
}
|
|
70
99
|
|
|
71
100
|
/**
|
|
72
|
-
* Ensure all glyphs of a shaped run are
|
|
73
|
-
*
|
|
101
|
+
* Ensure all glyphs of a shaped run are drawable; returns nothing.
|
|
102
|
+
*
|
|
103
|
+
* New glyphs are rasterized and sent in a single AddGlyphs request to the
|
|
104
|
+
* page's private set — this draw's pixels never wait on anything. When the
|
|
105
|
+
* shared directory is live they are also asked about (docs/shared-glyphs.md):
|
|
106
|
+
* the reply re-binds their entries to the display-wide set — uploading the
|
|
107
|
+
* retained bitmap only when this process was first to need the glyph — and
|
|
108
|
+
* once nothing private remains the private set is freed. Every later draw
|
|
109
|
+
* of those glyphs, in this process and every other, is then pure
|
|
110
|
+
* CompositeGlyphs. A page that wants the shared entries *before* first
|
|
111
|
+
* paint warms up front: see `warmSharedGlyphs`.
|
|
74
112
|
*/
|
|
75
113
|
ensure(glyphs) {
|
|
76
114
|
let batch = null;
|
|
115
|
+
let ask = null;
|
|
116
|
+
const shared = this._sharedBinding();
|
|
77
117
|
for (const g of glyphs) {
|
|
78
118
|
if (this.entries.has(g.id)) continue;
|
|
79
|
-
const lid = this.
|
|
119
|
+
const lid = this._lids++;
|
|
80
120
|
if (lid >= 65536) {
|
|
81
121
|
// 2^16 distinct glyphs at one size — not reachable in practice
|
|
82
122
|
// (fonts cap at 65535 glyphs) but fail loudly rather than corrupt
|
|
@@ -84,36 +124,77 @@ export class GlyphPage {
|
|
|
84
124
|
}
|
|
85
125
|
const adv = Math.round(this.font.advanceOf(g.id, this.size));
|
|
86
126
|
const bitmap = this.font.rasterize(g.id, this.size);
|
|
87
|
-
this.
|
|
127
|
+
if (!this.glyphset) this.glyphset = new GlyphSet(this.app);
|
|
128
|
+
if (lid > this._maxLid) this._maxLid = lid;
|
|
129
|
+
this.entries.set(g.id, { lid, adv, gs: this.glyphset.id, private: true });
|
|
130
|
+
this._privateCount++;
|
|
88
131
|
if (!batch) batch = [];
|
|
89
|
-
batch.push(
|
|
90
|
-
id: lid,
|
|
91
|
-
width: bitmap ? bitmap.width : 0,
|
|
92
|
-
height: bitmap ? bitmap.height : 0,
|
|
93
|
-
// XRender GLYPHINFO places the image at origin - (x, y); node-x11
|
|
94
|
-
// packs -x and +y, so x here is the bitmap's left bearing and y its
|
|
95
|
-
// (negated, y-down) top — i.e. the ascent above the baseline
|
|
96
|
-
x: bitmap ? bitmap.left : 0,
|
|
97
|
-
y: bitmap ? -bitmap.top : 0,
|
|
98
|
-
// node-x11 AddGlyphs expects 26.6 fixed point and divides by 64
|
|
99
|
-
offX: adv * 64,
|
|
100
|
-
offY: 0,
|
|
101
|
-
image: bitmap ? bitmap.data : Buffer.alloc(0)
|
|
102
|
-
});
|
|
132
|
+
batch.push(uploadGlyph(lid, bitmap, adv));
|
|
103
133
|
this.bytes += bitmap ? bitmap.data.length : 0;
|
|
134
|
+
if (shared && shared.open) {
|
|
135
|
+
if (!ask) ask = [];
|
|
136
|
+
ask.push({ key: g.id, payload: { bitmap, adv } });
|
|
137
|
+
}
|
|
104
138
|
}
|
|
105
139
|
if (batch) this.glyphset.addGlyphs(batch);
|
|
140
|
+
if (ask) shared.ask(ask);
|
|
106
141
|
}
|
|
107
142
|
|
|
108
143
|
entry(fontGlyphId) {
|
|
109
144
|
return this.entries.get(fontGlyphId);
|
|
110
145
|
}
|
|
111
146
|
|
|
112
|
-
/**
|
|
147
|
+
/** the shared side of this page, bound on first use; null when the
|
|
148
|
+
* feature is off or the font's bytes cannot be content-addressed */
|
|
149
|
+
_sharedBinding() {
|
|
150
|
+
if (this._shared !== undefined) return this._shared;
|
|
151
|
+
const client = sharedGlyphsFor(this.app);
|
|
152
|
+
const token = client ? fontPageToken(this.font, this.size) : null;
|
|
153
|
+
this._shared = token
|
|
154
|
+
? client.bindPage({
|
|
155
|
+
token,
|
|
156
|
+
indices: true, // member keys are the font glyph indices themselves
|
|
157
|
+
makeGlyph: (key, payload, lid) => {
|
|
158
|
+
// absent from the shared set: upload the bitmap retained at
|
|
159
|
+
// mint time, or rasterize now on the warm path (which retained
|
|
160
|
+
// nothing precisely because the glyph was expected to be there)
|
|
161
|
+
const bitmap =
|
|
162
|
+
payload.bitmap !== undefined ? payload.bitmap : this.font.rasterize(key, this.size);
|
|
163
|
+
return uploadGlyph(lid, bitmap, payload.adv);
|
|
164
|
+
},
|
|
165
|
+
adopt: (key, lid, gsid, payload) => this._adoptShared(key, lid, gsid, payload)
|
|
166
|
+
})
|
|
167
|
+
: null;
|
|
168
|
+
return this._shared;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** re-bind one glyph to the shared set (confirmed present, or uploaded by
|
|
172
|
+
* us); drop the private set once nothing composites from it any more */
|
|
173
|
+
_adoptShared(key, lid, gsid, payload) {
|
|
174
|
+
const prev = this.entries.get(key);
|
|
175
|
+
this.entries.set(key, { lid, adv: payload.adv, gs: gsid });
|
|
176
|
+
if (lid > this._maxLid) this._maxLid = lid;
|
|
177
|
+
if (prev && prev.private && --this._privateCount === 0 && this.glyphset) {
|
|
178
|
+
// ordered after any CompositeGlyphs already issued, like the LRU's
|
|
179
|
+
// frees; new glyphs during a directory outage mint a fresh set
|
|
180
|
+
this.glyphset.destroy();
|
|
181
|
+
this.glyphset = null;
|
|
182
|
+
this.bytes = 0;
|
|
183
|
+
this._lids = 0;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** free the server-side glyphset and shared alias (LRU eviction / shutdown) */
|
|
113
188
|
destroy() {
|
|
114
|
-
this.glyphset.destroy();
|
|
189
|
+
if (this.glyphset) this.glyphset.destroy();
|
|
190
|
+
this.glyphset = null;
|
|
191
|
+
if (this._shared) this._shared.destroy();
|
|
192
|
+
this._shared = undefined;
|
|
115
193
|
this.entries.clear();
|
|
116
194
|
this.bytes = 0;
|
|
195
|
+
this._lids = 0;
|
|
196
|
+
this._maxLid = -1;
|
|
197
|
+
this._privateCount = 0;
|
|
117
198
|
}
|
|
118
199
|
}
|
|
119
200
|
|
|
@@ -133,6 +214,42 @@ export function getGlyphPage(app, font, size) {
|
|
|
133
214
|
return page;
|
|
134
215
|
}
|
|
135
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Warm one (font, size) page from the shared glyph directory before first
|
|
219
|
+
* paint (docs/shared-glyphs.md): resolve `text`'s glyphs into shared-set
|
|
220
|
+
* entries so that the first draw of already-shared text rasterizes nothing
|
|
221
|
+
* and uploads nothing — the cold-start win a synchronous first `fillText`
|
|
222
|
+
* cannot have, since it cannot wait for the directory's answer and falls
|
|
223
|
+
* back to a private upload for exactly one frame instead.
|
|
224
|
+
*
|
|
225
|
+
* Glyphs the directory has never seen are rasterized here and uploaded once,
|
|
226
|
+
* which is the same work the first draw would have done. Resolves `true`
|
|
227
|
+
* when the page ended up bound to the shared cache; `false` means the
|
|
228
|
+
* feature is off or degraded and drawing will use the private path — either
|
|
229
|
+
* way the following draws are correct.
|
|
230
|
+
*
|
|
231
|
+
* @param {App} app
|
|
232
|
+
* @param {Font} font
|
|
233
|
+
* @param {number} size pixel size
|
|
234
|
+
* @param {string} text whose glyphs to warm (shaped with defaults)
|
|
235
|
+
* @returns {Promise<boolean>}
|
|
236
|
+
*/
|
|
237
|
+
export async function warmSharedGlyphs(app, font, size, text) {
|
|
238
|
+
if (!sharedGlyphsFor(app)) return false;
|
|
239
|
+
const page = getGlyphPage(app, font, size);
|
|
240
|
+
const shared = page._sharedBinding();
|
|
241
|
+
if (!shared || !shared.open) return false;
|
|
242
|
+
const ask = [];
|
|
243
|
+
const seen = new Set();
|
|
244
|
+
for (const g of font.shape(String(text), size).glyphs) {
|
|
245
|
+
if (page.entries.has(g.id) || seen.has(g.id)) continue;
|
|
246
|
+
seen.add(g.id);
|
|
247
|
+
ask.push({ key: g.id, payload: { adv: Math.round(font.advanceOf(g.id, size)) } });
|
|
248
|
+
}
|
|
249
|
+
if (ask.length) await shared.ask(ask);
|
|
250
|
+
return shared.open && shared.bound;
|
|
251
|
+
}
|
|
252
|
+
|
|
136
253
|
/**
|
|
137
254
|
* Evict least-recently-used glyph pages until uploaded bitmaps fit the
|
|
138
255
|
* policy budget. `inUse` pages (referenced by requests queued this draw)
|
|
@@ -422,7 +539,7 @@ function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy) {
|
|
|
422
539
|
for (const pos of positionGlyphs(positioned)) {
|
|
423
540
|
const page = pages.get(pos.run);
|
|
424
541
|
const e = page.entry(pos.glyph.id);
|
|
425
|
-
items.push({ gs:
|
|
542
|
+
items.push({ gs: e.gs, lid: e.lid, adv: e.adv, x: pos.x, y: pos.y });
|
|
426
543
|
}
|
|
427
544
|
const encoded = encodeGlyphItems(items, bits);
|
|
428
545
|
if (!encoded) return;
|
package/lib/widgets/svgview.js
CHANGED
|
@@ -220,9 +220,11 @@ function pathBBox(path) {
|
|
|
220
220
|
* inline `style` beats the presentation attribute, the initial fill is black
|
|
221
221
|
* and the initial stroke is none, `<line>` never fills, `<use>` paints its
|
|
222
222
|
* target with the *use* element's style, and non-rendered subtrees
|
|
223
|
-
* contribute nothing. It starts at the root
|
|
224
|
-
*
|
|
225
|
-
*
|
|
223
|
+
* contribute nothing. It starts at the root element itself, because `draw`
|
|
224
|
+
* does: the root `<svg>` is an ordinary element for inheritance, and every
|
|
225
|
+
* mainstream icon set puts `fill`/`stroke` there (issue #306). The two walks
|
|
226
|
+
* have to agree — a scan that skipped the root would disagree with what is
|
|
227
|
+
* painted.
|
|
226
228
|
*
|
|
227
229
|
* @returns {{ kind: 'mono'|'multi', solo: string|null }} `solo` is the one
|
|
228
230
|
* paint a `mono` document uses — a colour, or the literal `'currentColor'`
|
|
@@ -284,7 +286,7 @@ function scanPaints(root, ids) {
|
|
|
284
286
|
}
|
|
285
287
|
};
|
|
286
288
|
|
|
287
|
-
|
|
289
|
+
visit(root, INHERITED.fill, INHERITED.stroke, 0);
|
|
288
290
|
return {
|
|
289
291
|
kind: multi || paints.size > 1 ? 'multi' : 'mono',
|
|
290
292
|
solo: paints.size === 1 ? [...paints][0] : null
|
|
@@ -433,7 +435,7 @@ export default class SvgView {
|
|
|
433
435
|
this._renderChildren(
|
|
434
436
|
this._root,
|
|
435
437
|
ctx,
|
|
436
|
-
{ ...INHERITED, color: opts.color ?? this.color },
|
|
438
|
+
this._style(this._root, { ...INHERITED, color: opts.color ?? this.color }),
|
|
437
439
|
1,
|
|
438
440
|
0
|
|
439
441
|
);
|