diff options
author | robot-piglet <[email protected]> | 2025-06-18 23:55:52 +0300 |
---|---|---|
committer | robot-piglet <[email protected]> | 2025-06-19 00:06:55 +0300 |
commit | 61ea5fc0cd73730d5ae6a9186d8c15057f183095 (patch) | |
tree | 31ddc3fcbabb680d9ce733ea36f581a6d062aff9 /contrib/python | |
parent | 2fcfb855cd7780ab07751cc16c80a0a58168668a (diff) |
Intermediate changes
commit_hash:283b0c2ecb46f54501d7d1b92643c0090cebaa95
Diffstat (limited to 'contrib/python')
19 files changed, 378 insertions, 165 deletions
diff --git a/contrib/python/fonttools/.dist-info/METADATA b/contrib/python/fonttools/.dist-info/METADATA index 51b093cabdc..052bb26273e 100644 --- a/contrib/python/fonttools/.dist-info/METADATA +++ b/contrib/python/fonttools/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: fonttools -Version: 4.58.0 +Version: 4.58.1 Summary: Tools to manipulate font files Home-page: http://github.com/fonttools/fonttools Author: Just van Rossum @@ -388,6 +388,25 @@ Have fun! Changelog ~~~~~~~~~ +4.58.1 (released 2025-05-28) +---------------------------- + +- [varLib] Make sure that fvar named instances only reuse name ID 2 or 17 if they are at the default location across all axes, to match OT spec requirement (#3831). +- [feaLib] Improve single substitution promotion to multiple/ligature substitutions, fixing a few bugs as well (#3849). +- [loggingTools] Make ``Timer._time`` a static method that doesn't take self, makes it easier to override (#3836). +- [featureVars] Use ``None`` for empty ConditionSet, which translates to a null offset in the compiled table (#3850). +- [feaLib] Raise an error on conflicting ligature substitution rules instead of silently taking the last one (#3835). +- Add typing annotations to T2CharStringPen (#3837). +- [feaLib] Add single substitutions that were promoted to multiple or ligature substitutions to ``aalt`` feature (#3847). +- [featureVars] Create a default ``LangSys`` in a ``ScriptRecord`` if missing when adding feature variations to existing GSUB later in the build (#3838). +- [symfont] Added a ``main()``. +- [cffLib.specializer] Fix rmoveto merging when blends used (#3839, #3840). +- [pyftmerge] Add support for cmap format 14 in the merge tool (#3830). +- [varLib.instancer/cff2] Fix vsindex of Private dicts when instantiating (#3828, #3232). +- Update text file read to use UTF-8 with optional BOM so it works with e.g. Windows Notepad.exe (#3824). +- [varLib] Ensure that instances only reuse name ID 2 or 17 if they are at the default location across all axes (#3831). +- [varLib] Create a dflt LangSys in a ScriptRecord when adding variations later, to fix an avoidable crash in an edge case (#3838). + 4.58.0 (released 2025-05-10) ---------------------------- diff --git a/contrib/python/fonttools/fontTools/__init__.py b/contrib/python/fonttools/fontTools/__init__.py index 6e41eb43a8b..ee6b53b553e 100644 --- a/contrib/python/fonttools/fontTools/__init__.py +++ b/contrib/python/fonttools/fontTools/__init__.py @@ -3,6 +3,6 @@ from fontTools.misc.loggingTools import configLogger log = logging.getLogger(__name__) -version = __version__ = "4.58.0" +version = __version__ = "4.58.1" __all__ = ["version", "log", "configLogger"] diff --git a/contrib/python/fonttools/fontTools/cffLib/specializer.py b/contrib/python/fonttools/fontTools/cffLib/specializer.py index 5fddcb67dd3..974060c40ec 100644 --- a/contrib/python/fonttools/fontTools/cffLib/specializer.py +++ b/contrib/python/fonttools/fontTools/cffLib/specializer.py @@ -580,7 +580,10 @@ def specializeCommands( for i in range(len(commands) - 1, 0, -1): if "rmoveto" == commands[i][0] == commands[i - 1][0]: v1, v2 = commands[i - 1][1], commands[i][1] - commands[i - 1] = ("rmoveto", [v1[0] + v2[0], v1[1] + v2[1]]) + commands[i - 1] = ( + "rmoveto", + [_addArgs(v1[0], v2[0]), _addArgs(v1[1], v2[1])], + ) del commands[i] # 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants. diff --git a/contrib/python/fonttools/fontTools/feaLib/ast.py b/contrib/python/fonttools/fontTools/feaLib/ast.py index 8479d7300d6..efcce8c680b 100644 --- a/contrib/python/fonttools/fontTools/feaLib/ast.py +++ b/contrib/python/fonttools/fontTools/feaLib/ast.py @@ -337,76 +337,6 @@ class AnonymousBlock(Statement): return res -def _upgrade_mixed_subst_statements(statements): - # https://github.com/fonttools/fonttools/issues/612 - # A multiple substitution may have a single destination, in which case - # it will look just like a single substitution. So if there are both - # multiple and single substitutions, upgrade all the single ones to - # multiple substitutions. Similarly, a ligature substitution may have a - # single source glyph, so if there are both ligature and single - # substitutions, upgrade all the single ones to ligature substitutions. - - has_single = False - has_multiple = False - has_ligature = False - for s in statements: - if isinstance(s, SingleSubstStatement): - has_single = not any([s.prefix, s.suffix, s.forceChain]) - elif isinstance(s, MultipleSubstStatement): - has_multiple = not any([s.prefix, s.suffix, s.forceChain]) - elif isinstance(s, LigatureSubstStatement): - has_ligature = not any([s.prefix, s.suffix, s.forceChain]) - - to_multiple = False - to_ligature = False - - # If we have mixed single and multiple substitutions, - # upgrade all single substitutions to multiple substitutions. - if has_single and has_multiple and not has_ligature: - to_multiple = True - - # If we have mixed single and ligature substitutions, - # upgrade all single substitutions to ligature substitutions. - elif has_single and has_ligature and not has_multiple: - to_ligature = True - - if to_multiple or to_ligature: - ret = [] - for s in statements: - if isinstance(s, SingleSubstStatement): - glyphs = s.glyphs[0].glyphSet() - replacements = s.replacements[0].glyphSet() - if len(replacements) == 1: - replacements *= len(glyphs) - for glyph, replacement in zip(glyphs, replacements): - if to_multiple: - ret.append( - MultipleSubstStatement( - s.prefix, - glyph, - s.suffix, - [replacement], - s.forceChain, - location=s.location, - ) - ) - elif to_ligature: - ret.append( - LigatureSubstStatement( - s.prefix, - [GlyphName(glyph)], - s.suffix, - replacement, - s.forceChain, - location=s.location, - ) - ) - else: - ret.append(s) - return ret - return statements - - class Block(Statement): """A block of statements: feature, lookup, etc.""" @@ -418,8 +348,7 @@ class Block(Statement): """When handed a 'builder' object of comparable interface to :class:`fontTools.feaLib.builder`, walks the statements in this block, calling the builder callbacks.""" - statements = _upgrade_mixed_subst_statements(self.statements) - for s in statements: + for s in self.statements: s.build(builder) def asFea(self, indent=""): diff --git a/contrib/python/fonttools/fontTools/feaLib/builder.py b/contrib/python/fonttools/fontTools/feaLib/builder.py index 1583f06d9ec..3563db6e377 100644 --- a/contrib/python/fonttools/fontTools/feaLib/builder.py +++ b/contrib/python/fonttools/fontTools/feaLib/builder.py @@ -29,6 +29,7 @@ from fontTools.otlLib.builder import ( PairPosBuilder, SinglePosBuilder, ChainContextualRule, + AnySubstBuilder, ) from fontTools.otlLib.error import OpenTypeLibError from fontTools.varLib.varStore import OnlineVarStoreBuilder @@ -866,13 +867,22 @@ class Builder(object): for lookup in self.lookups_: if lookup.table != tag: continue - lookup.lookup_index = len(lookups) - self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo( - location=str(lookup.location), - name=self.get_lookup_name_(lookup), - feature=None, - ) - lookups.append(lookup) + name = self.get_lookup_name_(lookup) + resolved = lookup.promote_lookup_type(is_named_lookup=name is not None) + if resolved is None: + raise FeatureLibError( + "Within a named lookup block, all rules must be of " + "the same lookup type and flag", + lookup.location, + ) + for l in resolved: + lookup.lookup_index = len(lookups) + self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo( + location=str(lookup.location), + name=name, + feature=None, + ) + lookups.append(l) otLookups = [] for l in lookups: try: @@ -1294,6 +1304,24 @@ class Builder(object): # GSUB rules + def add_any_subst_(self, location, mapping): + lookup = self.get_lookup_(location, AnySubstBuilder) + for key, value in mapping.items(): + if key in lookup.mapping: + if value == lookup.mapping[key]: + log.info( + 'Removing duplicate substitution from "%s" to "%s" at %s', + ", ".join(key), + ", ".join(value), + location, + ) + else: + raise FeatureLibError( + 'Already defined substitution for "%s"' % ", ".join(key), + location, + ) + lookup.mapping[key] = value + # GSUB 1 def add_single_subst(self, location, prefix, suffix, mapping, forceChain): if self.cur_feature_name_ == "aalt": @@ -1305,24 +1333,11 @@ class Builder(object): if prefix or suffix or forceChain: self.add_single_subst_chained_(location, prefix, suffix, mapping) return - lookup = self.get_lookup_(location, SingleSubstBuilder) - for from_glyph, to_glyph in mapping.items(): - if from_glyph in lookup.mapping: - if to_glyph == lookup.mapping[from_glyph]: - log.info( - "Removing duplicate single substitution from glyph" - ' "%s" to "%s" at %s', - from_glyph, - to_glyph, - location, - ) - else: - raise FeatureLibError( - 'Already defined rule for replacing glyph "%s" by "%s"' - % (from_glyph, lookup.mapping[from_glyph]), - location, - ) - lookup.mapping[from_glyph] = to_glyph + + self.add_any_subst_( + location, + {(key,): (value,) for key, value in mapping.items()}, + ) # GSUB 2 def add_multiple_subst( @@ -1331,21 +1346,10 @@ class Builder(object): if prefix or suffix or forceChain: self.add_multi_subst_chained_(location, prefix, glyph, suffix, replacements) return - lookup = self.get_lookup_(location, MultipleSubstBuilder) - if glyph in lookup.mapping: - if replacements == lookup.mapping[glyph]: - log.info( - "Removing duplicate multiple substitution from glyph" - ' "%s" to %s%s', - glyph, - replacements, - f" at {location}" if location else "", - ) - else: - raise FeatureLibError( - 'Already defined substitution for glyph "%s"' % glyph, location - ) - lookup.mapping[glyph] = replacements + self.add_any_subst_( + location, + {(glyph,): tuple(replacements)}, + ) # GSUB 3 def add_alternate_subst(self, location, prefix, glyph, suffix, replacement): @@ -1375,9 +1379,6 @@ class Builder(object): location, prefix, glyphs, suffix, replacement ) return - else: - lookup = self.get_lookup_(location, LigatureSubstBuilder) - if not all(glyphs): raise FeatureLibError("Empty glyph class in substitution", location) @@ -1386,8 +1387,10 @@ class Builder(object): # substitutions to be specified on target sequences that contain # glyph classes, the implementation software will enumerate # all specific glyph sequences if glyph classes are detected" - for g in itertools.product(*glyphs): - lookup.ligatures[g] = replacement + self.add_any_subst_( + location, + {g: (replacement,) for g in itertools.product(*glyphs)}, + ) # GSUB 5/6 def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups): @@ -1445,6 +1448,13 @@ class Builder(object): sub = self.get_chained_lookup_(location, LigatureSubstBuilder) for g in itertools.product(*glyphs): + existing = sub.ligatures.get(g, replacement) + if existing != replacement: + raise FeatureLibError( + f"Conflicting ligature sub rules: '{g}' maps to '{existing}' and '{replacement}'", + location, + ) + sub.ligatures[g] = replacement chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, [sub])) diff --git a/contrib/python/fonttools/fontTools/merge/cmap.py b/contrib/python/fonttools/fontTools/merge/cmap.py index 3209a5d7b82..7cc4a4ead15 100644 --- a/contrib/python/fonttools/fontTools/merge/cmap.py +++ b/contrib/python/fonttools/fontTools/merge/cmap.py @@ -54,6 +54,28 @@ def _glyphsAreSame( return True +def computeMegaUvs(merger, uvsTables): + """Returns merged UVS subtable (cmap format=14).""" + uvsDict = {} + cmap = merger.cmap + for table in uvsTables: + for variationSelector, uvsMapping in table.uvsDict.items(): + if variationSelector not in uvsDict: + uvsDict[variationSelector] = {} + for unicodeValue, glyphName in uvsMapping: + if cmap.get(unicodeValue) == glyphName: + # this is a default variation + glyphName = None + # prefer previous glyph id if both fonts defined UVS + if unicodeValue not in uvsDict[variationSelector]: + uvsDict[variationSelector][unicodeValue] = glyphName + + for variationSelector in uvsDict: + uvsDict[variationSelector] = [*uvsDict[variationSelector].items()] + + return uvsDict + + # Valid (format, platformID, platEncID) triplets for cmap subtables containing # Unicode BMP-only and Unicode Full Repertoire semantics. # Cf. OpenType spec for "Platform specific encodings": @@ -61,24 +83,29 @@ def _glyphsAreSame( class _CmapUnicodePlatEncodings: BMP = {(4, 3, 1), (4, 0, 3), (4, 0, 4), (4, 0, 6)} FullRepertoire = {(12, 3, 10), (12, 0, 4), (12, 0, 6)} + UVS = {(14, 0, 5)} def computeMegaCmap(merger, cmapTables): - """Sets merger.cmap and merger.glyphOrder.""" + """Sets merger.cmap and merger.uvsDict.""" # TODO Handle format=14. # Only merge format 4 and 12 Unicode subtables, ignores all other subtables # If there is a format 12 table for a font, ignore the format 4 table of it chosenCmapTables = [] + chosenUvsTables = [] for fontIdx, table in enumerate(cmapTables): format4 = None format12 = None + format14 = None for subtable in table.tables: properties = (subtable.format, subtable.platformID, subtable.platEncID) if properties in _CmapUnicodePlatEncodings.BMP: format4 = subtable elif properties in _CmapUnicodePlatEncodings.FullRepertoire: format12 = subtable + elif properties in _CmapUnicodePlatEncodings.UVS: + format14 = subtable else: log.warning( "Dropped cmap subtable from font '%s':\t" @@ -93,6 +120,9 @@ def computeMegaCmap(merger, cmapTables): elif format4 is not None: chosenCmapTables.append((format4, fontIdx)) + if format14 is not None: + chosenUvsTables.append(format14) + # Build the unicode mapping merger.cmap = cmap = {} fontIndexForGlyph = {} @@ -127,6 +157,8 @@ def computeMegaCmap(merger, cmapTables): "Dropped mapping from codepoint %#06X to glyphId '%s'", uni, gid ) + merger.uvsDict = computeMegaUvs(merger, chosenUvsTables) + def renameCFFCharStrings(merger, glyphOrder, cffTable): """Rename topDictIndex charStrings based on glyphOrder.""" diff --git a/contrib/python/fonttools/fontTools/merge/tables.py b/contrib/python/fonttools/fontTools/merge/tables.py index 208a5099ff1..a46977f0b4b 100644 --- a/contrib/python/fonttools/fontTools/merge/tables.py +++ b/contrib/python/fonttools/fontTools/merge/tables.py @@ -312,7 +312,6 @@ def merge(self, m, tables): @add_method(ttLib.getTableClass("cmap")) def merge(self, m, tables): - # TODO Handle format=14. if not hasattr(m, "cmap"): computeMegaCmap(m, tables) cmap = m.cmap @@ -336,6 +335,18 @@ def merge(self, m, tables): cmapTable.cmap = cmapBmpOnly # ordered by platform then encoding self.tables.insert(0, cmapTable) + + uvsDict = m.uvsDict + if uvsDict: + # format-14 + uvsTable = module.cmap_classes[14](14) + uvsTable.platformID = 0 + uvsTable.platEncID = 5 + uvsTable.language = 0 + uvsTable.cmap = {} + uvsTable.uvsDict = uvsDict + # ordered by platform then encoding + self.tables.insert(0, uvsTable) self.tableVersion = 0 self.numSubTables = len(self.tables) return self diff --git a/contrib/python/fonttools/fontTools/misc/loggingTools.py b/contrib/python/fonttools/fontTools/misc/loggingTools.py index 78704f5a9aa..be6c2d369b6 100644 --- a/contrib/python/fonttools/fontTools/misc/loggingTools.py +++ b/contrib/python/fonttools/fontTools/misc/loggingTools.py @@ -284,7 +284,7 @@ class Timer(object): """ # timeit.default_timer choses the most accurate clock for each platform - _time = timeit.default_timer + _time: Callable[[], float] = staticmethod(timeit.default_timer) default_msg = "elapsed time: %(time).3fs" default_format = "Took %(time).3fs to %(msg)s" diff --git a/contrib/python/fonttools/fontTools/misc/symfont.py b/contrib/python/fonttools/fontTools/misc/symfont.py index 4dea4184080..9bba2d2d567 100644 --- a/contrib/python/fonttools/fontTools/misc/symfont.py +++ b/contrib/python/fonttools/fontTools/misc/symfont.py @@ -234,11 +234,9 @@ if __name__ == '__main__': if __name__ == "__main__": - pen = AreaPen() - pen.moveTo((100, 100)) - pen.lineTo((100, 200)) - pen.lineTo((200, 200)) - pen.curveTo((200, 250), (300, 300), (250, 350)) - pen.lineTo((200, 100)) - pen.closePath() - print(pen.value) + import sys + + if sys.argv[1:]: + penName = sys.argv[1] + funcs = [(name, eval(f)) for name, f in zip(sys.argv[2::2], sys.argv[3::2])] + printGreenPen(penName, funcs, file=sys.stdout) diff --git a/contrib/python/fonttools/fontTools/mtiLib/__init__.py b/contrib/python/fonttools/fontTools/mtiLib/__init__.py index e797be375b6..32d3a244e1f 100644 --- a/contrib/python/fonttools/fontTools/mtiLib/__init__.py +++ b/contrib/python/fonttools/fontTools/mtiLib/__init__.py @@ -1375,7 +1375,7 @@ def main(args=None, font=None): for f in args.inputs: log.debug("Processing %s", f) - with open(f, "rt", encoding="utf-8") as f: + with open(f, "rt", encoding="utf-8-sig") as f: table = build(f, font, tableTag=args.tableTag) blob = table.compile(font) # Make sure it compiles decompiled = table.__class__() diff --git a/contrib/python/fonttools/fontTools/otlLib/builder.py b/contrib/python/fonttools/fontTools/otlLib/builder.py index 064b2fce31c..da00e9c5ebd 100644 --- a/contrib/python/fonttools/fontTools/otlLib/builder.py +++ b/contrib/python/fonttools/fontTools/otlLib/builder.py @@ -170,6 +170,9 @@ class LookupBuilder(object): and self.extension == other.extension ) + def promote_lookup_type(self, is_named_lookup): + return [self] + def inferGlyphClasses(self): """Infers glyph glasses for the GDEF table, such as {"cedilla":3}.""" return {} @@ -883,6 +886,14 @@ class LigatureSubstBuilder(LookupBuilder): ) return self.buildLookup_(subtables) + def getAlternateGlyphs(self): + # https://github.com/fonttools/fonttools/issues/3845 + return { + components[0]: [ligature] + for components, ligature in self.ligatures.items() + if len(components) == 1 + } + def add_subtable_break(self, location): self.ligatures[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ @@ -921,6 +932,14 @@ class MultipleSubstBuilder(LookupBuilder): subtables = self.build_subst_subtables(self.mapping, buildMultipleSubstSubtable) return self.buildLookup_(subtables) + def getAlternateGlyphs(self): + # https://github.com/fonttools/fonttools/issues/3845 + return { + glyph: replacements + for glyph, replacements in self.mapping.items() + if len(replacements) == 1 + } + def add_subtable_break(self, location): self.mapping[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ @@ -1308,6 +1327,151 @@ class ReverseChainSingleSubstBuilder(LookupBuilder): pass +class AnySubstBuilder(LookupBuilder): + """A temporary builder for Single, Multiple, or Ligature substitution lookup. + + Users are expected to manually add substitutions to the ``mapping`` + attribute after the object has been initialized, e.g.:: + + # sub x by y; + builder.mapping[("x",)] = ("y",) + # sub a by b c; + builder.mapping[("a",)] = ("b", "c") + # sub f i by f_i; + builder.mapping[("f", "i")] = ("f_i",) + + Then call `promote_lookup_type()` to convert this builder into the + appropriate type of substitution lookup builder. This would promote single + substitutions to either multiple or ligature substitutions, depending on the + rest of the rules in the mapping. + + Attributes: + font (``fontTools.TTLib.TTFont``): A font object. + location: A string or tuple representing the location in the original + source which produced this lookup. + mapping: An ordered dictionary mapping a tuple of glyph names to another + tuple of glyph names. + lookupflag (int): The lookup's flag + markFilterSet: Either ``None`` if no mark filtering set is used, or + an integer representing the filtering set to be used for this + lookup. If a mark filtering set is provided, + `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's + flags. + """ + + def __init__(self, font, location): + LookupBuilder.__init__(self, font, location, "GSUB", 0) + self.mapping = OrderedDict() + + def _add_to_single_subst(self, builder, key, value): + if key[0] != self.SUBTABLE_BREAK_: + key = key[0] + builder.mapping[key] = value[0] + + def _add_to_multiple_subst(self, builder, key, value): + if key[0] != self.SUBTABLE_BREAK_: + key = key[0] + builder.mapping[key] = value + + def _add_to_ligature_subst(self, builder, key, value): + builder.ligatures[key] = value[0] + + def promote_lookup_type(self, is_named_lookup): + # https://github.com/fonttools/fonttools/issues/612 + # A multiple substitution may have a single destination, in which case + # it will look just like a single substitution. So if there are both + # multiple and single substitutions, upgrade all the single ones to + # multiple substitutions. Similarly, a ligature substitution may have a + # single source glyph, so if there are both ligature and single + # substitutions, upgrade all the single ones to ligature substitutions. + builder_classes = [] + for key, value in self.mapping.items(): + if key[0] == self.SUBTABLE_BREAK_: + builder_classes.append(None) + elif len(key) == 1 and len(value) == 1: + builder_classes.append(SingleSubstBuilder) + elif len(key) == 1 and len(value) != 1: + builder_classes.append(MultipleSubstBuilder) + elif len(key) > 1 and len(value) == 1: + builder_classes.append(LigatureSubstBuilder) + else: + assert False, "Should not happen" + + has_multiple = any(b is MultipleSubstBuilder for b in builder_classes) + has_ligature = any(b is LigatureSubstBuilder for b in builder_classes) + + # If we have mixed single and multiple substitutions, + # upgrade all single substitutions to multiple substitutions. + to_multiple = has_multiple and not has_ligature + + # If we have mixed single and ligature substitutions, + # upgrade all single substitutions to ligature substitutions. + to_ligature = has_ligature and not has_multiple + + # If we have only single substitutions, we can keep them as is. + to_single = not has_ligature and not has_multiple + + ret = [] + if to_single: + builder = SingleSubstBuilder(self.font, self.location) + for key, value in self.mapping.items(): + self._add_to_single_subst(builder, key, value) + ret = [builder] + elif to_multiple: + builder = MultipleSubstBuilder(self.font, self.location) + for key, value in self.mapping.items(): + self._add_to_multiple_subst(builder, key, value) + ret = [builder] + elif to_ligature: + builder = LigatureSubstBuilder(self.font, self.location) + for key, value in self.mapping.items(): + self._add_to_ligature_subst(builder, key, value) + ret = [builder] + elif is_named_lookup: + # This is a named lookup with mixed substitutions that can’t be promoted, + # since we can’t split it into multiple lookups, we return None here to + # signal that to the caller + return None + else: + curr_builder = None + for builder_class, (key, value) in zip( + builder_classes, self.mapping.items() + ): + if curr_builder is None or type(curr_builder) is not builder_class: + curr_builder = builder_class(self.font, self.location) + ret.append(curr_builder) + if builder_class is SingleSubstBuilder: + self._add_to_single_subst(curr_builder, key, value) + elif builder_class is MultipleSubstBuilder: + self._add_to_multiple_subst(curr_builder, key, value) + elif builder_class is LigatureSubstBuilder: + self._add_to_ligature_subst(curr_builder, key, value) + else: + assert False, "Should not happen" + + for builder in ret: + builder.extension = self.extension + builder.lookupflag = self.lookupflag + builder.markFilterSet = self.markFilterSet + return ret + + def equals(self, other): + return LookupBuilder.equals(self, other) and self.mapping == other.mapping + + def build(self): + assert False + + def getAlternateGlyphs(self): + return { + key[0]: value + for key, value in self.mapping.items() + if len(key) == 1 and len(value) == 1 + } + + def add_subtable_break(self, location): + self.mapping[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ + + class SingleSubstBuilder(LookupBuilder): """Builds a Single Substitution (GSUB1) lookup. diff --git a/contrib/python/fonttools/fontTools/pens/t2CharStringPen.py b/contrib/python/fonttools/fontTools/pens/t2CharStringPen.py index 41ab0f92f2b..ddff2c93f0a 100644 --- a/contrib/python/fonttools/fontTools/pens/t2CharStringPen.py +++ b/contrib/python/fonttools/fontTools/pens/t2CharStringPen.py @@ -1,10 +1,14 @@ # Copyright (c) 2009 Type Supply LLC # Author: Tal Leming -from fontTools.misc.roundTools import otRound, roundFunc +from __future__ import annotations + +from typing import Any, Dict, List, Tuple + +from fontTools.cffLib.specializer import commandsToProgram, specializeCommands from fontTools.misc.psCharStrings import T2CharString +from fontTools.misc.roundTools import otRound, roundFunc from fontTools.pens.basePen import BasePen -from fontTools.cffLib.specializer import specializeCommands, commandsToProgram class T2CharStringPen(BasePen): @@ -18,36 +22,52 @@ class T2CharStringPen(BasePen): which are close to their integral part within the tolerated range. """ - def __init__(self, width, glyphSet, roundTolerance=0.5, CFF2=False): + def __init__( + self, + width: float | None, + glyphSet: Dict[str, Any] | None, + roundTolerance: float = 0.5, + CFF2: bool = False, + ) -> None: super(T2CharStringPen, self).__init__(glyphSet) self.round = roundFunc(roundTolerance) self._CFF2 = CFF2 self._width = width - self._commands = [] + self._commands: List[Tuple[str | bytes, List[float]]] = [] self._p0 = (0, 0) - def _p(self, pt): + def _p(self, pt: Tuple[float, float]) -> List[float]: p0 = self._p0 pt = self._p0 = (self.round(pt[0]), self.round(pt[1])) return [pt[0] - p0[0], pt[1] - p0[1]] - def _moveTo(self, pt): + def _moveTo(self, pt: Tuple[float, float]) -> None: self._commands.append(("rmoveto", self._p(pt))) - def _lineTo(self, pt): + def _lineTo(self, pt: Tuple[float, float]) -> None: self._commands.append(("rlineto", self._p(pt))) - def _curveToOne(self, pt1, pt2, pt3): + def _curveToOne( + self, + pt1: Tuple[float, float], + pt2: Tuple[float, float], + pt3: Tuple[float, float], + ) -> None: _p = self._p self._commands.append(("rrcurveto", _p(pt1) + _p(pt2) + _p(pt3))) - def _closePath(self): + def _closePath(self) -> None: pass - def _endPath(self): + def _endPath(self) -> None: pass - def getCharString(self, private=None, globalSubrs=None, optimize=True): + def getCharString( + self, + private: Dict | None = None, + globalSubrs: List | None = None, + optimize: bool = True, + ) -> T2CharString: commands = self._commands if optimize: maxstack = 48 if not self._CFF2 else 513 diff --git a/contrib/python/fonttools/fontTools/subset/__init__.py b/contrib/python/fonttools/fontTools/subset/__init__.py index 056ad81babe..71d5f5d80d6 100644 --- a/contrib/python/fonttools/fontTools/subset/__init__.py +++ b/contrib/python/fonttools/fontTools/subset/__init__.py @@ -3757,7 +3757,7 @@ def main(args=None): text += g[7:] continue if g.startswith("--text-file="): - with open(g[12:], encoding="utf-8") as f: + with open(g[12:], encoding="utf-8-sig") as f: text += f.read().replace("\n", "") continue if g.startswith("--unicodes="): diff --git a/contrib/python/fonttools/fontTools/ufoLib/__init__.py b/contrib/python/fonttools/fontTools/ufoLib/__init__.py index f76938a8f15..2c5c51d61bd 100644 --- a/contrib/python/fonttools/fontTools/ufoLib/__init__.py +++ b/contrib/python/fonttools/fontTools/ufoLib/__init__.py @@ -654,7 +654,7 @@ class UFOReader(_UFOBaseIO): The returned string is empty if the file is missing. """ try: - with self.fs.open(FEATURES_FILENAME, "r", encoding="utf-8") as f: + with self.fs.open(FEATURES_FILENAME, "r", encoding="utf-8-sig") as f: return f.read() except fs.errors.ResourceNotFound: return "" diff --git a/contrib/python/fonttools/fontTools/varLib/__init__.py b/contrib/python/fonttools/fontTools/varLib/__init__.py index e3c00d73fa9..7ccecb75ff9 100644 --- a/contrib/python/fonttools/fontTools/varLib/__init__.py +++ b/contrib/python/fonttools/fontTools/varLib/__init__.py @@ -109,6 +109,8 @@ def _add_fvar(font, axes, instances: List[InstanceDescriptor]): axis.flags = int(a.hidden) fvar.axes.append(axis) + default_coordinates = {axis.axisTag: axis.defaultValue for axis in fvar.axes} + for instance in instances: # Filter out discrete axis locations coordinates = { @@ -130,16 +132,26 @@ def _add_fvar(font, axes, instances: List[InstanceDescriptor]): psname = instance.postScriptFontName inst = NamedInstance() - inst.subfamilyNameID = nameTable.addMultilingualName( - localisedStyleName, mac=macNames + inst.coordinates = { + axes[k].tag: axes[k].map_backward(v) for k, v in coordinates.items() + } + + subfamilyNameID = nameTable.findMultilingualName( + localisedStyleName, windows=True, mac=macNames ) + if subfamilyNameID in {2, 17} and inst.coordinates == default_coordinates: + # Instances can only reuse an existing name ID 2 or 17 if they are at the + # default location across all axes, see: + # https://github.com/fonttools/fonttools/issues/3825. + inst.subfamilyNameID = subfamilyNameID + else: + inst.subfamilyNameID = nameTable.addMultilingualName( + localisedStyleName, windows=True, mac=macNames, minNameID=256 + ) + if psname is not None: psname = tostr(psname) inst.postscriptNameID = nameTable.addName(psname, platforms=platforms) - inst.coordinates = { - axes[k].tag: axes[k].map_backward(v) for k, v in coordinates.items() - } - # inst.coordinates = {axes[k].tag:v for k,v in coordinates.items()} fvar.instances.append(inst) assert "fvar" not in font diff --git a/contrib/python/fonttools/fontTools/varLib/featureVars.py b/contrib/python/fonttools/fontTools/varLib/featureVars.py index 2e957f55859..856f00bcb9c 100644 --- a/contrib/python/fonttools/fontTools/varLib/featureVars.py +++ b/contrib/python/fonttools/fontTools/varLib/featureVars.py @@ -392,10 +392,13 @@ def addFeatureVariationsRaw(font, table, conditionalSubstitutions, featureTag="r for scriptRecord in table.ScriptList.ScriptRecord: if scriptRecord.Script.DefaultLangSys is None: - raise VarLibError( - "Feature variations require that the script " - f"'{scriptRecord.ScriptTag}' defines a default language system." - ) + # We need to have a default LangSys to attach variations to. + langSys = ot.LangSys() + langSys.LookupOrder = None + langSys.ReqFeatureIndex = 0xFFFF + langSys.FeatureIndex = [] + langSys.FeatureCount = 0 + scriptRecord.Script.DefaultLangSys = langSys langSystems = [lsr.LangSys for lsr in scriptRecord.Script.LangSysRecord] for langSys in [scriptRecord.Script.DefaultLangSys] + langSystems: langSys.FeatureIndex.append(varFeatureIndex) @@ -597,9 +600,12 @@ def buildFeatureRecord(featureTag, lookupListIndices): def buildFeatureVariationRecord(conditionTable, substitutionRecords): """Build a FeatureVariationRecord.""" fvr = ot.FeatureVariationRecord() - fvr.ConditionSet = ot.ConditionSet() - fvr.ConditionSet.ConditionTable = conditionTable - fvr.ConditionSet.ConditionCount = len(conditionTable) + if len(conditionTable) != 0: + fvr.ConditionSet = ot.ConditionSet() + fvr.ConditionSet.ConditionTable = conditionTable + fvr.ConditionSet.ConditionCount = len(conditionTable) + else: + fvr.ConditionSet = None fvr.FeatureTableSubstitution = ot.FeatureTableSubstitution() fvr.FeatureTableSubstitution.Version = 0x00010000 fvr.FeatureTableSubstitution.SubstitutionRecord = substitutionRecords diff --git a/contrib/python/fonttools/fontTools/varLib/hvar.py b/contrib/python/fonttools/fontTools/varLib/hvar.py index d243f9aa8ce..0bdb16c62b8 100644 --- a/contrib/python/fonttools/fontTools/varLib/hvar.py +++ b/contrib/python/fonttools/fontTools/varLib/hvar.py @@ -18,7 +18,7 @@ def _get_advance_metrics(font, axisTags, tableFields): # advance width at that peak. Then pass these all to a VariationModel # builder to compute back the deltas. # 2. For each master peak, pull out the deltas of the advance width directly, - # and feed these to the VarStoreBuilder, forgoing the remoding step. + # and feed these to the VarStoreBuilder, forgoing the remodeling step. # We'll go with the second option, as it's simpler, faster, and more direct. gvar = font["gvar"] vhAdvanceDeltasAndSupports = {} diff --git a/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py b/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py index f6df65e96d1..a0b832a3c18 100644 --- a/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py +++ b/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py @@ -675,6 +675,7 @@ def instantiateCFF2( privateDicts.append(fd.Private) allCommands = [] + allCommandPrivates = [] for cs in charStrings: assert cs.private.vstore.otVarStore is varStore # Or in many places!! commands = programToCommands(cs.program, getNumRegions=getNumRegions) @@ -683,6 +684,7 @@ def instantiateCFF2( if specialize: commands = specializeCommands(commands, generalizeFirst=not generalize) allCommands.append(commands) + allCommandPrivates.append(cs.private) def storeBlendsToVarStore(arg): if not isinstance(arg, list): @@ -742,8 +744,8 @@ def instantiateCFF2( assert varData.ItemCount == 0 # Add charstring blend lists to VarStore so we can instantiate them - for commands in allCommands: - vsindex = 0 + for commands, private in zip(allCommands, allCommandPrivates): + vsindex = getattr(private, "vsindex", 0) for command in commands: if command[0] == "vsindex": vsindex = command[1][0] @@ -752,7 +754,6 @@ def instantiateCFF2( storeBlendsToVarStore(arg) # Add private blend lists to VarStore so we can instantiate values - vsindex = 0 for opcode, name, arg_type, default, converter in privateDictOperators2: if arg_type not in ("number", "delta", "array"): continue @@ -763,6 +764,7 @@ def instantiateCFF2( continue values = getattr(private, name) + # This is safe here since "vsindex" is the first in the privateDictOperators2 if name == "vsindex": vsindex = values[0] continue @@ -783,8 +785,8 @@ def instantiateCFF2( # Read back new charstring blends from the instantiated VarStore varDataCursor = [0] * len(varStore.VarData) - for commands in allCommands: - vsindex = 0 + for commands, private in zip(allCommands, allCommandPrivates): + vsindex = getattr(private, "vsindex", 0) for command in commands: if command[0] == "vsindex": vsindex = command[1][0] @@ -799,9 +801,16 @@ def instantiateCFF2( if arg_type not in ("number", "delta", "array"): continue + vsindex = 0 for private in privateDicts: if not hasattr(private, name): continue + + # This is safe here since "vsindex" is the first in the privateDictOperators2 + if name == "vsindex": + vsindex = values[0] + continue + values = getattr(private, name) if arg_type == "number": values = [values] diff --git a/contrib/python/fonttools/ya.make b/contrib/python/fonttools/ya.make index f8dcab48540..21543c545db 100644 --- a/contrib/python/fonttools/ya.make +++ b/contrib/python/fonttools/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(4.58.0) +VERSION(4.58.1) LICENSE(MIT) |