python-fontbro 0.30.0__tar.gz → 0.31.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 (87) hide show
  1. {python_fontbro-0.30.0/python_fontbro.egg-info → python_fontbro-0.31.0}/PKG-INFO +49 -2
  2. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/README.md +48 -1
  3. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/font.py +59 -1
  4. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/metadata.py +1 -1
  5. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/names.py +253 -17
  6. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/variable.py +32 -0
  7. {python_fontbro-0.30.0 → python_fontbro-0.31.0/python_fontbro.egg-info}/PKG-INFO +49 -2
  8. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_instantiation.py +46 -0
  9. python_fontbro-0.31.0/tests/test_rename.py +454 -0
  10. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_unicode_blocks_and_scripts.py +16 -16
  11. python_fontbro-0.30.0/tests/test_rename.py +0 -163
  12. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/LICENSE.txt +0 -0
  13. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/MANIFEST.in +0 -0
  14. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/__init__.py +0 -0
  15. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/bitmap.py +0 -0
  16. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/color.py +0 -0
  17. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/data/family-classifications.json +0 -0
  18. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/data/features.json +0 -0
  19. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/data/unicode-blocks.json +0 -0
  20. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/data/unicode-scripts.json +0 -0
  21. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/embedding_permissions.py +0 -0
  22. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/exceptions.py +0 -0
  23. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/family_classification.py +0 -0
  24. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/features.py +0 -0
  25. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/files.py +0 -0
  26. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/fingerprint.py +0 -0
  27. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/flags.py +0 -0
  28. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/glyphs.py +0 -0
  29. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/math.py +0 -0
  30. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/metrics.py +0 -0
  31. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/monospace.py +0 -0
  32. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/pixel.py +0 -0
  33. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/py.typed +0 -0
  34. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/render.py +0 -0
  35. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/sanitize.py +0 -0
  36. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/style_flags.py +0 -0
  37. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/subset.py +0 -0
  38. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/support.py +0 -0
  39. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/tables.py +0 -0
  40. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/unicode.py +0 -0
  41. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/utils.py +0 -0
  42. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/fontbro/version.py +0 -0
  43. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/pyproject.toml +0 -0
  44. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/python_fontbro.egg-info/SOURCES.txt +0 -0
  45. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/python_fontbro.egg-info/dependency_links.txt +0 -0
  46. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/python_fontbro.egg-info/requires.txt +0 -0
  47. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/python_fontbro.egg-info/top_level.txt +0 -0
  48. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/setup.cfg +0 -0
  49. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/setup.py +0 -0
  50. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_bitmap.py +0 -0
  51. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_characters.py +0 -0
  52. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_clone.py +0 -0
  53. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_close.py +0 -0
  54. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_collection.py +0 -0
  55. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_color.py +0 -0
  56. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_context_manager.py +0 -0
  57. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_embedding_permissions.py +0 -0
  58. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_family_classification.py +0 -0
  59. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_family_name.py +0 -0
  60. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_features.py +0 -0
  61. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_filename.py +0 -0
  62. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_fingerprint.py +0 -0
  63. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_format.py +0 -0
  64. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_glyphs.py +0 -0
  65. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_image.py +0 -0
  66. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_init.py +0 -0
  67. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_issues.py +0 -0
  68. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_italic_angle.py +0 -0
  69. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_monospace.py +0 -0
  70. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_names.py +0 -0
  71. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_pixel.py +0 -0
  72. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_sanitize.py +0 -0
  73. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_save.py +0 -0
  74. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_str.py +0 -0
  75. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_style_flags.py +0 -0
  76. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_style_name.py +0 -0
  77. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_subset.py +0 -0
  78. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_support.py +0 -0
  79. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_svg.py +0 -0
  80. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_tables.py +0 -0
  81. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_unicode.py +0 -0
  82. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_update_unicode_data.py +0 -0
  83. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_variable.py +0 -0
  84. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_version.py +0 -0
  85. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_vertical_metrics.py +0 -0
  86. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_weight.py +0 -0
  87. {python_fontbro-0.30.0 → python_fontbro-0.31.0}/tests/test_width.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fontbro
3
- Version: 0.30.0
3
+ Version: 0.31.0
4
4
  Summary: friendly font operations on top of fontTools.
5
5
  Author-email: Fabio Caccamo <fabio.caccamo@gmail.com>
6
6
  Maintainer-email: Fabio Caccamo <fabio.caccamo@gmail.com>
