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
@@ -29,8 +29,10 @@ from fontTools.otlLib.builder import (
29
29
  PairPosBuilder,
30
30
  SinglePosBuilder,
31
31
  ChainContextualRule,
32
+ AnySubstBuilder,
32
33
  )
33
34
  from fontTools.otlLib.error import OpenTypeLibError
35
+ from fontTools.varLib.errors import VarLibError
34
36
  from fontTools.varLib.varStore import OnlineVarStoreBuilder
35
37
  from fontTools.varLib.builder import buildVarDevTable
36
38
  from fontTools.varLib.featureVars import addFeatureVariationsRaw
@@ -126,6 +128,7 @@ class Builder(object):
126
128
  self.script_ = None
127
129
  self.lookupflag_ = 0
128
130
  self.lookupflag_markFilterSet_ = None
131
+ self.use_extension_ = False
129
132
  self.language_systems = set()
130
133
  self.seen_non_DFLT_script_ = False
131
134
  self.named_lookups_ = {}
@@ -141,6 +144,7 @@ class Builder(object):
141
144
  self.aalt_features_ = [] # [(location, featureName)*], for 'aalt'
142
145
  self.aalt_location_ = None
143
146
  self.aalt_alternates_ = {}
147
+ self.aalt_use_extension_ = False
144
148
  # for 'featureNames'
145
149
  self.featureNames_ = set()
146
150
  self.featureNames_ids_ = {}
@@ -247,6 +251,7 @@ class Builder(object):
247
251
  result = builder_class(self.font, location)
248
252
  result.lookupflag = self.lookupflag_
249
253
  result.markFilterSet = self.lookupflag_markFilterSet_
254
+ result.extension = self.use_extension_
250
255
  self.lookups_.append(result)
251
256
  return result
252
257
 
@@ -255,12 +260,13 @@ class Builder(object):
255
260
  key = (script, lang, feature_name)
256
261
  self.features_.setdefault(key, []).append(lookup)
257
262
 
258
- def get_lookup_(self, location, builder_class):
263
+ def get_lookup_(self, location, builder_class, mapping=None):
259
264
  if (
260
265
  self.cur_lookup_
261
266
  and type(self.cur_lookup_) == builder_class
262
267
  and self.cur_lookup_.lookupflag == self.lookupflag_
263
268
  and self.cur_lookup_.markFilterSet == self.lookupflag_markFilterSet_
269
+ and self.cur_lookup_.can_add_mapping(mapping)
264
270
  ):
265
271
  return self.cur_lookup_
266
272
  if self.cur_lookup_name_ and self.cur_lookup_:
@@ -272,6 +278,7 @@ class Builder(object):
272
278
  self.cur_lookup_ = builder_class(self.font, location)
273
279
  self.cur_lookup_.lookupflag = self.lookupflag_
274
280
  self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_
281
+ self.cur_lookup_.extension = self.use_extension_
275
282
  self.lookups_.append(self.cur_lookup_)
276
283
  if self.cur_lookup_name_:
277
284
  # We are starting a lookup rule inside a named lookup block.
@@ -323,7 +330,7 @@ class Builder(object):
323
330
  }
324
331
  old_lookups = self.lookups_
325
332
  self.lookups_ = []
326
- self.start_feature(self.aalt_location_, "aalt")
333
+ self.start_feature(self.aalt_location_, "aalt", self.aalt_use_extension_)
327
334
  if single:
328
335
  single_lookup = self.get_lookup_(location, SingleSubstBuilder)
329
336
  single_lookup.mapping = single
@@ -341,6 +348,7 @@ class Builder(object):
341
348
  table = self.font["head"] = newTable("head")
342
349
  table.decompile(b"\0" * 54, self.font)
343
350
  table.tableVersion = 1.0
351
+ table.magicNumber = 0x5F0F3CF5
344
352
  table.created = table.modified = 3406620153 # 2011-12-13 11:22:33
345
353
  table.fontRevision = self.fontRevision_
346
354
 
@@ -727,10 +735,16 @@ class Builder(object):
727
735
  result.table = base
728
736
  return result
729
737
 
738
+ def buildBASECoord(self, c):
739
+ coord = otTables.BaseCoord()
740
+ coord.Format = 1
741
+ coord.Coordinate = c
742
+ return coord
743
+
730
744
  def buildBASEAxis(self, axis):
