fonttools 4.55.4__cp313-cp313-musllinux_1_2_aarch64.whl → 4.61.1__cp313-cp313-musllinux_1_2_aarch64.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.
Files changed (140) hide show
  1. fontTools/__init__.py +1 -1
  2. fontTools/annotations.py +30 -0
  3. fontTools/cffLib/CFF2ToCFF.py +65 -10
  4. fontTools/cffLib/__init__.py +61 -26
  5. fontTools/cffLib/specializer.py +4 -1
  6. fontTools/cffLib/transforms.py +11 -6
  7. fontTools/config/__init__.py +15 -0
  8. fontTools/cu2qu/cu2qu.c +6567 -5579
  9. fontTools/cu2qu/cu2qu.cpython-313-aarch64-linux-musl.so +0 -0
  10. fontTools/cu2qu/cu2qu.py +36 -4
  11. fontTools/cu2qu/ufo.py +14 -0
  12. fontTools/designspaceLib/__init__.py +8 -3
  13. fontTools/designspaceLib/statNames.py +14 -7
  14. fontTools/feaLib/ast.py +24 -15
  15. fontTools/feaLib/builder.py +139 -66
  16. fontTools/feaLib/error.py +1 -1
  17. fontTools/feaLib/lexer.c +7038 -7995
  18. fontTools/feaLib/lexer.cpython-313-aarch64-linux-musl.so +0 -0
  19. fontTools/feaLib/parser.py +75 -40
  20. fontTools/feaLib/variableScalar.py +6 -1
  21. fontTools/fontBuilder.py +50 -44
  22. fontTools/merge/__init__.py +1 -1
  23. fontTools/merge/cmap.py +33 -1
  24. fontTools/merge/tables.py +12 -1
  25. fontTools/misc/bezierTools.c +14913 -17013
  26. fontTools/misc/bezierTools.cpython-313-aarch64-linux-musl.so +0 -0
  27. fontTools/misc/bezierTools.py +4 -1
  28. fontTools/misc/configTools.py +3 -1
  29. fontTools/misc/enumTools.py +23 -0
  30. fontTools/misc/etree.py +4 -27
  31. fontTools/misc/filesystem/__init__.py +68 -0
  32. fontTools/misc/filesystem/_base.py +134 -0
  33. fontTools/misc/filesystem/_copy.py +45 -0
  34. fontTools/misc/filesystem/_errors.py +54 -0
  35. fontTools/misc/filesystem/_info.py +75 -0
  36. fontTools/misc/filesystem/_osfs.py +164 -0
  37. fontTools/misc/filesystem/_path.py +67 -0
  38. fontTools/misc/filesystem/_subfs.py +92 -0
  39. fontTools/misc/filesystem/_tempfs.py +34 -0
  40. fontTools/misc/filesystem/_tools.py +34 -0
  41. fontTools/misc/filesystem/_walk.py +55 -0
  42. fontTools/misc/filesystem/_zipfs.py +204 -0
  43. fontTools/misc/fixedTools.py +1 -1
  44. fontTools/misc/loggingTools.py +1 -1
  45. fontTools/misc/psCharStrings.py +17 -2
  46. fontTools/misc/sstruct.py +2 -6
  47. fontTools/misc/symfont.py +6 -8
  48. fontTools/misc/testTools.py +5 -1
  49. fontTools/misc/textTools.py +4 -2
  50. fontTools/misc/visitor.py +32 -16
  51. fontTools/misc/xmlWriter.py +44 -8
  52. fontTools/mtiLib/__init__.py +1 -3
  53. fontTools/otlLib/builder.py +402 -155
  54. fontTools/otlLib/optimize/gpos.py +49 -63
  55. fontTools/pens/filterPen.py +218 -26
  56. fontTools/pens/momentsPen.c +5514 -5584
  57. fontTools/pens/momentsPen.cpython-313-aarch64-linux-musl.so +0 -0
  58. fontTools/pens/pointPen.py +61 -18
  59. fontTools/pens/roundingPen.py +2 -2
  60. fontTools/pens/t2CharStringPen.py +31 -11
  61. fontTools/qu2cu/qu2cu.c +6581 -6168
  62. fontTools/qu2cu/qu2cu.cpython-313-aarch64-linux-musl.so +0 -0
  63. fontTools/subset/__init__.py +283 -25
  64. fontTools/subset/svg.py +2 -3
  65. fontTools/ttLib/__init__.py +4 -0
  66. fontTools/ttLib/__main__.py +47 -8
  67. fontTools/ttLib/removeOverlaps.py +7 -5
  68. fontTools/ttLib/reorderGlyphs.py +8 -7
  69. fontTools/ttLib/sfnt.py +11 -9
  70. fontTools/ttLib/tables/D__e_b_g.py +20 -2
  71. fontTools/ttLib/tables/G_V_A_R_.py +5 -0
  72. fontTools/ttLib/tables/S__i_l_f.py +2 -2
  73. fontTools/ttLib/tables/T_S_I__0.py +14 -3
  74. fontTools/ttLib/tables/T_S_I__1.py +2 -5
  75. fontTools/ttLib/tables/T_S_I__5.py +18 -7
  76. fontTools/ttLib/tables/__init__.py +1 -0
  77. fontTools/ttLib/tables/_a_v_a_r.py +12 -3
  78. fontTools/ttLib/tables/_c_m_a_p.py +20 -7
  79. fontTools/ttLib/tables/_c_v_t.py +3 -2
  80. fontTools/ttLib/tables/_f_p_g_m.py +3 -1
  81. fontTools/ttLib/tables/_g_l_y_f.py +45 -21
  82. fontTools/ttLib/tables/_g_v_a_r.py +67 -19
  83. fontTools/ttLib/tables/_h_d_m_x.py +4 -4
  84. fontTools/ttLib/tables/_h_m_t_x.py +7 -3
  85. fontTools/ttLib/tables/_l_o_c_a.py +2 -2
  86. fontTools/ttLib/tables/_n_a_m_e.py +11 -6
  87. fontTools/ttLib/tables/_p_o_s_t.py +9 -7
  88. fontTools/ttLib/tables/otBase.py +5 -12
  89. fontTools/ttLib/tables/otConverters.py +5 -2
  90. fontTools/ttLib/tables/otData.py +1 -1
  91. fontTools/ttLib/tables/otTables.py +33 -30
  92. fontTools/ttLib/tables/otTraverse.py +2 -1
  93. fontTools/ttLib/tables/sbixStrike.py +3 -3
  94. fontTools/ttLib/ttFont.py +666 -120
  95. fontTools/ttLib/ttGlyphSet.py +0 -10
  96. fontTools/ttLib/woff2.py +10 -13
  97. fontTools/ttx.py +13 -1
  98. fontTools/ufoLib/__init__.py +300 -202
  99. fontTools/ufoLib/converters.py +103 -30
  100. fontTools/ufoLib/errors.py +8 -0
  101. fontTools/ufoLib/etree.py +1 -1
  102. fontTools/ufoLib/filenames.py +171 -106
  103. fontTools/ufoLib/glifLib.py +303 -205
  104. fontTools/ufoLib/kerning.py +98 -48
  105. fontTools/ufoLib/utils.py +46 -15
  106. fontTools/ufoLib/validators.py +121 -99
  107. fontTools/unicodedata/Blocks.py +35 -20
  108. fontTools/unicodedata/Mirrored.py +446 -0
  109. fontTools/unicodedata/ScriptExtensions.py +63 -37
  110. fontTools/unicodedata/Scripts.py +173 -152
  111. fontTools/unicodedata/__init__.py +10 -2
  112. fontTools/varLib/__init__.py +198 -109
  113. fontTools/varLib/avar/__init__.py +0 -0
  114. fontTools/varLib/avar/__main__.py +72 -0
  115. fontTools/varLib/avar/build.py +79 -0
  116. fontTools/varLib/avar/map.py +108 -0
  117. fontTools/varLib/avar/plan.py +1004 -0
  118. fontTools/varLib/{avar.py → avar/unbuild.py} +70 -59
  119. fontTools/varLib/avarPlanner.py +3 -999
  120. fontTools/varLib/featureVars.py +21 -7
  121. fontTools/varLib/hvar.py +113 -0
  122. fontTools/varLib/instancer/__init__.py +180 -65
  123. fontTools/varLib/interpolatableHelpers.py +3 -0
  124. fontTools/varLib/iup.c +7564 -6903
  125. fontTools/varLib/iup.cpython-313-aarch64-linux-musl.so +0 -0
  126. fontTools/varLib/models.py +17 -2
  127. fontTools/varLib/mutator.py +11 -0
  128. fontTools/varLib/varStore.py +10 -38
  129. fontTools/voltLib/__main__.py +206 -0
  130. fontTools/voltLib/ast.py +4 -0
  131. fontTools/voltLib/parser.py +16 -8
  132. fontTools/voltLib/voltToFea.py +347 -166
  133. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/METADATA +269 -1410
  134. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/RECORD +318 -294
  135. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/WHEEL +1 -1
  136. fonttools-4.61.1.dist-info/licenses/LICENSE.external +388 -0
  137. {fonttools-4.55.4.data → fonttools-4.61.1.data}/data/share/man/man1/ttx.1 +0 -0
  138. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/entry_points.txt +0 -0
  139. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info/licenses}/LICENSE +0 -0
  140. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/top_level.txt +0 -0
