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
fontTools/__init__.py CHANGED
@@ -3,6 +3,6 @@ from fontTools.misc.loggingTools import configLogger
3
3
 
4
4
  log = logging.getLogger(__name__)
5
5
 
6
- version = __version__ = "4.56.0"
6
+ version = __version__ = "4.58.0"
7
7
 
8
8
  __all__ = ["version", "log", "configLogger"]
@@ -1464,10 +1464,11 @@ class CharsetConverter(SimpleConverter):
1464
1464
  if glyphName in allNames:
1465
1465
  # make up a new glyphName that's unique
1466
1466
  n = allNames[glyphName]
1467
- while (glyphName + "#" + str(n)) in allNames:
1467
+ names = set(allNames) | set(charset)
1468
+ while (glyphName + "." + str(n)) in names:
1468
1469
  n += 1
1469
1470
  allNames[glyphName] = n + 1
1470
- glyphName = glyphName + "#" + str(n)
1471
+ glyphName = glyphName + "." + str(n)
1471
1472
  allNames[glyphName] = 1
1472
1473
  newCharset.append(glyphName)
1473
1474
  charset = newCharset
@@ -1663,25 +1664,26 @@ class EncodingConverter(SimpleConverter):
1663
1664
  return "StandardEncoding"
1664
1665
  elif value == 1:
1665
1666
  return "ExpertEncoding"
1667
+ # custom encoding at offset `value`
1668
+ assert value > 1
1669
+ file = parent.file
1670
+ file.seek(value)
1671
+ log.log(DEBUG, "loading Encoding at %s", value)
1672
+ fmt = readCard8(file)
1673
+ haveSupplement = bool(fmt & 0x80)
1674
+ fmt = fmt & 0x7F
1675
+
1676
+ if fmt == 0:
1677
+ encoding = parseEncoding0(parent.charset, file)
1678
+ elif fmt == 1:
1679
+ encoding = parseEncoding1(parent.charset, file)
1666
1680
  else:
1667
- assert value > 1
1668
- file = parent.file
1669
- file.seek(value)
1670
- log.log(DEBUG, "loading Encoding at %s", value)
1671
- fmt = readCard8(file)
1672
- haveSupplement = fmt & 0x80
1673
- if haveSupplement:
1674
- raise NotImplementedError("Encoding supplements are not yet supported")
1675
- fmt = fmt & 0x7F
1676
- if fmt == 0:
1677
- encoding = parseEncoding0(
1678
- parent.charset, file, haveSupplement, parent.strings
1679
- )
1680
- elif fmt == 1:
1681
- encoding = parseEncoding1(
1682
- parent.charset, file, haveSupplement, parent.strings
1683
- )
1684
- return encoding
1681
+ raise ValueError(f"Unknown Encoding format: {fmt}")
1682
+
1683
+ if haveSupplement:
1684
+ parseEncodingSupplement(file, encoding, parent.strings)
1685
+
1686
+ return encoding
1685
1687
 
1686
1688
  def write(self, parent, value):
1687
1689
  if value == "StandardEncoding":
@@ -1719,27 +1721,60 @@ class EncodingConverter(SimpleConverter):
1719
1721
  return encoding
1720
1722
 
1721
1723
 
1722
- def parseEncoding0(charset, file, haveSupplement, strings):
1724
+ def readSID(file):
1725
+ """Read a String ID (SID) — 2-byte unsigned integer."""
1726
+ data = file.read(2)
1727
+ if len(data) != 2:
1728
+ raise EOFError("Unexpected end of file while reading SID")
1729
+ return struct.unpack(">H", data)[0] # big-endian uint16
1730
+
1731
+
1732
+ def parseEncodingSupplement(file, encoding, strings):
1733
+ """
1734
+ Parse the CFF Encoding supplement data:
1735
+ - nSups: number of supplementary mappings
1736
+ - each mapping: (code, SID) pair
1737
+ and apply them to the `encoding` list in place.
1738
+ """
1739
+ nSups = readCard8(file)
1740
+ for _ in range(nSups):
1741
+ code = readCard8(file)
1742
+ sid = readSID(file)
1743
+ name = strings[sid]
1744
+ encoding[code] = name
1745
+
1746
+
1747
+ def parseEncoding0(charset, file):
1748
+ """
1749
+ Format 0: simple list of codes.
1750
+ After reading the base table, optionally parse the supplement.
1751
+ """
1723
1752
  nCodes = readCard8(file)