731
745
  if not axis:
732
746
  return
733
- bases, scripts = axis
747
+ bases, scripts, minmax = axis
734
748
  axis = otTables.Axis()
735
749
  axis.BaseTagList = otTables.BaseTagList()
736
750
  axis.BaseTagList.BaselineTag = bases
@@ -739,19 +753,35 @@ class Builder(object):
739
753
  axis.BaseScriptList.BaseScriptRecord = []
740
754
  axis.BaseScriptList.BaseScriptCount = len(scripts)
741
755
  for script in sorted(scripts):
756
+ minmax_for_script = [
757
+ record[1:] for record in minmax if record[0] == script[0]
758
+ ]
742
759
  record = otTables.BaseScriptRecord()
743
760
  record.BaseScriptTag = script[0]
744
761
  record.BaseScript = otTables.BaseScript()
745
- record.BaseScript.BaseLangSysCount = 0
746
762
  record.BaseScript.BaseValues = otTables.BaseValues()
747
763
  record.BaseScript.BaseValues.DefaultIndex = bases.index(script[1])
748
764
  record.BaseScript.BaseValues.BaseCoord = []
749
765
  record.BaseScript.BaseValues.BaseCoordCount = len(script[2])
766
+ record.BaseScript.BaseLangSysRecord = []
767
+
750
768
  for c in script[2]:
751
- coord = otTables.BaseCoord()
752
- coord.Format = 1
753
- coord.Coordinate = c
754
- record.BaseScript.BaseValues.BaseCoord.append(coord)
769
+ record.BaseScript.BaseValues.BaseCoord.append(self.buildBASECoord(c))
770
+ for language, min_coord, max_coord in sorted(minmax_for_script):
771
+ minmax_record = otTables.MinMax()
772
+ minmax_record.MinCoord = self.buildBASECoord(min_coord)
773
+ minmax_record.MaxCoord = self.buildBASECoord(max_coord)
774
+ minmax_record.FeatMinMaxCount = 0
775
+ if language == "dflt":
776
+ record.BaseScript.DefaultMinMax = minmax_record
777
+ else:
778
+ lang_record = otTables.BaseLangSysRecord()
779
+ lang_record.BaseLangSysTag = language
780
+ lang_record.MinMax = minmax_record
781
+ record.BaseScript.BaseLangSysRecord.append(lang_record)
782
+ record.BaseScript.BaseLangSysCount = len(
783
+ record.BaseScript.BaseLangSysRecord
784
+ )
755
785
  axis.BaseScriptList.BaseScriptRecord.append(record)
756
786
  return axis
757
787
 
@@ -839,13 +869,22 @@ class Builder(object):
839
869
  for lookup in self.lookups_:
840
870
  if lookup.table != tag:
841
871
  continue
842
- lookup.lookup_index = len(lookups)
843
- self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo(
844
- location=str(lookup.location),
845
- name=self.get_lookup_name_(lookup),
846
- feature=None,
847
- )
848
- lookups.append(lookup)
872
+ name = self.get_lookup_name_(lookup)
873
+ resolved = lookup.promote_lookup_type(is_named_lookup=name is not None)
874
+ if resolved is None:
875
+ raise FeatureLibError(
876
+ "Within a named lookup block, all rules must be of "
877
+ "the same lookup type and flag",
878
+ lookup.location,
879
+ )
880
+ for l in resolved:
881
+ lookup.lookup_index = len(lookups)
882
+ self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo(
883
+ location=str(lookup.location),
884
+ name=name,
885
+ feature=None,
886
+ )
887
+ lookups.append(l)
849
888
  otLookups = []
850
889
  for l in lookups:
851
890
  try:
@@ -888,6 +927,11 @@ class Builder(object):
888
927
  l.lookup_index for l in lookups if l.lookup_index is not None
889
928
  )
890
929
  )
930
+ # order doesn't matter, but lookup_indices preserves it.
931
+ # We want to combine identical sets of lookups (order doesn't matter)
932
+ # but also respect the order provided by the user (although there's
933
+ # a reasonable argument to just sort and dedupe, which fontc does)
934
+ lookup_key = frozenset(lookup_indices)
891
935
 
892
936
  size_feature = tag == "GPOS" and feature_tag == "size"
