foundrytools-cli 2.2.0__py3-none-any.whl → 2.2.2__py3-none-any.whl

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.
@@ -1,3 +1,3 @@
1
- VERSION = __version__ = "2.2.0"
1
+ VERSION = __version__ = "2.2.2"
2
2
 
3
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,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
@@ -1,4 +1,4 @@
1
- foundrytools_cli/__init__.py,sha256=piRNIMDwxlVsenO9vEcQRdlc_hlbUDikT3wDfiEQIEI,55
1
+ foundrytools_cli/__init__.py,sha256=kFf6kATo3E1f6TMp1TkglYgUcEhMpNJgM65FbPdfLXM,55
2
2
  foundrytools_cli/__main__.py,sha256=wN2lDr2iQyzfnOP00_P3DyXIxEBbf6K_cuzX5YFH5Fg,1273
3
3
  foundrytools_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  foundrytools_cli/commands/cff.py,sha256=6eEZydw0ZyISjYuV0JObmNBI7ijdEvVatKwWc8FM5vw,3386
@@ -10,12 +10,12 @@ foundrytools_cli/commands/fix/__init__.py,sha256=1kJOW26OAkCYYvYDG2H9sCSqqQUQEtw
10
10
  foundrytools_cli/commands/fix/cli.py,sha256=bcmppksAZ_vkV5ZyHiCw-B3ukEbul20h5i3A5NfcYhA,17578
11
11
  foundrytools_cli/commands/fix/legacy_accents.py,sha256=pUHWxV-a2GZgI8W_K2u-lu2yMQt5YKEpQHUD9FyKl8A,2094
12
12
  foundrytools_cli/commands/fix/vertical_metrics.py,sha256=j563oWfe3znOKuEW333Yx60d_14Nt0cGdXg3mkCTIN8,691
13
- foundrytools_cli/commands/font.py,sha256=Ee5yi_QkvfpCJv-HruHa3F9L0jXSXJYXVXvHuAumK_A,8240
13
+ foundrytools_cli/commands/font.py,sha256=WIxz2qqcLloXWxiuvBMp1lAnXyftQ3lLJSKM_3Xhc08,9490
14
14
  foundrytools_cli/commands/gsub.py,sha256=iaOAy4jqW6p_RIBxvixtwaEV5AQivaezy0k_Mcufoqg,1454
15
15
  foundrytools_cli/commands/hhea.py,sha256=wRld1Cj2q6RnLspMgj_1Z62pIw6Vhqgrm1RO06_qwwc,3158
16
16
  foundrytools_cli/commands/name.py,sha256=2EizKjc3puytua2BVnuCcRuP6NzuwNZpZryPZ8uNL-o,10354
17
17
  foundrytools_cli/commands/os_2.py,sha256=htdQgD6QLM4v_opVhDtFf9Qhyrz7_gKToQ-xiMHCTCk,19495
18
- foundrytools_cli/commands/otf.py,sha256=lgsd-xQ3ID0eCwHo7vqWjYGUnjJFFZz72pESnKJv5IA,10621
18
+ foundrytools_cli/commands/otf.py,sha256=cb8lfxXooGs7nXOx2i33MfFYYgU8nVgTH7A20Q3doCM,12543
19
19
  foundrytools_cli/commands/post.py,sha256=HpcQD_FKSw8Pr8a6gs62Ln_X9TtqODWHSKNZBmmMT40,2016
20
20
  foundrytools_cli/commands/print/__init__.py,sha256=B1h44RtXGw3V4X5se75oCvQPWkh0RbQ0luHxK6z4ge8,71
21
21
  foundrytools_cli/commands/print/cli.py,sha256=34CaQIby0u9tZ2vrsKFmvSVg5WH6eEgP5p9FZRoVDEU,1910
@@ -25,14 +25,14 @@ foundrytools_cli/commands/print/vf_instances.py,sha256=vpp3sMyCcNh9f6CBbOn5lnWr4
25
25
  foundrytools_cli/commands/ttf.py,sha256=0ZJc36MM8m12_reMVD6GS5ZpSt84g_9RpXrKz_zqqfI,2151
26
26
  foundrytools_cli/commands/utils/__init__.py,sha256=bF8Ue4I4QAaDplSpDSMvtb4AZMWjcYsTNIL5ZBp6-t8,71
27
27
  foundrytools_cli/commands/utils/cli.py,sha256=OKX4FEzcDP9DZHyAk0tN2vuFoQ__BtmvIfP34hbsEYE,3685
28
- foundrytools_cli/commands/utils/font_organizer.py,sha256=9biz8Cxg81qNGtK53zp4xhgncbItE5xRXBL1Y1ZWHqg,4096
28
+ foundrytools_cli/commands/utils/font_organizer.py,sha256=UL2y7Rauax3S3PxMX6CX1lV0Uk-EA8kKjhW2C7s_MpI,4576
29
29
  foundrytools_cli/commands/utils/font_renamer.py,sha256=eEWT_1CCXu0mRXTOx5okUQ1NJrqs2RiG-TsRhbAMoRw,5755
30
30
  foundrytools_cli/commands/utils/sync_timestamps.py,sha256=h9BMafxlmFllXKyrqBwibQIupS_d2Rp0TXEIfjk7UZQ,3989
31
31
  foundrytools_cli/utils/__init__.py,sha256=m-jBbONfS-_WMbpql8GV_1KufNoWOLQuNQtqErH-WmQ,7962
32
32
  foundrytools_cli/utils/logger.py,sha256=ZjVgZ1AmzwwH1GEnBU1HP-pFvZv0BKkVV93P2gYvnLc,525
33
33
  foundrytools_cli/utils/task_runner.py,sha256=CXVItMJTslNuie_rhPpZrQBX5rgkWbAWX805dFIBInA,6965
34
34
  foundrytools_cli/utils/timer.py,sha256=i_E1nM4KuooZwT2_2bkNJCxLrSKBaQCIMaHxTHY-Hzs,1870
35
- foundrytools_cli-2.2.0.dist-info/WHEEL,sha256=9IUYslYuG_asT4WlWvfk_K76nveuLvSTKDoLeL0cV-Q,80
36
- foundrytools_cli-2.2.0.dist-info/entry_points.txt,sha256=sNn6z8yZnx3Koh85LS6BYDqzaXZSP4KEvN3AdFGYRao,57
37
- foundrytools_cli-2.2.0.dist-info/METADATA,sha256=qe-pXLQXlBlh_7Z9QUQAv5VPIk6mkszxwcGvrsHfa5I,3777
38
- foundrytools_cli-2.2.0.dist-info/RECORD,,
35
+ foundrytools_cli-2.2.2.dist-info/WHEEL,sha256=MWx7psoF7KNST9G72pGtiBYuaXRXQXBk9pw5R6FzYko,80
36
+ foundrytools_cli-2.2.2.dist-info/entry_points.txt,sha256=sNn6z8yZnx3Koh85LS6BYDqzaXZSP4KEvN3AdFGYRao,57
37
+ foundrytools_cli-2.2.2.dist-info/METADATA,sha256=6tU5bWSXQHGA5ZfwJSJF06pr0_LYIkif7VKaxCkSLGs,3777
38
+ foundrytools_cli-2.2.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.13.1
2
+ Generator: uv 0.13.7
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any