@@ -275,10 +275,11 @@ def reorderGlyphs(font: ttLib.TTFont, new_glyph_order: List[str]):
275
275
  for reorder in _REORDER_RULES.get(reorder_key, []):
276
276
  reorder.apply(font, value)
277
277
 
278
- if "CFF " in font:
279
- cff_table = font["CFF "]
280
- charstrings = cff_table.cff.topDictIndex[0].CharStrings.charStrings
281
- cff_table.cff.topDictIndex[0].charset = new_glyph_order
282
- cff_table.cff.topDictIndex[0].CharStrings.charStrings = {
283
- k: charstrings.get(k) for k in new_glyph_order
284
- }
278
+ for tag in ["CFF ", "CFF2"]:
279
+ if tag in font:
280
+ cff_table = font[tag]
281
+ charstrings = cff_table.cff.topDictIndex[0].CharStrings.charStrings
282
+ cff_table.cff.topDictIndex[0].charset = new_glyph_order
283
+ cff_table.cff.topDictIndex[0].CharStrings.charStrings = {
284
+ k: charstrings.get(k) for k in new_glyph_order
285
+ }
fontTools/ttLib/sfnt.py CHANGED
@@ -13,6 +13,9 @@ classes, since whenever the number of tables changes or whenever
13
13
  a table's length changes you need to rewrite the whole file anyway.