@@ -149,6 +149,7 @@ with open("fonts/MyFont.ttf") as fh:
149
149
  - [`is_static`](#is_static)
150
150
  - [`is_variable`](#is_variable)
151
151
  - [`rename`](#rename)
152
+ - [`rename_variable_instances`](#rename_variable_instances)
152
153
  - [`sanitize`](#sanitize)
153
154
  - [`save`](#save)
154
155
  - [`save_as_woff`](#save_as_woff)
@@ -966,6 +967,8 @@ variable = font.is_variable()
966
967
  """
967
968
  Renames the font names records (1, 2, 4, 6, 16, 17) according to
968
969
  the given family_name and style_name (subfamily_name).
970
+ The names records (18, 25), the fvar named instances PostScript names
971
+ and the CFF table names are updated too, only if already defined.
969
972
 
970
973
  If family_name is not defined it will be auto-detected.
971
974
  If style_name is not defined it will be auto-detected.
@@ -985,6 +988,39 @@ font.rename(
985
988
  )
986
989
  ```
987
990
 
991
+ #### `rename_variable_instances`
992
+ ```python
993
+ """
994
+ Renames the named instances of the variable font.
995
+
996
+ The family_name replaces the family part of the instances PostScript names
997
+ (and of the variations PostScript name prefix, if defined).
998
+ The style_names replace the instances style names and the style part
999
+ of their PostScript names. The axes names are unchanged.
1000
+ The default instance style name must match the font style name,
1001
+ so renaming it renames the font style too.
1002
+
1003
+ If family_name is not defined it will be auto-detected.
1004
+
1005
+ :param family_name: The family name
1006
+ :type family_name: str
1007
+ :param style_names: The new style names mapped by the old ones (case-insensitive),
1008
+ eg. {"Thin": "Hairline"}
1009
+ :type style_names: dict or None
1010
+ :param update_style_flags: if True the style flags will be updated by subfamily name
1011
+ (only if the default instance style name is renamed)
1012
+ :type update_style_flags: bool
1013
+
1014
+ :raises OperationError: if the font is not a variable font.
1015
+ """
1016
+
1017
+ font.rename_variable_instances(
1018
+ family_name="My Font New",
1019
+ style_names={"Thin": "Hairline"},
1020
+ update_style_flags=True,
1021
+ )
1022
+ ```
1023
+
988
1024
  #### `sanitize`
989
1025
  ```python
990
1026
  """
@@ -1360,6 +1396,12 @@ If an axis min and max values are equal, the axis will be pinned.
1360
1396
  {'wdth':100, 'wght':[100,600], 'ital':[30,65,70]} or
1361
1397
  {'wdth':100, 'wght':{'min':100,'max':600}, 'ital':{'min':30,'default':65,'max':70}}
1362
1398
  :type coordinates: dict
1399
+ :param update_names: if True the name records will be updated based on
1400
+ the closest instance to the new default coordinates (only if changed)
1401
+ :type update_names: bool
1402
+ :param update_style_flags: if True the style flags will be updated based on
1403
+ the closest instance to the new default coordinates (only if changed)
1404
+ :type update_style_flags: bool
1363
1405
  :param options: The options for the fontTools.varLib.instancer
1364
1406
  :type options: dictionary
1365
1407
 
@@ -1368,7 +1410,12 @@ If an axis min and max values are equal, the axis will be pinned.
1368
1410
  :raises ValueError: If the coordinates axes are all pinned
1369
1411
  """
1370
1412
 
1371
- font.to_sliced_variable(coordinates, **options)
1413
+ font.to_sliced_variable(
1414
+ coordinates,
1415
+ update_names=True,
1416
+ update_style_flags=True,
1417
+ **options,
1418
+ )
1372
1419
  ```
1373
1420
 
1374
1421
  #### `to_static`
@@ -85,6 +85,7 @@ with open("fonts/MyFont.ttf") as fh:
85
85
  - [`is_static`](#is_static)
86
86
  - [`is_variable`](#is_variable)
87
87
  - [`rename`](#rename)
88
+ - [`rename_variable_instances`](#rename_variable_instances)
88
89
  - [`sanitize`](#sanitize)
89
90
  - [`save`](#save)
90
91
  - [`save_as_woff`](#save_as_woff)
@@ -902,6 +903,8 @@ variable = font.is_variable()
902
903
  """
903
904
  Renames the font names records (1, 2, 4, 6, 16, 17) according to
904
905
  the given family_name and style_name (subfamily_name).
906
+ The names records (18, 25), the fvar named instances PostScript names
907
+ and the CFF table names are updated too, only if already defined.
905
908
 
906
909
  If family_name is not defined it will be auto-detected.
907
910
  If style_name is not defined it will be auto-detected.
@@ -921,6 +924,39 @@ font.rename(
921
924
  )
922
925
  ```
923
926
 
927
+ #### `rename_variable_instances`
928
+ ```python
929
+ """
930
+ Renames the named instances of the variable font.
931
+
932
+ The family_name replaces the family part of the instances PostScript names
933
+ (and of the variations PostScript name prefix, if defined).
934
+ The style_names replace the instances style names and the style part
935
+ of their PostScript names. The axes names are unchanged.
936
+ The default instance style name must match the font style name,
937
+ so renaming it renames the font style too.
938
+
939
+ If family_name is not defined it will be auto-detected.
940
+
941
+ :param family_name: The family name
942
+ :type family_name: str
943
+ :param style_names: The new style names mapped by the old ones (case-insensitive),
944
+ eg. {"Thin": "Hairline"}
945
+ :type style_names: dict or None
946
+ :param update_style_flags: if True the style flags will be updated by subfamily name
947
+ (only if the default instance style name is renamed)
948
+ :type update_style_flags: bool
949
+
950
+ :raises OperationError: if the font is not a variable font.
951
+ """
952
+
953
+ font.rename_variable_instances(
954
+ family_name="My Font New",
955
+ style_names={"Thin": "Hairline"},
956
+ update_style_flags=True,
957
+ )
958
+ ```
959
+
924
960
  #### `sanitize`
925
961
  ```python
926
962
  """
@@ -1296,6 +1332,12 @@ If an axis min and max values are equal, the axis will be pinned.
1296
1332
  {'wdth':100, 'wght':[100,600], 'ital':[30,65,70]} or
1297
1333
  {'wdth':100, 'wght':{'min':100,'max':600}, 'ital':{'min':30,'default':65,'max':70}}
1298
1334
  :type coordinates: dict
1335
+ :param update_names: if True the name records will be updated based on
1336
+ the closest instance to the new default coordinates (only if changed)
1337
+ :type update_names: bool
1338
+ :param update_style_flags: if True the style flags will be updated based on
1339
+ the closest instance to the new default coordinates (only if changed)
1340
+ :type update_style_flags: bool
1299
1341
  :param options: The options for the fontTools.varLib.instancer
1300
1342
  :type options: dictionary
1301
1343
 
@@ -1304,7 +1346,12 @@ If an axis min and max values are equal, the axis will be pinned.
1304
1346
  :raises ValueError: If the coordinates axes are all pinned
1305
1347
  """
1306
1348
 
1307
- font.to_sliced_variable(coordinates, **options)
1349
+ font.to_sliced_variable(
1350
+ coordinates,
1351
+ update_names=True,
1352
+ update_style_flags=True,
1353
+ **options,
1354
+ )
1308
1355
  ```
1309
1356
 
1310
1357
  #### `to_static`
@@ -1227,6 +1227,8 @@ class Font:
1227
1227
  """
1228
1228
  Renames the font names records (1, 2, 4, 6, 16, 17) according to
1229
1229
  the given family_name and style_name (subfamily_name).
1230
+ The names records (18, 25), the fvar named instances PostScript names
1231
+ and the CFF table names are updated too, only if already defined.
1230
1232
 
1231
1233
  If family_name is not defined it will be auto-detected.
1232
1234
  If style_name is not defined it will be auto-detected.
@@ -1247,6 +1249,48 @@ class Font:
1247
1249
  if update_style_flags:
1248
1250
  self.set_style_flags_by_subfamily_name()
1249
1251
 
1252
+ def rename_variable_instances(
1253
+ self,
1254
+ *,
1255
+ family_name: str = "",
1256
+ style_names: dict[str, str] | None = None,
1257
+ update_style_flags: bool = True,
1258
+ ) -> None:
1259
+ """
1260
+ Renames the named instances of the variable font.
1261
+
1262
+ The family_name replaces the family part of the instances PostScript names
1263
+ (and of the variations PostScript name prefix, if defined).
1264
+ The style_names replace the instances style names and the style part
1265
+ of their PostScript names. The axes names are unchanged.
1266
+ The default instance style name must match the font style name,
1267
+ so renaming it renames the font style too.
1268
+
1269
+ If family_name is not defined it will be auto-detected.
1270
+
1271
+ :param family_name: The family name
1272
+ :type family_name: str
1273
+ :param style_names: The new style names mapped by the old ones (case-insensitive),
1274
+ eg. {"Thin": "Hairline"}
1275
+ :type style_names: dict or None
1276
+ :param update_style_flags: if True the style flags will be updated by subfamily name
1277
+ (only if the default instance style name is renamed)
1278
+ :type update_style_flags: bool
1279
+
1280
+ :raises OperationError: if the font is not a variable font.
1281
+ :return: None
1282
+ """
1283
+ ttfont = self.get_ttfont()
1284
+ style_name = self.get_style_name()
1285
+ _names.rename_variable_instances(
1286
+ ttfont,
1287
+ family_name=family_name,
1288
+ style_names=style_names,
1289
+ )
1290
+
1291
+ if update_style_flags and self.get_style_name() != style_name:
1292
+ self.set_style_flags_by_subfamily_name()
1293
+
1250
1294
  def sanitize(
1251
1295
  self,
1252
1296
  *,
@@ -1791,6 +1835,8 @@ class Font:
1791
1835
  self,
1792
1836
  *,
1793
1837
  coordinates: dict[str, Any],
1838
+ update_names: bool = True,
1839
+ update_style_flags: bool = True,
1794
1840
  **options: Any,
1795
1841
  ) -> None:
1796
1842
  """
@@ -1805,6 +1851,12 @@ class Font:
1805
1851
  {'wdth':100, 'wght':[100,600], 'ital':[30,65,70]} or
1806
1852
  {'wdth':100, 'wght':{'min':100,'max':600}, 'ital':{'min':30,'default':65,'max':70}}
1807
1853
  :type coordinates: dict
1854
+ :param update_names: if True the name records will be updated based on
1855
+ the closest instance to the new default coordinates (only if changed)
1856
+ :type update_names: bool
1857
+ :param update_style_flags: if True the style flags will be updated based on
1858
+ the closest instance to the new default coordinates (only if changed)
1859
+ :type update_style_flags: bool
1808
1860
  :param options: The options for the fontTools.varLib.instancer
1809
1861
  :type options: dictionary
1810
1862
 
@@ -1813,7 +1865,13 @@ class Font:
1813
1865
  :raises ValueError: If the coordinates axes are all pinned
1814
1866
  """
1815
1867
  ttfont = self.get_ttfont()
1816
- _variable.to_sliced_variable(ttfont, coordinates=coordinates, **options)
1868
+ _variable.to_sliced_variable(
1869
+ ttfont,
1870
+ coordinates=coordinates,
1871
+ update_names=update_names,
1872
+ update_style_flags=update_style_flags,
1873
+ **options,
1874
+ )
1817
1875
 
1818
1876
  def to_static(
1819
1877
  self,
@@ -4,4 +4,4 @@ __description__ = "friendly font operations on top of fontTools."
4
4
  __email__ = "fabio.caccamo@gmail.com"
5
5
  __license__ = "MIT"
6
6
  __title__ = "python-fontbro"
7
- __version__ = "0.30.0"
7
+ __version__ = "0.31.0"
@@ -6,7 +6,7 @@ from typing import Any
6
6
  from fontTools.misc.encodingTools import getEncoding
7
7
  from fontTools.ttLib import TTFont
8
8
 
9
- from fontbro.exceptions import ArgumentError
9
+ from fontbro.exceptions import ArgumentError, OperationError
10
10
  from fontbro.utils import concat_names, remove_spaces
11
11
 
12
12
  # Names:
@@ -182,26 +182,41 @@ def set_name(
182
182
  ttfont: TTFont,
183
183
  key: int | str,
184
184
  value: str,
185
+ *,
186
+ windows: bool = True,
187
+ mac: bool = True,
188
+ unicode: bool = True,
185
189
  ) -> None:
186
190
  """
187
- Sets the name by its identifier in the name table of the given font.
191
+ Sets the name by its identifier in the name table of the given font,
192
+ only in the records of the platforms enabled by the windows, mac
193
+ and unicode flags.
188
194
  """
189
195
  name_id = _get_name_id(key)
190
196
  name_table = ttfont["name"]
191
- # https://github.com/fonttools/fonttools/blob/main/Lib/fontTools/ttLib/tables/_n_a_m_e.py#L568
192
- # the windows record is always written, it's the one read first by get_name
193
- name_table.setName(value, name_id, **_NAMES_WIN_IDS)
197
+ if windows:
198
+ # https://github.com/fonttools/fonttools/blob/main/Lib/fontTools/ttLib/tables/_n_a_m_e.py#L568
199
+ # the windows record is always written, it's the one read first by get_name
200
+ name_table.setName(value, name_id, **_NAMES_WIN_IDS)
194
201
  # the other records of the same name id are updated only if the font
195
202
  # already has them (mac and unicode records are legacy, modern tools don't
196
203
  # produce them anymore, so they are never added): a record left with its
197
204
  # previous value would contradict the record just written
198
- for platform_ids in (_NAMES_WIN_IDS, _NAMES_UNI_IDS, _NAMES_MAC_IDS):
205
+ platforms = [
206
+ platform_ids
207
+ for platform_ids, enabled in (
208
+ (_NAMES_WIN_IDS, windows),
209
+ (_NAMES_UNI_IDS, unicode),
210
+ (_NAMES_MAC_IDS, mac),
211
+ )
212
+ if enabled
213
+ ]
214
+ for platform_ids in platforms:
199
215
  for record_ids in _get_name_records_ids(name_table, name_id, platform_ids):
200
216
  # the value is written only if the record encoding can encode it
201
217
  # (eg. mac roman can't encode greek, cyrillic, cjk), otherwise the
202
218
  # record is removed (as fontTools addMultilingualName does),
203
- # because the font would raise UnicodeEncodeError when saved;
204
- # removing it is safe because the windows record has just been written
219
+ # because the font would raise UnicodeEncodeError when saved
205
220
  if _is_encodable(value, record_ids):
206
221
  name_table.setName(value, name_id, **record_ids)
207
222
  else:
@@ -249,6 +264,185 @@ def get_style_name(
249
264
  )
250
265
 
251
266
 
267
+ def _get_postscript_name(
268
+ value: str,
269
+ ) -> str:
270
+ """
271
+ Gets a PostScript name from the given value,
272
+ replacing the characters not allowed with hyphens.
273
+ """
274
+ # keep only printable ASCII subset:
275
+ # https://learn.microsoft.com/en-us/typography/opentype/spec/name#name-ids
276
+ # postscript_name_allowed_chars = {chr(code) for code in range(33, 127)}
277
+ # !"#$&'*+,-.0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\^_`abcdefghijklmnopqrstuvwxyz|~
278
+ postscript_name_pattern = (
279
+ r"[^0-9A-Za-z\!\"\#\$\&\'\*\+\,\-\.\:\;\=\?\@\\\^\_\`\|\~]"
280
+ )
281
+ postscript_name = re.sub(postscript_name_pattern, "-", value)
282
+ postscript_name = re.sub(r"[\-]+", "-", postscript_name).strip("-")
283
+ return postscript_name
284
+
285
+
286
+ def _get_default_instance_style_name(
287
+ ttfont: TTFont,
288
+ ) -> str:
289
+ """
290
+ Gets the style name of the fvar named instance at the default coordinates.
291
+ """
292
+ fvar = ttfont["fvar"]
293
+ coordinates = {axis.axisTag: axis.defaultValue for axis in fvar.axes}
294
+ for instance in fvar.instances:
295
+ if instance.coordinates == coordinates:
296
+ return get_name(ttfont, instance.subfamilyNameID) or ""
297
+ return ""
298
+
299
+
300
+ def rename_variable_instances(
301
+ ttfont: TTFont,
302
+ *,
303
+ family_name: str = "",
304
+ style_names: dict[str, str] | None = None,
305
+ ) -> None:
306
+ """
307
+ Renames the named instances of the given variable font: the family_name
308
+ replaces the family part of the instances PostScript names (and of the
309
+ variations PostScript name prefix, 25), the style_names (old style name
310
+ to new style name) replace the instances style names and the style part
311
+ of their PostScript names. The axes names are unchanged.
312
+ The default instance style name must match the font style name,
313
+ so renaming it renames the font style too.
314
+ """
315
+ if "fvar" not in ttfont:
316
+ raise OperationError("Only the instances of a variable font can be renamed.")
317
+ # style names are matched case-insensitively
318
+ style_names = {key.lower(): value for key, value in (style_names or {}).items()}
319
+ default_style_name = _get_default_instance_style_name(ttfont).lower()
320
+ default_style_name_new = style_names.get(default_style_name, "")
321
+ if default_style_name_new:
322
+ rename(ttfont, style_name=default_style_name_new)
323
+ family_name = (family_name or "").strip() or get_family_name(ttfont)
324
+ _rename_variable_instances(ttfont, family_name, style_names)
325
+
326
+
327
+ def _get_instance_postscript_name(
328
+ prefix: str,
329
+ style_name: str,
330
+ ) -> str:
331
+ """
332
+ Gets the PostScript name of a variable font named instance, the "Italic"
333
+ style suffix is omitted if the prefix already ends with it
334
+ (eg. "TourneyItalic-Thin" instead of "TourneyItalic-ThinItalic").
335
+ """
336
+ style_name = remove_spaces(style_name)
337
+ if prefix.lower().endswith("italic"):
338
+ style_name = re.sub(r"italic$", "", style_name, flags=re.IGNORECASE)
339
+ style_name = style_name or "Regular"
340
+ return _get_postscript_name(f"{prefix}-{style_name}")
341
+
342
+
343
+ def _rename_variable_instances(
344
+ ttfont: TTFont,
345
+ family_name: str,
346
+ style_names: dict[str, str],
347
+ ) -> None:
348
+ """
349
+ Renames the named instances of the given variable font,
350
+ the style_names keys must be lowercase.
351
+ """
352
+ name_table = ttfont["name"]
353
+
354
+ # the old family name is read before updating it (rename calls this function
355
+ # before updating the name records), the old prefixes are computed from it
356
+ family_name_old = get_family_name(ttfont)
357
+ prefix_old = get_name(ttfont, NAME_VARIATIONS_POSTSCRIPT_NAME_PREFIX) or ""
358
+ if prefix_old:
359
+ # variations postscript name prefix: only ASCII letters and digits allowed,
360
+ # its suffix is kept (eg. "Roman", "Italic") to avoid the same instances
361
+ # postscript names in the roman and italic fonts
362
+ suffix = ""
363
+ family_name_old_prefix = re.sub(r"[^0-9A-Za-z]", "", family_name_old)
364
+ if family_name_old_prefix and prefix_old.startswith(family_name_old_prefix):
365
+ suffix = prefix_old[len(family_name_old_prefix) :]
366
+ elif "italic" in get_style_name(ttfont).lower() and not (
367
+ family_name.lower().endswith("italic")
368
+ ):
369
+ suffix = "Italic"
370
+ prefix = re.sub(r"[^0-9A-Za-z]", "", f"{family_name}{suffix}")
371
+ # the windows record is written only if the font already has it
372
+ name_id = _get_name_id(NAME_VARIATIONS_POSTSCRIPT_NAME_PREFIX)
373
+ windows = bool(name_table.getName(name_id, **_NAMES_WIN_IDS))
374
+ set_name(
375
+ ttfont, NAME_VARIATIONS_POSTSCRIPT_NAME_PREFIX, prefix, windows=windows
376
+ )
377
+ else:
378
+ prefix_old = _get_postscript_name(remove_spaces(family_name_old))
379
+ prefix = _get_postscript_name(remove_spaces(family_name))
380
+
381
+ # the style names are read before updating them,
382
+ # because the same name record can be shared by more instances
383
+ instances = ttfont["fvar"].instances
384
+ instances_style_names = [
385
+ get_name(ttfont, instance.subfamilyNameID) or "" for instance in instances
386
+ ]
387
+ for instance, style_name in zip(instances, instances_style_names, strict=True):
388
+ style_name_new = style_names.get(style_name.lower(), "")
389
+ if style_name_new and instance.subfamilyNameID < 256:
390
+ # name ids 2 and 17 are the font style names (updated by rename),
391
+ # so a new name record is added for the instance
392
+ instance.subfamilyNameID = name_table.addName(
393
+ style_name_new, platforms=((3, 1, 0x409),)
394
+ )
395
+ elif style_name_new:
396
+ # the existing name record is updated, also where it is shared
397
+ # (eg. the STAT table axis values), the windows record is written
398
+ # only if the font already has it
399
+ name_id = instance.subfamilyNameID
400
+ windows = bool(name_table.getName(name_id, **_NAMES_WIN_IDS))
401
+ set_name(ttfont, name_id, style_name_new, windows=windows)
402
+ name_id = instance.postscriptNameID
403
+ name = get_name(ttfont, name_id) if 255 < name_id < 0xFFFF else None
404
+ if not name:
405
+ continue
406
+ if not style_name_new and prefix_old and name.startswith(prefix_old):
407
+ # only the prefix is replaced, to keep the font naming convention
408
+ name = prefix + name[len(prefix_old) :]
409
+ else:
410
+ # the name is generated if the style is renamed,
411
+ # or if it doesn't start with the old prefix (inconsistent font)
412
+ name = _get_instance_postscript_name(prefix, style_name_new or style_name)
413
+ set_name(ttfont, name_id, name)
414
+
415
+
416
+ def _rename_cff(
417
+ ttfont: TTFont,
418
+ postscript_name: str,
419
+ full_name: str,
420
+ family_name: str,
421
+ weight: str,
422
+ ) -> None:
423
+ """
424
+ Renames the CFF table names (otf fonts): the font name and the FDArray
425
+ font names (CID-keyed fonts) prefixed by it, FullName, FamilyName and
426
+ Weight (only if not empty) only if already defined.
427
+ """
428
+ if "CFF " not in ttfont:
429
+ return
430
+ cff = ttfont["CFF "].cff
431
+ postscript_name_old = cff.fontNames[0]
432
+ cff.fontNames[0] = postscript_name
433
+ top_dict = cff.topDictIndex[0]
434
+ for font_dict in getattr(top_dict, "FDArray", []):
435
+ font_name = getattr(font_dict, "FontName", None)
436
+ if font_name and font_name.startswith(postscript_name_old):
437
+ font_dict.FontName = postscript_name + font_name[len(postscript_name_old) :]
438
+ if getattr(top_dict, "FullName", None):
439
+ top_dict.FullName = full_name
440
+ if getattr(top_dict, "FamilyName", None):
441
+ top_dict.FamilyName = family_name
442
+ if weight and getattr(top_dict, "Weight", None):
443
+ top_dict.Weight = weight
444
+
445
+
252
446
  def rename(
253
447
  ttfont: TTFont,
254
448
  *,
@@ -259,6 +453,8 @@ def rename(
259
453
  Renames the names records (1, 2, 3, 4, 6, 16, 17, 21, 22) of the given font
260
454
  according to the given family_name and style_name (subfamily_name),
261
455
  if not defined they will be auto-detected.
456
+ The records 18 and 25, the fvar instances PostScript names and the CFF
457
+ names are updated only if already defined.
262
458
  """
263
459
  family_name = (family_name or "").strip() or get_family_name(ttfont)
264
460
  style_name = (style_name or "").strip() or get_style_name(ttfont)
@@ -289,15 +485,7 @@ def rename(
289
485
  remove_spaces(typographic_subfamily_name),
290
486
  )
291
487
 
292
- # keep only printable ASCII subset:
293
- # https://learn.microsoft.com/en-us/typography/opentype/spec/name#name-ids
294
- # postscript_name_allowed_chars = {chr(code) for code in range(33, 127)}
295
- # !"#$&'*+,-.0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\^_`abcdefghijklmnopqrstuvwxyz|~
296
- postscript_name_pattern = (
297
- r"[^0-9A-Za-z\!\"\#\$\&\'\*\+\,\-\.\:\;\=\?\@\\\^\_\`\|\~]"
298
- )
299
- postscript_name = re.sub(postscript_name_pattern, "-", postscript_name)
300
- postscript_name = re.sub(r"[\-]+", "-", postscript_name).strip("-")
488
+ postscript_name = _get_postscript_name(postscript_name)
301
489
  postscript_name_length = len(postscript_name)
302
490
  if postscript_name_length > 63:
303
491
  raise ArgumentError(
@@ -325,4 +513,52 @@ def rename(
325
513
  NAME_WWS_FAMILY_NAME: wws_family_name,
326
514
  NAME_WWS_SUBFAMILY_NAME: wws_subfamily_name,
327
515
  }
516
+
517
+ # compatible full name: if it's not the full name (eg. a designer-defined
518
+ # short menu name), only the family name in it is replaced
519
+ compatible_full_name = get_name(ttfont, NAME_COMPATIBLE_FULL) or ""
520
+ if compatible_full_name:
521
+ family_name_old = get_family_name(ttfont)
522
+ full_name_old = concat_names(family_name_old, get_style_name(ttfont))
523
+ if (
524
+ compatible_full_name == full_name_old
525
+ or not family_name_old
526
+ or family_name_old not in compatible_full_name
527
+ ):
528
+ compatible_full_name = full_name
529
+ else:
530
+ compatible_full_name = compatible_full_name.replace(
531
+ family_name_old, typographic_family_name, 1
532
+ )
533
+
534
+ # cff weight (eg. "Bold" for "Bold Italic"), updated only if the style changes
535
+ style_name_old = get_style_name(ttfont)
536
+ weight = ""
537
+ if style_name_old != typographic_subfamily_name:
538
+ weight = re.sub(
539
+ r"\s*italic$", "", typographic_subfamily_name, flags=re.IGNORECASE
540
+ )
541
+ weight = weight or "Regular"
542
+
543
+ # fvar instances (and name id 25) need the old postscript name
544
+ if "fvar" in ttfont:
545
+ # the default instance style name must match the font style name, it's
546
+ # renamed only if it matches the old one (otherwise a family rename of
547
+ # an inconsistent font would rename it, and the STAT values sharing it)
548
+ style_names = {}
549
+ if (
550
+ style_name_old != typographic_subfamily_name
551
+ and _get_default_instance_style_name(ttfont) == style_name_old
552
+ ):
553
+ style_names[style_name_old.lower()] = typographic_subfamily_name
554
+ _rename_variable_instances(ttfont, typographic_family_name, style_names)
555
+
328
556
  set_names(ttfont, names)
557
+ # name id 18 (macintosh only) is updated only if already defined,
558
+ # the windows record is written only if the font already has it
559
+ if compatible_full_name:
560
+ name_id = _get_name_id(NAME_COMPATIBLE_FULL)
561
+ windows = bool(ttfont["name"].getName(name_id, **_NAMES_WIN_IDS))
562
+ set_name(ttfont, NAME_COMPATIBLE_FULL, compatible_full_name, windows=windows)
563
+
564
+ _rename_cff(ttfont, postscript_name, full_name, typographic_family_name, weight)
@@ -301,10 +301,18 @@ def _remove_stat_table(
301
301
  return False
302
302
 
303
303
 
304
+ def _get_default_coordinates(
305
+ ttfont: TTFont,
306
+ ) -> dict[str, float]:
307
+ return {axis.axisTag: axis.defaultValue for axis in ttfont["fvar"].axes}
308
+
309
+
304
310
  def to_sliced_variable(
305
311
  ttfont: TTFont,
306
312
  *,
307
313
  coordinates: dict[str, Any],
314
+ update_names: bool = True,
315
+ update_style_flags: bool = True,
308
316
  **options: Any,
309
317
  ) -> None:
310
318
  """
@@ -322,8 +330,25 @@ def to_sliced_variable(
322
330
  options.setdefault("updateFontNames", False)
323
331
 
324
332
  # instantiate the sliced variable font
333
+ default_coordinates = _get_default_coordinates(ttfont)
325
334
  instancer.instantiateVariableFont(ttfont, coordinates, inplace=True, **options)
326
335
 
336
+ # update name records and style flags if the default instance changed
337
+ # (unless they have been updated by fontTools using the STAT table)
338
+ default_coordinates_new = _get_default_coordinates(ttfont)
339
+ if (
340
+ update_names
341
+ and default_coordinates_new != default_coordinates
342
+ and not options["updateFontNames"]
343
+ ):
344
+ instance = get_variable_instance_closest_to_coordinates(
345
+ ttfont, default_coordinates_new
346
+ )
347
+ if instance:
348
+ _names.rename(ttfont, style_name=instance["style_name"])
349
+ if update_style_flags:
350
+ _style_flags.set_style_flags_by_subfamily_name(ttfont)
351
+
327
352
 
328
353
  def to_static(
329
354
  ttfont: TTFont,
@@ -363,6 +388,13 @@ def to_static(
363
388
  # not useful in static fonts and after instancing it may contain incorrect values
364
389
  _remove_stat_table(ttfont)
365
390
 
391
+ # remove variations postscript name prefix (25), used only by variable fonts
392
+ # (as fontTools does when instancing with updateFontNames=True)
393
+ # and the names used only by the removed fvar and STAT tables
394
+ if "name" in ttfont:
395
+ ttfont["name"].removeNames(nameID=25)
396
+ ttfont["name"].removeUnusedNames(ttfont)
397
+
366
398
  # update name records and style flags based on instance style name
367
399
  if instance and update_names:
368
400
  _names.rename(ttfont, style_name=instance["style_name"])