@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.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const U=require("node:module");var I=typeof document<"u"?document.currentScript:null;const E=U.createRequire(typeof document>"u"?require("url").pathToFileURL(__filename).href:I&&I.tagName.toUpperCase()==="SCRIPT"&&I.src||new URL("index.cjs",document.baseURI).href),{preparePyodide:d,PyodideFile:x}=E("@web-alchemy/fonttools/src/pyodide.js");let S=null;async function B(){return S||(S=d().then(e=>e.runPythonAsync(`
|
|
2
2
|
from fontTools.ttLib import TTFont
|
|
3
3
|
|
|
4
4
|
def vf_convert_flavor(file_options):
|
|
@@ -7,15 +7,16 @@ def vf_convert_flavor(file_options):
|
|
|
7
7
|
font.save(file_options['output-file'])
|
|
8
8
|
|
|
9
9
|
vf_convert_flavor
|
|
10
|
-
`))),
|
|
10
|
+
`))),S}async function P(e,a){const t=await d(),n=new x({pyodide:t}),i=new x({pyodide:t});try{await n.upload(e);const o=new Map([["input-file",n.filename],["output-file",i.filename],["flavor",a]]);return(await B())(o),i.download()}finally{try{n.delete()}catch{}try{i.delete()}catch{}}}async function $(e){return P(e,"woff")}async function L(e){return P(e,"woff2")}let A=null;async function W(){return A||(A=d().then(e=>e.runPythonAsync(`
|
|
11
11
|
import json
|
|
12
12
|
from fontTools.ttLib import TTFont
|
|
13
13
|
|
|
14
14
|
def get_instances_fn(input_file):
|
|
15
15
|
font = TTFont(input_file)
|
|
16
16
|
|
|
17
|
+
family = font['name'].getDebugName(16) or font['name'].getDebugName(1) or ''
|
|
17
18
|
if 'fvar' not in font:
|
|
18
|
-
return json.dumps({'axes': [], 'instances': []})
|
|
19
|
+
return json.dumps({'axes': [], 'instances': [], 'family': family})
|
|
19
20
|
|
|
20
21
|
fvar = font['fvar']
|
|
21
22
|
name_table = font['name']
|
|
@@ -39,10 +40,10 @@ def get_instances_fn(input_file):
|
|
|
39
40
|
for i, inst in enumerate(fvar.instances)
|
|
40
41
|
]
|
|
41
42
|
|
|
42
|
-
return json.dumps({'axes': axes, 'instances': instances})
|
|
43
|
+
return json.dumps({'axes': axes, 'instances': instances, 'family': family})
|
|
43
44
|
|
|
44
45
|
get_instances_fn
|
|
45
|
-
`))),
|
|
46
|
+
`))),A}async function M(e){const a=await d(),t=e instanceof ArrayBuffer?new Uint8Array(e):e,n=new x({pyodide:a});try{await n.upload(t);const o=(await W())(n.filename);return JSON.parse(o)}finally{try{n.delete()}catch{}}}function C(e,a){if(e===a)return e;const t=e.split(" "),n=a.split(" ");let i=0;for(;i<t.length&&i<n.length&&t[i]===n[i];)i++;let o=0;for(;o<t.length-i&&o<n.length-i&&t[t.length-1-o]===n[n.length-1-o];)o++;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;return[r,f,y].filter(Boolean).join(" ")}function T(e,a){const t=a.filter(n=>n.name===e);if(!t.length)throw new Error(`Named instance "${e}" not found in font`);if(t.length>1)throw new Error(`Named instance "${e}" is ambiguous: ${t.length} instances share that name`);return t[0]}let V=null,N=null,D=null,O=null;async function z(){return V||(V=d().then(e=>e.runPythonAsync(`
|
|
46
47
|
from fontTools import ttLib
|
|
47
48
|
|
|
48
49
|
def patch_font_names_fn(file_options):
|
|
@@ -53,6 +54,27 @@ def patch_font_names_fn(file_options):
|
|
|
53
54
|
|
|
54
55
|
existing_ids = {r.nameID for r in name_table.names}
|
|
55
56
|
|
|
57
|
+
# A family name with no ASCII letters gives an empty PostScript name: fall back to the
|
|
58
|
+
# source PostScript name plus a short hash of the family name (passed in from JS).
|
|
59
|
+
if not ps_name:
|
|
60
|
+
source_ps = name_table.getDebugName(6) or 'Font'
|
|
61
|
+
ps_name = ('%s-%s' % (source_ps[:52], file_options['postscript-suffix']))[:63]
|
|
62
|
+
|
|
63
|
+
# nameID 2 must be a RIBBI style that matches OS/2 (already updated): Regular, Italic, Bold or Bold Italic.
|
|
64
|
+
fs = font['OS/2'].fsSelection if 'OS/2' in font else 0x40
|
|
65
|
+
is_italic = bool(fs & 0x01)
|
|
66
|
+
is_bold = bool(fs & 0x20)
|
|
67
|
+
ribbi = ('Bold Italic' if is_italic else 'Bold') if is_bold else ('Italic' if is_italic else 'Regular')
|
|
68
|
+
|
|
69
|
+
# nameID 17 (typographic subfamily): the name of the named instance at the new default, if any.
|
|
70
|
+
default_style = None
|
|
71
|
+
if 'fvar' in font:
|
|
72
|
+
defaults = {ax.axisTag: ax.defaultValue for ax in font['fvar'].axes}
|
|
73
|
+
for inst in font['fvar'].instances:
|
|
74
|
+
if all(abs(inst.coordinates.get(t, v) - v) < 0.01 for t, v in defaults.items()):
|
|
75
|
+
default_style = name_table.getDebugName(inst.subfamilyNameID)
|
|
76
|
+
break
|
|
77
|
+
|
|
56
78
|
# nameID 1 = Family, 4 = Full name, 6 = PostScript name
|
|
57
79
|
# nameID 2 = Subfamily — reset to 'Regular' so the restricted file does not
|
|
58
80
|
# collide with the source font's Subfamily in OS font caches.
|
|
@@ -70,32 +92,50 @@ def patch_font_names_fn(file_options):
|
|
|
70
92
|
|
|
71
93
|
updates = {
|
|
72
94
|
1: family_name,
|
|
73
|
-
2:
|
|
95
|
+
2: ribbi,
|
|
74
96
|
3: unique_id,
|
|
75
|
-
4: family_name,
|
|
97
|
+
4: family_name if ribbi == 'Regular' else '%s %s' % (family_name, ribbi),
|
|
76
98
|
6: ps_name,
|
|
77
99
|
}
|
|
78
100
|
if 16 in existing_ids:
|
|
79
101
|
updates[16] = family_name
|
|
102
|
+
if 17 in existing_ids and default_style:
|
|
103
|
+
updates[17] = default_style
|
|
80
104
|
if 25 in existing_ids:
|
|
81
105
|
updates[25] = ps_name
|
|
82
106
|
|
|
107
|
+
# Named instances' PostScript names must follow the new prefix, e.g. Inter-Regular-Bold-Medium.
|
|
108
|
+
if 'fvar' in font:
|
|
109
|
+
for inst in font['fvar'].instances:
|
|
110
|
+
pid = getattr(inst, 'postscriptNameID', 0xFFFF)
|
|
111
|
+
if pid in (None, 0xFFFF):
|
|
112
|
+
continue
|
|
113
|
+
style = (name_table.getDebugName(inst.subfamilyNameID) or '').replace(' ', '')
|
|
114
|
+
style = ''.join(c for c in style if c.isascii() and (c.isalnum() or c == '-'))
|
|
115
|
+
updates[pid] = ('%s-%s' % (ps_name, style))[:63]
|
|
116
|
+
|
|
117
|
+
# Rewrite every platform. A Mac (platform 1) record that can't be encoded in Mac Roman is dropped
|
|
118
|
+
# rather than filled with '?'; Windows and Unicode records always carry the full name.
|
|
119
|
+
keep = []
|
|
83
120
|
for record in name_table.names:
|
|
84
121
|
if record.nameID not in updates:
|
|
122
|
+
keep.append(record)
|
|
85
123
|
continue
|
|
86
124
|
value = updates[record.nameID]
|
|
87
|
-
if record.platformID
|
|
125
|
+
if record.platformID in (0, 3):
|
|
88
126
|
record.string = value.encode('utf-16-be')
|
|
89
127
|
elif record.platformID == 1:
|
|
90
128
|
try:
|
|
91
129
|
record.string = value.encode('mac_roman')
|
|
92
130
|
except Exception:
|
|
93
|
-
|
|
131
|
+
continue
|
|
132
|
+
keep.append(record)
|
|
133
|
+
name_table.names = keep
|
|
94
134
|
|
|
95
135
|
font.save(file_options['output-file'])
|
|
96
136
|
|
|
97
137
|
patch_font_names_fn
|
|
98
|
-
`))),
|
|
138
|
+
`))),V}async function q(){return N||(N=d().then(e=>e.runPythonAsync(`
|
|
99
139
|
from fontTools.ttLib import TTFont
|
|
100
140
|
from fontTools.varLib import instancer
|
|
101
141
|
import json
|
|
@@ -104,6 +144,12 @@ def vf_clamp_instantiate(file_options, axes_json):
|
|
|
104
144
|
font = TTFont(file_options['input-file'])
|
|
105
145
|
axes_spec = json.loads(axes_json)
|
|
106
146
|
|
|
147
|
+
# The bundled fontTools can't restrict avar version 2 or VARC fonts correctly: refuse instead of mis-cutting.
|
|
148
|
+
if 'avar' in font and getattr(font['avar'], 'majorVersion', 1) >= 2:
|
|
149
|
+
raise ValueError('vf-clamp: avar version 2 fonts are not supported yet')
|
|
150
|
+
if 'VARC' in font:
|
|
151
|
+
raise ValueError('vf-clamp: fonts with a VARC table are not supported yet')
|
|
152
|
+
|
|
107
153
|
limits = {}
|
|
108
154
|
axes_by_tag = {ax.axisTag: ax for ax in font['fvar'].axes}
|
|
109
155
|
|
|
@@ -124,7 +170,7 @@ def vf_clamp_instantiate(file_options, axes_json):
|
|
|
124
170
|
partial.save(file_options['output-file'])
|
|
125
171
|
|
|
126
172
|
vf_clamp_instantiate
|
|
127
|
-
`))),
|
|
173
|
+
`))),N}async function H(){return D||(D=d().then(e=>e.runPythonAsync(`
|
|
128
174
|
from fontTools.ttLib import TTFont
|
|
129
175
|
|
|
130
176
|
def vf_clamp_normalize_wght(file_options, new_min_str):
|
|
@@ -144,6 +190,10 @@ def vf_clamp_normalize_wght(file_options, new_min_str):
|
|
|
144
190
|
|
|
145
191
|
old_min = wght_axis.minValue
|
|
146
192
|
default = wght_axis.defaultValue
|
|
193
|
+
# With the default at the minimum there is no below-default range to stretch; leave the font as is.
|
|
194
|
+
if default <= old_min:
|
|
195
|
+
font.save(file_options['output-file'])
|
|
196
|
+
return 'skipped'
|
|
147
197
|
scale = (default - new_min) / (default - old_min)
|
|
148
198
|
|
|
149
199
|
def remap(v):
|
|
@@ -177,69 +227,16 @@ def vf_clamp_normalize_wght(file_options, new_min_str):
|
|
|
177
227
|
av.NominalValue = remap(av.NominalValue)
|
|
178
228
|
av.RangeMinValue = remap(av.RangeMinValue)
|
|
179
229
|
av.RangeMaxValue = remap(av.RangeMaxValue)
|
|
230
|
+
elif fmt == 4:
|
|
231
|
+
for rec in getattr(av, 'AxisValueRecord', []) or []:
|
|
232
|
+
if rec.AxisIndex == wght_idx:
|
|
233
|
+
rec.Value = remap(rec.Value)
|
|
180
234
|
|
|
181
235
|
font.save(file_options['output-file'])
|
|
236
|
+
return 'done'
|
|
182
237
|
|
|
183
238
|
vf_clamp_normalize_wght
|
|
184
|
-
`))),
|
|
185
|
-
from fontTools.ttLib import TTFont
|
|
186
|
-
|
|
187
|
-
def vf_clamp_prune_stat(file_options):
|
|
188
|
-
font = TTFont(file_options['input-file'])
|
|
189
|
-
|
|
190
|
-
if 'STAT' not in font or 'fvar' not in font:
|
|
191
|
-
font.save(file_options['output-file'])
|
|
192
|
-
return
|
|
193
|
-
|
|
194
|
-
stat = font['STAT'].table
|
|
195
|
-
if not getattr(stat, 'DesignAxisRecord', None) or not stat.DesignAxisRecord.Axis:
|
|
196
|
-
font.save(file_options['output-file'])
|
|
197
|
-
return
|
|
198
|
-
|
|
199
|
-
fvar_tags = {ax.axisTag for ax in font['fvar'].axes}
|
|
200
|
-
stat_axes = list(stat.DesignAxisRecord.Axis)
|
|
201
|
-
|
|
202
|
-
# Map old STAT AxisIndex -> new index after pruning STAT axes that vanish from fvar.
|
|
203
|
-
surviving_indices = [i for i, ax in enumerate(stat_axes) if ax.AxisTag in fvar_tags]
|
|
204
|
-
if len(surviving_indices) == len(stat_axes):
|
|
205
|
-
# No axes removed — nothing to prune.
|
|
206
|
-
font.save(file_options['output-file'])
|
|
207
|
-
return
|
|
208
|
-
old_to_new = {old: new for new, old in enumerate(surviving_indices)}
|
|
209
|
-
|
|
210
|
-
# Rewrite the DesignAxisRecord.Axis list to surviving axes only.
|
|
211
|
-
stat.DesignAxisRecord.Axis = [stat_axes[i] for i in surviving_indices]
|
|
212
|
-
if hasattr(stat, 'DesignAxisCount'):
|
|
213
|
-
stat.DesignAxisCount = len(stat.DesignAxisRecord.Axis)
|
|
214
|
-
|
|
215
|
-
# Prune AxisValueArray entries that reference removed axes.
|
|
216
|
-
if stat.AxisValueArray and stat.AxisValueArray.AxisValue:
|
|
217
|
-
kept = []
|
|
218
|
-
for av in stat.AxisValueArray.AxisValue:
|
|
219
|
-
fmt = av.Format
|
|
220
|
-
if fmt in (1, 2, 3):
|
|
221
|
-
if av.AxisIndex not in old_to_new:
|
|
222
|
-
continue # references a removed axis
|
|
223
|
-
av.AxisIndex = old_to_new[av.AxisIndex]
|
|
224
|
-
kept.append(av)
|
|
225
|
-
elif fmt == 4:
|
|
226
|
-
inner = list(getattr(av, 'AxisValueRecord', []) or [])
|
|
227
|
-
if any(rec.AxisIndex not in old_to_new for rec in inner):
|
|
228
|
-
continue # whole record dies if any inner ref is dead
|
|
229
|
-
for rec in inner:
|
|
230
|
-
rec.AxisIndex = old_to_new[rec.AxisIndex]
|
|
231
|
-
kept.append(av)
|
|
232
|
-
else:
|
|
233
|
-
# Unknown format — keep as-is to be safe.
|
|
234
|
-
kept.append(av)
|
|
235
|
-
stat.AxisValueArray.AxisValue = kept
|
|
236
|
-
if hasattr(stat, 'AxisValueCount'):
|
|
237
|
-
stat.AxisValueCount = len(kept)
|
|
238
|
-
|
|
239
|
-
font.save(file_options['output-file'])
|
|
240
|
-
|
|
241
|
-
vf_clamp_prune_stat
|
|
242
|
-
`))),T}async function $(){return b||(b=l().then(e=>e.runPythonAsync(`
|
|
239
|
+
`))),D}async function J(){return O||(O=d().then(e=>e.runPythonAsync(`
|
|
243
240
|
from fontTools.ttLib import TTFont
|
|
244
241
|
|
|
245
242
|
def vf_clamp_update_os2(file_options):
|
|
@@ -250,25 +247,26 @@ def vf_clamp_update_os2(file_options):
|
|
|
250
247
|
return
|
|
251
248
|
|
|
252
249
|
wght_axis = next((ax for ax in font['fvar'].axes if ax.axisTag == 'wght'), None)
|
|
253
|
-
if wght_axis is None:
|
|
250
|
+
if wght_axis is not None:
|
|
251
|
+
# OS/2.usWeightClass valid range is 1..1000.
|
|
252
|
+
weight_class = int(round(max(1, min(1000, wght_axis.defaultValue))))
|
|
253
|
+
elif 'OS/2' in font:
|
|
254
|
+
# wght was pinned: fontTools has already set usWeightClass to the pinned weight.
|
|
255
|
+
weight_class = font['OS/2'].usWeightClass
|
|
256
|
+
else:
|
|
254
257
|
font.save(file_options['output-file'])
|
|
255
258
|
return
|
|
256
259
|
|
|
257
|
-
new_default = wght_axis.defaultValue
|
|
258
|
-
# OS/2.usWeightClass valid range is 1..1000.
|
|
259
|
-
weight_class = int(round(max(1, min(1000, new_default))))
|
|
260
|
-
|
|
261
260
|
if 'OS/2' in font:
|
|
262
261
|
os2 = font['OS/2']
|
|
263
262
|
os2.usWeightClass = weight_class
|
|
264
|
-
# fsSelection
|
|
265
|
-
# Mirror the convention: REGULAR when usWeightClass < 600, BOLD when >= 700.
|
|
263
|
+
# fsSelection: 0x01 ITALIC (kept), 0x20 BOLD, 0x40 REGULAR. REGULAR only when neither ITALIC nor BOLD.
|
|
266
264
|
fs = os2.fsSelection
|
|
267
265
|
fs &= ~(0x20 | 0x40)
|
|
268
266
|
if weight_class >= 700:
|
|
269
|
-
fs |= 0x20
|
|
270
|
-
elif
|
|
271
|
-
fs |= 0x40
|
|
267
|
+
fs |= 0x20
|
|
268
|
+
elif not fs & 0x01:
|
|
269
|
+
fs |= 0x40
|
|
272
270
|
os2.fsSelection = fs
|
|
273
271
|
|
|
274
272
|
if 'head' in font:
|
|
@@ -284,4 +282,4 @@ def vf_clamp_update_os2(file_options):
|
|
|
284
282
|
font.save(file_options['output-file'])
|
|
285
283
|
|
|
286
284
|
vf_clamp_update_os2
|
|
287
|
-
`))),
|
|
285
|
+
`))),O}function G(e){const a=e.normalize("NFKD").replace(/[\u0300-\u036f]/g,"").replace(/[^A-Za-z0-9 -]/g,"").trim().split(/\s+/).join("-").replace(/-+/g,"-").replace(/^-+|-+$/g,"");return a.length<=63?a:`${a.slice(0,56).replace(/-+$/,"")}-${k(e)}`}function k(e){let a=2166136261;for(const t of e)a^=t.codePointAt(0),a=Math.imul(a,16777619)>>>0;return a.toString(36).padStart(6,"0").slice(-6)}async function K(e,a){if(!a)return e;const t=await d(),n=new x({pyodide:t}),i=new x({pyodide:t});try{const o=G(a);await n.upload(e);const r=new Map([["input-file",n.filename],["output-file",i.filename],["family-name",a],["postscript-name",o],["postscript-suffix",k(a)]]);return(await z())(r),i.download()}catch(o){throw Object.assign(new Error(`vf-clamp: name table patching failed for "${a}"`),{cause:o})}finally{try{n.delete()}catch{}try{i.delete()}catch{}}}function Z(e){return typeof e=="number"?e:e===null?null:[e.min,e.max]}async function Q(e,a){const t=await d(),n=new x({pyodide:t}),i=new x({pyodide:t});try{await n.upload(e);const o=new Map([["input-file",n.filename],["output-file",i.filename]]);return(await q())(o,JSON.stringify(a)),i.download()}finally{try{n.delete()}catch{}try{i.delete()}catch{}}}async function X(e){const a=await d(),t=new x({pyodide:a}),n=new x({pyodide:a});try{await t.upload(e);const i=new Map([["input-file",t.filename],["output-file",n.filename]]);return(await J())(i),n.download()}catch(i){throw Object.assign(new Error("vf-clamp: OS/2 and macStyle update failed"),{cause:i})}finally{try{t.delete()}catch{}try{n.delete()}catch{}}}async function Y(e,a){const t=await d(),n=new x({pyodide:t}),i=new x({pyodide:t});try{await n.upload(e);const o=new Map([["input-file",n.filename],["output-file",i.filename]]);return(await H())(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()}catch(o){throw Object.assign(new Error("vf-clamp: wght normalisation failed"),{cause:o})}finally{try{n.delete()}catch{}try{i.delete()}catch{}}}function ee(e,a){const t={};for(const i of e){const o=T(i,a);for(const[r,s]of Object.entries(o.coordinates))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}}const n={};for(const[i,{min:o,max:r}]of Object.entries(t))n[i]=o===r?o:{min:o,max:r};return n}function te(e){return e.length>=4&&e[0]===79&&e[1]===84&&e[2]===84&&e[3]===79}async function ne(e,a){var h,y;if(a.outputs.length===0)return[];const t=e instanceof ArrayBuffer?new Uint8Array(e):e,n=a.format??"ttf",{instances:i,axes:o,family:r}=await M(t),s=[];for(const f of a.outputs){let l={};if((h=f.instances)!=null&&h.length&&(l=ee(f.instances,i)),f.axes&&(l={...l,...f.axes}),a.strict){const _=new Set((f.instances??[]).map(p=>T(p,i))),w=i.filter(p=>!_.has(p)&&o.every(b=>{const F=p.coordinates[b.tag]??b.default,v=l[b.tag];return v==null?!0:typeof v=="number"?F===v:F>=v.min&&F<=v.max})).map(p=>p.name);if(w.length)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`)}for(const _ of o){const w=l[_.tag];if(w!=null&&typeof w=="object"){const p=w;if(_.default<p.min||_.default>p.max){const b=Math.max(p.min,Math.min(p.max,_.default));console.warn(`vf-clamp: axis "${_.tag}" default (${_.default}) is outside restricted range [${p.min}, ${p.max}] — will be clamped to ${b}`)}}}const c={};for(const[_,w]of Object.entries(l))w!==null&&(c[_]=Z(w));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);let m=await Q(t,c);if(n==="otf"&&!te(m))throw new Error("vf-clamp: format 'otf' needs a CFF/CFF2 source font; this font has TrueType outlines — use 'ttf'");a.normalizeWeightAxis&&(m=await Y(m,100)),m=await X(m),m=await K(m,g),n==="woff2"?m=await L(m):n==="woff"&&(m=await $(m)),s.push({name:g,buffer:m,format:n})}return s}function R(e,a){const t={};for(const n of a){const i=e.map(o=>o.coordinates[n.tag]??n.default);t[n.tag]={min:Math.min(...i),max:Math.max(...i)}}return t}function j(e,a,t,n){return a.filter(i=>t.has(i)?!1:n.every(o=>{const r=i.coordinates[o.tag]??o.default,s=e[o.tag];return!s||r>=s.min&&r<=s.max}))}function ae(e){return e.reduce((a,t)=>!a||t.maximum-t.minimum>a.maximum-a.minimum?t:a,void 0)}function ie(e,a,t){const{axes:n,instances:i}=e,o=[...new Set(a)].map(l=>T(l,i));if(!o.length)return[];const r=new Set(o);let s;if(!j(R(o,n),i,r,n).length)s=[o];else{s=o.map(c=>[c]);let l=!0;for(;l;){l=!1;e:for(let c=0;c<s.length;c++)for(let u=c+1;u<s.length;u++){const g=[...s[c],...s[u]];if(!j(R(g,n),i,r,n).length){s[c]=g,s.splice(u,1),l=!0;break e}}}}const h=ae(n),y=h?[h,...n.filter(l=>l.tag!==h.tag)]:n,f=(l,c)=>{for(const u of y){const g=(l.coordinates[u.tag]??u.default)-(c.coordinates[u.tag]??u.default);if(g)return g}return 0};return s=s.map(l=>[...l].sort(f)).sort((l,c)=>f(l[0],c[0])),s.map(l=>{const c=l[0],u=l[l.length-1],g=l.length===1?c.name:C(c.name,u.name);return{name:t?`${t} ${g}`:g,instances:l.map(m=>m.name)}})}function oe(e,a){const t=a.map(n=>T(n,e.instances));return t.length?j(R(t,e.axes),e.instances,new Set(t),e.axes).map(n=>n.name):[]}exports.clampFont=ne;exports.compactName=C;exports.convertToWoff=$;exports.convertToWoff2=L;exports.getInstances=M;exports.planOutputs=ie;exports.unboughtInstances=oe;
|
package/dist/index.d.ts
CHANGED
|
@@ -52,9 +52,16 @@ export declare interface ClampOptions {
|
|
|
52
52
|
/**
|
|
53
53
|
* Output format — defaults to 'ttf'.
|
|
54
54
|
* 'woff' and 'woff2' transcode the result to a web-compressed flavour.
|
|
55
|
-
* 'otf' is a
|
|
55
|
+
* 'otf' is a label, not a conversion — it requires a CFF/CFF2 source and throws for TrueType outlines.
|
|
56
56
|
*/
|
|
57
57
|
format?: OutputFormat;
|
|
58
|
+
/**
|
|
59
|
+
* Refuse any output whose range would include a named instance that was not listed
|
|
60
|
+
* (e.g. Regular + Bold would also hand over Medium and SemiBold). Axes-only outputs list none,
|
|
61
|
+
* so they pass only if their range holds no named instance. Throws instead of clamping.
|
|
62
|
+
* Use planOutputs() to split a customer's selection into safe outputs. Defaults to false.
|
|
63
|
+
*/
|
|
64
|
+
strict?: boolean;
|
|
58
65
|
/**
|
|
59
66
|
* Remap the wght axis so its minimum becomes 100, making CSS font-weight values
|
|
60
67
|
* (100–900) work as expected. Instance and STAT axis value coordinates are
|
|
@@ -111,6 +118,8 @@ export declare interface FontInstancesResult {
|
|
|
111
118
|
axes: AxisDefinition[];
|
|
112
119
|
/** All named instances defined in the font */
|
|
113
120
|
instances: FontInstance[];
|
|
121
|
+
/** The font's family name (typographic family, nameID 16, else nameID 1); used to name outputs that set no name */
|
|
122
|
+
family?: string;
|
|
114
123
|
}
|
|
115
124
|
|
|
116
125
|
/**
|
|
@@ -147,14 +156,40 @@ export declare interface OutputConfig {
|
|
|
147
156
|
/**
|
|
148
157
|
* Output format for clampFont() — defaults to 'ttf'.
|
|
149
158
|
* 'woff' and 'woff2' transcode to web-compressed flavours.
|
|
150
|
-
* 'otf'
|
|
159
|
+
* 'otf' requires a CFF/CFF2 source (no outline conversion is done) and throws for TrueType outlines.
|
|
151
160
|
*/
|
|
152
161
|
export declare type OutputFormat = 'ttf' | 'otf' | 'woff' | 'woff2';
|
|
153
162
|
|
|
154
163
|
/** Pin an axis at a single value — the axis is locked and removed from the output font's design space */
|
|
155
164
|
export declare type PinnedAxis = number;
|
|
156
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Groups a customer's selected named instances into clampFont outputs so that no output's range
|
|
168
|
+
* contains a named instance the customer did not select.
|
|
169
|
+
*
|
|
170
|
+
* Selections merge into one output only when their combined range holds no unselected named
|
|
171
|
+
* instance: Regular + Medium + SemiBold + Bold → one output; Regular + Bold alone → two outputs
|
|
172
|
+
* (each pinned to its own instance). Output names come from compactName(), optionally prefixed
|
|
173
|
+
* with `family`.
|
|
174
|
+
*
|
|
175
|
+
* @param font - The font's axes and named instances, from getInstances()
|
|
176
|
+
* @param selected - Names of the instances the customer bought (must match exactly)
|
|
177
|
+
* @param family - Optional family name to prefix each output name with, e.g. "Encode Sans"
|
|
178
|
+
* @returns OutputConfig entries ready for clampFont(), sorted along the primary axis
|
|
179
|
+
* @throws If a selected name is not a named instance of the font, or names more than one (ambiguous)
|
|
180
|
+
*/
|
|
181
|
+
export declare function planOutputs(font: FontInstancesResult, selected: string[], family?: string): OutputConfig[];
|
|
182
|
+
|
|
157
183
|
/** @deprecated Use OutputConfig instead */
|
|
158
184
|
export declare type SubfamilyConfig = OutputConfig;
|
|
159
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Lists the named instances that a single output built from `names` would include without being selected,
|
|
188
|
+
* e.g. ['Medium', 'SemiBold'] for Regular + Bold. An empty array means the output is purchase-safe.
|
|
189
|
+
*
|
|
190
|
+
* @param font - The font's axes and named instances, from getInstances()
|
|
191
|
+
* @param names - The instance names one output would hull
|
|
192
|
+
*/
|
|
193
|
+
export declare function unboughtInstances(font: FontInstancesResult, names: string[]): string[];
|
|
194
|
+
|
|
160
195
|
export { }
|