14
14
  """
15
15
 
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import KeysView
16
19
  from io import BytesIO
17
20
  from types import SimpleNamespace
18
21
  from fontTools.misc.textTools import Tag
@@ -84,7 +87,7 @@ class SFNTReader(object):
84
87
 
85
88
  if self.sfntVersion not in ("\x00\x01\x00\x00", "OTTO", "true"):
86
89
  raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)")
87
- tables = {}
90
+ tables: dict[Tag, DirectoryEntry] = {}
88
91
  for i in range(self.numTables):
89
92
  entry = self.DirectoryEntry()
90
93
  entry.fromFile(self.file)
@@ -96,15 +99,15 @@ class SFNTReader(object):
96
99
  if self.flavor == "woff":
97
100
  self.flavorData = WOFFFlavorData(self)
98
101
 
99
- def has_key(self, tag):
102
+ def has_key(self, tag: str | bytes) -> bool:
100
103
  return tag in self.tables
101
104
 
102
105
  __contains__ = has_key
103
106
 
104
- def keys(self):
107
+ def keys(self) -> KeysView[Tag]:
105
108
  return self.tables.keys()
106
109
 
107
- def __getitem__(self, tag):
110
+ def __getitem__(self, tag: str | bytes) -> bytes:
108
111
  """Fetch the raw table data."""
109
112
  entry = self.tables[Tag(tag)]
110
113
  data = entry.loadData(self.file)
@@ -122,10 +125,10 @@ class SFNTReader(object):
122
125
  log.warning("bad checksum for '%s' table", tag)
123
126
  return data
124
127
 
125
- def __delitem__(self, tag):
128
+ def __delitem__(self, tag: str | bytes) -> None:
126
129
  del self.tables[Tag(tag)]
127
130
 
128
- def close(self):
131
+ def close(self) -> None:
129
132
  self.file.close()
130
133
 
131
134
  # We define custom __getstate__ and __setstate__ to make SFNTReader pickle-able
@@ -375,10 +378,9 @@ class SFNTWriter(object):
375
378
 
376
379
  def _calcMasterChecksum(self, directory):
377
380
  # calculate checkSumAdjustment
378
- tags = list(self.tables.keys())
379
381
  checksums = []
380
- for i in range(len(tags)):
381
- checksums.append(self.tables[tags[i]].checkSum)
382
+ for tag in self.tables.keys():
383
+ checksums.append(self.tables[tag].checkSum)
382
384
 
383
385
  if self.DirectoryEntry != SFNTDirectoryEntry:
384
386
  # Create a SFNT directory for checksum calculation purposes
@@ -1,9 +1,15 @@
1
1
  import json
2
+ from textwrap import indent
2
3
 
3
4
  from . import DefaultTable
5
+ from fontTools.misc.textTools import tostr
4
6
 
5
7
 
6
8
  class table_D__e_b_g(DefaultTable.DefaultTable):
9
+ def __init__(self, tag=None):
10
+ DefaultTable.DefaultTable.__init__(self, tag)
11
+ self.data = {}
12
+
7
13
  def decompile(self, data, ttFont):
8
14
  self.data = json.loads(data)
9
15
 
@@ -11,7 +17,19 @@ class table_D__e_b_g(DefaultTable.DefaultTable):
11
17
  return json.dumps(self.data).encode("utf-8")
12
18
 
13
19
  def toXML(self, writer, ttFont):
14
- writer.writecdata(json.dumps(self.data, indent=2))
20
+ # make sure json indentation inside CDATA block matches XMLWriter's
21
+ data = json.dumps(self.data, indent=len(writer.indentwhite))
22
+ prefix = tostr(writer.indentwhite) * (writer.indentlevel + 1)
23
+ # but don't indent the first json line so it's adjacent to `<![CDATA[{`
24
+ cdata = indent(data, prefix, lambda ln: ln != "{\n")
25
+
26
+ writer.begintag("json")
27
+ writer.newline()
28
+ writer.writecdata(cdata)
29
+ writer.newline()
30
+ writer.endtag("json")
31
+ writer.newline()
15
32
 
16
33
  def fromXML(self, name, attrs, content, ttFont):
17
- self.data = json.loads(content)
34
+ if name == "json":
35
+ self.data = json.loads("".join(content))
@@ -0,0 +1,5 @@
1
+ from ._g_v_a_r import table__g_v_a_r
2
+
3
+
4
+ class table_G_V_A_R_(table__g_v_a_r):
5
+ gid_size = 3
@@ -948,7 +948,7 @@ class Pass(object):
948
948
  writer.newline()
949
949
  writer.begintag("rules")
950
950
  writer.newline()
951
- for i in range(len(self.actions)):
951
+ for i, action in enumerate(self.actions):
952
952
  writer.begintag(
953
953
  "rule",
954
954
  index=i,
@@ -958,7 +958,7 @@ class Pass(object):
958
958
  writer.newline()
959
959
  if len(self.ruleConstraints[i]):
960
960
  writecode("constraint", writer, self.ruleConstraints[i])
961
- writecode("action", writer, self.actions[i])
961
+ writecode("action", writer, action)
962
962
  writer.endtag("rule")
963
963
  writer.newline()
964
964
  writer.endtag("rules")
@@ -1,4 +1,4 @@
1
- """ TSI{0,1,2,3,5} are private tables used by Microsoft Visual TrueType (VTT)
1
+ """TSI{0,1,2,3,5} are private tables used by Microsoft Visual TrueType (VTT)
2
2
  tool to store its hinting source data.
3
3
 
4
4
  TSI0 is the index table containing the lengths and offsets for the glyph
@@ -8,9 +8,13 @@ in the TSI1 table.
8
8
  See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables
9
9
  """
