@turntrout/subfont 1.1.0 → 1.2.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/CLAUDE.md CHANGED
@@ -34,7 +34,7 @@ pnpm run check-coverage # Verify coverage thresholds
34
34
  - Built on **assetgraph** for HTML/CSS asset graph traversal
35
35
  - Uses **puppeteer-core** for headless browser font tracing
36
36
  - **font-tracer** traces which fonts are used on each page
37
- - **subset-font** / **harfbuzzjs** for WOFF2 subsetting
37
+ - **harfbuzzjs** for WOFF2 subsetting (via direct WASM calls in `lib/subsetFontWithGlyphs.js`)
38
38
  - `lib/subsetFonts.js` — Main orchestration logic
39
39
  - `lib/FontTracerPool.js` — Manages puppeteer browser pool for parallel tracing
40
40
 
package/README.md CHANGED
@@ -4,6 +4,25 @@
4
4
 
5
5
  A faster fork of [subfont](https://github.com/Munter/subfont) that subsets web fonts to only the characters used on your pages. Adds parallel tracing, disk caching, woff2-only output, and always-on variable font instancing. On [`turntrout.com`](https://github.com/alexander-turner/TurnTrout.com) (382 pages, 20+ font variants), switching to this fork cut font subsetting from [111 minutes](https://github.com/alexander-turner/TurnTrout.com/actions/runs/23470135763) to [28 minutes](https://github.com/alexander-turner/TurnTrout.com/actions/runs/23518006824).
6
6
 
7
+ ### Aggressive woff2 subsetting
8
+
9
+ subfont produces dramatically smaller font files by stripping data that browsers never use:
10
+
11
+ | Optimization | Technique |
12
+ | --------------------------- | ---------------------------------------------------------------------------------- |
13
+ | Hinting removal | Strips TrueType hinting instructions (browsers auto-hint) |
14
+ | Name table pruning | Keeps only the 4 IDs browsers read (family, subfamily, full name, PostScript name) |
15
+ | Table stripping | Drops DSIG, LTSH, VDMX, hdmx, gasp, PCLT |
16
+ | CSS-aware feature filtering | Only collects alternate glyphs for OpenType features actually used in your CSS |
17
+
18
+ On the [`turntrout.com/design`](https://turntrout.com/design) page, a typical font subset (OpenSans, woff2) is **48-68% smaller** than a naive subset of the same glyphs:
19
+
20
+ | Text sample | Naive subset | subfont | Savings |
21
+ | ----------------- | ------------ | ------- | ------- |
22
+ | Heading (short) | 2,604 B | 824 B | **68%** |
23
+ | Paragraph | 4,052 B | 1,840 B | **55%** |
24
+ | Full page charset | 5,268 B | 2,716 B | **48%** |
25
+
7
26
  ## Install
8
27
 
9
28
  ```
@@ -1,22 +1,40 @@
1
1
  const { toSfnt } = require('./sfntCache');
2
2
 
3
+ // GSUB feature tags that can produce alternate glyphs. Used as the
4
+ // fallback set when CSS doesn't specify which features are in use.
5
+ // Keep in sync with fontVariantToOTTags in collectTextsByPage.js.
3
6
  const GSUB_FEATURE_TAGS = new Set([
4
7
  'aalt',
8
+ 'afrc',
9
+ 'c2pc',
5
10
  'c2sc',
6
11
  'calt',
7
12
  'clig',
8
13
  'dlig',
9
14
  'dnom',
10
15
  'frac',
16
+ 'fwid',
17
+ 'hist',
18
+ 'hlig',
19
+ 'jp04',
20
+ 'jp78',
21
+ 'jp83',
22
+ 'jp90',
11
23
  'liga',
12
24
  'lnum',
25
+ 'nalt',
13
26
  'numr',
14
27
  'onum',
15
28
  'ordn',
29
+ 'ornm',
30
+ 'pcap',
16
31
  'pnum',
32
+ 'pwid',
33
+ 'ruby',
17
34
  'salt',
18
35
  'sinf',
19
36
  'smcp',
37
+ 'smpl',
20
38
  'ss01',
21
39
  'ss02',
22
40
  'ss03',
@@ -40,13 +58,16 @@ const GSUB_FEATURE_TAGS = new Set([
40
58
  'subs',
41
59
  'sups',
42
60
  'swsh',
61
+ 'titl',
43
62
  'tnum',
63
+ 'trad',
64
+ 'unic',
44
65
  'zero',
45
66
  ]);
46
67
 
47
68
  const enqueueWasm = require('./wasmQueue');
48
69
 
49
- async function collectFeatureGlyphIdsImpl(fontBuffer, text) {
70
+ async function collectFeatureGlyphIdsImpl(fontBuffer, text, cssFeatureTags) {
50
71
  const harfbuzzJs = await require('harfbuzzjs');
51
72
  const sfnt = await toSfnt(fontBuffer);
52
73
 
@@ -56,8 +77,16 @@ async function collectFeatureGlyphIdsImpl(fontBuffer, text) {
56
77
 
57
78
  try {
58
79
  const fontFeatures = new Set(face.getTableFeatureTags('GSUB'));
80
+
81
+ // When CSS specifies which features are used, only test those.
82
+ // Otherwise fall back to the full set of supported GSUB features.
83
+ const allowedTags =
84
+ cssFeatureTags && cssFeatureTags.length > 0
85
+ ? new Set(cssFeatureTags)
86
+ : GSUB_FEATURE_TAGS;
87
+
59
88
  const featuresToTest = [...fontFeatures].filter((tag) =>
60
- GSUB_FEATURE_TAGS.has(tag)
89
+ allowedTags.has(tag)
61
90
  );
62
91
 
63
92
  if (featuresToTest.length === 0) return [];
@@ -108,8 +137,10 @@ async function collectFeatureGlyphIdsImpl(fontBuffer, text) {
108
137
  }
109
138
  }
110
139
 
111
- function collectFeatureGlyphIds(fontBuffer, text) {
112
- return enqueueWasm(() => collectFeatureGlyphIdsImpl(fontBuffer, text));
140
+ function collectFeatureGlyphIds(fontBuffer, text, cssFeatureTags) {
141
+ return enqueueWasm(() =>
142
+ collectFeatureGlyphIdsImpl(fontBuffer, text, cssFeatureTags)
143
+ );
113
144
  }
114
145
 
115
146
  module.exports = collectFeatureGlyphIds;
@@ -45,11 +45,118 @@ const featureSettingsProps = new Set([
45
45
  'font-variant-position',
46
46
  ]);
47
47
 
48
- function ruleUsesFeatureSettings(rule) {
49
- return rule.nodes.some(
50
- (node) =>
51
- node.type === 'decl' && featureSettingsProps.has(node.prop.toLowerCase())
52
- );
48
+ // Map font-variant-* CSS values to their corresponding OpenType feature tags.
49
+ const fontVariantToOTTags = {
50
+ 'font-variant-ligatures': {
51
+ 'common-ligatures': ['liga', 'clig'],
52
+ 'no-common-ligatures': ['liga', 'clig'],
53
+ 'discretionary-ligatures': ['dlig'],
54
+ 'no-discretionary-ligatures': ['dlig'],
55
+ 'historical-ligatures': ['hlig'],
56
+ 'no-historical-ligatures': ['hlig'],
57
+ contextual: ['calt'],
58
+ 'no-contextual': ['calt'],
59
+ },
60
+ 'font-variant-caps': {
61
+ 'small-caps': ['smcp'],
62
+ 'all-small-caps': ['smcp', 'c2sc'],
63
+ 'petite-caps': ['pcap'],
64
+ 'all-petite-caps': ['pcap', 'c2pc'],
65
+ unicase: ['unic'],
66
+ 'titling-caps': ['titl'],
67
+ },
68
+ 'font-variant-numeric': {
69
+ 'lining-nums': ['lnum'],
70
+ 'oldstyle-nums': ['onum'],
71
+ 'proportional-nums': ['pnum'],
72
+ 'tabular-nums': ['tnum'],
73
+ 'diagonal-fractions': ['frac'],
74
+ 'stacked-fractions': ['afrc'],
75
+ ordinal: ['ordn'],
76
+ 'slashed-zero': ['zero'],
77
+ },
78
+ 'font-variant-position': {
79
+ sub: ['subs'],
80
+ super: ['sups'],
81
+ },
82
+ 'font-variant-east-asian': {
83
+ jis78: ['jp78'],
84
+ jis83: ['jp83'],
85
+ jis90: ['jp90'],
86
+ jis04: ['jp04'],
87
+ simplified: ['smpl'],
88
+ traditional: ['trad'],
89
+ 'proportional-width': ['pwid'],
90
+ 'full-width': ['fwid'],
91
+ ruby: ['ruby'],
92
+ },
93
+ };
94
+
95
+ // Extract OpenType feature tags referenced by a CSS declaration.
96
+ function extractFeatureTagsFromDecl(prop, value) {
97
+ const tags = new Set();
98
+ const propLower = prop.toLowerCase();
99
+
100
+ if (propLower === 'font-feature-settings') {
101
+ // Parse quoted 4-letter tags: "liga" 1, 'dlig', etc.
102
+ const re = /["']([a-zA-Z0-9]{4})["']/g;
103
+ let m;
104
+ while ((m = re.exec(value)) !== null) {
105
+ tags.add(m[1]);
106
+ }
107
+ return tags;
108
+ }
109
+
110
+ if (propLower === 'font-variant-alternates') {
111
+ const v = value.toLowerCase();
112
+ if (v.includes('historical-forms')) tags.add('hist');
113
+ if (/stylistic\s*\(/.test(v)) tags.add('salt');
114
+ if (/swash\s*\(/.test(v)) tags.add('swsh');
115
+ if (/ornaments\s*\(/.test(v)) tags.add('ornm');
116
+ if (/annotation\s*\(/.test(v)) tags.add('nalt');
117
+ if (/styleset\s*\(/.test(v)) {
118
+ for (let i = 1; i <= 20; i++) {
119
+ tags.add(`ss${String(i).padStart(2, '0')}`);
120
+ }
121
+ }
122
+ if (/character-variant\s*\(/.test(v)) {
123
+ for (let i = 1; i <= 99; i++) {
124
+ tags.add(`cv${String(i).padStart(2, '0')}`);
125
+ }
126
+ }
127
+ return tags;
128
+ }
129
+
130
+ const mapping = fontVariantToOTTags[propLower];
131
+ if (mapping) {
132
+ // Split into tokens for exact keyword matching — substring matching
133
+ // would falsely trigger e.g. "sub" inside "super".
134
+ const tokens = new Set(value.toLowerCase().split(/\s+/));
135
+ for (const [keyword, otTags] of Object.entries(mapping)) {
136
+ if (tokens.has(keyword)) {
137
+ for (const t of otTags) tags.add(t);
138
+ }
139
+ }
140
+ }
141
+ return tags;
142
+ }
143
+
144
+ // Collect feature tags from all feature-related declarations in a CSS rule.
145
+ function ruleFeatureTags(rule) {
146
+ const tags = new Set();
147
+ let hasFeatureDecl = false;
148
+ for (const node of rule.nodes) {
149
+ if (
150
+ node.type === 'decl' &&
151
+ featureSettingsProps.has(node.prop.toLowerCase())
152
+ ) {
153
+ hasFeatureDecl = true;
154
+ for (const t of extractFeatureTagsFromDecl(node.prop, node.value)) {
155
+ tags.add(t);
156
+ }
157
+ }
158
+ }
159
+ return hasFeatureDecl ? tags : null;
53
160
  }
54
161
 
55
162
  function ruleFontFamily(rule) {
@@ -62,32 +169,108 @@ function ruleFontFamily(rule) {
62
169
  return null;
63
170
  }
64
171
 
172
+ // Add all items from `tags` into the Set stored at `key` in `map`,
173
+ // creating the Set if it doesn't exist yet.
174
+ function addTagsToMapEntry(map, key, tags) {
175
+ let s = map.get(key);
176
+ if (!s) {
177
+ s = new Set();
178
+ map.set(key, s);
179
+ }
180
+ for (const t of tags) s.add(t);
181
+ }
182
+
183
+ // Record the OT tags from a single CSS rule into featureTagsByFamily,
184
+ // keyed by font-family (or '*' when no font-family is specified).
185
+ function recordRuleFeatureTags(rule, featureTagsByFamily) {
186
+ const tags = ruleFeatureTags(rule);
187
+ if (!tags) return null;
188
+
189
+ const fontFamily = ruleFontFamily(rule);
190
+ if (!fontFamily) {
191
+ if (featureTagsByFamily) addTagsToMapEntry(featureTagsByFamily, '*', tags);
192
+ return true; // signals "all families"
193
+ }
194
+
195
+ const families = cssFontParser.parseFontFamily(fontFamily);
196
+ if (featureTagsByFamily) {
197
+ for (const family of families) {
198
+ addTagsToMapEntry(featureTagsByFamily, family.toLowerCase(), tags);
199
+ }
200
+ }
201
+ return families;
202
+ }
203
+
65
204
  // Determine which font-families use font-feature-settings or font-variant-*.
66
205
  // Returns null (none detected), a Set of lowercase family names, or true (all).
67
- function findFontFamiliesWithFeatureSettings(stylesheetsWithPredicates) {
206
+ // Also populates featureTagsByFamily with the OT tags per family (lowercase).
207
+ function findFontFamiliesWithFeatureSettings(
208
+ stylesheetsWithPredicates,
209
+ featureTagsByFamily
210
+ ) {
68
211
  let result = null;
69
212
  for (const { asset } of stylesheetsWithPredicates) {
70
213
  if (!asset || !asset.parseTree) continue;
71
214
  asset.parseTree.walkRules((rule) => {
72
- if (result === true) return;
73
- if (!ruleUsesFeatureSettings(rule)) return;
215
+ if (result === true && !featureTagsByFamily) return;
74
216
 
75
- const fontFamily = ruleFontFamily(rule);
76
- if (!fontFamily) {
77
- // No font-family in this rule — conservatively assume all fonts
217
+ const recorded = recordRuleFeatureTags(rule, featureTagsByFamily);
218
+ if (!recorded) return;
219
+
220
+ if (recorded === true) {
78
221
  result = true;
79
- return;
80
- }
81
- if (!result) result = new Set();
82
- for (const family of cssFontParser.parseFontFamily(fontFamily)) {
83
- result.add(family.toLowerCase());
222
+ } else {
223
+ if (!result) result = new Set();
224
+ for (const family of recorded) {
225
+ result.add(family.toLowerCase());
226
+ }
84
227
  }
85
228
  });
86
- if (result === true) break;
229
+ if (result === true && !featureTagsByFamily) break;
87
230
  }
88
231
  return result;
89
232
  }
90
233
 
234
+ // Determine whether a template's font families use feature settings, and
235
+ // collect the corresponding OT feature tags from featureTagsByFamily.
236
+ function resolveFeatureSettings(
237
+ fontFamilies,
238
+ fontFamiliesWithFeatureSettings,
239
+ featureTagsByFamily
240
+ ) {
241
+ let hasFontFeatureSettings = false;
242
+ if (fontFamiliesWithFeatureSettings === true) {
243
+ hasFontFeatureSettings = true;
244
+ } else if (fontFamiliesWithFeatureSettings instanceof Set) {
245
+ for (const f of fontFamilies) {
246
+ if (fontFamiliesWithFeatureSettings.has(f.toLowerCase())) {
247
+ hasFontFeatureSettings = true;
248
+ break;
249
+ }
250
+ }
251
+ }
252
+
253
+ let fontFeatureTags;
254
+ if (hasFontFeatureSettings && featureTagsByFamily) {
255
+ const tags = new Set();
256
+ const globalTags = featureTagsByFamily.get('*');
257
+ if (globalTags) {
258
+ for (const t of globalTags) tags.add(t);
259
+ }
260
+ for (const f of fontFamilies) {
261
+ const familyTags = featureTagsByFamily.get(f.toLowerCase());
262
+ if (familyTags) {
263
+ for (const t of familyTags) tags.add(t);
264
+ }
265
+ }
266
+ if (tags.size > 0) {
267
+ fontFeatureTags = [...tags];
268
+ }
269
+ }
270
+
271
+ return { hasFontFeatureSettings, fontFeatureTags };
272
+ }
273
+
91
274
  const allInitialValues = require('./initialValueByProp');
92
275
  const initialValueByProp = {
93
276
  'font-style': allInitialValues['font-style'],
@@ -708,14 +891,17 @@ async function collectTextsByPage(
708
891
  }
709
892
  }
710
893
 
894
+ const featureTagsByFamily = new Map();
711
895
  const fontFamiliesWithFeatureSettings = findFontFamiliesWithFeatureSettings(
712
- stylesheetsWithPredicates
896
+ stylesheetsWithPredicates,
897
+ featureTagsByFamily
713
898
  );
714
899
 
715
900
  const result = {
716
901
  accumulatedFontFaceDeclarations,
717
902
  stylesheetsWithPredicates,
718
903
  fontFamiliesWithFeatureSettings,
904
+ featureTagsByFamily,
719
905
  fastPathKey: buildStylesheetKey(htmlOrSvgAsset, true),
720
906
  };
721
907
  stylesheetResultCache.set(key, result);
@@ -739,6 +925,7 @@ async function collectTextsByPage(
739
925
  accumulatedFontFaceDeclarations,
740
926
  stylesheetsWithPredicates,
741
927
  fontFamiliesWithFeatureSettings,
928
+ featureTagsByFamily,
742
929
  fastPathKey,
743
930
  } = getOrComputeStylesheetResults(htmlOrSvgAsset);
744
931
  fontFaceDeclarationsByHtmlOrSvgAsset.set(
@@ -755,6 +942,7 @@ async function collectTextsByPage(
755
942
  accumulatedFontFaceDeclarations,
756
943
  stylesheetsWithPredicates,
757
944
  fontFamiliesWithFeatureSettings,
945
+ featureTagsByFamily,
758
946
  stylesheetCacheKey: fastPathKey,
759
947
  });
760
948
  }
@@ -833,6 +1021,7 @@ async function collectTextsByPage(
833
1021
  textByProps: pd.textByProps,
834
1022
  accumulatedFontFaceDeclarations: pd.accumulatedFontFaceDeclarations,
835
1023
  fontFamiliesWithFeatureSettings: pd.fontFamiliesWithFeatureSettings,
1024
+ featureTagsByFamily: pd.featureTagsByFamily,
836
1025
  });
837
1026
  }
838
1027
 
@@ -872,6 +1061,7 @@ async function collectTextsByPage(
872
1061
  textByProps,
873
1062
  accumulatedFontFaceDeclarations,
874
1063
  fontFamiliesWithFeatureSettings,
1064
+ featureTagsByFamily,
875
1065
  } = htmlOrSvgAssetTextsWithPropsEntry;
876
1066
 
877
1067
  // Get or compute the snapped global entries for this declarations set
@@ -932,17 +1122,12 @@ async function collectTextsByPage(
932
1122
  uniqueCharsCache.set(pageTextStr, pageTextUnique);
933
1123
  }
934
1124
 
935
- let hasFontFeatureSettings = false;
936
- if (fontFamiliesWithFeatureSettings === true) {
937
- hasFontFeatureSettings = true;
938
- } else if (fontFamiliesWithFeatureSettings instanceof Set) {
939
- for (const f of template.fontFamilies) {
940
- if (fontFamiliesWithFeatureSettings.has(f.toLowerCase())) {
941
- hasFontFeatureSettings = true;
942
- break;
943
- }
944
- }
945
- }
1125
+ const { hasFontFeatureSettings, fontFeatureTags } =
1126
+ resolveFeatureSettings(
1127
+ template.fontFamilies,
1128
+ fontFamiliesWithFeatureSettings,
1129
+ featureTagsByFamily
1130
+ );
946
1131
 
947
1132
  return {
948
1133
  smallestOriginalSize: template.smallestOriginalSize,
@@ -959,6 +1144,7 @@ async function collectTextsByPage(
959
1144
  fontVariationSettings: template.fontVariationSettings,
960
1145
  preload: preloadFontUrls.has(template.fontUrl),
961
1146
  hasFontFeatureSettings,
1147
+ fontFeatureTags,
962
1148
  };
963
1149
  }
964
1150
  );
@@ -1003,3 +1189,7 @@ async function collectTextsByPage(
1003
1189
  }
1004
1190
 
1005
1191
  module.exports = collectTextsByPage;
1192
+
1193
+ // Exported for testing only
1194
+ module.exports._extractFeatureTagsFromDecl = extractFeatureTagsFromDecl;
1195
+ module.exports._resolveFeatureSettings = resolveFeatureSettings;
@@ -4,9 +4,16 @@ const { toSfnt } = require('./sfntCache');
4
4
 
5
5
  // hb_subset_sets_t enum values — https://github.com/harfbuzz/harfbuzz/blob/main/src/hb-subset.h
6
6
  const HB_SUBSET_SETS_GLYPH_INDEX = 0;
7
+ const HB_SUBSET_SETS_DROP_TABLE_TAG = 5;
7
8
  const HB_SUBSET_SETS_LAYOUT_FEATURE_TAG = 6;
9
+ const HB_SUBSET_SETS_NAME_ID = 4;
8
10
 
9
- // subset-font doesn't expose a glyphIds option, so we call harfbuzz directly.
11
+ // hb_subset_flags_t
12
+ const HB_SUBSET_FLAGS_NO_HINTING = 0x00000001;
13
+
14
+ // All font subsetting goes through harfbuzz directly so we can apply
15
+ // web-specific optimizations (no hinting, minimal name table, table
16
+ // stripping) and support explicit glyph-ID inclusion.
10
17
  let _wasmExports;
11
18
  let _loadPromise;
12
19
  async function loadHarfbuzz() {
@@ -62,6 +69,15 @@ function setAxisRange(exports, input, face, axisName, value) {
62
69
  }
63
70
  }
64
71
 
72
+ // Tables unnecessary for web rendering — safe to drop unconditionally.
73
+ // gasp is only meaningful when hinting is present (which we strip above).
74
+ const DROP_TABLE_TAGS = ['DSIG', 'LTSH', 'VDMX', 'hdmx', 'gasp', 'PCLT'];
75
+
76
+ // Name IDs needed for web fonts: family (1), subfamily (2), full name (4),
77
+ // PostScript name (6). Copyright (0), unique ID (3), version (5), and
78
+ // everything above 6 are display/license metadata that browsers never read.
79
+ const KEEP_NAME_IDS = [1, 2, 4, 6];
80
+
65
81
  function configureSubsetInput(
66
82
  exports,
67
83
  input,
@@ -70,6 +86,7 @@ function configureSubsetInput(
70
86
  glyphIds,
71
87
  variationAxes
72
88
  ) {
89
+ // --- Retain all layout features ---
73
90
  const layoutFeatures = exports.hb_subset_input_set(
74
91
  input,
75
92
  HB_SUBSET_SETS_LAYOUT_FEATURE_TAG
@@ -77,11 +94,33 @@ function configureSubsetInput(
77
94
  exports.hb_set_clear(layoutFeatures);
78
95
  exports.hb_set_invert(layoutFeatures);
79
96
 
97
+ // --- Strip hinting instructions (ignored by modern browsers) ---
98
+ const flags = exports.hb_subset_input_get_flags(input);
99
+ exports.hb_subset_input_set_flags(input, flags | HB_SUBSET_FLAGS_NO_HINTING);
100
+
101
+ // --- Keep only essential name table entries ---
102
+ const nameIdSet = exports.hb_subset_input_set(input, HB_SUBSET_SETS_NAME_ID);
103
+ exports.hb_set_clear(nameIdSet);
104
+ for (const id of KEEP_NAME_IDS) {
105
+ exports.hb_set_add(nameIdSet, id);
106
+ }
107
+
108
+ // --- Drop tables not needed for web rendering ---
109
+ const dropTableSet = exports.hb_subset_input_set(
110
+ input,
111
+ HB_SUBSET_SETS_DROP_TABLE_TAG
112
+ );
113
+ for (const tag of DROP_TABLE_TAGS) {
114
+ exports.hb_set_add(dropTableSet, HB_TAG(tag));
115
+ }
116
+
117
+ // --- Add unicode codepoints ---
80
118
  const inputUnicodes = exports.hb_subset_input_unicode_set(input);
81
119
  for (const c of text) {
82
120
  exports.hb_set_add(inputUnicodes, c.codePointAt(0));
83
121
  }
84
122
 
123
+ // --- Add explicit glyph IDs (from feature glyph collection) ---
85
124
  if (glyphIds && glyphIds.length > 0) {
86
125
  const glyphSet = exports.hb_subset_input_set(
87
126
  input,
@@ -92,6 +131,7 @@ function configureSubsetInput(
92
131
  }
93
132
  }
94
133
 
134
+ // --- Pin/reduce variation axes ---
95
135
  if (variationAxes) {
96
136
  for (const [axisName, value] of Object.entries(variationAxes)) {
97
137
  if (typeof value === 'number') {
@@ -607,7 +607,9 @@ async function subsetFonts(
607
607
  ({ fontUsages, htmlOrSvgAsset }) => ({
608
608
  assetFileName: htmlOrSvgAsset.nonInlineAncestor.urlOrDescription,
609
609
  fontUsages: fontUsages.map((fontUsage) =>
610
- (({ hasFontFeatureSettings, ...rest }) => rest)(fontUsage)
610
+ (({ hasFontFeatureSettings, fontFeatureTags, ...rest }) => rest)(
611
+ fontUsage
612
+ )
611
613
  ),
612
614
  })
613
615
  ),
@@ -1256,7 +1258,8 @@ async function subsetFonts(
1256
1258
  ({ fontUsages, htmlOrSvgAsset }) => ({
1257
1259
  assetFileName: htmlOrSvgAsset.nonInlineAncestor.urlOrDescription,
1258
1260
  fontUsages: fontUsages.map((fontUsage) =>
1259
- (({ subsets, hasFontFeatureSettings, ...rest }) => rest)(fontUsage)
1261
+ (({ subsets, hasFontFeatureSettings, fontFeatureTags, ...rest }) =>
1262
+ rest)(fontUsage)
1260
1263
  ),
1261
1264
  })
1262
1265
  ),
@@ -1,11 +1,14 @@
1
1
  const fs = require('fs/promises');
2
2
  const pathModule = require('path');
3
3
  const crypto = require('crypto');
4
- const subsetFont = require('subset-font');
5
4
  const { getVariationAxisBounds } = require('./variationAxes');
6
5
  const collectFeatureGlyphIds = require('./collectFeatureGlyphIds');
7
6
  const subsetFontWithGlyphs = require('./subsetFontWithGlyphs');
8
7
 
8
+ // Bump when subsetting behaviour changes to invalidate stale disk-cache
9
+ // entries (e.g. after adding hinting removal or table stripping).
10
+ const SUBSET_CACHE_VERSION = '2';
11
+
9
12
  function subsetCacheKey(
10
13
  fontBuffer,
11
14
  text,
@@ -14,6 +17,7 @@ function subsetCacheKey(
14
17
  featureGlyphIds
15
18
  ) {
16
19
  const hash = crypto.createHash('sha256');
20
+ hash.update(SUBSET_CACHE_VERSION);
17
21
  hash.update(fontBuffer);
18
22
  hash.update(text);
19
23
  hash.update(targetFormat);
@@ -182,7 +186,11 @@ async function getSubsetsForFontUsage(
182
186
 
183
187
  let featureGlyphIds;
184
188
  if (fontUsage.hasFontFeatureSettings && fontBuffer) {
185
- featureGlyphIds = await collectFeatureGlyphIds(fontBuffer, text);
189
+ featureGlyphIds = await collectFeatureGlyphIds(
190
+ fontBuffer,
191
+ text,
192
+ fontUsage.fontFeatureTags
193
+ );
186
194
  }
187
195
 
188
196
  for (const targetFormat of formats) {
@@ -207,17 +215,11 @@ async function getSubsetsForFontUsage(
207
215
  if (cachedResult) {
208
216
  subsetPromiseMap.set(promiseId, Promise.resolve(cachedResult));
209
217
  } else {
210
- const subsetCall =
211
- featureGlyphIds && featureGlyphIds.length > 0
212
- ? subsetFontWithGlyphs(fontBuffer, text, {
213
- targetFormat,
214
- glyphIds: featureGlyphIds,
215
- variationAxes: subsetInfo.variationAxes,
216
- })
217
- : subsetFont(fontBuffer, text, {
218
- targetFormat,
219
- variationAxes: subsetInfo.variationAxes,
220
- });
218
+ const subsetCall = subsetFontWithGlyphs(fontBuffer, text, {
219
+ targetFormat,
220
+ glyphIds: featureGlyphIds,
221
+ variationAxes: subsetInfo.variationAxes,
222
+ });
221
223
 
222
224
  subsetPromiseMap.set(
223
225
  promiseId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turntrout/subfont",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Automatically subset web fonts to only the characters used on your pages. Fork of Munter/subfont with modern defaults.",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -64,7 +64,6 @@
64
64
  "pretty-bytes": "^5.1.0",
65
65
  "puppeteer-core": "^24.39.1",
66
66
  "specificity": "^0.4.1",
67
- "subset-font": "^2.3.0",
68
67
  "urltools": "^0.4.1",
69
68
  "yargs": "^17.7.2"
70
69
  },