@turntrout/subfont 1.4.0 → 1.5.0
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/collectTextsByPage.js +1 -1
- package/lib/subsetFontWithGlyphs.js +23 -6
- package/lib/subsetFonts.js +8 -0
- package/lib/subsetGeneration.js +101 -73
- package/package.json +1 -1
|
@@ -20,7 +20,7 @@ const {
|
|
|
20
20
|
} = require('./fontFaceHelpers');
|
|
21
21
|
|
|
22
22
|
const fontRelevantCssRegex =
|
|
23
|
-
/font-family|font-weight|font-style|font-stretch|font-display
|
|
23
|
+
/font-family|font-weight|font-style|font-stretch|font-display|@font-face|font-variation|font-feature/i;
|
|
24
24
|
|
|
25
25
|
// font-tracer defaults omit font-size. We need it for opsz axis mapping.
|
|
26
26
|
const PROPS_TO_RETURN = [
|
|
@@ -29,7 +29,7 @@ function compileModule() {
|
|
|
29
29
|
|
|
30
30
|
const _pool = []; // Array of { exports, busy: boolean }
|
|
31
31
|
let _poolReady;
|
|
32
|
-
const POOL_SIZE = Math.min(os.cpus().length, 8);
|
|
32
|
+
const POOL_SIZE = Math.max(1, Math.min(os.cpus().length, 8));
|
|
33
33
|
|
|
34
34
|
async function initPool() {
|
|
35
35
|
if (!_poolReady) {
|
|
@@ -73,9 +73,15 @@ function releaseInstance(inst) {
|
|
|
73
73
|
|
|
74
74
|
// Serialize fontverter.convert calls — the wawoff2 module (used internally by
|
|
75
75
|
// fontverter for WOFF2 compression) has a shared WASM instance whose memory
|
|
76
|
-
// is corrupted by concurrent calls.
|
|
76
|
+
// is corrupted by concurrent calls. Only woff2 paths need this; woff and
|
|
77
|
+
// truetype conversions use separate synchronous/JS libraries that are safe
|
|
78
|
+
// to call in parallel.
|
|
77
79
|
const convertLimiter = require('p-limit')(1);
|
|
78
80
|
|
|
81
|
+
function usesWawoff2(buffer) {
|
|
82
|
+
return buffer.length >= 4 && buffer.toString('ascii', 0, 4) === 'wOF2';
|
|
83
|
+
}
|
|
84
|
+
|
|
79
85
|
// Re-create on every call — WASM memory.buffer is detached when memory grows,
|
|
80
86
|
// so a cached Uint8Array would silently read/write stale data.
|
|
81
87
|
function getHeapu8(exports) {
|
|
@@ -224,7 +230,11 @@ async function subsetFontWithGlyphs(
|
|
|
224
230
|
) {
|
|
225
231
|
// Reuse cached sfnt conversion when available (same buffer may have
|
|
226
232
|
// been converted by getFontInfo or collectFeatureGlyphIds already).
|
|
227
|
-
|
|
233
|
+
// Serialize through convertLimiter when the source is woff2 — toSfnt
|
|
234
|
+
// would call wawoff2.decompress which isn't concurrency-safe.
|
|
235
|
+
const ttf = usesWawoff2(originalFont)
|
|
236
|
+
? await convertLimiter(() => toSfnt(originalFont))
|
|
237
|
+
: await toSfnt(originalFont);
|
|
228
238
|
|
|
229
239
|
const inst = await acquireInstance();
|
|
230
240
|
const { exports } = inst;
|
|
@@ -269,12 +279,19 @@ async function subsetFontWithGlyphs(
|
|
|
269
279
|
released = true;
|
|
270
280
|
releaseInstance(inst);
|
|
271
281
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
282
|
+
// Only serialize through convertLimiter when targeting woff2 —
|
|
283
|
+
// woff and truetype conversions don't use wawoff2.
|
|
284
|
+
return targetFormat === 'woff2'
|
|
285
|
+
? convertLimiter(() =>
|
|
286
|
+
fontverter.convert(subsetFont, targetFormat, 'truetype')
|
|
287
|
+
)
|
|
288
|
+
: fontverter.convert(subsetFont, targetFormat, 'truetype');
|
|
275
289
|
} finally {
|
|
276
290
|
if (!released) releaseInstance(inst);
|
|
277
291
|
}
|
|
278
292
|
}
|
|
279
293
|
|
|
294
|
+
// Pre-warm the WASM pool: call early to overlap compilation with other work.
|
|
295
|
+
subsetFontWithGlyphs.warmup = () => initPool();
|
|
296
|
+
|
|
280
297
|
module.exports = subsetFontWithGlyphs;
|
package/lib/subsetFonts.js
CHANGED
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
} = require('./fontFaceHelpers');
|
|
29
29
|
const { getVariationAxisUsage } = require('./variationAxes');
|
|
30
30
|
const { getSubsetsForFontUsage } = require('./subsetGeneration');
|
|
31
|
+
const subsetFontWithGlyphs = require('./subsetFontWithGlyphs');
|
|
31
32
|
|
|
32
33
|
const googleFontsCssUrlRegex = /^(?:https?:)?\/\/fonts\.googleapis\.com\/css/;
|
|
33
34
|
|
|
@@ -385,6 +386,13 @@ async function subsetFonts(
|
|
|
385
386
|
fontDisplay = undefined;
|
|
386
387
|
}
|
|
387
388
|
|
|
389
|
+
// Pre-warm the WASM pool: start compiling harfbuzz WASM while
|
|
390
|
+
// collectTextsByPage traces fonts. Compilation (~50-200ms) overlaps
|
|
391
|
+
// with tracing work rather than appearing on the critical path.
|
|
392
|
+
// Catch silently — the error will surface when subsetFontWithGlyphs
|
|
393
|
+
// is actually called, where it's properly handled.
|
|
394
|
+
subsetFontWithGlyphs.warmup().catch(() => {});
|
|
395
|
+
|
|
388
396
|
const subsetUrl = urltools.ensureTrailingSlash(assetGraph.root + subsetPath);
|
|
389
397
|
|
|
390
398
|
const timings = {};
|
package/lib/subsetGeneration.js
CHANGED
|
@@ -9,6 +9,20 @@ const subsetFontWithGlyphs = require('./subsetFontWithGlyphs');
|
|
|
9
9
|
// entries (e.g. after adding hinting removal or table stripping).
|
|
10
10
|
const SUBSET_CACHE_VERSION = '2';
|
|
11
11
|
|
|
12
|
+
// Cache the SHA-256 hash state after feeding SUBSET_CACHE_VERSION + fontBuffer.
|
|
13
|
+
// For a font with 2 target formats this halves the hashing work on large buffers.
|
|
14
|
+
// Uses WeakMap so entries are garbage-collected when the buffer is released.
|
|
15
|
+
const fontBufferHashPrefixes = new WeakMap();
|
|
16
|
+
function getFontBufferHashPrefix(fontBuffer) {
|
|
17
|
+
if (!fontBufferHashPrefixes.has(fontBuffer)) {
|
|
18
|
+
const hash = crypto.createHash('sha256');
|
|
19
|
+
hash.update(SUBSET_CACHE_VERSION);
|
|
20
|
+
hash.update(fontBuffer);
|
|
21
|
+
fontBufferHashPrefixes.set(fontBuffer, hash);
|
|
22
|
+
}
|
|
23
|
+
return fontBufferHashPrefixes.get(fontBuffer);
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
function subsetCacheKey(
|
|
13
27
|
fontBuffer,
|
|
14
28
|
text,
|
|
@@ -16,9 +30,10 @@ function subsetCacheKey(
|
|
|
16
30
|
variationAxes,
|
|
17
31
|
featureGlyphIds
|
|
18
32
|
) {
|
|
19
|
-
|
|
20
|
-
hash.
|
|
21
|
-
|
|
33
|
+
// Clone the pre-computed prefix (version + font buffer) and append
|
|
34
|
+
// the remaining fields. hash.copy() is O(1) — just copies the
|
|
35
|
+
// internal digest state, avoiding re-hashing the entire font buffer.
|
|
36
|
+
const hash = getFontBufferHashPrefix(fontBuffer).copy();
|
|
22
37
|
hash.update(text);
|
|
23
38
|
hash.update(targetFormat);
|
|
24
39
|
if (variationAxes) hash.update(JSON.stringify(variationAxes));
|
|
@@ -163,85 +178,98 @@ async function getSubsetsForFontUsage(
|
|
|
163
178
|
const subsetPromiseMap = new Map();
|
|
164
179
|
const subsetInfoByFontUrl = new Map();
|
|
165
180
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
181
|
+
// Process fonts concurrently — each font's feature glyph collection
|
|
182
|
+
// and subset queuing run in parallel, so fonts without feature settings
|
|
183
|
+
// don't wait behind fonts that need collectFeatureGlyphIds.
|
|
184
|
+
await Promise.all(
|
|
185
|
+
[...canonicalFontUsageByUrl].map(async ([fontUrl, fontUsage]) => {
|
|
186
|
+
const fontBuffer = originalFontBuffers.get(fontUrl);
|
|
187
|
+
if (!fontBuffer) return;
|
|
188
|
+
const text = fontUsage.text;
|
|
170
189
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
190
|
+
const bounds = variationAxisBoundsCache.get(fontUrl);
|
|
191
|
+
const subsetInfo = bounds
|
|
192
|
+
? {
|
|
193
|
+
variationAxes: bounds.variationAxes,
|
|
194
|
+
fullyInstanced: bounds.fullyInstanced,
|
|
195
|
+
numAxesPinned: bounds.numAxesPinned,
|
|
196
|
+
numAxesReduced: bounds.numAxesReduced,
|
|
197
|
+
}
|
|
198
|
+
: {
|
|
199
|
+
variationAxes: undefined,
|
|
200
|
+
fullyInstanced: false,
|
|
201
|
+
numAxesPinned: 0,
|
|
202
|
+
numAxesReduced: 0,
|
|
203
|
+
};
|
|
204
|
+
subsetInfoByFontUrl.set(fontUrl, subsetInfo);
|
|
205
|
+
|
|
206
|
+
let featureGlyphIds;
|
|
207
|
+
if (fontUsage.hasFontFeatureSettings && fontBuffer) {
|
|
208
|
+
try {
|
|
209
|
+
featureGlyphIds = await collectFeatureGlyphIds(
|
|
210
|
+
fontBuffer,
|
|
211
|
+
text,
|
|
212
|
+
fontUsage.fontFeatureTags
|
|
213
|
+
);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
// Feature glyph collection failed — continue without feature
|
|
216
|
+
// glyphs rather than blocking all fonts (Promise.all would
|
|
217
|
+
// reject entirely if this propagated).
|
|
218
|
+
err.asset = err.asset || fontAssetsByUrl.get(fontUrl);
|
|
219
|
+
assetGraph.warn(err);
|
|
178
220
|
}
|
|
179
|
-
|
|
180
|
-
variationAxes: undefined,
|
|
181
|
-
fullyInstanced: false,
|
|
182
|
-
numAxesPinned: 0,
|
|
183
|
-
numAxesReduced: 0,
|
|
184
|
-
};
|
|
185
|
-
subsetInfoByFontUrl.set(fontUrl, subsetInfo);
|
|
221
|
+
}
|
|
186
222
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
);
|
|
194
|
-
}
|
|
223
|
+
for (const targetFormat of formats) {
|
|
224
|
+
const promiseId = getSubsetPromiseId(
|
|
225
|
+
fontUsage,
|
|
226
|
+
targetFormat,
|
|
227
|
+
subsetInfo.variationAxes
|
|
228
|
+
);
|
|
195
229
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
230
|
+
if (!subsetPromiseMap.has(promiseId)) {
|
|
231
|
+
const cacheKey = diskCache
|
|
232
|
+
? subsetCacheKey(
|
|
233
|
+
fontBuffer,
|
|
234
|
+
text,
|
|
235
|
+
targetFormat,
|
|
236
|
+
subsetInfo.variationAxes,
|
|
237
|
+
featureGlyphIds
|
|
238
|
+
)
|
|
239
|
+
: null;
|
|
240
|
+
const cachedResult = diskCache ? await diskCache.get(cacheKey) : null;
|
|
202
241
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
text,
|
|
242
|
+
if (cachedResult) {
|
|
243
|
+
subsetPromiseMap.set(promiseId, Promise.resolve(cachedResult));
|
|
244
|
+
} else {
|
|
245
|
+
const subsetCall = subsetFontWithGlyphs(fontBuffer, text, {
|
|
208
246
|
targetFormat,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
)
|
|
212
|
-
: null;
|
|
213
|
-
const cachedResult = diskCache ? await diskCache.get(cacheKey) : null;
|
|
247
|
+
glyphIds: featureGlyphIds,
|
|
248
|
+
variationAxes: subsetInfo.variationAxes,
|
|
249
|
+
});
|
|
214
250
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
return result;
|
|
235
|
-
})
|
|
236
|
-
.catch((err) => {
|
|
237
|
-
err.asset = err.asset || fontAssetsByUrl.get(fontUrl);
|
|
238
|
-
assetGraph.warn(err);
|
|
239
|
-
})
|
|
240
|
-
);
|
|
251
|
+
subsetPromiseMap.set(
|
|
252
|
+
promiseId,
|
|
253
|
+
subsetCall
|
|
254
|
+
.then(async (result) => {
|
|
255
|
+
if (diskCache && result) {
|
|
256
|
+
// Fire-and-forget: cache writes are best-effort.
|
|
257
|
+
// Errors are handled inside set(); the catch is a
|
|
258
|
+
// safety net against unhandled rejections.
|
|
259
|
+
diskCache.set(cacheKey, result).catch(() => {});
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
})
|
|
263
|
+
.catch((err) => {
|
|
264
|
+
err.asset = err.asset || fontAssetsByUrl.get(fontUrl);
|
|
265
|
+
assetGraph.warn(err);
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
}
|
|
241
269
|
}
|
|
242
270
|
}
|
|
243
|
-
}
|
|
244
|
-
|
|
271
|
+
})
|
|
272
|
+
);
|
|
245
273
|
|
|
246
274
|
// Await all subset promises
|
|
247
275
|
const resolvedSubsets = new Map(
|
package/package.json
CHANGED