1724
1753
  encoding = [".notdef"] * 256
1725
1754
  for glyphID in range(1, nCodes + 1):
1726
1755
  code = readCard8(file)
1727
1756
  if code != 0:
1728
1757
  encoding[code] = charset[glyphID]
1758
+
1729
1759
  return encoding
1730
1760
 
1731
1761
 
1732
- def parseEncoding1(charset, file, haveSupplement, strings):
1762
+ def parseEncoding1(charset, file):
1763
+ """
1764
+ Format 1: range-based encoding.
1765
+ After reading the base ranges, optionally parse the supplement.
1766
+ """
1733
1767
  nRanges = readCard8(file)
1734
1768
  encoding = [".notdef"] * 256
1735
1769
  glyphID = 1
1736
- for i in range(nRanges):
1770
+ for _ in range(nRanges):
1737
1771
  code = readCard8(file)
1738
1772
  nLeft = readCard8(file)
1739
- for glyphID in range(glyphID, glyphID + nLeft + 1):
1773
+ for _ in range(nLeft + 1):
1740
1774
  encoding[code] = charset[glyphID]
1741
- code = code + 1
1742
- glyphID = glyphID + 1
1775
+ code += 1
1776
+ glyphID += 1
1777
+
1743
1778
  return encoding
1744
1779
 
1745
1780
 
@@ -73,3 +73,18 @@ Config.register_option(
73
73
  parse=Option.parse_optional_bool,
74
74
  validate=Option.validate_optional_bool,
75
75
  )
76
+
77
+ Config.register_option(
78
+ name="fontTools.ttLib:OPTIMIZE_FONT_SPEED",
79
+ help=dedent(
80
+ """\
81
+ Enable optimizations that prioritize speed over file size. This
82
+ mainly affects how glyf table and gvar / VARC tables are compiled.
83
+ The produced fonts will be larger, but rendering performance will
84
+ be improved with HarfBuzz and other text layout engines.
85
+ """
86
+ ),
87
+ default=False,
88
+ parse=Option.parse_optional_bool,
89
+ validate=Option.validate_optional_bool,
90
+ )
@@ -12,14 +12,13 @@ instance:
12
12
  from __future__ import annotations
13
13
 
14
14
  from dataclasses import dataclass
15
- from typing import Dict, Optional, Tuple, Union
15
+ from typing import Dict, Literal, Optional, Tuple, Union
16
16
  import logging
17
17
 
