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
@@ -1,5 +1,7 @@
1
1
  """Variation fonts interpolation models."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  __all__ = [
4
6
  "normalizeValue",
5
7
  "normalizeLocation",
@@ -8,10 +10,15 @@ __all__ = [
8
10
  "VariationModel",
9
11
  ]
10
12
 
13
+ from typing import TYPE_CHECKING
11
14
  from fontTools.misc.roundTools import noRound
12
15
  from .errors import VariationModelError
13
16
 
14
17
 
18
+ if TYPE_CHECKING:
19
+ from typing import Mapping, Sequence
20
+
21
+
15
22
  def nonNone(lst):
16
23
  return [l for l in lst if l is not None]
17
24
 
@@ -44,7 +51,9 @@ def subList(truth, lst):
44
51
  return [l for l, t in zip(lst, truth) if t]
45
52
 
46
53
 
47
- def normalizeValue(v, triple, extrapolate=False):
54
+ def normalizeValue(
55
+ v: float, triple: Sequence[float], extrapolate: bool = False
56
+ ) -> float:
48
57
  """Normalizes value based on a min/default/max triple.
49
58
 
50
59
  >>> normalizeValue(400, (100, 400, 900))
@@ -75,7 +84,13 @@ def normalizeValue(v, triple, extrapolate=False):
75
84
  return (v - default) / (upper - default)
76
85
 
77
86
 
78
- def normalizeLocation(location, axes, extrapolate=False, *, validate=False):
87
+ def normalizeLocation(
88
+ location: Mapping[str, float],
89
+ axes: Mapping[str, tuple[float, float, float]],
90
+ extrapolate: bool = False,
91
+ *,
92
+ validate: bool = False,
93
+ ) -> dict[str, float]:
79
94
  """Normalizes location based on axis min/default/max values from axes.
80
95
 
81
96
  >>> axes = {"wght": (100, 400, 900)}
@@ -4,9 +4,16 @@ Instantiate a variation font. Run, eg:
4
4
  .. code-block:: sh
5
5
 
6
6
  $ fonttools varLib.mutator ./NotoSansArabic-VF.ttf wght=140 wdth=85
7
+
8
+ .. warning::
9
+ ``fontTools.varLib.mutator`` is deprecated in favor of :mod:`fontTools.varLib.instancer`
10
+ which provides equivalent full instancing and also supports partial instancing.
11
+ Please migrate CLI usage to ``fonttools varLib.instancer`` and API usage to
12
+ :func:`fontTools.varLib.instancer.instantiateVariableFont`.
7
13
  """
8
14
 
9
15
  from fontTools.misc.fixedTools import floatToFixedToFloat, floatToFixed
16
+ from fontTools.misc.loggingTools import deprecateFunction
10
17
  from fontTools.misc.roundTools import otRound
11
18
  from fontTools.pens.boundsPen import BoundsPen
12
19
  from fontTools.ttLib import TTFont, newTable
@@ -159,6 +166,10 @@ def interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc):
159
166
  hmtx[gname] = tuple(entry)
160
167
 
161
168
 
169
+ @deprecateFunction(
170
+ "use fontTools.varLib.instancer.instantiateVariableFont instead "
171
+ "for either full or partial instancing",
172
+ )
162
173
  def instantiateVariableFont(varfont, location, inplace=False, overlap=True):
