var2stat 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ environment: pypi
14
+
15
+ steps:
16
+ - name: Checkout
17
+ uses: actions/checkout@v6
18
+
19
+ - name: Setup uv
20
+ uses: astral-sh/setup-uv@v7
21
+
22
+ - name: Build package
23
+ run: uv build
24
+
25
+ - name: Publish to PyPI
26
+ env:
27
+ TWINE_USERNAME: __token__
28
+ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
29
+ run: |
30
+ uv tool install twine
31
+ twine upload dist/*
@@ -0,0 +1,20 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ jobs:
13
+ release-please:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - name: Run release-please
17
+ id: release
18
+ uses: googleapis/release-please-action@v4
19
+ with:
20
+ token: ${{ secrets.RELEASE_PLEASE_PAT }}
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1 @@
1
+ {".":"0.1.0"}
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-03-26)
4
+
5
+
6
+ ### Features
7
+
8
+ * uv tool support ([2ed51af](https://github.com/decipher3114/Var2Stat/commit/2ed51af59460117bf5e350e4aa509856239460e2))
var2stat-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Var2Stat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: var2stat
3
+ Version: 0.1.0
4
+ Summary: Convert variable fonts to static font instances
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.14
7
+ Requires-Dist: click>=8.3.1
8
+ Requires-Dist: fonttools>=4.62.1
9
+ Requires-Dist: jsonschema>=4.26.0
10
+ Description-Content-Type: text/markdown
11
+
12
+ # Var2Stat
13
+
14
+ Convert variable fonts to static font instances with configuration-based axis control.
15
+
16
+ ## Features
17
+
18
+ - Extract axis information and generate optimized configuration files
19
+ - Smart axis inheritance (global defaults with variant overrides)
20
+ - True static output with all variation tables removed
21
+
22
+ ## Usage
23
+
24
+ - **Step 1**: Generate config from variable font (creates `{FontName}-config.json` in the same directory as the font):
25
+
26
+ ```/dev/null/usage.sh#L1-1
27
+ uvx var2stat config <font_file.ttf>
28
+ ```
29
+
30
+ - **Step 2**: Generate static fonts from a config file:
31
+
32
+ ```/dev/null/usage.sh#L1-1
33
+ uvx var2stat generate <config_file.json>
34
+ ```
35
+
36
+ Output is always saved to a folder named after the canonicalized font name, with files named `<canonical_font_name>_<variant>.ttf`.
37
+
38
+ ## Config Structure
39
+
40
+ ```json
41
+ {
42
+ "$schema": "https://raw.githubusercontent.com/decipher3114/Var2Stat/refs/heads/main/schema.json",
43
+ "file": "font.ttf",
44
+ "font_name": "Font Name",
45
+ "axes": { "wght": null, "opsz": 16 },
46
+ "variants": {
47
+ "Regular": { "wght": 400 },
48
+ "Bold": { "wght": 700 }
49
+ }
50
+ }
51
+ ```
52
+
53
+ - Global axes with `null` use font defaults
54
+ - Variant axes override global values
@@ -0,0 +1,43 @@
1
+ # Var2Stat
2
+
3
+ Convert variable fonts to static font instances with configuration-based axis control.
4
+
5
+ ## Features
6
+
7
+ - Extract axis information and generate optimized configuration files
8
+ - Smart axis inheritance (global defaults with variant overrides)
9
+ - True static output with all variation tables removed
10
+
11
+ ## Usage
12
+
13
+ - **Step 1**: Generate config from variable font (creates `{FontName}-config.json` in the same directory as the font):
14
+
15
+ ```/dev/null/usage.sh#L1-1
16
+ uvx var2stat config <font_file.ttf>
17
+ ```
18
+
19
+ - **Step 2**: Generate static fonts from a config file:
20
+
21
+ ```/dev/null/usage.sh#L1-1
22
+ uvx var2stat generate <config_file.json>
23
+ ```
24
+
25
+ Output is always saved to a folder named after the canonicalized font name, with files named `<canonical_font_name>_<variant>.ttf`.
26
+
27
+ ## Config Structure
28
+
29
+ ```json
30
+ {
31
+ "$schema": "https://raw.githubusercontent.com/decipher3114/Var2Stat/refs/heads/main/schema.json",
32
+ "file": "font.ttf",
33
+ "font_name": "Font Name",
34
+ "axes": { "wght": null, "opsz": 16 },
35
+ "variants": {
36
+ "Regular": { "wght": 400 },
37
+ "Bold": { "wght": 700 }
38
+ }
39
+ }
40
+ ```
41
+
42
+ - Global axes with `null` use font defaults
43
+ - Variant axes override global values
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "var2stat"
3
+ version = "0.1.0"
4
+ description = "Convert variable fonts to static font instances"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ "click>=8.3.1",
9
+ "fonttools>=4.62.1",
10
+ "jsonschema>=4.26.0",
11
+ ]
12
+
13
+ [project.scripts]
14
+ var2stat = "var2stat.__main__:cli"
15
+
16
+ [build-system]
17
+ requires = ["hatchling"]
18
+ build-backend = "hatchling.build"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["src/var2stat"]
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "packages": {
4
+ ".": {
5
+ "package-name": "var2stat"
6
+ }
7
+ },
8
+ "include-v-in-release-name": true,
9
+ "release-type": "python"
10
+ }
@@ -0,0 +1,167 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/decipher3114/Var2Stat",
4
+ "title": "Var2Stat Configuration",
5
+ "description": "Configuration schema for generating static fonts from variable fonts",
6
+ "type": "object",
7
+ "required": [
8
+ "file",
9
+ "font_name",
10
+ "axes",
11
+ "variants"
12
+ ],
13
+ "definitions": {
14
+ "axes": {
15
+ "type": "object",
16
+ "properties": {
17
+ "wght": {
18
+ "type": [
19
+ "number",
20
+ "null"
21
+ ],
22
+ "minimum": 1,
23
+ "maximum": 1000,
24
+ "description": "Weight axis (1-1000, typically 100-900)"
25
+ },
26
+ "wdth": {
27
+ "type": [
28
+ "number",
29
+ "null"
30
+ ],
31
+ "minimum": 25,
32
+ "maximum": 200,
33
+ "description": "Width axis (25-200%, typically 50-200)"
34
+ },
35
+ "opsz": {
36
+ "type": [
37
+ "number",
38
+ "null"
39
+ ],
40
+ "minimum": 6,
41
+ "maximum": 144,
42
+ "description": "Optical size axis (6-144pt)"
43
+ },
44
+ "slnt": {
45
+ "type": [
46
+ "number",
47
+ "null"
48
+ ],
49
+ "minimum": -90,
50
+ "maximum": 90,
51
+ "description": "Slant axis (-90° to 90°)"
52
+ },
53
+ "ital": {
54
+ "anyOf": [
55
+ {
56
+ "enum": [
57
+ 0,
58
+ 1
59
+ ]
60
+ },
61
+ {
62
+ "type": "null"
63
+ }
64
+ ],
65
+ "description": "Italic axis (0=roman, 1=italic)"
66
+ },
67
+ "GRAD": {
68
+ "type": [
69
+ "number",
70
+ "null"
71
+ ],
72
+ "minimum": -200,
73
+ "maximum": 300,
74
+ "description": "Grade axis (-200 to 300)"
75
+ },
76
+ "ROND": {
77
+ "type": [
78
+ "number",
79
+ "null"
80
+ ],
81
+ "description": "Roundness axis"
82
+ },
83
+ "CASL": {
84
+ "type": [
85
+ "number",
86
+ "null"
87
+ ],
88
+ "description": "Casual axis"
89
+ },
90
+ "CRSV": {
91
+ "type": [
92
+ "number",
93
+ "null"
94
+ ],
95
+ "description": "Cursive axis"
96
+ },
97
+ "MONO": {
98
+ "type": [
99
+ "number",
100
+ "null"
101
+ ],
102
+ "description": "Monospace axis"
103
+ },
104
+ "SOFT": {
105
+ "type": [
106
+ "number",
107
+ "null"
108
+ ],
109
+ "description": "Softness axis"
110
+ },
111
+ "WONK": {
112
+ "type": [
113
+ "number",
114
+ "null"
115
+ ],
116
+ "description": "Wonky axis"
117
+ }
118
+ },
119
+ "additionalProperties": {
120
+ "type": [
121
+ "number",
122
+ "null"
123
+ ],
124
+ "description": "Custom axis value"
125
+ }
126
+ }
127
+ },
128
+ "properties": {
129
+ "$schema": {
130
+ "type": "string",
131
+ "description": "JSON schema reference"
132
+ },
133
+ "file": {
134
+ "type": "string",
135
+ "description": "Path to the variable font file",
136
+ "pattern": "\\.(ttf|otf)$"
137
+ },
138
+ "font_name": {
139
+ "type": [
140
+ "string",
141
+ "null"
142
+ ],
143
+ "description": "Target font family name for generated static fonts"
144
+ },
145
+ "axes": {
146
+ "allOf": [
147
+ {
148
+ "$ref": "#/definitions/axes"
149
+ }
150
+ ],
151
+ "description": "Global axis default values for all variants"
152
+ },
153
+ "variants": {
154
+ "type": "object",
155
+ "description": "Static font variants to generate keys",
156
+ "minProperties": 1,
157
+ "patternProperties": {
158
+ "^[a-zA-Z0-9_-]+$": {
159
+ "$ref": "#/definitions/axes",
160
+ "description": "Variant-specific axis values"
161
+ }
162
+ },
163
+ "additionalProperties": false
164
+ }
165
+ },
166
+ "additionalProperties": false
167
+ }
@@ -0,0 +1,3 @@
1
+ """var2stat package."""
2
+
3
+ __all__: list[str] = []
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import click
4
+
5
+ from .config import config_command
6
+ from .generate import generate_command
7
+
8
+
9
+ @click.group()
10
+ def cli() -> None:
11
+ """Convert variable fonts to static font instances."""
12
+ pass
13
+
14
+
15
+ cli.add_command(config_command, name="config")
16
+ cli.add_command(generate_command, name="generate")
17
+
18
+
19
+ if __name__ == "__main__":
20
+ cli()
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, Dict
6
+
7
+ import click
8
+ from fontTools.ttLib import TTFont
9
+
10
+ SCHEMA = "https://raw.githubusercontent.com/decipher3114/Var2Stat/refs/heads/main/schema.json"
11
+
12
+
13
+ def extract_font_info(font_path: str) -> Dict[str, Any]:
14
+ font_path_obj = Path(font_path).resolve()
15
+ if not font_path_obj.exists():
16
+ raise FileNotFoundError(f"Font file '{font_path}' not found")
17
+
18
+ font = TTFont(font_path)
19
+ print(f"[INFO]: Font file loaded: {font_path_obj.name}")
20
+
21
+ try:
22
+ if "fvar" not in font:
23
+ raise ValueError("This is not a variable font (missing fvar table)")
24
+
25
+ # Extract original font family name
26
+ original_font_name = None
27
+ for record in font["name"].names:
28
+ if record.nameID == 1 and record.platformID == 3 and record.platEncID == 1:
29
+ original_font_name = record.toUnicode()
30
+ break
31
+
32
+ if not original_font_name:
33
+ original_font_name = font_path_obj.stem
34
+
35
+ # Process axis defaults
36
+ axes_defaults = {axis.axisTag: axis.defaultValue for axis in font["fvar"].axes}
37
+
38
+ # Process available instances
39
+ variants = {}
40
+ weight_names = {
41
+ 100: "Thin",
42
+ 200: "ExtraLight",
43
+ 300: "Light",
44
+ 400: "Regular",
45
+ 500: "Medium",
46
+ 600: "SemiBold",
47
+ 700: "Bold",
48
+ 800: "ExtraBold",
49
+ 900: "Black",
50
+ }
51
+
52
+ for i, instance in enumerate(font["fvar"].instances):
53
+ coordinates = dict(instance.coordinates)
54
+
55
+ # Generate descriptive name from weight
56
+ if "wght" in coordinates:
57
+ weight_value = coordinates["wght"]
58
+ if weight_value in weight_names:
59
+ instance_name = weight_names[weight_value]
60
+ else:
61
+ closest = min(
62
+ weight_names.keys(), key=lambda x: abs(x - weight_value)
63
+ )
64
+ instance_name = f"{weight_names[closest]}{int(weight_value)}"
65
+ else:
66
+ instance_name = f"Instance{i + 1}"
67
+
68
+ variants[instance_name] = coordinates
69
+
70
+ return {
71
+ "font_path": str(font_path_obj),
72
+ "font_name": original_font_name,
73
+ "axes_defaults": axes_defaults,
74
+ "variants": variants,
75
+ }
76
+
77
+ finally:
78
+ font.close()
79
+
80
+
81
+ def create_config_structure(font_info: Dict[str, Any]) -> Dict[str, Any]:
82
+ variants = font_info["variants"]
83
+ available_axes = list(font_info["axes_defaults"].keys())
84
+
85
+ # Analyze common parameters across instances
86
+ global_axes = {}
87
+ for axis_tag in available_axes:
88
+ axis_values = [
89
+ variant_axes.get(axis_tag)
90
+ for variant_axes in variants.values()
91
+ if axis_tag in variant_axes
92
+ ]
93
+
94
+ # Set global value if all instances have the same value
95
+ if axis_values and all(val == axis_values[0] for val in axis_values):
96
+ global_axes[axis_tag] = axis_values[0]
97
+ else:
98
+ global_axes[axis_tag] = None
99
+
100
+ # Build instance-specific configurations
101
+ optimized_variants = {}
102
+ for variant_name, variant_axes in variants.items():
103
+ optimized_variant = {
104
+ axis_tag: value
105
+ for axis_tag, value in variant_axes.items()
106
+ if global_axes.get(axis_tag) != value
107
+ }
108
+ optimized_variants[variant_name] = optimized_variant
109
+
110
+ return {
111
+ "$schema": SCHEMA,
112
+ "file": font_info["font_path"],
113
+ "font_name": font_info["font_name"],
114
+ "axes": global_axes,
115
+ "variants": optimized_variants,
116
+ }
117
+
118
+
119
+ @click.command("config")
120
+ @click.argument("font_file", type=str)
121
+ def config_command(font_file: str) -> None:
122
+ """Generate configuration file for variable font.
123
+
124
+ FONT_FILE: Path to the variable font file (.ttf or .otf)
125
+ """
126
+ try:
127
+ font_info = extract_font_info(font_file)
128
+ config = create_config_structure(font_info)
129
+
130
+ config_file = Path(font_file).stem + "-config.json"
131
+ with open(config_file, "w", encoding="utf-8") as f:
132
+ json.dump(config, f, indent=4, ensure_ascii=False)
133
+
134
+ print(f"[INFO]: Configuration saved to: {config_file}")
135
+
136
+ except KeyboardInterrupt:
137
+ print("\n[INFO]: Config generation interrupted by user")
138
+ raise click.Abort()
139
+ except Exception as e:
140
+ print(f"[ERROR]: {e}")
141
+ raise click.Abort()
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Optional, Union
7
+
8
+ import click
9
+ import jsonschema
10
+ from fontTools.ttLib import TTFont
11
+ from fontTools.varLib import instancer
12
+
13
+ SCHEMA_URL = "https://raw.githubusercontent.com/decipher3114/Var2Stat/refs/heads/main/schema.json"
14
+
15
+
16
+ def load_config(config_path: str) -> Dict[str, Any]:
17
+ if not os.path.exists(config_path):
18
+ raise FileNotFoundError(f"Configuration file '{config_path}' not found")
19
+
20
+ try:
21
+ with open(config_path, "r", encoding="utf-8") as f:
22
+ config_data = json.load(f)
23
+ print(f"[INFO]: Config file loaded: {Path(config_path).name}")
24
+ except json.JSONDecodeError as e:
25
+ raise ValueError(f"Invalid JSON syntax at line {e.lineno}, column {e.colno}")
26
+
27
+ # Validate against schema if available
28
+ schema_ref = config_data.get("$schema")
29
+ if schema_ref == SCHEMA_URL and os.path.exists("schema.json"):
30
+ try:
31
+ with open("schema.json", "r", encoding="utf-8") as f:
32
+ schema = json.load(f)
33
+ jsonschema.validate(config_data, schema)
34
+ print("[INFO]: Config validated")
35
+ except jsonschema.ValidationError as e:
36
+ error_msg = e.message
37
+ if e.absolute_path:
38
+ path_str = " -> ".join(str(p) for p in e.absolute_path)
39
+ error_msg += f" at {path_str}"
40
+ raise ValueError(error_msg)
41
+
42
+ return config_data
43
+
44
+
45
+ def remove_variation_tables(font: TTFont) -> None:
46
+ # Remove variable font tables to create static font
47
+ variation_tables = ["fvar", "avar", "gvar", "cvar", "HVAR", "VVAR", "MVAR", "STAT"]
48
+
49
+ for table in variation_tables:
50
+ if table in font:
51
+ del font[table]
52
+
53
+
54
+ def update_font_names(
55
+ font: TTFont, font_name: str, variant_name: str, axes: Dict[str, Union[int, float]]
56
+ ) -> None:
57
+ # Update font metadata and naming information
58
+ name_table = font["name"]
59
+ weight_value = axes.get("wght", 400)
60
+
61
+ for record in name_table.names:
62
+ if record.platformID == 3 and record.platEncID == 1:
63
+ if record.nameID == 1: # Family name
64
+ record.string = font_name.encode("utf-16-be")
65
+ elif record.nameID == 2: # Subfamily name
66
+ record.string = variant_name.encode("utf-16-be")
67
+ elif record.nameID == 4: # Full name
68
+ full_name = (
69
+ font_name
70
+ if variant_name == "Regular"
71
+ else f"{font_name} {variant_name}"
72
+ )
73
+ record.string = full_name.encode("utf-16-be")
74
+ elif record.nameID == 6: # PostScript name
75
+ ps_name = f"{font_name.replace(' ', '')}-{variant_name}"
76
+ record.string = ps_name.encode("utf-16-be")
77
+
78
+ # Update OS/2 weight class
79
+ if "OS/2" in font:
80
+ font["OS/2"].usWeightClass = int(weight_value)
81
+
82
+ # Update head table macStyle for bold
83
+ if "head" in font:
84
+ if weight_value >= 700:
85
+ font["head"].macStyle |= 0x01
86
+ else:
87
+ font["head"].macStyle &= ~0x01
88
+
89
+
90
+ def extract_font_info(font: TTFont) -> tuple[Dict[str, float], str]:
91
+ # Extract axis defaults and original font name from variable font
92
+ if "fvar" not in font:
93
+ raise ValueError("Not a variable font (missing fvar table)")
94
+
95
+ if "name" not in font:
96
+ raise ValueError("Font missing name table")
97
+
98
+ # Extract axis defaults
99
+ font_defaults = {axis.axisTag: axis.defaultValue for axis in font["fvar"].axes}
100
+
101
+ # Extract original font family name
102
+ for record in font["name"].names:
103
+ if record.nameID == 1 and record.platformID == 3 and record.platEncID == 1:
104
+ return font_defaults, record.toUnicode()
105
+
106
+ raise ValueError("Could not extract font family name from font file")
107
+
108
+
109
+ def generate_variant(
110
+ font: TTFont,
111
+ font_name: str,
112
+ variant_name: str,
113
+ variant_config: Dict[str, Union[int, float]],
114
+ global_axes: Dict[str, Optional[Union[int, float]]],
115
+ font_defaults: Dict[str, float],
116
+ output_folder: str,
117
+ ) -> bool:
118
+ # Generate a single static font variant
119
+ try:
120
+ # Merge global and variant axes
121
+ merged_axes = global_axes.copy()
122
+ merged_axes.update(variant_config)
123
+
124
+ # Resolve axis values
125
+ resolved_axes = {
126
+ axis: font_defaults[axis] if value is None else value
127
+ for axis, value in merged_axes.items()
128
+ if axis in font_defaults
129
+ }
130
+
131
+ # Display variant info
132
+ axis_parts = [f"'{axis}'={value}" for axis, value in resolved_axes.items()]
133
+ print(f'- Variant: "{variant_name}", Axes: [{", ".join(axis_parts)}]')
134
+
135
+ # Generate static font
136
+ static_font = instancer.instantiateVariableFont(font, resolved_axes)
137
+ remove_variation_tables(static_font)
138
+ update_font_names(static_font, font_name, variant_name, resolved_axes)
139
+
140
+ # Save font
141
+ canonical_font_name = "".join(
142
+ ch if ch not in '<>:"/\\|?*' else "_" for ch in font_name
143
+ ).strip()
144
+ if not canonical_font_name:
145
+ canonical_font_name = "Font"
146
+ output_filename = f"{canonical_font_name}_{variant_name}.ttf"
147
+ static_font.save(os.path.join(output_folder, output_filename))
148
+
149
+ return True
150
+
151
+ except Exception as e:
152
+ print(f"[ERROR]: Failed to generate {variant_name}: {e}")
153
+ return False
154
+
155
+
156
+ def resolve_output_directory(font_name: str) -> str:
157
+ sanitized_font_name = "".join(
158
+ ch if ch not in '<>:"/\\|?*' else "_" for ch in font_name
159
+ ).strip()
160
+ if not sanitized_font_name:
161
+ sanitized_font_name = "Font"
162
+ os.makedirs(sanitized_font_name, exist_ok=True)
163
+ return sanitized_font_name
164
+
165
+
166
+ def generate_from_config(config_path: str) -> None:
167
+ # Process configuration and generate static font files
168
+ config = load_config(config_path)
169
+
170
+ font_file = config["file"]
171
+ font_name = config["font_name"]
172
+
173
+ print("[INFO]: Font Info:")
174
+
175
+ if not os.path.exists(font_file):
176
+ raise FileNotFoundError(f"Font file '{font_file}' not found")
177
+
178
+ # Load and validate font
179
+ font = TTFont(font_file)
180
+ try:
181
+ font_defaults, original_font_name = extract_font_info(font)
182
+
183
+ # Use config font name or fallback to original
184
+ if not font_name:
185
+ font_name = original_font_name
186
+
187
+ # Display font information
188
+ if original_font_name == font_name:
189
+ print(f'- Font Name: "{font_name}"')
190
+ else:
191
+ print(f'- Original Font Name: "{original_font_name}"')
192
+ print(f'- Target Font Name: "{font_name}"')
193
+ print(f"- Available Axes: {list(font_defaults.keys())}")
194
+
195
+ output_dir = resolve_output_directory(font_name)
196
+
197
+ print("[INFO]: Variants:")
198
+
199
+ # Generate all variants
200
+ success_count = 0
201
+ for variant_name, variant_config in config["variants"].items():
202
+ if generate_variant(
203
+ font,
204
+ font_name,
205
+ variant_name,
206
+ variant_config,
207
+ config["axes"],
208
+ font_defaults,
209
+ output_dir,
210
+ ):
211
+ success_count += 1
212
+
213
+ print(f"[INFO]: Generated {success_count}/{len(config['variants'])} variants")
214
+ print(f"[INFO]: Static font files saved to '{output_dir}' directory")
215
+
216
+ finally:
217
+ font.close()
218
+
219
+
220
+ @click.command("generate")
221
+ @click.argument("config", type=str)
222
+ def generate_command(config: str) -> None:
223
+ """Generate static fonts from variable fonts.
224
+
225
+ CONFIG: Configuration file path (required)
226
+ """
227
+ try:
228
+ generate_from_config(config)
229
+ except KeyboardInterrupt:
230
+ print("\n[INFO]: Generation interrupted by user")
231
+ raise click.Abort()
232
+ except (FileNotFoundError, ValueError) as e:
233
+ print(f"[ERROR]: {e}")
234
+ print("[INFO]: Generate config using: uvx var2stat config <font_file.ttf>")
235
+ raise click.Abort()
236
+ except Exception as e:
237
+ print(f"[ERROR]: Generation failed: {e}")
238
+ raise click.Abort()
var2stat-0.1.0/uv.lock ADDED
@@ -0,0 +1,152 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.14"
4
+
5
+ [[package]]
6
+ name = "attrs"
7
+ version = "26.1.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "click"
16
+ version = "8.3.1"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ dependencies = [
19
+ { name = "colorama", marker = "sys_platform == 'win32'" },
20
+ ]
21
+ sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
22
+ wheels = [
23
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
24
+ ]
25
+
26
+ [[package]]
27
+ name = "colorama"
28
+ version = "0.4.6"
29
+ source = { registry = "https://pypi.org/simple" }
30
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
31
+ wheels = [
32
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
33
+ ]
34
+
35
+ [[package]]
36
+ name = "fonttools"
37
+ version = "4.62.1"
38
+ source = { registry = "https://pypi.org/simple" }
39
+ sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" }
40
+ wheels = [
41
+ { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" },
42
+ { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" },
43
+ { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" },
44
+ { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" },
45
+ { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" },
46
+ { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" },
47
+ { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" },
48
+ { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" },
49
+ { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" },
50
+ { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" },
51
+ { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" },
52
+ { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" },
53
+ { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" },
54
+ { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" },
55
+ { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" },
56
+ { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" },
57
+ { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" },
58
+ ]
59
+
60
+ [[package]]
61
+ name = "jsonschema"
62
+ version = "4.26.0"
63
+ source = { registry = "https://pypi.org/simple" }
64
+ dependencies = [
65
+ { name = "attrs" },
66
+ { name = "jsonschema-specifications" },
67
+ { name = "referencing" },
68
+ { name = "rpds-py" },
69
+ ]
70
+ sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
71
+ wheels = [
72
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
73
+ ]
74
+
75
+ [[package]]
76
+ name = "jsonschema-specifications"
77
+ version = "2025.9.1"
78
+ source = { registry = "https://pypi.org/simple" }
79
+ dependencies = [
80
+ { name = "referencing" },
81
+ ]
82
+ sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
83
+ wheels = [
84
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
85
+ ]
86
+
87
+ [[package]]
88
+ name = "referencing"
89
+ version = "0.37.0"
90
+ source = { registry = "https://pypi.org/simple" }
91
+ dependencies = [
92
+ { name = "attrs" },
93
+ { name = "rpds-py" },
94
+ ]
95
+ sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
96
+ wheels = [
97
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
98
+ ]
99
+
100
+ [[package]]
101
+ name = "rpds-py"
102
+ version = "0.30.0"
103
+ source = { registry = "https://pypi.org/simple" }
104
+ sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
105
+ wheels = [
106
+ { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
107
+ { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
108
+ { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
109
+ { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
110
+ { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
111
+ { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
112
+ { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
113
+ { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
114
+ { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
115
+ { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
116
+ { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
117
+ { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
118
+ { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
119
+ { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
120
+ { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
121
+ { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
122
+ { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
123
+ { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
124
+ { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
125
+ { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
126
+ { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
127
+ { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
128
+ { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
129
+ { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
130
+ { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
131
+ { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
132
+ { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
133
+ { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
134
+ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
135
+ ]
136
+
137
+ [[package]]
138
+ name = "var2stat"
139
+ version = "0.1.0"
140
+ source = { editable = "." }
141
+ dependencies = [
142
+ { name = "click" },
143
+ { name = "fonttools" },
144
+ { name = "jsonschema" },
145
+ ]
146
+
147
+ [package.metadata]
148
+ requires-dist = [
149
+ { name = "click", specifier = ">=8.3.1" },
150
+ { name = "fonttools", specifier = ">=4.62.1" },
151
+ { name = "jsonschema", specifier = ">=4.26.0" },
152
+ ]