10
10
 
11
- from . import DefaultTable
11
+ import logging
12
12
  import struct
13
13
 
14
+ from . import DefaultTable
15
+
16
+ log = logging.getLogger(__name__)
17
+
14
18
  tsi0Format = ">HHL"
15
19
 
16
20
 
@@ -25,7 +29,14 @@ class table_T_S_I__0(DefaultTable.DefaultTable):
25
29
  numGlyphs = ttFont["maxp"].numGlyphs
26
30
  indices = []
27
31
  size = struct.calcsize(tsi0Format)
28
- for i in range(numGlyphs + 5):
32
+ numEntries = len(data) // size
33
+ if numEntries != numGlyphs + 5:
34
+ diff = numEntries - numGlyphs - 5
35
+ log.warning(
36
+ "Number of glyphPrograms differs from the number of glyphs in the font "
37
+ f"by {abs(diff)} ({numEntries - 5} programs vs. {numGlyphs} glyphs)."
38
+ )
39
+ for _ in range(numEntries):
29
40
  glyphID, textLength, textOffset = fixlongs(
30
41
  *struct.unpack(tsi0Format, data[:size])
31
42
  )
@@ -91,12 +91,11 @@ class table_T_S_I__1(LogMixin, DefaultTable.DefaultTable):
91
91
  glyphNames = ttFont.getGlyphOrder()
