@turntrout/subfont 1.0.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 +35 -0
- package/CLAUDE.md +53 -0
- package/LICENSE.md +7 -0
- package/README.md +93 -0
- package/lib/FontTracerPool.js +158 -0
- package/lib/HeadlessBrowser.js +223 -0
- package/lib/cli.js +14 -0
- package/lib/collectFeatureGlyphIds.js +137 -0
- package/lib/collectTextsByPage.js +1017 -0
- package/lib/extractReferencedCustomPropertyNames.js +20 -0
- package/lib/extractVisibleText.js +64 -0
- package/lib/findCustomPropertyDefinitions.js +54 -0
- package/lib/fontFaceHelpers.js +292 -0
- package/lib/fontTracerWorker.js +76 -0
- package/lib/gatherStylesheetsWithPredicates.js +87 -0
- package/lib/getCssRulesByProperty.js +343 -0
- package/lib/getFontInfo.js +36 -0
- package/lib/initialValueByProp.js +18 -0
- package/lib/injectSubsetDefinitions.js +65 -0
- package/lib/normalizeFontPropertyValue.js +34 -0
- package/lib/parseCommandLineOptions.js +131 -0
- package/lib/parseFontVariationSettings.js +39 -0
- package/lib/sfntCache.js +29 -0
- package/lib/stripLocalTokens.js +23 -0
- package/lib/subfont.js +571 -0
- package/lib/subsetFontWithGlyphs.js +193 -0
- package/lib/subsetFonts.js +1218 -0
- package/lib/subsetGeneration.js +347 -0
- package/lib/unicodeRange.js +38 -0
- package/lib/unquote.js +23 -0
- package/lib/variationAxes.js +162 -0
- package/lib/warnAboutMissingGlyphs.js +145 -0
- package/lib/wasmQueue.js +11 -0
- package/package.json +113 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const pathModule = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const subsetFont = require('subset-font');
|
|
5
|
+
const { getVariationAxisBounds } = require('./variationAxes');
|
|
6
|
+
const collectFeatureGlyphIds = require('./collectFeatureGlyphIds');
|
|
7
|
+
const subsetFontWithGlyphs = require('./subsetFontWithGlyphs');
|
|
8
|
+
|
|
9
|
+
// Simple disk cache for subset results.
|
|
10
|
+
// Cache key: hash(fontBuffer + text + format + variationAxes)
|
|
11
|
+
// Cache value: the subset font buffer
|
|
12
|
+
|
|
13
|
+
function subsetCacheKey(
|
|
14
|
+
fontBuffer,
|
|
15
|
+
text,
|
|
16
|
+
targetFormat,
|
|
17
|
+
variationAxes,
|
|
18
|
+
featureGlyphIds
|
|
19
|
+
) {
|
|
20
|
+
const hash = crypto.createHash('sha256');
|
|
21
|
+
hash.update(fontBuffer);
|
|
22
|
+
hash.update(text);
|
|
23
|
+
hash.update(targetFormat);
|
|
24
|
+
if (variationAxes) hash.update(JSON.stringify(variationAxes));
|
|
25
|
+
if (featureGlyphIds) hash.update(JSON.stringify(featureGlyphIds));
|
|
26
|
+
return hash.digest('hex');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class SubsetDiskCache {
|
|
30
|
+
constructor(cacheDir) {
|
|
31
|
+
this._cacheDir = cacheDir;
|
|
32
|
+
this._ensured = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_ensureDir() {
|
|
36
|
+
if (!this._ensured) {
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(this._cacheDir, { recursive: true });
|
|
39
|
+
} catch {
|
|
40
|
+
// Ignore errors (path is a file, read-only FS, etc.)
|
|
41
|
+
}
|
|
42
|
+
this._ensured = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get(key) {
|
|
47
|
+
const filePath = pathModule.join(this._cacheDir, key);
|
|
48
|
+
try {
|
|
49
|
+
return fs.readFileSync(filePath);
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
set(key, buffer) {
|
|
56
|
+
this._ensureDir();
|
|
57
|
+
const filePath = pathModule.join(this._cacheDir, key);
|
|
58
|
+
try {
|
|
59
|
+
fs.writeFileSync(filePath, buffer);
|
|
60
|
+
} catch {
|
|
61
|
+
// Ignore write errors (read-only FS, etc.)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getSubsetPromiseId(fontUsage, format, variationAxes = null) {
|
|
67
|
+
return [
|
|
68
|
+
fontUsage.text,
|
|
69
|
+
fontUsage.fontUrl,
|
|
70
|
+
format,
|
|
71
|
+
JSON.stringify(variationAxes),
|
|
72
|
+
].join('\x1d');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function collectCanonicalFontUsages(htmlOrSvgAssetTextsWithProps) {
|
|
76
|
+
const canonicalFontUsageByUrl = new Map();
|
|
77
|
+
|
|
78
|
+
for (const item of htmlOrSvgAssetTextsWithProps) {
|
|
79
|
+
for (const fontUsage of item.fontUsages) {
|
|
80
|
+
if (!fontUsage.fontUrl) continue;
|
|
81
|
+
if (!canonicalFontUsageByUrl.has(fontUsage.fontUrl)) {
|
|
82
|
+
canonicalFontUsageByUrl.set(fontUsage.fontUrl, fontUsage);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return canonicalFontUsageByUrl;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function loadFontAssets(assetGraph, allFonts) {
|
|
91
|
+
await assetGraph.populate({
|
|
92
|
+
followRelations: {
|
|
93
|
+
to: { url: { $or: allFonts } },
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const fontAssetsByUrl = new Map();
|
|
98
|
+
const originalFontBuffers = new Map();
|
|
99
|
+
for (const fontUrl of allFonts) {
|
|
100
|
+
const fontAsset = assetGraph.findAssets({
|
|
101
|
+
url: fontUrl,
|
|
102
|
+
isLoaded: true,
|
|
103
|
+
})[0];
|
|
104
|
+
if (fontAsset) {
|
|
105
|
+
fontAssetsByUrl.set(fontUrl, fontAsset);
|
|
106
|
+
originalFontBuffers.set(fontUrl, fontAsset.rawSrc);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { fontAssetsByUrl, originalFontBuffers };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function computeVariationAxisBounds(
|
|
114
|
+
canonicalFontUsageByUrl,
|
|
115
|
+
fontAssetsByUrl,
|
|
116
|
+
seenAxisValuesByFontUrlAndAxisName
|
|
117
|
+
) {
|
|
118
|
+
const cache = new Map();
|
|
119
|
+
const fontUrls = [...canonicalFontUsageByUrl.keys()].filter((url) =>
|
|
120
|
+
fontAssetsByUrl.has(url)
|
|
121
|
+
);
|
|
122
|
+
const boundsResults = await Promise.all(
|
|
123
|
+
fontUrls.map((fontUrl) =>
|
|
124
|
+
getVariationAxisBounds(
|
|
125
|
+
fontAssetsByUrl,
|
|
126
|
+
fontUrl,
|
|
127
|
+
seenAxisValuesByFontUrlAndAxisName
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
for (let i = 0; i < fontUrls.length; i++) {
|
|
132
|
+
cache.set(fontUrls[i], boundsResults[i]);
|
|
133
|
+
}
|
|
134
|
+
return cache;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getSubsetInfoForFont(fontUrl, variationAxisBoundsCache) {
|
|
138
|
+
const res = variationAxisBoundsCache.get(fontUrl);
|
|
139
|
+
if (res) {
|
|
140
|
+
return {
|
|
141
|
+
variationAxes: res.variationAxes,
|
|
142
|
+
fullyInstanced: res.fullyInstanced,
|
|
143
|
+
numAxesPinned: res.numAxesPinned,
|
|
144
|
+
numAxesReduced: res.numAxesReduced,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
variationAxes: undefined,
|
|
149
|
+
fullyInstanced: false,
|
|
150
|
+
numAxesPinned: 0,
|
|
151
|
+
numAxesReduced: 0,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function applySubsetInfo(fontUsage, info) {
|
|
156
|
+
fontUsage.variationAxes = info.variationAxes;
|
|
157
|
+
fontUsage.fullyInstanced = info.fullyInstanced;
|
|
158
|
+
fontUsage.numAxesPinned = info.numAxesPinned;
|
|
159
|
+
fontUsage.numAxesReduced = info.numAxesReduced;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function assignSubsetResults(
|
|
163
|
+
canonicalFontUsageByUrl,
|
|
164
|
+
subsetResultsByFontUrl,
|
|
165
|
+
resolvedSubsets,
|
|
166
|
+
formats
|
|
167
|
+
) {
|
|
168
|
+
for (const [, fontUsage] of canonicalFontUsageByUrl) {
|
|
169
|
+
const info = subsetResultsByFontUrl.get(fontUsage.fontUrl);
|
|
170
|
+
for (const targetFormat of formats) {
|
|
171
|
+
const promiseId = getSubsetPromiseId(
|
|
172
|
+
fontUsage,
|
|
173
|
+
targetFormat,
|
|
174
|
+
info.variationAxes
|
|
175
|
+
);
|
|
176
|
+
const subsetBuffer = resolvedSubsets.get(promiseId);
|
|
177
|
+
if (subsetBuffer) {
|
|
178
|
+
if (!fontUsage.subsets) {
|
|
179
|
+
fontUsage.subsets = {};
|
|
180
|
+
}
|
|
181
|
+
fontUsage.subsets[targetFormat] = subsetBuffer;
|
|
182
|
+
const size = subsetBuffer.length;
|
|
183
|
+
if (
|
|
184
|
+
!fontUsage.smallestSubsetSize ||
|
|
185
|
+
size < fontUsage.smallestSubsetSize
|
|
186
|
+
) {
|
|
187
|
+
fontUsage.smallestSubsetSize = size;
|
|
188
|
+
fontUsage.smallestSubsetFormat = targetFormat;
|
|
189
|
+
applySubsetInfo(fontUsage, info);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function propagateSubsets(
|
|
197
|
+
htmlOrSvgAssetTextsWithProps,
|
|
198
|
+
canonicalFontUsageByUrl,
|
|
199
|
+
subsetResultsByFontUrl
|
|
200
|
+
) {
|
|
201
|
+
for (const item of htmlOrSvgAssetTextsWithProps) {
|
|
202
|
+
for (const fontUsage of item.fontUsages) {
|
|
203
|
+
if (!fontUsage.fontUrl) continue;
|
|
204
|
+
const canonical = canonicalFontUsageByUrl.get(fontUsage.fontUrl);
|
|
205
|
+
if (canonical && canonical !== fontUsage && canonical.subsets) {
|
|
206
|
+
fontUsage.subsets = canonical.subsets;
|
|
207
|
+
fontUsage.smallestSubsetSize = canonical.smallestSubsetSize;
|
|
208
|
+
fontUsage.smallestSubsetFormat = canonical.smallestSubsetFormat;
|
|
209
|
+
applySubsetInfo(
|
|
210
|
+
fontUsage,
|
|
211
|
+
subsetResultsByFontUrl.get(fontUsage.fontUrl)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function getSubsetsForFontUsage(
|
|
219
|
+
assetGraph,
|
|
220
|
+
htmlOrSvgAssetTextsWithProps,
|
|
221
|
+
formats,
|
|
222
|
+
seenAxisValuesByFontUrlAndAxisName,
|
|
223
|
+
cacheDir = null
|
|
224
|
+
) {
|
|
225
|
+
const diskCache = cacheDir ? new SubsetDiskCache(cacheDir) : null;
|
|
226
|
+
const canonicalFontUsageByUrl = collectCanonicalFontUsages(
|
|
227
|
+
htmlOrSvgAssetTextsWithProps
|
|
228
|
+
);
|
|
229
|
+
const allFontUrls = [...canonicalFontUsageByUrl.keys()];
|
|
230
|
+
|
|
231
|
+
const { fontAssetsByUrl, originalFontBuffers } = await loadFontAssets(
|
|
232
|
+
assetGraph,
|
|
233
|
+
allFontUrls
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const subsetPromiseMap = new Map();
|
|
237
|
+
const subsetResultsByFontUrl = new Map();
|
|
238
|
+
|
|
239
|
+
const variationAxisBoundsCache = await computeVariationAxisBounds(
|
|
240
|
+
canonicalFontUsageByUrl,
|
|
241
|
+
fontAssetsByUrl,
|
|
242
|
+
seenAxisValuesByFontUrlAndAxisName
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
for (const [fontUrl, fontUsage] of canonicalFontUsageByUrl) {
|
|
246
|
+
const fontBuffer = originalFontBuffers.get(fontUrl);
|
|
247
|
+
if (!fontBuffer) continue;
|
|
248
|
+
const text = fontUsage.text;
|
|
249
|
+
const subsetInfo = getSubsetInfoForFont(fontUrl, variationAxisBoundsCache);
|
|
250
|
+
subsetResultsByFontUrl.set(fontUrl, subsetInfo);
|
|
251
|
+
|
|
252
|
+
// When font-feature-settings or font-variant-* CSS properties are used,
|
|
253
|
+
// collect the alternate glyph IDs that GSUB features produce for the
|
|
254
|
+
// page text. These are passed directly to HarfBuzz's subset glyph set,
|
|
255
|
+
// preserving the alternate glyphs without including all codepoints.
|
|
256
|
+
let featureGlyphIds;
|
|
257
|
+
if (fontUsage.hasFontFeatureSettings && fontBuffer) {
|
|
258
|
+
featureGlyphIds = await collectFeatureGlyphIds(fontBuffer, text);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const targetFormat of formats) {
|
|
262
|
+
const promiseId = getSubsetPromiseId(
|
|
263
|
+
fontUsage,
|
|
264
|
+
targetFormat,
|
|
265
|
+
subsetInfo.variationAxes
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (!subsetPromiseMap.has(promiseId)) {
|
|
269
|
+
// Check disk cache first if available
|
|
270
|
+
const cacheKey = diskCache
|
|
271
|
+
? subsetCacheKey(
|
|
272
|
+
fontBuffer,
|
|
273
|
+
text,
|
|
274
|
+
targetFormat,
|
|
275
|
+
subsetInfo.variationAxes,
|
|
276
|
+
featureGlyphIds
|
|
277
|
+
)
|
|
278
|
+
: null;
|
|
279
|
+
const cachedResult = diskCache && diskCache.get(cacheKey);
|
|
280
|
+
|
|
281
|
+
if (cachedResult) {
|
|
282
|
+
subsetPromiseMap.set(promiseId, Promise.resolve(cachedResult));
|
|
283
|
+
} else {
|
|
284
|
+
const subsetCall =
|
|
285
|
+
featureGlyphIds && featureGlyphIds.length > 0
|
|
286
|
+
? subsetFontWithGlyphs(fontBuffer, text, {
|
|
287
|
+
targetFormat,
|
|
288
|
+
glyphIds: featureGlyphIds,
|
|
289
|
+
variationAxes: subsetInfo.variationAxes,
|
|
290
|
+
})
|
|
291
|
+
: subsetFont(fontBuffer, text, {
|
|
292
|
+
targetFormat,
|
|
293
|
+
variationAxes: subsetInfo.variationAxes,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
subsetPromiseMap.set(
|
|
297
|
+
promiseId,
|
|
298
|
+
subsetCall
|
|
299
|
+
.then((result) => {
|
|
300
|
+
if (diskCache && result) {
|
|
301
|
+
diskCache.set(cacheKey, result);
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
})
|
|
305
|
+
.catch((err) => {
|
|
306
|
+
const error = new Error(err.message);
|
|
307
|
+
error.asset = fontAssetsByUrl.get(fontUrl);
|
|
308
|
+
assetGraph.warn(error);
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Await all subset promises, then assign results synchronously to avoid
|
|
317
|
+
// race conditions when multiple formats resolve concurrently.
|
|
318
|
+
const promiseKeys = [...subsetPromiseMap.keys()];
|
|
319
|
+
const promiseResults = await Promise.all(subsetPromiseMap.values());
|
|
320
|
+
const resolvedSubsets = new Map();
|
|
321
|
+
for (let i = 0; i < promiseKeys.length; i++) {
|
|
322
|
+
resolvedSubsets.set(promiseKeys[i], promiseResults[i]);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
assignSubsetResults(
|
|
326
|
+
canonicalFontUsageByUrl,
|
|
327
|
+
subsetResultsByFontUrl,
|
|
328
|
+
resolvedSubsets,
|
|
329
|
+
formats
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
propagateSubsets(
|
|
333
|
+
htmlOrSvgAssetTextsWithProps,
|
|
334
|
+
canonicalFontUsageByUrl,
|
|
335
|
+
subsetResultsByFontUrl
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
return fontAssetsByUrl;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
module.exports = {
|
|
342
|
+
getSubsetPromiseId,
|
|
343
|
+
getSubsetsForFontUsage,
|
|
344
|
+
// Exported for testing
|
|
345
|
+
_subsetCacheKey: subsetCacheKey,
|
|
346
|
+
_SubsetDiskCache: SubsetDiskCache,
|
|
347
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// A much, much smarter person than me solved this problem, and their code represents the bulk of the work here:
|
|
2
|
+
// http://stackoverflow.com/questions/2270910/how-to-convert-sequence-of-numbers-in-an-array-to-range-of-numbers
|
|
3
|
+
|
|
4
|
+
function getHexValue(num) {
|
|
5
|
+
return num.toString(16).toUpperCase();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generates a unicode-range string from an array of unicode codepoints
|
|
10
|
+
* @param {Number[]} codePoints The code points
|
|
11
|
+
* @return {String} The resulting [unicode-range](https://developer.mozilla.org/en-US/docs/Web/CSS/%40font-face/unicode-range)
|
|
12
|
+
*/
|
|
13
|
+
const getUnicodeRanges = (codePoints) => {
|
|
14
|
+
const ranges = [];
|
|
15
|
+
let start, end;
|
|
16
|
+
|
|
17
|
+
codePoints.sort((a, b) => a - b);
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < codePoints.length; i++) {
|
|
20
|
+
start = codePoints[i];
|
|
21
|
+
end = start;
|
|
22
|
+
|
|
23
|
+
while (codePoints[i + 1] - codePoints[i] === 1) {
|
|
24
|
+
end = codePoints[i + 1];
|
|
25
|
+
i++;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ranges.push(
|
|
29
|
+
start === end
|
|
30
|
+
? `U+${getHexValue(start)}`
|
|
31
|
+
: `U+${getHexValue(start)}-${getHexValue(end)}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return ranges.toString();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
module.exports = getUnicodeRanges;
|
package/lib/unquote.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function unescapeCssString(str) {
|
|
2
|
+
return str.replace(
|
|
3
|
+
/\\([0-9a-f]{1,6})(\s?)/gi,
|
|
4
|
+
($0, hexChars, followingWhitespace) =>
|
|
5
|
+
`${String.fromCodePoint(parseInt(hexChars, 16))}${
|
|
6
|
+
hexChars.length === 6 ? followingWhitespace : ''
|
|
7
|
+
}`
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = function unquote(str) {
|
|
12
|
+
if (typeof str !== 'string') {
|
|
13
|
+
return str;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return str.replace(
|
|
17
|
+
/^'([^']*)'$|^"([^"]*)"$/,
|
|
18
|
+
($0, singleQuoted, doubleQuoted) =>
|
|
19
|
+
typeof singleQuoted === 'string'
|
|
20
|
+
? unescapeCssString(singleQuoted.replace(/\\'/g, "'"))
|
|
21
|
+
: unescapeCssString(doubleQuoted.replace(/\\"/g, '"'))
|
|
22
|
+
);
|
|
23
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const getFontInfo = require('./getFontInfo');
|
|
2
|
+
const parseFontVariationSettings = require('./parseFontVariationSettings');
|
|
3
|
+
|
|
4
|
+
const standardVariationAxes = new Set(['wght', 'wdth', 'ital', 'slnt', 'opsz']);
|
|
5
|
+
// It would be very hard to trace statically which values of opsz (font-optical-sizing)
|
|
6
|
+
// are going to be used, so we ignore that one:
|
|
7
|
+
const ignoredVariationAxes = new Set(['opsz']);
|
|
8
|
+
|
|
9
|
+
function clamp(value, min, max) {
|
|
10
|
+
return Math.min(Math.max(value, min), max);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function renderNumberRange(min, max) {
|
|
14
|
+
if (min === max) {
|
|
15
|
+
return String(min);
|
|
16
|
+
} else {
|
|
17
|
+
return `${min}-${max}`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getVariationAxisUsage(
|
|
22
|
+
htmlOrSvgAssetTextsWithProps,
|
|
23
|
+
parseFontWeightRange,
|
|
24
|
+
parseFontStretchRange
|
|
25
|
+
) {
|
|
26
|
+
const seenAxisValuesByFontUrlAndAxisName = new Map();
|
|
27
|
+
|
|
28
|
+
function noteUsedValue(fontUrl, axisName, axisValue) {
|
|
29
|
+
let seenAxes = seenAxisValuesByFontUrlAndAxisName.get(fontUrl);
|
|
30
|
+
if (!seenAxes) {
|
|
31
|
+
seenAxes = new Map();
|
|
32
|
+
seenAxisValuesByFontUrlAndAxisName.set(fontUrl, seenAxes);
|
|
33
|
+
}
|
|
34
|
+
if (seenAxes.has(axisName)) {
|
|
35
|
+
seenAxes.get(axisName).add(axisValue);
|
|
36
|
+
} else {
|
|
37
|
+
seenAxes.set(axisName, new Set([axisValue]));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Since fontUsages are built from shared templates, all pages produce
|
|
42
|
+
// the same fontStyles/fontWeights/etc. for a given fontUrl. Process
|
|
43
|
+
// each unique fontUrl only once to avoid num_pages × redundant iterations.
|
|
44
|
+
const seenFontUrls = new Set();
|
|
45
|
+
for (const { fontUsages } of htmlOrSvgAssetTextsWithProps) {
|
|
46
|
+
for (const {
|
|
47
|
+
fontUrl,
|
|
48
|
+
fontStyles,
|
|
49
|
+
fontWeights,
|
|
50
|
+
fontStretches,
|
|
51
|
+
fontVariationSettings,
|
|
52
|
+
props,
|
|
53
|
+
} of fontUsages) {
|
|
54
|
+
if (seenFontUrls.has(fontUrl)) continue;
|
|
55
|
+
seenFontUrls.add(fontUrl);
|
|
56
|
+
if (fontStyles.has('italic')) {
|
|
57
|
+
noteUsedValue(fontUrl, 'ital', 1);
|
|
58
|
+
}
|
|
59
|
+
// If any font-style value except italic is seen (including normal or oblique)
|
|
60
|
+
// we're also utilizing value 0:
|
|
61
|
+
if (fontStyles.size > (fontStyles.has('italic') ? 1 : 0)) {
|
|
62
|
+
noteUsedValue(fontUrl, 'ital', 0);
|
|
63
|
+
}
|
|
64
|
+
if (fontStyles.has('oblique')) {
|
|
65
|
+
// https://www.w3.org/TR/css-fonts-4/#font-style-prop
|
|
66
|
+
// oblique <angle>?
|
|
67
|
+
// [...] The lack of an <angle> represents 14deg.
|
|
68
|
+
// And also:
|
|
69
|
+
// Note: the OpenType slnt axis is defined with a positive angle meaning a counter-clockwise slant, the opposite direction to CSS.
|
|
70
|
+
// The CSS implementation will take this into account when using variations to produce oblique faces.
|
|
71
|
+
noteUsedValue(fontUrl, 'slnt', -14);
|
|
72
|
+
}
|
|
73
|
+
// If any font-style value except oblique is seen (including normal or italic)
|
|
74
|
+
// we're also utilizing value 0:
|
|
75
|
+
if (fontStyles.size > (fontStyles.has('oblique') ? 1 : 0)) {
|
|
76
|
+
noteUsedValue(fontUrl, 'slnt', 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const minMaxFontWeight = parseFontWeightRange(props['font-weight']);
|
|
80
|
+
for (const fontWeight of fontWeights) {
|
|
81
|
+
noteUsedValue(fontUrl, 'wght', clamp(fontWeight, ...minMaxFontWeight));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const minMaxFontStretch = parseFontStretchRange(props['font-stretch']);
|
|
85
|
+
for (const fontStrech of fontStretches) {
|
|
86
|
+
noteUsedValue(fontUrl, 'wdth', clamp(fontStrech, ...minMaxFontStretch));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const fontVariationSettingsValue of fontVariationSettings) {
|
|
90
|
+
for (const [axisName, axisValue] of parseFontVariationSettings(
|
|
91
|
+
fontVariationSettingsValue
|
|
92
|
+
)) {
|
|
93
|
+
noteUsedValue(fontUrl, axisName, axisValue);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { seenAxisValuesByFontUrlAndAxisName };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function getVariationAxisBounds(
|
|
103
|
+
fontAssetsByUrl,
|
|
104
|
+
fontUrl,
|
|
105
|
+
seenAxisValuesByFontUrlAndAxisName
|
|
106
|
+
) {
|
|
107
|
+
let fontInfo;
|
|
108
|
+
try {
|
|
109
|
+
fontInfo = await getFontInfo(fontAssetsByUrl.get(fontUrl).rawSrc);
|
|
110
|
+
} catch {
|
|
111
|
+
// Invalid font -- skip instancing, return safe defaults
|
|
112
|
+
return {
|
|
113
|
+
fullyInstanced: false,
|
|
114
|
+
numAxesPinned: 0,
|
|
115
|
+
numAxesReduced: 0,
|
|
116
|
+
variationAxes: {},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const variationAxes = {};
|
|
121
|
+
let fullyInstanced = true;
|
|
122
|
+
let numAxesPinned = 0;
|
|
123
|
+
let numAxesReduced = 0;
|
|
124
|
+
const fontVariationEntries = Object.entries(fontInfo.variationAxes);
|
|
125
|
+
const seenAxisValuesByAxisName =
|
|
126
|
+
seenAxisValuesByFontUrlAndAxisName.get(fontUrl);
|
|
127
|
+
if (fontVariationEntries.length > 0 && seenAxisValuesByAxisName) {
|
|
128
|
+
for (const [
|
|
129
|
+
axisName,
|
|
130
|
+
{ min, max, default: defaultValue },
|
|
131
|
+
] of fontVariationEntries) {
|
|
132
|
+
let seenAxisValues = seenAxisValuesByAxisName.get(axisName);
|
|
133
|
+
if (!seenAxisValues && !ignoredVariationAxes.has(axisName)) {
|
|
134
|
+
seenAxisValues = new Set([defaultValue]);
|
|
135
|
+
}
|
|
136
|
+
if (seenAxisValues && seenAxisValues.size === 1) {
|
|
137
|
+
variationAxes[axisName] = clamp([...seenAxisValues][0], min, max);
|
|
138
|
+
numAxesPinned += 1;
|
|
139
|
+
} else if (seenAxisValues) {
|
|
140
|
+
const minSeenValue = Math.min(...seenAxisValues);
|
|
141
|
+
const maxSeenValue = Math.max(...seenAxisValues);
|
|
142
|
+
variationAxes[axisName] = {
|
|
143
|
+
min: Math.max(minSeenValue, min),
|
|
144
|
+
max: Math.min(maxSeenValue, max),
|
|
145
|
+
};
|
|
146
|
+
fullyInstanced = false;
|
|
147
|
+
if (minSeenValue > min || maxSeenValue < max) {
|
|
148
|
+
numAxesReduced += 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { fullyInstanced, numAxesPinned, numAxesReduced, variationAxes };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
standardVariationAxes,
|
|
158
|
+
ignoredVariationAxes,
|
|
159
|
+
renderNumberRange,
|
|
160
|
+
getVariationAxisUsage,
|
|
161
|
+
getVariationAxisBounds,
|
|
162
|
+
};
|