fonttools 4.56.0__py3-none-any.whl → 4.58.0__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 (59) hide show
  1. fontTools/__init__.py +1 -1
  2. fontTools/cffLib/__init__.py +61 -26
  3. fontTools/config/__init__.py +15 -0
  4. fontTools/designspaceLib/statNames.py +14 -7
  5. fontTools/feaLib/ast.py +92 -13
  6. fontTools/feaLib/builder.py +52 -13
  7. fontTools/feaLib/parser.py +59 -39
  8. fontTools/fontBuilder.py +6 -0
  9. fontTools/misc/etree.py +4 -27
  10. fontTools/misc/testTools.py +2 -1
  11. fontTools/mtiLib/__init__.py +0 -2
  12. fontTools/otlLib/builder.py +195 -145
  13. fontTools/otlLib/optimize/gpos.py +49 -63
  14. fontTools/pens/pointPen.py +21 -12
  15. fontTools/subset/__init__.py +11 -0
  16. fontTools/ttLib/__init__.py +4 -0
  17. fontTools/ttLib/__main__.py +47 -8
  18. fontTools/ttLib/tables/D__e_b_g.py +20 -2
  19. fontTools/ttLib/tables/G_V_A_R_.py +5 -0
  20. fontTools/ttLib/tables/T_S_I__0.py +14 -3
  21. fontTools/ttLib/tables/T_S_I__5.py +16 -5
  22. fontTools/ttLib/tables/__init__.py +1 -0
  23. fontTools/ttLib/tables/_c_m_a_p.py +19 -6
  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 +11 -10
  27. fontTools/ttLib/tables/_g_v_a_r.py +62 -17
  28. fontTools/ttLib/tables/_p_o_s_t.py +5 -2
  29. fontTools/ttLib/tables/otBase.py +1 -0
  30. fontTools/ttLib/tables/otConverters.py +5 -2
  31. fontTools/ttLib/tables/otTables.py +5 -1
  32. fontTools/ttLib/ttFont.py +3 -5
  33. fontTools/ttLib/ttGlyphSet.py +0 -10
  34. fontTools/ttx.py +13 -1
  35. fontTools/ufoLib/__init__.py +2 -2
  36. fontTools/ufoLib/converters.py +89 -25
  37. fontTools/ufoLib/errors.py +8 -0
  38. fontTools/ufoLib/etree.py +1 -1
  39. fontTools/ufoLib/filenames.py +155 -100
  40. fontTools/ufoLib/glifLib.py +9 -2
  41. fontTools/ufoLib/kerning.py +66 -36
  42. fontTools/ufoLib/utils.py +5 -2
  43. fontTools/unicodedata/Mirrored.py +446 -0
  44. fontTools/unicodedata/__init__.py +6 -2
  45. fontTools/varLib/__init__.py +94 -89
  46. fontTools/varLib/hvar.py +113 -0
  47. fontTools/varLib/varStore.py +1 -1
  48. fontTools/voltLib/__main__.py +206 -0
  49. fontTools/voltLib/ast.py +4 -0
  50. fontTools/voltLib/parser.py +16 -8
  51. fontTools/voltLib/voltToFea.py +347 -166
  52. {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info}/METADATA +60 -12
  53. {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info}/RECORD +59 -54
  54. {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info}/WHEEL +1 -1
  55. fonttools-4.58.0.dist-info/licenses/LICENSE.external +359 -0
  56. {fonttools-4.56.0.data → fonttools-4.58.0.data}/data/share/man/man1/ttx.1 +0 -0
  57. {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info}/entry_points.txt +0 -0
  58. {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info/licenses}/LICENSE +0 -0
  59. {fonttools-4.56.0.dist-info → fonttools-4.58.0.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
@@ -53,12 +54,18 @@ def compact(font: TTFont, level: int) -> TTFont:
53
54
  # are not grouped together first; instead each subtable is treated
54
55
  # independently, so currently this step is:
55
56
  # Split existing subtables into more smaller subtables
56
- gpos = font["GPOS"]
57
+ gpos = font.get("GPOS")
58
+
59
+ # If the font does not contain a GPOS table, there is nothing to do.
60
+ if gpos is None:
61
+ return font
62
+
57
63
  for lookup in gpos.table.LookupList.Lookup:
58
64
  if lookup.LookupType == 2:
59
65
  compact_lookup(font, level, lookup)
60
66
  elif lookup.LookupType == 9 and lookup.SubTable[0].ExtensionLookupType == 2:
61
67
  compact_ext_lookup(font, level, lookup)
68
+
62
69
  return font
63
70
 
64
71
 
@@ -186,79 +193,58 @@ ClusteringContext = namedtuple(
186
193
  )
187
194
 
188
195
 
196
+ @dataclass
189
197
  class Cluster:
190
- # TODO(Python 3.7): Turn this into a dataclass
191
- # ctx: ClusteringContext
192
- # indices: int
193
- # Caches
194
- # TODO(Python 3.8): use functools.cached_property instead of the
195
- # manually cached properties, and remove the cache fields listed below.
196
- # _indices: Optional[List[int]] = None
197
- # _column_indices: Optional[List[int]] = None
198
- # _cost: Optional[int] = None
199
-
200
- __slots__ = "ctx", "indices_bitmask", "_indices", "_column_indices", "_cost"
201
-
202
- def __init__(self, ctx: ClusteringContext, indices_bitmask: int):
203
- self.ctx = ctx
204
- self.indices_bitmask = indices_bitmask
205
- self._indices = None
206
- self._column_indices = None
207
- self._cost = None
198
+ ctx: ClusteringContext
199
+ indices_bitmask: int
208
200
 
209
- @property
201
+ @cached_property
210
202
  def indices(self):
211
- if self._indices is None:
212
- self._indices = bit_indices(self.indices_bitmask)
213
- return self._indices
203
+ return bit_indices(self.indices_bitmask)
214
204
 
215
- @property
205
+ @cached_property
216
206
  def column_indices(self):
217
- if self._column_indices is None:
218
- # Indices of columns that have a 1 in at least 1 line
219
- # => binary OR all the lines
220
- bitmask = reduce(int.__or__, (self.ctx.lines[i] for i in self.indices))
221
- self._column_indices = bit_indices(bitmask)
222
- 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)
223
211
 
