@overpunch/vf-clamp 2.1.4 → 2.3.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/dist/index.js CHANGED
@@ -1,9 +1,9 @@
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(`
1
+ import { createRequire as $ } from "node:module";
2
+ const M = $(import.meta.url), { preparePyodide: d, PyodideFile: x } = M("@web-alchemy/fonttools/src/pyodide.js");
3
+ let A = null;
4
+ async function L() {
5
+ return A || (A = d().then(
6
+ (e) => e.runPythonAsync(`
7
7
  from fontTools.ttLib import TTFont
8
8
 
9
9
  def vf_convert_flavor(file_options):
@@ -13,47 +13,48 @@ def vf_convert_flavor(file_options):
13
13
 
14
14
  vf_convert_flavor
15
15
  `)
16
- )), g;
16
+ )), A;
17
17
  }
18
- async function V(t, o) {
19
- const a = await l(), e = new u({ pyodide: a }), n = new u({ pyodide: a });
18
+ async function R(e, a) {
19
+ const t = await d(), n = new x({ pyodide: t }), i = new x({ pyodide: t });
20
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]
21
+ await n.upload(e);
22
+ const o = /* @__PURE__ */ new Map([
23
+ ["input-file", n.filename],
24
+ ["output-file", i.filename],
25
+ ["flavor", a]
26
26
  ]);
27
- return (await P())(i), n.download();
27
+ return (await L())(o), i.download();
28
28
  } finally {
29
29
  try {
30
- e.delete();
30
+ n.delete();
31
31
  } catch {
32
32
  }
33
33
  try {
34
- n.delete();
34
+ i.delete();
35
35
  } catch {
36
36
  }
37
37
  }
38
38
  }
39
- async function R(t) {
40
- return V(t, "woff");
39
+ async function k(e) {
40
+ return R(e, "woff");
41
41
  }
42
- async function N(t) {
43
- return V(t, "woff2");
42
+ async function C(e) {
43
+ return R(e, "woff2");
44
44
  }
45
- let h = null;
46
- async function O() {
47
- return h || (h = l().then(
48
- (t) => t.runPythonAsync(`
45
+ let I = null;
46
+ async function E() {
47
+ return I || (I = d().then(
48
+ (e) => e.runPythonAsync(`
49
49
  import json
50
50
  from fontTools.ttLib import TTFont
51
51
 
52
52
  def get_instances_fn(input_file):
53
53
  font = TTFont(input_file)
54
54
 
55
+ family = font['name'].getDebugName(16) or font['name'].getDebugName(1) or ''
55
56
  if 'fvar' not in font:
56
- return json.dumps({'axes': [], 'instances': []})
57
+ return json.dumps({'axes': [], 'instances': [], 'family': family})
57
58
 
58
59
  fvar = font['fvar']
59
60
  name_table = font['name']
@@ -77,29 +78,47 @@ def get_instances_fn(input_file):
77
78
  for i, inst in enumerate(fvar.instances)
78
79
  ]
79
80
 
80
- return json.dumps({'axes': axes, 'instances': instances})
81
+ return json.dumps({'axes': axes, 'instances': instances, 'family': family})
81
82
 
82
83
  get_instances_fn
83
84
  `)
84
- )), h;
85
+ )), I;
85
86
  }