92
92
 
93
93
  indices = []
94
- for i in range(len(glyphNames)):
94
+ for i, name in enumerate(glyphNames):
95
95
  if len(data) % 2:
96
96
  data = (
97
97
  data + b"\015"
98
98
  ) # align on 2-byte boundaries, fill with return chars. Yum.
99
- name = glyphNames[i]
100
99
  if name in self.glyphPrograms:
101
100
  text = tobytes(self.glyphPrograms[name], encoding="utf-8")
102
101
  else:
@@ -108,13 +107,11 @@ class table_T_S_I__1(LogMixin, DefaultTable.DefaultTable):
108
107
  data = data + text
109
108
 
110
109
  extra_indices = []
111
- codes = sorted(self.extras.items())
112
- for i in range(len(codes)):
110
+ for code, name in sorted(self.extras.items()):
113
111
  if len(data) % 2:
114
112
  data = (
115
113
  data + b"\015"
116
114
  ) # align on 2-byte boundaries, fill with return chars.
117
- code, name = codes[i]
118
115
  if name in self.extraPrograms:
119
116
  text = tobytes(self.extraPrograms[name], encoding="utf-8")
120
117
  else:
@@ -1,4 +1,4 @@
1
- """ TSI{0,1,2,3,5} are private tables used by Microsoft Visual TrueType (VTT)
1
+ """TSI{0,1,2,3,5} are private tables used by Microsoft Visual TrueType (VTT)
2
2
  tool to store its hinting source data.
3
3
 
4
4
  TSI5 contains the VTT character groups.
@@ -6,29 +6,40 @@ TSI5 contains the VTT character groups.
6
6
  See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables
7
7
  """
8
8
 
9
+ import array
10
+ import logging
11
+ import sys
12
+
9
13
  from fontTools.misc.textTools import safeEval
14
+
10
15
  from . import DefaultTable
11
- import sys
12
- import array
16
+
17
+ log = logging.getLogger(__name__)
13
18
 
14
19
 
15
20
  class table_T_S_I__5(DefaultTable.DefaultTable):
16
21
  def decompile(self, data, ttFont):
17
22
  numGlyphs = ttFont["maxp"].numGlyphs
18
- assert len(data) == 2 * numGlyphs
19
23
  a = array.array("H")
20
24
  a.frombytes(data)
21
25
  if sys.byteorder != "big":
22
26
  a.byteswap()
23
27
  self.glyphGrouping = {}
24
- for i in range(numGlyphs):
28
+ numEntries = len(data) // 2
29
+ if numEntries != numGlyphs:
30
+ diff = numEntries - numGlyphs
31
+ log.warning(
32
+ "Number of entries differs from the number of glyphs in the font "
33
+ f"by {abs(diff)} ({numEntries} entries vs. {numGlyphs} glyphs)."
34
+ )
35
+ for i in range(numEntries):
25
36
  self.glyphGrouping[ttFont.getGlyphName(i)] = a[i]
26
37
 
27
38
  def compile(self, ttFont):
28
39
  glyphNames = ttFont.getGlyphOrder()
29
40
  a = array.array("H")
30
- for i in range(len(glyphNames)):
31
- a.append(self.glyphGrouping.get(glyphNames[i], 0))
41
+ for glyphName in glyphNames:
42
+ a.append(self.glyphGrouping.get(glyphName, 0))
32
43
  if sys.byteorder != "big":
33
44
  a.byteswap()
34
45
  return a.tobytes()
@@ -23,6 +23,7 @@ def _moduleFinderHint():
23
23
  from . import G_P_K_G_
24
24
  from . import G_P_O_S_
25
25
  from . import G_S_U_B_
26
+ from . import G_V_A_R_
26
27
  from . import G__l_a_t
27
28
  from . import G__l_o_c
28
29
  from . import H_V_A_R_
@@ -1,3 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Mapping, TYPE_CHECKING
1
4
  from fontTools.misc import sstruct
2
5
  from fontTools.misc.fixedTools import (
3
6
  fixedToFloat as fi2fl,
@@ -20,6 +23,9 @@ log = logging.getLogger(__name__)
20
23
 
21
24
  from .otBase import BaseTTXConverter
22
25
 
26
+ if TYPE_CHECKING:
27
+ from ..ttFont import TTFont
28
+
23
29
 
24
30
  class table__a_v_a_r(BaseTTXConverter):
25
31
  """Axis Variations table
@@ -143,8 +149,9 @@ class table__a_v_a_r(BaseTTXConverter):
143
149
  else:
