fontforge-fontchecker 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MihailJP
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,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: fontforge_fontchecker
3
+ Version: 0.1.0
4
+ Summary: FontForge_plugin of font checker frontend
5
+ Home-page: https://github.com/MihailJP/fontforge-fontchecker
6
+ Author: MihailJP
7
+ Author-email: mihailjp@gmail.com
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Plugins
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Text Processing :: Fonts
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ Fontforge font checker plugin
21
+ =============================
22
+
23
+ Font checker wrapper for Fontforge
24
+
25
+ Install
26
+ -------
27
+
28
+ ```shell
29
+ pip3 install fontforge_fontchecker
30
+ ```
31
+
32
+ ### Make sure Fontforge Python module is usable
33
+
34
+ In interactive mode of Python, run:
35
+
36
+ ```python
37
+ import fontforge
38
+ ```
39
+
40
+ If it raises ``ModuleNotFoundError`` exception, install Fontforge first. If
41
+ installed, make sure the build option set that the Python module gets also
42
+ installed. If already so, Python interpreter does not recognize the module
43
+ path where the required module.
44
+
45
+ ```shell
46
+ export PYTHONPATH=/path/to/fontforge/python/module:$PYTHONPATH
47
+ ```
48
+
49
+ Usage
50
+ -----
51
+
52
+ Explanation here
@@ -0,0 +1,33 @@
1
+ Fontforge font checker plugin
2
+ =============================
3
+
4
+ Font checker wrapper for Fontforge
5
+
6
+ Install
7
+ -------
8
+
9
+ ```shell
10
+ pip3 install fontforge_fontchecker
11
+ ```
12
+
13
+ ### Make sure Fontforge Python module is usable
14
+
15
+ In interactive mode of Python, run:
16
+
17
+ ```python
18
+ import fontforge
19
+ ```
20
+
21
+ If it raises ``ModuleNotFoundError`` exception, install Fontforge first. If
22
+ installed, make sure the build option set that the Python module gets also
23
+ installed. If already so, Python interpreter does not recognize the module
24
+ path where the required module.
25
+
26
+ ```shell
27
+ export PYTHONPATH=/path/to/fontforge/python/module:$PYTHONPATH
28
+ ```
29
+
30
+ Usage
31
+ -----
32
+
33
+ Explanation here
@@ -0,0 +1,18 @@
1
+ import fontforge
2
+ from . import config, run_check
3
+
4
+
5
+ def fontforge_plugin_config(**_):
6
+ config.configInterface()
7
+
8
+
9
+ def fontforge_plugin_init(preferences_path=None, **_):
10
+ config.checkFontTools()
11
+ config.loadConf(preferences_path)
12
+
13
+ fontforge.registerMenuItem(
14
+ callback=run_check.run_check,
15
+ enable=run_check.enabled,
16
+ context="Font",
17
+ name="Check font"
18
+ )
@@ -0,0 +1,165 @@
1
+ import fontforge
2
+ import shutil
3
+ import os
4
+ from tomlkit.toml_file import TOMLFile, TOMLDocument
5
+ from typing import Iterable, Optional
6
+
7
+ fontbakery_path = None
8
+ fontspector_path = None
9
+ fontbakery_config = TOMLDocument()
10
+ fontspector_config = TOMLDocument()
11
+
12
+ plugin_config = TOMLDocument()
13
+ _plugin_dir = ''
14
+ CONFIGFILE = 'config.toml'
15
+ FONTBAKERY_CONFIGFILE = 'fontbakery.toml'
16
+ FONTSPECTOR_CONFIGFILE = 'fontspector.toml'
17
+
18
+ profiles = {
19
+ 'opentype': 'OpenType (standards compliance)',
20
+ 'universal': 'Universal (community best practice)',
21
+ 'googlefonts': 'Google Fonts',
22
+ 'iso15008': 'ISO 15008 (in-car accessibility)',
23
+ 'fontwerk': 'Fontwerk',
24
+ 'adobefonts': 'Adobe Fonts',
25
+ 'fontbureau': 'Font Bureau',
26
+ 'microsoft': 'Microsoft',
27
+ 'notofonts': 'Noto fonts',
28
+ 'typenetwork': 'Type Network',
29
+ }
30
+ """List of known Fontbakery/Fontspector check profile"""
31
+
32
+
33
+ def checkFontTools():
34
+ global fontbakery_path, fontspector_path
35
+ fontbakery_path = shutil.which('fontbakery')
36
+ fontspector_path = shutil.which('fontspector')
37
+
38
+
39
+ def _validateConfItem(key: str, defaultVal, *, choice: Optional[Iterable] = None) -> bool:
40
+ loaded = True
41
+ if (key not in plugin_config) or (not isinstance(plugin_config[key], type(defaultVal))):
42
+ plugin_config[key] = defaultVal # load default
43
+ loaded = False
44
+ if choice:
45
+ if not any(plugin_config[key] == x for x in choice):
46
+ fontforge.logWarning("Invalid " + key + " '" + str(plugin_config[key]) + "' ignored")
47
+ plugin_config[key] = defaultVal
48
+ return loaded
49
+
50
+
51
+ def _validateConf():
52
+ global profiles
53
+ _validateConfItem('backend', 'auto', choice=['auto', 'fontbakery', 'fontspector'])
54
+ _validateConfItem('check_as', 'ttf', choice=['ttf', 'ufo'])
55
+ if _validateConfItem('profiles', profiles):
56
+ profiles |= plugin_config['profiles']
57
+ if _validateConfItem('profile', 'universal'):
58
+ if plugin_config['profile'] not in profiles:
59
+ profiles[plugin_config['profile']] = plugin_config['profile']
60
+ _validateConfItem('explicit_checks', [])
61
+ _validateConfItem('exclude_checks', [])
62
+
63
+
64
+ def fontBakeryConfigFile() -> str:
65
+ return _plugin_dir + '/' + FONTBAKERY_CONFIGFILE
66
+
67
+
68
+ def fontSpectorConfigFile() -> str:
69
+ return _plugin_dir + '/' + FONTSPECTOR_CONFIGFILE
70
+
71
+
72
+ def loadConf(confdir: str):
73
+ global _plugin_dir, plugin_config, fontbakery_config, fontspector_config
74
+ _plugin_dir = confdir
75
+ try:
76
+ plugin_config = TOMLFile(_plugin_dir + '/' + CONFIGFILE).read()
77
+ except FileNotFoundError:
78
+ pass
79
+
80
+ _validateConf()
81
+
82
+ try:
83
+ fontbakery_config = TOMLFile(fontBakeryConfigFile()).read()
84
+ except FileNotFoundError:
85
+ pass
86
+ try:
87
+ fontspector_config = TOMLFile(fontSpectorConfigFile()).read()
88
+ except FileNotFoundError:
89
+ pass
90
+
91
+
92
+ def saveConf():
93
+ os.makedirs(_plugin_dir, exist_ok=True)
94
+ TOMLFile(_plugin_dir + '/' + CONFIGFILE).write(plugin_config)
95
+ TOMLFile(_plugin_dir + '/' + FONTBAKERY_CONFIGFILE).write(fontbakery_config)
96
+ TOMLFile(_plugin_dir + '/' + FONTSPECTOR_CONFIGFILE).write(fontspector_config)
97
+
98
+
99
+ def _writeBackendConf():
100
+ fontspector_config['explicit_checks'] \
101
+ = fontbakery_config['explicit_checks'] \
102
+ = plugin_config['explicit_checks']
103
+ fontspector_config['exclude_checks'] \
104
+ = fontbakery_config['exclude_checks'] \
105
+ = plugin_config['exclude_checks']
106
+ for conf in (fontspector_config, fontbakery_config):
107
+ for i in [x[0] for x in conf.items() if not x[1]]:
108
+ conf.remove(i)
109
+
110
+
111
+ def configInterface():
112
+ ans = fontforge.askMulti(
113
+ 'Configuration',
114
+ [
115
+ {
116
+ 'type': 'choice',
117
+ 'question': 'Backend',
118
+ 'tag': 'backend',
119
+ 'checks': True,
120
+ 'answers': [
121
+ {'name': p.capitalize(), 'tag': p, 'default': plugin_config['backend'] == p}
122
+ for p in ['auto', 'fontbakery', 'fontspector']
123
+ ],
124
+ },
125
+ {
126
+ 'type': 'choice',
127
+ 'question': 'Check as',
128
+ 'tag': 'check_as',
129
+ 'checks': True,
130
+ 'answers': [
131
+ {'name': p, 'tag': p.lower(), 'default': plugin_config['check_as'] == p.lower()}
132
+ for p in ['TTF', 'UFO']
133
+ ],
134
+ },
135
+ {
136
+ 'type': 'choice',
137
+ 'question': 'Profile',
138
+ 'tag': 'profile',
139
+ 'answers': [
140
+ {'name': p[1], 'tag': p[0], 'default': plugin_config['profile'] == p[0]}
141
+ for p in profiles.items()
142
+ ],
143
+ },
144
+ {
145
+ 'type': 'string',
146
+ 'question': 'Explicit checks\n(comma-separated)',
147
+ 'tag': 'explicit_checks',
148
+ 'default': ','.join(plugin_config['explicit_checks']),
149
+ },
150
+ {
151
+ 'type': 'string',
152
+ 'question': 'Excluded checks\n(comma-separated)',
153
+ 'tag': 'exclude_checks',
154
+ 'default': ','.join(plugin_config['exclude_checks']),
155
+ },
156
+ ]
157
+ )
158
+ if ans:
159
+ plugin_config['backend'] = ans['backend']
160
+ plugin_config['check_as'] = ans['check_as']
161
+ plugin_config['profile'] = ans['profile']
162
+ plugin_config['explicit_checks'] = [a for a in (ans['explicit_checks'] or '').split(',') if a]
163
+ plugin_config['exclude_checks'] = [a for a in (ans['exclude_checks'] or '').split(',') if a]
164
+ _writeBackendConf()
165
+ saveConf()
@@ -0,0 +1,158 @@
1
+ from . import config
2
+ import fontforge
3
+ import tempfile
4
+ from typing import Optional
5
+ from subprocess import run
6
+ import json
7
+ import webbrowser
8
+ from pathlib import Path
9
+
10
+ RESULT_JSON = 'lastresult.json'
11
+ RESULT_HTML = 'lastresult.html'
12
+
13
+
14
+ def _jsonFile() -> str:
15
+ return config._plugin_dir + '/' + RESULT_JSON
16
+
17
+
18
+ def _htmlFile() -> str:
19
+ return config._plugin_dir + '/' + RESULT_HTML
20
+
21
+
22
+ def _executable() -> Optional[str]:
23
+ if config.plugin_config['backend'] == 'auto':
24
+ return config.fontspector_path or config.fontbakery_path
25
+ elif config.plugin_config['backend'] == 'fontbakery':
26
+ return config.fontbakery_path
27
+ elif config.plugin_config['backend'] == 'fontspector':
28
+ return config.fontspector_path
29
+ else: # invalid!
30
+ return None
31
+
32
+
33
+ def enabled(u, font) -> bool:
34
+ return bool(_executable())
35
+
36
+
37
+ def _cmdline(filename: str) -> list[str]:
38
+ if _executable():
39
+ isFontSpector = (_executable() == config.fontspector_path)
40
+ cmdline = [_executable()]
41
+ if isFontSpector:
42
+ cmdline.append('-p')
43
+ cmdline.append(config.plugin_config['profile'])
44
+ else:
45
+ cmdline.append('check-' + config.plugin_config['profile'])
46
+ cmdline.append('-q')
47
+ cmdline.append('--full-lists')
48
+ cmdline.append('-l')
49
+ cmdline.append('info' if isFontSpector else 'INFO')
50
+ cmdline.append('--configuration')
51
+ cmdline.append(config.fontSpectorConfigFile() if isFontSpector else config.fontBakeryConfigFile())
52
+ cmdline.append('--json')
53
+ cmdline.append(_jsonFile())
54
+ cmdline.append('--html')
55
+ cmdline.append(_htmlFile())
56
+ cmdline.append(filename)
57
+ return cmdline
58
+ else:
59
+ raise RuntimeError('neither Fontbakery nor Fontspector available')
60
+
61
+
62
+ def _outroTitle(summary: dict) -> str:
63
+ if 'ERROR' in summary and summary['ERROR'] > 0:
64
+ return 'Error'
65
+ elif 'FATAL' in summary and summary['FATAL'] > 0:
66
+ return 'Check failed'
67
+ elif 'FAIL' in summary and summary['FAIL'] > 0:
68
+ return 'Check failed'
69
+ else:
70
+ return 'Check passed'
71
+
72
+
73
+ def _outroMessage(summary: dict) -> str:
74
+ if 'ERROR' in summary and summary['ERROR'] > 0:
75
+ return 'There ' + ('are errors' if summary['ERROR'] > 1 else 'is an error') + 'during check.'
76
+ elif 'FATAL' in summary and summary['FATAL'] > 0:
77
+ return 'Check failed with ' + ('severe issues' if summary['FATAL'] > 1 else 'a severe issue') + '.'
78
+ elif 'FAIL' in summary and summary['FAIL'] > 0:
79
+ return 'Check failed with ' + ('some issues' if summary['FAIL'] > 1 else 'an issue') + '.'
80
+ elif 'WARN' in summary and summary['WARN'] > 0:
81
+ return 'Check passed, but with ' + ('warnings' if summary['WARN'] > 1 else 'a warning') + '.'
82
+ else:
83
+ return 'Check passed.'
84
+
85
+
86
+ def _outroResultText(summary: dict) -> str:
87
+ def toString(key: str, label: str) -> str:
88
+ if key in summary and summary[key] > 0:
89
+ return label + ' ' + str(summary[key])
90
+ else:
91
+ return label + ' 0'
92
+
93
+ return ', '.join(
94
+ toString(x[0], x[1]) for x in [
95
+ ('ERROR', '💥'),
96
+ ('FATAL', '☠'),
97
+ ('FAIL', '🔥'),
98
+ ('WARN', '⚠️'),
99
+ ('INFO', 'ℹ️'),
100
+ ('SKIP', '⏩'),
101
+ ('PASS', '✅'),
102
+ ]
103
+ )
104
+
105
+
106
+ def _outro(filename: str):
107
+ isFontSpector = (_executable() == config.fontspector_path)
108
+ with open(_jsonFile(), 'r') as file:
109
+ jsonDoc = json.load(file)
110
+ summary = jsonDoc['summary'] if isFontSpector else jsonDoc['result']
111
+ fontforge.logWarning(filename + ': ' + _outroResultText(summary))
112
+ ans = fontforge.ask(
113
+ _outroTitle(summary),
114
+ _outroMessage(summary) + '\n'
115
+ 'Would you like to open details with the browser?',
116
+ ['_Yes', '_No'])
117
+ if ans == 0:
118
+ webbrowser.open('file://' + _htmlFile(), 1)
119
+
120
+
121
+ def _run_check_direct(font: fontforge.font):
122
+ run(_cmdline(font.path))
123
+ _outro(Path(font.path).name)
124
+
125
+
126
+ def _run_check_tmpfile(font: fontforge.font):
127
+ with tempfile.TemporaryDirectory() as tmpdir:
128
+ changed = font.changed
129
+ basename = (
130
+ (font.default_base_filename or font.cidfontname or font.fontname) +
131
+ '.' + config.plugin_config['check_as']
132
+ )
133
+ testfile = tmpdir + '/' + basename
134
+ font.generate(testfile)
135
+ font.changed = changed
136
+ run(_cmdline(testfile))
137
+ _outro(basename)
138
+
139
+
140
+ def run_check(u, font: fontforge.font):
141
+ config.saveConf()
142
+ if any(font.path.endswith(x) for x in ['.ttf', '.otf', '.ufo', '.ufo2', '.ufo3']):
143
+ if font.changed:
144
+ ans = fontforge.ask(
145
+ 'Font has been changed',
146
+ 'The font\n' + font.path + '\n'
147
+ 'has unsaved changes.\n'
148
+ 'How would you like to check?',
149
+ ['Expor_t a temporary file', 'Check _existing font'],
150
+ )
151
+ if ans == 0:
152
+ _run_check_tmpfile(font)
153
+ else:
154
+ _run_check_direct(font)
155
+ else:
156
+ _run_check_direct(font)
157
+ else:
158
+ _run_check_tmpfile(font)
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: fontforge_fontchecker
3
+ Version: 0.1.0
4
+ Summary: FontForge_plugin of font checker frontend
5
+ Home-page: https://github.com/MihailJP/fontforge-fontchecker
6
+ Author: MihailJP
7
+ Author-email: mihailjp@gmail.com
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Plugins
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Text Processing :: Fonts
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ Fontforge font checker plugin
21
+ =============================
22
+
23
+ Font checker wrapper for Fontforge
24
+
25
+ Install
26
+ -------
27
+
28
+ ```shell
29
+ pip3 install fontforge_fontchecker
30
+ ```
31
+
32
+ ### Make sure Fontforge Python module is usable
33
+
34
+ In interactive mode of Python, run:
35
+
36
+ ```python
37
+ import fontforge
38
+ ```
39
+
40
+ If it raises ``ModuleNotFoundError`` exception, install Fontforge first. If
41
+ installed, make sure the build option set that the Python module gets also
42
+ installed. If already so, Python interpreter does not recognize the module
43
+ path where the required module.
44
+
45
+ ```shell
46
+ export PYTHONPATH=/path/to/fontforge/python/module:$PYTHONPATH
47
+ ```
48
+
49
+ Usage
50
+ -----
51
+
52
+ Explanation here
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.cfg
5
+ fontforge_fontchecker/__init__.py
6
+ fontforge_fontchecker/__main__.py
7
+ fontforge_fontchecker/config.py
8
+ fontforge_fontchecker/run_check.py
9
+ fontforge_fontchecker.egg-info/PKG-INFO
10
+ fontforge_fontchecker.egg-info/SOURCES.txt
11
+ fontforge_fontchecker.egg-info/dependency_links.txt
12
+ fontforge_fontchecker.egg-info/entry_points.txt
13
+ fontforge_fontchecker.egg-info/top_level.txt
14
+ test/test_dummy.py
@@ -0,0 +1,2 @@
1
+ [fontforge_plugin]
2
+ Font checker = fontforge_fontchecker.__main__
@@ -0,0 +1 @@
1
+ fontforge_fontchecker
@@ -0,0 +1,5 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools>=42"
4
+ ]
5
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,32 @@
1
+ [metadata]
2
+ name = fontforge_fontchecker
3
+ version = 0.1.0
4
+ author = MihailJP
5
+ author_email = mihailjp@gmail.com
6
+ description = FontForge_plugin of font checker frontend
7
+ license = MIT
8
+ license_files =
9
+ LICENSE
10
+ long_description = file: README.md
11
+ long_description_content_type = text/markdown
12
+ url = https://github.com/MihailJP/fontforge-fontchecker
13
+ classifiers =
14
+ Development Status :: 3 - Alpha
15
+ Environment :: Plugins
16
+ Intended Audience :: Developers
17
+ Programming Language :: Python :: 3
18
+ Operating System :: OS Independent
19
+ Topic :: Text Processing :: Fonts
20
+
21
+ [options]
22
+ packages = fontforge_fontchecker
23
+ python_requires = >=3.8
24
+
25
+ [options.entry_points]
26
+ fontforge_plugin =
27
+ Font checker = fontforge_fontchecker.__main__
28
+
29
+ [egg_info]
30
+ tag_build =
31
+ tag_date = 0
32
+
@@ -0,0 +1,2 @@
1
+ def test_dummy():
2
+ pass