foundrytools-cli 2.2.0__tar.gz → 2.2.2__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 (39) hide show
  1. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/PKG-INFO +2 -2
  2. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/pyproject.toml +5 -5
  3. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/pyproject.toml.orig +5 -5
  4. foundrytools_cli-2.2.2/src/foundrytools_cli/__init__.py +3 -0
  5. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/font.py +40 -0
  6. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/otf.py +71 -16
  7. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/utils/font_organizer.py +10 -3
  8. foundrytools_cli-2.2.0/src/foundrytools_cli/__init__.py +0 -3
  9. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/README.md +0 -0
  10. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/__main__.py +0 -0
  11. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/__init__.py +0 -0
  12. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/cff.py +0 -0
  13. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/cmap.py +0 -0
  14. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/converter/__init__.py +0 -0
  15. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/converter/cli.py +0 -0
  16. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/converter/ttf_to_otf.py +0 -0
  17. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/fix/__init__.py +0 -0
  18. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/fix/cli.py +0 -0
  19. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/fix/legacy_accents.py +0 -0
  20. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/fix/vertical_metrics.py +0 -0
  21. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/gsub.py +0 -0
  22. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/hhea.py +0 -0
  23. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/name.py +0 -0
  24. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/os_2.py +0 -0
  25. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/post.py +0 -0
  26. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/print/__init__.py +0 -0
  27. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/print/cli.py +0 -0
  28. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/print/font_info.py +0 -0
  29. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/print/font_names.py +0 -0
  30. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/print/vf_instances.py +0 -0
  31. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/ttf.py +0 -0
  32. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/utils/__init__.py +0 -0
  33. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/utils/cli.py +0 -0
  34. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/utils/font_renamer.py +0 -0
  35. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/commands/utils/sync_timestamps.py +0 -0
  36. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/utils/__init__.py +0 -0
  37. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/utils/logger.py +0 -0
  38. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/utils/task_runner.py +0 -0
  39. {foundrytools_cli-2.2.0 → foundrytools_cli-2.2.2}/src/foundrytools_cli/utils/timer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: foundrytools-cli
3
- Version: 2.2.0
3
+ Version: 2.2.2
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>
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.15
19
19
  Classifier: Topic :: Text Processing :: Fonts
20
20
  Requires-Dist: afdko==4.0.3
21
21
  Requires-Dist: click>=8.5.0
22
- Requires-Dist: foundrytools>=0.1.6
22
+ Requires-Dist: foundrytools>=0.1.8
23
23
  Requires-Dist: loguru>=0.7.3
24
24
  Requires-Dist: pathvalidate>=3.3.1
25
25
  Requires-Dist: rich>=15.0.0
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "foundrytools-cli"
7
- version = "2.2.0"
7
+ version = "2.2.2"
8
8
  description = "A set of command line tools to inspect, manipulate and convert font files"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -12,7 +12,7 @@ license = "MIT"
