foundrytools-cli 2.0.4__tar.gz → 2.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.
Files changed (38) hide show
  1. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/PKG-INFO +9 -9
  2. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/README.md +2 -2
  3. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/pyproject.toml +18 -30
  4. foundrytools_cli-2.1.0/src/foundrytools_cli/__init__.py +3 -0
  5. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/cff.py +2 -1
  6. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/converter/cli.py +8 -8
  7. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/converter/ttf_to_otf.py +5 -6
  8. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/fix/cli.py +33 -0
  9. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/font.py +3 -3
  10. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/hhea.py +3 -1
  11. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/name.py +10 -10
  12. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/os_2.py +4 -4
  13. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/otf.py +14 -6
  14. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/post.py +5 -5
  15. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/print/cli.py +1 -2
  16. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/print/font_names.py +4 -5
  17. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/utils/cli.py +4 -2
  18. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/utils/font_renamer.py +62 -0
  19. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/utils/__init__.py +10 -11
  20. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/utils/task_runner.py +5 -4
  21. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/utils/timer.py +5 -4
  22. foundrytools_cli-2.0.4/src/foundrytools_cli/__init__.py +0 -3
  23. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/__main__.py +0 -0
  24. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/__init__.py +0 -0
  25. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/cmap.py +0 -0
  26. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/converter/__init__.py +0 -0
  27. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/fix/__init__.py +0 -0
  28. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/fix/legacy_accents.py +0 -0
  29. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/fix/vertical_metrics.py +0 -0
  30. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/gsub.py +0 -0
  31. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/print/__init__.py +0 -0
  32. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/print/font_info.py +0 -0
  33. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/print/vf_instances.py +0 -0
  34. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/ttf.py +0 -0
  35. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/utils/__init__.py +0 -0
  36. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/utils/font_organizer.py +0 -0
  37. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/commands/utils/sync_timestamps.py +0 -0
  38. {foundrytools_cli-2.0.4 → foundrytools_cli-2.1.0}/src/foundrytools_cli/utils/logger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: foundrytools-cli
3
- Version: 2.0.4
3
+ Version: 2.1.0
4
4
  Summary: A set of command line tools to inspect, manipulate and convert font files
5
5
  Author: Cesare Gilento
6
6
  Author-email: Cesare Gilento <ftcli@proton.me>
@@ -11,22 +11,22 @@ Classifier: Intended Audience :: End Users/Desktop
11
11
  Classifier: Natural Language :: English
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.9
15
14
  Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
19
  Classifier: License :: OSI Approved :: MIT License
20
20
  Classifier: Topic :: Text Processing :: Fonts
21
- Requires-Dist: afdko>=4.0.2
22
- Requires-Dist: click>=8.1.8
23
- Requires-Dist: foundrytools>=0.1.4
21
+ Requires-Dist: afdko>=4.0.3
22
+ Requires-Dist: click>=8.3.1
23
+ Requires-Dist: foundrytools>=0.1.6
24
24
  Requires-Dist: loguru>=0.7.3
25
25
  Requires-Dist: pathvalidate>=3.3.1
26
- Requires-Dist: rich>=14.1.0
26
+ Requires-Dist: rich>=14.3.0
27
27
  Requires-Dist: ufolib2>=0.18.1
28
28
  Requires-Dist: win32-setctime>=1.2.0 ; sys_platform >= 'win32'
29
- Requires-Python: >=3.9
29
+ Requires-Python: >=3.10
30
30
  Project-URL: Homepage, https://github.com/ftCLI/FoundryTools-CLI
31
31
  Description-Content-Type: text/markdown
32
32
 