144
150
  super().fromXML(name, attrs, content, ttFont)
145
151
 
146
- def renormalizeLocation(self, location, font):
147
-
152
+ def renormalizeLocation(
153
+ self, location: Mapping[str, float], font: TTFont, dropZeroes: bool = True
154
+ ) -> dict[str, float]:
148
155
  majorVersion = getattr(self, "majorVersion", 1)
149
156
 
150
157
  if majorVersion not in (1, 2):
@@ -185,7 +192,9 @@ class table__a_v_a_r(BaseTTXConverter):
185
192
  out.append(v)
186
193
 
187
194
  mappedLocation = {
188
- axis.axisTag: fi2fl(v, 14) for v, axis in zip(out, axes) if v != 0
195
+ axis.axisTag: fi2fl(v, 14)
196
+ for v, axis in zip(out, axes)
197
+ if v != 0 or not dropZeroes
189
198
  }
190
199
 
191
200
  return mappedLocation
@@ -141,6 +141,17 @@ class table__c_m_a_p(DefaultTable.DefaultTable):
141
141
  result.setdefault(name, set()).add(codepoint)
142
142
  return result
143
143
 
144
+ def buildReversedMin(self):
145
+ result = {}
146
+ for subtable in self.tables:
147
+ if subtable.isUnicode():
148
+ for codepoint, name in subtable.cmap.items():
149
+ if name in result:
150
+ result[name] = min(result[name], codepoint)
151
+ else:
152
+ result[name] = codepoint
153
+ return result
154
+
144
155
  def decompile(self, data, ttFont):
145
156
  tableVersion, numSubTables = struct.unpack(">HH", data[:4])
146
157
  self.tableVersion = int(tableVersion)
@@ -387,7 +398,7 @@ class cmap_format_0(CmapSubtable):
387
398
  assert 262 == self.length, "Format 0 cmap subtable not 262 bytes"
388
399
  gids = array.array("B")
389
400
  gids.frombytes(self.data)
390
- charCodes = list(range(len(gids)))
401
+ charCodes = range(len(gids))
391
402
  self.cmap = _make_map(self.ttFont, charCodes, gids)
392
403
 
393
404
  def compile(self, ttFont):
@@ -1162,13 +1173,15 @@ class cmap_format_12_or_13(CmapSubtable):
1162
1173
  charCodes = []
1163
1174
  gids = []
1164
1175
  pos = 0
1176
+ groups = array.array("I", data[: self.nGroups * 12])
1177
+ if sys.byteorder != "big":
1178
+ groups.byteswap()
1165
1179
  for i in range(self.nGroups):
1166
- startCharCode, endCharCode, glyphID = struct.unpack(
1167
- ">LLL", data[pos : pos + 12]
1168
- )
1169
- pos += 12
1180
+ startCharCode = groups[i * 3]
1181
+ endCharCode = groups[i * 3 + 1]
1182
+ glyphID = groups[i * 3 + 2]
1170
1183
  lenGroup = 1 + endCharCode - startCharCode
1171
- charCodes.extend(list(range(startCharCode, endCharCode + 1)))
1184
+ charCodes.extend(range(startCharCode, endCharCode + 1))
1172
1185
  gids.extend(self._computeGIDs(glyphID, lenGroup))
1173
1186
  self.data = data = None
1174
1187
  self.cmap = _make_map(self.ttFont, charCodes, gids)
@@ -1299,7 +1312,7 @@ class cmap_format_12(cmap_format_12_or_13):
1299
1312
  cmap_format_12_or_13.__init__(self, format)
1300
1313
 
1301
1314
  def _computeGIDs(self, startingGlyph, numberOfGlyphs):
1302
- return list(range(startingGlyph, startingGlyph + numberOfGlyphs))
1315
+ return range(startingGlyph, startingGlyph + numberOfGlyphs)
1303
1316
 
1304
1317
  def _IsInSameRun(self, glyphID, lastGlyphID, charCode, lastCharCode):
1305
1318
  return (glyphID == 1 + lastGlyphID) and (charCode == 1 + lastCharCode)
@@ -21,14 +21,15 @@ class table__c_v_t(DefaultTable.DefaultTable):
21
21
  self.values = values
22
22
 
23
23
  def compile(self, ttFont):
24
+ if not hasattr(self, "values"):
25
+ return b""
24
26
  values = self.values[:]
25
27
  if sys.byteorder != "big":
26
28
  values.byteswap()
27
29
  return values.tobytes()
