fonttools 4.57.0__py3-none-any.whl → 4.58.1__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.

Potentially problematic release.


This version of fonttools might be problematic. Click here for more details.

Files changed (55) hide show
  1. fontTools/__init__.py +1 -1
  2. fontTools/cffLib/__init__.py +61 -26
  3. fontTools/cffLib/specializer.py +4 -1
  4. fontTools/designspaceLib/statNames.py +14 -7
  5. fontTools/feaLib/ast.py +12 -9
  6. fontTools/feaLib/builder.py +75 -49
  7. fontTools/feaLib/parser.py +1 -39
  8. fontTools/fontBuilder.py +6 -0
  9. fontTools/merge/cmap.py +33 -1
  10. fontTools/merge/tables.py +12 -1
  11. fontTools/misc/etree.py +4 -27
  12. fontTools/misc/loggingTools.py +1 -1
  13. fontTools/misc/symfont.py +6 -8
  14. fontTools/mtiLib/__init__.py +1 -3
  15. fontTools/otlLib/builder.py +359 -145
  16. fontTools/otlLib/optimize/gpos.py +42 -62
  17. fontTools/pens/pointPen.py +21 -12
  18. fontTools/pens/t2CharStringPen.py +31 -11
  19. fontTools/subset/__init__.py +12 -1
  20. fontTools/ttLib/tables/G_V_A_R_.py +5 -0
  21. fontTools/ttLib/tables/T_S_I__0.py +14 -3
  22. fontTools/ttLib/tables/T_S_I__5.py +16 -5
  23. fontTools/ttLib/tables/__init__.py +1 -0
  24. fontTools/ttLib/tables/_c_v_t.py +2 -0
  25. fontTools/ttLib/tables/_f_p_g_m.py +3 -1
  26. fontTools/ttLib/tables/_g_l_y_f.py +2 -6
  27. fontTools/ttLib/tables/_g_v_a_r.py +58 -15
  28. fontTools/ttLib/tables/_p_o_s_t.py +5 -2
  29. fontTools/ttLib/tables/otBase.py +1 -0
  30. fontTools/ufoLib/__init__.py +3 -3
  31. fontTools/ufoLib/converters.py +89 -25
  32. fontTools/ufoLib/errors.py +8 -0
  33. fontTools/ufoLib/etree.py +1 -1
  34. fontTools/ufoLib/filenames.py +155 -100
  35. fontTools/ufoLib/glifLib.py +9 -2
  36. fontTools/ufoLib/kerning.py +66 -36
  37. fontTools/ufoLib/utils.py +5 -2
  38. fontTools/unicodedata/Mirrored.py +446 -0
  39. fontTools/unicodedata/__init__.py +6 -2
  40. fontTools/varLib/__init__.py +20 -6
  41. fontTools/varLib/featureVars.py +13 -7
  42. fontTools/varLib/hvar.py +1 -1
  43. fontTools/varLib/instancer/__init__.py +14 -5
  44. fontTools/voltLib/__main__.py +206 -0
  45. fontTools/voltLib/ast.py +4 -0
  46. fontTools/voltLib/parser.py +16 -8
  47. fontTools/voltLib/voltToFea.py +347 -166
  48. {fonttools-4.57.0.dist-info → fonttools-4.58.1.dist-info}/METADATA +64 -11
  49. {fonttools-4.57.0.dist-info → fonttools-4.58.1.dist-info}/RECORD +55 -51
  50. {fonttools-4.57.0.dist-info → fonttools-4.58.1.dist-info}/WHEEL +1 -1
  51. fonttools-4.58.1.dist-info/licenses/LICENSE.external +359 -0
  52. {fonttools-4.57.0.data → fonttools-4.58.1.data}/data/share/man/man1/ttx.1 +0 -0
  53. {fonttools-4.57.0.dist-info → fonttools-4.58.1.dist-info}/entry_points.txt +0 -0
  54. {fonttools-4.57.0.dist-info → fonttools-4.58.1.dist-info}/licenses/LICENSE +0 -0
  55. {fonttools-4.57.0.dist-info → fonttools-4.58.1.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,8 @@
1
1
  import logging
2
2
  import os
3
3
  from collections import defaultdict, namedtuple
4
- from functools import reduce
4
+ from dataclasses import dataclass
5
+ from functools import cached_property, reduce
5
6
  from itertools import chain
6
7
  from math import log2
7
8
  from typing import DefaultDict, Dict, Iterable, List, Sequence, Tuple
@@ -192,79 +193,58 @@ ClusteringContext = namedtuple(
192
193
  )
193
194
 
194
195
 
196
+ @dataclass
195
197
  class Cluster:
196
- # TODO(Python 3.7): Turn this into a dataclass
197
- # ctx: ClusteringContext
198
- # indices: int
199
- # Caches
200
- # TODO(Python 3.8): use functools.cached_property instead of the
201
- # manually cached properties, and remove the cache fields listed below.
202
- # _indices: Optional[List[int]] = None
203
- # _column_indices: Optional[List[int]] = None
204
- # _cost: Optional[int] = None
205
-
206
- __slots__ = "ctx", "indices_bitmask", "_indices", "_column_indices", "_cost"
207
-
208
- def __init__(self, ctx: ClusteringContext, indices_bitmask: int):
209
- self.ctx = ctx
210
- self.indices_bitmask = indices_bitmask
211
- self._indices = None
212
- self._column_indices = None
213
- self._cost = None
198
+ ctx: ClusteringContext
199
+ indices_bitmask: int
214
200
 
215
- @property
201
+ @cached_property
216
202
  def indices(self):
217
- if self._indices is None:
218
- self._indices = bit_indices(self.indices_bitmask)
219
- return self._indices
203
+ return bit_indices(self.indices_bitmask)
220
204
 
221
- @property
205
+ @cached_property
222
206
  def column_indices(self):
223
- if self._column_indices is None:
224
- # Indices of columns that have a 1 in at least 1 line
225
- # => binary OR all the lines
226
- bitmask = reduce(int.__or__, (self.ctx.lines[i] for i in self.indices))
227
- self._column_indices = bit_indices(bitmask)
228
- return self._column_indices
207
+ # Indices of columns that have a 1 in at least 1 line
208
+ # => binary OR all the lines
209
+ bitmask = reduce(int.__or__, (self.ctx.lines[i] for i in self.indices))
210
+ return bit_indices(bitmask)
229
211
 
230
212
  @property
231
213
  def width(self):
232
214
  # Add 1 because Class2=0 cannot be used but needs to be encoded.
233
215
  return len(self.column_indices) + 1
234
216
 
235
- @property
217
+ @cached_property
236
218
  def cost(self):
237
- if self._cost is None:
238
- self._cost = (
239
- # 2 bytes to store the offset to this subtable in the Lookup table above
240
- 2
241
- # Contents of the subtable
242
- # From: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#pair-adjustment-positioning-format-2-class-pair-adjustment
243
- # uint16 posFormat Format identifier: format = 2
244
- + 2
245
- # Offset16 coverageOffset Offset to Coverage table, from beginning of PairPos subtable.
246
- + 2
247
- + self.coverage_bytes
248
- # uint16 valueFormat1 ValueRecord definition — for the first glyph of the pair (may be zero).
249
- + 2
250
- # uint16 valueFormat2 ValueRecord definition — for the second glyph of the pair (may be zero).
251
- + 2
252
- # Offset16 classDef1Offset Offset to ClassDef table, from beginning of PairPos subtable — for the first glyph of the pair.
253
- + 2
254
- + self.classDef1_bytes
255
- # Offset16 classDef2Offset Offset to ClassDef table, from beginning of PairPos subtable — for the second glyph of the pair.
256
- + 2
257
- + self.classDef2_bytes
258
- # uint16 class1Count Number of classes in classDef1 table — includes Class 0.
259
- + 2
260
- # uint16 class2Count Number of classes in classDef2 table — includes Class 0.
261
- + 2
262
- # Class1Record class1Records[class1Count] Array of Class1 records, ordered by classes in classDef1.
263
- + (self.ctx.valueFormat1_bytes + self.ctx.valueFormat2_bytes)
264
- * len(self.indices)
265
- * self.width
266
- )
267
- return self._cost
219
+ return (
220
+ # 2 bytes to store the offset to this subtable in the Lookup table above
221
+ 2
222
+ # Contents of the subtable
223
+ # From: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#pair-adjustment-positioning-format-2-class-pair-adjustment
224
+ # uint16 posFormat Format identifier: format = 2
225
+ + 2
226
+ # Offset16 coverageOffset Offset to Coverage table, from beginning of PairPos subtable.
227
+ + 2
228
+ + self.coverage_bytes
229
+ # uint16 valueFormat1 ValueRecord definition — for the first glyph of the pair (may be zero).
230
+ + 2
231
+ # uint16 valueFormat2 ValueRecord definition — for the second glyph of the pair (may be zero).
232
+ + 2
233
+ # Offset16 classDef1Offset Offset to ClassDef table, from beginning of PairPos subtable — for the first glyph of the pair.
234
+ + 2
235
+ + self.classDef1_bytes
236
+ # Offset16 classDef2Offset Offset to ClassDef table, from beginning of PairPos subtable — for the second glyph of the pair.
237
+ + 2
238
+ + self.classDef2_bytes
239
+ # uint16 class1Count Number of classes in classDef1 table — includes Class 0.
240
+ + 2
241
+ # uint16 class2Count Number of classes in classDef2 table — includes Class 0.
242
+ + 2
243
+ # Class1Record class1Records[class1Count] Array of Class1 records, ordered by classes in classDef1.
244
+ + (self.ctx.valueFormat1_bytes + self.ctx.valueFormat2_bytes)
245
+ * len(self.indices)
246
+ * self.width
247
+ )
268
248
 
269
249
  @property
270
250
  def coverage_bytes(self):
@@ -12,12 +12,14 @@ This allows the caller to provide more data for each point.
12
12
  For instance, whether or not a point is smooth, and its name.
13
13
  """
14
14
 
15
+ from __future__ import annotations
16
+
15
17
  import math
16
- from typing import Any, Optional, Tuple, Dict
18
+ from typing import Any, Dict, List, Optional, Tuple
17
19
 
18
20
  from fontTools.misc.loggingTools import LogMixin
19
- from fontTools.pens.basePen import AbstractPen, MissingComponentError, PenError
20
21
  from fontTools.misc.transform import DecomposedTransform, Identity
22
+ from fontTools.pens.basePen import AbstractPen, MissingComponentError, PenError
21
23
 
22
24
  __all__ = [
23
25
  "AbstractPointPen",
@@ -28,6 +30,14 @@ __all__ = [
28
30
  "ReverseContourPointPen",
29
31
  ]
30
32
 
33
+ # Some type aliases to make it easier below
34
+ Point = Tuple[float, float]
35
+ PointName = Optional[str]
36
+ # [(pt, smooth, name, kwargs)]
37
+ SegmentPointList = List[Tuple[Optional[Point], bool, PointName, Any]]
38
+ SegmentType = Optional[str]
39
+ SegmentList = List[Tuple[SegmentType, SegmentPointList]]
40
+
31
41
 
32
42
  class AbstractPointPen:
33
43
  """Baseclass for all PointPens."""
@@ -88,7 +98,7 @@ class BasePointToSegmentPen(AbstractPointPen):
88
98
  care of all the edge cases.
89
99
  """
90
100
 
91
- def __init__(self):
101
+ def __init__(self) -> None:
92
102
  self.currentPath = None
93
103
 
94
104
  def beginPath(self, identifier=None, **kwargs):
@@ -96,7 +106,7 @@ class BasePointToSegmentPen(AbstractPointPen):
96
106
  raise PenError("Path already begun.")
97
107
  self.currentPath = []
98
108
 
99
- def _flushContour(self, segments):
109
+ def _flushContour(self, segments: SegmentList) -> None:
100
110
  """Override this method.
101
111
 
102
112
  It will be called for each non-empty sub path with a list
@@ -124,7 +134,7 @@ class BasePointToSegmentPen(AbstractPointPen):
124
134
  """
125
135
  raise NotImplementedError
126
136
 
127
- def endPath(self):
137
+ def endPath(self) -> None:
128
138
  if self.currentPath is None:
129
139
  raise PenError("Path not begun.")
130
140
  points = self.currentPath
@@ -134,7 +144,7 @@ class BasePointToSegmentPen(AbstractPointPen):
134
144
  if len(points) == 1:
135
145
  # Not much more we can do than output a single move segment.
136
146
  pt, segmentType, smooth, name, kwargs = points[0]
137
- segments = [("move", [(pt, smooth, name, kwargs)])]
147
+ segments: SegmentList = [("move", [(pt, smooth, name, kwargs)])]
138
148
  self._flushContour(segments)
139
149
  return
140
150
  segments = []
@@ -162,7 +172,7 @@ class BasePointToSegmentPen(AbstractPointPen):
162
172
  else:
163
173
  points = points[firstOnCurve + 1 :] + points[: firstOnCurve + 1]
164
174
 
165
- currentSegment = []
175
+ currentSegment: SegmentPointList = []
166
176
  for pt, segmentType, smooth, name, kwargs in points:
167
177
  currentSegment.append((pt, smooth, name, kwargs))
168
178
  if segmentType is None:
@@ -189,7 +199,7 @@ class PointToSegmentPen(BasePointToSegmentPen):
189
199
  and kwargs.
190
200
  """
191
201
 
192
- def __init__(self, segmentPen, outputImpliedClosingLine=False):
202
+ def __init__(self, segmentPen, outputImpliedClosingLine: bool = False) -> None:
193
203
  BasePointToSegmentPen.__init__(self)
194
204
  self.pen = segmentPen
195
205
  self.outputImpliedClosingLine = outputImpliedClosingLine
@@ -271,14 +281,14 @@ class SegmentToPointPen(AbstractPen):
271
281
  PointPen protocol.
272
282
  """
273
283
 
274
- def __init__(self, pointPen, guessSmooth=True):
284
+ def __init__(self, pointPen, guessSmooth=True) -> None:
275
285
  if guessSmooth:
276
286
  self.pen = GuessSmoothPointPen(pointPen)
277
287
  else:
278
288
  self.pen = pointPen
279
- self.contour = None
289
+ self.contour: Optional[List[Tuple[Point, SegmentType]]] = None
280
290
 
281
- def _flushContour(self):
291
+ def _flushContour(self) -> None:
282
292
  pen = self.pen
283
293
  pen.beginPath()
284
294
  for pt, segmentType in self.contour:
@@ -594,7 +604,6 @@ class DecomposingPointPen(LogMixin, AbstractPointPen):
594
604
  # if the transformation has a negative determinant, it will
595
605
  # reverse the contour direction of the component
596
606
  a, b, c, d = transformation[:4]
597
- det = a * d - b * c
598
607
  if a * d - b * c < 0:
599
608
  pen = ReverseContourPointPen(pen)
600
609
  glyph.drawPoints(pen)
@@ -1,10 +1,14 @@
1
1
  # Copyright (c) 2009 Type Supply LLC
2
2
  # Author: Tal Leming
3
3
 
4
- from fontTools.misc.roundTools import otRound, roundFunc
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Dict, List, Tuple
7
+
8
+ from fontTools.cffLib.specializer import commandsToProgram, specializeCommands
5
9
  from fontTools.misc.psCharStrings import T2CharString
10
+ from fontTools.misc.roundTools import otRound, roundFunc
6
11
  from fontTools.pens.basePen import BasePen
7
- from fontTools.cffLib.specializer import specializeCommands, commandsToProgram
8
12
 
9
13
 
10
14
  class T2CharStringPen(BasePen):
@@ -18,36 +22,52 @@ class T2CharStringPen(BasePen):
18
22
  which are close to their integral part within the tolerated range.
19
23
  """
20
24
 
21
- def __init__(self, width, glyphSet, roundTolerance=0.5, CFF2=False):
25
+ def __init__(
26
+ self,
27
+ width: float | None,
28
+ glyphSet: Dict[str, Any] | None,
29
+ roundTolerance: float = 0.5,
30
+ CFF2: bool = False,
31
+ ) -> None:
22
32
  super(T2CharStringPen, self).__init__(glyphSet)
23
33
  self.round = roundFunc(roundTolerance)
24
34
  self._CFF2 = CFF2
25
35
  self._width = width
26
- self._commands = []
36
+ self._commands: List[Tuple[str | bytes, List[float]]] = []
27
37
  self._p0 = (0, 0)
28
38
 
29
- def _p(self, pt):
39
+ def _p(self, pt: Tuple[float, float]) -> List[float]:
30
40
  p0 = self._p0
31
41
  pt = self._p0 = (self.round(pt[0]), self.round(pt[1]))
32
42
  return [pt[0] - p0[0], pt[1] - p0[1]]
33
43
 
34
- def _moveTo(self, pt):
44
+ def _moveTo(self, pt: Tuple[float, float]) -> None:
35
45
  self._commands.append(("rmoveto", self._p(pt)))
36
46
 
37
- def _lineTo(self, pt):
47
+ def _lineTo(self, pt: Tuple[float, float]) -> None:
38
48
  self._commands.append(("rlineto", self._p(pt)))
39
49
 
40
- def _curveToOne(self, pt1, pt2, pt3):
50
+ def _curveToOne(
51
+ self,
52
+ pt1: Tuple[float, float],
53
+ pt2: Tuple[float, float],
54
+ pt3: Tuple[float, float],
55
+ ) -> None:
41
56
  _p = self._p
42
57
  self._commands.append(("rrcurveto", _p(pt1) + _p(pt2) + _p(pt3)))
43
58
 
44
- def _closePath(self):
59
+ def _closePath(self) -> None:
45
60
  pass
46
61
 
47
- def _endPath(self):
62
+ def _endPath(self) -> None:
48
63
  pass
49
64
 
50
- def getCharString(self, private=None, globalSubrs=None, optimize=True):
65
+ def getCharString(
66
+ self,
67
+ private: Dict | None = None,
68
+ globalSubrs: List | None = None,
69
+ optimize: bool = True,
70
+ ) -> T2CharString:
51
71
  commands = self._commands
52
72
  if optimize:
53
73
  maxstack = 48 if not self._CFF2 else 513
@@ -16,6 +16,7 @@ from fontTools.subset.cff import *
16
16
  from fontTools.subset.svg import *
17
17
  from fontTools.varLib import varStore, multiVarStore # For monkey-patching
18
18
  from fontTools.ttLib.tables._n_a_m_e import NameRecordVisitor
19
+ from fontTools.unicodedata import mirrored
19
20
  import sys
20
21
  import struct
21
22
  import array
@@ -2870,6 +2871,15 @@ def prune_post_subset(self, font, options):
2870
2871
  def closure_glyphs(self, s):
2871
2872
  tables = [t for t in self.tables if t.isUnicode()]
2872
2873
 
2874
+ # Closure unicodes, which for now is pulling in bidi mirrored variants
2875
+ if s.options.bidi_closure:
2876
+ additional_unicodes = set()
2877
+ for u in s.unicodes_requested:
2878
+ mirror_u = mirrored(u)
2879
+ if mirror_u is not None:
2880
+ additional_unicodes.add(mirror_u)
2881
+ s.unicodes_requested.update(additional_unicodes)
2882
+
2873
2883
  # Close glyphs
2874
2884
  for table in tables:
2875
2885
  if table.format == 14:
@@ -3191,6 +3201,7 @@ class Options(object):
3191
3201
  self.font_number = -1
3192
3202
  self.pretty_svg = False
3193
3203
  self.lazy = True
3204
+ self.bidi_closure = True
3194
3205
 
3195
3206
  self.set(**kwargs)
3196
3207
 
@@ -3746,7 +3757,7 @@ def main(args=None):
3746
3757
  text += g[7:]
3747
3758
  continue
3748
3759
  if g.startswith("--text-file="):
3749
- with open(g[12:], encoding="utf-8") as f:
3760
+ with open(g[12:], encoding="utf-8-sig") as f:
3750
3761
  text += f.read().replace("\n", "")
3751
3762
  continue
3752
3763
  if g.startswith("--unicodes="):
@@ -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
@@ -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
  )
@@ -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,22 +6,33 @@ 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):
@@ -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_
@@ -21,6 +21,8 @@ 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()
@@ -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)
@@ -1225,7 +1225,7 @@ class Glyph(object):
1225
1225
  if boundsDone is not None:
1226
1226
  boundsDone.add(glyphName)
1227
1227
  # empty components shouldn't update the bounds of the parent glyph
1228
- if g.numberOfContours == 0:
1228
+ if g.yMin == g.yMax and g.xMin == g.xMax:
1229
1229
  continue
1230
1230
 
1231
1231
  x, y = compo.x, compo.y
@@ -1285,11 +1285,7 @@ class Glyph(object):
1285
1285
  # however, if the referenced component glyph is another composite, we
1286
1286
  # must not round here but only at the end, after all the nested
1287
1287
  # transforms have been applied, or else rounding errors will compound.
1288
- if (
1289
- round is not noRound
1290
- and g.numberOfContours > 0
1291
- and not compo._hasOnlyIntegerTranslate()
1292
- ):
1288
+ if round is not noRound and g.numberOfContours > 0:
1293
1289
  coordinates.toInt(round=round)
1294
1290
  if hasattr(compo, "firstPt"):
1295
1291
  # component uses two reference points: we apply the transform _before_
@@ -24,19 +24,24 @@ log = logging.getLogger(__name__)
24
24
  # FreeType2 source code for parsing 'gvar':
25
25
  # http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/src/truetype/ttgxvar.c
26
26
 
27
- GVAR_HEADER_FORMAT = """
27
+ GVAR_HEADER_FORMAT_HEAD = """
28
28
  > # big endian
29
29
  version: H
30
30
  reserved: H
31
31
  axisCount: H
32
32
  sharedTupleCount: H
33
33
  offsetToSharedTuples: I
34
- glyphCount: H
34
+ """
35
+ # In between the HEAD and TAIL lies the glyphCount, which is
36
+ # of different size: 2 bytes for gvar, and 3 bytes for GVAR.
37
+ GVAR_HEADER_FORMAT_TAIL = """
38
+ > # big endian
35
39
  flags: H
36
40
  offsetToGlyphVariationData: I
37
41
  """
38
42
 
39
- GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT)
43
+ GVAR_HEADER_SIZE_HEAD = sstruct.calcsize(GVAR_HEADER_FORMAT_HEAD)
44
+ GVAR_HEADER_SIZE_TAIL = sstruct.calcsize(GVAR_HEADER_FORMAT_TAIL)
40
45
 