12
12
  dependencies = [
13
13
  "afdko==4.0.3",
14
14
  "click>=8.5.0",
15
- "foundrytools>=0.1.6",
15
+ "foundrytools>=0.1.8",
16
16
  "loguru>=0.7.3",
17
17
  "pathvalidate>=3.3.1",
18
18
  "rich>=15.0.0",
@@ -107,9 +107,9 @@ docstring-code-format = true
107
107
  docstring-code-line-length = "dynamic"
108
108
 
109
109
  [tool.bumpversion]
110
- current_version = "2.2.0"
111
- commit = false
112
- tag = false
110
+ current_version = "2.2.2"
111
+ commit = true
112
+ tag = true
113
113
 
114
114
  [[tool.bumpversion.files]]
115
115
  filename = "src/foundrytools_cli/__init__.py"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "foundrytools-cli"
7
- version = "2.2.0"
7
+ version = "2.2.2"
8
8
  description = "A set of command line tools to inspect, manipulate and convert font files"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -13,7 +13,7 @@ authors = [{ name = "Cesare Gilento", email = "ftcli@proton.me" }]
13
13
  dependencies = [
14
14
  "afdko==4.0.3",
15
15
  "click>=8.5.0",
16
- "foundrytools>=0.1.6",
16
+ "foundrytools>=0.1.8",
17
17
  "loguru>=0.7.3",
18
18
  "pathvalidate>=3.3.1",
19
19
  "rich>=15.0.0",
@@ -92,9 +92,9 @@ docstring-code-format = true
92
92
  docstring-code-line-length = "dynamic"
93
93
 
94
94
  [tool.bumpversion]
95
- current_version = "2.2.0"
96
- commit = false
97
- tag = false
95
+ current_version = "2.2.2"
96
+ commit = true
97
+ tag = true
98
98
 
99
99
  [[tool.bumpversion.files]]
100
100
  filename = "src/foundrytools_cli/__init__.py"
@@ -0,0 +1,3 @@
1
+ VERSION = __version__ = "2.2.2"
2
+
3
+ __all__ = ["VERSION"]
@@ -11,6 +11,46 @@ from foundrytools_cli.utils.task_runner import TaskRunner
11
11
  cli = click.Group(help="Font level utilities.")
12
12
 
13
13
 
14
+ @cli.command("add-extremes", cls=BaseCommand)
15
+ @click.option(
16
+ "--keep-hinting",
17
+ is_flag=True,
18
+ help="Preserve hinting when adding extrema points.",
19
+ )
20
+ @click.option(
21
+ "--tolerance",
22
+ type=click.FloatRange(min=0),
23
+ default=1.0,
24
+ show_default=True,
25
+ help="Do not add extrema points within this distance (font units) of existing points.",
26
+ )
27
+ def add_extremes(input_path: Path, **options: dict[str, Any]) -> None:
28
+ """
29
+ Add points at the horizontal and vertical extrema of glyph curves.
30
+
31
+ Composite glyphs in TrueType fonts and variable fonts are not modified.
32
+ """
33
+
34
+ def task(font: Font, keep_hinting: bool = False, tolerance: float = 1.0) -> bool:
35
+ modified_glyphs = font.add_extremes(
36
+ remove_hinting=not keep_hinting,
37
+ tolerance=tolerance,
38
+ )
39
+ if not modified_glyphs:
40
+ logger.info("No glyphs were modified")
41
+ return False
42
+
43
+ logger.opt(colors=True).info(
44
+ f"{len(modified_glyphs)} glyphs were modified: "
45
+ f"<lc>{', '.join(sorted(modified_glyphs))}</lc>"
46
+ )
47
+ return True
48
+
49
+ runner = TaskRunner(input_path=input_path, task=task, **options)
50
+ runner.filter.filter_out_variable = True
51
+ runner.run()
52
+
53
+
14
54
  @cli.command("correct-contours", cls=BaseCommand)
15
55
  @click.option(
16
56
  "-ma",
@@ -258,12 +258,53 @@ def round_coordinates(input_path: Path, **options: dict[str, Any]) -> None:
258
258
  The number of vertical stem values to extract.
259
259
  """,
260
260
  )
261
+ @click.option(
262
+ "--use-samples/--all-glyphs",
263
+ default=True,
264
+ show_default=True,
265
+ help="Calculate each value from its default or custom glyph sample, or from the whole font.",
266
+ )
267
+ @click.option(
268
+ "--std-h-sample",
269
+ multiple=True,
270
+ help="Characters to sample for StdHW; may be repeated. Defaults to the built-in sample.",
271
+ )
272
+ @click.option(
273
+ "--std-v-sample",
274
+ multiple=True,
275
+ help="Characters to sample for StdVW; may be repeated. Defaults to the built-in sample.",
276
+ )
277
+ @click.option(
278
+ "--snap-h-sample",
279
+ multiple=True,
280
+ help="Characters to sample for StemSnapH; may be repeated. Defaults to the built-in sample.",
281
+ )
282
+ @click.option(
283
+ "--snap-v-sample",
284
+ multiple=True,
285
+ help="Characters to sample for StemSnapV; may be repeated. Defaults to the built-in sample.",
286
+ )
261
287
  def recalc_stems(input_path: Path, **options: dict[str, Any]) -> None:
262
288
  """
263
289
  Recalculate the hinting stems of OpenType-PS fonts.
264
290
  """
265
291
 
266
- def task(font: Font) -> bool:
292
+ sample_options = ("std_h_sample", "std_v_sample", "snap_h_sample", "snap_v_sample")
293
+ if not options["use_samples"] and any(options[name] for name in sample_options):
294
+ raise click.UsageError("Custom glyph samples cannot be used with --all-glyphs.")
295
+
296
+ def task(
297
+ font: Font,
298
+ report_all_stems: bool = False,
299
+ max_distance: int = 1,
300
+ max_h_stems: int = 2,
301
+ max_v_stems: int = 2,
302
+ use_samples: bool = True,
303
+ std_h_sample: tuple[str, ...] = (),
304
+ std_v_sample: tuple[str, ...] = (),
305
+ snap_h_sample: tuple[str, ...] = (),
306
+ snap_v_sample: tuple[str, ...] = (),
307
+ ) -> bool:
267
308
  if not font.is_ps:
268
309
  logger.error("Font is not a PostScript font")
269
310
  return False
@@ -273,11 +314,17 @@ def recalc_stems(input_path: Path, **options: dict[str, Any]) -> None:
273
314
  return False
274
315
 
275
316
  flavor = font.ttfont.flavor
276
- temp_file = get_temp_file_path()
317
+ temp_file: Path | None = None
277
318
  if flavor is not None:
278
319
  font.ttfont.flavor = None
279
320
  temp_file = get_temp_file_path()
280
- font.save(temp_file)
321
+ try:
322
+ font.save(temp_file)
323
+ except Exception:
324
+ temp_file.unlink(missing_ok=True)
325
+ raise
326
+ finally:
327
+ font.ttfont.flavor = flavor
281
328
  input_file = temp_file
282
329
  else:
283
330
  input_file = font.file
@@ -289,22 +336,31 @@ def recalc_stems(input_path: Path, **options: dict[str, Any]) -> None:
289
336
  current_stem_snap_h = font.t_cff_.get_hinting_data().get("StemSnapH", None)
290
337
  current_stem_snap_v = font.t_cff_.get_hinting_data().get("StemSnapV", None)
291
338
 
292
- report_all_stems = cast(bool, options["report_all_stems"])
293
- max_distance = cast(int, options["max_distance"])
294
- max_h_stems = cast(int, options["max_h_stems"])
295
- max_v_stems = cast(int, options["max_v_stems"])
296
- std_h_w, std_v_w, stem_snap_h, stem_snap_v = get_stems(
297
- input_file,
298
- report_all_stems=report_all_stems,
299
- max_distance=max_distance,
300
- max_h_stems=max_h_stems,
301
- max_v_stems=max_v_stems,
302
- )
339
+ def to_codepoints(sample: tuple[str, ...]) -> set[int] | None:
340
+ codepoints = {ord(char) for text in sample for char in text}
341
+ return codepoints or None
342
+
343
+ try:
344
+ std_h_w, std_v_w, stem_snap_h, stem_snap_v = get_stems(
345
+ input_file,
346
+ report_all_stems=report_all_stems,
347
+ max_distance=max_distance,
348
+ max_h_stems=max_h_stems,
349
+ max_v_stems=max_v_stems,
350
+ use_samples=use_samples,
351
+ std_h_codepoints=to_codepoints(std_h_sample),
352
+ std_v_codepoints=to_codepoints(std_v_sample),
353
+ snap_h_codepoints=to_codepoints(snap_h_sample),
354
+ snap_v_codepoints=to_codepoints(snap_v_sample),
355
+ )
356
+ finally:
357
+ if temp_file is not None:
358
+ temp_file.unlink(missing_ok=True)
359
+
303
360
  logger.info(f"StdHW: {current_std_h_w} -> {std_h_w}")
304
361
  logger.info(f"StdVW: {current_std_v_w} -> {std_v_w}")
305
362
  logger.info(f"StemSnapH: {current_stem_snap_h} -> {stem_snap_h}")
306
363
  logger.info(f"StemSnapV: {current_stem_snap_v} -> {stem_snap_v}")
307
- temp_file.unlink(missing_ok=True)
308
364
 
309
365
  if (current_std_h_w, current_std_v_w, current_stem_snap_h, current_stem_snap_v) == (
310
366
  std_h_w,
@@ -321,7 +377,6 @@ def recalc_stems(input_path: Path, **options: dict[str, Any]) -> None:
321
377
  "StemSnapV": stem_snap_v,
322
378
  }
323
379
  font.t_cff_.set_hinting_data(**hinting_data)
324
- font.ttfont.flavor = flavor
325
380
  return True
326
381
 
327
382
  runner = TaskRunner(input_path=input_path, task=task, **options)
@@ -50,14 +50,20 @@ def _determine_output_directory(
50
50
  return sanitize_filepath(out_dir, platform="auto")
51
51
 
52
52
 
53
- def _remove_empty_directories(directory: Path) -> None:
53
+ def _remove_empty_directories(directory: Path, stop_at: Path | None = None) -> None:
54
54
  """
55
55
  Removes empty directories recursively.
56
56
 
57
57
  Args:
58
58
  directory (Path): The directory to start removing from.
59
+ stop_at (Path): The directory to stop at (never removed). Defaults to None, which
60
+ means no boundary. Callers should pass the input directory so cleanup never
61
+ climbs above it.
59
62
  """
60
- while not any(directory.iterdir()):
63
+ stop = stop_at.resolve() if stop_at is not None else None
64
+ while directory.is_dir() and (stop is None or directory.resolve() != stop):
65
+ if any(directory.iterdir()):
66
+ break
61
67
  directory.rmdir()
62
68
  logger.opt(colors=True).success(f"{directory} <magenta>--></> <red>Deleted</>")
63
69
  directory = directory.parent
@@ -115,4 +121,5 @@ def main(
115
121
  )
116
122
 
117
123
  if delete_empty_directories:
118
- _remove_empty_directories(font.file.parent)
124
+ stop_at = in_path if in_path.is_dir() else in_path.parent
125
+ _remove_empty_directories(font.file.parent, stop_at=stop_at)
@@ -1,3 +0,0 @@
1
- VERSION = __version__ = "2.2.0"
2
-
3
- __all__ = ["VERSION"]