ntk 8.17.8 → 8.18.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/README.md +4 -0
- package/lib/app.js +3 -0
- package/lib/index.js +2 -0
- package/lib/precise.js +506 -0
- package/lib/rasterize.js +21 -8
- package/lib/renderingcontext_2d.js +160 -104
- package/lib/text/brotlidictionary.js +99 -0
- package/lib/text/font.js +16 -6
- package/lib/text/woff2.js +102 -0
- package/lib/trapezoid.js +140 -52
- package/lib/vendor/fontkit.js +14379 -0
- package/package.json +12 -3
|
@@ -29,6 +29,7 @@ import Picture from "./picture.js";
|
|
|
29
29
|
import { formatForDepth } from "./pictformat.js";
|
|
30
30
|
import Pixmap from "./pixmap.js";
|
|
31
31
|
import { REGION_DOCS, regionId } from "./region.js";
|
|
32
|
+
import { snapFixed16 } from "./precise.js";
|
|
32
33
|
import { routeRaster } from "./rasterize.js";
|
|
33
34
|
import {
|
|
34
35
|
clipRingToRect,
|
|
@@ -2098,29 +2099,33 @@ class RenderingContext2d {
|
|
|
2098
2099
|
});
|
|
2099
2100
|
}
|
|
2100
2101
|
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2102
|
+
/**
|
|
2103
|
+
* The shapes a `w`×`h` picture whose pixel (0, 0) is device (-dx, -dy)
|
|
2104
|
+
* is sent, in device space: `_rasterizePolys` trapezoidizes them for the
|
|
2105
|
+
* server, and the local route hands the same ones to the rasterizer, so
|
|
2106
|
+
* that both rasterize one geometry (lib/precise.js).
|
|
2107
|
+
*
|
|
2108
|
+
* Trapezoids are 16.16 fixed point, and a coordinate past 32,767 throws
|
|
2109
|
+
* out of the paint (lib/cliprect.js). They go on the wire in device space
|
|
2110
|
+
* with the picture's offset beside them, which the server adds, so a
|
|
2111
|
+
* shape has to be in reach both in device space and in the picture's.
|
|
2112
|
+
* One that is not is cut to the picture first, a pixel to spare on every
|
|
2113
|
+
* side: nothing outside the picture is ever composited. One in reach
|
|
2114
|
+
* goes as it came, for the server to clip.
|
|
2115
|
+
*/
|
|
2116
|
+
_wireShapes(shapes, dx = 0, dy = 0, w = this.width, h = this.height) {
|
|
2116
2117
|
const x0 = -dx - 1;
|
|
2117
2118
|
const y0 = -dy - 1;
|
|
2118
2119
|
const x1 = w - dx + 1;
|
|
2119
2120
|
const y1 = h - dy + 1;
|
|
2121
|
+
const rx0 = Math.max(-WIRE_REACH, -WIRE_REACH - dx);
|
|
2122
|
+
const ry0 = Math.max(-WIRE_REACH, -WIRE_REACH - dy);
|
|
2123
|
+
const rx1 = Math.min(WIRE_REACH, WIRE_REACH - dx);
|
|
2124
|
+
const ry1 = Math.min(WIRE_REACH, WIRE_REACH - dy);
|
|
2120
2125
|
let fitted = null;
|
|
2121
2126
|
for (let i = 0; i < shapes.length; i++) {
|
|
2122
2127
|
const shape = shapes[i];
|
|
2123
|
-
if (withinRect(shape,
|
|
2128
|
+
if (withinRect(shape, rx0, ry0, rx1, ry1)) {
|
|
2124
2129
|
if (fitted) fitted.push(shape);
|
|
2125
2130
|
continue;
|
|
2126
2131
|
}
|
|
@@ -2128,11 +2133,30 @@ class RenderingContext2d {
|
|
|
2128
2133
|
const ring = clipRingToRect(shape, x0, y0, x1, y1);
|
|
2129
2134
|
if (ring.length >= 6) fitted.push(ring);
|
|
2130
2135
|
}
|
|
2131
|
-
|
|
2136
|
+
return fitted ?? shapes;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
_rasterizePolys(
|
|
2140
|
+
picture,
|
|
2141
|
+
shapes,
|
|
2142
|
+
rule,
|
|
2143
|
+
dx = 0,
|
|
2144
|
+
dy = 0,
|
|
2145
|
+
w = this.width,
|
|
2146
|
+
h = this.height,
|
|
2147
|
+
) {
|
|
2148
|
+
if (!shapes.length) return;
|
|
2149
|
+
// In device space and on the 16.16 grid, with the offset into the
|
|
2150
|
+
// picture left to the request: these are the numbers PreciseRasterizer
|
|
2151
|
+
// rasterizes when the same mask is drawn here, so the two routes agree
|
|
2152
|
+
// to the byte. An offset folded into the coordinates instead rounds
|
|
2153
|
+
// differently from one added to them on the wire.
|
|
2154
|
+
const traps = trapezoidize(this._wireShapes(shapes, dx, dy, w, h), 0, 0, [], rule);
|
|
2155
|
+
for (let i = 0; i < traps.length; i++) traps[i] = snapFixed16(traps[i]);
|
|
2132
2156
|
// stay under the server's maximum request size
|
|
2133
2157
|
const chunk = 4000 * 6;
|
|
2134
2158
|
for (let i = 0; i < traps.length; i += chunk) {
|
|
2135
|
-
this.Render.AddTraps(picture.id,
|
|
2159
|
+
this.Render.AddTraps(picture.id, dx, dy, traps.slice(i, i + chunk));
|
|
2136
2160
|
}
|
|
2137
2161
|
}
|
|
2138
2162
|
|
|
@@ -2491,17 +2515,20 @@ class RenderingContext2d {
|
|
|
2491
2515
|
box = box ? unionBox(box, shown) : shown;
|
|
2492
2516
|
}
|
|
2493
2517
|
if (!box) continue;
|
|
2518
|
+
// what the server would be sent for the surface-sized mask, so that
|
|
2519
|
+
// drawn here it comes out the same
|
|
2520
|
+
const wire = this._wireShapes(flat);
|
|
2494
2521
|
painted =
|
|
2495
2522
|
this._paintThroughMask(
|
|
2496
2523
|
box,
|
|
2497
|
-
{ polys:
|
|
2524
|
+
{ polys: wire, rule },
|
|
2498
2525
|
{
|
|
2499
2526
|
edges: drawn,
|
|
2500
2527
|
route: this._routeFor(cluster, edges),
|
|
2501
2528
|
src,
|
|
2502
2529
|
op,
|
|
2503
2530
|
alpha,
|
|
2504
|
-
server: () => this._rasterizePolys(this.fillMask,
|
|
2531
|
+
server: () => this._rasterizePolys(this.fillMask, wire, rule),
|
|
2505
2532
|
},
|
|
2506
2533
|
) || painted;
|
|
2507
2534
|
}
|
|
@@ -2864,6 +2891,10 @@ class RenderingContext2d {
|
|
|
2864
2891
|
}
|
|
2865
2892
|
if (w < tris.length) tris.length = w;
|
|
2866
2893
|
if (!tris.length) return;
|
|
2894
|
+
// On the 16.16 grid the wire carries, once, for every route below: the
|
|
2895
|
+
// server rasterizes these numbers, and so does PreciseRasterizer when
|
|
2896
|
+
// the stroke is drawn here (lib/precise.js)
|
|
2897
|
+
for (let i = 0; i < tris.length; i++) tris[i] = snapFixed16(tris[i]);
|
|
2867
2898
|
|
|
2868
2899
|
const op = this._op();
|
|
2869
2900
|
// round-cap/join disks overlap the stroke body; overlapping coverage
|
|
@@ -3138,8 +3169,12 @@ class RenderingContext2d {
|
|
|
3138
3169
|
* relative positions, painted under the glyphs (issue #283). A paragraph
|
|
3139
3170
|
* whose spans change colour draws as several calls, and — as several
|
|
3140
3171
|
* `fillText`s would — casts a shadow per call.
|
|
3172
|
+
*
|
|
3173
|
+
* So does `globalAlpha`, to the glyphs and their shadow both, as it does
|
|
3174
|
+
* to every fill: at 0 nothing is drawn, not even the shadow.
|
|
3141
3175
|
*/
|
|
3142
3176
|
drawGlyphs(op, src, positioned) {
|
|
3177
|
+
if (this.globalAlpha <= 0) return;
|
|
3143
3178
|
const m = this._m;
|
|
3144
3179
|
if (!matIsIdentity(m)) {
|
|
3145
3180
|
positioned = positioned.map((p) => {
|
|
@@ -3164,12 +3199,19 @@ class RenderingContext2d {
|
|
|
3164
3199
|
* instead, intersect that with the clip, and paint the real source
|
|
3165
3200
|
* through the result — the same shape as _fillPolys. A rectangular clip
|
|
3166
3201
|
* takes the server-side fast path below instead of the mask.
|
|
3202
|
+
*
|
|
3203
|
+
* Nor has it a mask slot for `globalAlpha`, which is why text used to be
|
|
3204
|
+
* drawn at full opacity whatever it was. A solid source takes the alpha
|
|
3205
|
+
* in its colour, as `fillRect`'s does, and the glyphs go the way they
|
|
3206
|
+
* always went; a gradient or a pattern has no one colour to fold it
|
|
3207
|
+
* into, and paints through the scratch mask with the alpha in there.
|
|
3167
3208
|
*/
|
|
3168
3209
|
_drawGlyphsDevice(op, src, positioned) {
|
|
3210
|
+
if (this.globalAlpha <= 0) return;
|
|
3169
3211
|
const app = this.window.app;
|
|
3170
|
-
const R = this.Render;
|
|
3171
3212
|
if (!prepareStyle(src, this._m, this.width, this.height)) return;
|
|
3172
|
-
|
|
3213
|
+
const faded = this._fadedSource(src);
|
|
3214
|
+
if (faded && !this._hasPolyClip) {
|
|
3173
3215
|
// Fast path: a rectangular clip is something the server can do itself.
|
|
3174
3216
|
// Two small requests around the ordinary glyph composite, instead of
|
|
3175
3217
|
// clearing and compositing a full-surface a8 mask three times — which
|
|
@@ -3184,77 +3226,37 @@ class RenderingContext2d {
|
|
|
3184
3226
|
drawGlyphRuns(
|
|
3185
3227
|
app,
|
|
3186
3228
|
op,
|
|
3187
|
-
|
|
3229
|
+
faded.id,
|
|
3188
3230
|
this._dst(),
|
|
3189
3231
|
positioned,
|
|
3190
3232
|
rect ?? { x: 0, y: 0, w: this.width, h: this.height },
|
|
3191
|
-
sourceOrigin(
|
|
3233
|
+
sourceOrigin(faded),
|
|
3192
3234
|
);
|
|
3193
3235
|
if (rect) this._invalidatePictureClip();
|
|
3194
3236
|
this._markDirty();
|
|
3195
3237
|
return;
|
|
3196
3238
|
}
|
|
3197
|
-
const
|
|
3198
|
-
this.
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
R.PictOp.
|
|
3202
|
-
this.fillMask.id,
|
|
3203
|
-
[0, 0, 0, 0],
|
|
3204
|
-
[0, 0, this.width, this.height],
|
|
3205
|
-
);
|
|
3206
|
-
// solid white through the glyphs leaves their coverage in the a8 mask
|
|
3207
|
-
drawGlyphRuns(
|
|
3208
|
-
app,
|
|
3209
|
-
R.PictOp.Over,
|
|
3210
|
-
this._glyphSource.id,
|
|
3211
|
-
this.fillMask.id,
|
|
3212
|
-
positioned,
|
|
3213
|
-
{ x: 0, y: 0, w: this.width, h: this.height },
|
|
3214
|
-
);
|
|
3215
|
-
R.Composite(
|
|
3216
|
-
R.PictOp.In,
|
|
3217
|
-
clipMask.id,
|
|
3218
|
-
0,
|
|
3219
|
-
this.fillMask.id,
|
|
3220
|
-
0,
|
|
3221
|
-
0,
|
|
3222
|
-
0,
|
|
3223
|
-
0,
|
|
3224
|
-
0,
|
|
3225
|
-
0,
|
|
3226
|
-
this.width,
|
|
3227
|
-
this.height,
|
|
3228
|
-
);
|
|
3229
|
-
const [ox, oy] = sourceOrigin(src);
|
|
3230
|
-
R.Composite(
|
|
3231
|
-
op,
|
|
3232
|
-
src.id,
|
|
3233
|
-
this.fillMask.id,
|
|
3234
|
-
this._dst(),
|
|
3235
|
-
-ox,
|
|
3236
|
-
-oy,
|
|
3237
|
-
0,
|
|
3238
|
-
0,
|
|
3239
|
-
0,
|
|
3240
|
-
0,
|
|
3241
|
-
this.width,
|
|
3242
|
-
this.height,
|
|
3239
|
+
const R = this.Render;
|
|
3240
|
+
const alpha = faded ? 1 : this.globalAlpha;
|
|
3241
|
+
const surface = { x: 0, y: 0, w: this.width, h: this.height };
|
|
3242
|
+
this._paintThroughScratch(op, faded ?? src, alpha, (white, mask) =>
|
|
3243
|
+
drawGlyphRuns(app, R.PictOp.Over, white, mask, positioned, surface),
|
|
3243
3244
|
);
|
|
3244
|
-
this._markDirty();
|
|
3245
3245
|
}
|
|
3246
3246
|
|
|
3247
3247
|
/**
|
|
3248
3248
|
* Trapezoid coverage under the clip — `drawGlyphs` for vector shapes that
|
|
3249
3249
|
* are already trapezoidized (KaTeX radicals and rules go this way).
|
|
3250
3250
|
* Without it they composite straight to the destination picture and spill
|
|
3251
|
-
* out of clipped boxes, which is what glyphs used to do.
|
|
3251
|
+
* out of clipped boxes, which is what glyphs used to do. `globalAlpha`
|
|
3252
|
+
* applies as it does to glyphs, so a radical fades with the formula.
|
|
3252
3253
|
*/
|
|
3253
3254
|
drawTraps(op, src, traps) {
|
|
3254
|
-
if (!traps.length) return;
|
|
3255
|
+
if (!traps.length || this.globalAlpha <= 0) return;
|
|
3255
3256
|
const app = this.window.app;
|
|
3256
|
-
|
|
3257
|
-
|
|
3257
|
+
if (!prepareStyle(src, this._m, this.width, this.height)) return;
|
|
3258
|
+
const faded = this._fadedSource(src);
|
|
3259
|
+
if (faded && !this._hasPolyClip) {
|
|
3258
3260
|
const rect = this._clipRect();
|
|
3259
3261
|
if (rect) {
|
|
3260
3262
|
if (rect.w === 0 || rect.h === 0) return;
|
|
@@ -3263,18 +3265,57 @@ class RenderingContext2d {
|
|
|
3263
3265
|
compositeTraps(
|
|
3264
3266
|
app,
|
|
3265
3267
|
op,
|
|
3266
|
-
|
|
3268
|
+
faded.id,
|
|
3267
3269
|
this._dst(),
|
|
3268
3270
|
traps,
|
|
3269
3271
|
undefined,
|
|
3270
3272
|
undefined,
|
|
3271
|
-
sourceOrigin(
|
|
3273
|
+
sourceOrigin(faded),
|
|
3272
3274
|
);
|
|
3273
3275
|
if (rect) this._invalidatePictureClip();
|
|
3274
3276
|
this._markDirty();
|
|
3275
3277
|
return;
|
|
3276
3278
|
}
|
|
3277
|
-
const
|
|
3279
|
+
const alpha = faded ? 1 : this.globalAlpha;
|
|
3280
|
+
this._paintThroughScratch(op, faded ?? src, alpha, (white, mask) =>
|
|
3281
|
+
compositeTraps(app, this.Render.PictOp.Over, white, mask, traps),
|
|
3282
|
+
);
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
/**
|
|
3286
|
+
* `src` with `globalAlpha` folded into it: the source itself at full
|
|
3287
|
+
* opacity, and at less, a solid of its colour with all four premultiplied
|
|
3288
|
+
* components scaled — which is compositing it at that opacity, for any op
|
|
3289
|
+
* (`_foldedColor`). Null when there is an alpha to apply and no colour to
|
|
3290
|
+
* fold it into: a gradient, a pattern, a picture of the caller's, or a
|
|
3291
|
+
* solid from a host whose `solidPicture` does not say what it paints with
|
|
3292
|
+
* (App#solidPicture does).
|
|
3293
|
+
*/
|
|
3294
|
+
_fadedSource(src) {
|
|
3295
|
+
const a = this.globalAlpha;
|
|
3296
|
+
if (a >= 1) return src;
|
|
3297
|
+
const c = src._rgba;
|
|
3298
|
+
if (!c) return null;
|
|
3299
|
+
return this.createSolidPicture(c[0] * a, c[1] * a, c[2] * a, c[3] * a);
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
/**
|
|
3303
|
+
* Paint `src` through coverage the server draws into the scratch a8 mask:
|
|
3304
|
+
* how glyphs and trapezoids reach the surface when their own composite
|
|
3305
|
+
* cannot carry what applies to it — a clip that is not a rectangle, or a
|
|
3306
|
+
* `globalAlpha` with no colour to fold into.
|
|
3307
|
+
*
|
|
3308
|
+
* `drawCoverage(white, mask)` composites the coverage into the mask, with
|
|
3309
|
+
* a solid white source: what that leaves in an a8 picture is exactly the
|
|
3310
|
+
* coverage. `alpha` scales it, the clip cuts it — the clip mask for a
|
|
3311
|
+
* stack with a path in it, the picture's clip rectangle for one of
|
|
3312
|
+
* rectangles — and `src` is composited through the result.
|
|
3313
|
+
*/
|
|
3314
|
+
_paintThroughScratch(op, src, alpha, drawCoverage) {
|
|
3315
|
+
const R = this.Render;
|
|
3316
|
+
const rect = this._clipRect();
|
|
3317
|
+
if (rect && (rect.w === 0 || rect.h === 0)) return;
|
|
3318
|
+
const clipMask = this._hasPolyClip ? this._requireClipMask() : null;
|
|
3278
3319
|
this._ensureFillMask();
|
|
3279
3320
|
this._glyphSource ??= this.createSolidPicture(1, 1, 1, 1);
|
|
3280
3321
|
R.FillRectangles(
|
|
@@ -3283,27 +3324,34 @@ class RenderingContext2d {
|
|
|
3283
3324
|
[0, 0, 0, 0],
|
|
3284
3325
|
[0, 0, this.width, this.height],
|
|
3285
3326
|
);
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3327
|
+
drawCoverage(this._glyphSource.id, this.fillMask.id);
|
|
3328
|
+
if (alpha < 1) {
|
|
3329
|
+
// In with a constant color scales the a8 coverage by that alpha
|
|
3330
|
+
R.FillRectangles(
|
|
3331
|
+
R.PictOp.In,
|
|
3332
|
+
this.fillMask.id,
|
|
3333
|
+
[0, 0, 0, alpha],
|
|
3334
|
+
[0, 0, this.width, this.height],
|
|
3335
|
+
);
|
|
3336
|
+
}
|
|
3337
|
+
if (clipMask) {
|
|
3338
|
+
R.Composite(
|
|
3339
|
+
R.PictOp.In,
|
|
3340
|
+
clipMask.id,
|
|
3341
|
+
0,
|
|
3342
|
+
this.fillMask.id,
|
|
3343
|
+
0,
|
|
3344
|
+
0,
|
|
3345
|
+
0,
|
|
3346
|
+
0,
|
|
3347
|
+
0,
|
|
3348
|
+
0,
|
|
3349
|
+
this.width,
|
|
3350
|
+
this.height,
|
|
3351
|
+
);
|
|
3352
|
+
} else if (rect) {
|
|
3353
|
+
this._setPictureClip(rect);
|
|
3354
|
+
}
|
|
3307
3355
|
const [ox, oy] = sourceOrigin(src);
|
|
3308
3356
|
R.Composite(
|
|
3309
3357
|
op,
|
|
@@ -3319,6 +3367,7 @@ class RenderingContext2d {
|
|
|
3319
3367
|
this.width,
|
|
3320
3368
|
this.height,
|
|
3321
3369
|
);
|
|
3370
|
+
if (rect) this._invalidatePictureClip();
|
|
3322
3371
|
this._markDirty();
|
|
3323
3372
|
}
|
|
3324
3373
|
|
|
@@ -4556,7 +4605,8 @@ class RenderingContext2d {
|
|
|
4556
4605
|
return false;
|
|
4557
4606
|
|
|
4558
4607
|
const coverage = rasterizer.rasterize({
|
|
4559
|
-
|
|
4608
|
+
// what the AddTraps fallback in _applyPolyClip would send
|
|
4609
|
+
polys: this._wireShapes(shapes, -bb.x, -bb.y, bb.w, bb.h),
|
|
4560
4610
|
rule: entry.rule,
|
|
4561
4611
|
dx: -bb.x,
|
|
4562
4612
|
dy: -bb.y,
|
|
@@ -4691,7 +4741,8 @@ class RenderingContext2d {
|
|
|
4691
4741
|
*/
|
|
4692
4742
|
fillText(text, x, y) {
|
|
4693
4743
|
text = String(text ?? "");
|
|
4694
|
-
|
|
4744
|
+
// nothing to draw, and at globalAlpha 0 nothing to shape it for
|
|
4745
|
+
if (!text || this.globalAlpha <= 0) return;
|
|
4695
4746
|
if (this._shadowed()) this._shadowOfText(text, x, y);
|
|
4696
4747
|
const style = this._resolvedTextStyle();
|
|
4697
4748
|
const app = this.window.app;
|
|
@@ -5184,14 +5235,19 @@ class RenderingContext2d {
|
|
|
5184
5235
|
* into it. The mask slot is taken by the coverage, so the alpha has to go
|
|
5185
5236
|
* somewhere — and a 1x1 solid is free, where a surface-sized alpha mask is
|
|
5186
5237
|
* not. Falls back to the fill picture for gradients, which have no single
|
|
5187
|
-
* colour to fold into; `_drawCoverage` routes those through the scratch.
|
|
5238
|
+
* colour to fold into; `_drawCoverage` routes those through the scratch.
|
|
5239
|
+
*
|
|
5240
|
+
* All four components are scaled, not the alpha alone: the colour is
|
|
5241
|
+
* premultiplied, and one whose alpha shrank under unchanged components is
|
|
5242
|
+
* brighter than it can be — `#0000ff` at 0.5 over black came out
|
|
5243
|
+
* `#0000ff`, where `fillRect` draws `#000080`. */
|
|
5188
5244
|
_coverageSource() {
|
|
5189
5245
|
const style = this._fillStyle;
|
|
5190
5246
|
if (this.globalAlpha >= 1 || !isPlainColor(style)) {
|
|
5191
5247
|
return this._backgroundPicture;
|
|
5192
5248
|
}
|
|
5193
|
-
const c =
|
|
5194
|
-
return this.createSolidPicture(c[0], c[1], c[2], c[3]
|
|
5249
|
+
const c = this._foldedColor(style);
|
|
5250
|
+
return this.createSolidPicture(c[0], c[1], c[2], c[3]);
|
|
5195
5251
|
}
|
|
5196
5252
|
|
|
5197
5253
|
/**
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Keeps brotli's static dictionary out of every app's startup (issue #427).
|
|
2
|
+
//
|
|
3
|
+
// The fontkit build ntk carries (lib/vendor/fontkit.js) imports brotli's
|
|
4
|
+
// decoder at the top, for WOFF2, and the decoder's last line is `BrotliDictionary.init()`, at module scope.
|
|
5
|
+
// `dec/dictionary.js` requires `dec/dictionary-data.js` to have something to
|
|
6
|
+
// init from: RFC 7932's 122,784-byte dictionary written out as a 756 KB array
|
|
7
|
+
// literal. Compiling and running it is 8 of the 10 ms the decoder takes to
|
|
8
|
+
// load on an M1 Pro — the issue measured the decoder at 16 ms on an i7-7700K
|
|
9
|
+
// — and `import 'ntk'` paid it in every app, including the ones that never
|
|
10
|
+
// open a WOFF2.
|
|
11
|
+
//
|
|
12
|
+
// The decoder reads the dictionary's fields as it decompresses, and nothing
|
|
13
|
+
// else does; `init()` is the one thing done at load. So before fontkit loads,
|
|
14
|
+
// a stand-in for `dec/dictionary.js` goes into `require.cache`: its `init()`
|
|
15
|
+
// does nothing, and the first read of a field — the first WOFF2 decompressed
|
|
16
|
+
// — drops the stand-in from the cache, requires the real module and copies
|
|
17
|
+
// its fields over, so the decoder holds the real values from then on.
|
|
18
|
+
//
|
|
19
|
+
// Why that module and not a shallower one: before ESM evaluates anything, it
|
|
20
|
+
// pre-parses the CommonJS it imports for export names, which leaves loader-
|
|
21
|
+
// owned placeholders in `require.cache` for `decompress.js` and, through
|
|
22
|
+
// what reads as a re-export, `dec/decode.js` — replace one and the import
|
|
23
|
+
// dies with ERR_INTERNAL_ASSERTION. `dec/dictionary.js` is only reached by
|
|
24
|
+
// `require`, while the decoder evaluates, so it is still free when this runs.
|
|
25
|
+
//
|
|
26
|
+
// Each step stands down where it cannot apply: outside Node, in a bundle
|
|
27
|
+
// (which carries its own copy of brotli and never consults `require.cache`),
|
|
28
|
+
// where brotli does not resolve, where something required the
|
|
29
|
+
// dictionary first, and for a brotli other than the 1.3 this was written
|
|
30
|
+
// against. A brotli release that loads the dictionary lazily itself makes
|
|
31
|
+
// this redundant, and the version check retires it.
|
|
32
|
+
//
|
|
33
|
+
// Where the runtime has a Brotli of its own, a WOFF2 ntk opens is not
|
|
34
|
+
// decompressed by this decoder at all (./woff2.js), so with the stand-in in
|
|
35
|
+
// place the dictionary never loads. Without it, it would still load with
|
|
36
|
+
// fontkit, used or not.
|
|
37
|
+
import { builtin, nodeRequire } from '../builtin.js';
|
|
38
|
+
|
|
39
|
+
// What the decoder reads off brotli 1.3's `dec/dictionary.js`.
|
|
40
|
+
const FIELDS = [
|
|
41
|
+
'dictionary',
|
|
42
|
+
'offsetsByLength',
|
|
43
|
+
'sizeBitsByLength',
|
|
44
|
+
'minDictionaryWordLength',
|
|
45
|
+
'maxDictionaryWordLength',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Put the stand-in for fontkit's `brotli/dec/dictionary.js` into
|
|
50
|
+
* `require.cache`, unless it cannot or should not be.
|
|
51
|
+
* @returns {boolean} whether it was installed
|
|
52
|
+
*/
|
|
53
|
+
export function deferBrotliDictionary() {
|
|
54
|
+
try {
|
|
55
|
+
const require = nodeRequire();
|
|
56
|
+
if (!require) return false;
|
|
57
|
+
const { Module } = builtin('node:module');
|
|
58
|
+
// brotli is ntk's own dependency, for the fontkit build in lib/vendor/,
|
|
59
|
+
// so it resolves from here to the copy that build imports
|
|
60
|
+
if (!/^1\.3\./.test(require('brotli/package.json').version)) return false;
|
|
61
|
+
const file = require.resolve('brotli/dec/dictionary.js');
|
|
62
|
+
const cache = require.cache;
|
|
63
|
+
if (cache[file]) return false;
|
|
64
|
+
|
|
65
|
+
const standIn = new Module(file);
|
|
66
|
+
standIn.filename = file;
|
|
67
|
+
standIn.loaded = true;
|
|
68
|
+
const exports = (standIn.exports = { init() {} });
|
|
69
|
+
const load = () => {
|
|
70
|
+
if (cache[file] === standIn) delete cache[file];
|
|
71
|
+
const real = require(file);
|
|
72
|
+
real.init();
|
|
73
|
+
for (const key of Object.keys(real)) {
|
|
74
|
+
Object.defineProperty(exports, key, {
|
|
75
|
+
value: real[key],
|
|
76
|
+
writable: true,
|
|
77
|
+
enumerable: true,
|
|
78
|
+
configurable: true,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return real;
|
|
82
|
+
};
|
|
83
|
+
for (const key of FIELDS) {
|
|
84
|
+
Object.defineProperty(exports, key, {
|
|
85
|
+
get: () => load()[key],
|
|
86
|
+
enumerable: true,
|
|
87
|
+
configurable: true,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
cache[file] = standIn;
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
// no node:module, no import.meta.url (a CommonJS bundle), or a layout
|
|
94
|
+
// that does not resolve: the dictionary loads with fontkit, as before
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
deferBrotliDictionary();
|
package/lib/text/font.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Must stay above fontkit, whose import loads brotli's decoder: this keeps
|
|
2
|
+
// the decoder's static dictionary from loading with it (see the module).
|
|
3
|
+
import './brotlidictionary.js';
|
|
4
|
+
import { builtin } from '../builtin.js';
|
|
3
5
|
import { flatten, rasterizePath } from '../rasterize.js';
|
|
6
|
+
// the windowkit fork's build, which ntk carries (scripts/vendor-fontkit.mjs)
|
|
7
|
+
import * as fontkit from '../vendor/fontkit.js';
|
|
4
8
|
import { NO_ANCHOR, tolerateAnchors, watchAnchors } from './anchors.js';
|
|
5
9
|
import { marksCover, standInMarks } from './marks.js';
|
|
6
10
|
import { pairGlyphs, pairsCover } from './pairs.js';
|
|
7
11
|
import { sfntOf } from './sfnt.js';
|
|
12
|
+
import { opened, useNativeBrotli } from './woff2.js';
|
|
13
|
+
|
|
14
|
+
// a WOFF2 decompresses with the runtime's Brotli where it has one (see the module)
|
|
15
|
+
useNativeBrotli(fontkit);
|
|
8
16
|
|
|
9
17
|
// Axis coordinates are rounded to this many decimals before anything is
|
|
10
18
|
// instantiated or cached. Every distinct coordinate is a font in its own
|
|
@@ -228,16 +236,18 @@ export default class Font {
|
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
static loadSync(path, postscriptName) {
|
|
231
|
-
|
|
239
|
+
// what fontkit's openSync does, which the build ntk carries leaves out:
|
|
240
|
+
// it is the one that imports node:fs
|
|
241
|
+
let fk = fontkit.create(builtin('node:fs').readFileSync(path));
|
|
232
242
|
// select the face of a .ttc/.dfont collection (first face by default);
|
|
233
|
-
// don't pass the name to
|
|
243
|
+
// don't pass the name to create — for single variable fonts it would
|
|
234
244
|
// be misread as a named variation instance
|
|
235
245
|
if (fk.fonts) {
|
|
236
246
|
fk =
|
|
237
247
|
(postscriptName && fk.fonts.find((f) => f.postscriptName === postscriptName)) ||
|
|
238
248
|
fk.fonts[0];
|
|
239
249
|
}
|
|
240
|
-
return new Font(instantiable(fk), path, postscriptName);
|
|
250
|
+
return new Font(instantiable(opened(fk, path)), path, postscriptName);
|
|
241
251
|
}
|
|
242
252
|
|
|
243
253
|
/**
|
|
@@ -258,7 +268,7 @@ export default class Font {
|
|
|
258
268
|
(postscriptName && fk.fonts.find((f) => f.postscriptName === postscriptName)) ||
|
|
259
269
|
fk.fonts[0];
|
|
260
270
|
}
|
|
261
|
-
const font = new Font(instantiable(fk), null, postscriptName);
|
|
271
|
+
const font = new Font(instantiable(opened(fk)), null, postscriptName);
|
|
262
272
|
font.key = key ?? `mem:#${fk.postscriptName || ''}`;
|
|
263
273
|
return font;
|
|
264
274
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// A WOFF2's font data, decompressed by the runtime's own Brotli.
|
|
2
|
+
//
|
|
3
|
+
// A WOFF2 keeps every table in one Brotli stream. The fontkit build ntk
|
|
4
|
+
// carries (lib/vendor/fontkit.js) is the fork's browser build, and it
|
|
5
|
+
// decompresses that with brotli.js, a decoder written in JavaScript. Node,
|
|
6
|
+
// Bun and Deno each carry a native one in `node:zlib` — every Node ntk runs
|
|
7
|
+
// on, and Bun since 1.1.8 — which gives the same bytes about six times as
|
|
8
|
+
// fast: the twenty KaTeX faces and the variable fixture decompress in 3.9 ms
|
|
9
|
+
// instead of 21.4 on an M1 Pro. It also never reaches brotli.js's static
|
|
10
|
+
// dictionary, which ./brotlidictionary.js keeps out of startup so that only a
|
|
11
|
+
// WOFF2 pays for it. On these runtimes, none does.
|
|
12
|
+
//
|
|
13
|
+
// The fork takes a decompressor from outside, `setBrotliDecompressor`
|
|
14
|
+
// (windowkit/fontkit#6), and checks what comes back against the size the
|
|
15
|
+
// table directory gives, as Google's reference decoder does. So this is one
|
|
16
|
+
// function, handed over once: zlib's Brotli where the runtime has one, looked
|
|
17
|
+
// for at the first WOFF2 rather than at import, and the build's own brotli.js
|
|
18
|
+
// where it has none, in a browser.
|
|
19
|
+
//
|
|
20
|
+
// What fontkit does not do is say so when a file is damaged. It decompresses
|
|
21
|
+
// at a WOFF2's first table read, and swallows a table's errors: the table
|
|
22
|
+
// reads as undefined. So a damaged WOFF2 opened as a font with no tables,
|
|
23
|
+
// and failed at the first glyph drawn, with brotli.js's own message. `opened`
|
|
24
|
+
// decompresses it as it is opened, calling fontkit's `_decompress` as
|
|
25
|
+
// ./sfnt.js does, and a damaged file is an error naming it, at the call that
|
|
26
|
+
// named it. Font sources skip a face that throws there (./fontsource.js),
|
|
27
|
+
// and a match moves on to the next candidate (./fontmanager.js).
|
|
28
|
+
import { builtin } from '../builtin.js';
|
|
29
|
+
|
|
30
|
+
// `node:zlib`, or null where it has no Brotli; undefined until first asked
|
|
31
|
+
let zlib;
|
|
32
|
+
|
|
33
|
+
function nativeBrotli() {
|
|
34
|
+
if (zlib === undefined) {
|
|
35
|
+
zlib = null;
|
|
36
|
+
try {
|
|
37
|
+
const z = builtin('node:zlib');
|
|
38
|
+
if (typeof z?.brotliDecompressSync === 'function') zlib = z;
|
|
39
|
+
} catch {
|
|
40
|
+
// no builtins to reach: brotli.js it is
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return zlib;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Test seam: what decompresses a WOFF2 — a `node:zlib`-shaped module, `null`
|
|
48
|
+
* for the build's brotli.js, or `undefined` to look for the runtime's again.
|
|
49
|
+
*/
|
|
50
|
+
export function setNativeBrotli(module) {
|
|
51
|
+
zlib = module;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Hand `fontkit` the runtime's Brotli for WOFF2, keeping the decompressor it
|
|
56
|
+
* had for where there is none.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} fontkit the build ntk carries
|
|
59
|
+
*/
|
|
60
|
+
export function useNativeBrotli(fontkit) {
|
|
61
|
+
const own = fontkit.setBrotliDecompressor(decompress);
|
|
62
|
+
|
|
63
|
+
function decompress(buffer, size) {
|
|
64
|
+
const z = nativeBrotli();
|
|
65
|
+
if (!z) return own(buffer, size);
|
|
66
|
+
let bytes;
|
|
67
|
+
try {
|
|
68
|
+
bytes = z.brotliDecompressSync(buffer, { maxOutputLength: Math.max(size, 1) });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err?.code !== 'ERR_BUFFER_TOO_LARGE') throw err;
|
|
71
|
+
throw new RangeError(`it holds more than the ${size} bytes its table directory gives`, {
|
|
72
|
+
cause: err,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// A plain Uint8Array, as brotli.js returns, not zlib's Buffer: a Buffer's
|
|
76
|
+
// slice() is a view, and what is read out of this expects a copy —
|
|
77
|
+
// ./sfnt.js rewrites `head` in the copy it slices.
|
|
78
|
+
return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.length);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `fk`, with its font data decompressed now if it is a WOFF2, so that a
|
|
84
|
+
* damaged file is an error naming it rather than a font with no tables.
|
|
85
|
+
*
|
|
86
|
+
* @param {object} fk a fontkit font
|
|
87
|
+
* @param {string} [where] the file it was read from
|
|
88
|
+
* @returns {object} `fk`
|
|
89
|
+
*/
|
|
90
|
+
export function opened(fk, where) {
|
|
91
|
+
if (fk?.type !== 'WOFF2' || typeof fk._decompress !== 'function') return fk;
|
|
92
|
+
try {
|
|
93
|
+
fk._decompress();
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`${where ? `${where}: ` : ''}${err.message} — the file is damaged or cut short; ` +
|
|
97
|
+
'browsers reject it too.',
|
|
98
|
+
{ cause: err }
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return fk;
|
|
102
|
+
}
|