224
212
  @property
225
213
  def width(self):
226
214
  # Add 1 because Class2=0 cannot be used but needs to be encoded.
227
215
  return len(self.column_indices) + 1
228
216
 
229
- @property
217
+ @cached_property
230
218
  def cost(self):
231
- if self._cost is None:
232
- self._cost = (
233
- # 2 bytes to store the offset to this subtable in the Lookup table above
234
- 2
235
- # Contents of the subtable
236
- # From: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#pair-adjustment-positioning-format-2-class-pair-adjustment
237
- # uint16 posFormat Format identifier: format = 2
238
- + 2
239
- # Offset16 coverageOffset Offset to Coverage table, from beginning of PairPos subtable.
240
- + 2
241
- + self.coverage_bytes
242
- # uint16 valueFormat1 ValueRecord definition — for the first glyph of the pair (may be zero).
243
- + 2
244
- # uint16 valueFormat2 ValueRecord definition — for the second glyph of the pair (may be zero).
245
- + 2
246
- # Offset16 classDef1Offset Offset to ClassDef table, from beginning of PairPos subtable — for the first glyph of the pair.
247
- + 2
248
- + self.classDef1_bytes
249
- # Offset16 classDef2Offset Offset to ClassDef table, from beginning of PairPos subtable — for the second glyph of the pair.
250
- + 2
251
- + self.classDef2_bytes
252
- # uint16 class1Count Number of classes in classDef1 table — includes Class 0.
253
- + 2
254
- # uint16 class2Count Number of classes in classDef2 table — includes Class 0.
255
- + 2
256
- # Class1Record class1Records[class1Count] Array of Class1 records, ordered by classes in classDef1.
257
- + (self.ctx.valueFormat1_bytes + self.ctx.valueFormat2_bytes)
258
- * len(self.indices)
259
- * self.width
260
- )
261
- 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
+ )
262
248
 
263
249
  @property
264
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)
@@ -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
 
@@ -1,5 +1,6 @@
1
1
  """fontTools.ttLib -- a package for dealing with TrueType fonts."""
2
2
 
3
+ from fontTools.config import OPTIONS
3
4
  from fontTools.misc.loggingTools import deprecateFunction
4
5
  import logging
5
6
 
@@ -7,6 +8,9 @@ import logging
7
8
  log = logging.getLogger(__name__)
8
9
 
9
10
 
11
+ OPTIMIZE_FONT_SPEED = OPTIONS["fontTools.ttLib:OPTIMIZE_FONT_SPEED"]
12
+
13
+
10
14
  class TTLibError(Exception):
11
15
  pass
12
16
 
@@ -1,5 +1,5 @@
1
1
  import sys
2
- from fontTools.ttLib import TTLibError, TTLibFileIsCollectionError
2
+ from fontTools.ttLib import OPTIMIZE_FONT_SPEED, TTLibError, TTLibFileIsCollectionError
3
3
  from fontTools.ttLib.ttFont import *
4
4
  from fontTools.ttLib.ttCollection import TTCollection
5
5
 
@@ -51,7 +51,7 @@ def main(args=None):
51
51
  )
52
52
  parser.add_argument("font", metavar="font", nargs="*", help="Font file.")
53
53
  parser.add_argument(
54
- "-t", "--table", metavar="table", nargs="*", help="Tables to decompile."
54
+ "-t", "--table", metavar="table", action="append", help="Tables to decompile."
55
55
  )
56
56
  parser.add_argument(
57
57
  "-o", "--output", metavar="FILE", default=None, help="Output file."
@@ -71,27 +71,66 @@ def main(args=None):
71
71
  default=None,
72
72
  help="Flavor of output font. 'woff' or 'woff2'.",
73
73
  )
74
+ parser.add_argument(
75
+ "--no-recalc-timestamp",
76
+ dest="recalcTimestamp",
77
+ action="store_false",
78
+ help="Keep the original font 'modified' timestamp.",
79
+ )
80
+ parser.add_argument(
81
+ "-b",
82
+ dest="recalcBBoxes",
83
+ action="store_false",
84
+ help="Don't recalc glyph bounding boxes: use the values in the original font.",
85
+ )
86
+ parser.add_argument(
87
+ "--optimize-font-speed",
88
+ action="store_true",
89
+ help=(
90
+ "Enable optimizations that prioritize speed over file size. This "
91
+ "mainly affects how glyf table and gvar / VARC tables are compiled."
92
+ ),
93
+ )
74
94
  options = parser.parse_args(args)
75
95
 
76
96
  fontNumber = int(options.y) if options.y is not None else None
77
97
  outFile = options.output
78
98
  lazy = options.lazy
79
99
  flavor = options.flavor
80
- tables = options.table if options.table is not None else ["*"]
100
+ tables = options.table
101
+ recalcBBoxes = options.recalcBBoxes
102
+ recalcTimestamp = options.recalcTimestamp
103
+ optimizeFontSpeed = options.optimize_font_speed
81
104
 
82
105
  fonts = []
83
106
  for f in options.font:
84
107
  try:
85
- font = TTFont(f, fontNumber=fontNumber, lazy=lazy)
108
+ font = TTFont(
109
+ f,
110
+ recalcBBoxes=recalcBBoxes,
111
+ recalcTimestamp=recalcTimestamp,
112
+ fontNumber=fontNumber,
113
+ lazy=lazy,
114
+ )
115
+ if optimizeFontSpeed:
116
+ font.cfg[OPTIMIZE_FONT_SPEED] = optimizeFontSpeed
86
117
  fonts.append(font)
87
118
  except TTLibFileIsCollectionError:
88
119
  collection = TTCollection(f, lazy=lazy)
89
120
  fonts.extend(collection.fonts)
90
121
 
91
- if lazy is False:
92
- for font in fonts:
93
- for table in tables if "*" not in tables else font.keys():
94
- font[table] # Decompiles
122
+ if tables is None:
123
+ if lazy is False:
124
+ tables = ["*"]
125
+ elif optimizeFontSpeed:
126
+ tables = {"glyf", "gvar", "VARC"}.intersection(font.keys())
127
+ else:
128
+ tables = []
129
+ for font in fonts:
130
+ if "GlyphOrder" in tables:
131
+ font.getGlyphOrder()
132
+ for table in tables if "*" not in tables else font.keys():
133
+ font[table] # Decompiles
95
134
 
96
135
  if outFile is not None:
97
136
  if len(fonts) == 1:
@@ -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
@@ -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_
@@ -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)
@@ -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,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)
@@ -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
 
@@ -1220,7 +1225,7 @@ class Glyph(object):
1220
1225
  if boundsDone is not None:
1221
1226
  boundsDone.add(glyphName)
1222
1227
  # empty components shouldn't update the bounds of the parent glyph
1223
- if g.numberOfContours == 0:
1228
+ if g.yMin == g.yMax and g.xMin == g.xMax:
1224
1229
  continue
1225
1230
 
1226
1231
  x, y = compo.x, compo.y
@@ -1280,11 +1285,7 @@ class Glyph(object):
1280
1285
  # however, if the referenced component glyph is another composite, we
1281
1286
  # must not round here but only at the end, after all the nested
1282
1287
  # transforms have been applied, or else rounding errors will compound.
1283
- if (
1284
- round is not noRound
1285
- and g.numberOfContours > 0
1286
- and not compo._hasOnlyIntegerTranslate()
1287
- ):
1288
+ if round is not noRound and g.numberOfContours > 0:
1288
1289
  coordinates.toInt(round=round)
1289
1290
  if hasattr(compo, "firstPt"):
1290
1291
  # component uses two reference points: we apply the transform _before_