28
30
 
29
31
  def toXML(self, writer, ttFont):
30
- for i in range(len(self.values)):
31
- value = self.values[i]
32
+ for i, value in enumerate(self.values):
32
33
  writer.simpletag("cv", value=value, index=i)
33
34
  writer.newline()
34
35
 
@@ -20,7 +20,9 @@ class table__f_p_g_m(DefaultTable.DefaultTable):
20
20
  self.program = program
21
21
 
22
22
  def compile(self, ttFont):
23
- return self.program.getBytecode()
23
+ if hasattr(self, "program"):
24
+ return self.program.getBytecode()
25
+ return b""
24
26
 
25
27
  def toXML(self, writer, ttFont):
26
28
  self.program.toXML(writer, ttFont)
@@ -134,6 +134,8 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
134
134
  glyph.expand(self)
135
135
 
136
136
  def compile(self, ttFont):
137
+ optimizeSpeed = ttFont.cfg[ttLib.OPTIMIZE_FONT_SPEED]
138
+
137
139
  self.axisTags = (
138
140
  [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else []
139
141
  )
@@ -148,7 +150,12 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
148
150
  boundsDone = set()
149
151
  for glyphName in self.glyphOrder:
150
152
  glyph = self.glyphs[glyphName]
151
- glyphData = glyph.compile(self, recalcBBoxes, boundsDone=boundsDone)
153
+ glyphData = glyph.compile(
154
+ self,
155
+ recalcBBoxes,
156
+ boundsDone=boundsDone,
157
+ optimizeSize=not optimizeSpeed,
158
+ )
152
159
  if padding > 1:
153
160
  glyphData = pad(glyphData, size=padding)
154
161
  locations.append(currentLocation)
@@ -714,7 +721,7 @@ class Glyph(object):
714
721
  self.decompileCoordinates(data)
715
722
 
716
723
  def compile(
717
- self, glyfTable, recalcBBoxes=True, *, boundsDone=None, optimizeSize=None
724
+ self, glyfTable, recalcBBoxes=True, *, boundsDone=None, optimizeSize=True
718
725
  ):
719
726
  if hasattr(self, "data"):
720
727
  if recalcBBoxes:
@@ -732,8 +739,6 @@ class Glyph(object):
732
739
  if self.isComposite():
733
740
  data = data + self.compileComponents(glyfTable)
734
741
  else:
735
- if optimizeSize is None:
736
- optimizeSize = getattr(glyfTable, "optimizeSize", True)
737
742
  data = data + self.compileCoordinates(optimizeSize=optimizeSize)
738
743
  return data
739
744
 
@@ -969,11 +974,10 @@ class Glyph(object):
969
974
  lastcomponent = len(self.components) - 1
970
975
  more = 1
971
976
  haveInstructions = 0
972
- for i in range(len(self.components)):
977
+ for i, compo in enumerate(self.components):
973
978
  if i == lastcomponent:
974
979
  haveInstructions = hasattr(self, "program")
975
980
  more = 0
976
- compo = self.components[i]
977
981
  data = data + compo.compile(more, haveInstructions, glyfTable)
978
982
  if haveInstructions:
979
983
  instructions = self.program.getBytecode()
@@ -1187,7 +1191,7 @@ class Glyph(object):
1187
1191
  ):
1188
1192
  return
1189
1193
  try:
1190
- coords, endPts, flags = self.getCoordinates(glyfTable)
1194
+ coords, endPts, flags = self.getCoordinates(glyfTable, round=otRound)
1191
1195
  self.xMin, self.yMin, self.xMax, self.yMax = coords.calcIntBounds()
1192
1196
  except NotImplementedError:
1193
1197
  pass
@@ -1206,9 +1210,7 @@ class Glyph(object):
1206
1210
  Return True if bounds were calculated, False otherwise.
1207
1211
  """
1208
1212
  for compo in self.components:
1209
- if hasattr(compo, "firstPt") or hasattr(compo, "transform"):
1210
- return False
1211
- if not float(compo.x).is_integer() or not float(compo.y).is_integer():
1213
+ if not compo._hasOnlyIntegerTranslate():
1212
1214
  return False
1213
1215
 
1214
1216
  # All components are untransformed and have an integer x/y translate
@@ -1222,7 +1224,7 @@ class Glyph(object):
1222
1224
  if boundsDone is not None:
1223
1225
  boundsDone.add(glyphName)
1224
1226
  # empty components shouldn't update the bounds of the parent glyph
1225
- if g.numberOfContours == 0:
1227
+ if g.yMin == g.yMax and g.xMin == g.xMax:
1226
1228
  continue
1227
1229
 
1228
1230
  x, y = compo.x, compo.y
@@ -1241,7 +1243,7 @@ class Glyph(object):
1241
1243
  else:
1242
1244
  return self.numberOfContours == -1
1243
1245
 
1244
- def getCoordinates(self, glyfTable):
1246
+ def getCoordinates(self, glyfTable, *, round=noRound):
1245
1247
  """Return the coordinates, end points and flags