163
174
  """Generate a static instance from a variable TTFont and a dictionary
164
175
  defining the desired location along the variable font's axes.
@@ -41,7 +41,7 @@ class OnlineVarStoreBuilder(object):
41
41
  def setSupports(self, supports):
42
42
  self._model = None
43
43
  self._supports = list(supports)
44
- if not self._supports[0]:
44
+ if self._supports and not self._supports[0]:
45
45
  del self._supports[0] # Drop base master support
46
46
  self._cache = None
47
47
  self._data = None
@@ -412,25 +412,6 @@ class _Encoding(object):
412
412
  def extend(self, lst):
413
413
  self.items.update(lst)
414
414
 
415
- def get_room(self):
416
- """Maximum number of bytes that can be added to characteristic
417
- while still being beneficial to merge it into another one."""
418
- count = len(self.items)
419
- return max(0, (self.overhead - 1) // count - self.width)
420
-
421
- room = property(get_room)
422
-
423
- def get_gain(self):
424
- """Maximum possible byte gain from merging this into another
425
- characteristic."""
426
- count = len(self.items)
427
- return max(0, self.overhead - count)
428
-
429
- gain = property(get_gain)
430
-
431
- def gain_sort_key(self):
432
- return self.gain, self.chars
433
-
434
415
  def width_sort_key(self):
435
416
  return self.width, self.chars
436
417
 
@@ -534,13 +515,9 @@ def VarStore_optimize(self, use_NO_VARIATION_INDEX=True, quantization=1):
534
515
  # of the old encoding is completely eliminated. However, each row
535
516
  # now would require more bytes to encode, to the tune of one byte
536
517
  # per characteristic bit that is active in the new encoding but not
537
- # in the old one. The number of bits that can be added to an encoding
538
- # while still beneficial to merge it into another encoding is called
539
- # the "room" for that encoding.
518
+ # in the old one.
540
519
  #
541
- # The "gain" of an encodings is the maximum number of bytes we can
542
- # save by merging it into another encoding. The "gain" of merging
543
- # two encodings is how many bytes we save by doing so.
520
+ # The "gain" of merging two encodings is how many bytes we save by doing so.
544
521
  #
545
522
  # High-level algorithm:
546
523
  #
@@ -554,7 +531,11 @@ def VarStore_optimize(self, use_NO_VARIATION_INDEX=True, quantization=1):
554
531
  #
555
532
  # - Put all encodings into a "todo" list.
556
533
  #
557
- # - Sort todo list by decreasing gain (for stability).
534
+ # - Sort todo list (for stability) by width_sort_key(), which is a tuple
535
+ # of the following items:
536
+ # * The "width" of the encoding.
537
+ # * The characteristic bitmap of the encoding, with higher-numbered
538
+ # columns compared first.
558
539
  #
559
540
  # - Make a priority-queue of the gain from combining each two
560
541
  # encodings in the todo list. The priority queue is sorted by
@@ -575,16 +556,7 @@ def VarStore_optimize(self, use_NO_VARIATION_INDEX=True, quantization=1):
575
556
  #
576
557
  # The output is then sorted for stability, in the following way:
577
558
  # - The VarRegionList of the input is kept intact.
578
- # - All encodings are sorted before the main algorithm, by
579
- # gain_key_sort(), which is a tuple of the following items:
580
- # * The gain of the encoding.
581
- # * The characteristic bitmap of the encoding, with higher-numbered
582
- # columns compared first.
583
- # - The VarData is sorted by width_sort_key(), which is a tuple
584
- # of the following items:
585
- # * The "width" of the encoding.
586
- # * The characteristic bitmap of the encoding, with higher-numbered
587
- # columns compared first.
559
+ # - The VarData is sorted by the same width_sort_key() used at the beginning.
588
560
  # - Within each VarData, the items are sorted as vectors of numbers.
589
561
  #
590
562
  # Finally, each VarData is optimized to remove the empty columns and
@@ -626,7 +598,7 @@ def VarStore_optimize(self, use_NO_VARIATION_INDEX=True, quantization=1):
626
598
  front_mapping[(major << 16) + minor] = row
627
599
 
628
600
  # Prepare for the main algorithm.
629
- todo = sorted(encodings.values(), key=_Encoding.gain_sort_key)
601
+ todo = sorted(encodings.values(), key=_Encoding.width_sort_key)
630
602
  del encodings
631
603
 
632
604
  # Repeatedly pick two best encodings to combine, and combine them.
@@ -0,0 +1,206 @@
1
+ import argparse
2
+ import logging
3
+ import sys
4
+ from io import StringIO
5
+ from pathlib import Path
6
+
7
+ from fontTools import configLogger
8
+ from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
9
+ from fontTools.feaLib.error import FeatureLibError
10
+ from fontTools.feaLib.lexer import Lexer
11
+ from fontTools.misc.cliTools import makeOutputFileName
12
+ from fontTools.ttLib import TTFont, TTLibError
13
+ from fontTools.voltLib.parser import Parser
14
+ from fontTools.voltLib.voltToFea import TABLES, VoltToFea
15
+
16
+ log = logging.getLogger("fontTools.feaLib")
17
+
18
+ SUPPORTED_TABLES = TABLES + ["cmap"]
19
+
20
+
21
+ def invalid_fea_glyph_name(name):
22
+ """Check if the glyph name is valid according to FEA syntax."""
23
+ if name[0] not in Lexer.CHAR_NAME_START_:
24
+ return True
25
+ if any(c not in Lexer.CHAR_NAME_CONTINUATION_ for c in name[1:]):
26
+ return True
27
+ return False
28
+
29
+
30
+ def sanitize_glyph_name(name):
31
+ """Sanitize the glyph name to ensure it is valid according to FEA syntax."""
32
+ sanitized = ""
33
+ for i, c in enumerate(name):
34
+ if i == 0 and c not in Lexer.CHAR_NAME_START_:
35
+ sanitized += "a" + c
36
+ elif c not in Lexer.CHAR_NAME_CONTINUATION_:
37
+ sanitized += "_"
38
+ else:
39
+ sanitized += c
40
+
41
+ return sanitized
42
+
43
+
44
+ def main(args=None):
45
+ """Build tables from a MS VOLT project into an OTF font"""
46
+ parser = argparse.ArgumentParser(
47
+ description="Use fontTools to compile MS VOLT projects."
48
+ )
49
+ parser.add_argument(
50
+ "input",
51
+ metavar="INPUT",
52
+ help="Path to the input font/VTP file to process",
53
+ type=Path,
54
+ )
55
+ parser.add_argument(
56
+ "-f",
57
+ "--font",
58
+ metavar="INPUT_FONT",
59
+ help="Path to the input font (if INPUT is a VTP file)",
60
+ type=Path,
61
+ )
62
+ parser.add_argument(
63
+ "-o",
64
+ "--output",
65
+ dest="output",
66
+ metavar="OUTPUT",
67
+ help="Path to the output font.",
68
+ type=Path,
69
+ )
70
+ parser.add_argument(
71
+ "-t",
72
+ "--tables",
73
+ metavar="TABLE_TAG",
74
+ choices=SUPPORTED_TABLES,
75
+ nargs="+",
76
+ help="Specify the table(s) to be built.",
77
+ )
78
+ parser.add_argument(
79
+ "-F",
80
+ "--debug-feature-file",
81
+ help="Write the generated feature file to disk.",
82
+ action="store_true",
83
+ )
84
+ parser.add_argument(
85
+ "--ship",
86
+ help="Remove source VOLT tables from output font.",
87
+ action="store_true",
88
+ )
89
+ parser.add_argument(
90
+ "-v",
91
+ "--verbose",
92
+ help="Increase the logger verbosity. Multiple -v options are allowed.",
93
+ action="count",
94
+ default=0,
95
+ )
96
+ parser.add_argument(
97
+ "-T",
98
+ "--traceback",
99
+ help="show traceback for exceptions.",
100
+ action="store_true",
101
+ )
102
+ options = parser.parse_args(args)
103
+
104
+ levels = ["WARNING", "INFO", "DEBUG"]
105
+ configLogger(level=levels[min(len(levels) - 1, options.verbose)])
106
+
107
+ output_font = options.output or Path(
108
+ makeOutputFileName(options.font or options.input)
109
+ )
110
+ log.info(f"Compiling MS VOLT to '{output_font}'")
111
+
112
+ file_or_path = options.input
113
+ font = None
114
+
115
+ # If the input is a font file, extract the VOLT data from the "TSIV" table
116
+ try:
117
+ font = TTFont(file_or_path)
118
+ if "TSIV" in font:
119
+ file_or_path = StringIO(font["TSIV"].data.decode("utf-8"))
120
+ else:
121
+ log.error('"TSIV" table is missing')
122
+ return 1
123
+ except TTLibError:
124
+ pass
125
+
126
+ # If input is not a font file, the font must be provided
127
+ if font is None:
128
+ if not options.font:
129
+ log.error("Please provide an input font")
130
+ return 1
131
+ font = TTFont(options.font)
132
+
133
+ # FEA syntax does not allow some glyph names that VOLT accepts, so if we
134
+ # found such glyph name we will temporarily rename such glyphs.
135
+ glyphOrder = font.getGlyphOrder()
136
+ tempGlyphOrder = None
137
+ if any(invalid_fea_glyph_name(n) for n in glyphOrder):
138
+ tempGlyphOrder = []
139
+ for n in glyphOrder:
140
+ if invalid_fea_glyph_name(n):
141
+ n = sanitize_glyph_name(n)
142
+ existing = set(tempGlyphOrder) | set(glyphOrder)
143
+ while n in existing:
144
+ n = "a" + n
145
+ tempGlyphOrder.append(n)
146
+ font.setGlyphOrder(tempGlyphOrder)
147
+
148
+ doc = Parser(file_or_path).parse()
149
+
150
+ log.info("Converting VTP data to FEA")
151
+ converter = VoltToFea(doc, font)
152
+ try:
153
+ fea = converter.convert(options.tables, ignore_unsupported_settings=True)
154
+ except NotImplementedError as e:
155
+ if options.traceback:
156
+ raise
157
+ location = getattr(e.args[0], "location", None)
158
+ message = f'"{e}" is not supported'
159
+ if location:
160
+ path, line, column = location
161
+ log.error(f"{path}:{line}:{column}: {message}")
162
+ else:
163
+ log.error(message)
164
+ return 1
165
+
166
+ fea_filename = options.input
167
+ if options.debug_feature_file:
168
+ fea_filename = output_font.with_suffix(".fea")
169
+ log.info(f"Writing FEA to '{fea_filename}'")
170
+ with open(fea_filename, "w") as fp:
171
+ fp.write(fea)
172
+
173
+ log.info("Compiling FEA to OpenType tables")
174
+ try:
175
+ addOpenTypeFeaturesFromString(
176
+ font,
177
+ fea,
178
+ filename=fea_filename,
179
+ tables=options.tables,
180
+ )
181
+ except FeatureLibError as e:
182
+ if options.traceback:
183
+ raise
184
+ log.error(e)
185
+ return 1
186
+
187
+ if options.ship:
188
+ for tag in ["TSIV", "TSIS", "TSIP", "TSID"]:
189
+ if tag in font:
190
+ del font[tag]
191
+
192
+ # Restore original glyph names.
193
+ if tempGlyphOrder:
194
+ import io
195
+
196
+ f = io.BytesIO()
197
+ font.save(f)
198
+ font = TTFont(f)
199
+ font.setGlyphOrder(glyphOrder)
200
+ font["post"].extraNames = []
201
+
202
+ font.save(output_font)
203
+
204
+
205
+ if __name__ == "__main__":
206
+ sys.exit(main())
fontTools/voltLib/ast.py CHANGED
@@ -317,6 +317,10 @@ class SubstitutionLigatureDefinition(SubstitutionDefinition):
317
317
  pass
318
318
 
319
319
 
320
+ class SubstitutionAlternateDefinition(SubstitutionDefinition):
321
+ pass
322
+
323
+
320
324
  class SubstitutionReverseChainingSingleDefinition(SubstitutionDefinition):
321
325
  pass
322
326
 
@@ -313,19 +313,27 @@ class Parser(object):
313
313
  self.expect_keyword_("END_SUBSTITUTION")
314
314
  max_src = max([len(cov) for cov in src])
315
315
  max_dest = max([len(cov) for cov in dest])
316
+
316
317
  # many to many or mixed is invalid
317
- if (max_src > 1 and max_dest > 1) or (
318
- reversal and (max_src > 1 or max_dest > 1)
319
- ):
318
+ if max_src > 1 and max_dest > 1:
320
319
  raise VoltLibError("Invalid substitution type", location)
320
+
321
321
  mapping = dict(zip(tuple(src), tuple(dest)))
322
322
  if max_src == 1 and max_dest == 1:
323
- if reversal:
324
- sub = ast.SubstitutionReverseChainingSingleDefinition(
325
- mapping, location=location
326
- )
323
+ # Alternate substitutions are represented by adding multiple
324
+ # substitutions for the same glyph, so we detect that here
325
+ glyphs = [x.glyphSet() for cov in src for x in cov] # flatten src
326
+ if len(set(glyphs)) != len(glyphs): # src has duplicates
327
+ sub = ast.SubstitutionAlternateDefinition(mapping, location=location)
327
328
  else:
328
- sub = ast.SubstitutionSingleDefinition(mapping, location=location)
329
+ if reversal:
330
+ # Reversal is valid only for single glyph substitutions
331
+ # and VOLT ignores it otherwise.
332
+ sub = ast.SubstitutionReverseChainingSingleDefinition(
333
+ mapping, location=location
334
+ )
335
+ else:
336
+ sub = ast.SubstitutionSingleDefinition(mapping, location=location)
329
337
  elif max_src == 1 and max_dest > 1:
330
338
  sub = ast.SubstitutionMultipleDefinition(mapping, location=location)
331
339
  elif max_src > 1 and max_dest == 1: