Skip to content

Commit 61ea5fc

Browse files
committed
Intermediate changes
commit_hash:283b0c2ecb46f54501d7d1b92643c0090cebaa95
1 parent 2fcfb85 commit 61ea5fc

File tree

19 files changed

+378
-165
lines changed

19 files changed

+378
-165
lines changed

contrib/python/fonttools/.dist-info/METADATA

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Metadata-Version: 2.4
22
Name: fonttools
3-
Version: 4.58.0
3+
Version: 4.58.1
44
Summary: Tools to manipulate font files
55
Home-page: http://github.com/fonttools/fonttools
66
Author: Just van Rossum
@@ -388,6 +388,25 @@ Have fun!
388388
Changelog
389389
~~~~~~~~~
390390

391+
4.58.1 (released 2025-05-28)
392+
----------------------------
393+
394+
- [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).
395+
- [feaLib] Improve single substitution promotion to multiple/ligature substitutions, fixing a few bugs as well (#3849).
396+
- [loggingTools] Make ``Timer._time`` a static method that doesn't take self, makes it easier to override (#3836).
397+
- [featureVars] Use ``None`` for empty ConditionSet, which translates to a null offset in the compiled table (#3850).
398+
- [feaLib] Raise an error on conflicting ligature substitution rules instead of silently taking the last one (#3835).
399+
- Add typing annotations to T2CharStringPen (#3837).
400+
- [feaLib] Add single substitutions that were promoted to multiple or ligature substitutions to ``aalt`` feature (#3847).
401+
- [featureVars] Create a default ``LangSys`` in a ``ScriptRecord`` if missing when adding feature variations to existing GSUB later in the build (#3838).
402+
- [symfont] Added a ``main()``.
403+
- [cffLib.specializer] Fix rmoveto merging when blends used (#3839, #3840).
404+
- [pyftmerge] Add support for cmap format 14 in the merge tool (#3830).
405+
- [varLib.instancer/cff2] Fix vsindex of Private dicts when instantiating (#3828, #3232).
406+
- Update text file read to use UTF-8 with optional BOM so it works with e.g. Windows Notepad.exe (#3824).
407+
- [varLib] Ensure that instances only reuse name ID 2 or 17 if they are at the default location across all axes (#3831).
408+
- [varLib] Create a dflt LangSys in a ScriptRecord when adding variations later, to fix an avoidable crash in an edge case (#3838).
409+
391410
4.58.0 (released 2025-05-10)
392411
----------------------------
393412

contrib/python/fonttools/fontTools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33

44
log = logging.getLogger(__name__)
55

6-
version = __version__ = "4.58.0"
6+
version = __version__ = "4.58.1"
77

88
__all__ = ["version", "log", "configLogger"]

contrib/python/fonttools/fontTools/cffLib/specializer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,10 @@ def specializeCommands(
580580
for i in range(len(commands) - 1, 0, -1):
581581
if "rmoveto" == commands[i][0] == commands[i - 1][0]:
582582
v1, v2 = commands[i - 1][1], commands[i][1]
583-
commands[i - 1] = ("rmoveto", [v1[0] + v2[0], v1[1] + v2[1]])
583+
commands[i - 1] = (
584+
"rmoveto",
585+
[_addArgs(v1[0], v2[0]), _addArgs(v1[1], v2[1])],
586+
)
584587
del commands[i]
585588

586589
# 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants.

contrib/python/fonttools/fontTools/feaLib/ast.py

Lines changed: 1 addition & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -337,76 +337,6 @@ def asFea(self, indent=""):
337337
return res
338338

339339

340-
def _upgrade_mixed_subst_statements(statements):
341-
# https://github.com/fonttools/fonttools/issues/612
342-
# A multiple substitution may have a single destination, in which case
343-
# it will look just like a single substitution. So if there are both
344-
# multiple and single substitutions, upgrade all the single ones to
345-
# multiple substitutions. Similarly, a ligature substitution may have a
346-
# single source glyph, so if there are both ligature and single
347-
# substitutions, upgrade all the single ones to ligature substitutions.
348-
349-
has_single = False
350-
has_multiple = False
351-
has_ligature = False
352-
for s in statements:
353-
if isinstance(s, SingleSubstStatement):
354-
has_single = not any([s.prefix, s.suffix, s.forceChain])
355-
elif isinstance(s, MultipleSubstStatement):
356-
has_multiple = not any([s.prefix, s.suffix, s.forceChain])
357-
elif isinstance(s, LigatureSubstStatement):
358-
has_ligature = not any([s.prefix, s.suffix, s.forceChain])
359-
360-
to_multiple = False
361-
to_ligature = False
362-
363-
# If we have mixed single and multiple substitutions,
364-
# upgrade all single substitutions to multiple substitutions.
365-
if has_single and has_multiple and not has_ligature:
366-
to_multiple = True
367-
368-
# If we have mixed single and ligature substitutions,
369-
# upgrade all single substitutions to ligature substitutions.
370-
elif has_single and has_ligature and not has_multiple:
371-
to_ligature = True
372-
373-
if to_multiple or to_ligature:
374-
ret = []
375-
for s in statements:
376-
if isinstance(s, SingleSubstStatement):
377-
glyphs = s.glyphs[0].glyphSet()
378-
replacements = s.replacements[0].glyphSet()
379-
if len(replacements) == 1:
380-
replacements *= len(glyphs)
381-
for glyph, replacement in zip(glyphs, replacements):
382-
if to_multiple:
383-
ret.append(
384-
MultipleSubstStatement(
385-
s.prefix,
386-
glyph,
387-
s.suffix,
388-
[replacement],
389-
s.forceChain,
390-
location=s.location,
391-
)
392-
)
393-
elif to_ligature:
394-
ret.append(
395-
LigatureSubstStatement(
396-
s.prefix,
397-
[GlyphName(glyph)],
398-
s.suffix,
399-
replacement,
400-
s.forceChain,
401-
location=s.location,
402-
)
403-
)
404-
else:
405-
ret.append(s)
406-
return ret
407-
return statements
408-
409-
410340
class Block(Statement):
411341
"""A block of statements: feature, lookup, etc."""
412342

@@ -418,8 +348,7 @@ def build(self, builder):
418348
"""When handed a 'builder' object of comparable interface to
419349
:class:`fontTools.feaLib.builder`, walks the statements in this
420350
block, calling the builder callbacks."""
421-
statements = _upgrade_mixed_subst_statements(self.statements)
422-
for s in statements:
351+
for s in self.statements:
423352
s.build(builder)
424353

425354
def asFea(self, indent=""):

contrib/python/fonttools/fontTools/feaLib/builder.py

Lines changed: 55 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
PairPosBuilder,
3030
SinglePosBuilder,
3131
ChainContextualRule,
32+
AnySubstBuilder,
3233
)
3334
from fontTools.otlLib.error import OpenTypeLibError
3435
from fontTools.varLib.varStore import OnlineVarStoreBuilder
@@ -866,13 +867,22 @@ def buildLookups_(self, tag):
866867
for lookup in self.lookups_:
867868
if lookup.table != tag:
868869
continue
869-
lookup.lookup_index = len(lookups)
870-
self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo(
871-
location=str(lookup.location),
872-
name=self.get_lookup_name_(lookup),
873-
feature=None,
874-
)
875-
lookups.append(lookup)
870+
name = self.get_lookup_name_(lookup)
871+
resolved = lookup.promote_lookup_type(is_named_lookup=name is not None)
872+
if resolved is None:
873+
raise FeatureLibError(
874+
"Within a named lookup block, all rules must be of "
875+
"the same lookup type and flag",
876+
lookup.location,
877+
)
878+
for l in resolved:
879+
lookup.lookup_index = len(lookups)
880+
self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo(
881+
location=str(lookup.location),
882+
name=name,
883+
feature=None,
884+
)
885+
lookups.append(l)
876886
otLookups = []
877887
for l in lookups:
878888
try:
@@ -1294,6 +1304,24 @@ def set_size_parameters(
12941304

12951305
# GSUB rules
12961306

1307+
def add_any_subst_(self, location, mapping):
1308+
lookup = self.get_lookup_(location, AnySubstBuilder)
1309+
for key, value in mapping.items():
1310+
if key in lookup.mapping:
1311+
if value == lookup.mapping[key]:
1312+
log.info(
1313+
'Removing duplicate substitution from "%s" to "%s" at %s',
1314+
", ".join(key),
1315+
", ".join(value),
1316+
location,
1317+
)
1318+
else:
1319+
raise FeatureLibError(
1320+
'Already defined substitution for "%s"' % ", ".join(key),
1321+
location,
1322+
)
1323+
lookup.mapping[key] = value
1324+
12971325
# GSUB 1
12981326
def add_single_subst(self, location, prefix, suffix, mapping, forceChain):
12991327
if self.cur_feature_name_ == "aalt":
@@ -1305,24 +1333,11 @@ def add_single_subst(self, location, prefix, suffix, mapping, forceChain):
13051333
if prefix or suffix or forceChain:
13061334
self.add_single_subst_chained_(location, prefix, suffix, mapping)
13071335
return
1308-
lookup = self.get_lookup_(location, SingleSubstBuilder)
1309-
for from_glyph, to_glyph in mapping.items():
1310-
if from_glyph in lookup.mapping:
1311-
if to_glyph == lookup.mapping[from_glyph]:
1312-
log.info(
1313-
"Removing duplicate single substitution from glyph"
1314-
' "%s" to "%s" at %s',
1315-
from_glyph,
1316-
to_glyph,
1317-
location,
1318-
)
1319-
else:
1320-
raise FeatureLibError(
1321-
'Already defined rule for replacing glyph "%s" by "%s"'
1322-
% (from_glyph, lookup.mapping[from_glyph]),
1323-
location,
1324-
)
1325-
lookup.mapping[from_glyph] = to_glyph
1336+
1337+
self.add_any_subst_(
1338+
location,
1339+
{(key,): (value,) for key, value in mapping.items()},
1340+
)
13261341

13271342
# GSUB 2
13281343
def add_multiple_subst(
@@ -1331,21 +1346,10 @@ def add_multiple_subst(
13311346
if prefix or suffix or forceChain:
13321347
self.add_multi_subst_chained_(location, prefix, glyph, suffix, replacements)
13331348
return
1334-
lookup = self.get_lookup_(location, MultipleSubstBuilder)
1335-
if glyph in lookup.mapping:
1336-
if replacements == lookup.mapping[glyph]:
1337-
log.info(
1338-
"Removing duplicate multiple substitution from glyph"
1339-
' "%s" to %s%s',
1340-
glyph,
1341-
replacements,
1342-
f" at {location}" if location else "",
1343-
)
1344-
else:
1345-
raise FeatureLibError(
1346-
'Already defined substitution for glyph "%s"' % glyph, location
1347-
)
1348-
lookup.mapping[glyph] = replacements
1349+
self.add_any_subst_(
1350+
location,
1351+
{(glyph,): tuple(replacements)},
1352+
)
13491353

13501354
# GSUB 3
13511355
def add_alternate_subst(self, location, prefix, glyph, suffix, replacement):
@@ -1375,9 +1379,6 @@ def add_ligature_subst(
13751379
location, prefix, glyphs, suffix, replacement
13761380
)
13771381
return
1378-
else:
1379-
lookup = self.get_lookup_(location, LigatureSubstBuilder)
1380-
13811382
if not all(glyphs):
13821383
raise FeatureLibError("Empty glyph class in substitution", location)
13831384

@@ -1386,8 +1387,10 @@ def add_ligature_subst(
13861387
# substitutions to be specified on target sequences that contain
13871388
# glyph classes, the implementation software will enumerate
13881389
# all specific glyph sequences if glyph classes are detected"
1389-
for g in itertools.product(*glyphs):
1390-
lookup.ligatures[g] = replacement
1390+
self.add_any_subst_(
1391+
location,
1392+
{g: (replacement,) for g in itertools.product(*glyphs)},
1393+
)
13911394

13921395
# GSUB 5/6
13931396
def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups):
@@ -1445,6 +1448,13 @@ def add_ligature_subst_chained_(
14451448
sub = self.get_chained_lookup_(location, LigatureSubstBuilder)
14461449

14471450
for g in itertools.product(*glyphs):
1451+
existing = sub.ligatures.get(g, replacement)
1452+
if existing != replacement:
1453+
raise FeatureLibError(
1454+
f"Conflicting ligature sub rules: '{g}' maps to '{existing}' and '{replacement}'",
1455+
location,
1456+
)
1457+
14481458
sub.ligatures[g] = replacement
14491459

14501460
chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, [sub]))

contrib/python/fonttools/fontTools/merge/cmap.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,31 +54,58 @@ def _glyphsAreSame(
5454
return True
5555

5656

57+
def computeMegaUvs(merger, uvsTables):
58+
"""Returns merged UVS subtable (cmap format=14)."""
59+
uvsDict = {}
60+
cmap = merger.cmap
61+
for table in uvsTables:
62+
for variationSelector, uvsMapping in table.uvsDict.items():
63+
if variationSelector not in uvsDict:
64+
uvsDict[variationSelector] = {}
65+
for unicodeValue, glyphName in uvsMapping:
66+
if cmap.get(unicodeValue) == glyphName:
67+
# this is a default variation
68+
glyphName = None
69+
# prefer previous glyph id if both fonts defined UVS
70+
if unicodeValue not in uvsDict[variationSelector]:
71+
uvsDict[variationSelector][unicodeValue] = glyphName
72+
73+
for variationSelector in uvsDict:
74+
uvsDict[variationSelector] = [*uvsDict[variationSelector].items()]
75+
76+
return uvsDict
77+
78+
5779
# Valid (format, platformID, platEncID) triplets for cmap subtables containing
5880
# Unicode BMP-only and Unicode Full Repertoire semantics.
5981
# Cf. OpenType spec for "Platform specific encodings":
6082
# https://docs.microsoft.com/en-us/typography/opentype/spec/name
6183
class _CmapUnicodePlatEncodings:
6284
BMP = {(4, 3, 1), (4, 0, 3), (4, 0, 4), (4, 0, 6)}
6385
FullRepertoire = {(12, 3, 10), (12, 0, 4), (12, 0, 6)}
86+
UVS = {(14, 0, 5)}
6487

6588

6689
def computeMegaCmap(merger, cmapTables):
67-
"""Sets merger.cmap and merger.glyphOrder."""
90+
"""Sets merger.cmap and merger.uvsDict."""
6891

6992
# TODO Handle format=14.
7093
# Only merge format 4 and 12 Unicode subtables, ignores all other subtables
7194
# If there is a format 12 table for a font, ignore the format 4 table of it
7295
chosenCmapTables = []
96+
chosenUvsTables = []
7397
for fontIdx, table in enumerate(cmapTables):
7498
format4 = None
7599
format12 = None
100+
format14 = None
76101
for subtable in table.tables:
77102
properties = (subtable.format, subtable.platformID, subtable.platEncID)
78103
if properties in _CmapUnicodePlatEncodings.BMP:
79104
format4 = subtable
80105
elif properties in _CmapUnicodePlatEncodings.FullRepertoire:
81106
format12 = subtable
107+
elif properties in _CmapUnicodePlatEncodings.UVS:
108+
format14 = subtable
82109
else:
83110
log.warning(
84111
"Dropped cmap subtable from font '%s':\t"
@@ -93,6 +120,9 @@ def computeMegaCmap(merger, cmapTables):
93120
elif format4 is not None:
94121
chosenCmapTables.append((format4, fontIdx))
95122

123+
if format14 is not None:
124+
chosenUvsTables.append(format14)
125+
96126
# Build the unicode mapping
97127
merger.cmap = cmap = {}
98128
fontIndexForGlyph = {}
@@ -127,6 +157,8 @@ def computeMegaCmap(merger, cmapTables):
127157
"Dropped mapping from codepoint %#06X to glyphId '%s'", uni, gid
128158
)
129159

160+
merger.uvsDict = computeMegaUvs(merger, chosenUvsTables)
161+
130162

131163
def renameCFFCharStrings(merger, glyphOrder, cffTable):
132164
"""Rename topDictIndex charStrings based on glyphOrder."""

0 commit comments

Comments
 (0)