41
46
 
42
47
  class table__g_v_a_r(DefaultTable.DefaultTable):
@@ -51,6 +56,7 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
51
56
  """
52
57
 
53
58
  dependencies = ["fvar", "glyf"]
59
+ gid_size = 2
54
60
 
55
61
  def __init__(self, tag=None):
56
62
  DefaultTable.DefaultTable.__init__(self, tag)
@@ -74,20 +80,25 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
74
80
  offsets.append(offset)
75
81
  compiledOffsets, tableFormat = self.compileOffsets_(offsets)
76
82
 
83
+ GVAR_HEADER_SIZE = GVAR_HEADER_SIZE_HEAD + self.gid_size + GVAR_HEADER_SIZE_TAIL
77
84
  header = {}
78
85
  header["version"] = self.version
79
86
  header["reserved"] = self.reserved
80
87
  header["axisCount"] = len(axisTags)
81
88
  header["sharedTupleCount"] = len(sharedTuples)
82
89
  header["offsetToSharedTuples"] = GVAR_HEADER_SIZE + len(compiledOffsets)
83
- header["glyphCount"] = len(compiledGlyphs)
84
90
  header["flags"] = tableFormat
85
91
  header["offsetToGlyphVariationData"] = (
86
92
  header["offsetToSharedTuples"] + sharedTupleSize
87
93
  )
88
- compiledHeader = sstruct.pack(GVAR_HEADER_FORMAT, header)
89
94
 
90
- result = [compiledHeader, compiledOffsets]
95
+ result = [
96
+ sstruct.pack(GVAR_HEADER_FORMAT_HEAD, header),
97
+ len(compiledGlyphs).to_bytes(self.gid_size, "big"),
98
+ sstruct.pack(GVAR_HEADER_FORMAT_TAIL, header),
99
+ ]
100
+
101
+ result.append(compiledOffsets)
91
102
  result.extend(sharedTuples)
92
103
  result.extend(compiledGlyphs)
93
104
  return b"".join(result)
@@ -104,6 +115,7 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
104
115
  pointCountUnused = 0 # pointCount is actually unused by compileGlyph
105
116
  result.append(
106
117
  compileGlyph_(
118
+ self.gid_size,
107
119
  variations,
108
120
  pointCountUnused,
109
121
  axisTags,
@@ -116,7 +128,19 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
116
128
  def decompile(self, data, ttFont):
117
129
  axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
118
130
  glyphs = ttFont.getGlyphOrder()
119
- sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self)
131
+
132
+ # Parse the header
133
+ GVAR_HEADER_SIZE = GVAR_HEADER_SIZE_HEAD + self.gid_size + GVAR_HEADER_SIZE_TAIL
134
+ sstruct.unpack(GVAR_HEADER_FORMAT_HEAD, data[:GVAR_HEADER_SIZE_HEAD], self)
135
+ self.glyphCount = int.from_bytes(
136
+ data[GVAR_HEADER_SIZE_HEAD : GVAR_HEADER_SIZE_HEAD + self.gid_size], "big"
137
+ )
138
+ sstruct.unpack(
139
+ GVAR_HEADER_FORMAT_TAIL,
140
+ data[GVAR_HEADER_SIZE_HEAD + self.gid_size : GVAR_HEADER_SIZE],
141
+ self,
142
+ )
143
+
120
144
  assert len(glyphs) == self.glyphCount
121
145
  assert len(axisTags) == self.axisCount
122
146
  sharedCoords = tv.decompileSharedTuples(
@@ -146,7 +170,7 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
146
170
  glyph = glyf[glyphName]
147
171
  numPointsInGlyph = self.getNumPoints_(glyph)
148
172
  return decompileGlyph_(
149
- numPointsInGlyph, sharedCoords, axisTags, gvarData
173
+ self.gid_size, numPointsInGlyph, sharedCoords, axisTags, gvarData
150
174
  )
151
175
 
152
176
  return read_item
@@ -264,23 +288,42 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
264
288
 
265
289
 
266
290
  def compileGlyph_(
267
- variations, pointCount, axisTags, sharedCoordIndices, *, optimizeSize=True
291
+ dataOffsetSize,
292
+ variations,
293
+ pointCount,
294
+ axisTags,
295
+ sharedCoordIndices,
296
+ *,
297
+ optimizeSize=True,
268
298
  ):
299
+ assert dataOffsetSize in (2, 3)
269
300
  tupleVariationCount, tuples, data = tv.compileTupleVariationStore(
270
301
  variations, pointCount, axisTags, sharedCoordIndices, optimizeSize=optimizeSize
271
302
  )
272
303
  if tupleVariationCount == 0:
273
304
  return b""
274
- result = [struct.pack(">HH", tupleVariationCount, 4 + len(tuples)), tuples, data]
275
- if (len(tuples) + len(data)) % 2 != 0:
305
+
306
+ offsetToData = 2 + dataOffsetSize + len(tuples)
307
+
308
+ result = [
309
+ tupleVariationCount.to_bytes(2, "big"),
310
+ offsetToData.to_bytes(dataOffsetSize, "big"),
311
+ tuples,
312
+ data,
313
+ ]
314
+ if (offsetToData + len(data)) % 2 != 0:
276
315
  result.append(b"\0") # padding
277
316
  return b"".join(result)
278
317
 
279
318
 
280
- def decompileGlyph_(pointCount, sharedTuples, axisTags, data):
281
- if len(data) < 4:
319
+ def decompileGlyph_(dataOffsetSize, pointCount, sharedTuples, axisTags, data):
320
+ assert dataOffsetSize in (2, 3)
321
+ if len(data) < 2 + dataOffsetSize:
282
322
  return []
283
- tupleVariationCount, offsetToData = struct.unpack(">HH", data[:4])
323
+
324
+ tupleVariationCount = int.from_bytes(data[:2], "big")
325
+ offsetToData = int.from_bytes(data[2 : 2 + dataOffsetSize], "big")
326
+
284
327
  dataPos = offsetToData
285
328
  return tv.decompileTupleVariationStore(
286
329
  "gvar",
@@ -289,6 +332,6 @@ def decompileGlyph_(pointCount, sharedTuples, axisTags, data):
289
332
  pointCount,
290
333
  sharedTuples,
291
334
  data,
292
- 4,
335
+ 2 + dataOffsetSize,
293
336
  offsetToData,
294
337
  )