86
- async function j(t) {
87
- const o = await l(), a = t instanceof ArrayBuffer ? new Uint8Array(t) : t, e = new u({ pyodide: o });
87
+ async function B(e) {
88
+ const a = await d(), t = e instanceof ArrayBuffer ? new Uint8Array(e) : e, n = new x({ pyodide: a });
88
89
  try {
89
- await e.upload(a);
90
- const i = (await O())(e.filename);
91
- return JSON.parse(i);
90
+ await n.upload(t);
91
+ const o = (await E())(n.filename);
92
+ return JSON.parse(o);
92
93
  } finally {
93
94
  try {
94
- e.delete();
95
+ n.delete();
95
96
  } catch {
96
97
  }
97
98
  }
98
99
  }
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(`
100
+ function U(e, a) {
101
+ if (e === a) return e;
102
+ const t = e.split(" "), n = a.split(" ");
103
+ let i = 0;
104
+ for (; i < t.length && i < n.length && t[i] === n[i]; )
105
+ i++;
106
+ let o = 0;
107
+ for (; o < t.length - i && o < n.length - i && t[t.length - 1 - o] === n[n.length - 1 - o]; )
108
+ o++;
109
+ const r = t.slice(0, i).join(" "), s = t.slice(i, t.length - (o || 0)).join(" "), h = n.slice(i, n.length - (o || 0)).join(" "), y = o > 0 ? t.slice(t.length - o).join(" ") : "", f = s && h ? `${s}-${h}` : s || h;
110
+ return [r, f, y].filter(Boolean).join(" ");
111
+ }
112
+ function T(e, a) {
113
+ const t = a.filter((n) => n.name === e);
114
+ if (!t.length) throw new Error(`Named instance "${e}" not found in font`);
115
+ if (t.length > 1) throw new Error(`Named instance "${e}" is ambiguous: ${t.length} instances share that name`);
116
+ return t[0];
117
+ }
118
+ let S = null, V = null, D = null, N = null;
119
+ async function z() {
120
+ return S || (S = d().then(
121
+ (e) => e.runPythonAsync(`
103
122
  from fontTools import ttLib
104
123
 
105
124
  def patch_font_names_fn(file_options):
@@ -110,6 +129,27 @@ def patch_font_names_fn(file_options):
110
129
 
111
130
  existing_ids = {r.nameID for r in name_table.names}
112
131
 
132
+ # A family name with no ASCII letters gives an empty PostScript name: fall back to the
133
+ # source PostScript name plus a short hash of the family name (passed in from JS).
134
+ if not ps_name:
135
+ source_ps = name_table.getDebugName(6) or 'Font'
136
+ ps_name = ('%s-%s' % (source_ps[:52], file_options['postscript-suffix']))[:63]
137
+
138
+ # nameID 2 must be a RIBBI style that matches OS/2 (already updated): Regular, Italic, Bold or Bold Italic.
139
+ fs = font['OS/2'].fsSelection if 'OS/2' in font else 0x40
140
+ is_italic = bool(fs & 0x01)
141
+ is_bold = bool(fs & 0x20)
142
+ ribbi = ('Bold Italic' if is_italic else 'Bold') if is_bold else ('Italic' if is_italic else 'Regular')
143
+
144
+ # nameID 17 (typographic subfamily): the name of the named instance at the new default, if any.
145
+ default_style = None
146
+ if 'fvar' in font:
147
+ defaults = {ax.axisTag: ax.defaultValue for ax in font['fvar'].axes}
148
+ for inst in font['fvar'].instances:
149
+ if all(abs(inst.coordinates.get(t, v) - v) < 0.01 for t, v in defaults.items()):
150
+ default_style = name_table.getDebugName(inst.subfamilyNameID)
151
+ break
152
+
113
153
  # nameID 1 = Family, 4 = Full name, 6 = PostScript name
114
154
  # nameID 2 = Subfamily — reset to 'Regular' so the restricted file does not
115
155
  # collide with the source font's Subfamily in OS font caches.
@@ -127,37 +167,55 @@ def patch_font_names_fn(file_options):
127
167
 
128
168
  updates = {
129
169
  1: family_name,
130
- 2: 'Regular',
170
+ 2: ribbi,
131
171
  3: unique_id,
132
- 4: family_name,
172
+ 4: family_name if ribbi == 'Regular' else '%s %s' % (family_name, ribbi),
133
173
  6: ps_name,
134
174
  }
135
175
  if 16 in existing_ids:
136
176
  updates[16] = family_name
177
+ if 17 in existing_ids and default_style:
178
+ updates[17] = default_style
137
179
  if 25 in existing_ids:
138
180
  updates[25] = ps_name
139
181
 
182
+ # Named instances' PostScript names must follow the new prefix, e.g. Inter-Regular-Bold-Medium.
183
+ if 'fvar' in font:
184
+ for inst in font['fvar'].instances:
185
+ pid = getattr(inst, 'postscriptNameID', 0xFFFF)
186
+ if pid in (None, 0xFFFF):
187
+ continue
188
+ style = (name_table.getDebugName(inst.subfamilyNameID) or '').replace(' ', '')
189
+ style = ''.join(c for c in style if c.isascii() and (c.isalnum() or c == '-'))
190
+ updates[pid] = ('%s-%s' % (ps_name, style))[:63]
191
+
192
+ # Rewrite every platform. A Mac (platform 1) record that can't be encoded in Mac Roman is dropped
193
+ # rather than filled with '?'; Windows and Unicode records always carry the full name.
194
+ keep = []
140
195
  for record in name_table.names:
141
196
  if record.nameID not in updates:
197
+ keep.append(record)
142
198
  continue
143
199
  value = updates[record.nameID]
144
- if record.platformID == 3:
200
+ if record.platformID in (0, 3):
145
201
  record.string = value.encode('utf-16-be')
146
202
  elif record.platformID == 1:
147
203
  try:
148
204
  record.string = value.encode('mac_roman')
149
205
  except Exception:
150
- record.string = value.encode('ascii', errors='replace')
206
+ continue
207
+ keep.append(record)
208
+ name_table.names = keep
151
209
 
152
210
  font.save(file_options['output-file'])
153
211
 
154
212
  patch_font_names_fn
155
213
  `)
156
- )), w;
214
+ )), S;
157
215
  }
158
- async function M() {
159
- return v || (v = l().then(
160
- (t) => t.runPythonAsync(`
216
+ async function W() {
217
+ return V || (V = d().then(
218
+ (e) => e.runPythonAsync(`
161
219
  from fontTools.ttLib import TTFont
162
220
  from fontTools.varLib import instancer
163
221
  import json
@@ -166,6 +224,12 @@ def vf_clamp_instantiate(file_options, axes_json):
166
224
  font = TTFont(file_options['input-file'])
167
225
  axes_spec = json.loads(axes_json)
168
226
 
227
+ # The bundled fontTools can't restrict avar version 2 or VARC fonts correctly: refuse instead of mis-cutting.
228
+ if 'avar' in font and getattr(font['avar'], 'majorVersion', 1) >= 2:
229
+ raise ValueError('vf-clamp: avar version 2 fonts are not supported yet')
230
+ if 'VARC' in font:
231
+ raise ValueError('vf-clamp: fonts with a VARC table are not supported yet')
232
+
169
233
  limits = {}
170
234
  axes_by_tag = {ax.axisTag: ax for ax in font['fvar'].axes}
171
235
 
@@ -187,11 +251,11 @@ def vf_clamp_instantiate(file_options, axes_json):
187
251
 
188
252
  vf_clamp_instantiate
189
253
  `)
190
- )), v;
254
+ )), V;
191
255
  }
