python-fontbro 0.28.0__tar.gz → 0.29.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 (88) hide show
  1. {python_fontbro-0.28.0/python_fontbro.egg-info → python_fontbro-0.29.0}/PKG-INFO +63 -4
  2. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/README.md +62 -3
  3. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/files.py +0 -11
  4. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/font.py +74 -3
  5. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/metadata.py +1 -1
  6. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/names.py +67 -18
  7. python_fontbro-0.29.0/fontbro/version.py +253 -0
  8. {python_fontbro-0.28.0 → python_fontbro-0.29.0/python_fontbro.egg-info}/PKG-INFO +63 -4
  9. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/python_fontbro.egg-info/SOURCES.txt +1 -0
  10. python_fontbro-0.29.0/tests/test_names.py +299 -0
  11. python_fontbro-0.29.0/tests/test_version.py +409 -0
  12. python_fontbro-0.28.0/tests/test_names.py +0 -128
  13. python_fontbro-0.28.0/tests/test_version.py +0 -35
  14. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/LICENSE.txt +0 -0
  15. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/MANIFEST.in +0 -0
  16. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/__init__.py +0 -0
  17. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/bitmap.py +0 -0
  18. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/color.py +0 -0
  19. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/data/family-classifications.json +0 -0
  20. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/data/features.json +0 -0
  21. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/data/unicode-blocks.json +0 -0
  22. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/data/unicode-scripts.json +0 -0
  23. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/embedding_permissions.py +0 -0
  24. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/exceptions.py +0 -0
  25. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/family_classification.py +0 -0
  26. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/features.py +0 -0
  27. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/fingerprint.py +0 -0
  28. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/flags.py +0 -0
  29. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/glyphs.py +0 -0
  30. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/math.py +0 -0
  31. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/metrics.py +0 -0
  32. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/monospace.py +0 -0
  33. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/pixel.py +0 -0
  34. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/py.typed +0 -0
  35. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/render.py +0 -0
  36. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/sanitize.py +0 -0
  37. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/style_flags.py +0 -0
  38. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/subset.py +0 -0
  39. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/support.py +0 -0
  40. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/tables.py +0 -0
  41. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/unicode.py +0 -0
  42. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/utils.py +0 -0
  43. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/fontbro/variable.py +0 -0
  44. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/pyproject.toml +0 -0
  45. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/python_fontbro.egg-info/dependency_links.txt +0 -0
  46. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/python_fontbro.egg-info/requires.txt +0 -0
  47. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/python_fontbro.egg-info/top_level.txt +0 -0
  48. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/setup.cfg +0 -0
  49. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/setup.py +0 -0
  50. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_bitmap.py +0 -0
  51. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_characters.py +0 -0
  52. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_clone.py +0 -0
  53. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_close.py +0 -0
  54. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_collection.py +0 -0
  55. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_color.py +0 -0
  56. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_context_manager.py +0 -0
  57. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_embedding_permissions.py +0 -0
  58. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_family_classification.py +0 -0
  59. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_family_name.py +0 -0
  60. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_features.py +0 -0
  61. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_filename.py +0 -0
  62. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_fingerprint.py +0 -0
  63. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_format.py +0 -0
  64. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_glyphs.py +0 -0
  65. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_image.py +0 -0
  66. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_init.py +0 -0
  67. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_instantiation.py +0 -0
  68. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_issues.py +0 -0
  69. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_italic_angle.py +0 -0
  70. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_monospace.py +0 -0
  71. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_pixel.py +0 -0
  72. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_rename.py +0 -0
  73. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_sanitize.py +0 -0
  74. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_save.py +0 -0
  75. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_str.py +0 -0
  76. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_style_flags.py +0 -0
  77. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_style_name.py +0 -0
  78. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_subset.py +0 -0
  79. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_support.py +0 -0
  80. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_svg.py +0 -0
  81. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_tables.py +0 -0
  82. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_unicode.py +0 -0
  83. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_unicode_blocks_and_scripts.py +0 -0
  84. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_update_unicode_data.py +0 -0
  85. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_variable.py +0 -0
  86. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_vertical_metrics.py +0 -0
  87. {python_fontbro-0.28.0 → python_fontbro-0.29.0}/tests/test_weight.py +0 -0
  88. {python_fontbro-0.28.0 → python_fontbro-0.29.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.28.0
3
+ Version: 0.29.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>
@@ -138,6 +138,7 @@ with open("fonts/MyFont.ttf") as fh:
138
138
  - [`get_variable_instance_by_style_name`](#get_variable_instance_by_style_name)
139
139
  - [`get_variable_instance_closest_to_coordinates`](#get_variable_instance_closest_to_coordinates)
140
140
  - [`get_version`](#get_version)
141
+ - [`get_version_formatted`](#get_version_formatted)
141
142
  - [`get_vertical_metrics`](#get_vertical_metrics)
142
143
  - [`get_weight`](#get_weight)
143
144
  - [`get_width`](#get_width)
@@ -163,6 +164,7 @@ with open("fonts/MyFont.ttf") as fh:
163
164
  - [`set_style_flags`](#set_style_flags)
164
165
  - [`set_style_flags_by_subfamily_name`](#set_style_flags_by_subfamily_name)
165
166
  - [`set_style_name`](#set_style_name)
167
+ - [`set_version`](#set_version)
166
168
  - [`set_vertical_metrics`](#set_vertical_metrics)
167
169
  - [`subset`](#subset)
168
170
  - [`to_sliced_variable`](#to_sliced_variable)
@@ -784,13 +786,48 @@ instance = font.get_variable_instance_closest_to_coordinates(
784
786
  #### `get_version`
785
787
  ```python
786
788
  """
787
- Gets the font version.
789
+ Gets the font version reading it from head.fontRevision,
790
+ with fallback on the version name record (name id 5) parsed value,
791
+ eg. "Version 1.015;git-0a5106e0b" -> 1.015.
788
792
 
789
- :returns: The font version value.
793
+ :param use_head_revision: If True, the head.fontRevision value is used (if set).
794
+ :type use_head_revision: bool
795
+ :param use_name_record: If True, the version name record value is used as fallback.
796
+ :type use_name_record: bool
797
+
798
+ :returns: The font version value, 0.0 if the version cannot be read.
790
799
  :rtype: float
800
+
801
+ :raises ArgumentError: If both options are False.
791
802
  """
792
803
 
793
- version = font.get_version()
804
+ version = font.get_version(use_head_revision=True, use_name_record=True)
805
+ ```
806
+
807
+ #### `get_version_formatted`
808
+ ```python
809
+ """
810
+ Gets the font version formatted using the version name record convention,
811
+ with the given prefix and a minimum of 3 decimal digits, eg. "v1.015".
812
+
813
+ :param use_head_revision: If True, the head.fontRevision value is used (if set).
814
+ :type use_head_revision: bool
815
+ :param use_name_record: If True, the version name record value is used as fallback.
816
+ :type use_name_record: bool
817
+ :param prefix: The prefix to prepend to the version value.
818
+ :type prefix: str
819
+
820
+ :returns: The formatted font version value, eg. "v0.000" if the version cannot be read.
821
+ :rtype: str
822
+
823
+ :raises ArgumentError: If both source options are False.
824
+ """
825
+
826
+ version = font.get_version_formatted(
827
+ use_head_revision=True,
828
+ use_name_record=True,
829
+ prefix="v",
830
+ )
794
831
  ```
795
832
 
796
833
  #### `get_vertical_metrics`
@@ -1240,6 +1277,28 @@ Sets the style name updating the related font names records.
1240
1277
  font.set_style_name(name="Bold Italic")
1241
1278
  ```
1242
1279
 
1280
+ #### `set_version`
1281
+ ```python
1282
+ """
1283
+ Sets the font version, the version value can be a number (eg. 1.015)
1284
+ or a version string (eg. "Version 1.015"), parsed the same way the
1285
+ version name record value is parsed when read.
1286
+ The head.fontRevision and the version name record (name id 5) are
1287
+ updated, the latter is overwritten with the canonical "Version X.YYY"
1288
+ form (any extra info it contained is dropped).
1289
+ The unique identifier name record (name id 3) is updated only if its
1290
+ first ";" separated part holds the current version value, and only in
1291
+ that part. The CFF version is updated only if the font already has it.
1292
+
1293
+ :param version: The version value, eg. 1.015 or "Version 1.015"
1294
+ :type version: float or str
1295
+
1296
+ :raises ArgumentError: If the version value is not valid.
1297
+ """
1298
+
1299
+ font.set_version(1.015)
1300
+ ```
1301
+
1243
1302
  #### `set_vertical_metrics`
1244
1303
  ```python
1245
1304
  """
@@ -74,6 +74,7 @@ with open("fonts/MyFont.ttf") as fh:
74
74
  - [`get_variable_instance_by_style_name`](#get_variable_instance_by_style_name)
75
75
  - [`get_variable_instance_closest_to_coordinates`](#get_variable_instance_closest_to_coordinates)
76
76
  - [`get_version`](#get_version)
77
+ - [`get_version_formatted`](#get_version_formatted)
77
78
  - [`get_vertical_metrics`](#get_vertical_metrics)
78
79
  - [`get_weight`](#get_weight)
79
80
  - [`get_width`](#get_width)
@@ -99,6 +100,7 @@ with open("fonts/MyFont.ttf") as fh:
99
100
  - [`set_style_flags`](#set_style_flags)
100
101
  - [`set_style_flags_by_subfamily_name`](#set_style_flags_by_subfamily_name)
101
102
  - [`set_style_name`](#set_style_name)
103
+ - [`set_version`](#set_version)
102
104
  - [`set_vertical_metrics`](#set_vertical_metrics)
103
105
  - [`subset`](#subset)
104
106
  - [`to_sliced_variable`](#to_sliced_variable)
@@ -720,13 +722,48 @@ instance = font.get_variable_instance_closest_to_coordinates(
720
722
  #### `get_version`
721
723
  ```python
722
724
  """
723
- Gets the font version.
725
+ Gets the font version reading it from head.fontRevision,
726
+ with fallback on the version name record (name id 5) parsed value,
727
+ eg. "Version 1.015;git-0a5106e0b" -> 1.015.
724
728
 
725
- :returns: The font version value.
729
+ :param use_head_revision: If True, the head.fontRevision value is used (if set).
730
+ :type use_head_revision: bool
731
+ :param use_name_record: If True, the version name record value is used as fallback.
732
+ :type use_name_record: bool
733
+
734
+ :returns: The font version value, 0.0 if the version cannot be read.
726
735
  :rtype: float
736
+
737
+ :raises ArgumentError: If both options are False.
727
738
  """
728
739
 
729
- version = font.get_version()
740
+ version = font.get_version(use_head_revision=True, use_name_record=True)
741
+ ```
742
+
743
+ #### `get_version_formatted`
744
+ ```python
745
+ """
746
+ Gets the font version formatted using the version name record convention,
747
+ with the given prefix and a minimum of 3 decimal digits, eg. "v1.015".
748
+
749
+ :param use_head_revision: If True, the head.fontRevision value is used (if set).
750
+ :type use_head_revision: bool
751
+ :param use_name_record: If True, the version name record value is used as fallback.
752
+ :type use_name_record: bool
753
+ :param prefix: The prefix to prepend to the version value.
754
+ :type prefix: str
755
+
756
+ :returns: The formatted font version value, eg. "v0.000" if the version cannot be read.
757
+ :rtype: str
758
+
759
+ :raises ArgumentError: If both source options are False.
760
+ """
761
+
762
+ version = font.get_version_formatted(
763
+ use_head_revision=True,
764
+ use_name_record=True,
765
+ prefix="v",
766
+ )
730
767
  ```
731
768
 
732
769
  #### `get_vertical_metrics`
@@ -1176,6 +1213,28 @@ Sets the style name updating the related font names records.
1176
1213
  font.set_style_name(name="Bold Italic")
1177
1214
  ```
1178
1215
 
1216
+ #### `set_version`
1217
+ ```python
1218
+ """
1219
+ Sets the font version, the version value can be a number (eg. 1.015)
1220
+ or a version string (eg. "Version 1.015"), parsed the same way the
1221
+ version name record value is parsed when read.
1222
+ The head.fontRevision and the version name record (name id 5) are
1223
+ updated, the latter is overwritten with the canonical "Version X.YYY"
1224
+ form (any extra info it contained is dropped).
1225
+ The unique identifier name record (name id 3) is updated only if its
1226
+ first ";" separated part holds the current version value, and only in
1227
+ that part. The CFF version is updated only if the font already has it.
1228
+
1229
+ :param version: The version value, eg. 1.015 or "Version 1.015"
1230
+ :type version: float or str
1231
+
1232
+ :raises ArgumentError: If the version value is not valid.
1233
+ """
1234
+
1235
+ font.set_version(1.015)
1236
+ ```
1237
+
1179
1238
  #### `set_vertical_metrics`
1180
1239
  ```python
1181
1240
  """
@@ -93,14 +93,3 @@ def get_filename(
93
93
  extension = get_format(ttfont)
94
94
  filename = f"{basename}.{extension}"
95
95
  return filename
96
-
97
-
98
- def get_version(
99
- ttfont: TTFont,
100
- ) -> float:
101
- """
102
- Gets the version of the given font (head.fontRevision).
103
- """
104
- head = ttfont.get("head")
105
- version = float(head.fontRevision)
106
- return version
@@ -29,6 +29,7 @@ from fontbro import support as _support
29
29
  from fontbro import tables as _tables
30
30
  from fontbro import unicode as _unicode
31
31
  from fontbro import variable as _variable
32
+ from fontbro import version as _version
32
33
  from fontbro.exceptions import (
33
34
  ArgumentError,
34
35
  OperationError,
@@ -1027,15 +1028,62 @@ class Font:
1027
1028
 
1028
1029
  def get_version(
1029
1030
  self,
1031
+ *,
1032
+ use_head_revision: bool = True,
1033
+ use_name_record: bool = True,
1030
1034
  ) -> float:
1031
1035
  """
1032
- Gets the font version.
1036
+ Gets the font version reading it from head.fontRevision,
1037
+ with fallback on the version name record (name id 5) parsed value,
1038
+ eg. "Version 1.015;git-0a5106e0b" -> 1.015.
1039
+
1040
+ :param use_head_revision: If True, the head.fontRevision value is used (if set).
1041
+ :type use_head_revision: bool
1042
+ :param use_name_record: If True, the version name record value is used as fallback.
1043
+ :type use_name_record: bool
1033
1044
 
1034
- :returns: The font version value.
1045
+ :returns: The font version value, 0.0 if the version cannot be read.
1035
1046
  :rtype: float
1047
+
1048
+ :raises ArgumentError: If both options are False.
1049
+ """
1050
+ ttfont = self.get_ttfont()
1051
+ return _version.get_version(
1052
+ ttfont,
1053
+ use_head_revision=use_head_revision,
1054
+ use_name_record=use_name_record,
1055
+ )
1056
+
1057
+ def get_version_formatted(
1058
+ self,
1059
+ *,
1060
+ use_head_revision: bool = True,
1061
+ use_name_record: bool = True,
1062
+ prefix: str = "v",
1063
+ ) -> str:
1064
+ """
1065
+ Gets the font version formatted using the version name record convention,
1066
+ with the given prefix and a minimum of 3 decimal digits, eg. "v1.015".
1067
+
1068
+ :param use_head_revision: If True, the head.fontRevision value is used (if set).
1069
+ :type use_head_revision: bool
1070
+ :param use_name_record: If True, the version name record value is used as fallback.
1071
+ :type use_name_record: bool
1072
+ :param prefix: The prefix to prepend to the version value.
1073
+ :type prefix: str
1074
+
1075
+ :returns: The formatted font version value, eg. "v0.000" if the version cannot be read.
1076
+ :rtype: str
1077
+
1078
+ :raises ArgumentError: If both source options are False.
1036
1079
  """
1037
1080
  ttfont = self.get_ttfont()
1038
- return _files.get_version(ttfont)
1081
+ return _version.get_version_formatted(
1082
+ ttfont,
1083
+ use_head_revision=use_head_revision,
1084
+ use_name_record=use_name_record,
1085
+ prefix=prefix,
1086
+ )
1039
1087
 
1040
1088
  def get_vertical_metrics(
1041
1089
  self,
@@ -1670,6 +1718,29 @@ class Font:
1670
1718
  style_name=name,
1671
1719
  )
1672
1720
 
1721
+ def set_version(
1722
+ self,
1723
+ version: float | str,
1724
+ ) -> None:
1725
+ """
1726
+ Sets the font version, the version value can be a number (eg. 1.015)
1727
+ or a version string (eg. "Version 1.015"), parsed the same way the
1728
+ version name record value is parsed when read.
1729
+ The head.fontRevision and the version name record (name id 5) are
1730
+ updated, the latter is overwritten with the canonical "Version X.YYY"
1731
+ form (any extra info it contained is dropped).
1732
+ The unique identifier name record (name id 3) is updated only if its
1733
+ first ";" separated part holds the current version value, and only in
1734
+ that part. The CFF version is updated only if the font already has it.
1735
+
1736
+ :param version: The version value, eg. 1.015 or "Version 1.015"
1737
+ :type version: float or str
1738
+
1739
+ :raises ArgumentError: If the version value is not valid.
1740
+ """
1741
+ ttfont = self.get_ttfont()
1742
+ _version.set_version(ttfont, version)
1743
+
1673
1744
  def set_vertical_metrics(
1674
1745
  self,
1675
1746
  **vertical_metrics: Any,
@@ -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.28.0"
7
+ __version__ = "0.29.0"
@@ -72,6 +72,8 @@ _NAMES_BY_KEY: dict[str, dict[str, Any]] = {item["key"]: item for item in _NAMES
72
72
  _NAMES_WIN_IDS: dict[str, Any] = {"platformID": 3, "platEncID": 1, "langID": 0x409}
73
73
  # macintosh: platform 1, encoding 0 (roman), language 0 (english)
74
74
  _NAMES_MAC_IDS: dict[str, Any] = {"platformID": 1, "platEncID": 0, "langID": 0x0}
75
+ # unicode: platform 0, encoding 3 (unicode bmp), language 0 (no language)
76
+ _NAMES_UNI_IDS: dict[str, Any] = {"platformID": 0, "platEncID": 3, "langID": 0x0}
75
77
 
76
78
 
77
79
  def _get_name_id(
@@ -101,6 +103,10 @@ def _is_encodable(
101
103
  platform_ids["platEncID"],
102
104
  platform_ids["langID"],
103
105
  )
106
+ if not encoding:
107
+ # the encoding of the name records is unknown (eg. the macintosh
108
+ # "uninterpreted" script), so the value cannot be written to them
109
+ return False
104
110
  try:
105
111
  value.encode(encoding)
106
112
  except UnicodeEncodeError:
@@ -108,6 +114,31 @@ def _is_encodable(
108
114
  return True
109
115
 
110
116
 
117
+ def _get_name_records_ids(
118
+ name_table: Any,
119
+ name_id: int,
120
+ platform_ids: dict[str, Any],
121
+ ) -> list[dict[str, Any]]:
122
+ """
123
+ Gets the ids of the existing name records with the given name id, matching
124
+ the platform and the language of the given platform ids. The encoding is
125
+ not matched: the same name id can have more than one record with different
126
+ encodings (eg. a windows symbol record and a windows unicode one).
127
+ Records in other languages are not matched, so they are never overwritten.
128
+ """
129
+ return [
130
+ {
131
+ "platformID": record.platformID,
132
+ "platEncID": record.platEncID,
133
+ "langID": record.langID,
134
+ }
135
+ for record in name_table.names
136
+ if record.nameID == name_id
137
+ and record.platformID == platform_ids["platformID"]
138
+ and record.langID == platform_ids["langID"]
139
+ ]
140
+
141
+
111
142
  def get_name(
112
143
  ttfont: TTFont,
113
144
  key: int | str,
@@ -116,11 +147,20 @@ def get_name(
116
147
  Gets the name by its identifier from the name table of the given font.
117
148
  """
118
149
  name_id = _get_name_id(key)
119
- name_table = ttfont["name"]
120
- name = name_table.getName(name_id, **_NAMES_WIN_IDS)
121
- if not name:
122
- name = name_table.getName(name_id, **_NAMES_MAC_IDS)
123
- return str(name.toUnicode()) if name else None
150
+ name_table = ttfont.get("name")
151
+ if not name_table:
152
+ return None
153
+ # the windows english record is preferred: mac records are legacy
154
+ # and they can follow different naming conventions (eg. name id 1)
155
+ name_record = name_table.getName(name_id, **_NAMES_WIN_IDS)
156
+ if name_record:
157
+ try:
158
+ return str(name_record.toUnicode())
159
+ except UnicodeDecodeError:
160
+ pass
161
+ # fallback to the english (mac) record or to any other decodable record
162
+ name: str | None = name_table.getDebugName(name_id)
163
+ return name
124
164
 
125
165
 
126
166
  def get_names(
@@ -129,12 +169,12 @@ def get_names(
129
169
  """
130
170
  Gets the names records of the given font mapped by their property name.
131
171
  """
132
- names_by_id = {record.nameID: f"{record}" for record in ttfont["name"].names}
133
- names = {
134
- _NAMES_BY_ID[name_id]["key"]: value
135
- for name_id, value in names_by_id.items()
136
- if name_id in _NAMES_BY_ID
137
- }
172
+ # read each name as get_name does, to get consistent values
173
+ names = {}
174
+ for name_id, name_item in _NAMES_BY_ID.items():
175
+ name = get_name(ttfont, name_id)
176
+ if name is not None:
177
+ names[name_item["key"]] = name
138
178
  return names
139
179
 
140
180
 
@@ -149,14 +189,23 @@ def set_name(
149
189
  name_id = _get_name_id(key)
150
190
  name_table = ttfont["name"]
151
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
152
193
  name_table.setName(value, name_id, **_NAMES_WIN_IDS)
153
- # the mac roman encoding can't encode many characters (eg. greek, cyrillic, cjk),
154
- # in that case the mac name record is removed (as fontTools addMultilingualName
155
- # does), otherwise the font would raise UnicodeEncodeError when saved
156
- if _is_encodable(value, _NAMES_MAC_IDS):
157
- name_table.setName(value, name_id, **_NAMES_MAC_IDS)
158
- else:
159
- name_table.removeNames(nameID=name_id, **_NAMES_MAC_IDS)
194
+ # the other records of the same name id are updated only if the font
195
+ # already has them (mac and unicode records are legacy, modern tools don't
196
+ # produce them anymore, so they are never added): a record left with its
197
+ # previous value would contradict the record just written
198
+ for platform_ids in (_NAMES_WIN_IDS, _NAMES_UNI_IDS, _NAMES_MAC_IDS):
199
+ for record_ids in _get_name_records_ids(name_table, name_id, platform_ids):
200
+ # the value is written only if the record encoding can encode it
201
+ # (eg. mac roman can't encode greek, cyrillic, cjk), otherwise the
202
+ # 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
205
+ if _is_encodable(value, record_ids):
206
+ name_table.setName(value, name_id, **record_ids)
207
+ else:
208
+ name_table.removeNames(nameID=name_id, **record_ids)
160
209
 
161
210
 
162
211
  def set_names(