1246
1248
 
1247
1249
  This method returns three values: A :py:class:`GlyphCoordinates` object,
@@ -1267,13 +1269,23 @@ class Glyph(object):
1267
1269
  for compo in self.components:
1268
1270
  g = glyfTable[compo.glyphName]
1269
1271
  try:
1270
- coordinates, endPts, flags = g.getCoordinates(glyfTable)
1272
+ coordinates, endPts, flags = g.getCoordinates(
1273
+ glyfTable, round=round
1274
+ )
1271
1275
  except RecursionError:
1272
1276
  raise ttLib.TTLibError(
1273
1277
  "glyph '%s' contains a recursive component reference"
1274
1278
  % compo.glyphName
1275
1279
  )
1276
1280
  coordinates = GlyphCoordinates(coordinates)
1281
+ # if asked to round e.g. while computing bboxes, it's important we
1282
+ # do it immediately before a component transform is applied to a
1283
+ # simple glyph's coordinates in case these might still contain floats;
1284
+ # however, if the referenced component glyph is another composite, we
1285
+ # must not round here but only at the end, after all the nested
1286
+ # transforms have been applied, or else rounding errors will compound.
1287
+ if round is not noRound and g.numberOfContours > 0:
1288
+ coordinates.toInt(round=round)
1277
1289
  if hasattr(compo, "firstPt"):
1278
1290
  # component uses two reference points: we apply the transform _before_
1279
1291
  # computing the offset between the points
@@ -1930,6 +1942,18 @@ class GlyphComponent(object):
1930
1942
  result = self.__eq__(other)
1931
1943
  return result if result is NotImplemented else not result
1932
1944
 
1945
+ def _hasOnlyIntegerTranslate(self):
1946
+ """Return True if it's a 'simple' component.
1947
+
1948
+ That is, it has no anchor points and no transform other than integer translate.
1949
+ """
1950
+ return (
1951
+ not hasattr(self, "firstPt")
1952
+ and not hasattr(self, "transform")
1953
+ and float(self.x).is_integer()
1954
+ and float(self.y).is_integer()
1955
+ )
1956
+
1933
1957
 
1934
1958
  class GlyphCoordinates(object):
1935
1959
  """A list of glyph coordinates.
@@ -2012,8 +2036,8 @@ class GlyphCoordinates(object):
2012
2036
  if round is noRound:
2013
2037
  return
2014
2038
  a = self._a
2015
- for i in range(len(a)):
2016
- a[i] = round(a[i])
2039
+ for i, value in enumerate(a):
2040
+ a[i] = round(value)
2017
2041
 
2018
2042
  def calcBounds(self):
2019
2043
  a = self._a
@@ -2143,8 +2167,8 @@ class GlyphCoordinates(object):
2143
2167
  """
2144
2168
  r = self.copy()
2145
2169
  a = r._a
2146
- for i in range(len(a)):
2147
- a[i] = -a[i]
2170
+ for i, value in enumerate(a):
2171
+ a[i] = -value
2148
2172
  return r
2149
2173
 
2150
2174
  def __round__(self, *, round=otRound):
@@ -2189,8 +2213,8 @@ class GlyphCoordinates(object):
2189
2213
  other = other._a
2190
2214
  a = self._a
2191
2215
  assert len(a) == len(other)
2192
- for i in range(len(a)):
2193
- a[i] += other[i]
2216
+ for i, value in enumerate(other):
2217
+ a[i] += value
2194
2218
  return self
2195
2219
  return NotImplemented
2196
2220
 
@@ -2213,8 +2237,8 @@ class GlyphCoordinates(object):
2213
2237
  other = other._a
2214
2238
  a = self._a
2215
2239
  assert len(a) == len(other)
2216
- for i in range(len(a)):
2217
- a[i] -= other[i]
2240
+ for i, value in enumerate(other):
2241
+ a[i] -= value
2218
2242
  return self
2219
2243
  return NotImplemented
2220
2244