192
- async function U() {
193
- return y || (y = l().then(
194
- (t) => t.runPythonAsync(`
256
+ async function q() {
257
+ return D || (D = d().then(
258
+ (e) => e.runPythonAsync(`
195
259
  from fontTools.ttLib import TTFont
196
260
 
197
261
  def vf_clamp_normalize_wght(file_options, new_min_str):
@@ -211,6 +275,10 @@ def vf_clamp_normalize_wght(file_options, new_min_str):
211
275
 
212
276
  old_min = wght_axis.minValue
213
277
  default = wght_axis.defaultValue
278
+ # With the default at the minimum there is no below-default range to stretch; leave the font as is.
279
+ if default <= old_min:
280
+ font.save(file_options['output-file'])
281
+ return 'skipped'
214
282
  scale = (default - new_min) / (default - old_min)
215
283
 
216
284
  def remap(v):
@@ -244,79 +312,21 @@ def vf_clamp_normalize_wght(file_options, new_min_str):
244
312
  av.NominalValue = remap(av.NominalValue)
245
313
  av.RangeMinValue = remap(av.RangeMinValue)
246
314
  av.RangeMaxValue = remap(av.RangeMaxValue)
315
+ elif fmt == 4:
316
+ for rec in getattr(av, 'AxisValueRecord', []) or []:
317
+ if rec.AxisIndex == wght_idx:
318
+ rec.Value = remap(rec.Value)
247
319
 
248
320
  font.save(file_options['output-file'])
321
+ return 'done'
249
322
 
250
323
  vf_clamp_normalize_wght
251
324
  `)
252
- )), y;
325
+ )), D;
253
326
  }
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(`
327
+ async function H() {
328
+ return N || (N = d().then(
329
+ (e) => e.runPythonAsync(`
320
330
  from fontTools.ttLib import TTFont
321
331
 
322
332
  def vf_clamp_update_os2(file_options):
@@ -327,25 +337,26 @@ def vf_clamp_update_os2(file_options):
327
337
  return
328
338
 
329
339
  wght_axis = next((ax for ax in font['fvar'].axes if ax.axisTag == 'wght'), None)
330
- if wght_axis is None:
340
+ if wght_axis is not None:
341
+ # OS/2.usWeightClass valid range is 1..1000.
342
+ weight_class = int(round(max(1, min(1000, wght_axis.defaultValue))))
343
+ elif 'OS/2' in font:
344
+ # wght was pinned: fontTools has already set usWeightClass to the pinned weight.
345
+ weight_class = font['OS/2'].usWeightClass
346
+ else:
331
347
  font.save(file_options['output-file'])
332
348
  return
333
349
 
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
350
  if 'OS/2' in font:
339
351
  os2 = font['OS/2']
340
352
  os2.usWeightClass = weight_class
341
- # fsSelection bits: 0x20 = BOLD, 0x40 = REGULAR.
342
- # Mirror the convention: REGULAR when usWeightClass < 600, BOLD when >= 700.
353
+ # fsSelection: 0x01 ITALIC (kept), 0x20 BOLD, 0x40 REGULAR. REGULAR only when neither ITALIC nor BOLD.
343
354
  fs = os2.fsSelection
344
355
  fs &= ~(0x20 | 0x40)
345
356
  if weight_class >= 700:
346
- fs |= 0x20 # BOLD
347
- elif weight_class < 600:
348
- fs |= 0x40 # REGULAR
357
+ fs |= 0x20
358
+ elif not fs & 0x01:
359
+ fs |= 0x40
349
360
  os2.fsSelection = fs
350
361
 
351
362
  if 'head' in font:
@@ -362,192 +373,225 @@ def vf_clamp_update_os2(file_options):
362
373
 
363
374
  vf_clamp_update_os2
364
375
  `)
365
- )), T;
376
+ )), N;
377
+ }
378
+ function J(e) {
379
+ const a = e.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^A-Za-z0-9 -]/g, "").trim().split(/\s+/).join("-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
380
+ return a.length <= 63 ? a : `${a.slice(0, 56).replace(/-+$/, "")}-${P(e)}`;
366
381
  }
367
- function C(t) {
368
- return t.replace(/[^A-Za-z0-9 -]/g, "").trim().split(/\s+/).join("-").replace(/^-+|-+$/g, "").slice(0, 63);
382
+ function P(e) {
383
+ let a = 2166136261;
384
+ for (const t of e)
385
+ a ^= t.codePointAt(0), a = Math.imul(a, 16777619) >>> 0;
386
+ return a.toString(36).padStart(6, "0").slice(-6);
369
387
  }
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 });
388
+ async function G(e, a) {
389
+ if (!a) return e;
390
+ const t = await d(), n = new x({ pyodide: t }), i = new x({ pyodide: t });
373
391
  try {
374
- const i = C(o);
375
- await e.upload(t);
392
+ const o = J(a);
393
+ await n.upload(e);
376
394
  const r = /* @__PURE__ */ new Map([
377
- ["input-file", e.filename],
378
- ["output-file", n.filename],
379
- ["family-name", o],
380
- ["postscript-name", i]
395
+ ["input-file", n.filename],
396
+ ["output-file", i.filename],
397
+ ["family-name", a],
398
+ ["postscript-name", o],
399
+ ["postscript-suffix", P(a)]
381
400
  ]);
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;
401
+ return (await z())(r), i.download();
402
+ } catch (o) {
403
+ throw Object.assign(new Error(`vf-clamp: name table patching failed for "${a}"`), { cause: o });
385
404
  } finally {
386
405
  try {
387
- e.delete();
406
+ n.delete();
388
407
  } catch {
389
408
  }
390
409
  try {
391
- n.delete();
410
+ i.delete();
392
411
  } catch {
393
412
  }
394
413
  }
395
414
  }
