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