@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.
@@ -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));