@overpunch/vf-clamp 2.1.4

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,160 @@
1
+ /** One axis defined in the font's fvar table */
2
+ export declare interface AxisDefinition {
3
+ /** Four-character axis tag, e.g. "wght", "wdth" */
4
+ tag: string;
5
+ /** Human-readable axis name from the font's name table */
6
+ name: string;
7
+ /** Minimum value on the axis */
8
+ minimum: number;
9
+ /** Default value (used when axis is not explicitly set) */
10
+ default: number;
11
+ /** Maximum value on the axis */
12
+ maximum: number;
13
+ }
14
+
15
+ /** Restrict an axis to a sub-range — the axis remains variable but clamped to [min, max] */
16
+ export declare interface AxisRange {
17
+ /** Minimum value on the axis */
18
+ min: number;
19
+ /** Maximum value on the axis */
20
+ max: number;
21
+ }
22
+
23
+ /**
24
+ * What to do with one axis when clamping a variable font:
25
+ * - number: pin to that value (axis locked and removed from output design space)
26
+ * - AxisRange: restrict to [min, max] (axis stays variable within that range)
27
+ * - null: omit this axis from the instancer call — axis keeps its full original range
28
+ *
29
+ * Axes not listed in the config also keep their full original range.
30
+ * Note: JS→Python bridge limitations prevent true axis dropping (fonttools' Python `None` semantic).
31
+ */
32
+ export declare type AxisValue = PinnedAxis | AxisRange | null;
33
+
34
+ /**
35
+ * Produce one restricted variable font per output config from a source variable font.
36
+ * Outputs are processed sequentially (Pyodide is single-threaded).
37
+ *
38
+ * Each output can specify:
39
+ * - instances: named instances to include — hull (min/max per axis) computed automatically
40
+ * - axes: explicit axis constraints — override or extend the instance hull
41
+ *
42
+ * @param input - Source variable font binary (TTF, OTF, WOFF, or WOFF2)
43
+ * @param options - Output configs and optional format
44
+ * @returns One ClampResult per output, in the same order as options.outputs
45
+ */
46
+ export declare function clampFont(input: ArrayBuffer | Uint8Array | Buffer, options: ClampOptions): Promise<ClampResult[]>;
47
+
48
+ /** Options passed to clampFont() */
49
+ export declare interface ClampOptions {
50
+ /** One entry per restricted variant to produce */
51
+ outputs: OutputConfig[];
52
+ /**
53
+ * Output format — defaults to 'ttf'.
54
+ * 'woff' and 'woff2' transcode the result to a web-compressed flavour.
55
+ * 'otf' is a passthrough — the instancer preserves the input outline format.
56
+ */
57
+ format?: OutputFormat;
58
+ /**
59
+ * Remap the wght axis so its minimum becomes 100, making CSS font-weight values
60
+ * (100–900) work as expected. Instance and STAT axis value coordinates are
61
+ * remapped proportionally; the avar is unchanged because the remapping preserves
62
+ * normalised values. Defaults to false — the axis keeps its original fvar range.
63
+ *
64
+ * Use this when the source font's design space starts above wght 100 (e.g. 250)
65
+ * and you need font-weight: 100 to reach the lightest weight.
66
+ */
67
+ normalizeWeightAxis?: boolean;
68
+ }
69
+
70
+ /** One output from clampFont() — a restricted variable font for a single output config */
71
+ export declare interface ClampResult {
72
+ /** Matches OutputConfig.name (or the auto-derived instance range if name was omitted) */
73
+ name: string;
74
+ /** Restricted font binary — write to disk or upload; format matches options.format */
75
+ buffer: Uint8Array;
76
+ /** Format of the buffer — matches options.format, defaults to 'ttf' */
77
+ format: OutputFormat;
78
+ }
79
+
80
+ /**
81
+ * Produce a compact display name from the first and last selected instance names.
82
+ * Strips shared leading prefix and trailing suffix tokens, joins differing parts with a dash.
83
+ *
84
+ * Examples:
85
+ * compactName('Inter Light', 'Inter Bold') → 'Inter Light-Bold'
86
+ * compactName('Condensed Thin', 'Condensed Black') → 'Condensed Thin-Black'
87
+ * compactName('Regular', 'Regular') → 'Regular'
88
+ *
89
+ * Canonical implementation — also duplicated in the Python plugins and VS Code webview
90
+ * (both sandboxed contexts that cannot import this package at runtime).
91
+ */
92
+ export declare function compactName(first: string, last: string): string;
93
+
94
+ /** Convert a font buffer to WOFF (zlib-compressed). */
95
+ export declare function convertToWoff(input: Uint8Array | Buffer): Promise<Uint8Array>;
96
+
97
+ /** Convert a font buffer to WOFF2 (Brotli-compressed). */
98
+ export declare function convertToWoff2(input: Uint8Array | Buffer): Promise<Uint8Array>;
99
+
100
+ /** One named instance from the font's fvar table */
101
+ export declare interface FontInstance {
102
+ /** Instance name from the font's name table, e.g. "Regular", "Bold Condensed" */
103
+ name: string;
104
+ /** Axis coordinates for this instance, e.g. { wght: 400, wdth: 100 } */
105
+ coordinates: Record<string, number>;
106
+ }
107
+
108
+ /** Result of getInstances() — describes the full design space of a variable font */
109
+ export declare interface FontInstancesResult {
110
+ /** All variable axes defined in the font */
111
+ axes: AxisDefinition[];
112
+ /** All named instances defined in the font */
113
+ instances: FontInstance[];
114
+ }
115
+
116
+ /**
117
+ * Extract axis definitions and named instances from a variable font's fvar table.
118
+ * Returns the full design space: which axes exist and what named positions are defined.
119
+ * Accepts TTF, OTF, WOFF, or WOFF2 input — fonttools handles all four formats.
120
+ *
121
+ * @param input - Source variable font binary (TTF, OTF, WOFF, or WOFF2)
122
+ * @returns Axis definitions and named instances, or empty arrays for a static font
123
+ */
124
+ export declare function getInstances(input: ArrayBuffer | Uint8Array | Buffer): Promise<FontInstancesResult>;
125
+
126
+ /**
127
+ * One output variant to produce from the source variable font.
128
+ * Specify instances, axes, or both — instances set the hull, axes override individual tags.
129
+ */
130
+ export declare interface OutputConfig {
131
+ /** Name for this output — used in ClampResult.name. Defaults to instance range when omitted. */
132
+ name?: string;
133
+ /**
134
+ * Named instances to include. The axis hull (min/max per axis) is computed automatically
135
+ * across all listed instances — producing one variable font that spans exactly that range.
136
+ * Instance names must match the font's fvar name table exactly.
137
+ */
138
+ instances?: string[];
139
+ /**
140
+ * Explicit axis constraints, applied after any instance hull.
141
+ * Axes listed here override the hull computed from instances for that tag.
142
+ * Axis tags not listed keep their full original range.
143
+ */
144
+ axes?: Record<string, AxisValue>;
145
+ }
146
+
147
+ /**
148
+ * Output format for clampFont() — defaults to 'ttf'.
149
+ * 'woff' and 'woff2' transcode to web-compressed flavours.
150
+ * 'otf' is a passthrough (no outline conversion); the instancer preserves the input flavour.
151
+ */
152
+ export declare type OutputFormat = 'ttf' | 'otf' | 'woff' | 'woff2';
153
+
154
+ /** Pin an axis at a single value — the axis is locked and removed from the output font's design space */
155
+ export declare type PinnedAxis = number;
156
+
157
+ /** @deprecated Use OutputConfig instead */
158
+ export declare type SubfamilyConfig = OutputConfig;
159
+
160
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,553 @@
1
+ import { createRequire as D } from "node:module";
2
+ const I = D(import.meta.url), { preparePyodide: l, PyodideFile: u } = I("@web-alchemy/fonttools/src/pyodide.js");
3
+ let g = null;
4
+ async function P() {
5
+ return g || (g = l().then(
6
+ (t) => t.runPythonAsync(`
7
+ from fontTools.ttLib import TTFont
8
+
9
+ def vf_convert_flavor(file_options):
10
+ font = TTFont(file_options['input-file'])
11
+ font.flavor = file_options['flavor']
12
+ font.save(file_options['output-file'])
13
+
14
+ vf_convert_flavor
15
+ `)
16
+ )), g;
17
+ }
18
+ async function V(t, o) {
19
+ const a = await l(), e = new u({ pyodide: a }), n = new u({ pyodide: a });
20
+ try {
21
+ await e.upload(t);
22
+ const i = /* @__PURE__ */ new Map([
23
+ ["input-file", e.filename],
24
+ ["output-file", n.filename],
25
+ ["flavor", o]
26
+ ]);
27
+ return (await P())(i), n.download();
28
+ } finally {
29
+ try {
30
+ e.delete();
31
+ } catch {
32
+ }
33
+ try {
34
+ n.delete();
35
+ } catch {
36
+ }
37
+ }
38
+ }
39
+ async function R(t) {
40
+ return V(t, "woff");
41
+ }
42
+ async function N(t) {
43
+ return V(t, "woff2");
44
+ }
45
+ let h = null;
46
+ async function O() {
47
+ return h || (h = l().then(
48
+ (t) => t.runPythonAsync(`
49
+ import json
50
+ from fontTools.ttLib import TTFont
51
+
52
+ def get_instances_fn(input_file):
53
+ font = TTFont(input_file)
54
+
55
+ if 'fvar' not in font:
56
+ return json.dumps({'axes': [], 'instances': []})
57
+
58
+ fvar = font['fvar']
59
+ name_table = font['name']
60
+
61
+ axes = [
62
+ {
63
+ 'tag': ax.axisTag,
64
+ 'name': name_table.getDebugName(ax.axisNameID) or ax.axisTag,
65
+ 'minimum': ax.minValue,
66
+ 'default': ax.defaultValue,
67
+ 'maximum': ax.maxValue,
68
+ }
69
+ for ax in fvar.axes
70
+ ]
71
+
72
+ instances = [
73
+ {
74
+ 'name': name_table.getDebugName(inst.subfamilyNameID) or f'Instance {i}',
75
+ 'coordinates': dict(inst.coordinates),
76
+ }
77
+ for i, inst in enumerate(fvar.instances)
78
+ ]
79
+
80
+ return json.dumps({'axes': axes, 'instances': instances})
81
+
82
+ get_instances_fn
83
+ `)
84
+ )), h;
85
+ }
86
+ async function j(t) {
87
+ const o = await l(), a = t instanceof ArrayBuffer ? new Uint8Array(t) : t, e = new u({ pyodide: o });
88
+ try {
89
+ await e.upload(a);
90
+ const i = (await O())(e.filename);
91
+ return JSON.parse(i);
92
+ } finally {
93
+ try {
94
+ e.delete();
95
+ } catch {
96
+ }
97
+ }
98
+ }
99
+ let w = null, v = null, y = null, A = null, T = null;
100
+ async function L() {
101
+ return w || (w = l().then(
102
+ (t) => t.runPythonAsync(`
103
+ from fontTools import ttLib
104
+
105
+ def patch_font_names_fn(file_options):
106
+ font = ttLib.TTFont(file_options['input-file'])
107
+ name_table = font['name']
108
+ family_name = file_options['family-name']
109
+ ps_name = file_options['postscript-name']
110
+
111
+ existing_ids = {r.nameID for r in name_table.names}
112
+
113
+ # nameID 1 = Family, 4 = Full name, 6 = PostScript name
114
+ # nameID 2 = Subfamily — reset to 'Regular' so the restricted file does not
115
+ # collide with the source font's Subfamily in OS font caches.
116
+ # nameID 3 = Unique ID — regenerate so it is distinct from the source font.
117
+ # nameID 16 = Preferred family (update only if present)
118
+ # nameID 25 = Variations PS Name Prefix (update only if present)
119
+ #
120
+ # head.fontRevision is a Fixed value (e.g. 1.000). Fall back to '1.000' if
121
+ # the head table is unreadable.
122
+ try:
123
+ version = '%.3f' % font['head'].fontRevision
124
+ except Exception:
125
+ version = '1.000'
126
+ unique_id = '%s;%s;%s' % (version, ps_name, family_name)
127
+
128
+ updates = {
129
+ 1: family_name,
130
+ 2: 'Regular',
131
+ 3: unique_id,
132
+ 4: family_name,
133
+ 6: ps_name,
134
+ }
135
+ if 16 in existing_ids:
136
+ updates[16] = family_name
137
+ if 25 in existing_ids:
138
+ updates[25] = ps_name
139
+
140
+ for record in name_table.names:
141
+ if record.nameID not in updates:
142
+ continue
143
+ value = updates[record.nameID]
144
+ if record.platformID == 3:
145
+ record.string = value.encode('utf-16-be')
146
+ elif record.platformID == 1:
147
+ try:
148
+ record.string = value.encode('mac_roman')
149
+ except Exception:
150
+ record.string = value.encode('ascii', errors='replace')
151
+
152
+ font.save(file_options['output-file'])
153
+
154
+ patch_font_names_fn
155
+ `)
156
+ )), w;
157
+ }
158
+ async function M() {
159
+ return v || (v = l().then(
160
+ (t) => t.runPythonAsync(`
161
+ from fontTools.ttLib import TTFont
162
+ from fontTools.varLib import instancer
163
+ import json
164
+
165
+ def vf_clamp_instantiate(file_options, axes_json):
166
+ font = TTFont(file_options['input-file'])
167
+ axes_spec = json.loads(axes_json)
168
+
169
+ limits = {}
170
+ axes_by_tag = {ax.axisTag: ax for ax in font['fvar'].axes}
171
+
172
+ for tag, spec in axes_spec.items():
173
+ if spec is None:
174
+ continue
175
+ ax = axes_by_tag.get(tag)
176
+ if isinstance(spec, list):
177
+ mn = float(spec[0])
178
+ mx = float(spec[1])
179
+ default = ax.defaultValue if ax else (mn + mx) / 2
180
+ default = max(mn, min(mx, default))
181
+ limits[tag] = instancer.AxisTriple(mn, default, mx)
182
+ else:
183
+ limits[tag] = float(spec)
184
+
185
+ partial = instancer.instantiateVariableFont(font, limits)
186
+ partial.save(file_options['output-file'])
187
+
188
+ vf_clamp_instantiate
189
+ `)
190
+ )), v;
191
+ }
192
+ async function U() {
193
+ return y || (y = l().then(
194
+ (t) => t.runPythonAsync(`
195
+ from fontTools.ttLib import TTFont
196
+
197
+ def vf_clamp_normalize_wght(file_options, new_min_str):
198
+ new_min = float(new_min_str)
199
+ font = TTFont(file_options['input-file'])
200
+
201
+ if 'fvar' not in font:
202
+ font.save(file_options['output-file'])
203
+ return
204
+
205
+ wght_axis = next((ax for ax in font['fvar'].axes if ax.axisTag == 'wght'), None)
206
+
207
+ # Only normalize when the font's minimum is above the target (e.g. 251 > 100)
208
+ if wght_axis is None or wght_axis.minValue <= new_min:
209
+ font.save(file_options['output-file'])
210
+ return
211
+
212
+ old_min = wght_axis.minValue
213
+ default = wght_axis.defaultValue
214
+ scale = (default - new_min) / (default - old_min)
215
+
216
+ def remap(v):
217
+ return (default + (v - default) * scale) if v < default else v
218
+
219
+ # Update fvar axis minimum
220
+ wght_axis.minValue = new_min
221
+
222
+ # Update named instance wght coordinates
223
+ for inst in font['fvar'].instances:
224
+ if 'wght' in inst.coordinates:
225
+ inst.coordinates['wght'] = remap(inst.coordinates['wght'])
226
+
227
+ # Update STAT axis values that reference wght
228
+ if 'STAT' in font and font['STAT'].table.AxisValueArray:
229
+ stat = font['STAT'].table
230
+ wght_idx = None
231
+ if hasattr(stat, 'DesignAxisRecord') and stat.DesignAxisRecord:
232
+ for i, ax in enumerate(stat.DesignAxisRecord.Axis):
233
+ if ax.AxisTag == 'wght':
234
+ wght_idx = i
235
+ break
236
+ if wght_idx is not None:
237
+ for av in stat.AxisValueArray.AxisValue:
238
+ fmt = av.Format
239
+ if fmt in (1, 3) and av.AxisIndex == wght_idx:
240
+ av.Value = remap(av.Value)
241
+ if fmt == 3:
242
+ av.LinkedValue = remap(av.LinkedValue)
243
+ elif fmt == 2 and av.AxisIndex == wght_idx:
244
+ av.NominalValue = remap(av.NominalValue)
245
+ av.RangeMinValue = remap(av.RangeMinValue)
246
+ av.RangeMaxValue = remap(av.RangeMaxValue)
247
+
248
+ font.save(file_options['output-file'])
249
+
250
+ vf_clamp_normalize_wght
251
+ `)
252
+ )), y;
253
+ }
254
+ async function k() {
255
+ return A || (A = l().then(
256
+ (t) => t.runPythonAsync(`
257
+ from fontTools.ttLib import TTFont
258
+
259
+ def vf_clamp_prune_stat(file_options):
260
+ font = TTFont(file_options['input-file'])
261
+
262
+ if 'STAT' not in font or 'fvar' not in font:
263
+ font.save(file_options['output-file'])
264
+ return
265
+
266
+ stat = font['STAT'].table
267
+ if not getattr(stat, 'DesignAxisRecord', None) or not stat.DesignAxisRecord.Axis:
268
+ font.save(file_options['output-file'])
269
+ return
270
+
271
+ fvar_tags = {ax.axisTag for ax in font['fvar'].axes}
272
+ stat_axes = list(stat.DesignAxisRecord.Axis)
273
+
274
+ # Map old STAT AxisIndex -> new index after pruning STAT axes that vanish from fvar.
275
+ surviving_indices = [i for i, ax in enumerate(stat_axes) if ax.AxisTag in fvar_tags]
276
+ if len(surviving_indices) == len(stat_axes):
277
+ # No axes removed — nothing to prune.
278
+ font.save(file_options['output-file'])
279
+ return
280
+ old_to_new = {old: new for new, old in enumerate(surviving_indices)}
281
+
282
+ # Rewrite the DesignAxisRecord.Axis list to surviving axes only.
283
+ stat.DesignAxisRecord.Axis = [stat_axes[i] for i in surviving_indices]
284
+ if hasattr(stat, 'DesignAxisCount'):
285
+ stat.DesignAxisCount = len(stat.DesignAxisRecord.Axis)
286
+
287
+ # Prune AxisValueArray entries that reference removed axes.
288
+ if stat.AxisValueArray and stat.AxisValueArray.AxisValue:
289
+ kept = []
290
+ for av in stat.AxisValueArray.AxisValue:
291
+ fmt = av.Format
292
+ if fmt in (1, 2, 3):
293
+ if av.AxisIndex not in old_to_new:
294
+ continue # references a removed axis
295
+ av.AxisIndex = old_to_new[av.AxisIndex]
296
+ kept.append(av)
297
+ elif fmt == 4:
298
+ inner = list(getattr(av, 'AxisValueRecord', []) or [])
299
+ if any(rec.AxisIndex not in old_to_new for rec in inner):
300
+ continue # whole record dies if any inner ref is dead
301
+ for rec in inner:
302
+ rec.AxisIndex = old_to_new[rec.AxisIndex]
303
+ kept.append(av)
304
+ else:
305
+ # Unknown format — keep as-is to be safe.
306
+ kept.append(av)
307
+ stat.AxisValueArray.AxisValue = kept
308
+ if hasattr(stat, 'AxisValueCount'):
309
+ stat.AxisValueCount = len(kept)
310
+
311
+ font.save(file_options['output-file'])
312
+
313
+ vf_clamp_prune_stat
314
+ `)
315
+ )), A;
316
+ }
317
+ async function $() {
318
+ return T || (T = l().then(
319
+ (t) => t.runPythonAsync(`
320
+ from fontTools.ttLib import TTFont
321
+
322
+ def vf_clamp_update_os2(file_options):
323
+ font = TTFont(file_options['input-file'])
324
+
325
+ if 'fvar' not in font:
326
+ font.save(file_options['output-file'])
327
+ return
328
+
329
+ wght_axis = next((ax for ax in font['fvar'].axes if ax.axisTag == 'wght'), None)
330
+ if wght_axis is None:
331
+ font.save(file_options['output-file'])
332
+ return
333
+
334
+ new_default = wght_axis.defaultValue
335
+ # OS/2.usWeightClass valid range is 1..1000.
336
+ weight_class = int(round(max(1, min(1000, new_default))))
337
+
338
+ if 'OS/2' in font:
339
+ os2 = font['OS/2']
340
+ os2.usWeightClass = weight_class
341
+ # fsSelection bits: 0x20 = BOLD, 0x40 = REGULAR.
342
+ # Mirror the convention: REGULAR when usWeightClass < 600, BOLD when >= 700.
343
+ fs = os2.fsSelection
344
+ fs &= ~(0x20 | 0x40)
345
+ if weight_class >= 700:
346
+ fs |= 0x20 # BOLD
347
+ elif weight_class < 600:
348
+ fs |= 0x40 # REGULAR
349
+ os2.fsSelection = fs
350
+
351
+ if 'head' in font:
352
+ head = font['head']
353
+ # macStyle bit 0 = bold.
354
+ ms = head.macStyle
355
+ if weight_class >= 700:
356
+ ms |= 0x01
357
+ else:
358
+ ms &= ~0x01
359
+ head.macStyle = ms
360
+
361
+ font.save(file_options['output-file'])
362
+
363
+ vf_clamp_update_os2
364
+ `)
365
+ )), T;
366
+ }
367
+ function C(t) {
368
+ return t.replace(/[^A-Za-z0-9 -]/g, "").trim().split(/\s+/).join("-").replace(/^-+|-+$/g, "").slice(0, 63);
369
+ }
370
+ async function z(t, o) {
371
+ if (!o) return t;
372
+ const a = await l(), e = new u({ pyodide: a }), n = new u({ pyodide: a });
373
+ try {
374
+ const i = C(o);
375
+ await e.upload(t);
376
+ const r = /* @__PURE__ */ new Map([
377
+ ["input-file", e.filename],
378
+ ["output-file", n.filename],
379
+ ["family-name", o],
380
+ ["postscript-name", i]
381
+ ]);
382
+ return (await L())(r), n.download();
383
+ } catch (i) {
384
+ return console.warn(`vf-clamp: name table patching failed for "${o}" — returning unpatched buffer`, i), t;
385
+ } finally {
386
+ try {
387
+ e.delete();
388
+ } catch {
389
+ }
390
+ try {
391
+ n.delete();
392
+ } catch {
393
+ }
394
+ }
395
+ }
396
+ function B(t) {
397
+ return typeof t == "number" ? t : t === null ? null : [t.min, t.max];
398
+ }
399
+ async function E(t, o) {
400
+ const a = await l(), e = new u({ pyodide: a }), n = new u({ pyodide: a });
401
+ try {
402
+ await e.upload(t);
403
+ const i = /* @__PURE__ */ new Map([
404
+ ["input-file", e.filename],
405
+ ["output-file", n.filename]
406
+ ]);
407
+ return (await M())(i, JSON.stringify(o)), n.download();
408
+ } finally {
409
+ try {
410
+ e.delete();
411
+ } catch {
412
+ }
413
+ try {
414
+ n.delete();
415
+ } catch {
416
+ }
417
+ }
418
+ }
419
+ async function W(t) {
420
+ const o = await l(), a = new u({ pyodide: o }), e = new u({ pyodide: o });
421
+ try {
422
+ await a.upload(t);
423
+ const n = /* @__PURE__ */ new Map([
424
+ ["input-file", a.filename],
425
+ ["output-file", e.filename]
426
+ ]);
427
+ return (await k())(n), e.download();
428
+ } catch (n) {
429
+ return console.warn("vf-clamp: STAT prune failed — returning unpruned buffer", n), t;
430
+ } finally {
431
+ try {
432
+ a.delete();
433
+ } catch {
434
+ }
435
+ try {
436
+ e.delete();
437
+ } catch {
438
+ }
439
+ }
440
+ }
441
+ async function q(t) {
442
+ const o = await l(), a = new u({ pyodide: o }), e = new u({ pyodide: o });
443
+ try {
444
+ await a.upload(t);
445
+ const n = /* @__PURE__ */ new Map([
446
+ ["input-file", a.filename],
447
+ ["output-file", e.filename]
448
+ ]);
449
+ return (await $())(n), e.download();
450
+ } catch (n) {
451
+ return console.warn("vf-clamp: OS/2 + macStyle update failed — returning original buffer", n), t;
452
+ } finally {
453
+ try {
454
+ a.delete();
455
+ } catch {
456
+ }
457
+ try {
458
+ e.delete();
459
+ } catch {
460
+ }
461
+ }
462
+ }
463
+ async function G(t, o) {
464
+ const a = await l(), e = new u({ pyodide: a }), n = new u({ pyodide: a });
465
+ try {
466
+ await e.upload(t);
467
+ const i = /* @__PURE__ */ new Map([
468
+ ["input-file", e.filename],
469
+ ["output-file", n.filename]
470
+ ]);
471
+ return (await U())(i, String(o)), n.download();
472
+ } catch (i) {
473
+ return console.warn("vf-clamp: wght normalisation failed — returning unnormalised buffer", i), t;
474
+ } finally {
475
+ try {
476
+ e.delete();
477
+ } catch {
478
+ }
479
+ try {
480
+ n.delete();
481
+ } catch {
482
+ }
483
+ }
484
+ }
485
+ function J(t, o) {
486
+ const a = new Map(o.map((i) => [i.name, i])), e = {};
487
+ for (const i of t) {
488
+ const r = a.get(i);
489
+ if (!r) throw new Error(`Named instance "${i}" not found in font`);
490
+ for (const [s, m] of Object.entries(r.coordinates))
491
+ e[s] ? (e[s].min = Math.min(e[s].min, m), e[s].max = Math.max(e[s].max, m)) : e[s] = { min: m, max: m };
492
+ }
493
+ const n = {};
494
+ for (const [i, { min: r, max: s }] of Object.entries(e))
495
+ n[i] = r === s ? r : { min: r, max: s };
496
+ return n;
497
+ }
498
+ async function Z(t, o) {
499
+ var s, m;
500
+ if (o.outputs.length === 0) return [];
501
+ const a = t instanceof ArrayBuffer ? new Uint8Array(t) : t, e = o.format ?? "ttf";
502
+ let n = [], i = [];
503
+ if (o.outputs.some((f) => {
504
+ var p;
505
+ return (p = f.instances) == null ? void 0 : p.length;
506
+ })) {
507
+ const f = await j(a);
508
+ n = f.instances, i = f.axes;
509
+ }
510
+ const r = [];
511
+ for (const f of o.outputs) {
512
+ let p = {};
513
+ (s = f.instances) != null && s.length && (p = J(f.instances, n)), f.axes && (p = { ...p, ...f.axes });
514
+ for (const d of i) {
515
+ const x = p[d.tag];
516
+ if (x != null && typeof x == "object") {
517
+ const _ = x;
518
+ if (d.default < _.min || d.default > _.max) {
519
+ const S = Math.max(_.min, Math.min(_.max, d.default));
520
+ console.warn(
521
+ `vf-clamp: axis "${d.tag}" default (${d.default}) is outside restricted range [${_.min}, ${_.max}] — will be clamped to ${S}`
522
+ );
523
+ }
524
+ }
525
+ }
526
+ const b = {};
527
+ for (const [d, x] of Object.entries(p))
528
+ x !== null && (b[d] = B(x));
529
+ const F = f.name ?? ((m = f.instances) != null && m.length ? f.instances.length === 1 ? f.instances[0] : `${f.instances[0]}-${f.instances[f.instances.length - 1]}` : "output");
530
+ let c = await E(a, b);
531
+ c = await W(c), o.normalizeWeightAxis && (c = await G(c, 100)), c = await q(c), c = await z(c, F), e === "woff2" ? c = await N(c) : e === "woff" && (c = await R(c)), r.push({ name: F, buffer: c, format: e });
532
+ }
533
+ return r;
534
+ }
535
+ function K(t, o) {
536
+ if (t === o) return t;
537
+ const a = t.split(" "), e = o.split(" ");
538
+ let n = 0;
539
+ for (; n < a.length && n < e.length && a[n] === e[n]; )
540
+ n++;
541
+ let i = 0;
542
+ for (; i < a.length - n && i < e.length - n && a[a.length - 1 - i] === e[e.length - 1 - i]; )
543
+ i++;
544
+ const r = a.slice(0, n).join(" "), s = a.slice(n, a.length - (i || 0)).join(" "), m = e.slice(n, e.length - (i || 0)).join(" "), f = i > 0 ? a.slice(a.length - i).join(" ") : "", p = s && m ? `${s}-${m}` : s || m;
545
+ return [r, p, f].filter(Boolean).join(" ");
546
+ }
547
+ export {
548
+ Z as clampFont,
549
+ K as compactName,
550
+ R as convertToWoff,
551
+ N as convertToWoff2,
552
+ j as getInstances
553
+ };