@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.
- package/README.md +372 -0
- package/dist/index.cjs +287 -0
- package/dist/index.d.ts +160 -0
- package/dist/index.js +553 -0
- package/package.json +70 -0
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
};
|