@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,193 @@
|
|
|
1
|
+
const { readFile } = require('fs').promises;
|
|
2
|
+
const fontverter = require('fontverter');
|
|
3
|
+
const { toSfnt } = require('./sfntCache');
|
|
4
|
+
|
|
5
|
+
// hb_subset_sets_t enum values from hb-subset.h
|
|
6
|
+
// https://github.com/harfbuzz/harfbuzz/blob/main/src/hb-subset.h
|
|
7
|
+
const HB_SUBSET_SETS_GLYPH_INDEX = 0;
|
|
8
|
+
const HB_SUBSET_SETS_LAYOUT_FEATURE_TAG = 6;
|
|
9
|
+
|
|
10
|
+
// Shared WASM loader — reuses the same harfbuzzjs WASM binary as subset-font.
|
|
11
|
+
// This file exists because subset-font doesn't expose a glyphIds option.
|
|
12
|
+
// Loaded lazily on first call to avoid startup latency when subsetting
|
|
13
|
+
// is never needed (e.g. no fonts use feature settings).
|
|
14
|
+
let _wasmExports;
|
|
15
|
+
let _loadPromise;
|
|
16
|
+
async function loadHarfbuzz() {
|
|
17
|
+
if (!_loadPromise) {
|
|
18
|
+
_loadPromise = (async () => {
|
|
19
|
+
const {
|
|
20
|
+
instance: { exports },
|
|
21
|
+
} = await WebAssembly.instantiate(
|
|
22
|
+
await readFile(require.resolve('harfbuzzjs/hb-subset.wasm'))
|
|
23
|
+
);
|
|
24
|
+
_wasmExports = exports;
|
|
25
|
+
return exports;
|
|
26
|
+
})();
|
|
27
|
+
}
|
|
28
|
+
return _loadPromise;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Re-create heapu8 on every call — WASM memory.buffer is detached when
|
|
32
|
+
// memory grows (e.g. via malloc), so a cached Uint8Array would silently
|
|
33
|
+
// read/write stale data.
|
|
34
|
+
function getHeapu8() {
|
|
35
|
+
return new Uint8Array(_wasmExports.memory.buffer);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function HB_TAG(str) {
|
|
39
|
+
return str.split('').reduce(function (a, ch) {
|
|
40
|
+
return (a << 8) + ch.charCodeAt(0);
|
|
41
|
+
}, 0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pinAxisLocation(exports, input, face, axisName, value) {
|
|
45
|
+
const ok = exports.hb_subset_input_pin_axis_location(
|
|
46
|
+
input,
|
|
47
|
+
face,
|
|
48
|
+
HB_TAG(axisName),
|
|
49
|
+
value
|
|
50
|
+
);
|
|
51
|
+
if (!ok) {
|
|
52
|
+
throw new Error(`Failed to pin axis ${axisName} to ${value}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setAxisRange(exports, input, face, axisName, value) {
|
|
57
|
+
const ok = exports.hb_subset_input_set_axis_range(
|
|
58
|
+
input,
|
|
59
|
+
face,
|
|
60
|
+
HB_TAG(axisName),
|
|
61
|
+
value.min,
|
|
62
|
+
value.max,
|
|
63
|
+
value.default ?? NaN
|
|
64
|
+
);
|
|
65
|
+
if (!ok) {
|
|
66
|
+
throw new Error(`Failed to set axis range for ${axisName}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function configureSubsetInput(
|
|
71
|
+
exports,
|
|
72
|
+
input,
|
|
73
|
+
face,
|
|
74
|
+
text,
|
|
75
|
+
glyphIds,
|
|
76
|
+
variationAxes
|
|
77
|
+
) {
|
|
78
|
+
// Keep all layout features (--font-features=*)
|
|
79
|
+
const layoutFeatures = exports.hb_subset_input_set(
|
|
80
|
+
input,
|
|
81
|
+
HB_SUBSET_SETS_LAYOUT_FEATURE_TAG
|
|
82
|
+
);
|
|
83
|
+
exports.hb_set_clear(layoutFeatures);
|
|
84
|
+
exports.hb_set_invert(layoutFeatures);
|
|
85
|
+
|
|
86
|
+
// Add unicode codepoints
|
|
87
|
+
const inputUnicodes = exports.hb_subset_input_unicode_set(input);
|
|
88
|
+
for (const c of text) {
|
|
89
|
+
exports.hb_set_add(inputUnicodes, c.codePointAt(0));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Add specific glyph IDs to preserve alternate glyphs
|
|
93
|
+
if (glyphIds && glyphIds.length > 0) {
|
|
94
|
+
const glyphSet = exports.hb_subset_input_set(
|
|
95
|
+
input,
|
|
96
|
+
HB_SUBSET_SETS_GLYPH_INDEX
|
|
97
|
+
);
|
|
98
|
+
for (const gid of glyphIds) {
|
|
99
|
+
exports.hb_set_add(glyphSet, gid);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle variation axes
|
|
104
|
+
if (variationAxes) {
|
|
105
|
+
for (const [axisName, value] of Object.entries(variationAxes)) {
|
|
106
|
+
if (typeof value === 'number') {
|
|
107
|
+
pinAxisLocation(exports, input, face, axisName, value);
|
|
108
|
+
} else if (value && typeof value === 'object') {
|
|
109
|
+
setAxisRange(exports, input, face, axisName, value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractSubsetFont(exports, heapu8, subset) {
|
|
116
|
+
const result = exports.hb_face_reference_blob(subset);
|
|
117
|
+
const offset = exports.hb_blob_get_data(result, 0);
|
|
118
|
+
const subsetByteLength = exports.hb_blob_get_length(result);
|
|
119
|
+
|
|
120
|
+
if (subsetByteLength === 0) {
|
|
121
|
+
exports.hb_blob_destroy(result);
|
|
122
|
+
throw new Error('Failed to create subset font');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const subsetFont = Buffer.from(
|
|
126
|
+
heapu8.subarray(offset, offset + subsetByteLength)
|
|
127
|
+
);
|
|
128
|
+
exports.hb_blob_destroy(result);
|
|
129
|
+
return subsetFont;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Subset a font, optionally including specific glyph IDs.
|
|
134
|
+
*
|
|
135
|
+
* This wraps HarfBuzz's subsetter like subset-font does, but additionally
|
|
136
|
+
* supports adding glyph IDs to the subset input. This preserves GSUB
|
|
137
|
+
* alternate glyphs without including all codepoints from the original font.
|
|
138
|
+
*
|
|
139
|
+
* @param {Buffer} originalFont - The original font data
|
|
140
|
+
* @param {string} text - Unicode text whose codepoints to include
|
|
141
|
+
* @param {object} options
|
|
142
|
+
* @param {string} options.targetFormat - Output format (woff, woff2, truetype)
|
|
143
|
+
* @param {number[]} [options.glyphIds] - Additional glyph IDs to include
|
|
144
|
+
* @param {object} [options.variationAxes] - Variation axis settings
|
|
145
|
+
* @returns {Promise<Buffer>} The subsetted font
|
|
146
|
+
*/
|
|
147
|
+
async function subsetFontWithGlyphs(
|
|
148
|
+
originalFont,
|
|
149
|
+
text,
|
|
150
|
+
{ targetFormat, glyphIds, variationAxes } = {}
|
|
151
|
+
) {
|
|
152
|
+
const exports = await loadHarfbuzz();
|
|
153
|
+
|
|
154
|
+
// Reuse cached sfnt conversion when available (same buffer may have
|
|
155
|
+
// been converted by getFontInfo or collectFeatureGlyphIds already).
|
|
156
|
+
const ttf = await toSfnt(originalFont);
|
|
157
|
+
|
|
158
|
+
const fontBuffer = exports.malloc(ttf.byteLength);
|
|
159
|
+
// Fresh view — memory.buffer may have been detached by a prior malloc/grow.
|
|
160
|
+
getHeapu8().set(new Uint8Array(ttf), fontBuffer);
|
|
161
|
+
|
|
162
|
+
const blob = exports.hb_blob_create(fontBuffer, ttf.byteLength, 2, 0, 0);
|
|
163
|
+
const face = exports.hb_face_create(blob, 0);
|
|
164
|
+
exports.hb_blob_destroy(blob);
|
|
165
|
+
|
|
166
|
+
const input = exports.hb_subset_input_create_or_fail();
|
|
167
|
+
if (input === 0) {
|
|
168
|
+
exports.hb_face_destroy(face);
|
|
169
|
+
exports.free(fontBuffer);
|
|
170
|
+
throw new Error('hb_subset_input_create_or_fail returned zero');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let subset = 0;
|
|
174
|
+
try {
|
|
175
|
+
configureSubsetInput(exports, input, face, text, glyphIds, variationAxes);
|
|
176
|
+
|
|
177
|
+
subset = exports.hb_subset_or_fail(face, input);
|
|
178
|
+
if (subset === 0) {
|
|
179
|
+
throw new Error('hb_subset_or_fail returned zero');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const subsetFont = extractSubsetFont(exports, getHeapu8(), subset);
|
|
183
|
+
return fontverter.convert(subsetFont, targetFormat, 'truetype');
|
|
184
|
+
} finally {
|
|
185
|
+
if (subset) exports.hb_face_destroy(subset);
|
|
186
|
+
exports.hb_subset_input_destroy(input);
|
|
187
|
+
exports.hb_face_destroy(face);
|
|
188
|
+
exports.free(fontBuffer);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const limiter = require('p-limit')(1);
|
|
193
|
+
module.exports = (...args) => limiter(() => subsetFontWithGlyphs(...args));
|