396
- function B(t) {
397
- return typeof t == "number" ? t : t === null ? null : [t.min, t.max];
415
+ function K(e) {
416
+ return typeof e == "number" ? e : e === null ? null : [e.min, e.max];
398
417
  }
399
- async function E(t, o) {
400
- const a = await l(), e = new u({ pyodide: a }), n = new u({ pyodide: a });
418
+ async function Z(e, a) {
419
+ const t = await d(), n = new x({ pyodide: t }), i = new x({ pyodide: t });
401
420
  try {
402
- await e.upload(t);
403
- const i = /* @__PURE__ */ new Map([
404
- ["input-file", e.filename],
405
- ["output-file", n.filename]
421
+ await n.upload(e);
422
+ const o = /* @__PURE__ */ new Map([
423
+ ["input-file", n.filename],
424
+ ["output-file", i.filename]
406
425
  ]);
407
- return (await M())(i, JSON.stringify(o)), n.download();
426
+ return (await W())(o, JSON.stringify(a)), i.download();
408
427
  } finally {
409
- try {
410
- e.delete();
411
- } catch {
412
- }
413
428
  try {
414
429
  n.delete();
415
430
  } catch {
416
431
  }
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
432
  try {
432
- a.delete();
433
- } catch {
434
- }
435
- try {
436
- e.delete();
433
+ i.delete();
437
434
  } catch {
438
435
  }
439
436
  }
440
437
  }
441
- async function q(t) {
442
- const o = await l(), a = new u({ pyodide: o }), e = new u({ pyodide: o });
438
+ async function Q(e) {
439
+ const a = await d(), t = new x({ pyodide: a }), n = new x({ pyodide: a });
443
440
  try {
444
- await a.upload(t);
445
- const n = /* @__PURE__ */ new Map([
446
- ["input-file", a.filename],
447
- ["output-file", e.filename]
441
+ await t.upload(e);
442
+ const i = /* @__PURE__ */ new Map([
443
+ ["input-file", t.filename],
444
+ ["output-file", n.filename]
448
445
  ]);
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;
446
+ return (await H())(i), n.download();
447
+ } catch (i) {
448
+ throw Object.assign(new Error("vf-clamp: OS/2 and macStyle update failed"), { cause: i });
452
449
  } finally {
453
450
  try {
454
- a.delete();
451
+ t.delete();
455
452
  } catch {
456
453
  }
457
454
  try {
458
- e.delete();
455
+ n.delete();
459
456
  } catch {
460
457
  }
461
458
  }
462
459
  }
463
- async function G(t, o) {
464
- const a = await l(), e = new u({ pyodide: a }), n = new u({ pyodide: a });
460
+ async function X(e, a) {
461
+ const t = await d(), n = new x({ pyodide: t }), i = new x({ pyodide: t });
465
462
  try {
466
- await e.upload(t);
467
- const i = /* @__PURE__ */ new Map([
468
- ["input-file", e.filename],
469
- ["output-file", n.filename]
463
+ await n.upload(e);
464
+ const o = /* @__PURE__ */ new Map([
465
+ ["input-file", n.filename],
466
+ ["output-file", i.filename]
470
467
  ]);
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;
468
+ return (await q())(o, String(a)) === "skipped" && console.warn("vf-clamp: normalizeWeightAxis skipped: the weight default is the axis minimum, so there is no lower range to remap"), i.download();
469
+ } catch (o) {
470
+ throw Object.assign(new Error("vf-clamp: wght normalisation failed"), { cause: o });
474
471
  } finally {
475
472
  try {
476
- e.delete();
473
+ n.delete();
477
474
  } catch {
478
475
  }
479
476
  try {
480
- n.delete();
477
+ i.delete();
481
478
  } catch {
482
479
  }
483
480
  }
484
481
  }
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 };
482
+ function Y(e, a) {
483
+ const t = {};
484
+ for (const i of e) {
485
+ const o = T(i, a);
486
+ for (const [r, s] of Object.entries(o.coordinates))
487
+ t[r] ? (t[r].min = Math.min(t[r].min, s), t[r].max = Math.max(t[r].max, s)) : t[r] = { min: s, max: s };
492
488
  }
493
489
  const n = {};
494
- for (const [i, { min: r, max: s }] of Object.entries(e))
495
- n[i] = r === s ? r : { min: r, max: s };
490
+ for (const [i, { min: o, max: r }] of Object.entries(t))
491
+ n[i] = o === r ? o : { min: o, max: r };
496
492
  return n;
497
493
  }
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));
494
+ function ee(e) {
495
+ return e.length >= 4 && e[0] === 79 && e[1] === 84 && e[2] === 84 && e[3] === 79;
496
+ }
497
+ async function ae(e, a) {
498
+ var h, y;
499
+ if (a.outputs.length === 0) return [];
500
+ const t = e instanceof ArrayBuffer ? new Uint8Array(e) : e, n = a.format ?? "ttf", { instances: i, axes: o, family: r } = await B(t), s = [];
501
+ for (const f of a.outputs) {
502
+ let l = {};
503
+ if ((h = f.instances) != null && h.length && (l = Y(f.instances, i)), f.axes && (l = { ...l, ...f.axes }), a.strict) {
504
+ const _ = new Set((f.instances ?? []).map((p) => T(p, i))), w = i.filter((p) => !_.has(p) && o.every((b) => {
505
+ const F = p.coordinates[b.tag] ?? b.default, v = l[b.tag];
506
+ return v == null ? !0 : typeof v == "number" ? F === v : F >= v.min && F <= v.max;
507
+ })).map((p) => p.name);
508
+ if (w.length)
509
+ throw new Error(`vf-clamp: output "${f.name || (f.instances ?? []).join(", ") || "axes-only"}" would include unselected instances (${w.join(", ")}); use planOutputs() to split the selection`);
510
+ }
511
+ for (const _ of o) {
512
+ const w = l[_.tag];
513
+ if (w != null && typeof w == "object") {
514
+ const p = w;
515
+ if (_.default < p.min || _.default > p.max) {
516
+ const b = Math.max(p.min, Math.min(p.max, _.default));
520
517
  console.warn(
521
- `vf-clamp: axis "${d.tag}" default (${d.default}) is outside restricted range [${_.min}, ${_.max}] — will be clamped to ${S}`
518
+ `vf-clamp: axis "${_.tag}" default (${_.default}) is outside restricted range [${p.min}, ${p.max}] — will be clamped to ${b}`
522
519
  );
523
520
  }
524
521
  }
525
522
  }
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 });
523
+ const c = {};
524
+ for (const [_, w] of Object.entries(l))
525
+ w !== null && (c[_] = K(w));
526
+ const u = (y = f.instances) != null && y.length ? f.instances.length === 1 ? f.instances[0] : `${f.instances[0]}-${f.instances[f.instances.length - 1]}` : "Clamped", g = f.name || (r ? `${r} ${u}` : u);
527
+ let m = await Z(t, c);
528
+ if (n === "otf" && !ee(m))
529
+ throw new Error("vf-clamp: format 'otf' needs a CFF/CFF2 source font; this font has TrueType outlines — use 'ttf'");
530
+ a.normalizeWeightAxis && (m = await X(m, 100)), m = await Q(m), m = await G(m, g), n === "woff2" ? m = await C(m) : n === "woff" && (m = await k(m)), s.push({ name: g, buffer: m, format: n });
532
531
  }
533
- return r;
532
+ return s;
534
533
  }
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(" ");
534
+ function O(e, a) {
535
+ const t = {};
536
+ for (const n of a) {
537
+ const i = e.map((o) => o.coordinates[n.tag] ?? n.default);
538
+ t[n.tag] = { min: Math.min(...i), max: Math.max(...i) };
539
+ }
540
+ return t;
541
+ }
542
+ function j(e, a, t, n) {
543
+ return a.filter((i) => t.has(i) ? !1 : n.every((o) => {
544
+ const r = i.coordinates[o.tag] ?? o.default, s = e[o.tag];
545
+ return !s || r >= s.min && r <= s.max;
546
+ }));
547
+ }
548
+ function te(e) {
549
+ return e.reduce((a, t) => !a || t.maximum - t.minimum > a.maximum - a.minimum ? t : a, void 0);
550
+ }
551
+ function ie(e, a, t) {
552
+ const { axes: n, instances: i } = e, o = [...new Set(a)].map((l) => T(l, i));
553
+ if (!o.length) return [];
554
+ const r = new Set(o);
555
+ let s;
556
+ if (!j(O(o, n), i, r, n).length)
557
+ s = [o];
558
+ else {
559
+ s = o.map((c) => [c]);
560
+ let l = !0;
561
+ for (; l; ) {
562
+ l = !1;
563
+ e: for (let c = 0; c < s.length; c++)
564
+ for (let u = c + 1; u < s.length; u++) {
565
+ const g = [...s[c], ...s[u]];
566
+ if (!j(O(g, n), i, r, n).length) {
567
+ s[c] = g, s.splice(u, 1), l = !0;
568
+ break e;
569
+ }
570
+ }
571
+ }
572
+ }
573
+ const h = te(n), y = h ? [h, ...n.filter((l) => l.tag !== h.tag)] : n, f = (l, c) => {
574
+ for (const u of y) {
575
+ const g = (l.coordinates[u.tag] ?? u.default) - (c.coordinates[u.tag] ?? u.default);
576
+ if (g) return g;
577
+ }
578
+ return 0;
579
+ };
580
+ return s = s.map((l) => [...l].sort(f)).sort((l, c) => f(l[0], c[0])), s.map((l) => {
581
+ const c = l[0], u = l[l.length - 1], g = l.length === 1 ? c.name : U(c.name, u.name);
582
+ return { name: t ? `${t} ${g}` : g, instances: l.map((m) => m.name) };
583
+ });
584
+ }
585
+ function oe(e, a) {
586
+ const t = a.map((n) => T(n, e.instances));
587
+ return t.length ? j(O(t, e.axes), e.instances, new Set(t), e.axes).map((n) => n.name) : [];
546
588
  }
547
589
  export {
548
- Z as clampFont,
549
- K as compactName,
550
- R as convertToWoff,
551
- N as convertToWoff2,
552
- j as getInstances
590
+ ae as clampFont,
591
+ U as compactName,
592
+ k as convertToWoff,
593
+ C as convertToWoff2,
594
+ B as getInstances,
595
+ ie as planOutputs,
596
+ oe as unboughtInstances
553
597
  };