@turntrout/subfont 1.5.0 → 1.6.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/CHANGELOG.md +7 -0
- package/README.md +31 -0
- package/lib/collectTextsByPage.js +109 -337
- package/lib/concurrencyLimit.js +5 -1
- package/lib/fontFeatureHelpers.js +249 -0
- package/lib/fontTracerWorker.js +0 -10
- package/lib/parseCommandLineOptions.js +2 -2
- package/lib/progress.js +101 -0
- package/lib/subfont.js +25 -38
- package/lib/subsetFontWithGlyphs.js +8 -0
- package/lib/subsetFonts.js +60 -91
- package/lib/subsetGeneration.js +14 -1
- package/lib/variationAxes.js +3 -32
- package/lib/wasmQueue.js +4 -1
- package/package.json +2 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
const cssFontParser = require('css-font-parser');
|
|
2
|
+
|
|
3
|
+
const featureSettingsProps = new Set([
|
|
4
|
+
'font-feature-settings',
|
|
5
|
+
'font-variant-alternates',
|
|
6
|
+
'font-variant-caps',
|
|
7
|
+
'font-variant-east-asian',
|
|
8
|
+
'font-variant-ligatures',
|
|
9
|
+
'font-variant-numeric',
|
|
10
|
+
'font-variant-position',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
// Map font-variant-* CSS values to their corresponding OpenType feature tags.
|
|
14
|
+
const fontVariantToOTTags = {
|
|
15
|
+
'font-variant-ligatures': {
|
|
16
|
+
'common-ligatures': ['liga', 'clig'],
|
|
17
|
+
'no-common-ligatures': ['liga', 'clig'],
|
|
18
|
+
'discretionary-ligatures': ['dlig'],
|
|
19
|
+
'no-discretionary-ligatures': ['dlig'],
|
|
20
|
+
'historical-ligatures': ['hlig'],
|
|
21
|
+
'no-historical-ligatures': ['hlig'],
|
|
22
|
+
contextual: ['calt'],
|
|
23
|
+
'no-contextual': ['calt'],
|
|
24
|
+
},
|
|
25
|
+
'font-variant-caps': {
|
|
26
|
+
'small-caps': ['smcp'],
|
|
27
|
+
'all-small-caps': ['smcp', 'c2sc'],
|
|
28
|
+
'petite-caps': ['pcap'],
|
|
29
|
+
'all-petite-caps': ['pcap', 'c2pc'],
|
|
30
|
+
unicase: ['unic'],
|
|
31
|
+
'titling-caps': ['titl'],
|
|
32
|
+
},
|
|
33
|
+
'font-variant-numeric': {
|
|
34
|
+
'lining-nums': ['lnum'],
|
|
35
|
+
'oldstyle-nums': ['onum'],
|
|
36
|
+
'proportional-nums': ['pnum'],
|
|
37
|
+
'tabular-nums': ['tnum'],
|
|
38
|
+
'diagonal-fractions': ['frac'],
|
|
39
|
+
'stacked-fractions': ['afrc'],
|
|
40
|
+
ordinal: ['ordn'],
|
|
41
|
+
'slashed-zero': ['zero'],
|
|
42
|
+
},
|
|
43
|
+
'font-variant-position': {
|
|
44
|
+
sub: ['subs'],
|
|
45
|
+
super: ['sups'],
|
|
46
|
+
},
|
|
47
|
+
'font-variant-east-asian': {
|
|
48
|
+
jis78: ['jp78'],
|
|
49
|
+
jis83: ['jp83'],
|
|
50
|
+
jis90: ['jp90'],
|
|
51
|
+
jis04: ['jp04'],
|
|
52
|
+
simplified: ['smpl'],
|
|
53
|
+
traditional: ['trad'],
|
|
54
|
+
'proportional-width': ['pwid'],
|
|
55
|
+
'full-width': ['fwid'],
|
|
56
|
+
ruby: ['ruby'],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Extract OpenType feature tags referenced by a CSS declaration.
|
|
61
|
+
function extractFeatureTagsFromDecl(prop, value) {
|
|
62
|
+
const tags = new Set();
|
|
63
|
+
const propLower = prop.toLowerCase();
|
|
64
|
+
|
|
65
|
+
if (propLower === 'font-feature-settings') {
|
|
66
|
+
// Parse quoted 4-letter tags: "liga" 1, 'dlig', etc.
|
|
67
|
+
const re = /["']([a-zA-Z0-9]{4})["']/g;
|
|
68
|
+
let m;
|
|
69
|
+
while ((m = re.exec(value)) !== null) {
|
|
70
|
+
tags.add(m[1]);
|
|
71
|
+
}
|
|
72
|
+
return tags;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (propLower === 'font-variant-alternates') {
|
|
76
|
+
const v = value.toLowerCase();
|
|
77
|
+
if (v.includes('historical-forms')) tags.add('hist');
|
|
78
|
+
if (/stylistic\s*\(/.test(v)) tags.add('salt');
|
|
79
|
+
if (/swash\s*\(/.test(v)) tags.add('swsh');
|
|
80
|
+
if (/ornaments\s*\(/.test(v)) tags.add('ornm');
|
|
81
|
+
if (/annotation\s*\(/.test(v)) tags.add('nalt');
|
|
82
|
+
if (/styleset\s*\(/.test(v)) {
|
|
83
|
+
for (let i = 1; i <= 20; i++) {
|
|
84
|
+
tags.add(`ss${String(i).padStart(2, '0')}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (/character-variant\s*\(/.test(v)) {
|
|
88
|
+
for (let i = 1; i <= 99; i++) {
|
|
89
|
+
tags.add(`cv${String(i).padStart(2, '0')}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return tags;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const mapping = fontVariantToOTTags[propLower];
|
|
96
|
+
if (mapping) {
|
|
97
|
+
// Split into tokens for exact keyword matching — substring matching
|
|
98
|
+
// would falsely trigger e.g. "sub" inside "super".
|
|
99
|
+
const tokens = new Set(value.toLowerCase().split(/\s+/));
|
|
100
|
+
for (const [keyword, otTags] of Object.entries(mapping)) {
|
|
101
|
+
if (tokens.has(keyword)) {
|
|
102
|
+
for (const t of otTags) tags.add(t);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return tags;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Collect feature tags from all feature-related declarations in a CSS rule.
|
|
110
|
+
function ruleFeatureTags(rule) {
|
|
111
|
+
const tags = new Set();
|
|
112
|
+
let hasFeatureDecl = false;
|
|
113
|
+
for (const node of rule.nodes) {
|
|
114
|
+
if (
|
|
115
|
+
node.type === 'decl' &&
|
|
116
|
+
featureSettingsProps.has(node.prop.toLowerCase())
|
|
117
|
+
) {
|
|
118
|
+
hasFeatureDecl = true;
|
|
119
|
+
for (const t of extractFeatureTagsFromDecl(node.prop, node.value)) {
|
|
120
|
+
tags.add(t);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return hasFeatureDecl ? tags : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ruleFontFamily(rule) {
|
|
128
|
+
for (let i = rule.nodes.length - 1; i >= 0; i--) {
|
|
129
|
+
const node = rule.nodes[i];
|
|
130
|
+
if (node.type === 'decl' && node.prop.toLowerCase() === 'font-family') {
|
|
131
|
+
return node.value;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Add all items from `tags` into the Set stored at `key` in `map`,
|
|
138
|
+
// creating the Set if it doesn't exist yet.
|
|
139
|
+
function addTagsToMapEntry(map, key, tags) {
|
|
140
|
+
let s = map.get(key);
|
|
141
|
+
if (!s) {
|
|
142
|
+
s = new Set();
|
|
143
|
+
map.set(key, s);
|
|
144
|
+
}
|
|
145
|
+
for (const t of tags) s.add(t);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Record the OT tags from a single CSS rule into featureTagsByFamily,
|
|
149
|
+
// keyed by font-family (or '*' when no font-family is specified).
|
|
150
|
+
function recordRuleFeatureTags(rule, featureTagsByFamily) {
|
|
151
|
+
const tags = ruleFeatureTags(rule);
|
|
152
|
+
if (!tags) return null;
|
|
153
|
+
|
|
154
|
+
const fontFamily = ruleFontFamily(rule);
|
|
155
|
+
if (!fontFamily) {
|
|
156
|
+
if (featureTagsByFamily) addTagsToMapEntry(featureTagsByFamily, '*', tags);
|
|
157
|
+
return true; // signals "all families"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const families = cssFontParser.parseFontFamily(fontFamily);
|
|
161
|
+
if (featureTagsByFamily) {
|
|
162
|
+
for (const family of families) {
|
|
163
|
+
addTagsToMapEntry(featureTagsByFamily, family.toLowerCase(), tags);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return families;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Determine which font-families use font-feature-settings or font-variant-*.
|
|
170
|
+
// Returns null (none detected), a Set of lowercase family names, or true (all).
|
|
171
|
+
// Also populates featureTagsByFamily with the OT tags per family (lowercase).
|
|
172
|
+
function findFontFamiliesWithFeatureSettings(
|
|
173
|
+
stylesheetsWithPredicates,
|
|
174
|
+
featureTagsByFamily
|
|
175
|
+
) {
|
|
176
|
+
let result = null;
|
|
177
|
+
for (const { asset } of stylesheetsWithPredicates) {
|
|
178
|
+
if (!asset || !asset.parseTree) continue;
|
|
179
|
+
asset.parseTree.walkRules((rule) => {
|
|
180
|
+
if (result === true && !featureTagsByFamily) return;
|
|
181
|
+
|
|
182
|
+
const recorded = recordRuleFeatureTags(rule, featureTagsByFamily);
|
|
183
|
+
if (!recorded) return;
|
|
184
|
+
|
|
185
|
+
if (recorded === true) {
|
|
186
|
+
result = true;
|
|
187
|
+
} else if (result !== true) {
|
|
188
|
+
if (!result) result = new Set();
|
|
189
|
+
for (const family of recorded) {
|
|
190
|
+
result.add(family.toLowerCase());
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
if (result === true && !featureTagsByFamily) break;
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Determine whether a template's font families use feature settings, and
|
|
200
|
+
// collect the corresponding OT feature tags from featureTagsByFamily.
|
|
201
|
+
function resolveFeatureSettings(
|
|
202
|
+
fontFamilies,
|
|
203
|
+
fontFamiliesWithFeatureSettings,
|
|
204
|
+
featureTagsByFamily
|
|
205
|
+
) {
|
|
206
|
+
let hasFontFeatureSettings = false;
|
|
207
|
+
if (fontFamiliesWithFeatureSettings === true) {
|
|
208
|
+
hasFontFeatureSettings = true;
|
|
209
|
+
} else if (fontFamiliesWithFeatureSettings instanceof Set) {
|
|
210
|
+
for (const f of fontFamilies) {
|
|
211
|
+
if (fontFamiliesWithFeatureSettings.has(f.toLowerCase())) {
|
|
212
|
+
hasFontFeatureSettings = true;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let fontFeatureTags;
|
|
219
|
+
if (hasFontFeatureSettings && featureTagsByFamily) {
|
|
220
|
+
const tags = new Set();
|
|
221
|
+
const globalTags = featureTagsByFamily.get('*');
|
|
222
|
+
if (globalTags) {
|
|
223
|
+
for (const t of globalTags) tags.add(t);
|
|
224
|
+
}
|
|
225
|
+
for (const f of fontFamilies) {
|
|
226
|
+
const familyTags = featureTagsByFamily.get(f.toLowerCase());
|
|
227
|
+
if (familyTags) {
|
|
228
|
+
for (const t of familyTags) tags.add(t);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (tags.size > 0) {
|
|
232
|
+
fontFeatureTags = [...tags];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { hasFontFeatureSettings, fontFeatureTags };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = {
|
|
240
|
+
featureSettingsProps,
|
|
241
|
+
fontVariantToOTTags,
|
|
242
|
+
extractFeatureTagsFromDecl,
|
|
243
|
+
ruleFeatureTags,
|
|
244
|
+
ruleFontFamily,
|
|
245
|
+
addTagsToMapEntry,
|
|
246
|
+
recordRuleFeatureTags,
|
|
247
|
+
findFontFamiliesWithFeatureSettings,
|
|
248
|
+
resolveFeatureSettings,
|
|
249
|
+
};
|
package/lib/fontTracerWorker.js
CHANGED
|
@@ -48,16 +48,6 @@ parentPort.on('message', (msg) => {
|
|
|
48
48
|
const textByProps = fontTracer(document, {
|
|
49
49
|
stylesheetsWithPredicates,
|
|
50
50
|
getCssRulesByProperty: memoizedGetCssRulesByProperty,
|
|
51
|
-
propsToReturn: [
|
|
52
|
-
'font-family',
|
|
53
|
-
'font-style',
|
|
54
|
-
'font-weight',
|
|
55
|
-
'font-variant',
|
|
56
|
-
'font-stretch',
|
|
57
|
-
'font-variation-settings',
|
|
58
|
-
'animation-timing-function',
|
|
59
|
-
'font-size',
|
|
60
|
-
],
|
|
61
51
|
});
|
|
62
52
|
|
|
63
53
|
// Strip any non-serializable data from results
|
|
@@ -111,7 +111,7 @@ module.exports = function parseCommandLineOptions(argv) {
|
|
|
111
111
|
},
|
|
112
112
|
})
|
|
113
113
|
.options('concurrency', {
|
|
114
|
-
describe: `Maximum number of worker threads for parallel font tracing. Defaults to the number of CPU cores (max 8). Upper bound: ${maxConcurrency} (based on
|
|
114
|
+
describe: `Maximum number of worker threads for parallel font tracing. Defaults to the number of CPU cores (max 8). Upper bound: ${maxConcurrency} (based on free memory and CPU count)`,
|
|
115
115
|
type: 'number',
|
|
116
116
|
})
|
|
117
117
|
.check((argv) => {
|
|
@@ -121,7 +121,7 @@ module.exports = function parseCommandLineOptions(argv) {
|
|
|
121
121
|
}
|
|
122
122
|
if (argv.concurrency > maxConcurrency) {
|
|
123
123
|
throw new Error(
|
|
124
|
-
`--concurrency must not exceed ${maxConcurrency} (each worker uses ~50 MB;
|
|
124
|
+
`--concurrency must not exceed ${maxConcurrency} (each worker uses ~50 MB; ${Math.round(os.freemem() / (1024 * 1024 * 1024))} GB free, ${os.cpus().length} CPUs)`
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
127
|
}
|
package/lib/progress.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Helpers for surfacing phase progress to the console.
|
|
2
|
+
//
|
|
3
|
+
// Two audiences:
|
|
4
|
+
// 1. Non-debug users want brief, always-visible signal that the tool
|
|
5
|
+
// isn't hung on large runs (banner + periodic page counts + done).
|
|
6
|
+
// 2. Debug users want to pinpoint *which* phase is stuck when output
|
|
7
|
+
// goes silent. Paired "→ starting" / "← finished in Nms" markers
|
|
8
|
+
// around each slow phase mean the last line printed is always the
|
|
9
|
+
// currently-running phase.
|
|
10
|
+
|
|
11
|
+
// Pick a progress step that gives roughly 10 updates across the run,
|
|
12
|
+
// clamped to [1, 10]. For a 500-page run this prints every 50; for 30,
|
|
13
|
+
// every 3. Avoids long silent stretches on large runs and per-page spam
|
|
14
|
+
// on small ones.
|
|
15
|
+
function progressStep(total) {
|
|
16
|
+
return Math.max(1, Math.min(10, Math.ceil(total / 10)));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Reporter for a phase that iterates `total` items.
|
|
20
|
+
// banner(msg): optional one-line header printed immediately.
|
|
21
|
+
// tick(): call once per completed item; returns the running count.
|
|
22
|
+
// done(): prints "<label>: <total>/<total> pages done." footer.
|
|
23
|
+
// All methods no-op when disabled (missing console or below minTotal).
|
|
24
|
+
function createPageProgress({ total, console, label, minTotal = 5 }) {
|
|
25
|
+
const enabled = Boolean(console) && total >= minTotal;
|
|
26
|
+
const step = progressStep(total);
|
|
27
|
+
let count = 0;
|
|
28
|
+
return {
|
|
29
|
+
enabled,
|
|
30
|
+
banner(msg) {
|
|
31
|
+
if (enabled) console.log(msg);
|
|
32
|
+
},
|
|
33
|
+
tick() {
|
|
34
|
+
count++;
|
|
35
|
+
if (enabled && count % step === 0 && count < total) {
|
|
36
|
+
console.log(` ${label}: ${count}/${total} pages...`);
|
|
37
|
+
}
|
|
38
|
+
return count;
|
|
39
|
+
},
|
|
40
|
+
done() {
|
|
41
|
+
if (enabled) {
|
|
42
|
+
console.log(` ${label}: ${total}/${total} pages done.`);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Per-page debug line emitted right after a page finishes tracing.
|
|
49
|
+
// When a run hangs, the last-printed page identifies which page stalled.
|
|
50
|
+
function logTracedPage(console, debug, index, total, asset, startMs) {
|
|
51
|
+
if (!debug || !console) return;
|
|
52
|
+
console.log(
|
|
53
|
+
`[subfont timing] traced [${index}/${total}] ${asset.urlOrDescription} in ${Date.now() - startMs}ms`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Start/end markers for a debug phase. The "→ label..." line is printed
|
|
58
|
+
// *before* the work begins, so if the work hangs the user sees exactly
|
|
59
|
+
// which phase is in flight. The returned end() logs elapsed ms and
|
|
60
|
+
// returns the duration so callers can still populate a timings map.
|
|
61
|
+
//
|
|
62
|
+
// const phase = logPhaseStart(console, debug, 'getSubsetsForFontUsage');
|
|
63
|
+
// await getSubsetsForFontUsage(...);
|
|
64
|
+
// timings.x = phase.end();
|
|
65
|
+
function logPhaseStart(console, debug, label) {
|
|
66
|
+
if (!debug || !console) {
|
|
67
|
+
const start = Date.now();
|
|
68
|
+
return {
|
|
69
|
+
end() {
|
|
70
|
+
return Date.now() - start;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const start = Date.now();
|
|
75
|
+
console.log(`[subfont timing] → ${label}...`);
|
|
76
|
+
return {
|
|
77
|
+
end(extraInfo) {
|
|
78
|
+
const ms = Date.now() - start;
|
|
79
|
+
const suffix = extraInfo ? ` (${extraInfo})` : '';
|
|
80
|
+
console.log(`[subfont timing] ← ${label}: ${ms}ms${suffix}`);
|
|
81
|
+
return ms;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Bind a (console, debug) pair once at the top of a function, then use
|
|
87
|
+
// the returned tracker to open phases with a single short call:
|
|
88
|
+
//
|
|
89
|
+
// const trackPhase = makePhaseTracker(console, debug);
|
|
90
|
+
// const p = trackPhase('codepoint generation');
|
|
91
|
+
// ...work...
|
|
92
|
+
// timings.x = p.end();
|
|
93
|
+
function makePhaseTracker(console, debug) {
|
|
94
|
+
return (label) => logPhaseStart(console, debug, label);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
createPageProgress,
|
|
99
|
+
logTracedPage,
|
|
100
|
+
makePhaseTracker,
|
|
101
|
+
};
|
package/lib/subfont.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
const fsPromises = require('fs/promises');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const pathModule = require('path');
|
|
4
|
+
const sanitizeFilename = require('sanitize-filename');
|
|
4
5
|
const { getMaxConcurrency } = require('./concurrencyLimit');
|
|
5
6
|
const AssetGraph = require('assetgraph');
|
|
6
7
|
const prettyBytes = require('pretty-bytes');
|
|
7
8
|
const urlTools = require('urltools');
|
|
8
9
|
const util = require('util');
|
|
9
10
|
const subsetFonts = require('./subsetFonts');
|
|
11
|
+
const { makePhaseTracker } = require('./progress');
|
|
10
12
|
|
|
11
13
|
class UsageError extends Error {
|
|
12
14
|
constructor(message) {
|
|
@@ -49,7 +51,7 @@ module.exports = async function subfont(
|
|
|
49
51
|
const maxConcurrency = getMaxConcurrency();
|
|
50
52
|
if (concurrency !== undefined && concurrency > maxConcurrency) {
|
|
51
53
|
throw new UsageError(
|
|
52
|
-
`--concurrency must not exceed ${maxConcurrency} (each worker uses ~50 MB;
|
|
54
|
+
`--concurrency must not exceed ${maxConcurrency} (each worker uses ~50 MB; ${Math.round(os.freemem() / (1024 * 1024 * 1024))} GB free, ${os.cpus().length} CPUs)`
|
|
53
55
|
);
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -191,21 +193,20 @@ module.exports = async function subfont(
|
|
|
191
193
|
}
|
|
192
194
|
|
|
193
195
|
const outerTimings = {};
|
|
196
|
+
// The tracker writes with console.log (duck-typed). Route it through
|
|
197
|
+
// the silent-aware log wrapper so --silent suppresses phase markers
|
|
198
|
+
// the same way it suppresses other subfont output.
|
|
199
|
+
const trackPhase = makePhaseTracker({ log }, debug);
|
|
194
200
|
|
|
195
|
-
|
|
201
|
+
const loadAssetsPhase = trackPhase('loadAssets');
|
|
196
202
|
await assetGraph.loadAssets(inputUrls);
|
|
197
|
-
outerTimings.loadAssets =
|
|
198
|
-
if (debug) log(`[subfont timing] loadAssets: ${outerTimings.loadAssets}ms`);
|
|
203
|
+
outerTimings.loadAssets = loadAssetsPhase.end();
|
|
199
204
|
|
|
200
|
-
|
|
205
|
+
const populatePhase = trackPhase('populate (initial)');
|
|
201
206
|
await assetGraph.populate({
|
|
202
207
|
followRelations: followRelationsQuery,
|
|
203
208
|
});
|
|
204
|
-
outerTimings['populate (initial)'] =
|
|
205
|
-
if (debug)
|
|
206
|
-
log(
|
|
207
|
-
`[subfont timing] populate (initial): ${outerTimings['populate (initial)']}ms`
|
|
208
|
-
);
|
|
209
|
+
outerTimings['populate (initial)'] = populatePhase.end();
|
|
209
210
|
|
|
210
211
|
const entrypointAssets = assetGraph.findAssets({ isInitial: true });
|
|
211
212
|
const redirectOrigins = new Set();
|
|
@@ -269,7 +270,7 @@ module.exports = async function subfont(
|
|
|
269
270
|
);
|
|
270
271
|
}
|
|
271
272
|
|
|
272
|
-
|
|
273
|
+
const subsetPhase = trackPhase('subsetFonts total');
|
|
273
274
|
const { fontInfo, timings: subsetTimings } = await subsetFonts(assetGraph, {
|
|
274
275
|
inlineCss,
|
|
275
276
|
fontDisplay,
|
|
@@ -285,11 +286,9 @@ module.exports = async function subfont(
|
|
|
285
286
|
chromeArgs: chromeFlags,
|
|
286
287
|
cacheDir,
|
|
287
288
|
});
|
|
289
|
+
const subsetFontsTotal = subsetPhase.end();
|
|
288
290
|
|
|
289
|
-
const
|
|
290
|
-
if (debug) log(`[subfont timing] subsetFonts total: ${subsetFontsTotal}ms`);
|
|
291
|
-
|
|
292
|
-
phaseStart = Date.now();
|
|
291
|
+
const postProcessingPhase = trackPhase('post-subsetFonts processing');
|
|
293
292
|
let sumSizesAfter = 0;
|
|
294
293
|
for (const asset of assetGraph.findAssets(sizeableAssetQuery)) {
|
|
295
294
|
sumSizesAfter += asset.rawSrc.length;
|
|
@@ -313,11 +312,12 @@ module.exports = async function subfont(
|
|
|
313
312
|
assetGraph.info(
|
|
314
313
|
new Error(`Pulling down modified stylesheet ${asset.url}`)
|
|
315
314
|
);
|
|
316
|
-
|
|
317
|
-
asset.baseName || 'index'
|
|
318
|
-
}-${asset.md5Hex.slice(
|
|
319
|
-
|
|
320
|
-
|
|
315
|
+
const safeName =
|
|
316
|
+
sanitizeFilename(asset.baseName || '', { replacement: '_' }) || 'index';
|
|
317
|
+
asset.url = `${assetGraph.root}subfont/${safeName}-${asset.md5Hex.slice(
|
|
318
|
+
0,
|
|
319
|
+
10
|
|
320
|
+
)}${asset.extension || asset.defaultExtension}`;
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
|
|
@@ -343,11 +343,7 @@ module.exports = async function subfont(
|
|
|
343
343
|
);
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
outerTimings['post-subsetFonts processing'] =
|
|
347
|
-
if (debug)
|
|
348
|
-
log(
|
|
349
|
-
`[subfont timing] post-subsetFonts processing: ${outerTimings['post-subsetFonts processing']}ms`
|
|
350
|
-
);
|
|
346
|
+
outerTimings['post-subsetFonts processing'] = postProcessingPhase.end();
|
|
351
347
|
|
|
352
348
|
if (strict && sawWarning) {
|
|
353
349
|
// In non-silent mode, assetgraph's logEvents normally exits earlier via
|
|
@@ -358,7 +354,7 @@ module.exports = async function subfont(
|
|
|
358
354
|
);
|
|
359
355
|
}
|
|
360
356
|
|
|
361
|
-
|
|
357
|
+
const writePhase = trackPhase('writeAssetsToDisc');
|
|
362
358
|
if (!dryRun) {
|
|
363
359
|
await assetGraph.writeAssetsToDisc(
|
|
364
360
|
{
|
|
@@ -370,14 +366,9 @@ module.exports = async function subfont(
|
|
|
370
366
|
assetGraph.root
|
|
371
367
|
);
|
|
372
368
|
}
|
|
369
|
+
outerTimings.writeAssetsToDisc = writePhase.end();
|
|
373
370
|
|
|
374
|
-
|
|
375
|
-
if (debug)
|
|
376
|
-
log(
|
|
377
|
-
`[subfont timing] writeAssetsToDisc: ${outerTimings.writeAssetsToDisc}ms`
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
phaseStart = Date.now();
|
|
371
|
+
const reportingPhase = trackPhase('output reporting');
|
|
381
372
|
if (debug) {
|
|
382
373
|
const compactFontInfo = fontInfo.map(({ fontUsages, ...rest }) => ({
|
|
383
374
|
...rest,
|
|
@@ -494,11 +485,7 @@ module.exports = async function subfont(
|
|
|
494
485
|
)}`
|
|
495
486
|
);
|
|
496
487
|
log(`Total savings: ${prettyBytes(totalSavings)}`);
|
|
497
|
-
outerTimings['output reporting'] =
|
|
498
|
-
if (debug)
|
|
499
|
-
log(
|
|
500
|
-
`[subfont timing] output reporting: ${outerTimings['output reporting']}ms`
|
|
501
|
-
);
|
|
488
|
+
outerTimings['output reporting'] = reportingPhase.end();
|
|
502
489
|
|
|
503
490
|
const st = subsetTimings || {};
|
|
504
491
|
const details = st.collectTextsByPageDetails || {};
|
|
@@ -216,6 +216,14 @@ function extractSubsetFont(exports, subset) {
|
|
|
216
216
|
// Fresh view AFTER the WASM calls above — memory.buffer may have been
|
|
217
217
|
// detached by a grow during hb_face_reference_blob / hb_blob_get_data.
|
|
218
218
|
const heapu8 = getHeapu8(exports);
|
|
219
|
+
|
|
220
|
+
if (offset + subsetByteLength > heapu8.byteLength) {
|
|
221
|
+
exports.hb_blob_destroy(result);
|
|
222
|
+
throw new Error(
|
|
223
|
+
`WASM returned out-of-bounds offset ${offset} + length ${subsetByteLength} (heap size ${heapu8.byteLength})`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
219
227
|
const subsetFont = Buffer.from(
|
|
220
228
|
heapu8.subarray(offset, offset + subsetByteLength)
|
|
221
229
|
);
|