@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/LICENSE +21 -0
- package/README.md +539 -372
- package/dist/index.cjs +79 -81
- package/dist/index.d.ts +37 -2
- package/dist/index.js +297 -253
- package/package.json +70 -70
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { createRequire as
|
|
2
|
-
const
|
|
3
|
-
let
|
|
4
|
-
async function
|
|
5
|
-
return
|
|
6
|
-
(
|
|
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
|
-
)),
|
|
16
|
+
)), A;
|
|
17
17
|
}
|
|
18
|
-
async function
|
|
19
|
-
const
|
|
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
|
|
22
|
-
const
|
|
23
|
-
["input-file",
|
|
24
|
-
["output-file",
|
|
25
|
-
["flavor",
|
|
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
|
|
27
|
+
return (await L())(o), i.download();
|
|
28
28
|
} finally {
|
|
29
29
|
try {
|
|
30
|
-
|
|
30
|
+
n.delete();
|
|
31
31
|
} catch {
|
|
32
32
|
}
|
|
33
33
|
try {
|
|
34
|
-
|
|
34
|
+
i.delete();
|
|
35
35
|
} catch {
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
async function
|
|
40
|
-
return
|
|
39
|
+
async function k(e) {
|
|
40
|
+
return R(e, "woff");
|
|
41
41
|
}
|
|
42
|
-
async function
|
|
43
|
-
return
|
|
42
|
+
async function C(e) {
|
|
43
|
+
return R(e, "woff2");
|
|
44
44
|
}
|
|
45
|
-
let
|
|
46
|
-
async function
|
|
47
|
-
return
|
|
48
|
-
(
|
|
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
|
-
)),
|
|
85
|
+
)), I;
|
|
85
86
|
}
|
|
86
|
-
async function
|
|
87
|
-
const
|
|
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
|
|
90
|
-
const
|
|
91
|
-
return JSON.parse(
|
|
90
|
+
await n.upload(t);
|
|
91
|
+
const o = (await E())(n.filename);
|
|
92
|
+
return JSON.parse(o);
|
|
92
93
|
} finally {
|
|
93
94
|
try {
|
|
94
|
-
|
|
95
|
+
n.delete();
|
|
95
96
|
} catch {
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
)),
|
|
214
|
+
)), S;
|
|
157
215
|
}
|
|
158
|
-
async function
|
|
159
|
-
return
|
|
160
|
-
(
|
|
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
|
-
)),
|
|
254
|
+
)), V;
|
|
191
255
|
}
|
|
192
|
-
async function
|
|
193
|
-
return
|
|
194
|
-
(
|
|
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
|
-
)),
|
|
325
|
+
)), D;
|
|
253
326
|
}
|
|
254
|
-
async function
|
|
255
|
-
return
|
|
256
|
-
(
|
|
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
|
|
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
|
|
347
|
-
elif
|
|
348
|
-
fs |= 0x40
|
|
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
|
-
)),
|
|
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
|
|
368
|
-
|
|
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
|
|
371
|
-
if (!
|
|
372
|
-
const
|
|
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
|
|
375
|
-
await
|
|
392
|
+
const o = J(a);
|
|
393
|
+
await n.upload(e);
|
|
376
394
|
const r = /* @__PURE__ */ new Map([
|
|
377
|
-
["input-file",
|
|
378
|
-
["output-file",
|
|
379
|
-
["family-name",
|
|
380
|
-
["postscript-name",
|
|
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
|
|
383
|
-
} catch (
|
|
384
|
-
|
|
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
|
-
|
|
406
|
+
n.delete();
|
|
388
407
|
} catch {
|
|
389
408
|
}
|
|
390
409
|
try {
|
|
391
|
-
|
|
410
|
+
i.delete();
|
|
392
411
|
} catch {
|
|
393
412
|
}
|
|
394
413
|
}
|
|
395
414
|
}
|
|
396
|
-
function
|
|
397
|
-
return typeof
|
|
415
|
+
function K(e) {
|
|
416
|
+
return typeof e == "number" ? e : e === null ? null : [e.min, e.max];
|
|
398
417
|
}
|
|
399
|
-
async function
|
|
400
|
-
const
|
|
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
|
|
403
|
-
const
|
|
404
|
-
["input-file",
|
|
405
|
-
["output-file",
|
|
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
|
|
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
|
-
|
|
433
|
-
} catch {
|
|
434
|
-
}
|
|
435
|
-
try {
|
|
436
|
-
e.delete();
|
|
433
|
+
i.delete();
|
|
437
434
|
} catch {
|
|
438
435
|
}
|
|
439
436
|
}
|
|
440
437
|
}
|
|
441
|
-
async function
|
|
442
|
-
const
|
|
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
|
|
445
|
-
const
|
|
446
|
-
["input-file",
|
|
447
|
-
["output-file",
|
|
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
|
|
450
|
-
} catch (
|
|
451
|
-
|
|
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
|
-
|
|
451
|
+
t.delete();
|
|
455
452
|
} catch {
|
|
456
453
|
}
|
|
457
454
|
try {
|
|
458
|
-
|
|
455
|
+
n.delete();
|
|
459
456
|
} catch {
|
|
460
457
|
}
|
|
461
458
|
}
|
|
462
459
|
}
|
|
463
|
-
async function
|
|
464
|
-
const
|
|
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
|
|
467
|
-
const
|
|
468
|
-
["input-file",
|
|
469
|
-
["output-file",
|
|
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
|
|
472
|
-
} catch (
|
|
473
|
-
|
|
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
|
-
|
|
473
|
+
n.delete();
|
|
477
474
|
} catch {
|
|
478
475
|
}
|
|
479
476
|
try {
|
|
480
|
-
|
|
477
|
+
i.delete();
|
|
481
478
|
} catch {
|
|
482
479
|
}
|
|
483
480
|
}
|
|
484
481
|
}
|
|
485
|
-
function
|
|
486
|
-
const
|
|
487
|
-
for (const i of
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
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:
|
|
495
|
-
n[i] =
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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 "${
|
|
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
|
|
527
|
-
for (const [
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
let
|
|
531
|
-
|
|
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
|
|
532
|
+
return s;
|
|
534
533
|
}
|
|
535
|
-
function
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
};
|