893
937
  force_feature = self.any_feature_variations(feature_tag, tag)
@@ -905,7 +949,7 @@ class Builder(object):
905
949
  "stash debug information. See fonttools#2065."
906
950
  )
907
951
 
908
- feature_key = (feature_tag, lookup_indices)
952
+ feature_key = (feature_tag, lookup_key)
909
953
  feature_index = feature_indices.get(feature_key)
910
954
  if feature_index is None:
911
955
  feature_index = len(table.FeatureList.FeatureRecord)
@@ -1031,15 +1075,22 @@ class Builder(object):
1031
1075
  else:
1032
1076
  return frozenset({("DFLT", "dflt")})
1033
1077
 
1034
- def start_feature(self, location, name):
1078
+ def start_feature(self, location, name, use_extension=False):
1079
+ if use_extension and name != "aalt":
1080
+ raise FeatureLibError(
1081
+ "'useExtension' keyword for feature blocks is allowed only for 'aalt' feature",
1082
+ location,
1083
+ )
1035
1084
  self.language_systems = self.get_default_language_systems_()
1036
1085
  self.script_ = "DFLT"
1037
1086
  self.cur_lookup_ = None
1038
1087
  self.cur_feature_name_ = name
1039
1088
  self.lookupflag_ = 0
1040
1089
  self.lookupflag_markFilterSet_ = None
1090
+ self.use_extension_ = use_extension
1041
1091
  if name == "aalt":
1042
1092
  self.aalt_location_ = location
1093
+ self.aalt_use_extension_ = use_extension
1043
1094
 
1044
1095
  def end_feature(self):
1045
1096
  assert self.cur_feature_name_ is not None
@@ -1048,8 +1099,9 @@ class Builder(object):
1048
1099
  self.cur_lookup_ = None
1049
1100
  self.lookupflag_ = 0
1050
1101
  self.lookupflag_markFilterSet_ = None
1102
+ self.use_extension_ = False
1051
1103
 
1052
- def start_lookup_block(self, location, name):
1104
+ def start_lookup_block(self, location, name, use_extension=False):
1053
1105
  if name in self.named_lookups_:
1054
1106
  raise FeatureLibError(
1055
1107
  'Lookup "%s" has already been defined' % name, location
@@ -1063,6 +1115,7 @@ class Builder(object):
1063
1115
  self.cur_lookup_name_ = name
1064
1116
  self.named_lookups_[name] = None
1065
1117
  self.cur_lookup_ = None
1118
+ self.use_extension_ = use_extension
1066
1119
  if self.cur_feature_name_ is None:
1067
1120
  self.lookupflag_ = 0
1068
1121
  self.lookupflag_markFilterSet_ = None
@@ -1071,6 +1124,7 @@ class Builder(object):
1071
1124
  assert self.cur_lookup_name_ is not None
1072
1125
  self.cur_lookup_name_ = None
1073
1126
  self.cur_lookup_ = None
1127
+ self.use_extension_ = False
1074
1128
  if self.cur_feature_name_ is None:
1075
1129
  self.lookupflag_ = 0
1076
1130
  self.lookupflag_markFilterSet_ = None
@@ -1106,7 +1160,13 @@ class Builder(object):
1106
1160
  if (language == "dflt" or include_default) and lookups:
1107
1161
  self.features_[key] = lookups[:]
1108
1162
  else:
1109
- self.features_[key] = []
1163
+ # if we aren't including default we need to manually remove the
1164
+ # default lookups, which were added to all declared langsystems
1165
+ # as they were encountered (we don't remove all lookups because
1166
+ # we want to allow duplicate script/lang statements;
1167
+ # see https://github.com/fonttools/fonttools/issues/3748
1168
+ cur_lookups = self.features_.get(key, [])
1169
+ self.features_[key] = [x for x in cur_lookups if x not in lookups]
1110
1170
  self.language_systems = frozenset([(self.script_, language)])
1111
1171
 
1112
1172
  if required:
@@ -1153,10 +1213,10 @@ class Builder(object):
1153
1213
 
1154
1214
  def set_lookup_flag(self, location, value, markAttach, markFilter):
1155
1215
  value = value & 0xFF
1156
- if markAttach:
1216
+ if markAttach is not None:
1157
1217
  markAttachClass = self.getMarkAttachClass_(location, markAttach)
1158
1218
  value = value | (markAttachClass << 8)
1159
- if markFilter:
1219
+ if markFilter is not None:
1160
1220
  markFilterSet = self.getMarkFilterSet_(location, markFilter)
1161
1221
  value = value | 0x10
1162
1222
  self.lookupflag_markFilterSet_ = markFilterSet
@@ -1229,11 +1289,11 @@ class Builder(object):
1229
1289
  def add_cv_character(self, character, tag):
1230
1290
  self.cv_characters_[tag].append(character)
1231
1291
 
1232
- def set_base_axis(self, bases, scripts, vertical):
1292
+ def set_base_axis(self, bases, scripts, vertical, minmax=[]):
1233
1293
  if vertical:
1234
- self.base_vert_axis_ = (bases, scripts)
1294
+ self.base_vert_axis_ = (bases, scripts, minmax)
1235
1295
  else:
1236
- self.base_horiz_axis_ = (bases, scripts)
1296
+ self.base_horiz_axis_ = (bases, scripts, minmax)
1237
1297
 
1238
1298
  def set_size_parameters(
1239
1299
  self, location, DesignSize, SubfamilyID, RangeStart, RangeEnd
@@ -1251,6 +1311,24 @@ class Builder(object):
1251
1311
 
1252
1312
  # GSUB rules
1253
1313
 
1314
+ def add_any_subst_(self, location, mapping):
1315
+ lookup = self.get_lookup_(location, AnySubstBuilder, mapping=mapping)
1316
+ for key, value in mapping.items():
1317
+ if key in lookup.mapping:
1318
+ if value == lookup.mapping[key]:
1319
+ log.info(
1320
+ 'Removing duplicate substitution from "%s" to "%s" at %s',
1321
+ ", ".join(key),
1322
+ ", ".join(value),
1323
+ location,
1324
+ )
1325
+ else:
1326
+ raise FeatureLibError(
1327
+ 'Already defined substitution for "%s"' % ", ".join(key),
1328
+ location,
1329
+ )
1330
+ lookup.mapping[key] = value
1331
+
1254
1332
  # GSUB 1
1255
1333
  def add_single_subst(self, location, prefix, suffix, mapping, forceChain):
1256
1334
  if self.cur_feature_name_ == "aalt":
@@ -1262,24 +1340,11 @@ class Builder(object):
1262
1340
  if prefix or suffix or forceChain:
1263
1341
  self.add_single_subst_chained_(location, prefix, suffix, mapping)
1264
1342
  return
1265
- lookup = self.get_lookup_(location, SingleSubstBuilder)
1266
- for from_glyph, to_glyph in mapping.items():
1267
- if from_glyph in lookup.mapping:
1268
- if to_glyph == lookup.mapping[from_glyph]:
1269
- log.info(
1270
- "Removing duplicate single substitution from glyph"
1271
- ' "%s" to "%s" at %s',
1272
- from_glyph,
1273
- to_glyph,
1274
- location,
1275
- )
1276
- else:
1277
- raise FeatureLibError(
1278
- 'Already defined rule for replacing glyph "%s" by "%s"'
1279
- % (from_glyph, lookup.mapping[from_glyph]),
1280
- location,
1281
- )
1282
- lookup.mapping[from_glyph] = to_glyph
1343
+
1344
+ self.add_any_subst_(
1345
+ location,
1346
+ {(key,): (value,) for key, value in mapping.items()},
1347
+ )
1283
1348
 
1284
1349
  # GSUB 2
1285
1350
  def add_multiple_subst(
@@ -1288,21 +1353,10 @@ class Builder(object):
1288
1353
  if prefix or suffix or forceChain:
1289
1354
  self.add_multi_subst_chained_(location, prefix, glyph, suffix, replacements)
1290
1355
  return
1291
- lookup = self.get_lookup_(location, MultipleSubstBuilder)
1292
- if glyph in lookup.mapping:
1293
- if replacements == lookup.mapping[glyph]:
1294
- log.info(
1295
- "Removing duplicate multiple substitution from glyph"
1296
- ' "%s" to %s%s',
1297
- glyph,
1298
- replacements,
1299
- f" at {location}" if location else "",
1300
- )
1301
- else:
1302
- raise FeatureLibError(
1303
- 'Already defined substitution for glyph "%s"' % glyph, location
1304
- )
1305
- lookup.mapping[glyph] = replacements
1356
+ self.add_any_subst_(
1357
+ location,
1358
+ {(glyph,): tuple(replacements)},
1359
+ )
1306
1360
 
1307
1361
  # GSUB 3
1308
1362
  def add_alternate_subst(self, location, prefix, glyph, suffix, replacement):
@@ -1332,9 +1386,6 @@ class Builder(object):
1332
1386
  location, prefix, glyphs, suffix, replacement
1333
1387
  )
1334
1388
  return
1335
- else:
1336
- lookup = self.get_lookup_(location, LigatureSubstBuilder)
1337
-
1338
1389
  if not all(glyphs):
1339
1390
  raise FeatureLibError("Empty glyph class in substitution", location)
1340
1391
 
@@ -1343,8 +1394,10 @@ class Builder(object):
1343
1394
  # substitutions to be specified on target sequences that contain
1344
1395
  # glyph classes, the implementation software will enumerate
1345
1396
  # all specific glyph sequences if glyph classes are detected"
1346
- for g in itertools.product(*glyphs):
1347
- lookup.ligatures[g] = replacement
1397
+ self.add_any_subst_(
1398
+ location,
1399
+ {g: (replacement,) for g in itertools.product(*glyphs)},
1400
+ )
1348
1401
 
1349
1402
  # GSUB 5/6
1350
1403
  def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups):
@@ -1402,6 +1455,13 @@ class Builder(object):
1402
1455
  sub = self.get_chained_lookup_(location, LigatureSubstBuilder)
1403
1456
 
1404
1457
  for g in itertools.product(*glyphs):
1458
+ existing = sub.ligatures.get(g, replacement)
1459
+ if existing != replacement:
1460
+ raise FeatureLibError(
1461
+ f"Conflicting ligature sub rules: '{g}' maps to '{existing}' and '{replacement}'",
1462
+ location,
1463
+ )
1464
+
1405
1465
  sub.ligatures[g] = replacement
1406
1466
 
1407
1467
  chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, [sub]))