18
18
  from fontTools.designspaceLib import (
19
19
  AxisDescriptor,
20
20
  AxisLabelDescriptor,
21
21
  DesignSpaceDocument,
22
- DesignSpaceDocumentError,
23
22
  DiscreteAxisDescriptor,
24
23
  SimpleLocationDict,
25
24
  SourceDescriptor,
@@ -27,9 +26,13 @@ from fontTools.designspaceLib import (
27
26
 
28
27
  LOGGER = logging.getLogger(__name__)
29
28
 
30
- # TODO(Python 3.8): use Literal
31
- # RibbiStyleName = Union[Literal["regular"], Literal["bold"], Literal["italic"], Literal["bold italic"]]
32
- RibbiStyle = str
29
+ RibbiStyleName = Union[
30
+ Literal["regular"],
31
+ Literal["bold"],
32
+ Literal["italic"],
33
+ Literal["bold italic"],
34
+ ]
35
+
33
36
  BOLD_ITALIC_TO_RIBBI_STYLE = {
34
37
  (False, False): "regular",
35
38
  (False, True): "italic",
@@ -46,7 +49,7 @@ class StatNames:
46
49
  styleNames: Dict[str, str]
47
50
  postScriptFontName: Optional[str]
48
51
  styleMapFamilyNames: Dict[str, str]
49
- styleMapStyleName: Optional[RibbiStyle]
52
+ styleMapStyleName: Optional[RibbiStyleName]
50
53
 
51
54
 
52
55
  def getStatNames(
@@ -61,6 +64,10 @@ def getStatNames(
61
64
  localized names will be empty (family and style names), or the name will be
62
65
  None (PostScript name).
63
66
 
67
+ Note: this method does not consider info attached to the instance, like
68
+ family name. The user needs to override all names on an instance that STAT
69
+ information would compute differently than desired.
70
+
64
71
  .. versionadded:: 5.0
65
72
  """
66
73
  familyNames: Dict[str, str] = {}
@@ -201,7 +208,7 @@ def _getAxisLabelsForUserLocation(
201
208
 
202
209
  def _getRibbiStyle(
203
210
  self: DesignSpaceDocument, userLocation: SimpleLocationDict
204
- ) -> Tuple[RibbiStyle, SimpleLocationDict]:
211
+ ) -> Tuple[RibbiStyleName, SimpleLocationDict]:
205
212
  """Compute the RIBBI style name of the given user location,
206
213
  return the location of the matching Regular in the RIBBI group.
207
214
 
fontTools/feaLib/ast.py CHANGED
@@ -337,6 +337,76 @@ class AnonymousBlock(Statement):
337
337
  return res
338
338
 
339
339
 
340
+ def _upgrade_mixed_subst_statements(statements):
341
+ # https://github.com/fonttools/fonttools/issues/612
342
+ # A multiple substitution may have a single destination, in which case
343
+ # it will look just like a single substitution. So if there are both
344
+ # multiple and single substitutions, upgrade all the single ones to
345
+ # multiple substitutions. Similarly, a ligature substitution may have a
346
+ # single source glyph, so if there are both ligature and single
347
+ # substitutions, upgrade all the single ones to ligature substitutions.
348
+
349
+ has_single = False
350
+ has_multiple = False
351
+ has_ligature = False
352
+ for s in statements:
353
+ if isinstance(s, SingleSubstStatement):
354
+ has_single = not any([s.prefix, s.suffix, s.forceChain])
355
+ elif isinstance(s, MultipleSubstStatement):
356
+ has_multiple = not any([s.prefix, s.suffix, s.forceChain])
357
+ elif isinstance(s, LigatureSubstStatement):
358
+ has_ligature = not any([s.prefix, s.suffix, s.forceChain])
359
+
360
+ to_multiple = False
361
+ to_ligature = False
362
+
363
+ # If we have mixed single and multiple substitutions,
364
+ # upgrade all single substitutions to multiple substitutions.
365
+ if has_single and has_multiple and not has_ligature:
366
+ to_multiple = True
367
+
368
+ # If we have mixed single and ligature substitutions,
369
+ # upgrade all single substitutions to ligature substitutions.
370
+ elif has_single and has_ligature and not has_multiple:
371
+ to_ligature = True
372
+
373
+ if to_multiple or to_ligature:
374
+ ret = []
375
+ for s in statements:
376
+ if isinstance(s, SingleSubstStatement):
377
+ glyphs = s.glyphs[0].glyphSet()
378
+ replacements = s.replacements[0].glyphSet()
379
+ if len(replacements) == 1:
380
+ replacements *= len(glyphs)
381
+ for glyph, replacement in zip(glyphs, replacements):
382
+ if to_multiple:
383
+ ret.append(
384
+ MultipleSubstStatement(
385
+ s.prefix,
386
+ glyph,
387
+ s.suffix,
388
+ [replacement],
389
+ s.forceChain,
390
+ location=s.location,
391
+ )
392
+ )
393
+ elif to_ligature:
394
+ ret.append(
395
+ LigatureSubstStatement(
396
+ s.prefix,
397
+ [GlyphName(glyph)],
398
+ s.suffix,
399
+ replacement,
400
+ s.forceChain,
401
+ location=s.location,
402
+ )
403
+ )
404
+ else:
405
+ ret.append(s)
406
+ return ret
407
+ return statements
408
+
409
+
340
410
  class Block(Statement):
341
411
  """A block of statements: feature, lookup, etc."""
342
412
 
@@ -348,7 +418,8 @@ class Block(Statement):
348
418
  """When handed a 'builder' object of comparable interface to
349
419
  :class:`fontTools.feaLib.builder`, walks the statements in this
350
420
  block, calling the builder callbacks."""
351
- for s in self.statements:
421
+ statements = _upgrade_mixed_subst_statements(self.statements)
422
+ for s in statements:
352
423
  s.build(builder)
353
424
 
354
425
  def asFea(self, indent=""):
@@ -382,8 +453,7 @@ class FeatureBlock(Block):
382
453
  def build(self, builder):
383
454
  """Call the ``start_feature`` callback on the builder object, visit
384
455
  all the statements in this feature, and then call ``end_feature``."""
385
- # TODO(sascha): Handle use_extension.
386
- builder.start_feature(self.location, self.name)
456
+ builder.start_feature(self.location, self.name, self.use_extension)
387
457
  # language exclude_dflt statements modify builder.features_
388
458
  # limit them to this block with temporary builder.features_
389
459
  features = builder.features_
@@ -433,8 +503,7 @@ class LookupBlock(Block):
433
503
  self.name, self.use_extension = name, use_extension
434
504
 
435
505
  def build(self, builder):
436
- # TODO(sascha): Handle use_extension.
437
- builder.start_lookup_block(self.location, self.name)
506
+ builder.start_lookup_block(self.location, self.name, self.use_extension)
438
507
  Block.build(self, builder)
439
508
  builder.end_lookup_block()
440
509
 
@@ -753,7 +822,7 @@ class ChainContextPosStatement(Statement):
753
822
  if len(self.suffix):
754
823
  res += " " + " ".join(map(asFea, self.suffix))
755
824
  else:
756
- res += " ".join(map(asFea, self.glyph))
825
+ res += " ".join(map(asFea, self.glyphs))
757
826
  res += ";"
758
827
  return res
759
828
 
@@ -811,7 +880,7 @@ class ChainContextSubstStatement(Statement):
811
880
  if len(self.suffix):
812
881
  res += " " + " ".join(map(asFea, self.suffix))
813
882
  else:
814
- res += " ".join(map(asFea, self.glyph))
883
+ res += " ".join(map(asFea, self.glyphs))
815
884
  res += ";"
816
885
  return res
817
886
 
@@ -1512,7 +1581,9 @@ class SinglePosStatement(Statement):
1512
1581
  res += " ".join(map(asFea, self.prefix)) + " "
1513
1582
  res += " ".join(
1514
1583
  [
1515
- asFea(x[0]) + "'" + ((" " + x[1].asFea()) if x[1] else "")
1584
+ asFea(x[0])
1585
+ + "'"
1586
+ + ((" " + x[1].asFea()) if x[1] is not None else "")
1516
1587
  for x in self.pos
1517
1588
  ]
1518
1589
  )
@@ -1520,7 +1591,10 @@ class SinglePosStatement(Statement):
1520
1591
  res += " " + " ".join(map(asFea, self.suffix))
1521
1592
  else:
1522
1593
  res += " ".join(
1523
- [asFea(x[0]) + " " + (x[1].asFea() if x[1] else "") for x in self.pos]
1594
+ [
1595
+ asFea(x[0]) + " " + (x[1].asFea() if x[1] is not None else "")
1596
+ for x in self.pos
1597
+ ]
1524
1598
  )
1525
1599
  res += ";"
1526
1600
  return res
@@ -1828,15 +1902,16 @@ class BaseAxis(Statement):
1828
1902
  """An axis definition, being either a ``VertAxis.BaseTagList/BaseScriptList``
1829
1903
  pair or a ``HorizAxis.BaseTagList/BaseScriptList`` pair."""
1830
1904
 
1831
- def __init__(self, bases, scripts, vertical, location=None):
1905
+ def __init__(self, bases, scripts, vertical, minmax=None, location=None):
1832
1906
  Statement.__init__(self, location)
1833
1907
  self.bases = bases #: A list of baseline tag names as strings
1834
1908
  self.scripts = scripts #: A list of script record tuplets (script tag, default baseline tag, base coordinate)
1835
1909
  self.vertical = vertical #: Boolean; VertAxis if True, HorizAxis if False
1910
+ self.minmax = [] #: A set of minmax record
1836
1911
 
1837
1912
  def build(self, builder):
1838
1913
  """Calls the builder object's ``set_base_axis`` callback."""
1839
- builder.set_base_axis(self.bases, self.scripts, self.vertical)
1914
+ builder.set_base_axis(self.bases, self.scripts, self.vertical, self.minmax)
1840
1915
 
1841
1916
  def asFea(self, indent=""):
1842
1917
  direction = "Vert" if self.vertical else "Horiz"
@@ -1844,9 +1919,13 @@ class BaseAxis(Statement):
1844
1919
  "{} {} {}".format(a[0], a[1], " ".join(map(str, a[2])))
1845
1920
  for a in self.scripts
1846
1921
  ]
1922
+ minmaxes = [
1923
+ "\n{}Axis.MinMax {} {} {}, {};".format(direction, a[0], a[1], a[2], a[3])
1924
+ for a in self.minmax
1925
+ ]
1847
1926
  return "{}Axis.BaseTagList {};\n{}{}Axis.BaseScriptList {};".format(
1848
1927
  direction, " ".join(self.bases), indent, direction, ", ".join(scripts)
1849
- )
1928
+ ) + "\n".join(minmaxes)
1850
1929
 
1851
1930
 
1852
1931
  class OS2Field(Statement):
@@ -2098,7 +2177,7 @@ class VariationBlock(Block):
2098
2177
  def build(self, builder):
2099
2178
  """Call the ``start_feature`` callback on the builder object, visit
2100
2179
  all the statements in this feature, and then call ``end_feature``."""
2101
- builder.start_feature(self.location, self.name)
2180
+ builder.start_feature(self.location, self.name, self.use_extension)
2102
2181
  if (
2103
2182
  self.conditionset != "NULL"
2104
2183
  and self.conditionset not in builder.conditionsets_
@@ -126,6 +126,7 @@ class Builder(object):
126
126
  self.script_ = None
127
127
  self.lookupflag_ = 0
128
128
  self.lookupflag_markFilterSet_ = None
129
+ self.use_extension_ = False
129
130
  self.language_systems = set()
130
131
  self.seen_non_DFLT_script_ = False
131
132
  self.named_lookups_ = {}
@@ -141,6 +142,7 @@ class Builder(object):
141
142
  self.aalt_features_ = [] # [(location, featureName)*], for 'aalt'
142
143
  self.aalt_location_ = None
143
144
  self.aalt_alternates_ = {}
145
+ self.aalt_use_extension_ = False
144
146
  # for 'featureNames'
145
147
  self.featureNames_ = set()
146
148
  self.featureNames_ids_ = {}
@@ -247,6 +249,7 @@ class Builder(object):
247
249
  result = builder_class(self.font, location)
248
250
  result.lookupflag = self.lookupflag_
249
251
  result.markFilterSet = self.lookupflag_markFilterSet_
252
+ result.extension = self.use_extension_
250
253
  self.lookups_.append(result)
251
254
  return result
252
255
 
@@ -272,6 +275,7 @@ class Builder(object):
272
275
  self.cur_lookup_ = builder_class(self.font, location)
273
276
  self.cur_lookup_.lookupflag = self.lookupflag_
274
277
  self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_
278
+ self.cur_lookup_.extension = self.use_extension_
275
279
  self.lookups_.append(self.cur_lookup_)
276
280
  if self.cur_lookup_name_:
277
281
  # We are starting a lookup rule inside a named lookup block.
@@ -323,7 +327,7 @@ class Builder(object):
323
327
  }
324
328
  old_lookups = self.lookups_
325
329
  self.lookups_ = []
326
- self.start_feature(self.aalt_location_, "aalt")
330
+ self.start_feature(self.aalt_location_, "aalt", self.aalt_use_extension_)
327
331
  if single:
328
332
  single_lookup = self.get_lookup_(location, SingleSubstBuilder)
329
333
  single_lookup.mapping = single
@@ -341,6 +345,7 @@ class Builder(object):
341
345
  table = self.font["head"] = newTable("head")
342
346
  table.decompile(b"\0" * 54, self.font)
343
347
  table.tableVersion = 1.0
348
+ table.magicNumber = 0x5F0F3CF5
344
349
  table.created = table.modified = 3406620153 # 2011-12-13 11:22:33
345
350
  table.fontRevision = self.fontRevision_
346
351
 
@@ -727,10 +732,16 @@ class Builder(object):
727
732
  result.table = base
728
733
  return result
729
734
 
735
+ def buildBASECoord(self, c):
736
+ coord = otTables.BaseCoord()
737
+ coord.Format = 1
738
+ coord.Coordinate = c
739
+ return coord
740
+
730
741
  def buildBASEAxis(self, axis):
731
742
  if not axis:
732
743
  return
733
- bases, scripts = axis
744
+ bases, scripts, minmax = axis
734
745
  axis = otTables.Axis()
735
746
  axis.BaseTagList = otTables.BaseTagList()
736
747
  axis.BaseTagList.BaselineTag = bases
@@ -739,19 +750,35 @@ class Builder(object):
739
750
  axis.BaseScriptList.BaseScriptRecord = []
740
751
  axis.BaseScriptList.BaseScriptCount = len(scripts)
741
752
  for script in sorted(scripts):
753
+ minmax_for_script = [
754
+ record[1:] for record in minmax if record[0] == script[0]
755
+ ]
742
756
  record = otTables.BaseScriptRecord()
743
757
  record.BaseScriptTag = script[0]
744
758
  record.BaseScript = otTables.BaseScript()
745
- record.BaseScript.BaseLangSysCount = 0
746
759
  record.BaseScript.BaseValues = otTables.BaseValues()
747
760
  record.BaseScript.BaseValues.DefaultIndex = bases.index(script[1])
748
761
  record.BaseScript.BaseValues.BaseCoord = []
749
762
  record.BaseScript.BaseValues.BaseCoordCount = len(script[2])
763
+ record.BaseScript.BaseLangSysRecord = []
764
+
750
765
  for c in script[2]:
751
- coord = otTables.BaseCoord()
752
- coord.Format = 1
753
- coord.Coordinate = c
754
- record.BaseScript.BaseValues.BaseCoord.append(coord)
766
+ record.BaseScript.BaseValues.BaseCoord.append(self.buildBASECoord(c))
767
+ for language, min_coord, max_coord in minmax_for_script:
768
+ minmax_record = otTables.MinMax()
769
+ minmax_record.MinCoord = self.buildBASECoord(min_coord)
770
+ minmax_record.MaxCoord = self.buildBASECoord(max_coord)
771
+ minmax_record.FeatMinMaxCount = 0
772
+ if language == "dflt":
773
+ record.BaseScript.DefaultMinMax = minmax_record
774
+ else:
775
+ lang_record = otTables.BaseLangSysRecord()
776
+ lang_record.BaseLangSysTag = language
777
+ lang_record.MinMax = minmax_record
778
+ record.BaseScript.BaseLangSysRecord.append(lang_record)
779
+ record.BaseScript.BaseLangSysCount = len(
780
+ record.BaseScript.BaseLangSysRecord
781
+ )
755
782
  axis.BaseScriptList.BaseScriptRecord.append(record)
756
783
  return axis
757
784
 
@@ -1031,15 +1058,22 @@ class Builder(object):
1031
1058
  else:
1032
1059
  return frozenset({("DFLT", "dflt")})
1033
1060
 
1034
- def start_feature(self, location, name):
1061
+ def start_feature(self, location, name, use_extension=False):
1062
+ if use_extension and name != "aalt":
1063
+ raise FeatureLibError(
1064
+ "'useExtension' keyword for feature blocks is allowed only for 'aalt' feature",
1065
+ location,
1066
+ )
1035
1067
  self.language_systems = self.get_default_language_systems_()
1036
1068
  self.script_ = "DFLT"
1037
1069
  self.cur_lookup_ = None
1038
1070
  self.cur_feature_name_ = name
1039
1071
  self.lookupflag_ = 0
1040
1072
  self.lookupflag_markFilterSet_ = None
1073
+ self.use_extension_ = use_extension
1041
1074
  if name == "aalt":
1042
1075
  self.aalt_location_ = location
1076
+ self.aalt_use_extension_ = use_extension
1043
1077
 
1044
1078
  def end_feature(self):
1045
1079
  assert self.cur_feature_name_ is not None
@@ -1048,8 +1082,9 @@ class Builder(object):
1048
1082
  self.cur_lookup_ = None
1049
1083
  self.lookupflag_ = 0
1050
1084
  self.lookupflag_markFilterSet_ = None
1085
+ self.use_extension_ = False
1051
1086
 
1052
- def start_lookup_block(self, location, name):
1087
+ def start_lookup_block(self, location, name, use_extension=False):
1053
1088
  if name in self.named_lookups_:
1054
1089
  raise FeatureLibError(
1055
1090
  'Lookup "%s" has already been defined' % name, location
@@ -1063,6 +1098,7 @@ class Builder(object):
1063
1098
  self.cur_lookup_name_ = name
1064
1099
  self.named_lookups_[name] = None
1065
1100
  self.cur_lookup_ = None
1101
+ self.use_extension_ = use_extension
1066
1102
  if self.cur_feature_name_ is None:
1067
1103
  self.lookupflag_ = 0
1068
1104
  self.lookupflag_markFilterSet_ = None
@@ -1071,6 +1107,7 @@ class Builder(object):
1071
1107
  assert self.cur_lookup_name_ is not None
1072
1108
  self.cur_lookup_name_ = None
1073
1109
  self.cur_lookup_ = None
1110
+ self.use_extension_ = False
1074
1111
  if self.cur_feature_name_ is None:
1075
1112
  self.lookupflag_ = 0
1076
1113
  self.lookupflag_markFilterSet_ = None
@@ -1235,11 +1272,11 @@ class Builder(object):
1235
1272
  def add_cv_character(self, character, tag):
1236
1273
  self.cv_characters_[tag].append(character)
1237
1274
 
1238
- def set_base_axis(self, bases, scripts, vertical):
1275
+ def set_base_axis(self, bases, scripts, vertical, minmax=[]):
1239
1276
  if vertical:
1240
- self.base_vert_axis_ = (bases, scripts)
1277
+ self.base_vert_axis_ = (bases, scripts, minmax)
1241
1278
  else:
1242
- self.base_horiz_axis_ = (bases, scripts)
1279
+ self.base_horiz_axis_ = (bases, scripts, minmax)
1243
1280
 
1244
1281
  def set_size_parameters(
1245
1282
  self, location, DesignSize, SubfamilyID, RangeStart, RangeEnd
@@ -1448,7 +1485,9 @@ class Builder(object):
1448
1485
  lookup = self.get_lookup_(location, PairPosBuilder)
1449
1486
  v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
1450
1487
  v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
1451
- lookup.addClassPair(location, glyphclass1, v1, glyphclass2, v2)
1488
+ cls1 = tuple(sorted(set(glyphclass1)))
1489
+ cls2 = tuple(sorted(set(glyphclass2)))
1490
+ lookup.addClassPair(location, cls1, v1, cls2, v2)
1452
1491
 
1453
1492
  def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
1454
1493
  if not glyph1 or not glyph2: