fonttools 4.57.0__py3-none-any.whl → 4.58.0__py3-none-any.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.
Potentially problematic release.
This version of fonttools might be problematic. Click here for more details.
- fontTools/__init__.py +1 -1
- fontTools/cffLib/__init__.py +61 -26
- fontTools/designspaceLib/statNames.py +14 -7
- fontTools/feaLib/ast.py +84 -10
- fontTools/feaLib/builder.py +20 -4
- fontTools/feaLib/parser.py +1 -39
- fontTools/fontBuilder.py +6 -0
- fontTools/misc/etree.py +4 -27
- fontTools/mtiLib/__init__.py +0 -2
- fontTools/otlLib/builder.py +195 -145
- fontTools/otlLib/optimize/gpos.py +42 -62
- fontTools/pens/pointPen.py +21 -12
- fontTools/subset/__init__.py +11 -0
- fontTools/ttLib/tables/G_V_A_R_.py +5 -0
- fontTools/ttLib/tables/T_S_I__0.py +14 -3
- fontTools/ttLib/tables/T_S_I__5.py +16 -5
- fontTools/ttLib/tables/__init__.py +1 -0
- fontTools/ttLib/tables/_c_v_t.py +2 -0
- fontTools/ttLib/tables/_f_p_g_m.py +3 -1
- fontTools/ttLib/tables/_g_l_y_f.py +2 -6
- fontTools/ttLib/tables/_g_v_a_r.py +58 -15
- fontTools/ttLib/tables/_p_o_s_t.py +5 -2
- fontTools/ttLib/tables/otBase.py +1 -0
- fontTools/ufoLib/__init__.py +2 -2
- fontTools/ufoLib/converters.py +89 -25
- fontTools/ufoLib/errors.py +8 -0
- fontTools/ufoLib/etree.py +1 -1
- fontTools/ufoLib/filenames.py +155 -100
- fontTools/ufoLib/glifLib.py +9 -2
- fontTools/ufoLib/kerning.py +66 -36
- fontTools/ufoLib/utils.py +5 -2
- fontTools/unicodedata/Mirrored.py +446 -0
- fontTools/unicodedata/__init__.py +6 -2
- fontTools/varLib/__init__.py +2 -0
- fontTools/voltLib/__main__.py +206 -0
- fontTools/voltLib/ast.py +4 -0
- fontTools/voltLib/parser.py +16 -8
- fontTools/voltLib/voltToFea.py +347 -166
- {fonttools-4.57.0.dist-info → fonttools-4.58.0.dist-info}/METADATA +45 -11
- {fonttools-4.57.0.dist-info → fonttools-4.58.0.dist-info}/RECORD +46 -42
- {fonttools-4.57.0.dist-info → fonttools-4.58.0.dist-info}/WHEEL +1 -1
- fonttools-4.58.0.dist-info/licenses/LICENSE.external +359 -0
- {fonttools-4.57.0.data → fonttools-4.58.0.data}/data/share/man/man1/ttx.1 +0 -0
- {fonttools-4.57.0.dist-info → fonttools-4.58.0.dist-info}/entry_points.txt +0 -0
- {fonttools-4.57.0.dist-info → fonttools-4.58.0.dist-info}/licenses/LICENSE +0 -0
- {fonttools-4.57.0.dist-info → fonttools-4.58.0.dist-info}/top_level.txt +0 -0
fontTools/__init__.py
CHANGED
fontTools/cffLib/__init__.py
CHANGED
|
@@ -1464,10 +1464,11 @@ class CharsetConverter(SimpleConverter):
|
|
|
1464
1464
|
if glyphName in allNames:
|
|
1465
1465
|
# make up a new glyphName that's unique
|
|
1466
1466
|
n = allNames[glyphName]
|
|
1467
|
-
|
|
1467
|
+
names = set(allNames) | set(charset)
|
|
1468
|
+
while (glyphName + "." + str(n)) in names:
|
|
1468
1469
|
n += 1
|
|
1469
1470
|
allNames[glyphName] = n + 1
|
|
1470
|
-
glyphName = glyphName + "
|
|
1471
|
+
glyphName = glyphName + "." + str(n)
|
|
1471
1472
|
allNames[glyphName] = 1
|
|
1472
1473
|
newCharset.append(glyphName)
|
|
1473
1474
|
charset = newCharset
|
|
@@ -1663,25 +1664,26 @@ class EncodingConverter(SimpleConverter):
|
|
|
1663
1664
|
return "StandardEncoding"
|
|
1664
1665
|
elif value == 1:
|
|
1665
1666
|
return "ExpertEncoding"
|
|
1667
|
+
# custom encoding at offset `value`
|
|
1668
|
+
assert value > 1
|
|
1669
|
+
file = parent.file
|
|
1670
|
+
file.seek(value)
|
|
1671
|
+
log.log(DEBUG, "loading Encoding at %s", value)
|
|
1672
|
+
fmt = readCard8(file)
|
|
1673
|
+
haveSupplement = bool(fmt & 0x80)
|
|
1674
|
+
fmt = fmt & 0x7F
|
|
1675
|
+
|
|
1676
|
+
if fmt == 0:
|
|
1677
|
+
encoding = parseEncoding0(parent.charset, file)
|
|
1678
|
+
elif fmt == 1:
|
|
1679
|
+
encoding = parseEncoding1(parent.charset, file)
|
|
1666
1680
|
else:
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
if haveSupplement:
|
|
1674
|
-
raise NotImplementedError("Encoding supplements are not yet supported")
|
|
1675
|
-
fmt = fmt & 0x7F
|
|
1676
|
-
if fmt == 0:
|
|
1677
|
-
encoding = parseEncoding0(
|
|
1678
|
-
parent.charset, file, haveSupplement, parent.strings
|
|
1679
|
-
)
|
|
1680
|
-
elif fmt == 1:
|
|
1681
|
-
encoding = parseEncoding1(
|
|
1682
|
-
parent.charset, file, haveSupplement, parent.strings
|
|
1683
|
-
)
|
|
1684
|
-
return encoding
|
|
1681
|
+
raise ValueError(f"Unknown Encoding format: {fmt}")
|
|
1682
|
+
|
|
1683
|
+
if haveSupplement:
|
|
1684
|
+
parseEncodingSupplement(file, encoding, parent.strings)
|
|
1685
|
+
|
|
1686
|
+
return encoding
|
|
1685
1687
|
|
|
1686
1688
|
def write(self, parent, value):
|
|
1687
1689
|
if value == "StandardEncoding":
|
|
@@ -1719,27 +1721,60 @@ class EncodingConverter(SimpleConverter):
|
|
|
1719
1721
|
return encoding
|
|
1720
1722
|
|
|
1721
1723
|
|
|
1722
|
-
def
|
|
1724
|
+
def readSID(file):
|
|
1725
|
+
"""Read a String ID (SID) — 2-byte unsigned integer."""
|
|
1726
|
+
data = file.read(2)
|
|
1727
|
+
if len(data) != 2:
|
|
1728
|
+
raise EOFError("Unexpected end of file while reading SID")
|
|
1729
|
+
return struct.unpack(">H", data)[0] # big-endian uint16
|
|
1730
|
+
|
|
1731
|
+
|
|
1732
|
+
def parseEncodingSupplement(file, encoding, strings):
|
|
1733
|
+
"""
|
|
1734
|
+
Parse the CFF Encoding supplement data:
|
|
1735
|
+
- nSups: number of supplementary mappings
|
|
1736
|
+
- each mapping: (code, SID) pair
|
|
1737
|
+
and apply them to the `encoding` list in place.
|
|
1738
|
+
"""
|
|
1739
|
+
nSups = readCard8(file)
|
|
1740
|
+
for _ in range(nSups):
|
|
1741
|
+
code = readCard8(file)
|
|
1742
|
+
sid = readSID(file)
|
|
1743
|
+
name = strings[sid]
|
|
1744
|
+
encoding[code] = name
|
|
1745
|
+
|
|
1746
|
+
|
|
1747
|
+
def parseEncoding0(charset, file):
|
|
1748
|
+
"""
|
|
1749
|
+
Format 0: simple list of codes.
|
|
1750
|
+
After reading the base table, optionally parse the supplement.
|
|
1751
|
+
"""
|
|
1723
1752
|
nCodes = readCard8(file)
|
|
1724
1753
|
encoding = [".notdef"] * 256
|
|
1725
1754
|
for glyphID in range(1, nCodes + 1):
|
|
1726
1755
|
code = readCard8(file)
|
|
1727
1756
|
if code != 0:
|
|
1728
1757
|
encoding[code] = charset[glyphID]
|
|
1758
|
+
|
|
1729
1759
|
return encoding
|
|
1730
1760
|
|
|
1731
1761
|
|
|
1732
|
-
def parseEncoding1(charset, file
|
|
1762
|
+
def parseEncoding1(charset, file):
|
|
1763
|
+
"""
|
|
1764
|
+
Format 1: range-based encoding.
|
|
1765
|
+
After reading the base ranges, optionally parse the supplement.
|
|
1766
|
+
"""
|
|
1733
1767
|
nRanges = readCard8(file)
|
|
1734
1768
|
encoding = [".notdef"] * 256
|
|
1735
1769
|
glyphID = 1
|
|
1736
|
-
for
|
|
1770
|
+
for _ in range(nRanges):
|
|
1737
1771
|
code = readCard8(file)
|
|
1738
1772
|
nLeft = readCard8(file)
|
|
1739
|
-
for
|
|
1773
|
+
for _ in range(nLeft + 1):
|
|
1740
1774
|
encoding[code] = charset[glyphID]
|
|
1741
|
-
code
|
|
1742
|
-
|
|
1775
|
+
code += 1
|
|
1776
|
+
glyphID += 1
|
|
1777
|
+
|
|
1743
1778
|
return encoding
|
|
1744
1779
|
|
|
1745
1780
|
|
|
@@ -12,14 +12,13 @@ instance:
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
from dataclasses import dataclass
|
|
15
|
-
from typing import Dict, Optional, Tuple, Union
|
|
15
|
+
from typing import Dict, Literal, Optional, Tuple, Union
|
|
16
16
|
import logging
|
|
17
17
|
|
|
18
18
|
from fontTools.designspaceLib import (
|
|
19
19
|
AxisDescriptor,
|
|
20
20
|
AxisLabelDescriptor,
|
|
21
21
|
DesignSpaceDocument,
|
|
22
|
-
DesignSpaceDocumentError,
|
|
23
22
|
DiscreteAxisDescriptor,
|
|
24
23
|
SimpleLocationDict,
|
|
25
24
|
SourceDescriptor,
|
|
@@ -27,9 +26,13 @@ from fontTools.designspaceLib import (
|
|
|
27
26
|
|
|
28
27
|
LOGGER = logging.getLogger(__name__)
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
RibbiStyleName = Union[
|
|
30
|
+
Literal["regular"],
|
|
31
|
+
Literal["bold"],
|
|
32
|
+
Literal["italic"],
|
|
33
|
+
Literal["bold italic"],
|
|
34
|
+
]
|
|
35
|
+
|
|
33
36
|
BOLD_ITALIC_TO_RIBBI_STYLE = {
|
|
34
37
|
(False, False): "regular",
|
|
35
38
|
(False, True): "italic",
|
|
@@ -46,7 +49,7 @@ class StatNames:
|
|
|
46
49
|
styleNames: Dict[str, str]
|
|
47
50
|
postScriptFontName: Optional[str]
|
|
48
51
|
styleMapFamilyNames: Dict[str, str]
|
|
49
|
-
styleMapStyleName: Optional[
|
|
52
|
+
styleMapStyleName: Optional[RibbiStyleName]
|
|
50
53
|
|
|
51
54
|
|
|
52
55
|
def getStatNames(
|
|
@@ -61,6 +64,10 @@ def getStatNames(
|
|
|
61
64
|
localized names will be empty (family and style names), or the name will be
|
|
62
65
|
None (PostScript name).
|
|
63
66
|
|
|
67
|
+
Note: this method does not consider info attached to the instance, like
|
|
68
|
+
family name. The user needs to override all names on an instance that STAT
|
|
69
|
+
information would compute differently than desired.
|
|
70
|
+
|
|
64
71
|
.. versionadded:: 5.0
|
|
65
72
|
"""
|
|
66
73
|
familyNames: Dict[str, str] = {}
|
|
@@ -201,7 +208,7 @@ def _getAxisLabelsForUserLocation(
|
|
|
201
208
|
|
|
202
209
|
def _getRibbiStyle(
|
|
203
210
|
self: DesignSpaceDocument, userLocation: SimpleLocationDict
|
|
204
|
-
) -> Tuple[
|
|
211
|
+
) -> Tuple[RibbiStyleName, SimpleLocationDict]:
|
|
205
212
|
"""Compute the RIBBI style name of the given user location,
|
|
206
213
|
return the location of the matching Regular in the RIBBI group.
|
|
207
214
|
|
fontTools/feaLib/ast.py
CHANGED
|
@@ -337,6 +337,76 @@ class AnonymousBlock(Statement):
|
|
|
337
337
|
return res
|
|
338
338
|
|
|
339
339
|
|
|
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
|
+
|
|
340
410
|
class Block(Statement):
|
|
341
411
|
"""A block of statements: feature, lookup, etc."""
|
|
342
412
|
|
|
@@ -348,7 +418,8 @@ class Block(Statement):
|
|
|
348
418
|
"""When handed a 'builder' object of comparable interface to
|
|
349
419
|
:class:`fontTools.feaLib.builder`, walks the statements in this
|
|
350
420
|
block, calling the builder callbacks."""
|
|
351
|
-
|
|
421
|
+
statements = _upgrade_mixed_subst_statements(self.statements)
|
|
422
|
+
for s in statements:
|
|
352
423
|
s.build(builder)
|
|
353
424
|
|
|
354
425
|
def asFea(self, indent=""):
|
|
@@ -382,8 +453,7 @@ class FeatureBlock(Block):
|
|
|
382
453
|
def build(self, builder):
|
|
383
454
|
"""Call the ``start_feature`` callback on the builder object, visit
|
|
384
455
|
all the statements in this feature, and then call ``end_feature``."""
|
|
385
|
-
|
|
386
|
-
builder.start_feature(self.location, self.name)
|
|
456
|
+
builder.start_feature(self.location, self.name, self.use_extension)
|
|
387
457
|
# language exclude_dflt statements modify builder.features_
|
|
388
458
|
# limit them to this block with temporary builder.features_
|
|
389
459
|
features = builder.features_
|
|
@@ -433,8 +503,7 @@ class LookupBlock(Block):
|
|
|
433
503
|
self.name, self.use_extension = name, use_extension
|
|
434
504
|
|
|
435
505
|
def build(self, builder):
|
|
436
|
-
|
|
437
|
-
builder.start_lookup_block(self.location, self.name)
|
|
506
|
+
builder.start_lookup_block(self.location, self.name, self.use_extension)
|
|
438
507
|
Block.build(self, builder)
|
|
439
508
|
builder.end_lookup_block()
|
|
440
509
|
|
|
@@ -753,7 +822,7 @@ class ChainContextPosStatement(Statement):
|
|
|
753
822
|
if len(self.suffix):
|
|
754
823
|
res += " " + " ".join(map(asFea, self.suffix))
|
|
755
824
|
else:
|
|
756
|
-
res += " ".join(map(asFea, self.
|
|
825
|
+
res += " ".join(map(asFea, self.glyphs))
|
|
757
826
|
res += ";"
|
|
758
827
|
return res
|
|
759
828
|
|
|
@@ -811,7 +880,7 @@ class ChainContextSubstStatement(Statement):
|
|
|
811
880
|
if len(self.suffix):
|
|
812
881
|
res += " " + " ".join(map(asFea, self.suffix))
|
|
813
882
|
else:
|
|
814
|
-
res += " ".join(map(asFea, self.
|
|
883
|
+
res += " ".join(map(asFea, self.glyphs))
|
|
815
884
|
res += ";"
|
|
816
885
|
return res
|
|
817
886
|
|
|
@@ -1512,7 +1581,9 @@ class SinglePosStatement(Statement):
|
|
|
1512
1581
|
res += " ".join(map(asFea, self.prefix)) + " "
|
|
1513
1582
|
res += " ".join(
|
|
1514
1583
|
[
|
|
1515
|
-
asFea(x[0])
|
|
1584
|
+
asFea(x[0])
|
|
1585
|
+
+ "'"
|
|
1586
|
+
+ ((" " + x[1].asFea()) if x[1] is not None else "")
|
|
1516
1587
|
for x in self.pos
|
|
1517
1588
|
]
|
|
1518
1589
|
)
|
|
@@ -1520,7 +1591,10 @@ class SinglePosStatement(Statement):
|
|
|
1520
1591
|
res += " " + " ".join(map(asFea, self.suffix))
|
|
1521
1592
|
else:
|
|
1522
1593
|
res += " ".join(
|
|
1523
|
-
[
|
|
1594
|
+
[
|
|
1595
|
+
asFea(x[0]) + " " + (x[1].asFea() if x[1] is not None else "")
|
|
1596
|
+
for x in self.pos
|
|
1597
|
+
]
|
|
1524
1598
|
)
|
|
1525
1599
|
res += ";"
|
|
1526
1600
|
return res
|
|
@@ -2103,7 +2177,7 @@ class VariationBlock(Block):
|
|
|
2103
2177
|
def build(self, builder):
|
|
2104
2178
|
"""Call the ``start_feature`` callback on the builder object, visit
|
|
2105
2179
|
all the statements in this feature, and then call ``end_feature``."""
|
|
2106
|
-
builder.start_feature(self.location, self.name)
|
|
2180
|
+
builder.start_feature(self.location, self.name, self.use_extension)
|
|
2107
2181
|
if (
|
|
2108
2182
|
self.conditionset != "NULL"
|
|
2109
2183
|
and self.conditionset not in builder.conditionsets_
|
fontTools/feaLib/builder.py
CHANGED
|
@@ -126,6 +126,7 @@ class Builder(object):
|
|
|
126
126
|
self.script_ = None
|
|
127
127
|
self.lookupflag_ = 0
|
|
128
128
|
self.lookupflag_markFilterSet_ = None
|
|
129
|
+
self.use_extension_ = False
|
|
129
130
|
self.language_systems = set()
|
|
130
131
|
self.seen_non_DFLT_script_ = False
|
|
131
132
|
self.named_lookups_ = {}
|
|
@@ -141,6 +142,7 @@ class Builder(object):
|
|
|
141
142
|
self.aalt_features_ = [] # [(location, featureName)*], for 'aalt'
|
|
142
143
|
self.aalt_location_ = None
|
|
143
144
|
self.aalt_alternates_ = {}
|
|
145
|
+
self.aalt_use_extension_ = False
|
|
144
146
|
# for 'featureNames'
|
|
145
147
|
self.featureNames_ = set()
|
|
146
148
|
self.featureNames_ids_ = {}
|
|
@@ -247,6 +249,7 @@ class Builder(object):
|
|
|
247
249
|
result = builder_class(self.font, location)
|
|
248
250
|
result.lookupflag = self.lookupflag_
|
|
249
251
|
result.markFilterSet = self.lookupflag_markFilterSet_
|
|
252
|
+
result.extension = self.use_extension_
|
|
250
253
|
self.lookups_.append(result)
|
|
251
254
|
return result
|
|
252
255
|
|
|
@@ -272,6 +275,7 @@ class Builder(object):
|
|
|
272
275
|
self.cur_lookup_ = builder_class(self.font, location)
|
|
273
276
|
self.cur_lookup_.lookupflag = self.lookupflag_
|
|
274
277
|
self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_
|
|
278
|
+
self.cur_lookup_.extension = self.use_extension_
|
|
275
279
|
self.lookups_.append(self.cur_lookup_)
|
|
276
280
|
if self.cur_lookup_name_:
|
|
277
281
|
# We are starting a lookup rule inside a named lookup block.
|
|
@@ -323,7 +327,7 @@ class Builder(object):
|
|
|
323
327
|
}
|
|
324
328
|
old_lookups = self.lookups_
|
|
325
329
|
self.lookups_ = []
|
|
326
|
-
self.start_feature(self.aalt_location_, "aalt")
|
|
330
|
+
self.start_feature(self.aalt_location_, "aalt", self.aalt_use_extension_)
|
|
327
331
|
if single:
|
|
328
332
|
single_lookup = self.get_lookup_(location, SingleSubstBuilder)
|
|
329
333
|
single_lookup.mapping = single
|
|
@@ -1054,15 +1058,22 @@ class Builder(object):
|
|
|
1054
1058
|
else:
|
|
1055
1059
|
return frozenset({("DFLT", "dflt")})
|
|
1056
1060
|
|
|
1057
|
-
def start_feature(self, location, name):
|
|
1061
|
+
def start_feature(self, location, name, use_extension=False):
|
|
1062
|
+
if use_extension and name != "aalt":
|
|
1063
|
+
raise FeatureLibError(
|
|
1064
|
+
"'useExtension' keyword for feature blocks is allowed only for 'aalt' feature",
|
|
1065
|
+
location,
|
|
1066
|
+
)
|
|
1058
1067
|
self.language_systems = self.get_default_language_systems_()
|
|
1059
1068
|
self.script_ = "DFLT"
|
|
1060
1069
|
self.cur_lookup_ = None
|
|
1061
1070
|
self.cur_feature_name_ = name
|
|
1062
1071
|
self.lookupflag_ = 0
|
|
1063
1072
|
self.lookupflag_markFilterSet_ = None
|
|
1073
|
+
self.use_extension_ = use_extension
|
|
1064
1074
|
if name == "aalt":
|
|
1065
1075
|
self.aalt_location_ = location
|
|
1076
|
+
self.aalt_use_extension_ = use_extension
|
|
1066
1077
|
|
|
1067
1078
|
def end_feature(self):
|
|
1068
1079
|
assert self.cur_feature_name_ is not None
|
|
@@ -1071,8 +1082,9 @@ class Builder(object):
|
|
|
1071
1082
|
self.cur_lookup_ = None
|
|
1072
1083
|
self.lookupflag_ = 0
|
|
1073
1084
|
self.lookupflag_markFilterSet_ = None
|
|
1085
|
+
self.use_extension_ = False
|
|
1074
1086
|
|
|
1075
|
-
def start_lookup_block(self, location, name):
|
|
1087
|
+
def start_lookup_block(self, location, name, use_extension=False):
|
|
1076
1088
|
if name in self.named_lookups_:
|
|
1077
1089
|
raise FeatureLibError(
|
|
1078
1090
|
'Lookup "%s" has already been defined' % name, location
|
|
@@ -1086,6 +1098,7 @@ class Builder(object):
|
|
|
1086
1098
|
self.cur_lookup_name_ = name
|
|
1087
1099
|
self.named_lookups_[name] = None
|
|
1088
1100
|
self.cur_lookup_ = None
|
|
1101
|
+
self.use_extension_ = use_extension
|
|
1089
1102
|
if self.cur_feature_name_ is None:
|
|
1090
1103
|
self.lookupflag_ = 0
|
|
1091
1104
|
self.lookupflag_markFilterSet_ = None
|
|
@@ -1094,6 +1107,7 @@ class Builder(object):
|
|
|
1094
1107
|
assert self.cur_lookup_name_ is not None
|
|
1095
1108
|
self.cur_lookup_name_ = None
|
|
1096
1109
|
self.cur_lookup_ = None
|
|
1110
|
+
self.use_extension_ = False
|
|
1097
1111
|
if self.cur_feature_name_ is None:
|
|
1098
1112
|
self.lookupflag_ = 0
|
|
1099
1113
|
self.lookupflag_markFilterSet_ = None
|
|
@@ -1471,7 +1485,9 @@ class Builder(object):
|
|
|
1471
1485
|
lookup = self.get_lookup_(location, PairPosBuilder)
|
|
1472
1486
|
v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
|
|
1473
1487
|
v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
|
|
1474
|
-
|
|
1488
|
+
cls1 = tuple(sorted(set(glyphclass1)))
|
|
1489
|
+
cls2 = tuple(sorted(set(glyphclass2)))
|
|
1490
|
+
lookup.addClassPair(location, cls1, v1, cls2, v2)
|
|
1475
1491
|
|
|
1476
1492
|
def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
|
|
1477
1493
|
if not glyph1 or not glyph2:
|
fontTools/feaLib/parser.py
CHANGED
|
@@ -1613,7 +1613,7 @@ class Parser(object):
|
|
|
1613
1613
|
"HorizAxis.BaseScriptList",
|
|
1614
1614
|
"VertAxis.BaseScriptList",
|
|
1615
1615
|
), self.cur_token_
|
|
1616
|
-
scripts = [
|
|
1616
|
+
scripts = [self.parse_base_script_record_(count)]
|
|
1617
1617
|
while self.next_token_ == ",":
|
|
1618
1618
|
self.expect_symbol_(",")
|
|
1619
1619
|
scripts.append(self.parse_base_script_record_(count))
|
|
@@ -2062,44 +2062,6 @@ class Parser(object):
|
|
|
2062
2062
|
)
|
|
2063
2063
|
self.expect_symbol_(";")
|
|
2064
2064
|
|
|
2065
|
-
# A multiple substitution may have a single destination, in which case
|
|
2066
|
-
# it will look just like a single substitution. So if there are both
|
|
2067
|
-
# multiple and single substitutions, upgrade all the single ones to
|
|
2068
|
-
# multiple substitutions.
|
|
2069
|
-
|
|
2070
|
-
# Check if we have a mix of non-contextual singles and multiples.
|
|
2071
|
-
has_single = False
|
|
2072
|
-
has_multiple = False
|
|
2073
|
-
for s in statements:
|
|
2074
|
-
if isinstance(s, self.ast.SingleSubstStatement):
|
|
2075
|
-
has_single = not any([s.prefix, s.suffix, s.forceChain])
|
|
2076
|
-
elif isinstance(s, self.ast.MultipleSubstStatement):
|
|
2077
|
-
has_multiple = not any([s.prefix, s.suffix, s.forceChain])
|
|
2078
|
-
|
|
2079
|
-
# Upgrade all single substitutions to multiple substitutions.
|
|
2080
|
-
if has_single and has_multiple:
|
|
2081
|
-
statements = []
|
|
2082
|
-
for s in block.statements:
|
|
2083
|
-
if isinstance(s, self.ast.SingleSubstStatement):
|
|
2084
|
-
glyphs = s.glyphs[0].glyphSet()
|
|
2085
|
-
replacements = s.replacements[0].glyphSet()
|
|
2086
|
-
if len(replacements) == 1:
|
|
2087
|
-
replacements *= len(glyphs)
|
|
2088
|
-
for i, glyph in enumerate(glyphs):
|
|
2089
|
-
statements.append(
|
|
2090
|
-
self.ast.MultipleSubstStatement(
|
|
2091
|
-
s.prefix,
|
|
2092
|
-
glyph,
|
|
2093
|
-
s.suffix,
|
|
2094
|
-
[replacements[i]],
|
|
2095
|
-
s.forceChain,
|
|
2096
|
-
location=s.location,
|
|
2097
|
-
)
|
|
2098
|
-
)
|
|
2099
|
-
else:
|
|
2100
|
-
statements.append(s)
|
|
2101
|
-
block.statements = statements
|
|
2102
|
-
|
|
2103
2065
|
def is_cur_keyword_(self, k):
|
|
2104
2066
|
if self.cur_token_type_ is Lexer.NAME:
|
|
2105
2067
|
if isinstance(k, type("")): # basestring is gone in Python3
|
fontTools/fontBuilder.py
CHANGED
|
@@ -714,6 +714,12 @@ class FontBuilder(object):
|
|
|
714
714
|
gvar.reserved = 0
|
|
715
715
|
gvar.variations = variations
|
|
716
716
|
|
|
717
|
+
def setupGVAR(self, variations):
|
|
718
|
+
gvar = self.font["GVAR"] = newTable("GVAR")
|
|
719
|
+
gvar.version = 1
|
|
720
|
+
gvar.reserved = 0
|
|
721
|
+
gvar.variations = variations
|
|
722
|
+
|
|
717
723
|
def calcGlyphBounds(self):
|
|
718
724
|
"""Calculate the bounding boxes of all glyphs in the `glyf` table.
|
|
719
725
|
This is usually not called explicitly by client code.
|
fontTools/misc/etree.py
CHANGED
|
@@ -56,21 +56,7 @@ except ImportError:
|
|
|
56
56
|
from xml.etree.ElementTree import *
|
|
57
57
|
_have_lxml = False
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# dict is always ordered in python >= 3.6 and on pypy
|
|
62
|
-
PY36 = sys.version_info >= (3, 6)
|
|
63
|
-
try:
|
|
64
|
-
import __pypy__
|
|
65
|
-
except ImportError:
|
|
66
|
-
__pypy__ = None
|
|
67
|
-
_dict_is_ordered = bool(PY36 or __pypy__)
|
|
68
|
-
del PY36, __pypy__
|
|
69
|
-
|
|
70
|
-
if _dict_is_ordered:
|
|
71
|
-
_Attrib = dict
|
|
72
|
-
else:
|
|
73
|
-
from collections import OrderedDict as _Attrib
|
|
59
|
+
_Attrib = dict
|
|
74
60
|
|
|
75
61
|
if isinstance(Element, type):
|
|
76
62
|
_Element = Element
|
|
@@ -221,18 +207,9 @@ except ImportError:
|
|
|
221
207
|
# characters, the surrogate blocks, FFFE, and FFFF:
|
|
222
208
|
# Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
|
|
223
209
|
# Here we reversed the pattern to match only the invalid characters.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
UCS2 = sys.maxunicode < 0x10FFFF
|
|
228
|
-
if UCS2:
|
|
229
|
-
_invalid_xml_string = re.compile(
|
|
230
|
-
"[\u0000-\u0008\u000B-\u000C\u000E-\u001F\uFFFE-\uFFFF]"
|
|
231
|
-
)
|
|
232
|
-
else:
|
|
233
|
-
_invalid_xml_string = re.compile(
|
|
234
|
-
"[\u0000-\u0008\u000B-\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE-\uFFFF]"
|
|
235
|
-
)
|
|
210
|
+
_invalid_xml_string = re.compile(
|
|
211
|
+
"[\u0000-\u0008\u000B-\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE-\uFFFF]"
|
|
212
|
+
)
|
|
236
213
|
|
|
237
214
|
def _tounicode(s):
|
|
238
215
|
"""Test if a string is valid user input and decode it to unicode string
|