@@ -1442,7 +1502,9 @@ class Builder(object):
1442
1502
  lookup = self.get_lookup_(location, PairPosBuilder)
1443
1503
  v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
1444
1504
  v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
1445
- lookup.addClassPair(location, glyphclass1, v1, glyphclass2, v2)
1505
+ cls1 = tuple(sorted(set(glyphclass1)))
1506
+ cls2 = tuple(sorted(set(glyphclass2)))
1507
+ lookup.addClassPair(location, cls1, v1, cls2, v2)
1446
1508
 
1447
1509
  def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
1448
1510
  if not glyph1 or not glyph2:
@@ -1628,6 +1690,12 @@ class Builder(object):
1628
1690
  location,
1629
1691
  )
1630
1692
 
1693
+ if key in self.conditionsets_:
1694
+ raise FeatureLibError(
1695
+ f"Condition set '{key}' has the same name as a previous condition set",
1696
+ location,
1697
+ )
1698
+
1631
1699
  # Normalize
1632
1700
  axisMap = {
1633
1701
  axis.axisTag: (axis.minValue, axis.defaultValue, axis.maxValue)
@@ -1667,9 +1735,14 @@ class Builder(object):
1667
1735
  if not varscalar.does_vary:
1668
1736
  return varscalar.default, None
1669
1737
 
1670
- default, index = varscalar.add_to_variation_store(
1671
- self.varstorebuilder, self.model_cache, self.font.get("avar")
1672
- )
1738
+ try:
1739
+ default, index = varscalar.add_to_variation_store(
1740
+ self.varstorebuilder, self.model_cache, self.font.get("avar")
1741
+ )
1742
+ except VarLibError as e:
1743
+ raise FeatureLibError(
1744
+ "Failed to compute deltas for variable scalar", location
1745
+ ) from e
1673
1746
 
1674
1747
  device = None
1675
1748
  if index is not None and index != 0xFFFFFFFF:
fontTools/feaLib/error.py CHANGED
@@ -1,5 +1,5 @@
1
1
  class FeatureLibError(Exception):
2
- def __init__(self, message, location):
2
+ def __init__(self, message, location=None):
3
3
  Exception.__init__(self, message)
4
4
  self.location = location
5
5