@@ -54,7 +54,7 @@ rendered by [rich](https://github.com/Textualize/rich).
54
54
 
55
55
  ## Installation
56
56
 
57
- FoundryTools-CLI requires Python 3.9 or later.
57
+ FoundryTools-CLI requires Python 3.10 or later.
58
58
 
59
59
  **IMPORTANT**: If you have Python 2.x installed in your system, you may have to use `python3`
60
60
  (instead of `python`) in the commands below.
@@ -96,7 +96,7 @@ python -m pip install -e .
96
96
 
97
97
  FoundryTools-CLI is a Terminal app where commands are logically organized into subcommands.
98
98
 
99
- Please refer to the [user documentation](https://foundrytools-cli.readthedocs.io/).
99
+ Please refer to the [user documentation](https://foundrytools-cli.readthedocs.io/en/stable/).
100
100
 
101
101
  ## License
102
102
 
@@ -22,7 +22,7 @@ rendered by [rich](https://github.com/Textualize/rich).
22
22
 
23
23
  ## Installation
24
24
 
25
- FoundryTools-CLI requires Python 3.9 or later.
25
+ FoundryTools-CLI requires Python 3.10 or later.
26
26
 
27
27
  **IMPORTANT**: If you have Python 2.x installed in your system, you may have to use `python3`
28
28
  (instead of `python`) in the commands below.
@@ -64,7 +64,7 @@ python -m pip install -e .
64
64
 
65
65
  FoundryTools-CLI is a Terminal app where commands are logically organized into subcommands.
66
66
 
67
- Please refer to the [user documentation](https://foundrytools-cli.readthedocs.io/).
67
+ Please refer to the [user documentation](https://foundrytools-cli.readthedocs.io/en/stable/).
68
68
 
69
69
  ## License
70
70
 
@@ -1,14 +1,14 @@
1
1
  [build-system]
2
- requires = ["uv_build>=0.8.13"]
2
+ requires = ["uv_build>=0.9.17"]
3
3
  build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "foundrytools-cli"
7
- version = "2.0.4"
7
+ version = "2.1.0"
8
8
  description = "A set of command line tools to inspect, manipulate and convert font files"
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
- requires-python = ">=3.9"
11
+ requires-python = ">=3.10"
12
12
  authors = [
13
13
  { name = "Cesare Gilento", email = "ftcli@proton.me" },
14
14
  ]
@@ -20,36 +20,36 @@ classifiers = [
20
20
  "Natural Language :: English",
21
21
  "Operating System :: OS Independent",
22
22
  "Programming Language :: Python :: 3",
23
- "Programming Language :: Python :: 3.9",
24
23
  "Programming Language :: Python :: 3.10",
25
24
  "Programming Language :: Python :: 3.11",
26
25
  "Programming Language :: Python :: 3.12",
27
26
  "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
28
  "License :: OSI Approved :: MIT License",
29
29
  "Topic :: Text Processing :: Fonts",
30
30
  ]
31
31
 
32
32
  dependencies = [
33
- "afdko>=4.0.2",
34
- "click>=8.1.8",
35
- "foundrytools>=0.1.4",
33
+ "afdko>=4.0.3",
34
+ "click>=8.3.1",
35
+ "foundrytools>=0.1.6",
36
36
  "loguru>=0.7.3",
37
37
  "pathvalidate>=3.3.1",
38
- "rich>=14.1.0",
38
+ "rich>=14.3.0",
39
39
  "ufolib2>=0.18.1",
40
40
  "win32-setctime>=1.2.0; sys_platform >= 'win32'",
41
41
  ]
42
42
 
43
- [dependencies-groups]
43
+ [dependency-groups]
44
44
  dev = [
45
- "bump-my-version>=1.2.1",
46
- "mypy>=1.17.1",
47
- "pre-commit>=4.2.0",
48
- "pylint>=3.3.8",
45
+ "bump-my-version>=1.2.6",
46
+ "mypy>=1.19.1",
47
+ "pre-commit>=4.5.1",
48
+ "pylint>=4.0.4",
49
49
  ]
50
50
  docs = [
51
- "sphinx-click>=6.0.0",
52
- "sphinx-rtd-theme>=3.0.2",
51
+ "sphinx-click>=6.2.0",
52
+ "sphinx-rtd-theme>=3.1.0",
53
53
  ]
54
54
 
55
55
  [project.scripts]
@@ -58,22 +58,10 @@ ftcli = "foundrytools_cli.__main__:cli"
58
58
  [project.urls]
59
59
  Homepage = "https://github.com/ftCLI/FoundryTools-CLI"
60
60
 
61
- [tool.hatch.version]
62
- path = "src/foundrytools_cli/__init__.py"
63
- pattern = 'VERSION = __version__ = "(?P<version>[^"]+)"'
64
-
65
- [tool.hatch.build.targets.sdist]
66
- include = [
67
- "/src",
68
- ]
69
- exclude = [
70
- "/.gitignore",
71
- ]
72
-
73
61
  [tool.bumpversion]
74
- current_version = "2.0.4"
75
- commit = true
76
- tag = true
62
+ current_version = "2.1.0"
63
+ commit = false
64
+ tag = false
77
65
 
78
66
  [[tool.bumpversion.files]]
79
67
  filename = "src/foundrytools_cli/__init__.py"
@@ -0,0 +1,3 @@
1
+ VERSION = __version__ = "2.1.0"
2
+
3
+ __all__ = ["VERSION"]
@@ -1,5 +1,6 @@
1
+ from collections.abc import Callable
1
2
  from pathlib import Path
2
- from typing import Any, Callable
3
+ from typing import Any
3
4
 
4
5
  import click
5
6
  from foundrytools import Font
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import Any, Literal, Optional
2
+ from typing import Any, Literal
3
3
 
4
4
  import click
5
5
  from fontTools.misc.cliTools import makeOutputFileName
@@ -74,8 +74,8 @@ def otf_to_ttf(input_path: Path, **options: dict[str, Any]) -> None:
74
74
  def task(
75
75
  font: Font,
76
76
  tolerance: float = 1.0,
77
- target_upm: Optional[int] = None,
78
- output_dir: Optional[Path] = None,
77
+ target_upm: int | None = None,
78
+ output_dir: Path | None = None,
79
79
  overwrite: bool = True,
80
80
  ) -> None:
81
81
  flavor = font.ttfont.flavor
@@ -142,7 +142,7 @@ def otf_to_ttf(input_path: Path, **options: dict[str, Any]) -> None:
142
142
  """,
143
143
  )
144
144
  @click.option(
145
- "-cc/--no-cc",
145
+ "-cc/-no-cc",
146
146
  "--correct-contours/--no-correct-contours",
147
147
  is_flag=True,
148
148
  default=True,
@@ -201,7 +201,7 @@ def ttf_to_otf(input_path: Path, **options: dict[str, Any]) -> None:
201
201
  )
202
202
  def web_to_sfnt(
203
203
  input_path: Path,
204
- in_format: Optional[Literal["woff", "woff2"]],
204
+ in_format: Literal["woff", "woff2"] | None,
205
205
  **options: dict[str, Any],
206
206
  ) -> None:
207
207
  """
@@ -236,8 +236,8 @@ def sfnt_to_web(input_path: Path, **options: dict[str, Any]) -> None:
236
236
 
237
237
  def task(
238
238
  font: Font,
239
- output_dir: Optional[Path] = None,
240
- out_format: Optional[Literal["woff", "woff2"]] = None,
239
+ output_dir: Path | None = None,
240
+ out_format: Literal["woff", "woff2"] | None = None,
241
241
  overwrite: bool = True,
242
242
  reorder_tables: bool = False,
243
243
  ) -> None:
@@ -302,7 +302,7 @@ def variable_to_static(input_path: Path, **options: dict[str, Any]) -> None:
302
302
  var_font: Font,
303
303
  select_instance: bool = False,
304
304
  overlap: int = 1,
305
- output_dir: Optional[Path] = None,
305
+ output_dir: Path | None = None,
306
306
  overwrite: bool = True,
307
307
  ) -> None:
308
308
  if select_instance:
@@ -1,5 +1,4 @@
1
1
  from pathlib import Path
2
- from typing import Optional
3
2
 
4
3
  from afdko.fdkutils import run_shell_command
5
4
  from foundrytools import Font
@@ -10,7 +9,7 @@ from foundrytools.lib.qu2cu import quadratics_to_cubics_2
10
9
  from foundrytools_cli.utils.logger import logger
11
10
 
12
11
 
13
- def _build_out_file_name(font: Font, output_dir: Optional[Path], overwrite: bool = True) -> Path:
12
+ def _build_out_file_name(font: Font, output_dir: Path | None, overwrite: bool = True) -> Path:
14
13
  """
15
14
  When converting a TrueType flavored web font to PS flavored web font, we need to add a suffix to
16
15
  the output file name to avoid overwriting the input file. This function builds the output file
@@ -39,11 +38,11 @@ def _build_out_file_name(font: Font, output_dir: Optional[Path], overwrite: bool
39
38
  def ttf2otf(
40
39
  font: Font,
41
40
  tolerance: float = 1.0,
42
- target_upm: Optional[int] = None,
41
+ target_upm: int | None = None,
43
42
  correct_contours: bool = True,
44
43
  check_outlines: bool = False,
45
44
  subroutinize: bool = True,
46
- output_dir: Optional[Path] = None,
45
+ output_dir: Path | None = None,
47
46
  overwrite: bool = True,
48
47
  ) -> None:
49
48
  """
@@ -104,11 +103,11 @@ def ttf2otf(
104
103
 
105
104
  def ttf2otf_with_tx(
106
105
  font: Font,
107
- target_upm: Optional[int] = None,
106
+ target_upm: int | None = None,
108
107
  correct_contours: bool = True,
109
108
  check_outlines: bool = False,
110
109
  subroutinize: bool = True,
111
- output_dir: Optional[Path] = None,
110
+ output_dir: Path | None = None,
112
111
  recalc_timestamp: bool = False,
113
112
  overwrite: bool = True,
114
113
  ) -> None:
@@ -344,6 +344,39 @@ def fix_monospace(input_path: Path, **options: dict[str, Any]) -> None:
344
344
  runner.run()
345
345
 
346
346
 
347
+ @cli.command("integer-ppem-if-hinted", cls=BaseCommand)
348
+ def fix_integer_ppm_if_hinted(input_path: Path, **options: dict[str, Any]) -> None:
349
+ """
350
+ Sets the ``head.flags`` bit 3 to force integer ppem for hinted fonts.
351
+
352
+ fontbakery check id: com.google.fonts/check/integer_ppem_if_hinted
353
+
354
+ Rationale:
355
+
356
+ Hinted fonts must have head table flag bit 3 set.
357
+
358
+ Per https://docs.microsoft.com/en-us/typography/opentype/spec/head, bit 3 of Head::flags decides
359
+ whether PPEM should be rounded. This bit should always be set for hinted fonts.
360
+
361
+ Note:
362
+ Bit 3 = Force ppem to integer values for all internal scaler math;
363
+
364
+ Fixing procedure:
365
+
366
+ * Check if the font is hinted by looking for the presence of the ``fpgm`` table.
367
+ * Set the ``head.flags`` bit 3 to force integer ppem for hinted fonts.
368
+ """
369
+
370
+ def task(font: Font) -> bool:
371
+ if font.ttfont.get("fpgm"):
372
+ font.t_head.set_bit("flags", 3, True)
373
+ return font.t_head.is_modified
374
+
375
+ runner = TaskRunner(input_path=input_path, task=task, **options)
376
+ runner.filter.filter_out_ps = True
377
+ runner.run()
378
+
379
+
347
380
  @cli.command("transformed-components", cls=BaseCommand)
348
381
  def fix_transformed_components(input_path: Path, **options: dict[str, Any]) -> None:
349
382
  """
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import Any, Literal, Optional
2
+ from typing import Any, Literal
3
3
 
4
4
  import click
5
5
  from foundrytools.core.font import Font
@@ -150,8 +150,8 @@ def remove_glyphs(input_path: Path, **options: dict[str, Any]) -> None:
150
150
 
151
151
  def task(
152
152
  font: Font,
153
- glyph_names_to_remove: Optional[set[str]],
154
- glyph_ids_to_remove: Optional[set[int]],
153
+ glyph_names_to_remove: set[str] | None,
154
+ glyph_ids_to_remove: set[int] | None,
155
155
  ) -> bool:
156
156
  removed_glyphs = font.remove_glyphs(
157
157
  glyph_names_to_remove=glyph_names_to_remove,
@@ -50,7 +50,9 @@ def recalc_caret_offset(input_path: Path, **options: dict[str, Any]) -> None:
50
50
  source_file = font.file
51
51
  else:
52
52
  font.ttfont.flavor = None
53
- source_file = font.temp_file
53
+ temp_file = get_temp_file_path()
54
+ font.save(temp_file)
55
+ source_file = temp_file
54
56
  font.save(source_file)
55
57
 
56
58
  command_1 = ["tx", "-t1", source_file, temp_t1_file]
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import Any, Optional
2
+ from typing import Any
3
3
 
4
4
  import click
5
5
  from foundrytools import Font
@@ -55,8 +55,8 @@ def del_names(input_path: Path, **options: dict[str, Any]) -> None:
55
55
  def task(
56
56
  font: Font,
57
57
  name_ids_to_process: tuple[int],
58
- platform_id: Optional[int] = None,
59
- language_string: Optional[str] = None,
58
+ platform_id: int | None = None,
59
+ language_string: str | None = None,
60
60
  ) -> bool:
61
61
  font.t_name.remove_names(
62
62
  name_ids=name_ids_to_process, platform_id=platform_id, language_string=language_string
@@ -171,8 +171,8 @@ def find_replace(input_path: Path, **options: dict[str, Any]) -> None:
171
171
  font: Font,
172
172
  old_string: str,
173
173
  new_string: str,
174
- name_ids_to_process: Optional[tuple[int]] = None,
175
- name_ids_to_skip: Optional[tuple[int]] = None,
174
+ name_ids_to_process: tuple[int] | None = None,
175
+ name_ids_to_skip: tuple[int] | None = None,
176
176
  ) -> bool:
177
177
  font.t_name.find_replace(
178
178
  old_string=old_string,
@@ -227,7 +227,7 @@ def set_name(input_path: Path, **options: dict[str, Any]) -> None:
227
227
  font: Font,
228
228
  name_id: int,
229
229
  name_string: str,
230
- platform_id: Optional[int] = None,
230
+ platform_id: int | None = None,
231
231
  language_string: str = "en",
232
232
  ) -> bool:
233
233
  font.t_name.set_name(
@@ -289,7 +289,7 @@ def build_unique_id(input_path: Path, **options: dict[str, Any]) -> None:
289
289
  ``Font Revision;Vendor ID;PostScript Name``
290
290
  """
291
291
 
292
- def task(font: Font, platform_id: Optional[int] = None, alternate: bool = False) -> bool:
292
+ def task(font: Font, platform_id: int | None = None, alternate: bool = False) -> bool:
293
293
  font.t_name.build_unique_identifier(platform_id=platform_id, alternate=alternate)
294
294
  return font.t_name.is_modified
295
295
 
@@ -313,7 +313,7 @@ def build_full_name(input_path: Path, **options: dict[str, Any]) -> None:
313
313
  Builds the NameID 4 (Full Font Name).
314
314
  """
315
315
 
316
- def task(font: Font, platform_id: Optional[int] = None) -> bool:
316
+ def task(font: Font, platform_id: int | None = None) -> bool:
317
317
  font.t_name.build_full_font_name(platform_id=platform_id)
318
318
  return font.t_name.is_modified
319
319
 
@@ -337,7 +337,7 @@ def build_version_string(input_path: Path, **options: dict[str, Any]) -> None:
337
337
  Builds the NameID 5 (Version String).
338
338
  """
339
339
 
340
- def task(font: Font, platform_id: Optional[int] = None) -> bool:
340
+ def task(font: Font, platform_id: int | None = None) -> bool:
341
341
  font.t_name.build_version_string(platform_id=platform_id)
342
342
  return font.t_name.is_modified
343
343
 
@@ -361,7 +361,7 @@ def build_postscript_name(input_path: Path, **options: dict[str, Any]) -> None:
361
361
  Builds the NameID 6 (PostScript Name).
362
362
  """
363
363
 
364
- def task(font: Font, platform_id: Optional[int] = None) -> bool:
364
+ def task(font: Font, platform_id: int | None = None) -> bool:
365
365
  font.t_name.build_postscript_name(platform_id=platform_id)
366
366
  return font.t_name.is_modified
367
367
 
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import Any, Optional, Union
2
+ from typing import Any
3
3
 
4
4
  import click
5
5
  from fontTools.misc.roundTools import otRound
@@ -257,7 +257,7 @@ def set_attrs(input_path: Path, **options: dict[str, Any]) -> None:
257
257
  """
258
258
  ensure_at_least_one_param(click.get_current_context())
259
259
 
260
- def task(font: Font, **kwargs: dict[str, Optional[Union[int, float, str, bool]]]) -> bool:
260
+ def task(font: Font, **kwargs: dict[str, int | float | str | bool | None]) -> bool:
261
261
  for attr, value in kwargs.items():
262
262
  if value is not None:
263
263
  try:
@@ -417,7 +417,7 @@ def set_fs_selection(input_path: Path, **options: dict[str, Any]) -> None:
417
417
  """
418
418
  ensure_at_least_one_param(click.get_current_context())
419
419
 
420
- def task(font: Font, **kwargs: dict[str, Optional[bool]]) -> bool:
420
+ def task(font: Font, **kwargs: dict[str, bool | None]) -> bool:
421
421
  for attr, value in kwargs.items():
422
422
  if value is not None:
423
423
  if hasattr(font.flags, attr):
@@ -497,7 +497,7 @@ def set_fs_type(input_path: Path, **options: dict[str, Any]) -> None:
497
497
  """
498
498
  ensure_at_least_one_param(click.get_current_context())
499
499
 
500
- def task(font: Font, **kwargs: dict[str, Optional[bool]]) -> bool:
500
+ def task(font: Font, **kwargs: dict[str, bool | None]) -> bool:
501
501
  for attr, value in kwargs.items():
502
502
  if hasattr(font.t_os_2, attr) and value is not None:
503
503
  setattr(font.t_os_2, attr, value)
@@ -1,8 +1,10 @@
1
+ from collections.abc import Callable
1
2
  from pathlib import Path
2
- from typing import Any, Callable, cast
3
+ from typing import Any, cast
3
4
 
4
5
  import click
5
6
  from foundrytools import Font
7
+ from foundrytools.app.otf_autohint import OTFAutohintError
6
8
  from foundrytools.app.otf_autohint import run as otf_autohint
7
9
  from foundrytools.app.otf_check_outlines import run as otf_check_outlines
8
10
  from foundrytools.app.otf_dehint import run as otf_dehint
@@ -57,8 +59,8 @@ def subroutinize_flag() -> Callable:
57
59
  """,
58
60
  )
59
61
  @click.option(
60
- "-d",
61
- "--decimal",
62
+ "-d/-no-d",
63
+ "--decimal/--no-decimal",
62
64
  "roundCoords",
63
65
  is_flag=True,
64
66
  default=True,
@@ -94,7 +96,12 @@ def autohint(input_path: Path, **options: dict[str, Any]) -> None:
94
96
 
95
97
  def task(font: Font, subroutinize: bool = True, **kwargs: dict[str, Any]) -> bool:
96
98
  logger.info("Autohinting...")
97
- otf_autohint(font, **kwargs)
99
+
100
+ try:
101
+ otf_autohint(font, **kwargs)
102
+ except OTFAutohintError as e:
103
+ logger.error(f"Autohinting failed: {e}")
104
+ return False
98
105
 
99
106
  if subroutinize:
100
107
  font.reload() # DO NOT REMOVE
@@ -269,8 +276,9 @@ def recalc_stems(input_path: Path, **options: dict[str, Any]) -> None:
269
276
  temp_file = get_temp_file_path()
270
277
  if flavor is not None:
271
278
  font.ttfont.flavor = None
272
- font.save(font.temp_file)
273
- input_file = font.temp_file
279
+ temp_file = get_temp_file_path()
280
+ font.save(temp_file)
281
+ input_file = temp_file
274
282
  else:
275
283
  input_file = font.file
276
284
 
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import Any, Optional
2
+ from typing import Any
3
3
 
4
4
  import click
5
5
  from foundrytools import Font
@@ -43,10 +43,10 @@ def cli(input_path: Path, **options: dict[str, Any]) -> None:
43
43
 
44
44
  def task(
45
45
  font: Font,
46
- italic_angle: Optional[float] = None,
47
- underline_position: Optional[int] = None,
48
- underline_thickness: Optional[int] = None,
49
- fixed_pitch: Optional[bool] = None,
46
+ italic_angle: float | None = None,
47
+ underline_position: int | None = None,
48
+ underline_thickness: int | None = None,
49
+ fixed_pitch: bool | None = None,
50
50
  ) -> bool:
51
51
  attrs = {
52
52
  "italic_angle": italic_angle,
@@ -1,5 +1,4 @@
1
1
  from pathlib import Path
2
- from typing import Optional
3
2
 
4
3
  import click
5
4
  from foundrytools import FontFinder
@@ -53,7 +52,7 @@ def font_info(input_path: Path) -> None:
53
52
  Prints a minimal set of NameRecords, omitting the ones with nameID not in 1, 2, 3, 4, 5, 6, 16,
54
53
  17, 18, 21, 22, 25""",
55
54
  )
56
- def font_names(input_path: Path, max_lines: Optional[int] = None, minimal: bool = False) -> None:
55
+ def font_names(input_path: Path, max_lines: int | None = None, minimal: bool = False) -> None:
57
56
  """
58
57
  Prints the name table.
59
58
  """
@@ -1,5 +1,4 @@
1
1
  from shutil import get_terminal_size
2
- from typing import Optional
3
2
 
4
3
  from fontTools.ttLib.tables._n_a_m_e import _MAC_LANGUAGES, _WINDOWS_LANGUAGES, NameRecord
5
4
  from foundrytools import Font
@@ -42,7 +41,7 @@ JUSTIFY = 22
42
41
 
43
42
 
44
43
  def _process_name_table(
45
- font: Font, table: Table, terminal_width: int, max_lines: Optional[int], minimal: bool
44
+ font: Font, table: Table, terminal_width: int, max_lines: int | None, minimal: bool
46
45
  ) -> None:
47
46
  name_table = NameTable(font.ttfont)
48
47
  names = name_table.table.names
@@ -63,7 +62,7 @@ def _process_name_table(
63
62
 
64
63
 
65
64
  def _add_name_row_to_table(
66
- table: Table, name: NameRecord, terminal_width: int, max_lines: Optional[int]
65
+ table: Table, name: NameRecord, terminal_width: int, max_lines: int | None
67
66
  ) -> None:
68
67
  row_string = _get_name_row(name)
69
68
  row_string = wrap_string(
@@ -77,7 +76,7 @@ def _add_name_row_to_table(
77
76
 
78
77
 
79
78
  def _process_cff_table(
80
- font: Font, table: Table, terminal_width: int, max_lines: Optional[int], minimal: bool
79
+ font: Font, table: Table, terminal_width: int, max_lines: int | None, minimal: bool
81
80
  ) -> None:
82
81
  if not font.is_ps:
83
82
  return
@@ -133,7 +132,7 @@ def _get_name_row(name: NameRecord) -> str:
133
132
  )
134
133
 
135
134
 
136
- def main(font: Font, max_lines: Optional[int] = None, minimal: bool = False) -> None:
135
+ def main(font: Font, max_lines: int | None = None, minimal: bool = False) -> None:
137
136
  """
138
137
  Prints the names of the font.
139
138
  """
@@ -1,6 +1,7 @@
1
1
  # pylint: disable=import-outside-toplevel
2
+ from collections.abc import Callable
2
3
  from pathlib import Path
3
- from typing import Any, Callable, cast
4
+ from typing import Any, cast
4
5
 
5
6
  import click
6
7
 
@@ -38,7 +39,7 @@ cli = click.Group(help="Miscellaneous utilities.")
38
39
  @click.option(
39
40
  "-s",
40
41
  "--source",
41
- type=click.Choice(choices=["1", "2", "3", "4", "5"]),
42
+ type=click.Choice(choices=["1", "2", "3", "4", "5", "6"]),
42
43
  default="1",
43
44
  callback=choice_to_int_callback,
44
45
  help="""
@@ -52,6 +53,7 @@ cli = click.Group(help="Miscellaneous utilities.")
52
53
  3: Full Font Name
53
54
  4: CFF fontNames (CFF fonts only)
54
55
  5: CFF TopDict FullName (CFF fonts only)
56
+ 6: Macintosh LWFN (LaserWriter Font Naming) using 5:3:3 rule
55
57
  """,
56
58
  )
57
59
  @click.option(
@@ -1,3 +1,5 @@
1
+ # ruff: noqa: C901
2
+ import re
1
3
  from pathlib import Path
2
4
 
3
5
  from foundrytools import Font
@@ -6,6 +8,59 @@ from pathvalidate import sanitize_filename
6
8
  from foundrytools_cli.utils.logger import logger
7
9
 
8
10
 
11
+ def _apply_533_rule(postscript_name: str) -> str:
12
+ """
13
+ Apply the Macintosh LaserWriter Font Naming (LWFN) 5:3:3 rule.
14
+
15
+ The rule divides the font name into "words" where each uppercase letter begins
16
+ a new word. The file name is derived by:
17
+ - Taking the initial uppercase letter and up to 4 lowercase letters in the first word
18
+ - Taking the initial uppercase letter and up to 2 lowercase letters for subsequent words
19
+ - Dropping hyphens between family and style portions
20
+
21
+ Args:
22
+ postscript_name: The PostScript name (e.g., "Helvetica-BoldOblique")
23
+
24
+ Returns:
25
+ The abbreviated filename following the 5:3:3 rule (e.g., "HelveBolObl")
26
+
27
+ Examples:
28
+ >>> _apply_533_rule("Helvetica-BoldOblique")
29
+ 'HelveBolObl'
30
+ >>> _apply_533_rule("AGaramond-Bold")
31
+ 'AGarBol'
32
+ >>> _apply_533_rule("Palatino-Roman")
33
+ 'PalatRom'
34
+ >>> _apply_533_rule("Optima")
35
+ 'Optim'
36
+ """
37
+ # Remove hyphens to get a continuous string
38
+ name_without_hyphens = postscript_name.replace("-", "")
39
+
40
+ # Split into words where each uppercase letter begins a new word
41
+ # This regex finds: uppercase letter followed by any number of lowercase letters
42
+ words = re.findall(r"[A-Z][a-z]*", name_without_hyphens)
43
+
44
+ if not words:
45
+ # Fallback for invalid/unusual PostScript names without uppercase letters.
46
+ # PostScript font names should start with uppercase per Adobe spec, but we handle
47
+ # the edge case gracefully. The 11-char limit approximates typical LWFN length
48
+ # (5+3+3 for a three-word name like FamilyStyleVariant).
49
+ MAX_FALLBACK_LENGTH = 11
50
+ return name_without_hyphens[:MAX_FALLBACK_LENGTH]
51
+
52
+ result = []
53
+ for i, word in enumerate(words):
54
+ if i == 0:
55
+ # First word: take up to 5 characters (1 uppercase + 4 lowercase)
56
+ result.append(word[:5])
57
+ else:
58
+ # Subsequent words: take up to 3 characters (1 uppercase + 2 lowercase)
59
+ result.append(word[:3])
60
+
61
+ return "".join(result)
62
+
63
+
9
64
  def _get_file_stem(font: Font, source: int = 1) -> str:
10
65
  """
11
66
  Get the best file name for a font.
@@ -20,6 +75,7 @@ def _get_file_stem(font: Font, source: int = 1) -> str:
20
75
  3: Full Font Name
21
76
  4: CFF fontNames (CFF fonts only)
22
77
  5: CFF TopDict FullName (CFF fonts only)
78
+ 6: Macintosh LWFN (LaserWriter Font Naming) using 5:3:3 rule
23
79
 
24
80
  Returns:
25
81
  A sanitized file name for the font.
@@ -49,6 +105,12 @@ def _get_file_stem(font: Font, source: int = 1) -> str:
49
105
  file_name = font.t_cff_.table.cff.fontNames[0]
50
106
  elif source == 5 and font.is_ps:
51
107
  file_name = font.t_cff_.top_dict.FullName
108
+ elif source == 6:
109
+ # Build name from family and subfamily for LWFN 5:3:3 rule
110
+ family_name = str(font.t_name.table.getBestFamilyName())
111
+ subfamily_name = str(font.t_name.table.getBestSubFamilyName())
112
+ font_name = f"{family_name}-{subfamily_name}".replace(" ", "").replace(".", "")
113
+ file_name = _apply_533_rule(font_name)
52
114
  else:
53
115
  raise ValueError("Invalid source value.")
54
116
  return sanitize_filename(file_name, platform="auto")
@@ -1,7 +1,8 @@
1
1
  import os
2
+ from collections.abc import Callable
2
3
  from pathlib import Path
3
4
  from textwrap import TextWrapper
4
- from typing import Any, Callable, Optional, Union
5
+ from typing import Any
5
6
 
6
7
  import click
7
8
 
@@ -41,7 +42,7 @@ class BaseCommand(click.Command):
41
42
  """,
42
43
  ),
43
44
  click.Option(
44
- ["-no-ow", "--no-overwrite", "overwrite"],
45
+ ["-ow/-no-ow", "--overwrite/--no-overwrite"],
45
46
  is_flag=True,
46
47
  default=True,
47
48
  help="""
@@ -54,7 +55,7 @@ class BaseCommand(click.Command):
54
55
  """,
55
56
  ),
56
57
  click.Option(
57
- ["-no-rbb", "--no-recalc-bboxes", "recalc_bboxes"],
58
+ ["-rbb/-no-rbb", "--recalc-bboxes/--no-recalc-bboxes"],
58
59
  is_flag=True,
59
60
  default=True,
60
61
  help="""
@@ -66,7 +67,7 @@ class BaseCommand(click.Command):
66
67
  """,
67
68
  ),
68
69
  click.Option(
69
- ["-rtb", "--reorder-tables", "reorder_tables"],
70
+ ["-rtb", "--reorder-tables"],
70
71
  is_flag=True,
71
72
  default=False,
72
73
  help="""
@@ -112,8 +113,8 @@ def make_options(options: list[Callable]) -> Callable:
112
113
 
113
114
 
114
115
  def choice_to_int_callback(
115
- ctx: click.Context, _: click.Parameter, value: Union[str, tuple[str, ...]]
116
- ) -> Optional[Union[int, tuple[int, ...]]]:
116
+ ctx: click.Context, _: click.Parameter, value: str | tuple[str, ...]
117
+ ) -> int | tuple[int, ...] | None:
117
118
  """
118
119
  Callback for click options that accept a choice. Converts the choice to an integer or a tuple
119
120
  of integers.
@@ -129,7 +130,7 @@ def choice_to_int_callback(
129
130
  :param value: The value to convert
130
131
  :type value: Union[str, tuple[str, ...]]
131
132
  :return: The converted value
132
- :rtype: Optional[Union[int, tuple[int, ...]]]
133
+ :rtype: int | tuple[int, ...] | None
133
134
  """
134
135
 
135
136
  # we do not check if the values can be converted to integers here because the click.Choice
@@ -169,9 +170,7 @@ def tuple_to_set_callback(
169
170
  return set(value)
170
171
 
171
172
 
172
- def output_dir_callback(
173
- ctx: click.Context, _: click.Parameter, value: Optional[Path]
174
- ) -> Optional[Path]:
173
+ def output_dir_callback(ctx: click.Context, _: click.Parameter, value: Path | None) -> Path | None:
175
174
  """
176
175
  Callback for ``--output-dir option``.
177
176
 
@@ -227,7 +226,7 @@ def ensure_at_least_one_param(ctx: click.Context) -> None:
227
226
 
228
227
 
229
228
  def wrap_string(
230
- string: str, width: int, initial_indent: int, indent: int, max_lines: Optional[int] = None
229
+ string: str, width: int, initial_indent: int, indent: int, max_lines: int | None = None
231
230
  ) -> str:
232
231
  """
233
232
  Wraps a string to a specified width.
@@ -1,6 +1,7 @@
1
+ from collections.abc import Callable
1
2
  from dataclasses import dataclass
2
3
  from pathlib import Path
3
- from typing import Any, Callable, Optional, Union, get_type_hints
4
+ from typing import Any, get_type_hints
4
5
 
5
6
  from foundrytools import Font
6
7
  from foundrytools.lib.font_finder import (
@@ -28,9 +29,9 @@ class SaveOptions:
28
29
  A class that specifies how to save the font.
29
30
  """
30
31
 
31
- reorder_tables: Optional[bool] = True
32
+ reorder_tables: bool | None = True
32
33
  suffix: str = ""
33
- output_dir: Optional[Path] = None
34
+ output_dir: Path | None = None
34
35
  overwrite: bool = False
35
36
 
36
37
 
@@ -74,7 +75,7 @@ class TaskRunnerConfig: # pylint: disable=too-few-public-methods
74
75
 
75
76
  @staticmethod
76
77
  def _set_options(
77
- options_group: Union[dict, FinderOptions, SaveOptions], options: dict[str, Any]
78
+ options_group: dict | FinderOptions | SaveOptions, options: dict[str, Any]
78
79
  ) -> None:
79
80
  """
80
81
  Update attributes of an options_group with provided options if the attribute exists.
@@ -1,9 +1,10 @@
1
1
  # https://realpython.com/python-timer/#the-python-timer-code
2
2
 
3
3
  import time
4
+ from collections.abc import Callable
4
5
  from contextlib import ContextDecorator
5
6
  from dataclasses import dataclass, field
6
- from typing import Any, Callable, ClassVar, Optional
7
+ from typing import Any, ClassVar
7
8
 
8
9
 
9
10
  class TimerError(Exception):
@@ -15,10 +16,10 @@ class Timer(ContextDecorator):
15
16
  """Time your code using a class, context manager, or decorator"""
16
17
 
17
18
  timers: ClassVar[dict[str, float]] = {}
18
- name: Optional[str] = None
19
+ name: str | None = None
19
20
  text: str = "Elapsed time: {:0.4f} seconds"
20
- logger: Optional[Callable[[str], None]] = print
21
- _start_time: Optional[float] = field(default=None, init=False, repr=False)
21
+ logger: Callable[[str], None] | None = print
22
+ _start_time: float | None = field(default=None, init=False, repr=False)
22
23
 
23
24
  def __post_init__(self) -> None:
24
25
  """Initialization: add timer to dict of timers"""
@@ -1,3 +0,0 @@
1
- VERSION = __version__ = "2.0.4"
2
-
3
- __all__ = ["VERSION"]