summaryrefslogtreecommitdiffstats
path: root/contrib/python
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2025-06-18 23:55:52 +0300
committerrobot-piglet <[email protected]>2025-06-19 00:06:55 +0300
commit61ea5fc0cd73730d5ae6a9186d8c15057f183095 (patch)
tree31ddc3fcbabb680d9ce733ea36f581a6d062aff9 /contrib/python
parent2fcfb855cd7780ab07751cc16c80a0a58168668a (diff)
Intermediate changes
commit_hash:283b0c2ecb46f54501d7d1b92643c0090cebaa95
Diffstat (limited to 'contrib/python')
-rw-r--r--contrib/python/fonttools/.dist-info/METADATA21
-rw-r--r--contrib/python/fonttools/fontTools/__init__.py2
-rw-r--r--contrib/python/fonttools/fontTools/cffLib/specializer.py5
-rw-r--r--contrib/python/fonttools/fontTools/feaLib/ast.py73
-rw-r--r--contrib/python/fonttools/fontTools/feaLib/builder.py100
-rw-r--r--contrib/python/fonttools/fontTools/merge/cmap.py34
-rw-r--r--contrib/python/fonttools/fontTools/merge/tables.py13
-rw-r--r--contrib/python/fonttools/fontTools/misc/loggingTools.py2
-rw-r--r--contrib/python/fonttools/fontTools/misc/symfont.py14
-rw-r--r--contrib/python/fonttools/fontTools/mtiLib/__init__.py2
-rw-r--r--contrib/python/fonttools/fontTools/otlLib/builder.py164
-rw-r--r--contrib/python/fonttools/fontTools/pens/t2CharStringPen.py42
-rw-r--r--contrib/python/fonttools/fontTools/subset/__init__.py2
-rw-r--r--contrib/python/fonttools/fontTools/ufoLib/__init__.py2
-rw-r--r--contrib/python/fonttools/fontTools/varLib/__init__.py24
-rw-r--r--contrib/python/fonttools/fontTools/varLib/featureVars.py20
-rw-r--r--contrib/python/fonttools/fontTools/varLib/hvar.py2
-rw-r--r--contrib/python/fonttools/fontTools/varLib/instancer/__init__.py19
-rw-r--r--contrib/python/fonttools/ya.make2
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)