@turntrout/subfont 1.7.0 → 1.7.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/FontTracerPool.d.ts +37 -0
- package/lib/FontTracerPool.d.ts.map +1 -0
- package/lib/FontTracerPool.js +210 -219
- package/lib/FontTracerPool.js.map +1 -0
- package/lib/cli.d.ts +3 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +15 -12
- package/lib/cli.js.map +1 -0
- package/lib/subfont.d.ts +33 -0
- package/lib/subfont.d.ts.map +1 -0
- package/lib/subfont.js +530 -629
- package/lib/subfont.js.map +1 -0
- package/lib/subsetFontWithGlyphs.d.ts +17 -0
- package/lib/subsetFontWithGlyphs.d.ts.map +1 -0
- package/lib/subsetFontWithGlyphs.js +223 -256
- package/lib/subsetFontWithGlyphs.js.map +1 -0
- package/lib/subsetFonts.d.ts +59 -0
- package/lib/subsetFonts.d.ts.map +1 -0
- package/lib/subsetFonts.js +898 -1200
- package/lib/subsetFonts.js.map +1 -0
- package/lib/subsetGeneration.d.ts +39 -0
- package/lib/subsetGeneration.d.ts.map +1 -0
- package/lib/subsetGeneration.js +294 -324
- package/lib/subsetGeneration.js.map +1 -0
- package/package.json +11 -2
package/lib/subsetGeneration.js
CHANGED
|
@@ -1,352 +1,322 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports._SubsetDiskCache = void 0;
|
|
37
|
+
exports.getSubsetPromiseId = getSubsetPromiseId;
|
|
38
|
+
exports.getSubsetsForFontUsage = getSubsetsForFontUsage;
|
|
39
|
+
exports._subsetCacheKey = subsetCacheKey;
|
|
40
|
+
const fs = __importStar(require("fs/promises"));
|
|
41
|
+
const pathModule = require("path");
|
|
42
|
+
const crypto = __importStar(require("crypto"));
|
|
43
|
+
const variationAxes_1 = require("./variationAxes");
|
|
44
|
+
const collectFeatureGlyphIds = require("./collectFeatureGlyphIds");
|
|
45
|
+
const subsetFontWithGlyphs = require("./subsetFontWithGlyphs");
|
|
8
46
|
// Bump when subsetting behaviour changes to invalidate stale disk-cache
|
|
9
47
|
// entries (e.g. after adding hinting removal or table stripping).
|
|
10
48
|
const SUBSET_CACHE_VERSION = '2';
|
|
11
|
-
|
|
12
49
|
// Cache the SHA-256 hash state after feeding SUBSET_CACHE_VERSION + fontBuffer.
|
|
13
50
|
// For a font with 2 target formats this halves the hashing work on large buffers.
|
|
14
51
|
// Uses WeakMap so entries are garbage-collected when the buffer is released.
|
|
15
52
|
const fontBufferHashPrefixes = new WeakMap();
|
|
16
53
|
function getFontBufferHashPrefix(fontBuffer) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
54
|
+
let cached = fontBufferHashPrefixes.get(fontBuffer);
|
|
55
|
+
if (!cached) {
|
|
56
|
+
cached = crypto.createHash('sha256');
|
|
57
|
+
cached.update(SUBSET_CACHE_VERSION);
|
|
58
|
+
cached.update(fontBuffer);
|
|
59
|
+
fontBufferHashPrefixes.set(fontBuffer, cached);
|
|
60
|
+
}
|
|
61
|
+
return cached;
|
|
24
62
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
hash.update(text);
|
|
38
|
-
hash.update(targetFormat);
|
|
39
|
-
if (variationAxes) hash.update(JSON.stringify(variationAxes));
|
|
40
|
-
if (featureGlyphIds) hash.update(JSON.stringify(featureGlyphIds));
|
|
41
|
-
return hash.digest('hex');
|
|
63
|
+
function subsetCacheKey(fontBuffer, text, targetFormat, variationAxes, featureGlyphIds) {
|
|
64
|
+
// Clone the pre-computed prefix (version + font buffer) and append
|
|
65
|
+
// the remaining fields. hash.copy() is O(1) — just copies the
|
|
66
|
+
// internal digest state, avoiding re-hashing the entire font buffer.
|
|
67
|
+
const hash = getFontBufferHashPrefix(fontBuffer).copy();
|
|
68
|
+
hash.update(text);
|
|
69
|
+
hash.update(targetFormat);
|
|
70
|
+
if (variationAxes)
|
|
71
|
+
hash.update(JSON.stringify(variationAxes));
|
|
72
|
+
if (featureGlyphIds)
|
|
73
|
+
hash.update(JSON.stringify(featureGlyphIds));
|
|
74
|
+
return hash.digest('hex');
|
|
42
75
|
}
|
|
43
|
-
|
|
44
76
|
class SubsetDiskCache {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
_cacheDir;
|
|
78
|
+
_console;
|
|
79
|
+
_ensured;
|
|
80
|
+
_warnedWrite;
|
|
81
|
+
constructor(cacheDir, console) {
|
|
82
|
+
this._cacheDir = cacheDir;
|
|
83
|
+
this._console = console ?? null;
|
|
84
|
+
this._ensured = false;
|
|
85
|
+
this._warnedWrite = false;
|
|
86
|
+
}
|
|
87
|
+
async _ensureDir() {
|
|
88
|
+
if (!this._ensured) {
|
|
89
|
+
// Only attempt once — persistent failures (bad path, permissions)
|
|
90
|
+
// are far more common than transient ones, and retrying just
|
|
91
|
+
// produces repeated warnings.
|
|
92
|
+
this._ensured = true;
|
|
93
|
+
try {
|
|
94
|
+
await fs.mkdir(this._cacheDir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (this._console) {
|
|
98
|
+
this._console.warn(`subfont: cache directory ${this._cacheDir} could not be created: ${err.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
65
101
|
}
|
|
66
|
-
}
|
|
67
102
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
103
|
+
async get(key) {
|
|
104
|
+
const filePath = pathModule.join(this._cacheDir, key);
|
|
105
|
+
try {
|
|
106
|
+
return await fs.readFile(filePath);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
76
111
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
await this._ensureDir();
|
|
81
|
-
const filePath = pathModule.join(this._cacheDir, key);
|
|
82
|
-
try {
|
|
83
|
-
await fs.writeFile(filePath, buffer);
|
|
84
|
-
} catch (err) {
|
|
85
|
-
// If the directory was removed after init, retry once
|
|
86
|
-
if (err.code === 'ENOENT') {
|
|
112
|
+
async set(key, buffer) {
|
|
113
|
+
await this._ensureDir();
|
|
114
|
+
const filePath = pathModule.join(this._cacheDir, key);
|
|
87
115
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
await fs.writeFile(filePath, buffer);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const errno = err;
|
|
120
|
+
// If the directory was removed after init, retry once
|
|
121
|
+
if (errno.code === 'ENOENT') {
|
|
122
|
+
try {
|
|
123
|
+
await fs.mkdir(this._cacheDir, { recursive: true });
|
|
124
|
+
await fs.writeFile(filePath, buffer);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Fall through to warning below
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (this._warnedWrite)
|
|
132
|
+
return;
|
|
133
|
+
this._warnedWrite = true;
|
|
134
|
+
if (this._console) {
|
|
135
|
+
this._console.warn(`subfont: failed to write cache entry ${key}: ${errno.message}`);
|
|
136
|
+
}
|
|
93
137
|
}
|
|
94
|
-
}
|
|
95
|
-
if (this._warnedWrite) return;
|
|
96
|
-
this._warnedWrite = true;
|
|
97
|
-
if (this._console) {
|
|
98
|
-
this._console.warn(
|
|
99
|
-
`subfont: failed to write cache entry ${key}: ${err.message}`
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
138
|
}
|
|
103
|
-
}
|
|
104
139
|
}
|
|
105
|
-
|
|
140
|
+
exports._SubsetDiskCache = SubsetDiskCache;
|
|
106
141
|
function getSubsetPromiseId(fontUsage, format, variationAxes = null) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
142
|
+
return [
|
|
143
|
+
fontUsage.text,
|
|
144
|
+
fontUsage.fontUrl,
|
|
145
|
+
format,
|
|
146
|
+
JSON.stringify(variationAxes),
|
|
147
|
+
].join('\x1d');
|
|
113
148
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
const cacheStats = diskCache ? { hits: 0, misses: 0 } : null;
|
|
126
|
-
|
|
127
|
-
// Collect one canonical fontUsage per font URL
|
|
128
|
-
const canonicalFontUsageByUrl = new Map();
|
|
129
|
-
for (const item of htmlOrSvgAssetTextsWithProps) {
|
|
130
|
-
for (const fontUsage of item.fontUsages) {
|
|
131
|
-
if (
|
|
132
|
-
fontUsage.fontUrl &&
|
|
133
|
-
!canonicalFontUsageByUrl.has(fontUsage.fontUrl)
|
|
134
|
-
) {
|
|
135
|
-
canonicalFontUsageByUrl.set(fontUsage.fontUrl, fontUsage);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const allFontUrls = [...canonicalFontUsageByUrl.keys()];
|
|
141
|
-
|
|
142
|
-
// Load font assets
|
|
143
|
-
await assetGraph.populate({
|
|
144
|
-
followRelations: {
|
|
145
|
-
to: { url: { $or: allFontUrls } },
|
|
146
|
-
},
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
const fontAssetsByUrl = new Map();
|
|
150
|
-
const originalFontBuffers = new Map();
|
|
151
|
-
for (const fontUrl of allFontUrls) {
|
|
152
|
-
const fontAsset = assetGraph.findAssets({
|
|
153
|
-
url: fontUrl,
|
|
154
|
-
isLoaded: true,
|
|
155
|
-
})[0];
|
|
156
|
-
if (fontAsset) {
|
|
157
|
-
fontAssetsByUrl.set(fontUrl, fontAsset);
|
|
158
|
-
originalFontBuffers.set(fontUrl, fontAsset.rawSrc);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Compute variation axis bounds for all fonts in parallel
|
|
163
|
-
const fontUrlsWithAssets = allFontUrls.filter((url) =>
|
|
164
|
-
fontAssetsByUrl.has(url)
|
|
165
|
-
);
|
|
166
|
-
const boundsResults = await Promise.all(
|
|
167
|
-
fontUrlsWithAssets.map((fontUrl) =>
|
|
168
|
-
getVariationAxisBounds(
|
|
169
|
-
fontAssetsByUrl,
|
|
170
|
-
fontUrl,
|
|
171
|
-
seenAxisValuesByFontUrlAndAxisName
|
|
172
|
-
)
|
|
173
|
-
)
|
|
174
|
-
);
|
|
175
|
-
const variationAxisBoundsCache = new Map();
|
|
176
|
-
for (let i = 0; i < fontUrlsWithAssets.length; i++) {
|
|
177
|
-
variationAxisBoundsCache.set(fontUrlsWithAssets[i], boundsResults[i]);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const subsetPromiseMap = new Map();
|
|
181
|
-
const subsetInfoByFontUrl = new Map();
|
|
182
|
-
|
|
183
|
-
// Process fonts concurrently — each font's feature glyph collection
|
|
184
|
-
// and subset queuing run in parallel, so fonts without feature settings
|
|
185
|
-
// don't wait behind fonts that need collectFeatureGlyphIds.
|
|
186
|
-
await Promise.all(
|
|
187
|
-
[...canonicalFontUsageByUrl].map(async ([fontUrl, fontUsage]) => {
|
|
188
|
-
const fontBuffer = originalFontBuffers.get(fontUrl);
|
|
189
|
-
if (!fontBuffer) return;
|
|
190
|
-
const text = fontUsage.text;
|
|
191
|
-
|
|
192
|
-
const bounds = variationAxisBoundsCache.get(fontUrl);
|
|
193
|
-
const subsetInfo = bounds
|
|
194
|
-
? {
|
|
195
|
-
variationAxes: bounds.variationAxes,
|
|
196
|
-
fullyInstanced: bounds.fullyInstanced,
|
|
197
|
-
numAxesPinned: bounds.numAxesPinned,
|
|
198
|
-
numAxesReduced: bounds.numAxesReduced,
|
|
199
|
-
}
|
|
200
|
-
: {
|
|
201
|
-
variationAxes: undefined,
|
|
202
|
-
fullyInstanced: false,
|
|
203
|
-
numAxesPinned: 0,
|
|
204
|
-
numAxesReduced: 0,
|
|
205
|
-
};
|
|
206
|
-
subsetInfoByFontUrl.set(fontUrl, subsetInfo);
|
|
207
|
-
|
|
208
|
-
let featureGlyphIds;
|
|
209
|
-
if (fontUsage.hasFontFeatureSettings && fontBuffer) {
|
|
210
|
-
try {
|
|
211
|
-
featureGlyphIds = await collectFeatureGlyphIds(
|
|
212
|
-
fontBuffer,
|
|
213
|
-
text,
|
|
214
|
-
fontUsage.fontFeatureTags
|
|
215
|
-
);
|
|
216
|
-
} catch (err) {
|
|
217
|
-
// Feature glyph collection failed — continue without feature
|
|
218
|
-
// glyphs rather than blocking all fonts (Promise.all would
|
|
219
|
-
// reject entirely if this propagated).
|
|
220
|
-
err.asset = err.asset || fontAssetsByUrl.get(fontUrl);
|
|
221
|
-
assetGraph.warn(err);
|
|
149
|
+
async function getSubsetsForFontUsage(assetGraph, htmlOrSvgAssetTextsWithProps, formats, seenAxisValuesByFontUrlAndAxisName, cacheDir = null, console = null, debug = false) {
|
|
150
|
+
const diskCache = cacheDir ? new SubsetDiskCache(cacheDir, console) : null;
|
|
151
|
+
const cacheStats = diskCache ? { hits: 0, misses: 0 } : null;
|
|
152
|
+
// Collect one canonical fontUsage per font URL
|
|
153
|
+
const canonicalFontUsageByUrl = new Map();
|
|
154
|
+
for (const item of htmlOrSvgAssetTextsWithProps) {
|
|
155
|
+
for (const fontUsage of item.fontUsages) {
|
|
156
|
+
if (fontUsage.fontUrl &&
|
|
157
|
+
!canonicalFontUsageByUrl.has(fontUsage.fontUrl)) {
|
|
158
|
+
canonicalFontUsageByUrl.set(fontUsage.fontUrl, fontUsage);
|
|
159
|
+
}
|
|
222
160
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
: null;
|
|
242
|
-
const cachedResult = diskCache ? await diskCache.get(cacheKey) : null;
|
|
243
|
-
|
|
244
|
-
if (cachedResult) {
|
|
245
|
-
if (cacheStats) cacheStats.hits++;
|
|
246
|
-
subsetPromiseMap.set(promiseId, Promise.resolve(cachedResult));
|
|
247
|
-
} else {
|
|
248
|
-
if (cacheStats) cacheStats.misses++;
|
|
249
|
-
const subsetCall = subsetFontWithGlyphs(fontBuffer, text, {
|
|
250
|
-
targetFormat,
|
|
251
|
-
glyphIds: featureGlyphIds,
|
|
252
|
-
variationAxes: subsetInfo.variationAxes,
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
subsetPromiseMap.set(
|
|
256
|
-
promiseId,
|
|
257
|
-
subsetCall
|
|
258
|
-
.then(async (result) => {
|
|
259
|
-
if (diskCache && result) {
|
|
260
|
-
// Fire-and-forget: cache writes are best-effort.
|
|
261
|
-
// Errors are handled inside set(); the catch is a
|
|
262
|
-
// safety net against unhandled rejections.
|
|
263
|
-
diskCache.set(cacheKey, result).catch(() => {});
|
|
264
|
-
}
|
|
265
|
-
return result;
|
|
266
|
-
})
|
|
267
|
-
.catch((err) => {
|
|
268
|
-
err.asset = err.asset || fontAssetsByUrl.get(fontUrl);
|
|
269
|
-
assetGraph.warn(err);
|
|
270
|
-
return null;
|
|
271
|
-
})
|
|
272
|
-
);
|
|
273
|
-
}
|
|
161
|
+
}
|
|
162
|
+
const allFontUrls = [...canonicalFontUsageByUrl.keys()];
|
|
163
|
+
// Load font assets
|
|
164
|
+
await assetGraph.populate({
|
|
165
|
+
followRelations: {
|
|
166
|
+
to: { url: { $or: allFontUrls } },
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
const fontAssetsByUrl = new Map();
|
|
170
|
+
const originalFontBuffers = new Map();
|
|
171
|
+
for (const fontUrl of allFontUrls) {
|
|
172
|
+
const fontAsset = assetGraph.findAssets({
|
|
173
|
+
url: fontUrl,
|
|
174
|
+
isLoaded: true,
|
|
175
|
+
})[0];
|
|
176
|
+
if (fontAsset) {
|
|
177
|
+
fontAssetsByUrl.set(fontUrl, fontAsset);
|
|
178
|
+
originalFontBuffers.set(fontUrl, fontAsset.rawSrc);
|
|
274
179
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
180
|
+
}
|
|
181
|
+
// Compute variation axis bounds for all fonts in parallel
|
|
182
|
+
const fontUrlsWithAssets = allFontUrls.filter((url) => fontAssetsByUrl.has(url));
|
|
183
|
+
const boundsResults = await Promise.all(fontUrlsWithAssets.map((fontUrl) => (0, variationAxes_1.getVariationAxisBounds)(fontAssetsByUrl, fontUrl, seenAxisValuesByFontUrlAndAxisName)));
|
|
184
|
+
const variationAxisBoundsCache = new Map();
|
|
185
|
+
for (let i = 0; i < fontUrlsWithAssets.length; i++) {
|
|
186
|
+
variationAxisBoundsCache.set(fontUrlsWithAssets[i], boundsResults[i]);
|
|
187
|
+
}
|
|
188
|
+
const subsetPromiseMap = new Map();
|
|
189
|
+
const subsetInfoByFontUrl = new Map();
|
|
190
|
+
// Process fonts concurrently — each font's feature glyph collection
|
|
191
|
+
// and subset queuing run in parallel, so fonts without feature settings
|
|
192
|
+
// don't wait behind fonts that need collectFeatureGlyphIds.
|
|
193
|
+
await Promise.all([...canonicalFontUsageByUrl].map(async ([fontUrl, fontUsage]) => {
|
|
194
|
+
const fontBuffer = originalFontBuffers.get(fontUrl);
|
|
195
|
+
if (!fontBuffer)
|
|
196
|
+
return;
|
|
197
|
+
const text = fontUsage.text;
|
|
198
|
+
const bounds = variationAxisBoundsCache.get(fontUrl);
|
|
199
|
+
const subsetInfo = bounds
|
|
200
|
+
? {
|
|
201
|
+
variationAxes: bounds.variationAxes,
|
|
202
|
+
fullyInstanced: bounds.fullyInstanced,
|
|
203
|
+
numAxesPinned: bounds.numAxesPinned,
|
|
204
|
+
numAxesReduced: bounds.numAxesReduced,
|
|
205
|
+
}
|
|
206
|
+
: {
|
|
207
|
+
variationAxes: undefined,
|
|
208
|
+
fullyInstanced: false,
|
|
209
|
+
numAxesPinned: 0,
|
|
210
|
+
numAxesReduced: 0,
|
|
211
|
+
};
|
|
212
|
+
subsetInfoByFontUrl.set(fontUrl, subsetInfo);
|
|
213
|
+
let featureGlyphIds;
|
|
214
|
+
if (fontUsage.hasFontFeatureSettings && fontBuffer) {
|
|
215
|
+
try {
|
|
216
|
+
featureGlyphIds = await collectFeatureGlyphIds(fontBuffer, text, fontUsage.fontFeatureTags);
|
|
217
|
+
}
|
|
218
|
+
catch (rawErr) {
|
|
219
|
+
// Feature glyph collection failed — continue without feature
|
|
220
|
+
// glyphs rather than blocking all fonts (Promise.all would
|
|
221
|
+
// reject entirely if this propagated).
|
|
222
|
+
const err = rawErr;
|
|
223
|
+
err.asset = err.asset || fontAssetsByUrl.get(fontUrl);
|
|
224
|
+
assetGraph.warn(err);
|
|
225
|
+
}
|
|
307
226
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
227
|
+
for (const targetFormat of formats) {
|
|
228
|
+
const promiseId = getSubsetPromiseId(fontUsage, targetFormat, subsetInfo.variationAxes);
|
|
229
|
+
if (!subsetPromiseMap.has(promiseId)) {
|
|
230
|
+
const cacheKey = diskCache
|
|
231
|
+
? subsetCacheKey(fontBuffer, text, targetFormat, subsetInfo.variationAxes, featureGlyphIds)
|
|
232
|
+
: null;
|
|
233
|
+
const cachedResult = diskCache && cacheKey ? await diskCache.get(cacheKey) : null;
|
|
234
|
+
if (cachedResult) {
|
|
235
|
+
if (cacheStats)
|
|
236
|
+
cacheStats.hits++;
|
|
237
|
+
subsetPromiseMap.set(promiseId, Promise.resolve(cachedResult));
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
if (cacheStats)
|
|
241
|
+
cacheStats.misses++;
|
|
242
|
+
const subsetCall = subsetFontWithGlyphs(fontBuffer, text, {
|
|
243
|
+
targetFormat,
|
|
244
|
+
glyphIds: featureGlyphIds,
|
|
245
|
+
variationAxes: subsetInfo.variationAxes,
|
|
246
|
+
});
|
|
247
|
+
subsetPromiseMap.set(promiseId, subsetCall
|
|
248
|
+
.then(async (result) => {
|
|
249
|
+
if (diskCache && result && cacheKey) {
|
|
250
|
+
// Fire-and-forget: cache writes are best-effort.
|
|
251
|
+
// Errors are handled inside set(); the catch is a
|
|
252
|
+
// safety net against unhandled rejections.
|
|
253
|
+
diskCache.set(cacheKey, result).catch(() => { });
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
})
|
|
257
|
+
.catch((rawErr) => {
|
|
258
|
+
const err = rawErr;
|
|
259
|
+
err.asset = err.asset || fontAssetsByUrl.get(fontUrl);
|
|
260
|
+
assetGraph.warn(err);
|
|
261
|
+
return null;
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
320
265
|
}
|
|
321
|
-
|
|
266
|
+
}));
|
|
267
|
+
// Await all subset promises
|
|
268
|
+
const resolvedSubsets = new Map(await Promise.all([...subsetPromiseMap].map(async ([key, promise]) => [key, await promise])));
|
|
269
|
+
if (cacheStats && debug && console) {
|
|
270
|
+
const total = cacheStats.hits + cacheStats.misses;
|
|
271
|
+
const pct = total > 0 ? Math.round((cacheStats.hits * 100) / total) : 0;
|
|
272
|
+
console.log(`[subfont timing] subset disk cache: ${cacheStats.hits} hit${cacheStats.hits === 1 ? '' : 's'}, ${cacheStats.misses} miss${cacheStats.misses === 1 ? '' : 'es'} (${pct}% hit rate)`);
|
|
322
273
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// Propagate subsets to non-canonical font usages
|
|
326
|
-
for (const item of htmlOrSvgAssetTextsWithProps) {
|
|
327
|
-
for (const fontUsage of item.fontUsages) {
|
|
328
|
-
if (!fontUsage.fontUrl) continue;
|
|
329
|
-
const canonical = canonicalFontUsageByUrl.get(fontUsage.fontUrl);
|
|
330
|
-
if (canonical && canonical !== fontUsage && canonical.subsets) {
|
|
274
|
+
// Assign subset results to canonical font usages
|
|
275
|
+
for (const [, fontUsage] of canonicalFontUsageByUrl) {
|
|
331
276
|
const info = subsetInfoByFontUrl.get(fontUsage.fontUrl);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
277
|
+
if (!info)
|
|
278
|
+
continue;
|
|
279
|
+
for (const targetFormat of formats) {
|
|
280
|
+
const promiseId = getSubsetPromiseId(fontUsage, targetFormat, info.variationAxes);
|
|
281
|
+
const subsetBuffer = resolvedSubsets.get(promiseId);
|
|
282
|
+
if (subsetBuffer) {
|
|
283
|
+
if (!fontUsage.subsets) {
|
|
284
|
+
fontUsage.subsets = {};
|
|
285
|
+
}
|
|
286
|
+
fontUsage.subsets[targetFormat] = subsetBuffer;
|
|
287
|
+
const size = subsetBuffer.length;
|
|
288
|
+
if (!fontUsage.smallestSubsetSize ||
|
|
289
|
+
size < fontUsage.smallestSubsetSize) {
|
|
290
|
+
fontUsage.smallestSubsetSize = size;
|
|
291
|
+
fontUsage.smallestSubsetFormat = targetFormat;
|
|
292
|
+
fontUsage.variationAxes = info.variationAxes;
|
|
293
|
+
fontUsage.fullyInstanced = info.fullyInstanced;
|
|
294
|
+
fontUsage.numAxesPinned = info.numAxesPinned;
|
|
295
|
+
fontUsage.numAxesReduced = info.numAxesReduced;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Propagate subsets to non-canonical font usages
|
|
301
|
+
for (const item of htmlOrSvgAssetTextsWithProps) {
|
|
302
|
+
for (const fontUsage of item.fontUsages) {
|
|
303
|
+
if (!fontUsage.fontUrl)
|
|
304
|
+
continue;
|
|
305
|
+
const canonical = canonicalFontUsageByUrl.get(fontUsage.fontUrl);
|
|
306
|
+
if (canonical && canonical !== fontUsage && canonical.subsets) {
|
|
307
|
+
const info = subsetInfoByFontUrl.get(fontUsage.fontUrl);
|
|
308
|
+
if (!info)
|
|
309
|
+
continue;
|
|
310
|
+
fontUsage.subsets = canonical.subsets;
|
|
311
|
+
fontUsage.smallestSubsetSize = canonical.smallestSubsetSize;
|
|
312
|
+
fontUsage.smallestSubsetFormat = canonical.smallestSubsetFormat;
|
|
313
|
+
fontUsage.variationAxes = info.variationAxes;
|
|
314
|
+
fontUsage.fullyInstanced = info.fullyInstanced;
|
|
315
|
+
fontUsage.numAxesPinned = info.numAxesPinned;
|
|
316
|
+
fontUsage.numAxesReduced = info.numAxesReduced;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
340
319
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return fontAssetsByUrl;
|
|
320
|
+
return fontAssetsByUrl;
|
|
344
321
|
}
|
|
345
|
-
|
|
346
|
-
module.exports = {
|
|
347
|
-
getSubsetPromiseId,
|
|
348
|
-
getSubsetsForFontUsage,
|
|
349
|
-
// Exported for testing
|
|
350
|
-
_subsetCacheKey: subsetCacheKey,
|
|
351
|
-
_SubsetDiskCache: SubsetDiskCache,
|
|
352
|
-
};
|
|
322
|
+
//# sourceMappingURL=subsetGeneration.js.map
|