aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHaibo Huang <hhb@google.com>2020-08-19 13:00:39 -0700
committerHaibo Huang <hhb@google.com>2020-08-19 13:00:39 -0700
commit3aedd105d78b03a1821e6b20f5ad8f096fa49c7a (patch)
tree151c2229b01f7c1c6bc424641d3498bdc93a49c2
parenta7ee29ebd4c9d39d29ee03f7746bb6af1c3f7e6b (diff)
downloadfonttools-3aedd105d78b03a1821e6b20f5ad8f096fa49c7a.tar.gz
Upgrade fonttools to 4.14.0
Change-Id: Ic1373449418488c7fc89c76a32892c20ab621356
-rw-r--r--Doc/docs-requirements.txt6
-rw-r--r--Doc/source/designspaceLib/readme.rst11
-rw-r--r--Lib/fontTools/__init__.py2
-rw-r--r--Lib/fontTools/feaLib/__main__.py40
-rw-r--r--Lib/fontTools/feaLib/ast.py605
-rw-r--r--Lib/fontTools/feaLib/builder.py447
-rw-r--r--Lib/fontTools/feaLib/error.py2
-rw-r--r--Lib/fontTools/feaLib/lexer.py62
-rw-r--r--Lib/fontTools/feaLib/location.py4
-rw-r--r--Lib/fontTools/feaLib/parser.py986
-rw-r--r--Lib/fontTools/otlLib/builder.py498
-rw-r--r--Lib/fontTools/otlLib/error.py2
-rw-r--r--Lib/fontTools/otlLib/maxContextCalc.py48
-rw-r--r--Lib/fontTools/subset/__init__.py7
-rw-r--r--Lib/fontTools/ttLib/tables/_g_l_y_f.py6
-rw-r--r--Lib/fontTools/ufoLib/filenames.py2
-rw-r--r--Lib/fontTools/varLib/models.py4
-rw-r--r--Lib/fonttools.egg-info/PKG-INFO33
-rw-r--r--Lib/fonttools.egg-info/SOURCES.txt7
-rw-r--r--METADATA8
-rw-r--r--NEWS.rst14
-rw-r--r--PKG-INFO33
-rw-r--r--README.rst5
-rwxr-xr-xSnippets/decompose-ttf.py53
-rw-r--r--Tests/feaLib/builder_test.py8
-rw-r--r--Tests/feaLib/data/ChainSubstSubtable.fea8
-rw-r--r--Tests/feaLib/data/ChainSubstSubtable.ttx20
-rw-r--r--Tests/feaLib/data/SubstSubtable.fea9
-rw-r--r--Tests/feaLib/data/SubstSubtable.ttx116
-rw-r--r--Tests/feaLib/data/bug506.ttx20
-rw-r--r--Tests/feaLib/data/bug509.ttx30
-rw-r--r--Tests/feaLib/data/bug512.ttx58
-rw-r--r--Tests/feaLib/data/lookupflag.fea10
-rw-r--r--Tests/feaLib/data/lookupflag.ttx27
-rw-r--r--Tests/feaLib/data/spec6h_ii.ttx24
-rw-r--r--Tests/feaLib/data/spec6h_iii_3d.ttx20
-rw-r--r--Tests/feaLib/parser_test.py24
-rw-r--r--Tests/otlLib/builder_test.py51
-rw-r--r--Tests/otlLib/mock_builder_test.py3
-rw-r--r--Tests/ttLib/tables/_g_l_y_f_test.py24
-rw-r--r--Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_diff.ttx (renamed from Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx)24
-rw-r--r--Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_same.ttx (renamed from Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx)24
-rw-r--r--Tests/varLib/interpolate_layout_test.py12
-rw-r--r--requirements.txt2
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py2
46 files changed, 2185 insertions, 1218 deletions
diff --git a/Doc/docs-requirements.txt b/Doc/docs-requirements.txt
index 998172d5..4ea38275 100644
--- a/Doc/docs-requirements.txt
+++ b/Doc/docs-requirements.txt
@@ -1,3 +1,3 @@
-sphinx==3.0.3
-sphinx_rtd_theme == 0.4.3
-reportlab == 3.5.42
+sphinx==3.2.1
+sphinx_rtd_theme==0.5.0
+reportlab==3.5.47
diff --git a/Doc/source/designspaceLib/readme.rst b/Doc/source/designspaceLib/readme.rst
index d563e850..c5757a6e 100644
--- a/Doc/source/designspaceLib/readme.rst
+++ b/Doc/source/designspaceLib/readme.rst
@@ -81,7 +81,7 @@ Methods
location. Returns None if there isn't one.
- ``normalizeLocation(aLocation)``: return a dict with normalized axis values.
- ``normalize()``: normalize the geometry of this designspace: scale all the
- locations of all masters and instances to the ``-1 - 0 - 1`` value.
+ locations of all masters and instances to the ``-1 - 0 - 1`` value.
- ``loadSourceFonts()``: Ensure SourceDescriptor.font attributes are loaded,
and return list of fonts.
- ``tostring(encoding=None)``: Returns the designspace as a string. Default
@@ -297,6 +297,7 @@ RuleDescriptor object
- Each condition is a dict with ``name``, ``minimum`` and ``maximum`` keys.
- ``subs``: list of substitutions
- Each substitution is stored as tuples of glyphnames, e.g. ("a", "a.alt").
+- Note: By default, rules are applied first, before other text shaping/OpenType layout, as they are part of the `Required Variation Alternates OpenType feature <https://docs.microsoft.com/en-us/typography/opentype/spec/features_pt#-tag-rvrn>`_. See `5.0 rules element`_ § Attributes.
Evaluating rules
----------------
@@ -312,8 +313,8 @@ Evaluating rules
r1 = RuleDescriptor()
r1.name = "unique.rule.name"
- r1.conditionsSets.append([dict(name="weight", minimum=-10, maximum=10), dict(...)])
- r1.conditionsSets.append([dict(...), dict(...)])
+ r1.conditionSets.append([dict(name="weight", minimum=-10, maximum=10), dict(...)])
+ r1.conditionSets.append([dict(...), dict(...)])
r1.subs.append(("a", "a.alt"))
@@ -849,12 +850,14 @@ glyphname pairs: the glyphs that need to be substituted. For a rule to be trigge
**only one** of the conditionsets needs to be true, ``OR``. Within a conditionset
**all** conditions need to be true, ``AND``.
+.. attributes-11:
Attributes
----------
- ``processing``: flag, optional. Valid values are [``first``, ``last``]. This flag indicates whether the substitution rules should be applied before or after other glyph substitution features.
-- If no ``processing`` attribute is given, interpret as ``first``.
+- If no ``processing`` attribute is given, interpret as ``first``, and put the substitution rule in the `rvrn` feature.
+- If ``processing`` is ``last``, put it in `rclt`.
.. 51-rule-element:
diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py
index 88d19ee1..104792a2 100644
--- a/Lib/fontTools/__init__.py
+++ b/Lib/fontTools/__init__.py
@@ -4,6 +4,6 @@ from fontTools.misc.loggingTools import configLogger
log = logging.getLogger(__name__)
-version = __version__ = "4.13.0"
+version = __version__ = "4.14.0"
__all__ = ["version", "log", "configLogger"]
diff --git a/Lib/fontTools/feaLib/__main__.py b/Lib/fontTools/feaLib/__main__.py
index e7db157f..9c682fc1 100644
--- a/Lib/fontTools/feaLib/__main__.py
+++ b/Lib/fontTools/feaLib/__main__.py
@@ -15,23 +15,39 @@ log = logging.getLogger("fontTools.feaLib")
def main(args=None):
"""Add features from a feature file (.fea) into a OTF font"""
parser = argparse.ArgumentParser(
- description="Use fontTools to compile OpenType feature files (*.fea).")
+ description="Use fontTools to compile OpenType feature files (*.fea)."
+ )
parser.add_argument(
- "input_fea", metavar="FEATURES", help="Path to the feature file")
+ "input_fea", metavar="FEATURES", help="Path to the feature file"
+ )
parser.add_argument(
- "input_font", metavar="INPUT_FONT", help="Path to the input font")
+ "input_font", metavar="INPUT_FONT", help="Path to the input font"
+ )
parser.add_argument(
- "-o", "--output", dest="output_font", metavar="OUTPUT_FONT",
- help="Path to the output font.")
+ "-o",
+ "--output",
+ dest="output_font",
+ metavar="OUTPUT_FONT",
+ help="Path to the output font.",
+ )
parser.add_argument(
- "-t", "--tables", metavar="TABLE_TAG", choices=Builder.supportedTables,
- nargs='+', help="Specify the table(s) to be built.")
+ "-t",
+ "--tables",
+ metavar="TABLE_TAG",
+ choices=Builder.supportedTables,
+ nargs="+",
+ help="Specify the table(s) to be built.",
+ )
parser.add_argument(
- "-v", "--verbose", help="increase the logger verbosity. Multiple -v "
- "options are allowed.", action="count", default=0)
+ "-v",
+ "--verbose",
+ help="increase the logger verbosity. Multiple -v " "options are allowed.",
+ action="count",
+ default=0,
+ )
parser.add_argument(
- "--traceback", help="show traceback for exceptions.",
- action="store_true")
+ "--traceback", help="show traceback for exceptions.", action="store_true"
+ )
options = parser.parse_args(args)
levels = ["WARNING", "INFO", "DEBUG"]
@@ -50,5 +66,5 @@ def main(args=None):
font.save(output_font)
-if __name__ == '__main__':
+if __name__ == "__main__":
sys.exit(main())
diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py
index 15278db7..7ef9afd9 100644
--- a/Lib/fontTools/feaLib/ast.py
+++ b/Lib/fontTools/feaLib/ast.py
@@ -8,65 +8,65 @@ import itertools
SHIFT = " " * 4
__all__ = [
- 'Element',
- 'FeatureFile',
- 'Comment',
- 'GlyphName',
- 'GlyphClass',
- 'GlyphClassName',
- 'MarkClassName',
- 'AnonymousBlock',
- 'Block',
- 'FeatureBlock',
- 'NestedBlock',
- 'LookupBlock',
- 'GlyphClassDefinition',
- 'GlyphClassDefStatement',
- 'MarkClass',
- 'MarkClassDefinition',
- 'AlternateSubstStatement',
- 'Anchor',
- 'AnchorDefinition',
- 'AttachStatement',
- 'BaseAxis',
- 'CVParametersNameStatement',
- 'ChainContextPosStatement',
- 'ChainContextSubstStatement',
- 'CharacterStatement',
- 'CursivePosStatement',
- 'Expression',
- 'FeatureNameStatement',
- 'FeatureReferenceStatement',
- 'FontRevisionStatement',
- 'HheaField',
- 'IgnorePosStatement',
- 'IgnoreSubstStatement',
- 'IncludeStatement',
- 'LanguageStatement',
- 'LanguageSystemStatement',
- 'LigatureCaretByIndexStatement',
- 'LigatureCaretByPosStatement',
- 'LigatureSubstStatement',
- 'LookupFlagStatement',
- 'LookupReferenceStatement',
- 'MarkBasePosStatement',
- 'MarkLigPosStatement',
- 'MarkMarkPosStatement',
- 'MultipleSubstStatement',
- 'NameRecord',
- 'OS2Field',
- 'PairPosStatement',
- 'ReverseChainSingleSubstStatement',
- 'ScriptStatement',
- 'SinglePosStatement',
- 'SingleSubstStatement',
- 'SizeParameters',
- 'Statement',
- 'SubtableStatement',
- 'TableBlock',
- 'ValueRecord',
- 'ValueRecordDefinition',
- 'VheaField',
+ "Element",
+ "FeatureFile",
+ "Comment",
+ "GlyphName",
+ "GlyphClass",
+ "GlyphClassName",
+ "MarkClassName",
+ "AnonymousBlock",
+ "Block",
+ "FeatureBlock",
+ "NestedBlock",
+ "LookupBlock",
+ "GlyphClassDefinition",
+ "GlyphClassDefStatement",
+ "MarkClass",
+ "MarkClassDefinition",
+ "AlternateSubstStatement",
+ "Anchor",
+ "AnchorDefinition",
+ "AttachStatement",
+ "BaseAxis",
+ "CVParametersNameStatement",
+ "ChainContextPosStatement",
+ "ChainContextSubstStatement",
+ "CharacterStatement",
+ "CursivePosStatement",
+ "Expression",
+ "FeatureNameStatement",
+ "FeatureReferenceStatement",
+ "FontRevisionStatement",
+ "HheaField",
+ "IgnorePosStatement",
+ "IgnoreSubstStatement",
+ "IncludeStatement",
+ "LanguageStatement",
+ "LanguageSystemStatement",
+ "LigatureCaretByIndexStatement",
+ "LigatureCaretByPosStatement",
+ "LigatureSubstStatement",
+ "LookupFlagStatement",
+ "LookupReferenceStatement",
+ "MarkBasePosStatement",
+ "MarkLigPosStatement",
+ "MarkMarkPosStatement",
+ "MultipleSubstStatement",
+ "NameRecord",
+ "OS2Field",
+ "PairPosStatement",
+ "ReverseChainSingleSubstStatement",
+ "ScriptStatement",
+ "SinglePosStatement",
+ "SingleSubstStatement",
+ "SizeParameters",
+ "Statement",
+ "SubtableStatement",
+ "TableBlock",
+ "ValueRecord",
+ "ValueRecordDefinition",
+ "VheaField",
]
@@ -77,32 +77,69 @@ def deviceToString(device):
return "<device %s>" % ", ".join("%d %d" % t for t in device)
-fea_keywords = set([
- "anchor", "anchordef", "anon", "anonymous",
- "by",
- "contour", "cursive",
- "device",
- "enum", "enumerate", "excludedflt", "exclude_dflt",
- "feature", "from",
- "ignore", "ignorebaseglyphs", "ignoreligatures", "ignoremarks",
- "include", "includedflt", "include_dflt",
- "language", "languagesystem", "lookup", "lookupflag",
- "mark", "markattachmenttype", "markclass",
- "nameid", "null",
- "parameters", "pos", "position",
- "required", "righttoleft", "reversesub", "rsub",
- "script", "sub", "substitute", "subtable",
- "table",
- "usemarkfilteringset", "useextension", "valuerecorddef",
- "base", "gdef", "head", "hhea", "name", "vhea", "vmtx"]
+fea_keywords = set(
+ [
+ "anchor",
+ "anchordef",
+ "anon",
+ "anonymous",
+ "by",
+ "contour",
+ "cursive",
+ "device",
+ "enum",
+ "enumerate",
+ "excludedflt",
+ "exclude_dflt",
+ "feature",
+ "from",
+ "ignore",
+ "ignorebaseglyphs",
+ "ignoreligatures",
+ "ignoremarks",
+ "include",
+ "includedflt",
+ "include_dflt",
+ "language",
+ "languagesystem",
+ "lookup",
+ "lookupflag",
+ "mark",
+ "markattachmenttype",
+ "markclass",
+ "nameid",
+ "null",
+ "parameters",
+ "pos",
+ "position",
+ "required",
+ "righttoleft",
+ "reversesub",
+ "rsub",
+ "script",
+ "sub",
+ "substitute",
+ "subtable",
+ "table",
+ "usemarkfilteringset",
+ "useextension",
+ "valuerecorddef",
+ "base",
+ "gdef",
+ "head",
+ "hhea",
+ "name",
+ "vhea",
+ "vmtx",
+ ]
)
def asFea(g):
- if hasattr(g, 'asFea'):
+ if hasattr(g, "asFea"):
return g.asFea()
elif isinstance(g, tuple) and len(g) == 2:
- return asFea(g[0]) + " - " + asFea(g[1]) # a range
+ return asFea(g[0]) + " - " + asFea(g[1]) # a range
elif g.lower() in fea_keywords:
return "\\" + g
else:
@@ -141,6 +178,7 @@ class Expression(Element):
class Comment(Element):
"""A comment in a feature file."""
+
def __init__(self, text, location=None):
super(Comment, self).__init__(location)
#: Text of the comment
@@ -152,6 +190,7 @@ class Comment(Element):
class GlyphName(Expression):
"""A single glyph name, such as ``cedilla``."""
+
def __init__(self, glyph, location=None):
Expression.__init__(self, location)
#: The name itself as a string
@@ -167,6 +206,7 @@ class GlyphName(Expression):
class GlyphClass(Expression):
"""A glyph class, such as ``[acute cedilla grave]``."""
+
def __init__(self, glyphs=None, location=None):
Expression.__init__(self, location)
#: The list of glyphs in this class, as :class:`GlyphName` objects.
@@ -181,7 +221,7 @@ class GlyphClass(Expression):
def asFea(self, indent=""):
if len(self.original):
if self.curr < len(self.glyphs):
- self.original.extend(self.glyphs[self.curr:])
+ self.original.extend(self.glyphs[self.curr :])
self.curr = len(self.glyphs)
return "[" + " ".join(map(asFea, self.original)) + "]"
else:
@@ -201,7 +241,7 @@ class GlyphClass(Expression):
start and end glyphs in the class, and ``glyphs`` is the full list of
:class:`GlyphName` objects in the range."""
if self.curr < len(self.glyphs):
- self.original.extend(self.glyphs[self.curr:])
+ self.original.extend(self.glyphs[self.curr :])
self.original.append((start, end))
self.glyphs.extend(glyphs)
self.curr = len(self.glyphs)
@@ -211,7 +251,7 @@ class GlyphClass(Expression):
initial and final IDs, and ``glyphs`` is the full list of
:class:`GlyphName` objects in the range."""
if self.curr < len(self.glyphs):
- self.original.extend(self.glyphs[self.curr:])
+ self.original.extend(self.glyphs[self.curr :])
self.original.append(("\\{}".format(start), "\\{}".format(end)))
self.glyphs.extend(glyphs)
self.curr = len(self.glyphs)
@@ -220,7 +260,7 @@ class GlyphClass(Expression):
"""Add glyphs from the given :class:`GlyphClassName` object to the
class."""
if self.curr < len(self.glyphs):
- self.original.extend(self.glyphs[self.curr:])
+ self.original.extend(self.glyphs[self.curr :])
self.original.append(gc)
self.glyphs.extend(gc.glyphSet())
self.curr = len(self.glyphs)
@@ -229,6 +269,7 @@ class GlyphClass(Expression):
class GlyphClassName(Expression):
"""A glyph class name, such as ``@FRENCH_MARKS``. This must be instantiated
with a :class:`GlyphClassDefinition` object."""
+
def __init__(self, glyphclass, location=None):
Expression.__init__(self, location)
assert isinstance(glyphclass, GlyphClassDefinition)
@@ -245,6 +286,7 @@ class GlyphClassName(Expression):
class MarkClassName(Expression):
"""A mark class name, such as ``@FRENCH_MARKS`` defined with ``markClass``.
This must be instantiated with a :class:`MarkClass` object."""
+
def __init__(self, markClass, location=None):
Expression.__init__(self, location)
assert isinstance(markClass, MarkClass)
@@ -275,6 +317,7 @@ class AnonymousBlock(Statement):
class Block(Statement):
"""A block of statements: feature, lookup, etc."""
+
def __init__(self, location=None):
Statement.__init__(self, location)
self.statements = [] #: Statements contained in the block
@@ -288,13 +331,17 @@ class Block(Statement):
def asFea(self, indent=""):
indent += SHIFT
- return indent + ("\n" + indent).join(
- [s.asFea(indent=indent) for s in self.statements]) + "\n"
+ return (
+ indent
+ + ("\n" + indent).join([s.asFea(indent=indent) for s in self.statements])
+ + "\n"
+ )
class FeatureFile(Block):
"""The top-level element of the syntax tree, containing the whole feature
file in its ``statements`` attribute."""
+
def __init__(self):
Block.__init__(self, location=None)
self.markClasses = {} # name --> ast.MarkClass
@@ -305,6 +352,7 @@ class FeatureFile(Block):
class FeatureBlock(Block):
"""A named feature block."""
+
def __init__(self, name, use_extension=False, location=None):
Block.__init__(self, location)
self.name, self.use_extension = name, use_extension
@@ -337,6 +385,7 @@ class FeatureBlock(Block):
class NestedBlock(Block):
"""A block inside another block, for example when found inside a
``cvParameters`` block."""
+
def __init__(self, tag, block_name, location=None):
Block.__init__(self, location)
self.tag = tag
@@ -356,6 +405,7 @@ class NestedBlock(Block):
class LookupBlock(Block):
"""A named lookup, containing ``statements``."""
+
def __init__(self, name, use_extension=False, location=None):
Block.__init__(self, location)
self.name, self.use_extension = name, use_extension
@@ -378,6 +428,7 @@ class LookupBlock(Block):
class TableBlock(Block):
"""A ``table ... { }`` block."""
+
def __init__(self, name, location=None):
Block.__init__(self, location)
self.name = name
@@ -391,6 +442,7 @@ class TableBlock(Block):
class GlyphClassDefinition(Statement):
"""Example: ``@UPPERCASE = [A-Z];``."""
+
def __init__(self, name, glyphs, location=None):
Statement.__init__(self, location)
self.name = name #: class name as a string, without initial ``@``
@@ -408,8 +460,10 @@ class GlyphClassDefStatement(Statement):
"""Example: ``GlyphClassDef @UPPERCASE, [B], [C], [D];``. The parameters
must be either :class:`GlyphClass` or :class:`GlyphClassName` objects, or
``None``."""
- def __init__(self, baseGlyphs, markGlyphs, ligatureGlyphs,
- componentGlyphs, location=None):
+
+ def __init__(
+ self, baseGlyphs, markGlyphs, ligatureGlyphs, componentGlyphs, location=None
+ ):
Statement.__init__(self, location)
self.baseGlyphs, self.markGlyphs = (baseGlyphs, markGlyphs)
self.ligatureGlyphs = ligatureGlyphs
@@ -418,11 +472,9 @@ class GlyphClassDefStatement(Statement):
def build(self, builder):
"""Calls the builder's ``add_glyphClassDef`` callback."""
base = self.baseGlyphs.glyphSet() if self.baseGlyphs else tuple()
- liga = self.ligatureGlyphs.glyphSet() \
- if self.ligatureGlyphs else tuple()
+ liga = self.ligatureGlyphs.glyphSet() if self.ligatureGlyphs else tuple()
mark = self.markGlyphs.glyphSet() if self.markGlyphs else tuple()
- comp = (self.componentGlyphs.glyphSet()
- if self.componentGlyphs else tuple())
+ comp = self.componentGlyphs.glyphSet() if self.componentGlyphs else tuple()
builder.add_glyphClassDef(self.location, base, liga, mark, comp)
def asFea(self, indent=""):
@@ -430,7 +482,8 @@ class GlyphClassDefStatement(Statement):
self.baseGlyphs.asFea() if self.baseGlyphs else "",
self.ligatureGlyphs.asFea() if self.ligatureGlyphs else "",
self.markGlyphs.asFea() if self.markGlyphs else "",
- self.componentGlyphs.asFea() if self.componentGlyphs else "")
+ self.componentGlyphs.asFea() if self.componentGlyphs else "",
+ )
class MarkClass(object):
@@ -465,8 +518,8 @@ class MarkClass(object):
else:
end = f" at {otherLoc}"
raise FeatureLibError(
- "Glyph %s already defined%s" % (glyph, end),
- definition.location)
+ "Glyph %s already defined%s" % (glyph, end), definition.location
+ )
self.glyphs[glyph] = definition
def glyphSet(self):
@@ -500,6 +553,7 @@ class MarkClassDefinition(Statement):
# markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS;
"""
+
def __init__(self, markClass, anchor, glyphs, location=None):
Statement.__init__(self, location)
assert isinstance(markClass, MarkClass)
@@ -512,8 +566,8 @@ class MarkClassDefinition(Statement):
def asFea(self, indent=""):
return "markClass {} {} @{};".format(
- self.glyphs.asFea(), self.anchor.asFea(),
- self.markClass.name)
+ self.glyphs.asFea(), self.anchor.asFea(), self.markClass.name
+ )
class AlternateSubstStatement(Statement):
@@ -535,15 +589,14 @@ class AlternateSubstStatement(Statement):
prefix = [p.glyphSet() for p in self.prefix]
suffix = [s.glyphSet() for s in self.suffix]
replacement = self.replacement.glyphSet()
- builder.add_alternate_subst(self.location, prefix, glyph, suffix,
- replacement)
+ builder.add_alternate_subst(self.location, prefix, glyph, suffix, replacement)
def asFea(self, indent=""):
res = "sub "
if len(self.prefix) or len(self.suffix):
if len(self.prefix):
res += " ".join(map(asFea, self.prefix)) + " "
- res += asFea(self.glyph) + "'" # even though we really only use 1
+ res += asFea(self.glyph) + "'" # even though we really only use 1
if len(self.suffix):
res += " " + " ".join(map(asFea, self.suffix))
else:
@@ -560,8 +613,17 @@ class Anchor(Expression):
If a ``name`` is given, this will be used in preference to the coordinates.
Other values should be integer.
"""
- def __init__(self, x, y, name=None, contourpoint=None,
- xDeviceTable=None, yDeviceTable=None, location=None):
+
+ def __init__(
+ self,
+ x,
+ y,
+ name=None,
+ contourpoint=None,
+ xDeviceTable=None,
+ yDeviceTable=None,
+ location=None,
+ ):
Expression.__init__(self, location)
self.name = name
self.x, self.y, self.contourpoint = x, y, contourpoint
@@ -584,6 +646,7 @@ class Anchor(Expression):
class AnchorDefinition(Statement):
"""A named anchor definition. (2.e.viii). ``name`` should be a string."""
+
def __init__(self, name, x, y, contourpoint=None, location=None):
Statement.__init__(self, location)
self.name, self.x, self.y, self.contourpoint = name, x, y, contourpoint
@@ -598,6 +661,7 @@ class AnchorDefinition(Statement):
class AttachStatement(Statement):
"""A ``GDEF`` table ``Attach`` statement."""
+
def __init__(self, glyphs, contourPoints, location=None):
Statement.__init__(self, location)
self.glyphs = glyphs #: A `glyph-containing object`_
@@ -610,7 +674,8 @@ class AttachStatement(Statement):
def asFea(self, indent=""):
return "Attach {} {};".format(
- self.glyphs.asFea(), " ".join(str(c) for c in self.contourPoints))
+ self.glyphs.asFea(), " ".join(str(c) for c in self.contourPoints)
+ )
class ChainContextPosStatement(Statement):
@@ -644,11 +709,16 @@ class ChainContextPosStatement(Statement):
glyphs = [g.glyphSet() for g in self.glyphs]
suffix = [s.glyphSet() for s in self.suffix]
builder.add_chain_context_pos(
- self.location, prefix, glyphs, suffix, self.lookups)
+ self.location, prefix, glyphs, suffix, self.lookups
+ )
def asFea(self, indent=""):
res = "pos "
- if len(self.prefix) or len(self.suffix) or any([x is not None for x in self.lookups]):
+ if (
+ len(self.prefix)
+ or len(self.suffix)
+ or any([x is not None for x in self.lookups])
+ ):
if len(self.prefix):
res += " ".join(g.asFea() for g in self.prefix) + " "
for i, g in enumerate(self.glyphs):
@@ -697,11 +767,16 @@ class ChainContextSubstStatement(Statement):
glyphs = [g.glyphSet() for g in self.glyphs]
suffix = [s.glyphSet() for s in self.suffix]
builder.add_chain_context_subst(
- self.location, prefix, glyphs, suffix, self.lookups)
+ self.location, prefix, glyphs, suffix, self.lookups
+ )
def asFea(self, indent=""):
res = "sub "
- if len(self.prefix) or len(self.suffix) or any([x is not None for x in self.lookups]):
+ if (
+ len(self.prefix)
+ or len(self.suffix)
+ or any([x is not None for x in self.lookups])
+ ):
if len(self.prefix):
res += " ".join(g.asFea() for g in self.prefix) + " "
for i, g in enumerate(self.glyphs):
@@ -722,6 +797,7 @@ class ChainContextSubstStatement(Statement):
class CursivePosStatement(Statement):
"""A cursive positioning statement. Entry and exit anchors can either
be :class:`Anchor` objects or ``None``."""
+
def __init__(self, glyphclass, entryAnchor, exitAnchor, location=None):
Statement.__init__(self, location)
self.glyphclass = glyphclass
@@ -730,7 +806,8 @@ class CursivePosStatement(Statement):
def build(self, builder):
"""Calls the builder object's ``add_cursive_pos`` callback."""
builder.add_cursive_pos(
- self.location, self.glyphclass.glyphSet(), self.entryAnchor, self.exitAnchor)
+ self.location, self.glyphclass.glyphSet(), self.entryAnchor, self.exitAnchor
+ )
def asFea(self, indent=""):
entry = self.entryAnchor.asFea() if self.entryAnchor else "<anchor NULL>"
@@ -740,6 +817,7 @@ class CursivePosStatement(Statement):
class FeatureReferenceStatement(Statement):
"""Example: ``feature salt;``"""
+
def __init__(self, featureName, location=None):
Statement.__init__(self, location)
self.location, self.featureName = (location, featureName)
@@ -770,8 +848,7 @@ class IgnorePosStatement(Statement):
prefix = [p.glyphSet() for p in prefix]
glyphs = [g.glyphSet() for g in glyphs]
suffix = [s.glyphSet() for s in suffix]
- builder.add_chain_context_pos(
- self.location, prefix, glyphs, suffix, [])
+ builder.add_chain_context_pos(self.location, prefix, glyphs, suffix, [])
def asFea(self, indent=""):
contexts = []
@@ -795,6 +872,7 @@ class IgnoreSubstStatement(Statement):
``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples,
with each of ``prefix``, ``glyphs`` and ``suffix`` being
`glyph-containing objects`_ ."""
+
def __init__(self, chainContexts, location=None):
Statement.__init__(self, location)
self.chainContexts = chainContexts
@@ -806,8 +884,7 @@ class IgnoreSubstStatement(Statement):
prefix = [p.glyphSet() for p in prefix]
glyphs = [g.glyphSet() for g in glyphs]
suffix = [s.glyphSet() for s in suffix]
- builder.add_chain_context_subst(
- self.location, prefix, glyphs, suffix, [])
+ builder.add_chain_context_subst(self.location, prefix, glyphs, suffix, [])
def asFea(self, indent=""):
contexts = []
@@ -827,6 +904,7 @@ class IgnoreSubstStatement(Statement):
class IncludeStatement(Statement):
"""An ``include()`` statement."""
+
def __init__(self, filename, location=None):
super(IncludeStatement, self).__init__(location)
self.filename = filename #: String containing name of file to include
@@ -836,7 +914,8 @@ class IncludeStatement(Statement):
raise FeatureLibError(
"Building an include statement is not implemented yet. "
"Instead, use Parser(..., followIncludes=True) for building.",
- self.location)
+ self.location,
+ )
def asFea(self, indent=""):
return indent + "include(%s);" % self.filename
@@ -844,19 +923,22 @@ class IncludeStatement(Statement):
class LanguageStatement(Statement):
"""A ``language`` statement within a feature."""
- def __init__(self, language, include_default=True, required=False,
- location=None):
+
+ def __init__(self, language, include_default=True, required=False, location=None):
Statement.__init__(self, location)
- assert(len(language) == 4)
+ assert len(language) == 4
self.language = language #: A four-character language tag
self.include_default = include_default #: If false, "exclude_dflt"
self.required = required
def build(self, builder):
"""Call the builder object's ``set_language`` callback."""
- builder.set_language(location=self.location, language=self.language,
- include_default=self.include_default,
- required=self.required)
+ builder.set_language(
+ location=self.location,
+ language=self.language,
+ include_default=self.include_default,
+ required=self.required,
+ )
def asFea(self, indent=""):
res = "language {}".format(self.language.strip())
@@ -870,6 +952,7 @@ class LanguageStatement(Statement):
class LanguageSystemStatement(Statement):
"""A top-level ``languagesystem`` statement."""
+
def __init__(self, script, language, location=None):
Statement.__init__(self, location)
self.script, self.language = (script, language)
@@ -885,6 +968,7 @@ class LanguageSystemStatement(Statement):
class FontRevisionStatement(Statement):
"""A ``head`` table ``FontRevision`` statement. ``revision`` should be a
number, and will be formatted to three significant decimal places."""
+
def __init__(self, revision, location=None):
Statement.__init__(self, location)
self.revision = revision
@@ -899,6 +983,7 @@ class FontRevisionStatement(Statement):
class LigatureCaretByIndexStatement(Statement):
"""A ``GDEF`` table ``LigatureCaretByIndex`` statement. ``glyphs`` should be
a `glyph-containing object`_, and ``carets`` should be a list of integers."""
+
def __init__(self, glyphs, carets, location=None):
Statement.__init__(self, location)
self.glyphs, self.carets = (glyphs, carets)
@@ -910,12 +995,14 @@ class LigatureCaretByIndexStatement(Statement):
def asFea(self, indent=""):
return "LigatureCaretByIndex {} {};".format(
- self.glyphs.asFea(), " ".join(str(x) for x in self.carets))
+ self.glyphs.asFea(), " ".join(str(x) for x in self.carets)
+ )
class LigatureCaretByPosStatement(Statement):
"""A ``GDEF`` table ``LigatureCaretByPos`` statement. ``glyphs`` should be
a `glyph-containing object`_, and ``carets`` should be a list of integers."""
+
def __init__(self, glyphs, carets, location=None):
Statement.__init__(self, location)
self.glyphs, self.carets = (glyphs, carets)
@@ -927,7 +1014,8 @@ class LigatureCaretByPosStatement(Statement):
def asFea(self, indent=""):
return "LigatureCaretByPos {} {};".format(
- self.glyphs.asFea(), " ".join(str(x) for x in self.carets))
+ self.glyphs.asFea(), " ".join(str(x) for x in self.carets)
+ )
class LigatureSubstStatement(Statement):
@@ -939,8 +1027,8 @@ class LigatureSubstStatement(Statement):
If ``forceChain`` is True, this is expressed as a chaining rule
(e.g. ``sub f' i' by f_i``) even when no context is given."""
- def __init__(self, prefix, glyphs, suffix, replacement,
- forceChain, location=None):
+
+ def __init__(self, prefix, glyphs, suffix, replacement, forceChain, location=None):
Statement.__init__(self, location)
self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix)
self.replacement, self.forceChain = replacement, forceChain
@@ -950,8 +1038,8 @@ class LigatureSubstStatement(Statement):
glyphs = [g.glyphSet() for g in self.glyphs]
suffix = [s.glyphSet() for s in self.suffix]
builder.add_ligature_subst(
- self.location, prefix, glyphs, suffix, self.replacement,
- self.forceChain)
+ self.location, prefix, glyphs, suffix, self.replacement, self.forceChain
+ )
def asFea(self, indent=""):
res = "sub "
@@ -974,8 +1062,10 @@ class LookupFlagStatement(Statement):
representing the flags in use, but not including the ``markAttachment``
class and ``markFilteringSet`` values, which must be specified as
glyph-containing objects."""
- def __init__(self, value=0, markAttachment=None, markFilteringSet=None,
- location=None):
+
+ def __init__(
+ self, value=0, markAttachment=None, markFilteringSet=None, location=None
+ ):
Statement.__init__(self, location)
self.value = value
self.markAttachment = markAttachment
@@ -989,8 +1079,7 @@ class LookupFlagStatement(Statement):
markFilter = None
if self.markFilteringSet is not None:
markFilter = self.markFilteringSet.glyphSet()
- builder.set_lookup_flag(self.location, self.value,
- markAttach, markFilter)
+ builder.set_lookup_flag(self.location, self.value, markAttach, markFilter)
def asFea(self, indent=""):
res = []
@@ -1013,6 +1102,7 @@ class LookupReferenceStatement(Statement):
"""Represents a ``lookup ...;`` statement to include a lookup in a feature.
The ``lookup`` should be a :class:`LookupBlock` object."""
+
def __init__(self, lookup, location=None):
Statement.__init__(self, location)
self.location, self.lookup = (location, lookup)
@@ -1029,6 +1119,7 @@ class MarkBasePosStatement(Statement):
"""A mark-to-base positioning rule. The ``base`` should be a
`glyph-containing object`_. The ``marks`` should be a list of
(:class:`Anchor`, :class:`MarkClass`) tuples."""
+
def __init__(self, base, marks, location=None):
Statement.__init__(self, location)
self.base, self.marks = base, marks
@@ -1100,6 +1191,7 @@ class MarkMarkPosStatement(Statement):
"""A mark-to-mark positioning rule. The ``baseMarks`` must be a
`glyph-containing object`_. The ``marks`` should be a list of
(:class:`Anchor`, :class:`MarkClass`) tuples."""
+
def __init__(self, baseMarks, marks, location=None):
Statement.__init__(self, location)
self.baseMarks, self.marks = baseMarks, marks
@@ -1127,6 +1219,7 @@ class MultipleSubstStatement(Statement):
forceChain: If true, the statement is expressed as a chaining rule
(e.g. ``sub f' i' by f_i``) even when no context is given.
"""
+
def __init__(
self, prefix, glyph, suffix, replacement, forceChain=False, location=None
):
@@ -1140,8 +1233,8 @@ class MultipleSubstStatement(Statement):
prefix = [p.glyphSet() for p in self.prefix]
suffix = [s.glyphSet() for s in self.suffix]
builder.add_multiple_subst(
- self.location, prefix, self.glyph, suffix, self.replacement,
- self.forceChain)
+ self.location, prefix, self.glyph, suffix, self.replacement, self.forceChain
+ )
def asFea(self, indent=""):
res = "sub "
@@ -1168,8 +1261,16 @@ class PairPosStatement(Statement):
If ``enumerated`` is true, then this is expressed as an
`enumerated pair <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_.
"""
- def __init__(self, glyphs1, valuerecord1, glyphs2, valuerecord2,
- enumerated=False, location=None):
+
+ def __init__(
+ self,
+ glyphs1,
+ valuerecord1,
+ glyphs2,
+ valuerecord2,
+ enumerated=False,
+ location=None,
+ ):
Statement.__init__(self, location)
self.enumerated = enumerated
self.glyphs1, self.valuerecord1 = glyphs1, valuerecord1
@@ -1188,31 +1289,43 @@ class PairPosStatement(Statement):
g = [self.glyphs1.glyphSet(), self.glyphs2.glyphSet()]
for glyph1, glyph2 in itertools.product(*g):
builder.add_specific_pair_pos(
- self.location, glyph1, self.valuerecord1,
- glyph2, self.valuerecord2)
+ self.location, glyph1, self.valuerecord1, glyph2, self.valuerecord2
+ )
return
- is_specific = (isinstance(self.glyphs1, GlyphName) and
- isinstance(self.glyphs2, GlyphName))
+ is_specific = isinstance(self.glyphs1, GlyphName) and isinstance(
+ self.glyphs2, GlyphName
+ )
if is_specific:
builder.add_specific_pair_pos(
- self.location, self.glyphs1.glyph, self.valuerecord1,
- self.glyphs2.glyph, self.valuerecord2)
+ self.location,
+ self.glyphs1.glyph,
+ self.valuerecord1,
+ self.glyphs2.glyph,
+ self.valuerecord2,
+ )
else:
builder.add_class_pair_pos(
- self.location, self.glyphs1.glyphSet(), self.valuerecord1,
- self.glyphs2.glyphSet(), self.valuerecord2)
+ self.location,
+ self.glyphs1.glyphSet(),
+ self.valuerecord1,
+ self.glyphs2.glyphSet(),
+ self.valuerecord2,
+ )
def asFea(self, indent=""):
res = "enum " if self.enumerated else ""
if self.valuerecord2:
res += "pos {} {} {} {};".format(
- self.glyphs1.asFea(), self.valuerecord1.asFea(),
- self.glyphs2.asFea(), self.valuerecord2.asFea())
+ self.glyphs1.asFea(),
+ self.valuerecord1.asFea(),
+ self.glyphs2.asFea(),
+ self.valuerecord2.asFea(),
+ )
else:
res += "pos {} {} {};".format(
- self.glyphs1.asFea(), self.glyphs2.asFea(),
- self.valuerecord1.asFea())
+ self.glyphs1.asFea(), self.glyphs2.asFea(), self.valuerecord1.asFea()
+ )
return res
@@ -1224,8 +1337,8 @@ class ReverseChainSingleSubstStatement(Statement):
lists of `glyph-containing objects`_. ``glyphs`` and ``replacements`` should
be one-item lists.
"""
- def __init__(self, old_prefix, old_suffix, glyphs, replacements,
- location=None):
+
+ def __init__(self, old_prefix, old_suffix, glyphs, replacements, location=None):
Statement.__init__(self, location)
self.old_prefix, self.old_suffix = old_prefix, old_suffix
self.glyphs = glyphs
@@ -1239,7 +1352,8 @@ class ReverseChainSingleSubstStatement(Statement):
if len(replaces) == 1:
replaces = replaces * len(originals)
builder.add_reverse_chain_single_subst(
- self.location, prefix, suffix, dict(zip(originals, replaces)))
+ self.location, prefix, suffix, dict(zip(originals, replaces))
+ )
def asFea(self, indent=""):
res = "rsub "
@@ -1264,8 +1378,7 @@ class SingleSubstStatement(Statement):
``replace`` should be one-item lists.
"""
- def __init__(self, glyphs, replace, prefix, suffix, forceChain,
- location=None):
+ def __init__(self, glyphs, replace, prefix, suffix, forceChain, location=None):
Statement.__init__(self, location)
self.prefix, self.suffix = prefix, suffix
self.forceChain = forceChain
@@ -1280,9 +1393,13 @@ class SingleSubstStatement(Statement):
replaces = self.replacements[0].glyphSet()
if len(replaces) == 1:
replaces = replaces * len(originals)
- builder.add_single_subst(self.location, prefix, suffix,
- OrderedDict(zip(originals, replaces)),
- self.forceChain)
+ builder.add_single_subst(
+ self.location,
+ prefix,
+ suffix,
+ OrderedDict(zip(originals, replaces)),
+ self.forceChain,
+ )
def asFea(self, indent=""):
res = "sub "
@@ -1300,6 +1417,7 @@ class SingleSubstStatement(Statement):
class ScriptStatement(Statement):
"""A ``script`` statement."""
+
def __init__(self, script, location=None):
Statement.__init__(self, location)
self.script = script #: the script code
@@ -1329,27 +1447,32 @@ class SinglePosStatement(Statement):
prefix = [p.glyphSet() for p in self.prefix]
suffix = [s.glyphSet() for s in self.suffix]
pos = [(g.glyphSet(), value) for g, value in self.pos]
- builder.add_single_pos(self.location, prefix, suffix,
- pos, self.forceChain)
+ builder.add_single_pos(self.location, prefix, suffix, pos, self.forceChain)
def asFea(self, indent=""):
res = "pos "
if len(self.prefix) or len(self.suffix) or self.forceChain:
if len(self.prefix):
res += " ".join(map(asFea, self.prefix)) + " "
- res += " ".join([asFea(x[0]) + "'" + (
- (" " + x[1].asFea()) if x[1] else "") for x in self.pos])
+ res += " ".join(
+ [
+ asFea(x[0]) + "'" + ((" " + x[1].asFea()) if x[1] else "")
+ for x in self.pos
+ ]
+ )
if len(self.suffix):
res += " " + " ".join(map(asFea, self.suffix))
else:
- res += " ".join([asFea(x[0]) + " " +
- (x[1].asFea() if x[1] else "") for x in self.pos])
+ res += " ".join(
+ [asFea(x[0]) + " " + (x[1].asFea() if x[1] else "") for x in self.pos]
+ )
res += ";"
return res
class SubtableStatement(Statement):
"""Represents a subtable break."""
+
def __init__(self, location=None):
Statement.__init__(self, location)
@@ -1363,11 +1486,20 @@ class SubtableStatement(Statement):
class ValueRecord(Expression):
"""Represents a value record."""
- def __init__(self, xPlacement=None, yPlacement=None,
- xAdvance=None, yAdvance=None,
- xPlaDevice=None, yPlaDevice=None,
- xAdvDevice=None, yAdvDevice=None,
- vertical=False, location=None):
+
+ def __init__(
+ self,
+ xPlacement=None,
+ yPlacement=None,
+ xAdvance=None,
+ yAdvance=None,
+ xPlaDevice=None,
+ yPlaDevice=None,
+ xAdvDevice=None,
+ yAdvDevice=None,
+ vertical=False,
+ location=None,
+ ):
Expression.__init__(self, location)
self.xPlacement, self.yPlacement = (xPlacement, yPlacement)
self.xAdvance, self.yAdvance = (xAdvance, yAdvance)
@@ -1376,21 +1508,29 @@ class ValueRecord(Expression):
self.vertical = vertical
def __eq__(self, other):
- return (self.xPlacement == other.xPlacement and
- self.yPlacement == other.yPlacement and
- self.xAdvance == other.xAdvance and
- self.yAdvance == other.yAdvance and
- self.xPlaDevice == other.xPlaDevice and
- self.xAdvDevice == other.xAdvDevice)
+ return (
+ self.xPlacement == other.xPlacement
+ and self.yPlacement == other.yPlacement
+ and self.xAdvance == other.xAdvance
+ and self.yAdvance == other.yAdvance
+ and self.xPlaDevice == other.xPlaDevice
+ and self.xAdvDevice == other.xAdvDevice
+ )
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
- return (hash(self.xPlacement) ^ hash(self.yPlacement) ^
- hash(self.xAdvance) ^ hash(self.yAdvance) ^
- hash(self.xPlaDevice) ^ hash(self.yPlaDevice) ^
- hash(self.xAdvDevice) ^ hash(self.yAdvDevice))
+ return (
+ hash(self.xPlacement)
+ ^ hash(self.yPlacement)
+ ^ hash(self.xAdvance)
+ ^ hash(self.yAdvance)
+ ^ hash(self.xPlaDevice)
+ ^ hash(self.yPlaDevice)
+ ^ hash(self.xAdvDevice)
+ ^ hash(self.yAdvDevice)
+ )
def asFea(self, indent=""):
if not self:
@@ -1416,15 +1556,25 @@ class ValueRecord(Expression):
yAdvance = yAdvance or 0
# Try format B, if possible.
- if (xPlaDevice is None and yPlaDevice is None and
- xAdvDevice is None and yAdvDevice is None):
+ if (
+ xPlaDevice is None
+ and yPlaDevice is None
+ and xAdvDevice is None
+ and yAdvDevice is None
+ ):
return "<%s %s %s %s>" % (x, y, xAdvance, yAdvance)
# Last resort is format C.
return "<%s %s %s %s %s %s %s %s>" % (
- x, y, xAdvance, yAdvance,
- deviceToString(xPlaDevice), deviceToString(yPlaDevice),
- deviceToString(xAdvDevice), deviceToString(yAdvDevice))
+ x,
+ y,
+ xAdvance,
+ yAdvance,
+ deviceToString(xPlaDevice),
+ deviceToString(yPlaDevice),
+ deviceToString(xAdvDevice),
+ deviceToString(yAdvDevice),
+ )
def __bool__(self):
return any(
@@ -1446,6 +1596,7 @@ class ValueRecord(Expression):
class ValueRecordDefinition(Statement):
"""Represents a named value record definition."""
+
def __init__(self, name, value, location=None):
Statement.__init__(self, location)
self.name = name #: Value record name as string
@@ -1466,8 +1617,8 @@ def simplify_name_attributes(pid, eid, lid):
class NameRecord(Statement):
"""Represents a name record. (`Section 9.e. <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_)"""
- def __init__(self, nameID, platformID, platEncID, langID, string,
- location=None):
+
+ def __init__(self, nameID, platformID, platEncID, langID, string, location=None):
Statement.__init__(self, location)
self.nameID = nameID #: Name ID as integer (e.g. 9 for designer's name)
self.platformID = platformID #: Platform ID as integer
@@ -1478,8 +1629,13 @@ class NameRecord(Statement):
def build(self, builder):
"""Calls the builder object's ``add_name_record`` callback."""
builder.add_name_record(
- self.location, self.nameID, self.platformID,
- self.platEncID, self.langID, self.string)
+ self.location,
+ self.nameID,
+ self.platformID,
+ self.platEncID,
+ self.langID,
+ self.string,
+ )
def asFea(self, indent=""):
def escape(c, escape_pattern):
@@ -1488,21 +1644,24 @@ class NameRecord(Statement):
return unichr(c)
else:
return escape_pattern % c
+
encoding = getEncoding(self.platformID, self.platEncID, self.langID)
if encoding is None:
raise FeatureLibError("Unsupported encoding", self.location)
s = tobytes(self.string, encoding=encoding)
if encoding == "utf_16_be":
- escaped_string = "".join([
- escape(byteord(s[i]) * 256 + byteord(s[i + 1]), r"\%04x")
- for i in range(0, len(s), 2)])
+ escaped_string = "".join(
+ [
+ escape(byteord(s[i]) * 256 + byteord(s[i + 1]), r"\%04x")
+ for i in range(0, len(s), 2)
+ ]
+ )
else:
escaped_string = "".join([escape(byteord(b), r"\%02x") for b in s])
- plat = simplify_name_attributes(
- self.platformID, self.platEncID, self.langID)
+ plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
if plat != "":
plat += " "
- return "nameid {} {}\"{}\";".format(self.nameID, plat, escaped_string)
+ return 'nameid {} {}"{}";'.format(self.nameID, plat, escaped_string)
class FeatureNameStatement(NameRecord):
@@ -1521,13 +1680,13 @@ class FeatureNameStatement(NameRecord):
plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
if plat != "":
plat += " "
- return "{} {}\"{}\";".format(tag, plat, self.string)
+ return '{} {}"{}";'.format(tag, plat, self.string)
class SizeParameters(Statement):
"""A ``parameters`` statement."""
- def __init__(self, DesignSize, SubfamilyID, RangeStart, RangeEnd,
- location=None):
+
+ def __init__(self, DesignSize, SubfamilyID, RangeStart, RangeEnd, location=None):
Statement.__init__(self, location)
self.DesignSize = DesignSize
self.SubfamilyID = SubfamilyID
@@ -1536,8 +1695,13 @@ class SizeParameters(Statement):
def build(self, builder):
"""Calls the builder object's ``set_size_parameters`` callback."""
- builder.set_size_parameters(self.location, self.DesignSize,
- self.SubfamilyID, self.RangeStart, self.RangeEnd)
+ builder.set_size_parameters(
+ self.location,
+ self.DesignSize,
+ self.SubfamilyID,
+ self.RangeStart,
+ self.RangeEnd,
+ )
def asFea(self, indent=""):
res = "parameters {:.1f} {}".format(self.DesignSize, self.SubfamilyID)
@@ -1548,10 +1712,13 @@ class SizeParameters(Statement):
class CVParametersNameStatement(NameRecord):
"""Represent a name statement inside a ``cvParameters`` block."""
- def __init__(self, nameID, platformID, platEncID, langID, string,
- block_name, location=None):
- NameRecord.__init__(self, nameID, platformID, platEncID, langID,
- string, location=location)
+
+ def __init__(
+ self, nameID, platformID, platEncID, langID, string, block_name, location=None
+ ):
+ NameRecord.__init__(
+ self, nameID, platformID, platEncID, langID, string, location=location
+ )
self.block_name = block_name
def build(self, builder):
@@ -1564,11 +1731,10 @@ class CVParametersNameStatement(NameRecord):
NameRecord.build(self, builder)
def asFea(self, indent=""):
- plat = simplify_name_attributes(self.platformID, self.platEncID,
- self.langID)
+ plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
if plat != "":
plat += " "
- return "name {}\"{}\";".format(plat, self.string)
+ return 'name {}"{}";'.format(plat, self.string)
class CharacterStatement(Statement):
@@ -1578,6 +1744,7 @@ class CharacterStatement(Statement):
notation. The value must be preceded by '0x' if it is a hexadecimal value.
The largest Unicode value allowed is 0xFFFFFF.
"""
+
def __init__(self, character, tag, location=None):
Statement.__init__(self, location)
self.character = character
@@ -1594,9 +1761,10 @@ class CharacterStatement(Statement):
class BaseAxis(Statement):
"""An axis definition, being either a ``VertAxis.BaseTagList/BaseScriptList``
pair or a ``HorizAxis.BaseTagList/BaseScriptList`` pair."""
+
def __init__(self, bases, scripts, vertical, location=None):
Statement.__init__(self, location)
- self.bases = bases #: A list of baseline tag names as strings
+ self.bases = bases #: A list of baseline tag names as strings
self.scripts = scripts #: A list of script record tuplets (script tag, default baseline tag, base coordinate)
self.vertical = vertical #: Boolean; VertAxis if True, HorizAxis if False
@@ -1606,15 +1774,20 @@ class BaseAxis(Statement):
def asFea(self, indent=""):
direction = "Vert" if self.vertical else "Horiz"
- scripts = ["{} {} {}".format(a[0], a[1], " ".join(map(str, a[2]))) for a in self.scripts]
+ scripts = [
+ "{} {} {}".format(a[0], a[1], " ".join(map(str, a[2])))
+ for a in self.scripts
+ ]
return "{}Axis.BaseTagList {};\n{}{}Axis.BaseScriptList {};".format(
- direction, " ".join(self.bases), indent, direction, ", ".join(scripts))
+ direction, " ".join(self.bases), indent, direction, ", ".join(scripts)
+ )
class OS2Field(Statement):
"""An entry in the ``OS/2`` table. Most ``values`` should be numbers or
strings, apart from when the key is ``UnicodeRange``, ``CodePageRange``
or ``Panose``, in which case it should be an array of integers."""
+
def __init__(self, key, value, location=None):
Statement.__init__(self, location)
self.key = key
@@ -1627,21 +1800,36 @@ class OS2Field(Statement):
def asFea(self, indent=""):
def intarr2str(x):
return " ".join(map(str, x))
- numbers = ("FSType", "TypoAscender", "TypoDescender", "TypoLineGap",
- "winAscent", "winDescent", "XHeight", "CapHeight",
- "WeightClass", "WidthClass", "LowerOpSize", "UpperOpSize")
+
+ numbers = (
+ "FSType",
+ "TypoAscender",
+ "TypoDescender",
+ "TypoLineGap",
+ "winAscent",
+ "winDescent",
+ "XHeight",
+ "CapHeight",
+ "WeightClass",
+ "WidthClass",
+ "LowerOpSize",
+ "UpperOpSize",
+ )
ranges = ("UnicodeRange", "CodePageRange")
keywords = dict([(x.lower(), [x, str]) for x in numbers])
keywords.update([(x.lower(), [x, intarr2str]) for x in ranges])
keywords["panose"] = ["Panose", intarr2str]
keywords["vendor"] = ["Vendor", lambda y: '"{}"'.format(y)]
if self.key in keywords:
- return "{} {};".format(keywords[self.key][0], keywords[self.key][1](self.value))
- return "" # should raise exception
+ return "{} {};".format(
+ keywords[self.key][0], keywords[self.key][1](self.value)
+ )
+ return "" # should raise exception
class HheaField(Statement):
"""An entry in the ``hhea`` table."""
+
def __init__(self, key, value, location=None):
Statement.__init__(self, location)
self.key = key
@@ -1659,6 +1847,7 @@ class HheaField(Statement):
class VheaField(Statement):
"""An entry in the ``vhea`` table."""
+
def __init__(self, key, value, location=None):
Statement.__init__(self, location)
self.key = key
diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py
index 3b8cfd85..00c6d85b 100644
--- a/Lib/fontTools/feaLib/builder.py
+++ b/Lib/fontTools/feaLib/builder.py
@@ -23,6 +23,7 @@ from fontTools.otlLib.builder import (
ClassPairPosSubtableBuilder,
PairPosBuilder,
SinglePosBuilder,
+ ChainContextualRule,
)
from fontTools.otlLib.error import OpenTypeLibError
from collections import defaultdict
@@ -72,17 +73,20 @@ def addOpenTypeFeaturesFromString(font, features, filename=None, tables=None):
class Builder(object):
- supportedTables = frozenset(Tag(tag) for tag in [
- "BASE",
- "GDEF",
- "GPOS",
- "GSUB",
- "OS/2",
- "head",
- "hhea",
- "name",
- "vhea",
- ])
+ supportedTables = frozenset(
+ Tag(tag)
+ for tag in [
+ "BASE",
+ "GDEF",
+ "GPOS",
+ "GSUB",
+ "OS/2",
+ "head",
+ "hhea",
+ "name",
+ "vhea",
+ ]
+ )
def __init__(self, font, featurefile):
self.font = font
@@ -170,19 +174,20 @@ class Builder(object):
self.build_name()
if "OS/2" in tables:
self.build_OS_2()
- for tag in ('GPOS', 'GSUB'):
+ for tag in ("GPOS", "GSUB"):
if tag not in tables:
continue
table = self.makeTable(tag)
- if (table.ScriptList.ScriptCount > 0 or
- table.FeatureList.FeatureCount > 0 or
- table.LookupList.LookupCount > 0):
+ if (
+ table.ScriptList.ScriptCount > 0
+ or table.FeatureList.FeatureCount > 0
+ or table.LookupList.LookupCount > 0
+ ):
fontTable = self.font[tag] = newTable(tag)
fontTable.table = table
elif tag in self.font:
del self.font[tag]
- if (any(tag in self.font for tag in ("GPOS", "GSUB")) and
- "OS/2" in self.font):
+ if any(tag in self.font for tag in ("GPOS", "GSUB")) and "OS/2" in self.font:
self.font["OS/2"].usMaxContext = maxCtxFont(self.font)
if "GDEF" in tables:
gdef = self.buildGDEF()
@@ -210,16 +215,19 @@ class Builder(object):
self.features_.setdefault(key, []).append(lookup)
def get_lookup_(self, location, builder_class):
- if (self.cur_lookup_ and
- type(self.cur_lookup_) == builder_class and
- self.cur_lookup_.lookupflag == self.lookupflag_ and
- self.cur_lookup_.markFilterSet ==
- self.lookupflag_markFilterSet_):
+ if (
+ self.cur_lookup_
+ and type(self.cur_lookup_) == builder_class
+ and self.cur_lookup_.lookupflag == self.lookupflag_
+ and self.cur_lookup_.markFilterSet == self.lookupflag_markFilterSet_
+ ):
return self.cur_lookup_
if self.cur_lookup_name_ and self.cur_lookup_:
raise FeatureLibError(
"Within a named lookup block, all rules must be of "
- "the same lookup type and flag", location)
+ "the same lookup type and flag",
+ location,
+ )
self.cur_lookup_ = builder_class(self.font, location)
self.cur_lookup_.lookupflag = self.lookupflag_
self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_
@@ -230,8 +238,7 @@ class Builder(object):
if self.cur_feature_name_:
# We are starting a lookup rule inside a feature. This includes
# lookup rules inside named lookups inside features.
- self.add_lookup_to_feature_(self.cur_lookup_,
- self.cur_feature_name_)
+ self.add_lookup_to_feature_(self.cur_lookup_, self.cur_feature_name_)
return self.cur_lookup_
def build_feature_aalt_(self):
@@ -239,14 +246,16 @@ class Builder(object):
return
alternates = {g: set(a) for g, a in self.aalt_alternates_.items()}
for location, name in self.aalt_features_ + [(None, "aalt")]:
- feature = [(script, lang, feature, lookups)
- for (script, lang, feature), lookups
- in self.features_.items()
- if feature == name]
+ feature = [
+ (script, lang, feature, lookups)
+ for (script, lang, feature), lookups in self.features_.items()
+ if feature == name
+ ]
# "aalt" does not have to specify its own lookups, but it might.
if not feature and name != "aalt":
- raise FeatureLibError("Feature %s has not been defined" % name,
- location)
+ raise FeatureLibError(
+ "Feature %s has not been defined" % name, location
+ )
for script, lang, feature, lookups in feature:
for lookuplist in lookups:
if not isinstance(lookuplist, list):
@@ -254,19 +263,23 @@ class Builder(object):
for lookup in lookuplist:
for glyph, alts in lookup.getAlternateGlyphs().items():
alternates.setdefault(glyph, set()).update(alts)
- single = {glyph: list(repl)[0] for glyph, repl in alternates.items()
- if len(repl) == 1}
+ single = {
+ glyph: list(repl)[0] for glyph, repl in alternates.items() if len(repl) == 1
+ }
# TODO: Figure out the glyph alternate ordering used by makeotf.
# https://github.com/fonttools/fonttools/issues/836
- multi = {glyph: sorted(repl, key=self.font.getGlyphID)
- for glyph, repl in alternates.items()
- if len(repl) > 1}
+ multi = {
+ glyph: sorted(repl, key=self.font.getGlyphID)
+ for glyph, repl in alternates.items()
+ if len(repl) > 1
+ }
if not single and not multi:
return
- self.features_ = {(script, lang, feature): lookups
- for (script, lang, feature), lookups
- in self.features_.items()
- if feature != "aalt"}
+ self.features_ = {
+ (script, lang, feature): lookups
+ for (script, lang, feature), lookups in self.features_.items()
+ if feature != "aalt"
+ }
old_lookups = self.lookups_
self.lookups_ = []
self.start_feature(self.aalt_location_, "aalt")
@@ -333,8 +346,12 @@ class Builder(object):
params = None
if tag == "size":
params = otTables.FeatureParamsSize()
- params.DesignSize, params.SubfamilyID, params.RangeStart, \
- params.RangeEnd = self.size_parameters_
+ (
+ params.DesignSize,
+ params.SubfamilyID,
+ params.RangeStart,
+ params.RangeEnd,
+ ) = self.size_parameters_
if tag in self.featureNames_ids_:
params.SubfamilyNameID = self.featureNames_ids_[tag]
else:
@@ -352,14 +369,18 @@ class Builder(object):
params = otTables.FeatureParamsCharacterVariants()
params.Format = 0
params.FeatUILabelNameID = self.cv_parameters_ids_.get(
- (tag, 'FeatUILabelNameID'), 0)
+ (tag, "FeatUILabelNameID"), 0
+ )
params.FeatUITooltipTextNameID = self.cv_parameters_ids_.get(
- (tag, 'FeatUITooltipTextNameID'), 0)
+ (tag, "FeatUITooltipTextNameID"), 0
+ )
params.SampleTextNameID = self.cv_parameters_ids_.get(
- (tag, 'SampleTextNameID'), 0)
+ (tag, "SampleTextNameID"), 0
+ )
params.NumNamedParameters = self.cv_num_named_params_.get(tag, 0)
params.FirstParamUILabelNameID = self.cv_parameters_ids_.get(
- (tag, 'ParamUILabelNameID_0'), 0)
+ (tag, "ParamUILabelNameID_0"), 0
+ )
params.CharCount = len(self.cv_characters_[tag])
params.Character = self.cv_characters_[tag]
return params
@@ -402,10 +423,18 @@ class Builder(object):
table.fsType = self.os2_["fstype"]
if "panose" in self.os2_:
panose = getTableModule("OS/2").Panose()
- panose.bFamilyType, panose.bSerifStyle, panose.bWeight,\
- panose.bProportion, panose.bContrast, panose.bStrokeVariation,\
- panose.bArmStyle, panose.bLetterForm, panose.bMidline, \
- panose.bXHeight = self.os2_["panose"]
+ (
+ panose.bFamilyType,
+ panose.bSerifStyle,
+ panose.bWeight,
+ panose.bProportion,
+ panose.bContrast,
+ panose.bStrokeVariation,
+ panose.bArmStyle,
+ panose.bLetterForm,
+ panose.bMidline,
+ panose.bXHeight,
+ ) = self.os2_["panose"]
table.panose = panose
if "typoascender" in self.os2_:
table.sTypoAscender = self.os2_["typoascender"]
@@ -441,28 +470,63 @@ class Builder(object):
if "upperopsize" in self.os2_:
table.usUpperOpticalPointSize = self.os2_["upperopsize"]
version = 5
+
def checkattr(table, attrs):
for attr in attrs:
if not hasattr(table, attr):
setattr(table, attr, 0)
+
table.version = max(version, table.version)
# this only happens for unit tests
if version >= 1:
checkattr(table, ("ulCodePageRange1", "ulCodePageRange2"))
if version >= 2:
- checkattr(table, ("sxHeight", "sCapHeight", "usDefaultChar",
- "usBreakChar", "usMaxContext"))
+ checkattr(
+ table,
+ (
+ "sxHeight",
+ "sCapHeight",
+ "usDefaultChar",
+ "usBreakChar",
+ "usMaxContext",
+ ),
+ )
if version >= 5:
- checkattr(table, ("usLowerOpticalPointSize",
- "usUpperOpticalPointSize"))
+ checkattr(table, ("usLowerOpticalPointSize", "usUpperOpticalPointSize"))
def build_codepages_(self, pages):
pages2bits = {
- 1252: 0, 1250: 1, 1251: 2, 1253: 3, 1254: 4, 1255: 5, 1256: 6,
- 1257: 7, 1258: 8, 874: 16, 932: 17, 936: 18, 949: 19, 950: 20,
- 1361: 21, 869: 48, 866: 49, 865: 50, 864: 51, 863: 52, 862: 53,
- 861: 54, 860: 55, 857: 56, 855: 57, 852: 58, 775: 59, 737: 60,
- 708: 61, 850: 62, 437: 63,
+ 1252: 0,
+ 1250: 1,
+ 1251: 2,
+ 1253: 3,
+ 1254: 4,
+ 1255: 5,
+ 1256: 6,
+ 1257: 7,
+ 1258: 8,
+ 874: 16,
+ 932: 17,
+ 936: 18,
+ 949: 19,
+ 950: 20,
+ 1361: 21,
+ 869: 48,
+ 866: 49,
+ 865: 50,
+ 864: 51,
+ 863: 52,
+ 862: 53,
+ 861: 54,
+ 860: 55,
+ 857: 56,
+ 855: 57,
+ 852: 58,
+ 775: 59,
+ 737: 60,
+ 708: 61,
+ 850: 62,
+ 437: 63,
}
bits = [pages2bits[p] for p in pages if p in pages2bits]
pages = []
@@ -518,16 +582,22 @@ class Builder(object):
def buildGDEF(self):
gdef = otTables.GDEF()
gdef.GlyphClassDef = self.buildGDEFGlyphClassDef_()
- gdef.AttachList = \
- otl.buildAttachList(self.attachPoints_, self.glyphMap)
- gdef.LigCaretList = \
- otl.buildLigCaretList(self.ligCaretCoords_, self.ligCaretPoints_,
- self.glyphMap)
+ gdef.AttachList = otl.buildAttachList(self.attachPoints_, self.glyphMap)
+ gdef.LigCaretList = otl.buildLigCaretList(
+ self.ligCaretCoords_, self.ligCaretPoints_, self.glyphMap
+ )
gdef.MarkAttachClassDef = self.buildGDEFMarkAttachClassDef_()
gdef.MarkGlyphSetsDef = self.buildGDEFMarkGlyphSetsDef_()
gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef else 0x00010000
- if any((gdef.GlyphClassDef, gdef.AttachList, gdef.LigCaretList,
- gdef.MarkAttachClassDef, gdef.MarkGlyphSetsDef)):
+ if any(
+ (
+ gdef.GlyphClassDef,
+ gdef.AttachList,
+ gdef.LigCaretList,
+ gdef.MarkAttachClassDef,
+ gdef.MarkGlyphSetsDef,
+ )
+ ):
result = newTable("GDEF")
result.table = gdef
return result
@@ -562,13 +632,14 @@ class Builder(object):
def buildGDEFMarkGlyphSetsDef_(self):
sets = []
- for glyphs, id_ in sorted(self.markFilterSets_.items(),
- key=lambda item: item[1]):
+ for glyphs, id_ in sorted(
+ self.markFilterSets_.items(), key=lambda item: item[1]
+ ):
sets.append(glyphs)
return otl.buildMarkGlyphSetsDef(sets, self.glyphMap)
def buildLookups_(self, tag):
- assert tag in ('GPOS', 'GSUB'), tag
+ assert tag in ("GPOS", "GSUB"), tag
for lookup in self.lookups_:
lookup.lookup_index = None
lookups = []
@@ -606,10 +677,11 @@ class Builder(object):
# l.lookup_index will be None when a lookup is not needed
# for the table under construction. For example, substitution
# rules will have no lookup_index while building GPOS tables.
- lookup_indices = tuple([l.lookup_index for l in lookups
- if l.lookup_index is not None])
+ lookup_indices = tuple(
+ [l.lookup_index for l in lookups if l.lookup_index is not None]
+ )
- size_feature = (tag == "GPOS" and feature_tag == "size")
+ size_feature = tag == "GPOS" and feature_tag == "size"
if len(lookup_indices) == 0 and not size_feature:
continue
@@ -620,14 +692,12 @@ class Builder(object):
frec = otTables.FeatureRecord()
frec.FeatureTag = feature_tag
frec.Feature = otTables.Feature()
- frec.Feature.FeatureParams = self.buildFeatureParams(
- feature_tag)
+ frec.Feature.FeatureParams = self.buildFeatureParams(feature_tag)
frec.Feature.LookupListIndex = list(lookup_indices)
frec.Feature.LookupCount = len(lookup_indices)
table.FeatureList.FeatureRecord.append(frec)
feature_indices[feature_key] = feature_index
- scripts.setdefault(script, {}).setdefault(lang, []).append(
- feature_index)
+ scripts.setdefault(script, {}).setdefault(lang, []).append(feature_index)
if self.required_features_.get((script, lang)) == feature_tag:
required_feature_indices[(script, lang)] = feature_index
@@ -643,17 +713,16 @@ class Builder(object):
langrec.LangSys = otTables.LangSys()
langrec.LangSys.LookupOrder = None
- req_feature_index = \
- required_feature_indices.get((script, lang))
+ req_feature_index = required_feature_indices.get((script, lang))
if req_feature_index is None:
langrec.LangSys.ReqFeatureIndex = 0xFFFF
else:
langrec.LangSys.ReqFeatureIndex = req_feature_index
- langrec.LangSys.FeatureIndex = [i for i in feature_indices
- if i != req_feature_index]
- langrec.LangSys.FeatureCount = \
- len(langrec.LangSys.FeatureIndex)
+ langrec.LangSys.FeatureIndex = [
+ i for i in feature_indices if i != req_feature_index
+ ]
+ langrec.LangSys.FeatureCount = len(langrec.LangSys.FeatureIndex)
if lang == "dflt":
srec.Script.DefaultLangSys = langrec.LangSys
@@ -670,24 +739,27 @@ class Builder(object):
def add_language_system(self, location, script, language):
# OpenType Feature File Specification, section 4.b.i
- if (script == "DFLT" and language == "dflt" and
- self.default_language_systems_):
+ if script == "DFLT" and language == "dflt" and self.default_language_systems_:
raise FeatureLibError(
'If "languagesystem DFLT dflt" is present, it must be '
- 'the first of the languagesystem statements', location)
+ "the first of the languagesystem statements",
+ location,
+ )
if script == "DFLT":
if self.seen_non_DFLT_script_:
raise FeatureLibError(
'languagesystems using the "DFLT" script tag must '
"precede all other languagesystems",
- location
+ location,
)
else:
self.seen_non_DFLT_script_ = True
if (script, language) in self.default_language_systems_:
raise FeatureLibError(
- '"languagesystem %s %s" has already been specified' %
- (script.strip(), language.strip()), location)
+ '"languagesystem %s %s" has already been specified'
+ % (script.strip(), language.strip()),
+ location,
+ )
self.default_language_systems_.add((script, language))
def get_default_language_systems_(self):
@@ -699,11 +771,11 @@ class Builder(object):
if self.default_language_systems_:
return frozenset(self.default_language_systems_)
else:
- return frozenset({('DFLT', 'dflt')})
+ return frozenset({("DFLT", "dflt")})
def start_feature(self, location, name):
self.language_systems = self.get_default_language_systems_()
- self.script_ = 'DFLT'
+ self.script_ = "DFLT"
self.cur_lookup_ = None
self.cur_feature_name_ = name
self.lookupflag_ = 0
@@ -722,12 +794,14 @@ class Builder(object):
def start_lookup_block(self, location, name):
if name in self.named_lookups_:
raise FeatureLibError(
- 'Lookup "%s" has already been defined' % name, location)
+ 'Lookup "%s" has already been defined' % name, location
+ )
if self.cur_feature_name_ == "aalt":
raise FeatureLibError(
"Lookup blocks cannot be placed inside 'aalt' features; "
"move it out, and then refer to it with a lookup statement",
- location)
+ location,
+ )
self.cur_lookup_name_ = name
self.named_lookups_[name] = None
self.cur_lookup_ = None
@@ -753,20 +827,24 @@ class Builder(object):
self.fontRevision_ = revision
def set_language(self, location, language, include_default, required):
- assert(len(language) == 4)
- if self.cur_feature_name_ in ('aalt', 'size'):
+ assert len(language) == 4
+ if self.cur_feature_name_ in ("aalt", "size"):
raise FeatureLibError(
"Language statements are not allowed "
- "within \"feature %s\"" % self.cur_feature_name_, location)
+ 'within "feature %s"' % self.cur_feature_name_,
+ location,
+ )
if self.cur_feature_name_ is None:
raise FeatureLibError(
"Language statements are not allowed "
- "within standalone lookup blocks", location)
+ "within standalone lookup blocks",
+ location,
+ )
self.cur_lookup_ = None
key = (self.script_, language, self.cur_feature_name_)
- lookups = self.features_.get((key[0], 'dflt', key[2]))
- if (language == 'dflt' or include_default) and lookups:
+ lookups = self.features_.get((key[0], "dflt", key[2]))
+ if (language == "dflt" or include_default) and lookups:
self.features_[key] = lookups[:]
else:
self.features_[key] = []
@@ -777,10 +855,14 @@ class Builder(object):
if key in self.required_features_:
raise FeatureLibError(
"Language %s (script %s) has already "
- "specified feature %s as its required feature" % (
- language.strip(), self.script_.strip(),
- self.required_features_[key].strip()),
- location)
+ "specified feature %s as its required feature"
+ % (
+ language.strip(),
+ self.script_.strip(),
+ self.required_features_[key].strip(),
+ ),
+ location,
+ )
self.required_features_[key] = self.cur_feature_name_
def getMarkAttachClass_(self, location, glyphs):
@@ -796,7 +878,8 @@ class Builder(object):
raise FeatureLibError(
"Glyph %s already has been assigned "
"a MarkAttachmentType at %s" % (glyph, loc),
- location)
+ location,
+ )
self.markAttach_[glyph] = (id_, location)
return id_
@@ -823,23 +906,25 @@ class Builder(object):
self.lookupflag_ = value
def set_script(self, location, script):
- if self.cur_feature_name_ in ('aalt', 'size'):
+ if self.cur_feature_name_ in ("aalt", "size"):
raise FeatureLibError(
"Script statements are not allowed "
- "within \"feature %s\"" % self.cur_feature_name_, location)
+ 'within "feature %s"' % self.cur_feature_name_,
+ location,
+ )
if self.cur_feature_name_ is None:
raise FeatureLibError(
- "Script statements are not allowed "
- "within standalone lookup blocks", location)
- if self.language_systems == {(script, 'dflt')}:
+ "Script statements are not allowed " "within standalone lookup blocks",
+ location,
+ )
+ if self.language_systems == {(script, "dflt")}:
# Nothing to do.
return
self.cur_lookup_ = None
self.script_ = script
self.lookupflag_ = 0
self.lookupflag_markFilterSet_ = None
- self.set_language(location, "dflt",
- include_default=True, required=False)
+ self.set_language(location, "dflt", include_default=True, required=False)
def find_lookup_builders_(self, lookups):
"""Helper for building chain contextual substitutions
@@ -850,8 +935,9 @@ class Builder(object):
lookup_builders = []
for lookuplist in lookups:
if lookuplist is not None:
- lookup_builders.append([self.named_lookups_.get(l.name)
- for l in lookuplist])
+ lookup_builders.append(
+ [self.named_lookups_.get(l.name) for l in lookuplist]
+ )
else:
lookup_builders.append(None)
return lookup_builders
@@ -862,17 +948,21 @@ class Builder(object):
def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups):
lookup = self.get_lookup_(location, ChainContextPosBuilder)
- lookup.rules.append((prefix, glyphs, suffix,
- self.find_lookup_builders_(lookups)))
+ lookup.rules.append(
+ ChainContextualRule(
+ prefix, glyphs, suffix, self.find_lookup_builders_(lookups)
+ )
+ )
- def add_chain_context_subst(self, location,
- prefix, glyphs, suffix, lookups):
+ def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups):
lookup = self.get_lookup_(location, ChainContextSubstBuilder)
- lookup.rules.append((prefix, glyphs, suffix,
- self.find_lookup_builders_(lookups)))
+ lookup.rules.append(
+ ChainContextualRule(
+ prefix, glyphs, suffix, self.find_lookup_builders_(lookups)
+ )
+ )
- def add_alternate_subst(self, location,
- prefix, glyph, suffix, replacement):
+ def add_alternate_subst(self, location, prefix, glyph, suffix, replacement):
if self.cur_feature_name_ == "aalt":
alts = self.aalt_alternates_.setdefault(glyph, set())
alts.update(replacement)
@@ -880,20 +970,20 @@ class Builder(object):
if prefix or suffix:
chain = self.get_lookup_(location, ChainContextSubstBuilder)
lookup = self.get_chained_lookup_(location, AlternateSubstBuilder)
- chain.rules.append((prefix, [{glyph}], suffix, [lookup]))
+ chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [lookup]))
else:
lookup = self.get_lookup_(location, AlternateSubstBuilder)
if glyph in lookup.alternates:
raise FeatureLibError(
- 'Already defined alternates for glyph "%s"' % glyph,
- location)
+ 'Already defined alternates for glyph "%s"' % glyph, location
+ )
lookup.alternates[glyph] = replacement
def add_feature_reference(self, location, featureName):
if self.cur_feature_name_ != "aalt":
raise FeatureLibError(
- 'Feature references are only allowed inside "feature aalt"',
- location)
+ 'Feature references are only allowed inside "feature aalt"', location
+ )
self.aalt_features_.append((location, featureName))
def add_featureName(self, tag):
@@ -919,23 +1009,27 @@ class Builder(object):
else:
self.base_horiz_axis_ = (bases, scripts)
- def set_size_parameters(self, location, DesignSize, SubfamilyID,
- RangeStart, RangeEnd):
- if self.cur_feature_name_ != 'size':
+ def set_size_parameters(
+ self, location, DesignSize, SubfamilyID, RangeStart, RangeEnd
+ ):
+ if self.cur_feature_name_ != "size":
raise FeatureLibError(
"Parameters statements are not allowed "
- "within \"feature %s\"" % self.cur_feature_name_, location)
+ 'within "feature %s"' % self.cur_feature_name_,
+ location,
+ )
self.size_parameters_ = [DesignSize, SubfamilyID, RangeStart, RangeEnd]
for script, lang in self.language_systems:
key = (script, lang, self.cur_feature_name_)
self.features_.setdefault(key, [])
- def add_ligature_subst(self, location,
- prefix, glyphs, suffix, replacement, forceChain):
+ def add_ligature_subst(
+ self, location, prefix, glyphs, suffix, replacement, forceChain
+ ):
if prefix or suffix or forceChain:
chain = self.get_lookup_(location, ChainContextSubstBuilder)
lookup = self.get_chained_lookup_(location, LigatureSubstBuilder)
- chain.rules.append((prefix, glyphs, suffix, [lookup]))
+ chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, [lookup]))
else:
lookup = self.get_lookup_(location, LigatureSubstBuilder)
@@ -947,31 +1041,32 @@ class Builder(object):
for g in sorted(itertools.product(*glyphs)):
lookup.ligatures[g] = replacement
- def add_multiple_subst(self, location,
- prefix, glyph, suffix, replacements, forceChain=False):
+ def add_multiple_subst(
+ self, location, prefix, glyph, suffix, replacements, forceChain=False
+ ):
if prefix or suffix or forceChain:
chain = self.get_lookup_(location, ChainContextSubstBuilder)
sub = self.get_chained_lookup_(location, MultipleSubstBuilder)
sub.mapping[glyph] = replacements
- chain.rules.append((prefix, [{glyph}], suffix, [sub]))
+ chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [sub]))
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'
+ "Removing duplicate multiple substitution from glyph"
' "%s" to %s%s',
- glyph, replacements,
- f' at {location}' if location else '',
+ glyph,
+ replacements,
+ f" at {location}" if location else "",
)
else:
raise FeatureLibError(
- 'Already defined substitution for glyph "%s"' % glyph,
- location)
+ 'Already defined substitution for glyph "%s"' % glyph, location
+ )
lookup.mapping[glyph] = replacements
- def add_reverse_chain_single_subst(self, location, old_prefix,
- old_suffix, mapping):
+ def add_reverse_chain_single_subst(self, location, old_prefix, old_suffix, mapping):
lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder)
lookup.rules.append((old_prefix, old_suffix, mapping))
@@ -989,15 +1084,18 @@ class Builder(object):
if from_glyph in lookup.mapping:
if to_glyph == lookup.mapping[from_glyph]:
log.info(
- 'Removing duplicate single substitution from glyph'
+ "Removing duplicate single substitution from glyph"
' "%s" to "%s" at %s',
- from_glyph, to_glyph, location,
+ from_glyph,
+ to_glyph,
+ location,
)
else:
raise FeatureLibError(
- 'Already defined rule for replacing glyph "%s" by "%s"' %
- (from_glyph, lookup.mapping[from_glyph]),
- location)
+ 'Already defined rule for replacing glyph "%s" by "%s"'
+ % (from_glyph, lookup.mapping[from_glyph]),
+ location,
+ )
lookup.mapping[from_glyph] = to_glyph
def add_single_subst_chained_(self, location, prefix, suffix, mapping):
@@ -1007,14 +1105,18 @@ class Builder(object):
if sub is None:
sub = self.get_chained_lookup_(location, SingleSubstBuilder)
sub.mapping.update(mapping)
- chain.rules.append((prefix, [mapping.keys()], suffix, [sub]))
+ chain.rules.append(
+ ChainContextualRule(prefix, [list(mapping.keys())], suffix, [sub])
+ )
def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor):
lookup = self.get_lookup_(location, CursivePosBuilder)
lookup.add_attachment(
- location, glyphclass,
+ location,
+ glyphclass,
makeOpenTypeAnchor(entryAnchor),
- makeOpenTypeAnchor(exitAnchor))
+ makeOpenTypeAnchor(exitAnchor),
+ )
def add_marks_(self, location, lookupBuilder, marks):
"""Helper for add_mark_{base,liga,mark}_pos."""
@@ -1023,15 +1125,15 @@ class Builder(object):
for mark in markClassDef.glyphs.glyphSet():
if mark not in lookupBuilder.marks:
otMarkAnchor = makeOpenTypeAnchor(markClassDef.anchor)
- lookupBuilder.marks[mark] = (
- markClass.name, otMarkAnchor)
+ lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor)
else:
existingMarkClass = lookupBuilder.marks[mark][0]
if markClass.name != existingMarkClass:
raise FeatureLibError(
- "Glyph %s cannot be in both @%s and @%s" % (
- mark, existingMarkClass, markClass.name),
- location)
+ "Glyph %s cannot be in both @%s and @%s"
+ % (mark, existingMarkClass, markClass.name),
+ location,
+ )
def add_mark_base_pos(self, location, bases, marks):
builder = self.get_lookup_(location, MarkBasePosBuilder)
@@ -1039,8 +1141,7 @@ class Builder(object):
for baseAnchor, markClass in marks:
otBaseAnchor = makeOpenTypeAnchor(baseAnchor)
for base in bases:
- builder.bases.setdefault(base, {})[markClass.name] = (
- otBaseAnchor)
+ builder.bases.setdefault(base, {})[markClass.name] = otBaseAnchor
def add_mark_lig_pos(self, location, ligatures, components):
builder = self.get_lookup_(location, MarkLigPosBuilder)
@@ -1060,11 +1161,11 @@ class Builder(object):
for baseAnchor, markClass in marks:
otBaseAnchor = makeOpenTypeAnchor(baseAnchor)
for baseMark in baseMarks:
- builder.baseMarks.setdefault(baseMark, {})[markClass.name] = (
- otBaseAnchor)
+ builder.baseMarks.setdefault(baseMark, {})[
+ markClass.name
+ ] = otBaseAnchor
- def add_class_pair_pos(self, location, glyphclass1, value1,
- glyphclass2, value2):
+ def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2):
lookup = self.get_lookup_(location, PairPosBuilder)
v1 = makeOpenTypeValueRecord(value1, pairPosContext=True)
v2 = makeOpenTypeValueRecord(value2, pairPosContext=True)
@@ -1113,19 +1214,22 @@ class Builder(object):
subs.append(sub)
assert len(pos) == len(subs), (pos, subs)
chain.rules.append(
- (prefix, [g for g, v in pos], suffix, subs))
+ ChainContextualRule(prefix, [g for g, v in pos], suffix, subs)
+ )
def setGlyphClass_(self, location, glyph, glyphClass):
oldClass, oldLocation = self.glyphClassDefs_.get(glyph, (None, None))
if oldClass and oldClass != glyphClass:
raise FeatureLibError(
- "Glyph %s was assigned to a different class at %s" %
- (glyph, oldLocation),
- location)
+ "Glyph %s was assigned to a different class at %s"
+ % (glyph, oldLocation),
+ location,
+ )
self.glyphClassDefs_[glyph] = (glyphClass, location)
- def add_glyphClassDef(self, location, baseGlyphs, ligatureGlyphs,
- markGlyphs, componentGlyphs):
+ def add_glyphClassDef(
+ self, location, baseGlyphs, ligatureGlyphs, markGlyphs, componentGlyphs
+ ):
for glyph in baseGlyphs:
self.setGlyphClass_(location, glyph, 1)
for glyph in ligatureGlyphs:
@@ -1145,8 +1249,7 @@ class Builder(object):
if glyph not in self.ligCaretCoords_:
self.ligCaretCoords_[glyph] = carets
- def add_name_record(self, location, nameID, platformID, platEncID,
- langID, string):
+ def add_name_record(self, location, nameID, platformID, platEncID, langID, string):
self.names_.append([nameID, platformID, platEncID, langID, string])
def add_os2_field(self, key, value):
@@ -1168,8 +1271,7 @@ def makeOpenTypeAnchor(anchor):
deviceX = otl.buildDevice(dict(anchor.xDeviceTable))
if anchor.yDeviceTable is not None:
deviceY = otl.buildDevice(dict(anchor.yDeviceTable))
- return otl.buildAnchor(anchor.x, anchor.y, anchor.contourpoint,
- deviceX, deviceY)
+ return otl.buildAnchor(anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY)
_VALUEREC_ATTRS = {
@@ -1193,6 +1295,3 @@ def makeOpenTypeValueRecord(v, pairPosContext):
vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0}
valRec = otl.buildValue(vr)
return valRec
-
-
-
diff --git a/Lib/fontTools/feaLib/error.py b/Lib/fontTools/feaLib/error.py
index 50322c48..a2c5f9db 100644
--- a/Lib/fontTools/feaLib/error.py
+++ b/Lib/fontTools/feaLib/error.py
@@ -1,5 +1,3 @@
-
-
class FeatureLibError(Exception):
def __init__(self, message, location):
Exception.__init__(self, message)
diff --git a/Lib/fontTools/feaLib/lexer.py b/Lib/fontTools/feaLib/lexer.py
index be7ac615..3caf3dc5 100644
--- a/Lib/fontTools/feaLib/lexer.py
+++ b/Lib/fontTools/feaLib/lexer.py
@@ -77,75 +77,75 @@ class Lexer(object):
self.line_start_ = self.pos_
return (Lexer.NEWLINE, None, location)
if cur_char == "\r":
- self.pos_ += (2 if next_char == "\n" else 1)
+ self.pos_ += 2 if next_char == "\n" else 1
self.line_ += 1
self.line_start_ = self.pos_
return (Lexer.NEWLINE, None, location)
if cur_char == "#":
self.scan_until_(Lexer.CHAR_NEWLINE_)
- return (Lexer.COMMENT, text[start:self.pos_], location)
+ return (Lexer.COMMENT, text[start : self.pos_], location)
if self.mode_ is Lexer.MODE_FILENAME_:
if cur_char != "(":
- raise FeatureLibError("Expected '(' before file name",
- location)
+ raise FeatureLibError("Expected '(' before file name", location)
self.scan_until_(")")
cur_char = text[self.pos_] if self.pos_ < limit else None
if cur_char != ")":
- raise FeatureLibError("Expected ')' after file name",
- location)
+ raise FeatureLibError("Expected ')' after file name", location)
self.pos_ += 1
self.mode_ = Lexer.MODE_NORMAL_
- return (Lexer.FILENAME, text[start + 1:self.pos_ - 1], location)
+ return (Lexer.FILENAME, text[start + 1 : self.pos_ - 1], location)
if cur_char == "\\" and next_char in Lexer.CHAR_DIGIT_:
self.pos_ += 1
self.scan_over_(Lexer.CHAR_DIGIT_)
- return (Lexer.CID, int(text[start + 1:self.pos_], 10), location)
+ return (Lexer.CID, int(text[start + 1 : self.pos_], 10), location)
if cur_char == "@":
self.pos_ += 1
self.scan_over_(Lexer.CHAR_NAME_CONTINUATION_)
- glyphclass = text[start + 1:self.pos_]
+ glyphclass = text[start + 1 : self.pos_]
if len(glyphclass) < 1:
raise FeatureLibError("Expected glyph class name", location)
if len(glyphclass) > 63:
raise FeatureLibError(
- "Glyph class names must not be longer than 63 characters",
- location)
+ "Glyph class names must not be longer than 63 characters", location
+ )
if not Lexer.RE_GLYPHCLASS.match(glyphclass):
raise FeatureLibError(
"Glyph class names must consist of letters, digits, "
- "underscore, period or hyphen", location)
+ "underscore, period or hyphen",
+ location,
+ )
return (Lexer.GLYPHCLASS, glyphclass, location)
if cur_char in Lexer.CHAR_NAME_START_:
self.pos_ += 1
self.scan_over_(Lexer.CHAR_NAME_CONTINUATION_)
- token = text[start:self.pos_]
+ token = text[start : self.pos_]
if token == "include":
self.mode_ = Lexer.MODE_FILENAME_
return (Lexer.NAME, token, location)
if cur_char == "0" and next_char in "xX":
self.pos_ += 2
self.scan_over_(Lexer.CHAR_HEXDIGIT_)
- return (Lexer.HEXADECIMAL, int(text[start:self.pos_], 16), location)
+ return (Lexer.HEXADECIMAL, int(text[start : self.pos_], 16), location)
if cur_char == "0" and next_char in Lexer.CHAR_DIGIT_:
self.scan_over_(Lexer.CHAR_DIGIT_)
- return (Lexer.OCTAL, int(text[start:self.pos_], 8), location)
+ return (Lexer.OCTAL, int(text[start : self.pos_], 8), location)
if cur_char in Lexer.CHAR_DIGIT_:
self.scan_over_(Lexer.CHAR_DIGIT_)
if self.pos_ >= limit or text[self.pos_] != ".":
- return (Lexer.NUMBER, int(text[start:self.pos_], 10), location)
+ return (Lexer.NUMBER, int(text[start : self.pos_], 10), location)
self.scan_over_(".")
self.scan_over_(Lexer.CHAR_DIGIT_)
- return (Lexer.FLOAT, float(text[start:self.pos_]), location)
+ return (Lexer.FLOAT, float(text[start : self.pos_]), location)
if cur_char == "-" and next_char in Lexer.CHAR_DIGIT_:
self.pos_ += 1
self.scan_over_(Lexer.CHAR_DIGIT_)
if self.pos_ >= limit or text[self.pos_] != ".":
- return (Lexer.NUMBER, int(text[start:self.pos_], 10), location)
+ return (Lexer.NUMBER, int(text[start : self.pos_], 10), location)
self.scan_over_(".")
self.scan_over_(Lexer.CHAR_DIGIT_)
- return (Lexer.FLOAT, float(text[start:self.pos_]), location)
+ return (Lexer.FLOAT, float(text[start : self.pos_]), location)
if cur_char in Lexer.CHAR_SYMBOL_:
self.pos_ += 1
return (Lexer.SYMBOL, cur_char, location)
@@ -155,13 +155,11 @@ class Lexer(object):
if self.pos_ < self.text_length_ and self.text_[self.pos_] == '"':
self.pos_ += 1
# strip newlines embedded within a string
- string = re.sub("[\r\n]", "", text[start + 1:self.pos_ - 1])
+ string = re.sub("[\r\n]", "", text[start + 1 : self.pos_ - 1])
return (Lexer.STRING, string, location)
else:
- raise FeatureLibError("Expected '\"' to terminate string",
- location)
- raise FeatureLibError("Unexpected character: %r" % cur_char,
- location)
+ raise FeatureLibError("Expected '\"' to terminate string", location)
+ raise FeatureLibError("Unexpected character: %r" % cur_char, location)
def scan_over_(self, valid):
p = self.pos_
@@ -180,12 +178,12 @@ class Lexer(object):
tag = tag.strip()
self.scan_until_(Lexer.CHAR_NEWLINE_)
self.scan_over_(Lexer.CHAR_NEWLINE_)
- regexp = r'}\s*' + tag + r'\s*;'
- split = re.split(regexp, self.text_[self.pos_:], maxsplit=1)
+ regexp = r"}\s*" + tag + r"\s*;"
+ split = re.split(regexp, self.text_[self.pos_ :], maxsplit=1)
if len(split) != 2:
raise FeatureLibError(
- "Expected '} %s;' to terminate anonymous block" % tag,
- location)
+ "Expected '} %s;' to terminate anonymous block" % tag, location
+ )
self.pos_ += len(split[0])
return (Lexer.ANONYMOUS_BLOCK, split[0], location)
@@ -237,8 +235,8 @@ class IncludingLexer(object):
fname_type, fname_token, fname_location = lexer.next()
if fname_type is not Lexer.FILENAME:
raise FeatureLibError("Expected file name", fname_location)
- #semi_type, semi_token, semi_location = lexer.next()
- #if semi_type is not Lexer.SYMBOL or semi_token != ";":
+ # semi_type, semi_token, semi_location = lexer.next()
+ # if semi_type is not Lexer.SYMBOL or semi_token != ";":
# raise FeatureLibError("Expected ';'", semi_location)
if os.path.isabs(fname_token):
path = fname_token
@@ -255,8 +253,7 @@ class IncludingLexer(object):
curpath = os.getcwd()
path = os.path.join(curpath, fname_token)
if len(self.lexers_) >= 5:
- raise FeatureLibError("Too many recursive includes",
- fname_location)
+ raise FeatureLibError("Too many recursive includes", fname_location)
try:
self.lexers_.append(self.make_lexer_(path))
except FileNotFoundError as err:
@@ -284,5 +281,6 @@ class IncludingLexer(object):
class NonIncludingLexer(IncludingLexer):
"""Lexer that does not follow `include` statements, emits them as-is."""
+
def __next__(self): # Python 3
return next(self.lexers_[0])
diff --git a/Lib/fontTools/feaLib/location.py b/Lib/fontTools/feaLib/location.py
index a11062bc..50f761d2 100644
--- a/Lib/fontTools/feaLib/location.py
+++ b/Lib/fontTools/feaLib/location.py
@@ -1,10 +1,12 @@
from typing import NamedTuple
+
class FeatureLibLocation(NamedTuple):
"""A location in a feature file"""
+
file: str
line: int
column: int
def __str__(self):
- return f"{self.file}:{self.line}:{self.column}"
+ return f"{self.file}:{self.line}:{self.column}"
diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py
index 40700d1d..7439fbf3 100644
--- a/Lib/fontTools/feaLib/parser.py
+++ b/Lib/fontTools/feaLib/parser.py
@@ -35,25 +35,30 @@ class Parser(object):
``includeDir`` to explicitly declare a directory to search included feature files
in.
"""
+
extensions = {}
ast = ast
- SS_FEATURE_TAGS = {"ss%02d" % i for i in range(1, 20+1)}
- CV_FEATURE_TAGS = {"cv%02d" % i for i in range(1, 99+1)}
+ SS_FEATURE_TAGS = {"ss%02d" % i for i in range(1, 20 + 1)}
+ CV_FEATURE_TAGS = {"cv%02d" % i for i in range(1, 99 + 1)}
- def __init__(self, featurefile, glyphNames=(), followIncludes=True,
- includeDir=None, **kwargs):
+ def __init__(
+ self, featurefile, glyphNames=(), followIncludes=True, includeDir=None, **kwargs
+ ):
if "glyphMap" in kwargs:
from fontTools.misc.loggingTools import deprecateArgument
+
deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead")
if glyphNames:
- raise TypeError("'glyphNames' and (deprecated) 'glyphMap' are "
- "mutually exclusive")
+ raise TypeError(
+ "'glyphNames' and (deprecated) 'glyphMap' are " "mutually exclusive"
+ )
glyphNames = kwargs.pop("glyphMap")
if kwargs:
- raise TypeError("unsupported keyword argument%s: %s"
- % ("" if len(kwargs) == 1 else "s",
- ", ".join(repr(k) for k in kwargs)))
+ raise TypeError(
+ "unsupported keyword argument%s: %s"
+ % ("" if len(kwargs) == 1 else "s", ", ".join(repr(k) for k in kwargs))
+ )
self.glyphNames_ = set(glyphNames)
self.doc_ = self.ast.FeatureFile()
@@ -61,9 +66,7 @@ class Parser(object):
self.glyphclasses_ = SymbolTable()
self.lookups_ = SymbolTable()
self.valuerecords_ = SymbolTable()
- self.symbol_tables_ = {
- self.anchors_, self.valuerecords_
- }
+ self.symbol_tables_ = {self.anchors_, self.valuerecords_}
self.next_token_type_, self.next_token_ = (None, None)
self.cur_comments_ = []
self.next_token_location_ = None
@@ -80,8 +83,8 @@ class Parser(object):
self.advance_lexer_(comments=True)
if self.cur_token_type_ is Lexer.COMMENT:
statements.append(
- self.ast.Comment(self.cur_token_,
- location=self.cur_token_location_))
+ self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
+ )
elif self.is_cur_keyword_("include"):
statements.append(self.parse_include_())
elif self.cur_token_type_ is Lexer.GLYPHCLASS:
@@ -101,17 +104,22 @@ class Parser(object):
elif self.is_cur_keyword_("table"):
statements.append(self.parse_table_())
elif self.is_cur_keyword_("valueRecordDef"):
- statements.append(
- self.parse_valuerecord_definition_(vertical=False))
- elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in self.extensions:
+ statements.append(self.parse_valuerecord_definition_(vertical=False))
+ elif (
+ self.cur_token_type_ is Lexer.NAME
+ and self.cur_token_ in self.extensions
+ ):
statements.append(self.extensions[self.cur_token_](self))
elif self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ";":
continue
else:
raise FeatureLibError(
"Expected feature, languagesystem, lookup, markClass, "
- "table, or glyph class definition, got {} \"{}\"".format(self.cur_token_type_, self.cur_token_),
- self.cur_token_location_)
+ 'table, or glyph class definition, got {} "{}"'.format(
+ self.cur_token_type_, self.cur_token_
+ ),
+ self.cur_token_location_,
+ )
return self.doc_
def parse_anchor_(self):
@@ -121,44 +129,52 @@ class Parser(object):
self.expect_keyword_("anchor")
location = self.cur_token_location_
- if self.next_token_ == "NULL": # Format D
+ if self.next_token_ == "NULL": # Format D
self.expect_keyword_("NULL")
self.expect_symbol_(">")
return None
- if self.next_token_type_ == Lexer.NAME: # Format E
+ if self.next_token_type_ == Lexer.NAME: # Format E
name = self.expect_name_()
anchordef = self.anchors_.resolve(name)
if anchordef is None:
raise FeatureLibError(
- 'Unknown anchor "%s"' % name,
- self.cur_token_location_)
+ 'Unknown anchor "%s"' % name, self.cur_token_location_
+ )
self.expect_symbol_(">")
- return self.ast.Anchor(anchordef.x, anchordef.y,
- name=name,
- contourpoint=anchordef.contourpoint,
- xDeviceTable=None, yDeviceTable=None,
- location=location)
+ return self.ast.Anchor(
+ anchordef.x,
+ anchordef.y,
+ name=name,
+ contourpoint=anchordef.contourpoint,
+ xDeviceTable=None,
+ yDeviceTable=None,
+ location=location,
+ )
x, y = self.expect_number_(), self.expect_number_()
contourpoint = None
- if self.next_token_ == "contourpoint": # Format B
+ if self.next_token_ == "contourpoint": # Format B
self.expect_keyword_("contourpoint")
contourpoint = self.expect_number_()
- if self.next_token_ == "<": # Format C
+ if self.next_token_ == "<": # Format C
xDeviceTable = self.parse_device_()
yDeviceTable = self.parse_device_()
else:
xDeviceTable, yDeviceTable = None, None
self.expect_symbol_(">")
- return self.ast.Anchor(x, y, name=None,
- contourpoint=contourpoint,
- xDeviceTable=xDeviceTable,
- yDeviceTable=yDeviceTable,
- location=location)
+ return self.ast.Anchor(
+ x,
+ y,
+ name=None,
+ contourpoint=contourpoint,
+ xDeviceTable=xDeviceTable,
+ yDeviceTable=yDeviceTable,
+ location=location,
+ )
def parse_anchor_marks_(self):
# Parses a sequence of ``[<anchor> mark @MARKCLASS]*.``
@@ -183,9 +199,9 @@ class Parser(object):
contourpoint = self.expect_number_()
name = self.expect_name_()
self.expect_symbol_(";")
- anchordef = self.ast.AnchorDefinition(name, x, y,
- contourpoint=contourpoint,
- location=location)
+ anchordef = self.ast.AnchorDefinition(
+ name, x, y, contourpoint=contourpoint, location=location
+ )
self.anchors_.define(name, anchordef)
return anchordef
@@ -195,10 +211,10 @@ class Parser(object):
tag = self.expect_tag_()
_, content, location = self.lexer_.scan_anonymous_block(tag)
self.advance_lexer_()
- self.expect_symbol_('}')
+ self.expect_symbol_("}")
end_tag = self.expect_tag_()
assert tag == end_tag, "bad splitting in Lexer.scan_anonymous_block()"
- self.expect_symbol_(';')
+ self.expect_symbol_(";")
return self.ast.AnonymousBlock(tag, content, location=location)
def parse_attach_(self):
@@ -210,8 +226,7 @@ class Parser(object):
while self.next_token_ != ";":
contourPoints.add(self.expect_number_())
self.expect_symbol_(";")
- return self.ast.AttachStatement(glyphs, contourPoints,
- location=location)
+ return self.ast.AttachStatement(glyphs, contourPoints, location=location)
def parse_enumerate_(self, vertical):
# Parse an enumerated pair positioning rule (`section 6.b.ii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_).
@@ -243,9 +258,9 @@ class Parser(object):
else:
componentGlyphs = None
self.expect_symbol_(";")
- return self.ast.GlyphClassDefStatement(baseGlyphs, markGlyphs,
- ligatureGlyphs, componentGlyphs,
- location=location)
+ return self.ast.GlyphClassDefStatement(
+ baseGlyphs, markGlyphs, ligatureGlyphs, componentGlyphs, location=location
+ )
def parse_glyphclass_definition_(self):
# Parses glyph class definitions such as '@UPPERCASE = [A-Z];'
@@ -253,8 +268,7 @@ class Parser(object):
self.expect_symbol_("=")
glyphs = self.parse_glyphclass_(accept_glyphname=False)
self.expect_symbol_(";")
- glyphclass = self.ast.GlyphClassDefinition(name, glyphs,
- location=location)
+ glyphclass = self.ast.GlyphClassDefinition(name, glyphs, location=location)
self.glyphclasses_.define(name, glyphclass)
return glyphclass
@@ -288,20 +302,22 @@ class Parser(object):
return start, limit
elif len(solutions) == 0:
raise FeatureLibError(
- "\"%s\" is not a glyph in the font, and it can not be split "
- "into a range of known glyphs" % name, location)
+ '"%s" is not a glyph in the font, and it can not be split '
+ "into a range of known glyphs" % name,
+ location,
+ )
else:
- ranges = " or ".join(["\"%s - %s\"" % (s, l) for s, l in solutions])
+ ranges = " or ".join(['"%s - %s"' % (s, l) for s, l in solutions])
raise FeatureLibError(
- "Ambiguous glyph range \"%s\"; "
+ 'Ambiguous glyph range "%s"; '
"please use %s to clarify what you mean" % (name, ranges),
- location)
+ location,
+ )
def parse_glyphclass_(self, accept_glyphname):
# Parses a glyph class, either named or anonymous, or (if
# ``bool(accept_glyphname)``) a glyph name.
- if (accept_glyphname and
- self.next_token_type_ in (Lexer.NAME, Lexer.CID)):
+ if accept_glyphname and self.next_token_type_ in (Lexer.NAME, Lexer.CID):
glyph = self.expect_glyph_()
self.check_glyph_name_in_glyph_set(glyph)
return self.ast.GlyphName(glyph, location=self.cur_token_location_)
@@ -311,13 +327,12 @@ class Parser(object):
if gc is None:
raise FeatureLibError(
"Unknown glyph class @%s" % self.cur_token_,
- self.cur_token_location_)
+ self.cur_token_location_,
+ )
if isinstance(gc, self.ast.MarkClass):
- return self.ast.MarkClassName(
- gc, location=self.cur_token_location_)
+ return self.ast.MarkClassName(gc, location=self.cur_token_location_)
else:
- return self.ast.GlyphClassName(
- gc, location=self.cur_token_location_)
+ return self.ast.GlyphClassName(gc, location=self.cur_token_location_)
self.expect_symbol_("[")
location = self.cur_token_location_
@@ -326,26 +341,30 @@ class Parser(object):
if self.next_token_type_ is Lexer.NAME:
glyph = self.expect_glyph_()
location = self.cur_token_location_
- if '-' in glyph and self.glyphNames_ and glyph not in self.glyphNames_:
+ if "-" in glyph and self.glyphNames_ and glyph not in self.glyphNames_:
start, limit = self.split_glyph_range_(glyph, location)
self.check_glyph_name_in_glyph_set(start, limit)
glyphs.add_range(
- start, limit,
- self.make_glyph_range_(location, start, limit))
+ start, limit, self.make_glyph_range_(location, start, limit)
+ )
elif self.next_token_ == "-":
start = glyph
self.expect_symbol_("-")
limit = self.expect_glyph_()
self.check_glyph_name_in_glyph_set(start, limit)
glyphs.add_range(
- start, limit,
- self.make_glyph_range_(location, start, limit))
+ start, limit, self.make_glyph_range_(location, start, limit)
+ )
else:
- if '-' in glyph and not self.glyphNames_:
- log.warning(str(FeatureLibError(
- f"Ambiguous glyph name that looks like a range: {glyph!r}",
- location
- )))
+ if "-" in glyph and not self.glyphNames_:
+ log.warning(
+ str(
+ FeatureLibError(
+ f"Ambiguous glyph name that looks like a range: {glyph!r}",
+ location,
+ )
+ )
+ )
self.check_glyph_name_in_glyph_set(glyph)
glyphs.append(glyph)
elif self.next_token_type_ is Lexer.CID:
@@ -356,12 +375,13 @@ class Parser(object):
self.expect_symbol_("-")
range_end = self.expect_cid_()
self.check_glyph_name_in_glyph_set(
- f"cid{range_start:05d}",
- f"cid{range_end:05d}",
+ f"cid{range_start:05d}", f"cid{range_end:05d}",
+ )
+ glyphs.add_cid_range(
+ range_start,
+ range_end,
+ self.make_cid_range_(range_location, range_start, range_end),
)
- glyphs.add_cid_range(range_start, range_end,
- self.make_cid_range_(range_location,
- range_start, range_end))
else:
glyph_name = f"cid{self.cur_token_:05d}"
self.check_glyph_name_in_glyph_set(glyph_name)
@@ -372,37 +392,22 @@ class Parser(object):
if gc is None:
raise FeatureLibError(
"Unknown glyph class @%s" % self.cur_token_,
- self.cur_token_location_)
+ self.cur_token_location_,
+ )
if isinstance(gc, self.ast.MarkClass):
- gc = self.ast.MarkClassName(
- gc, location=self.cur_token_location_)
+ gc = self.ast.MarkClassName(gc, location=self.cur_token_location_)
else:
- gc = self.ast.GlyphClassName(
- gc, location=self.cur_token_location_)
+ gc = self.ast.GlyphClassName(gc, location=self.cur_token_location_)
glyphs.add_class(gc)
else:
raise FeatureLibError(
"Expected glyph name, glyph range, "
f"or glyph class reference, found {self.next_token_!r}",
- self.next_token_location_)
+ self.next_token_location_,
+ )
self.expect_symbol_("]")
return glyphs
- def parse_class_name_(self):
- # Parses named class - either a glyph class or mark class.
- name = self.expect_class_name_()
- gc = self.glyphclasses_.resolve(name)
- if gc is None:
- raise FeatureLibError(
- "Unknown glyph class @%s" % name,
- self.cur_token_location_)
- if isinstance(gc, self.ast.MarkClass):
- return self.ast.MarkClassName(
- gc, location=self.cur_token_location_)
- else:
- return self.ast.GlyphClassName(
- gc, location=self.cur_token_location_)
-
def parse_glyph_pattern_(self, vertical):
# Parses a glyph pattern, including lookups and context, e.g.::
#
@@ -425,7 +430,8 @@ class Parser(object):
raise FeatureLibError(
"Unsupported contextual target sequence: at most "
"one run of marked (') glyph/class names allowed",
- self.cur_token_location_)
+ self.cur_token_location_,
+ )
glyphs.append(gc)
elif glyphs:
suffix.append(gc)
@@ -445,13 +451,14 @@ class Parser(object):
if not marked:
raise FeatureLibError(
"Lookups can only follow marked glyphs",
- self.cur_token_location_)
+ self.cur_token_location_,
+ )
lookup_name = self.expect_name_()
lookup = self.lookups_.resolve(lookup_name)
if lookup is None:
raise FeatureLibError(
- 'Unknown lookup "%s"' % lookup_name,
- self.cur_token_location_)
+ 'Unknown lookup "%s"' % lookup_name, self.cur_token_location_
+ )
lookuplist.append(lookup)
if marked:
lookups.append(lookuplist)
@@ -460,22 +467,33 @@ class Parser(object):
assert lookups == []
return ([], prefix, [None] * len(prefix), values, [], hasMarks)
else:
- assert not any(values[:len(prefix)]), values
- format1 = values[len(prefix):][:len(glyphs)]
- format2 = values[(len(prefix) + len(glyphs)):][:len(suffix)]
- values = format2 if format2 and isinstance(format2[0], self.ast.ValueRecord) else format1
+ assert not any(values[: len(prefix)]), values
+ format1 = values[len(prefix) :][: len(glyphs)]
+ format2 = values[(len(prefix) + len(glyphs)) :][: len(suffix)]
+ values = (
+ format2
+ if format2 and isinstance(format2[0], self.ast.ValueRecord)
+ else format1
+ )
return (prefix, glyphs, lookups, values, suffix, hasMarks)
def parse_chain_context_(self):
location = self.cur_token_location_
- prefix, glyphs, lookups, values, suffix, hasMarks = \
- self.parse_glyph_pattern_(vertical=False)
+ prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_(
+ vertical=False
+ )
chainContext = [(prefix, glyphs, suffix)]
hasLookups = any(lookups)
while self.next_token_ == ",":
self.expect_symbol_(",")
- prefix, glyphs, lookups, values, suffix, hasMarks = \
- self.parse_glyph_pattern_(vertical=False)
+ (
+ prefix,
+ glyphs,
+ lookups,
+ values,
+ suffix,
+ hasMarks,
+ ) = self.parse_glyph_pattern_(vertical=False)
chainContext.append((prefix, glyphs, suffix))
hasLookups = hasLookups or any(lookups)
self.expect_symbol_(";")
@@ -490,21 +508,19 @@ class Parser(object):
chainContext, hasLookups = self.parse_chain_context_()
if hasLookups:
raise FeatureLibError(
- "No lookups can be specified for \"ignore sub\"",
- location)
- return self.ast.IgnoreSubstStatement(chainContext,
- location=location)
+ 'No lookups can be specified for "ignore sub"', location
+ )
+ return self.ast.IgnoreSubstStatement(chainContext, location=location)
if self.cur_token_ in ["position", "pos"]:
chainContext, hasLookups = self.parse_chain_context_()
if hasLookups:
raise FeatureLibError(
- "No lookups can be specified for \"ignore pos\"",
- location)
- return self.ast.IgnorePosStatement(chainContext,
- location=location)
+ 'No lookups can be specified for "ignore pos"', location
+ )
+ return self.ast.IgnorePosStatement(chainContext, location=location)
raise FeatureLibError(
- "Expected \"substitute\" or \"position\"",
- self.cur_token_location_)
+ 'Expected "substitute" or "position"', self.cur_token_location_
+ )
def parse_include_(self):
assert self.cur_token_ == "include"
@@ -519,14 +535,14 @@ class Parser(object):
language = self.expect_language_tag_()
include_default, required = (True, False)
if self.next_token_ in {"exclude_dflt", "include_dflt"}:
- include_default = (self.expect_name_() == "include_dflt")
+ include_default = self.expect_name_() == "include_dflt"
if self.next_token_ == "required":
self.expect_keyword_("required")
required = True
self.expect_symbol_(";")
- return self.ast.LanguageStatement(language,
- include_default, required,
- location=location)
+ return self.ast.LanguageStatement(
+ language, include_default, required, location=location
+ )
def parse_ligatureCaretByIndex_(self):
assert self.is_cur_keyword_("LigatureCaretByIndex")
@@ -536,8 +552,7 @@ class Parser(object):
while self.next_token_ != ";":
carets.append(self.expect_number_())
self.expect_symbol_(";")
- return self.ast.LigatureCaretByIndexStatement(glyphs, carets,
- location=location)
+ return self.ast.LigatureCaretByIndexStatement(glyphs, carets, location=location)
def parse_ligatureCaretByPos_(self):
assert self.is_cur_keyword_("LigatureCaretByPos")
@@ -547,8 +562,7 @@ class Parser(object):
while self.next_token_ != ";":
carets.append(self.expect_number_())
self.expect_symbol_(";")
- return self.ast.LigatureCaretByPosStatement(glyphs, carets,
- location=location)
+ return self.ast.LigatureCaretByPosStatement(glyphs, carets, location=location)
def parse_lookup_(self, vertical):
# Parses a ``lookup`` - either a lookup block, or a lookup reference
@@ -559,11 +573,11 @@ class Parser(object):
if self.next_token_ == ";":
lookup = self.lookups_.resolve(name)
if lookup is None:
- raise FeatureLibError("Unknown lookup \"%s\"" % name,
- self.cur_token_location_)
+ raise FeatureLibError(
+ 'Unknown lookup "%s"' % name, self.cur_token_location_
+ )
self.expect_symbol_(";")
- return self.ast.LookupReferenceStatement(lookup,
- location=location)
+ return self.ast.LookupReferenceStatement(lookup, location=location)
use_extension = False
if self.next_token_ == "useExtension":
@@ -591,39 +605,46 @@ class Parser(object):
value_seen = False
value, markAttachment, markFilteringSet = 0, None, None
flags = {
- "RightToLeft": 1, "IgnoreBaseGlyphs": 2,
- "IgnoreLigatures": 4, "IgnoreMarks": 8
+ "RightToLeft": 1,
+ "IgnoreBaseGlyphs": 2,
+ "IgnoreLigatures": 4,
+ "IgnoreMarks": 8,
}
seen = set()
while self.next_token_ != ";":
if self.next_token_ in seen:
raise FeatureLibError(
"%s can be specified only once" % self.next_token_,
- self.next_token_location_)
+ self.next_token_location_,
+ )
seen.add(self.next_token_)
if self.next_token_ == "MarkAttachmentType":
self.expect_keyword_("MarkAttachmentType")
- markAttachment = self.parse_class_name_()
+ markAttachment = self.parse_glyphclass_(accept_glyphname=False)
elif self.next_token_ == "UseMarkFilteringSet":
self.expect_keyword_("UseMarkFilteringSet")
- markFilteringSet = self.parse_class_name_()
+ markFilteringSet = self.parse_glyphclass_(accept_glyphname=False)
elif self.next_token_ in flags:
value_seen = True
value = value | flags[self.expect_name_()]
else:
raise FeatureLibError(
'"%s" is not a recognized lookupflag' % self.next_token_,
- self.next_token_location_)
+ self.next_token_location_,
+ )
self.expect_symbol_(";")
if not any([value_seen, markAttachment, markFilteringSet]):
raise FeatureLibError(
- 'lookupflag must have a value', self.next_token_location_)
+ "lookupflag must have a value", self.next_token_location_
+ )
- return self.ast.LookupFlagStatement(value,
- markAttachment=markAttachment,
- markFilteringSet=markFilteringSet,
- location=location)
+ return self.ast.LookupFlagStatement(
+ value,
+ markAttachment=markAttachment,
+ markFilteringSet=markFilteringSet,
+ location=location,
+ )
def parse_markClass_(self):
assert self.is_cur_keyword_("markClass")
@@ -637,8 +658,9 @@ class Parser(object):
markClass = self.ast.MarkClass(name)
self.doc_.markClasses[name] = markClass
self.glyphclasses_.define(name, markClass)
- mcdef = self.ast.MarkClassDefinition(markClass, anchor, glyphs,
- location=location)
+ mcdef = self.ast.MarkClassDefinition(
+ markClass, anchor, glyphs, location=location
+ )
markClass.addDefinition(mcdef)
return mcdef
@@ -646,26 +668,28 @@ class Parser(object):
assert self.cur_token_ in {"position", "pos"}
if self.next_token_ == "cursive": # GPOS type 3
return self.parse_position_cursive_(enumerated, vertical)
- elif self.next_token_ == "base": # GPOS type 4
+ elif self.next_token_ == "base": # GPOS type 4
return self.parse_position_base_(enumerated, vertical)
- elif self.next_token_ == "ligature": # GPOS type 5
+ elif self.next_token_ == "ligature": # GPOS type 5
return self.parse_position_ligature_(enumerated, vertical)
- elif self.next_token_ == "mark": # GPOS type 6
+ elif self.next_token_ == "mark": # GPOS type 6
return self.parse_position_mark_(enumerated, vertical)
location = self.cur_token_location_
- prefix, glyphs, lookups, values, suffix, hasMarks = \
- self.parse_glyph_pattern_(vertical)
+ prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_(
+ vertical
+ )
self.expect_symbol_(";")
if any(lookups):
# GPOS type 8: Chaining contextual positioning; explicit lookups
if any(values):
raise FeatureLibError(
- "If \"lookup\" is present, no values must be specified",
- location)
+ 'If "lookup" is present, no values must be specified', location
+ )
return self.ast.ChainContextPosStatement(
- prefix, glyphs, suffix, lookups, location=location)
+ prefix, glyphs, suffix, lookups, location=location
+ )
# Pair positioning, format A: "pos V 10 A -10;"
# Pair positioning, format B: "pos V A -20;"
@@ -673,31 +697,41 @@ class Parser(object):
if values[0] is None: # Format B: "pos V A -20;"
values.reverse()
return self.ast.PairPosStatement(
- glyphs[0], values[0], glyphs[1], values[1],
+ glyphs[0],
+ values[0],
+ glyphs[1],
+ values[1],
enumerated=enumerated,
- location=location)
+ location=location,
+ )
if enumerated:
raise FeatureLibError(
- '"enumerate" is only allowed with pair positionings', location)
- return self.ast.SinglePosStatement(list(zip(glyphs, values)),
- prefix, suffix, forceChain=hasMarks,
- location=location)
+ '"enumerate" is only allowed with pair positionings', location
+ )
+ return self.ast.SinglePosStatement(
+ list(zip(glyphs, values)),
+ prefix,
+ suffix,
+ forceChain=hasMarks,
+ location=location,
+ )
def parse_position_cursive_(self, enumerated, vertical):
location = self.cur_token_location_
self.expect_keyword_("cursive")
if enumerated:
raise FeatureLibError(
- '"enumerate" is not allowed with '
- 'cursive attachment positioning',
- location)
+ '"enumerate" is not allowed with ' "cursive attachment positioning",
+ location,
+ )
glyphclass = self.parse_glyphclass_(accept_glyphname=True)
entryAnchor = self.parse_anchor_()
exitAnchor = self.parse_anchor_()
self.expect_symbol_(";")
return self.ast.CursivePosStatement(
- glyphclass, entryAnchor, exitAnchor, location=location)
+ glyphclass, entryAnchor, exitAnchor, location=location
+ )
def parse_position_base_(self, enumerated, vertical):
location = self.cur_token_location_
@@ -705,8 +739,9 @@ class Parser(object):
if enumerated:
raise FeatureLibError(
'"enumerate" is not allowed with '
- 'mark-to-base attachment positioning',
- location)
+ "mark-to-base attachment positioning",
+ location,
+ )
base = self.parse_glyphclass_(accept_glyphname=True)
marks = self.parse_anchor_marks_()
self.expect_symbol_(";")
@@ -718,8 +753,9 @@ class Parser(object):
if enumerated:
raise FeatureLibError(
'"enumerate" is not allowed with '
- 'mark-to-ligature attachment positioning',
- location)
+ "mark-to-ligature attachment positioning",
+ location,
+ )
ligatures = self.parse_glyphclass_(accept_glyphname=True)
marks = [self.parse_anchor_marks_()]
while self.next_token_ == "ligComponent":
@@ -734,13 +770,13 @@ class Parser(object):
if enumerated:
raise FeatureLibError(
'"enumerate" is not allowed with '
- 'mark-to-mark attachment positioning',
- location)
+ "mark-to-mark attachment positioning",
+ location,
+ )
baseMarks = self.parse_glyphclass_(accept_glyphname=True)
marks = self.parse_anchor_marks_()
self.expect_symbol_(";")
- return self.ast.MarkMarkPosStatement(baseMarks, marks,
- location=location)
+ return self.ast.MarkMarkPosStatement(baseMarks, marks, location=location)
def parse_script_(self):
assert self.is_cur_keyword_("script")
@@ -752,11 +788,18 @@ class Parser(object):
assert self.cur_token_ in {"substitute", "sub", "reversesub", "rsub"}
location = self.cur_token_location_
reverse = self.cur_token_ in {"reversesub", "rsub"}
- old_prefix, old, lookups, values, old_suffix, hasMarks = \
- self.parse_glyph_pattern_(vertical=False)
+ (
+ old_prefix,
+ old,
+ lookups,
+ values,
+ old_suffix,
+ hasMarks,
+ ) = self.parse_glyph_pattern_(vertical=False)
if any(values):
raise FeatureLibError(
- "Substitution statements cannot contain values", location)
+ "Substitution statements cannot contain values", location
+ )
new = []
if self.next_token_ == "by":
keyword = self.expect_keyword_("by")
@@ -772,25 +815,25 @@ class Parser(object):
if len(new) == 0 and not any(lookups):
raise FeatureLibError(
'Expected "by", "from" or explicit lookup references',
- self.cur_token_location_)
+ self.cur_token_location_,
+ )
# GSUB lookup type 3: Alternate substitution.
# Format: "substitute a from [a.1 a.2 a.3];"
if keyword == "from":
if reverse:
raise FeatureLibError(
- 'Reverse chaining substitutions do not support "from"',
- location)
+ 'Reverse chaining substitutions do not support "from"', location
+ )
if len(old) != 1 or len(old[0].glyphSet()) != 1:
- raise FeatureLibError(
- 'Expected a single glyph before "from"',
- location)
+ raise FeatureLibError('Expected a single glyph before "from"', location)
if len(new) != 1:
raise FeatureLibError(
- 'Expected a single glyphclass after "from"',
- location)
+ 'Expected a single glyphclass after "from"', location
+ )
return self.ast.AlternateSubstStatement(
- old_prefix, old[0], old_suffix, new[0], location=location)
+ old_prefix, old[0], old_suffix, new[0], location=location
+ )
num_lookups = len([l for l in lookups if l is not None])
@@ -798,8 +841,7 @@ class Parser(object):
# Format A: "substitute a by a.sc;"
# Format B: "substitute [one.fitted one.oldstyle] by one;"
# Format C: "substitute [a-d] by [A.sc-D.sc];"
- if (not reverse and len(old) == 1 and len(new) == 1 and
- num_lookups == 0):
+ if not reverse and len(old) == 1 and len(new) == 1 and num_lookups == 0:
glyphs = list(old[0].glyphSet())
replacements = list(new[0].glyphSet())
if len(replacements) == 1:
@@ -807,36 +849,50 @@ class Parser(object):
if len(glyphs) != len(replacements):
raise FeatureLibError(
'Expected a glyph class with %d elements after "by", '
- 'but found a glyph class with %d elements' %
- (len(glyphs), len(replacements)), location)
+ "but found a glyph class with %d elements"
+ % (len(glyphs), len(replacements)),
+ location,
+ )
return self.ast.SingleSubstStatement(
- old, new,
- old_prefix, old_suffix,
- forceChain=hasMarks,
- location=location
+ old, new, old_prefix, old_suffix, forceChain=hasMarks, location=location
)
# GSUB lookup type 2: Multiple substitution.
# Format: "substitute f_f_i by f f i;"
- if (not reverse and
- len(old) == 1 and len(old[0].glyphSet()) == 1 and
- len(new) > 1 and max([len(n.glyphSet()) for n in new]) == 1 and
- num_lookups == 0):
+ if (
+ not reverse
+ and len(old) == 1
+ and len(old[0].glyphSet()) == 1
+ and len(new) > 1
+ and max([len(n.glyphSet()) for n in new]) == 1
+ and num_lookups == 0
+ ):
return self.ast.MultipleSubstStatement(
- old_prefix, tuple(old[0].glyphSet())[0], old_suffix,
+ old_prefix,
+ tuple(old[0].glyphSet())[0],
+ old_suffix,
tuple([list(n.glyphSet())[0] for n in new]),
- forceChain=hasMarks, location=location)
+ forceChain=hasMarks,
+ location=location,
+ )
# GSUB lookup type 4: Ligature substitution.
# Format: "substitute f f i by f_f_i;"
- if (not reverse and
- len(old) > 1 and len(new) == 1 and
- len(new[0].glyphSet()) == 1 and
- num_lookups == 0):
+ if (
+ not reverse
+ and len(old) > 1
+ and len(new) == 1
+ and len(new[0].glyphSet()) == 1
+ and num_lookups == 0
+ ):
return self.ast.LigatureSubstStatement(
- old_prefix, old, old_suffix,
- list(new[0].glyphSet())[0], forceChain=hasMarks,
- location=location)
+ old_prefix,
+ old,
+ old_suffix,
+ list(new[0].glyphSet())[0],
+ forceChain=hasMarks,
+ location=location,
+ )
# GSUB lookup type 8: Reverse chaining substitution.
if reverse:
@@ -844,16 +900,19 @@ class Parser(object):
raise FeatureLibError(
"In reverse chaining single substitutions, "
"only a single glyph or glyph class can be replaced",
- location)
+ location,
+ )
if len(new) != 1:
raise FeatureLibError(
- 'In reverse chaining single substitutions, '
+ "In reverse chaining single substitutions, "
'the replacement (after "by") must be a single glyph '
- 'or glyph class', location)
+ "or glyph class",
+ location,
+ )
if num_lookups != 0:
raise FeatureLibError(
- "Reverse chaining substitutions cannot call named lookups",
- location)
+ "Reverse chaining substitutions cannot call named lookups", location
+ )
glyphs = sorted(list(old[0].glyphSet()))
replacements = sorted(list(new[0].glyphSet()))
if len(replacements) == 1:
@@ -861,27 +920,29 @@ class Parser(object):
if len(glyphs) != len(replacements):
raise FeatureLibError(
'Expected a glyph class with %d elements after "by", '
- 'but found a glyph class with %d elements' %
- (len(glyphs), len(replacements)), location)
+ "but found a glyph class with %d elements"
+ % (len(glyphs), len(replacements)),
+ location,
+ )
return self.ast.ReverseChainSingleSubstStatement(
- old_prefix, old_suffix, old, new, location=location)
+ old_prefix, old_suffix, old, new, location=location
+ )
if len(old) > 1 and len(new) > 1:
raise FeatureLibError(
- 'Direct substitution of multiple glyphs by multiple glyphs '
- 'is not supported',
- location)
+ "Direct substitution of multiple glyphs by multiple glyphs "
+ "is not supported",
+ location,
+ )
# If there are remaining glyphs to parse, this is an invalid GSUB statement
if len(new) != 0:
- raise FeatureLibError(
- 'Invalid substitution statement',
- location
- )
+ raise FeatureLibError("Invalid substitution statement", location)
# GSUB lookup type 6: Chaining contextual substitution.
rule = self.ast.ChainContextSubstStatement(
- old_prefix, old, old_suffix, lookups, location=location)
+ old_prefix, old, old_suffix, lookups, location=location
+ )
return rule
def parse_subtable_(self):
@@ -899,23 +960,22 @@ class Parser(object):
SubfamilyID = self.expect_number_()
RangeStart = 0
RangeEnd = 0
- if self.next_token_type_ in (Lexer.NUMBER, Lexer.FLOAT) or \
- SubfamilyID != 0:
+ if self.next_token_type_ in (Lexer.NUMBER, Lexer.FLOAT) or SubfamilyID != 0:
RangeStart = self.expect_decipoint_()
RangeEnd = self.expect_decipoint_()
self.expect_symbol_(";")
- return self.ast.SizeParameters(DesignSize, SubfamilyID,
- RangeStart, RangeEnd,
- location=location)
+ return self.ast.SizeParameters(
+ DesignSize, SubfamilyID, RangeStart, RangeEnd, location=location
+ )
def parse_size_menuname_(self):
assert self.is_cur_keyword_("sizemenuname")
location = self.cur_token_location_
platformID, platEncID, langID, string = self.parse_name_()
- return self.ast.FeatureNameStatement("size", platformID,
- platEncID, langID, string,
- location=location)
+ return self.ast.FeatureNameStatement(
+ "size", platformID, platEncID, langID, string, location=location
+ )
def parse_table_(self):
assert self.is_cur_keyword_("table")
@@ -934,13 +994,15 @@ class Parser(object):
if handler:
handler(table)
else:
- raise FeatureLibError('"table %s" is not supported' % name.strip(),
- location)
+ raise FeatureLibError(
+ '"table %s" is not supported' % name.strip(), location
+ )
self.expect_symbol_("}")
end_tag = self.expect_tag_()
if end_tag != name:
- raise FeatureLibError('Expected "%s"' % name.strip(),
- self.cur_token_location_)
+ raise FeatureLibError(
+ 'Expected "%s"' % name.strip(), self.cur_token_location_
+ )
self.expect_symbol_(";")
return table
@@ -949,8 +1011,9 @@ class Parser(object):
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_(comments=True)
if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(self.ast.Comment(
- self.cur_token_, location=self.cur_token_location_))
+ statements.append(
+ self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
+ )
elif self.is_cur_keyword_("Attach"):
statements.append(self.parse_attach_())
elif self.is_cur_keyword_("GlyphClassDef"):
@@ -963,24 +1026,24 @@ class Parser(object):
continue
else:
raise FeatureLibError(
- "Expected Attach, LigatureCaretByIndex, "
- "or LigatureCaretByPos",
- self.cur_token_location_)
+ "Expected Attach, LigatureCaretByIndex, " "or LigatureCaretByPos",
+ self.cur_token_location_,
+ )
def parse_table_head_(self, table):
statements = table.statements
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_(comments=True)
if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(self.ast.Comment(
- self.cur_token_, location=self.cur_token_location_))
+ statements.append(
+ self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
+ )
elif self.is_cur_keyword_("FontRevision"):
statements.append(self.parse_FontRevision_())
elif self.cur_token_ == ";":
continue
else:
- raise FeatureLibError("Expected FontRevision",
- self.cur_token_location_)
+ raise FeatureLibError("Expected FontRevision", self.cur_token_location_)
def parse_table_hhea_(self, table):
statements = table.statements
@@ -988,22 +1051,26 @@ class Parser(object):
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_(comments=True)
if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(self.ast.Comment(
- self.cur_token_, location=self.cur_token_location_))
+ statements.append(
+ self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
+ )
elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields:
key = self.cur_token_.lower()
value = self.expect_number_()
statements.append(
- self.ast.HheaField(key, value,
- location=self.cur_token_location_))
+ self.ast.HheaField(key, value, location=self.cur_token_location_)
+ )
if self.next_token_ != ";":
- raise FeatureLibError("Incomplete statement", self.next_token_location_)
+ raise FeatureLibError(
+ "Incomplete statement", self.next_token_location_
+ )
elif self.cur_token_ == ";":
continue
else:
- raise FeatureLibError("Expected CaretOffset, Ascender, "
- "Descender or LineGap",
- self.cur_token_location_)
+ raise FeatureLibError(
+ "Expected CaretOffset, Ascender, " "Descender or LineGap",
+ self.cur_token_location_,
+ )
def parse_table_vhea_(self, table):
statements = table.statements
@@ -1011,30 +1078,36 @@ class Parser(object):
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_(comments=True)
if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(self.ast.Comment(
- self.cur_token_, location=self.cur_token_location_))
+ statements.append(
+ self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
+ )
elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields:
key = self.cur_token_.lower()
value = self.expect_number_()
statements.append(
- self.ast.VheaField(key, value,
- location=self.cur_token_location_))
+ self.ast.VheaField(key, value, location=self.cur_token_location_)
+ )
if self.next_token_ != ";":
- raise FeatureLibError("Incomplete statement", self.next_token_location_)
+ raise FeatureLibError(
+ "Incomplete statement", self.next_token_location_
+ )
elif self.cur_token_ == ";":
continue
else:
- raise FeatureLibError("Expected VertTypoAscender, "
- "VertTypoDescender or VertTypoLineGap",
- self.cur_token_location_)
+ raise FeatureLibError(
+ "Expected VertTypoAscender, "
+ "VertTypoDescender or VertTypoLineGap",
+ self.cur_token_location_,
+ )
def parse_table_name_(self, table):
statements = table.statements
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_(comments=True)
if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(self.ast.Comment(
- self.cur_token_, location=self.cur_token_location_))
+ statements.append(
+ self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
+ )
elif self.is_cur_keyword_("nameid"):
statement = self.parse_nameid_()
if statement:
@@ -1042,8 +1115,7 @@ class Parser(object):
elif self.cur_token_ == ";":
continue
else:
- raise FeatureLibError("Expected nameid",
- self.cur_token_location_)
+ raise FeatureLibError("Expected nameid", self.cur_token_location_)
def parse_name_(self):
"""Parses a name record. See `section 9.e <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_."""
@@ -1061,12 +1133,12 @@ class Parser(object):
platformID = 3
location = self.cur_token_location_
- if platformID == 1: # Macintosh
- platEncID = platEncID or 0 # Roman
- langID = langID or 0 # English
- else: # 3, Windows
- platEncID = platEncID or 1 # Unicode
- langID = langID or 0x0409 # English
+ if platformID == 1: # Macintosh
+ platEncID = platEncID or 0 # Roman
+ langID = langID or 0 # English
+ else: # 3, Windows
+ platEncID = platEncID or 1 # Unicode
+ langID = langID or 0x0409 # English
string = self.expect_string_()
self.expect_symbol_(";")
@@ -1081,17 +1153,21 @@ class Parser(object):
assert self.cur_token_ == "nameid", self.cur_token_
location, nameID = self.cur_token_location_, self.expect_any_number_()
if nameID > 32767:
- raise FeatureLibError("Name id value cannot be greater than 32767",
- self.cur_token_location_)
+ raise FeatureLibError(
+ "Name id value cannot be greater than 32767", self.cur_token_location_
+ )
if 1 <= nameID <= 6:
- log.warning("Name id %d cannot be set from the feature file. "
- "Ignoring record" % nameID)
+ log.warning(
+ "Name id %d cannot be set from the feature file. "
+ "Ignoring record" % nameID
+ )
self.parse_name_() # skip to the next record
return None
platformID, platEncID, langID, string = self.parse_name_()
- return self.ast.NameRecord(nameID, platformID, platEncID,
- langID, string, location=location)
+ return self.ast.NameRecord(
+ nameID, platformID, platEncID, langID, string, location=location
+ )
def unescape_string_(self, string, encoding):
if encoding == "utf_16_be":
@@ -1120,38 +1196,59 @@ class Parser(object):
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_(comments=True)
if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(self.ast.Comment(
- self.cur_token_, location=self.cur_token_location_))
+ statements.append(
+ self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
+ )
elif self.is_cur_keyword_("HorizAxis.BaseTagList"):
horiz_bases = self.parse_base_tag_list_()
elif self.is_cur_keyword_("HorizAxis.BaseScriptList"):
horiz_scripts = self.parse_base_script_list_(len(horiz_bases))
statements.append(
- self.ast.BaseAxis(horiz_bases,
- horiz_scripts, False,
- location=self.cur_token_location_))
+ self.ast.BaseAxis(
+ horiz_bases,
+ horiz_scripts,
+ False,
+ location=self.cur_token_location_,
+ )
+ )
elif self.is_cur_keyword_("VertAxis.BaseTagList"):
vert_bases = self.parse_base_tag_list_()
elif self.is_cur_keyword_("VertAxis.BaseScriptList"):
vert_scripts = self.parse_base_script_list_(len(vert_bases))
statements.append(
- self.ast.BaseAxis(vert_bases,
- vert_scripts, True,
- location=self.cur_token_location_))
+ self.ast.BaseAxis(
+ vert_bases,
+ vert_scripts,
+ True,
+ location=self.cur_token_location_,
+ )
+ )
elif self.cur_token_ == ";":
continue
def parse_table_OS_2_(self, table):
statements = table.statements
- numbers = ("FSType", "TypoAscender", "TypoDescender", "TypoLineGap",
- "winAscent", "winDescent", "XHeight", "CapHeight",
- "WeightClass", "WidthClass", "LowerOpSize", "UpperOpSize")
+ numbers = (
+ "FSType",
+ "TypoAscender",
+ "TypoDescender",
+ "TypoLineGap",
+ "winAscent",
+ "winDescent",
+ "XHeight",
+ "CapHeight",
+ "WeightClass",
+ "WidthClass",
+ "LowerOpSize",
+ "UpperOpSize",
+ )
ranges = ("UnicodeRange", "CodePageRange")
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_(comments=True)
if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(self.ast.Comment(
- self.cur_token_, location=self.cur_token_location_))
+ statements.append(
+ self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
+ )
elif self.cur_token_type_ is Lexer.NAME:
key = self.cur_token_.lower()
value = None
@@ -1164,19 +1261,21 @@ class Parser(object):
elif self.cur_token_ in ranges:
value = []
while self.next_token_ != ";":
- value.append(self.expect_number_())
+ value.append(self.expect_number_())
elif self.is_cur_keyword_("Vendor"):
value = self.expect_string_()
statements.append(
- self.ast.OS2Field(key, value,
- location=self.cur_token_location_))
+ self.ast.OS2Field(key, value, location=self.cur_token_location_)
+ )
elif self.cur_token_ == ";":
continue
def parse_base_tag_list_(self):
# Parses BASE table entries. (See `section 9.a <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.a>`_)
- assert self.cur_token_ in ("HorizAxis.BaseTagList",
- "VertAxis.BaseTagList"), self.cur_token_
+ assert self.cur_token_ in (
+ "HorizAxis.BaseTagList",
+ "VertAxis.BaseTagList",
+ ), self.cur_token_
bases = []
while self.next_token_ != ";":
bases.append(self.expect_script_tag_())
@@ -1184,8 +1283,10 @@ class Parser(object):
return bases
def parse_base_script_list_(self, count):
- assert self.cur_token_ in ("HorizAxis.BaseScriptList",
- "VertAxis.BaseScriptList"), self.cur_token_
+ assert self.cur_token_ in (
+ "HorizAxis.BaseScriptList",
+ "VertAxis.BaseScriptList",
+ ), self.cur_token_
scripts = [(self.parse_base_script_record_(count))]
while self.next_token_ == ",":
self.expect_symbol_(",")
@@ -1221,13 +1322,13 @@ class Parser(object):
if self.next_token_type_ is Lexer.NUMBER:
number, location = self.expect_number_(), self.cur_token_location_
if vertical:
- val = self.ast.ValueRecord(yAdvance=number,
- vertical=vertical,
- location=location)
+ val = self.ast.ValueRecord(
+ yAdvance=number, vertical=vertical, location=location
+ )
else:
- val = self.ast.ValueRecord(xAdvance=number,
- vertical=vertical,
- location=location)
+ val = self.ast.ValueRecord(
+ xAdvance=number, vertical=vertical, location=location
+ )
return val
self.expect_symbol_("<")
location = self.cur_token_location_
@@ -1238,40 +1339,57 @@ class Parser(object):
return self.ast.ValueRecord()
vrd = self.valuerecords_.resolve(name)
if vrd is None:
- raise FeatureLibError("Unknown valueRecordDef \"%s\"" % name,
- self.cur_token_location_)
+ raise FeatureLibError(
+ 'Unknown valueRecordDef "%s"' % name, self.cur_token_location_
+ )
value = vrd.value
xPlacement, yPlacement = (value.xPlacement, value.yPlacement)
xAdvance, yAdvance = (value.xAdvance, value.yAdvance)
else:
xPlacement, yPlacement, xAdvance, yAdvance = (
- self.expect_number_(), self.expect_number_(),
- self.expect_number_(), self.expect_number_())
+ self.expect_number_(),
+ self.expect_number_(),
+ self.expect_number_(),
+ self.expect_number_(),
+ )
if self.next_token_ == "<":
xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = (
- self.parse_device_(), self.parse_device_(),
- self.parse_device_(), self.parse_device_())
- allDeltas = sorted([
- delta
- for size, delta
- in (xPlaDevice if xPlaDevice else ()) +
- (yPlaDevice if yPlaDevice else ()) +
- (xAdvDevice if xAdvDevice else ()) +
- (yAdvDevice if yAdvDevice else ())])
+ self.parse_device_(),
+ self.parse_device_(),
+ self.parse_device_(),
+ self.parse_device_(),
+ )
+ allDeltas = sorted(
+ [
+ delta
+ for size, delta in (xPlaDevice if xPlaDevice else ())
+ + (yPlaDevice if yPlaDevice else ())
+ + (xAdvDevice if xAdvDevice else ())
+ + (yAdvDevice if yAdvDevice else ())
+ ]
+ )
if allDeltas[0] < -128 or allDeltas[-1] > 127:
raise FeatureLibError(
"Device value out of valid range (-128..127)",
- self.cur_token_location_)
+ self.cur_token_location_,
+ )
else:
- xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = (
- None, None, None, None)
+ xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = (None, None, None, None)
self.expect_symbol_(">")
return self.ast.ValueRecord(
- xPlacement, yPlacement, xAdvance, yAdvance,
- xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice,
- vertical=vertical, location=location)
+ xPlacement,
+ yPlacement,
+ xAdvance,
+ yAdvance,
+ xPlaDevice,
+ yPlaDevice,
+ xAdvDevice,
+ yAdvDevice,
+ vertical=vertical,
+ location=location,
+ )
def parse_valuerecord_definition_(self, vertical):
# Parses a named value record definition. (See section `2.e.v <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.v>`_)
@@ -1290,14 +1408,13 @@ class Parser(object):
script = self.expect_script_tag_()
language = self.expect_language_tag_()
self.expect_symbol_(";")
- return self.ast.LanguageSystemStatement(script, language,
- location=location)
+ return self.ast.LanguageSystemStatement(script, language, location=location)
def parse_feature_block_(self):
assert self.cur_token_ == "feature"
location = self.cur_token_location_
tag = self.expect_tag_()
- vertical = (tag in {"vkrn", "vpal", "vhal", "valt"})
+ vertical = tag in {"vkrn", "vpal", "vhal", "valt"}
stylisticset = None
cv_feature = None
@@ -1314,10 +1431,10 @@ class Parser(object):
self.expect_keyword_("useExtension")
use_extension = True
- block = self.ast.FeatureBlock(tag, use_extension=use_extension,
- location=location)
- self.parse_block_(block, vertical, stylisticset, size_feature,
- cv_feature)
+ block = self.ast.FeatureBlock(
+ tag, use_extension=use_extension, location=location
+ )
+ self.parse_block_(block, vertical, stylisticset, size_feature, cv_feature)
return block
def parse_feature_reference_(self):
@@ -1325,35 +1442,36 @@ class Parser(object):
location = self.cur_token_location_
featureName = self.expect_tag_()
self.expect_symbol_(";")
- return self.ast.FeatureReferenceStatement(featureName,
- location=location)
+ return self.ast.FeatureReferenceStatement(featureName, location=location)
def parse_featureNames_(self, tag):
"""Parses a ``featureNames`` statement found in stylistic set features.
See section `8.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.c>`_."""
assert self.cur_token_ == "featureNames", self.cur_token_
- block = self.ast.NestedBlock(tag, self.cur_token_,
- location=self.cur_token_location_)
+ block = self.ast.NestedBlock(
+ tag, self.cur_token_, location=self.cur_token_location_
+ )
self.expect_symbol_("{")
for symtab in self.symbol_tables_:
symtab.enter_scope()
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_(comments=True)
if self.cur_token_type_ is Lexer.COMMENT:
- block.statements.append(self.ast.Comment(
- self.cur_token_, location=self.cur_token_location_))
+ block.statements.append(
+ self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
+ )
elif self.is_cur_keyword_("name"):
location = self.cur_token_location_
platformID, platEncID, langID, string = self.parse_name_()
block.statements.append(
- self.ast.FeatureNameStatement(tag, platformID,
- platEncID, langID, string,
- location=location))
+ self.ast.FeatureNameStatement(
+ tag, platformID, platEncID, langID, string, location=location
+ )
+ )
elif self.cur_token_ == ";":
continue
else:
- raise FeatureLibError('Expected "name"',
- self.cur_token_location_)
+ raise FeatureLibError('Expected "name"', self.cur_token_location_)
self.expect_symbol_("}")
for symtab in self.symbol_tables_:
symtab.exit_scope()
@@ -1364,8 +1482,9 @@ class Parser(object):
# Parses a ``cvParameters`` block found in Character Variant features.
# See section `8.d <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.d>`_.
assert self.cur_token_ == "cvParameters", self.cur_token_
- block = self.ast.NestedBlock(tag, self.cur_token_,
- location=self.cur_token_location_)
+ block = self.ast.NestedBlock(
+ tag, self.cur_token_, location=self.cur_token_location_
+ )
self.expect_symbol_("{")
for symtab in self.symbol_tables_:
symtab.enter_scope()
@@ -1374,12 +1493,17 @@ class Parser(object):
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_(comments=True)
if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(self.ast.Comment(
- self.cur_token_, location=self.cur_token_location_))
- elif self.is_cur_keyword_({"FeatUILabelNameID",
- "FeatUITooltipTextNameID",
- "SampleTextNameID",
- "ParamUILabelNameID"}):
+ statements.append(
+ self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
+ )
+ elif self.is_cur_keyword_(
+ {
+ "FeatUILabelNameID",
+ "FeatUITooltipTextNameID",
+ "SampleTextNameID",
+ "ParamUILabelNameID",
+ }
+ ):
statements.append(self.parse_cvNameIDs_(tag, self.cur_token_))
elif self.is_cur_keyword_("Character"):
statements.append(self.parse_cvCharacter_(tag))
@@ -1388,8 +1512,10 @@ class Parser(object):
else:
raise FeatureLibError(
"Expected statement: got {} {}".format(
- self.cur_token_type_, self.cur_token_),
- self.cur_token_location_)
+ self.cur_token_type_, self.cur_token_
+ ),
+ self.cur_token_location_,
+ )
self.expect_symbol_("}")
for symtab in self.symbol_tables_:
@@ -1399,28 +1525,34 @@ class Parser(object):
def parse_cvNameIDs_(self, tag, block_name):
assert self.cur_token_ == block_name, self.cur_token_
- block = self.ast.NestedBlock(tag, block_name,
- location=self.cur_token_location_)
+ block = self.ast.NestedBlock(tag, block_name, location=self.cur_token_location_)
self.expect_symbol_("{")
for symtab in self.symbol_tables_:
symtab.enter_scope()
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_(comments=True)
if self.cur_token_type_ is Lexer.COMMENT:
- block.statements.append(self.ast.Comment(
- self.cur_token_, location=self.cur_token_location_))
+ block.statements.append(
+ self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
+ )
elif self.is_cur_keyword_("name"):
location = self.cur_token_location_
platformID, platEncID, langID, string = self.parse_name_()
block.statements.append(
self.ast.CVParametersNameStatement(
- tag, platformID, platEncID, langID, string,
- block_name, location=location))
+ tag,
+ platformID,
+ platEncID,
+ langID,
+ string,
+ block_name,
+ location=location,
+ )
+ )
elif self.cur_token_ == ";":
continue
else:
- raise FeatureLibError('Expected "name"',
- self.cur_token_location_)
+ raise FeatureLibError('Expected "name"', self.cur_token_location_)
self.expect_symbol_("}")
for symtab in self.symbol_tables_:
symtab.exit_scope()
@@ -1432,9 +1564,11 @@ class Parser(object):
location, character = self.cur_token_location_, self.expect_any_number_()
self.expect_symbol_(";")
if not (0xFFFFFF >= character >= 0):
- raise FeatureLibError("Character value must be between "
- "{:#x} and {:#x}".format(0, 0xFFFFFF),
- location)
+ raise FeatureLibError(
+ "Character value must be between "
+ "{:#x} and {:#x}".format(0, 0xFFFFFF),
+ location,
+ )
return self.ast.CharacterStatement(character, tag, location=location)
def parse_FontRevision_(self):
@@ -1444,12 +1578,12 @@ class Parser(object):
location, version = self.cur_token_location_, self.expect_float_()
self.expect_symbol_(";")
if version <= 0:
- raise FeatureLibError("Font revision numbers must be positive",
- location)
+ raise FeatureLibError("Font revision numbers must be positive", location)
return self.ast.FontRevisionStatement(version, location=location)
- def parse_block_(self, block, vertical, stylisticset=None,
- size_feature=False, cv_feature=None):
+ def parse_block_(
+ self, block, vertical, stylisticset=None, size_feature=False, cv_feature=None
+ ):
self.expect_symbol_("{")
for symtab in self.symbol_tables_:
symtab.enter_scope()
@@ -1458,8 +1592,9 @@ class Parser(object):
while self.next_token_ != "}" or self.cur_comments_:
self.advance_lexer_(comments=True)
if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(self.ast.Comment(
- self.cur_token_, location=self.cur_token_location_))
+ statements.append(
+ self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
+ )
elif self.cur_token_type_ is Lexer.GLYPHCLASS:
statements.append(self.parse_glyphclass_definition_())
elif self.is_cur_keyword_("anchorDef"):
@@ -1480,11 +1615,11 @@ class Parser(object):
statements.append(self.parse_markClass_())
elif self.is_cur_keyword_({"pos", "position"}):
statements.append(
- self.parse_position_(enumerated=False, vertical=vertical))
+ self.parse_position_(enumerated=False, vertical=vertical)
+ )
elif self.is_cur_keyword_("script"):
statements.append(self.parse_script_())
- elif (self.is_cur_keyword_({"sub", "substitute",
- "rsub", "reversesub"})):
+ elif self.is_cur_keyword_({"sub", "substitute", "rsub", "reversesub"}):
statements.append(self.parse_substitute_())
elif self.is_cur_keyword_("subtable"):
statements.append(self.parse_subtable_())
@@ -1498,14 +1633,20 @@ class Parser(object):
statements.append(self.parse_size_parameters_())
elif size_feature and self.is_cur_keyword_("sizemenuname"):
statements.append(self.parse_size_menuname_())
- elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in self.extensions:
+ elif (
+ self.cur_token_type_ is Lexer.NAME
+ and self.cur_token_ in self.extensions
+ ):
statements.append(self.extensions[self.cur_token_](self))
elif self.cur_token_ == ";":
continue
else:
raise FeatureLibError(
- "Expected glyph class definition or statement: got {} {}".format(self.cur_token_type_, self.cur_token_),
- self.cur_token_location_)
+ "Expected glyph class definition or statement: got {} {}".format(
+ self.cur_token_type_, self.cur_token_
+ ),
+ self.cur_token_location_,
+ )
self.expect_symbol_("}")
for symtab in self.symbol_tables_:
@@ -1513,8 +1654,9 @@ class Parser(object):
name = self.expect_name_()
if name != block.name.strip():
- raise FeatureLibError("Expected \"%s\"" % block.name.strip(),
- self.cur_token_location_)
+ raise FeatureLibError(
+ 'Expected "%s"' % block.name.strip(), self.cur_token_location_
+ )
self.expect_symbol_(";")
# A multiple substitution may have a single destination, in which case
@@ -1543,8 +1685,14 @@ class Parser(object):
for i, glyph in enumerate(glyphs):
statements.append(
self.ast.MultipleSubstStatement(
- s.prefix, glyph, s.suffix, [replacements[i]],
- s.forceChain, location=s.location))
+ s.prefix,
+ glyph,
+ s.suffix,
+ [replacements[i]],
+ s.forceChain,
+ location=s.location,
+ )
+ )
else:
statements.append(s)
block.statements = statements
@@ -1572,8 +1720,7 @@ class Parser(object):
def expect_filename_(self):
self.advance_lexer_()
if self.cur_token_type_ is not Lexer.FILENAME:
- raise FeatureLibError("Expected file name",
- self.cur_token_location_)
+ raise FeatureLibError("Expected file name", self.cur_token_location_)
return self.cur_token_
def expect_glyph_(self):
@@ -1583,12 +1730,12 @@ class Parser(object):
if len(self.cur_token_) > 63:
raise FeatureLibError(
"Glyph names must not be longer than 63 characters",
- self.cur_token_location_)
+ self.cur_token_location_,
+ )
return self.cur_token_
elif self.cur_token_type_ is Lexer.CID:
return "cid%05d" % self.cur_token_
- raise FeatureLibError("Expected a glyph name or CID",
- self.cur_token_location_)
+ raise FeatureLibError("Expected a glyph name or CID", self.cur_token_location_)
def check_glyph_name_in_glyph_set(self, *names):
"""Raises if glyph name (just `start`) or glyph names of a
@@ -1602,18 +1749,20 @@ class Parser(object):
raise FeatureLibError(
"The following glyph names are referenced but are missing from the "
f"glyph set: {', '.join(missing)}",
- self.cur_token_location_
+ self.cur_token_location_,
)
def expect_markClass_reference_(self):
name = self.expect_class_name_()
mc = self.glyphclasses_.resolve(name)
if mc is None:
- raise FeatureLibError("Unknown markClass @%s" % name,
- self.cur_token_location_)
+ raise FeatureLibError(
+ "Unknown markClass @%s" % name, self.cur_token_location_
+ )
if not isinstance(mc, self.ast.MarkClass):
- raise FeatureLibError("@%s is not a markClass" % name,
- self.cur_token_location_)
+ raise FeatureLibError(
+ "@%s is not a markClass" % name, self.cur_token_location_
+ )
return mc
def expect_tag_(self):
@@ -1621,8 +1770,9 @@ class Parser(object):
if self.cur_token_type_ is not Lexer.NAME:
raise FeatureLibError("Expected a tag", self.cur_token_location_)
if len(self.cur_token_) > 4:
- raise FeatureLibError("Tags can not be longer than 4 characters",
- self.cur_token_location_)
+ raise FeatureLibError(
+ "Tags can not be longer than 4 characters", self.cur_token_location_
+ )
return (self.cur_token_ + " ")[:4]
def expect_script_tag_(self):
@@ -1630,7 +1780,8 @@ class Parser(object):
if tag == "dflt":
raise FeatureLibError(
'"dflt" is not a valid script tag; use "DFLT" instead',
- self.cur_token_location_)
+ self.cur_token_location_,
+ )
return tag
def expect_language_tag_(self):
@@ -1638,22 +1789,21 @@ class Parser(object):
if tag == "DFLT":
raise FeatureLibError(
'"DFLT" is not a valid language tag; use "dflt" instead',
- self.cur_token_location_)
+ self.cur_token_location_,
+ )
return tag
def expect_symbol_(self, symbol):
self.advance_lexer_()
if self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == symbol:
return symbol
- raise FeatureLibError("Expected '%s'" % symbol,
- self.cur_token_location_)
+ raise FeatureLibError("Expected '%s'" % symbol, self.cur_token_location_)
def expect_keyword_(self, keyword):
self.advance_lexer_()
if self.cur_token_type_ is Lexer.NAME and self.cur_token_ == keyword:
return self.cur_token_
- raise FeatureLibError("Expected \"%s\"" % keyword,
- self.cur_token_location_)
+ raise FeatureLibError('Expected "%s"' % keyword, self.cur_token_location_)
def expect_name_(self):
self.advance_lexer_()
@@ -1671,15 +1821,17 @@ class Parser(object):
self.advance_lexer_()
if self.cur_token_type_ in Lexer.NUMBERS:
return self.cur_token_
- raise FeatureLibError("Expected a decimal, hexadecimal or octal number",
- self.cur_token_location_)
+ raise FeatureLibError(
+ "Expected a decimal, hexadecimal or octal number", self.cur_token_location_
+ )
def expect_float_(self):
self.advance_lexer_()
if self.cur_token_type_ is Lexer.FLOAT:
return self.cur_token_
- raise FeatureLibError("Expected a floating-point number",
- self.cur_token_location_)
+ raise FeatureLibError(
+ "Expected a floating-point number", self.cur_token_location_
+ )
def expect_decipoint_(self):
if self.next_token_type_ == Lexer.FLOAT:
@@ -1687,8 +1839,9 @@ class Parser(object):
elif self.next_token_type_ is Lexer.NUMBER:
return self.expect_number_() / 10
else:
- raise FeatureLibError("Expected an integer or floating-point number",
- self.cur_token_location_)
+ raise FeatureLibError(
+ "Expected an integer or floating-point number", self.cur_token_location_
+ )
def expect_string_(self):
self.advance_lexer_()
@@ -1703,11 +1856,17 @@ class Parser(object):
return
else:
self.cur_token_type_, self.cur_token_, self.cur_token_location_ = (
- self.next_token_type_, self.next_token_, self.next_token_location_)
+ self.next_token_type_,
+ self.next_token_,
+ self.next_token_location_,
+ )
while True:
try:
- (self.next_token_type_, self.next_token_,
- self.next_token_location_) = next(self.lexer_)
+ (
+ self.next_token_type_,
+ self.next_token_,
+ self.next_token_location_,
+ ) = next(self.lexer_)
except StopIteration:
self.next_token_type_, self.next_token_ = (None, None)
if self.next_token_type_ != Lexer.COMMENT:
@@ -1717,14 +1876,15 @@ class Parser(object):
@staticmethod
def reverse_string_(s):
"""'abc' --> 'cba'"""
- return ''.join(reversed(list(s)))
+ return "".join(reversed(list(s)))
def make_cid_range_(self, location, start, limit):
"""(location, 999, 1001) --> ["cid00999", "cid01000", "cid01001"]"""
result = list()
if start > limit:
raise FeatureLibError(
- "Bad range: start should be less than limit", location)
+ "Bad range: start should be less than limit", location
+ )
for cid in range(start, limit + 1):
result.append("cid%05d" % cid)
return result
@@ -1734,45 +1894,45 @@ class Parser(object):
result = list()
if len(start) != len(limit):
raise FeatureLibError(
- "Bad range: \"%s\" and \"%s\" should have the same length" %
- (start, limit), location)
+ 'Bad range: "%s" and "%s" should have the same length' % (start, limit),
+ location,
+ )
rev = self.reverse_string_
prefix = os.path.commonprefix([start, limit])
suffix = rev(os.path.commonprefix([rev(start), rev(limit)]))
if len(suffix) > 0:
- start_range = start[len(prefix):-len(suffix)]
- limit_range = limit[len(prefix):-len(suffix)]
+ start_range = start[len(prefix) : -len(suffix)]
+ limit_range = limit[len(prefix) : -len(suffix)]
else:
- start_range = start[len(prefix):]
- limit_range = limit[len(prefix):]
+ start_range = start[len(prefix) :]
+ limit_range = limit[len(prefix) :]
if start_range >= limit_range:
raise FeatureLibError(
- "Start of range must be smaller than its end",
- location)
+ "Start of range must be smaller than its end", location
+ )
- uppercase = re.compile(r'^[A-Z]$')
+ uppercase = re.compile(r"^[A-Z]$")
if uppercase.match(start_range) and uppercase.match(limit_range):
for c in range(ord(start_range), ord(limit_range) + 1):
result.append("%s%c%s" % (prefix, c, suffix))
return result
- lowercase = re.compile(r'^[a-z]$')
+ lowercase = re.compile(r"^[a-z]$")
if lowercase.match(start_range) and lowercase.match(limit_range):
for c in range(ord(start_range), ord(limit_range) + 1):
result.append("%s%c%s" % (prefix, c, suffix))
return result
- digits = re.compile(r'^[0-9]{1,3}$')
+ digits = re.compile(r"^[0-9]{1,3}$")
if digits.match(start_range) and digits.match(limit_range):
for i in range(int(start_range, 10), int(limit_range, 10) + 1):
- number = ("000" + str(i))[-len(start_range):]
+ number = ("000" + str(i))[-len(start_range) :]
result.append("%s%s%s" % (prefix, number, suffix))
return result
- raise FeatureLibError("Bad range: \"%s-%s\"" % (start, limit),
- location)
+ raise FeatureLibError('Bad range: "%s-%s"' % (start, limit), location)
class SymbolTable(object):
diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py
index 5731f51c..a9d13ec6 100644
--- a/Lib/fontTools/otlLib/builder.py
+++ b/Lib/fontTools/otlLib/builder.py
@@ -88,9 +88,10 @@ def buildLookup(subtables, flags=0, markFilterSet=None):
subtables = [st for st in subtables if st is not None]
if not subtables:
return None
- assert all(t.LookupType == subtables[0].LookupType for t in subtables), \
- ("all subtables must have the same LookupType; got %s" %
- repr([t.LookupType for t in subtables]))
+ assert all(t.LookupType == subtables[0].LookupType for t in subtables), (
+ "all subtables must have the same LookupType; got %s"
+ % repr([t.LookupType for t in subtables])
+ )
self = ot.Lookup()
self.LookupType = subtables[0].LookupType
self.LookupFlag = flags
@@ -101,9 +102,10 @@ def buildLookup(subtables, flags=0, markFilterSet=None):
assert isinstance(markFilterSet, int), markFilterSet
self.MarkFilteringSet = markFilterSet
else:
- assert (self.LookupFlag & LOOKUP_FLAG_USE_MARK_FILTERING_SET) == 0, \
- ("if markFilterSet is None, flags must not set "
- "LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x%04x" % flags)
+ assert (self.LookupFlag & LOOKUP_FLAG_USE_MARK_FILTERING_SET) == 0, (
+ "if markFilterSet is None, flags must not set "
+ "LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x%04x" % flags
+ )
return self
@@ -118,13 +120,15 @@ class LookupBuilder(object):
self.lookupflag = 0
self.markFilterSet = None
self.lookup_index = None # assigned when making final tables
- assert table in ('GPOS', 'GSUB')
+ assert table in ("GPOS", "GSUB")
def equals(self, other):
- return (isinstance(other, self.__class__) and
- self.table == other.table and
- self.lookupflag == other.lookupflag and
- self.markFilterSet == other.markFilterSet)
+ return (
+ isinstance(other, self.__class__)
+ and self.table == other.table
+ and self.lookupflag == other.lookupflag
+ and self.markFilterSet == other.markFilterSet
+ )
def inferGlyphClasses(self):
"""Infers glyph glasses for the GDEF table, such as {"cedilla":3}."""
@@ -172,6 +176,13 @@ class LookupBuilder(object):
coverage = buildCoverage(g, self.glyphMap)
subtable.InputCoverage.append(coverage)
+ def setCoverage_(self, glyphs, subtable):
+ subtable.GlyphCount = len(glyphs)
+ subtable.Coverage = []
+ for g in glyphs:
+ coverage = buildCoverage(g, self.glyphMap)
+ subtable.Coverage.append(coverage)
+
def build_subst_subtables(self, mapping, klass):
substitutions = [{}]
for key in mapping:
@@ -190,10 +201,11 @@ class LookupBuilder(object):
original source which produced this break, or ``None`` if
no location is provided.
"""
- log.warning(OpenTypeLibError(
- 'unsupported "subtable" statement for lookup type',
- location
- ))
+ log.warning(
+ OpenTypeLibError(
+ 'unsupported "subtable" statement for lookup type', location
+ )
+ )
class AlternateSubstBuilder(LookupBuilder):
@@ -218,13 +230,13 @@ class AlternateSubstBuilder(LookupBuilder):
`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', 3)
+ LookupBuilder.__init__(self, font, location, "GSUB", 3)
self.alternates = OrderedDict()
def equals(self, other):
- return (LookupBuilder.equals(self, other) and
- self.alternates == other.alternates)
+ return LookupBuilder.equals(self, other) and self.alternates == other.alternates
def build(self):
"""Build the lookup.
@@ -233,8 +245,9 @@ class AlternateSubstBuilder(LookupBuilder):
An ``otTables.Lookup`` object representing the alternate
substitution lookup.
"""
- subtables = self.build_subst_subtables(self.alternates,
- buildAlternateSubstSubtable)
+ subtables = self.build_subst_subtables(
+ self.alternates, buildAlternateSubstSubtable
+ )
return self.buildLookup_(subtables)
def getAlternateGlyphs(self):
@@ -244,10 +257,78 @@ class AlternateSubstBuilder(LookupBuilder):
self.alternates[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_
+class ChainContextualRule(
+ namedtuple("ChainContextualRule", ["prefix", "glyphs", "suffix", "lookups"])
+):
+ @property
+ def is_subtable_break(self):
+ return self.prefix == LookupBuilder.SUBTABLE_BREAK_
+
+
+class ChainContextualRuleset:
+ def __init__(self):
+ self.rules = []
+
+ def addRule(self, rule):
+ self.rules.append(rule)
+
+ @property
+ def hasPrefixOrSuffix(self):
+ # Do we have any prefixes/suffixes? If this is False for all
+ # rulesets, we can express the whole lookup as GPOS5/GSUB7.
+ for rule in self.rules:
+ if len(rule.prefix) > 0 or len(rule.suffix) > 0:
+ return True
+ return False
+
+ @property
+ def hasAnyGlyphClasses(self):
+ # Do we use glyph classes anywhere in the rules? If this is False
+ # we can express this subtable as a Format 1.
+ for rule in self.rules:
+ for coverage in (rule.prefix, rule.glyphs, rule.suffix):
+ if any(len(x) > 1 for x in coverage):
+ return True
+ return False
+
+ def format2ClassDefs(self):
+ PREFIX, GLYPHS, SUFFIX = 0, 1, 2
+ classDefBuilders = []
+ for ix in [PREFIX, GLYPHS, SUFFIX]:
+ context = []
+ for r in self.rules:
+ context.append(r[ix])
+ classes = self._classBuilderForContext(context)
+ if not classes:
+ return None
+ classDefBuilders.append(classes)
+ return classDefBuilders
+
+ def _classBuilderForContext(self, context):
+ classdefbuilder = ClassDefBuilder(useClass0=False)
+ for position in context:
+ for glyphset in position:
+ if not classdefbuilder.canAdd(glyphset):
+ return None
+ classdefbuilder.add(glyphset)
+ return classdefbuilder
+
+
class ChainContextualBuilder(LookupBuilder):
def equals(self, other):
- return (LookupBuilder.equals(self, other) and
- self.rules == other.rules)
+ return LookupBuilder.equals(self, other) and self.rules == other.rules
+
+ def rulesets(self):
+ # Return a list of ChainContextRuleset objects, taking explicit
+ # subtable breaks into account
+ ruleset = [ChainContextualRuleset()]
+ for rule in self.rules:
+ if rule.is_subtable_break:
+ ruleset.append(ChainContextualRuleset())
+ continue
+ ruleset[-1].addRule(rule)
+ # Squish any empty subtables
+ return [x for x in ruleset if len(x.rules) > 0]
def build(self):
"""Build the lookup.
@@ -257,39 +338,99 @@ class ChainContextualBuilder(LookupBuilder):
contextual positioning lookup.
"""
subtables = []
- for (prefix, glyphs, suffix, lookups) in self.rules:
- if prefix == self.SUBTABLE_BREAK_:
- continue
- st = self.newSubtable_()
- subtables.append(st)
- st.Format = 3
- self.setBacktrackCoverage_(prefix, st)
- self.setLookAheadCoverage_(suffix, st)
- self.setInputCoverage_(glyphs, st)
-
- for sequenceIndex, lookupList in enumerate(lookups):
- if lookupList is not None:
- if not isinstance(lookupList, list):
- # Can happen with synthesised lookups
- lookupList = [ lookupList ]
- for l in lookupList:
- if l.lookup_index is None:
- if isinstance(self, ChainContextPosBuilder):
- other = "substitution"
- else:
- other = "positioning"
- raise OpenTypeLibError('Missing index of the specified '
- f'lookup, might be a {other} lookup',
- self.location)
- rec = self.newLookupRecord_()
- rec.SequenceIndex = sequenceIndex
- rec.LookupListIndex = l.lookup_index
- self.addLookupRecordToSubtable_(st, rec)
+ chaining = False
+ rulesets = self.rulesets()
+ chaining = any(ruleset.hasPrefixOrSuffix for ruleset in rulesets)
+ for ruleset in rulesets:
+ for rule in ruleset.rules:
+ subtables.append(self.buildFormat3Subtable(rule, chaining))
+ # If we are not chaining, lookup type will be automatically fixed by
+ # buildLookup_
return self.buildLookup_(subtables)
+ def buildFormat3Subtable(self, rule, chaining=True):
+ st = self.newSubtable_(chaining=chaining)
+ st.Format = 3
+ if chaining:
+ self.setBacktrackCoverage_(rule.prefix, st)
+ self.setLookAheadCoverage_(rule.suffix, st)
+ self.setInputCoverage_(rule.glyphs, st)
+ else:
+ self.setCoverage_(rule.glyphs, st)
+
+ for sequenceIndex, lookupList in enumerate(rule.lookups):
+ if lookupList is not None:
+ if not isinstance(lookupList, list):
+ # Can happen with synthesised lookups
+ lookupList = [lookupList]
+ for l in lookupList:
+ if l.lookup_index is None:
+ if isinstance(self, ChainContextPosBuilder):
+ other = "substitution"
+ else:
+ other = "positioning"
+ raise OpenTypeLibError(
+ "Missing index of the specified "
+ f"lookup, might be a {other} lookup",
+ self.location,
+ )
+ rec = self.newLookupRecord_(st)
+ rec.SequenceIndex = sequenceIndex
+ rec.LookupListIndex = l.lookup_index
+ return st
+
def add_subtable_break(self, location):
- self.rules.append((self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_,
- self.SUBTABLE_BREAK_, [self.SUBTABLE_BREAK_]))
+ self.rules.append(
+ ChainContextualRule(
+ self.SUBTABLE_BREAK_,
+ self.SUBTABLE_BREAK_,
+ self.SUBTABLE_BREAK_,
+ [self.SUBTABLE_BREAK_],
+ )
+ )
+
+ def newSubtable_(self, chaining=True):
+ subtablename = f"Context{self.subtable_type}"
+ if chaining:
+ subtablename = "Chain" + subtablename
+ st = getattr(ot, subtablename)() # ot.ChainContextPos()/ot.ChainSubst()/etc.
+ setattr(st, f"{self.subtable_type}Count", 0)
+ setattr(st, f"{self.subtable_type}LookupRecord", [])
+ return st
+
+ def attachSubtableWithCount_(
+ self, st, subtable_name, count_name, existing=None, index=None, chaining=False
+ ):
+ if chaining:
+ subtable_name = "Chain" + subtable_name
+ count_name = "Chain" + count_name
+
+ if not hasattr(st, count_name):
+ setattr(st, count_name, 0)
+ setattr(st, subtable_name, [])
+
+ if existing:
+ new_subtable = existing
+ else:
+ # Create a new, empty subtable from otTables
+ new_subtable = getattr(ot, subtable_name)()
+
+ setattr(st, count_name, getattr(st, count_name) + 1)
+
+ if index:
+ getattr(st, subtable_name).insert(index, new_subtable)
+ else:
+ getattr(st, subtable_name).append(new_subtable)
+
+ return new_subtable
+
+ def newLookupRecord_(self, st):
+ return self.attachSubtableWithCount_(
+ st,
+ f"{self.subtable_type}LookupRecord",
+ f"{self.subtable_type}Count",
+ chaining=False,
+ ) # Oddly, it isn't ChainSubstLookupRecord
class ChainContextPosBuilder(ChainContextualBuilder):
@@ -318,22 +459,11 @@ class ChainContextPosBuilder(ChainContextualBuilder):
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
flags.
"""
- def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, 'GPOS', 8)
- self.rules = [] # (prefix, input, suffix, lookups)
-
- def newSubtable_(self):
- st = ot.ChainContextPos()
- st.PosCount = 0
- st.PosLookupRecord = []
- return st
- def newLookupRecord_(self):
- return ot.PosLookupRecord()
-
- def addLookupRecordToSubtable_(self, st, rec):
- st.PosCount += 1
- st.PosLookupRecord.append(rec)
+ def __init__(self, font, location):
+ LookupBuilder.__init__(self, font, location, "GPOS", 8)
+ self.rules = []
+ self.subtable_type = "Pos"
def find_chainable_single_pos(self, lookups, glyphs, value):
"""Helper for add_single_pos_chained_()"""
@@ -341,8 +471,9 @@ class ChainContextPosBuilder(ChainContextualBuilder):
for lookup in lookups[::-1]:
if lookup == self.SUBTABLE_BREAK_:
return res
- if isinstance(lookup, SinglePosBuilder) and \
- all(lookup.can_add(glyph, value) for glyph in glyphs):
+ if isinstance(lookup, SinglePosBuilder) and all(
+ lookup.can_add(glyph, value) for glyph in glyphs
+ ):
res = lookup
return res
@@ -373,29 +504,18 @@ class ChainContextSubstBuilder(ChainContextualBuilder):
`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', 6)
+ LookupBuilder.__init__(self, font, location, "GSUB", 6)
self.rules = [] # (prefix, input, suffix, lookups)
-
- def newSubtable_(self):
- st = ot.ChainContextSubst()
- st.SubstCount = 0
- st.SubstLookupRecord = []
- return st
-
- def newLookupRecord_(self):
- return ot.SubstLookupRecord()
-
- def addLookupRecordToSubtable_(self, st, rec):
- st.SubstCount += 1
- st.SubstLookupRecord.append(rec)
+ self.subtable_type = "Subst"
def getAlternateGlyphs(self):
result = {}
- for (prefix, _, _, lookuplist) in self.rules:
- if prefix == self.SUBTABLE_BREAK_:
+ for rule in self.rules:
+ if rule.is_subtable_break:
continue
- for lookups in lookuplist:
+ for lookups in rule.lookups:
if not isinstance(lookups, list):
lookups = [lookups]
for lookup in lookups:
@@ -408,12 +528,13 @@ class ChainContextSubstBuilder(ChainContextualBuilder):
def find_chainable_single_subst(self, glyphs):
"""Helper for add_single_subst_chained_()"""
res = None
- for prefix, _, _, rules in self.rules[::-1]:
- if prefix == self.SUBTABLE_BREAK_:
+ for rule in self.rules[::-1]:
+ if rule.is_subtable_break:
return res
- for sub in rules:
- if (isinstance(sub, SingleSubstBuilder) and
- not any(g in glyphs for g in sub.mapping.keys())):
+ for sub in rule.lookups:
+ if isinstance(sub, SingleSubstBuilder) and not any(
+ g in glyphs for g in sub.mapping.keys()
+ ):
res = sub
return res
@@ -440,13 +561,13 @@ class LigatureSubstBuilder(LookupBuilder):
`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', 4)
+ LookupBuilder.__init__(self, font, location, "GSUB", 4)
self.ligatures = OrderedDict() # {('f','f','i'): 'f_f_i'}
def equals(self, other):
- return (LookupBuilder.equals(self, other) and
- self.ligatures == other.ligatures)
+ return LookupBuilder.equals(self, other) and self.ligatures == other.ligatures
def build(self):
"""Build the lookup.
@@ -455,8 +576,9 @@ class LigatureSubstBuilder(LookupBuilder):
An ``otTables.Lookup`` object representing the ligature
substitution lookup.
"""
- subtables = self.build_subst_subtables(self.ligatures,
- buildLigatureSubstSubtable)
+ subtables = self.build_subst_subtables(
+ self.ligatures, buildLigatureSubstSubtable
+ )
return self.buildLookup_(subtables)
def add_subtable_break(self, location):
@@ -485,17 +607,16 @@ class MultipleSubstBuilder(LookupBuilder):
`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', 2)
+ LookupBuilder.__init__(self, font, location, "GSUB", 2)
self.mapping = OrderedDict()
def equals(self, other):
- return (LookupBuilder.equals(self, other) and
- self.mapping == other.mapping)
+ return LookupBuilder.equals(self, other) and self.mapping == other.mapping
def build(self):
- subtables = self.build_subst_subtables(self.mapping,
- buildMultipleSubstSubtable)
+ subtables = self.build_subst_subtables(self.mapping, buildMultipleSubstSubtable)
return self.buildLookup_(subtables)
def add_subtable_break(self, location):
@@ -518,13 +639,15 @@ class CursivePosBuilder(LookupBuilder):
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
flags.
"""
+
def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, 'GPOS', 3)
+ LookupBuilder.__init__(self, font, location, "GPOS", 3)
self.attachments = {}
def equals(self, other):
- return (LookupBuilder.equals(self, other) and
- self.attachments == other.attachments)
+ return (
+ LookupBuilder.equals(self, other) and self.attachments == other.attachments
+ )
def add_attachment(self, location, glyphs, entryAnchor, exitAnchor):
"""Adds attachment information to the cursive positioning lookup.
@@ -580,15 +703,18 @@ class MarkBasePosBuilder(LookupBuilder):
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
flags.
"""
+
def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, 'GPOS', 4)
+ LookupBuilder.__init__(self, font, location, "GPOS", 4)
self.marks = {} # glyphName -> (markClassName, anchor)
self.bases = {} # glyphName -> {markClassName: anchor}
def equals(self, other):
- return (LookupBuilder.equals(self, other) and
- self.marks == other.marks and
- self.bases == other.bases)
+ return (
+ LookupBuilder.equals(self, other)
+ and self.marks == other.marks
+ and self.bases == other.bases
+ )
def inferGlyphClasses(self):
result = {glyph: 1 for glyph in self.bases}
@@ -603,12 +729,12 @@ class MarkBasePosBuilder(LookupBuilder):
positioning lookup.
"""
markClasses = self.buildMarkClasses_(self.marks)
- marks = {mark: (markClasses[mc], anchor)
- for mark, (mc, anchor) in self.marks.items()}
+ marks = {
+ mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items()
+ }
bases = {}
for glyph, anchors in self.bases.items():
- bases[glyph] = {markClasses[mc]: anchor
- for (mc, anchor) in anchors.items()}
+ bases[glyph] = {markClasses[mc]: anchor for (mc, anchor) in anchors.items()}
subtables = buildMarkBasePos(marks, bases, self.glyphMap)
return self.buildLookup_(subtables)
@@ -643,15 +769,18 @@ class MarkLigPosBuilder(LookupBuilder):
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
flags.
"""
+
def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, 'GPOS', 5)
+ LookupBuilder.__init__(self, font, location, "GPOS", 5)
self.marks = {} # glyphName -> (markClassName, anchor)
self.ligatures = {} # glyphName -> [{markClassName: anchor}, ...]
def equals(self, other):
- return (LookupBuilder.equals(self, other) and
- self.marks == other.marks and
- self.ligatures == other.ligatures)
+ return (
+ LookupBuilder.equals(self, other)
+ and self.marks == other.marks
+ and self.ligatures == other.ligatures
+ )
def inferGlyphClasses(self):
result = {glyph: 2 for glyph in self.ligatures}
@@ -666,8 +795,9 @@ class MarkLigPosBuilder(LookupBuilder):
positioning lookup.
"""
markClasses = self.buildMarkClasses_(self.marks)
- marks = {mark: (markClasses[mc], anchor)
- for mark, (mc, anchor) in self.marks.items()}
+ marks = {
+ mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items()
+ }
ligs = {}
for lig, components in self.ligatures.items():
ligs[lig] = []
@@ -703,15 +833,18 @@ class MarkMarkPosBuilder(LookupBuilder):
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
flags.
"""
+
def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, 'GPOS', 6)
- self.marks = {} # glyphName -> (markClassName, anchor)
+ LookupBuilder.__init__(self, font, location, "GPOS", 6)
+ self.marks = {} # glyphName -> (markClassName, anchor)
self.baseMarks = {} # glyphName -> {markClassName: anchor}
def equals(self, other):
- return (LookupBuilder.equals(self, other) and
- self.marks == other.marks and
- self.baseMarks == other.baseMarks)
+ return (
+ LookupBuilder.equals(self, other)
+ and self.marks == other.marks
+ and self.baseMarks == other.baseMarks
+ )
def inferGlyphClasses(self):
result = {glyph: 3 for glyph in self.baseMarks}
@@ -727,8 +860,9 @@ class MarkMarkPosBuilder(LookupBuilder):
"""
markClasses = self.buildMarkClasses_(self.marks)
markClassList = sorted(markClasses.keys(), key=markClasses.get)
- marks = {mark: (markClasses[mc], anchor)
- for mark, (mc, anchor) in self.marks.items()}
+ marks = {
+ mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items()
+ }
st = ot.MarkMarkPos()
st.Format = 1
@@ -770,13 +904,13 @@ class ReverseChainSingleSubstBuilder(LookupBuilder):
`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', 8)
+ LookupBuilder.__init__(self, font, location, "GSUB", 8)
self.rules = [] # (prefix, suffix, mapping)
def equals(self, other):
- return (LookupBuilder.equals(self, other) and
- self.rules == other.rules)
+ return LookupBuilder.equals(self, other) and self.rules == other.rules
def build(self):
"""Build the lookup.
@@ -823,13 +957,13 @@ class SingleSubstBuilder(LookupBuilder):
`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', 1)
+ LookupBuilder.__init__(self, font, location, "GSUB", 1)
self.mapping = OrderedDict()
def equals(self, other):
- return (LookupBuilder.equals(self, other) and
- self.mapping == other.mapping)
+ return LookupBuilder.equals(self, other) and self.mapping == other.mapping
def build(self):
"""Build the lookup.
@@ -838,8 +972,7 @@ class SingleSubstBuilder(LookupBuilder):
An ``otTables.Lookup`` object representing the multiple
substitution lookup.
"""
- subtables = self.build_subst_subtables(self.mapping,
- buildSingleSubstSubtable)
+ subtables = self.build_subst_subtables(self.mapping, buildSingleSubstSubtable)
return self.buildLookup_(subtables)
def getAlternateGlyphs(self):
@@ -859,6 +992,7 @@ class ClassPairPosSubtableBuilder(object):
Attributes:
builder (PairPosBuilder): A pair positioning lookup builder.
"""
+
def __init__(self, builder):
self.builder_ = builder
self.classDef1_, self.classDef2_ = None, None
@@ -877,11 +1011,13 @@ class ClassPairPosSubtableBuilder(object):
value2: An ``otTables.ValueRecord`` object for the right glyph's
positioning.
"""
- mergeable = (not self.forceSubtableBreak_ and
- self.classDef1_ is not None and
- self.classDef1_.canAdd(gc1) and
- self.classDef2_ is not None and
- self.classDef2_.canAdd(gc2))
+ mergeable = (
+ not self.forceSubtableBreak_
+ and self.classDef1_ is not None
+ and self.classDef1_.canAdd(gc1)
+ and self.classDef2_ is not None
+ and self.classDef2_.canAdd(gc2)
+ )
if not mergeable:
self.flush_()
self.classDef1_ = ClassDefBuilder(useClass0=True)
@@ -903,8 +1039,7 @@ class ClassPairPosSubtableBuilder(object):
def flush_(self):
if self.classDef1_ is None or self.classDef2_ is None:
return
- st = buildPairPosClassesSubtable(self.values_,
- self.builder_.glyphMap)
+ st = buildPairPosClassesSubtable(self.values_, self.builder_.glyphMap)
if st.Coverage is None:
return
self.subtables_.append(st)
@@ -930,8 +1065,9 @@ class PairPosBuilder(LookupBuilder):
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
flags.
"""
+
def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, 'GPOS', 2)
+ LookupBuilder.__init__(self, font, location, "GPOS", 2)
self.pairs = [] # [(gc1, value1, gc2, value2)*]
self.glyphPairs = {} # (glyph1, glyph2) --> (value1, value2)
self.locations = {} # (gc1, gc2) --> (filepath, line, column)
@@ -967,21 +1103,32 @@ class PairPosBuilder(LookupBuilder):
# by an 'enum' rule to be overridden by preceding single pairs
otherLoc = self.locations[key]
log.debug(
- 'Already defined position for pair %s %s at %s; '
- 'choosing the first value',
- glyph1, glyph2, otherLoc)
+ "Already defined position for pair %s %s at %s; "
+ "choosing the first value",
+ glyph1,
+ glyph2,
+ otherLoc,
+ )
else:
self.glyphPairs[key] = (value1, value2)
self.locations[key] = location
def add_subtable_break(self, location):
- self.pairs.append((self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_,
- self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_))
+ self.pairs.append(
+ (
+ self.SUBTABLE_BREAK_,
+ self.SUBTABLE_BREAK_,
+ self.SUBTABLE_BREAK_,
+ self.SUBTABLE_BREAK_,
+ )
+ )
def equals(self, other):
- return (LookupBuilder.equals(self, other) and
- self.glyphPairs == other.glyphPairs and
- self.pairs == other.pairs)
+ return (
+ LookupBuilder.equals(self, other)
+ and self.glyphPairs == other.glyphPairs
+ and self.pairs == other.pairs
+ )
def build(self):
"""Build the lookup.
@@ -1009,8 +1156,7 @@ class PairPosBuilder(LookupBuilder):
builder.addPair(glyphclass1, value1, glyphclass2, value2)
subtables = []
if self.glyphPairs:
- subtables.extend(
- buildPairPosGlyphs(self.glyphPairs, self.glyphMap))
+ subtables.extend(buildPairPosGlyphs(self.glyphPairs, self.glyphMap))
for key in sorted(builders.keys()):
subtables.extend(builders[key].subtables())
return self.buildLookup_(subtables)
@@ -1032,8 +1178,9 @@ class SinglePosBuilder(LookupBuilder):
`LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
flags.
"""
+
def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, 'GPOS', 1)
+ LookupBuilder.__init__(self, font, location, "GPOS", 1)
self.locations = {} # glyph -> (filename, line, column)
self.mapping = {} # glyph -> ot.ValueRecord
@@ -1052,7 +1199,8 @@ class SinglePosBuilder(LookupBuilder):
raise OpenTypeLibError(
'Already defined different position for glyph "%s" at %s'
% (glyph, otherLoc),
- location)
+ location,
+ )
if otValueRecord:
self.mapping[glyph] = otValueRecord
self.locations[glyph] = location
@@ -1063,8 +1211,7 @@ class SinglePosBuilder(LookupBuilder):
return curValue is None or curValue == value
def equals(self, other):
- return (LookupBuilder.equals(self, other) and
- self.mapping == other.mapping)
+ return LookupBuilder.equals(self, other) and self.mapping == other.mapping
def build(self):
"""Build the lookup.
@@ -1236,8 +1383,9 @@ def buildAnchor(x, y, point=None, deviceX=None, deviceY=None):
self.AnchorPoint = point
self.Format = 2
if deviceX is not None or deviceY is not None:
- assert self.Format == 1, \
- "Either point, or both of deviceX/deviceY, must be None."
+ assert (
+ self.Format == 1
+ ), "Either point, or both of deviceX/deviceY, must be None."
self.XDeviceTable = deviceX
self.YDeviceTable = deviceY
self.Format = 3
@@ -1375,8 +1523,8 @@ def buildDevice(deltas):
self.EndSize = endSize = max(keys)
assert 0 <= startSize <= endSize
self.DeltaValue = deltaValues = [
- deltas.get(size, 0)
- for size in range(startSize, endSize + 1)]
+ deltas.get(size, 0) for size in range(startSize, endSize + 1)
+ ]
maxDelta = max(deltaValues)
minDelta = min(deltaValues)
assert minDelta > -129 and maxDelta < 128
@@ -1666,8 +1814,7 @@ def _getValueFormat(f, values, i):
return mask
-def buildPairPosClassesSubtable(pairs, glyphMap,
- valueFormat1=None, valueFormat2=None):
+def buildPairPosClassesSubtable(pairs, glyphMap, valueFormat1=None, valueFormat2=None):
"""Builds a class pair adjustment (GPOS2 format 2) subtable.
Kerning tables are generally expressed as pair positioning tables using
@@ -1776,11 +1923,11 @@ def buildPairPosGlyphs(pairs, glyphMap):
pos[(glyphA, glyphB)] = (valA, valB)
return [
buildPairPosGlyphsSubtable(pos, glyphMap, formatA, formatB)
- for ((formatA, formatB), pos) in sorted(p.items())]
+ for ((formatA, formatB), pos) in sorted(p.items())
+ ]
-def buildPairPosGlyphsSubtable(pairs, glyphMap,
- valueFormat1=None, valueFormat2=None):
+def buildPairPosGlyphsSubtable(pairs, glyphMap, valueFormat1=None, valueFormat2=None):
"""Builds a single glyph-based pair adjustment (GPOS2 format 1) subtable.
This builds a PairPos subtable from a dictionary of glyph pairs and
@@ -1825,8 +1972,7 @@ def buildPairPosGlyphsSubtable(pairs, glyphMap,
ps = ot.PairSet()
ps.PairValueRecord = []
self.PairSet.append(ps)
- for glyph2, val1, val2 in \
- sorted(p[glyph], key=lambda x: glyphMap[x[0]]):
+ for glyph2, val1, val2 in sorted(p[glyph], key=lambda x: glyphMap[x[0]]):
pvr = ot.PairValueRecord()
pvr.SecondGlyph = glyph2
pvr.Value1 = val1 if val1 and val1.getFormat() != 0 else None
@@ -1998,7 +2144,7 @@ def _makeDeviceTuple(device):
device.DeltaFormat,
device.StartSize,
device.EndSize,
- () if device.DeltaFormat & 0x8000 else tuple(device.DeltaValue)
+ () if device.DeltaFormat & 0x8000 else tuple(device.DeltaValue),
)
@@ -2012,6 +2158,7 @@ def _getSinglePosValueSize(valueKey):
count += 1
return count
+
def buildValue(value):
"""Builds a positioning value record.
@@ -2042,6 +2189,7 @@ def buildValue(value):
# GDEF
+
def buildAttachList(attachPoints, glyphMap):
"""Builds an AttachList subtable.
@@ -2061,8 +2209,7 @@ def buildAttachList(attachPoints, glyphMap):
return None
self = ot.AttachList()
self.Coverage = buildCoverage(attachPoints.keys(), glyphMap)
- self.AttachPoint = [buildAttachPoint(attachPoints[g])
- for g in self.Coverage.glyphs]
+ self.AttachPoint = [buildAttachPoint(attachPoints[g]) for g in self.Coverage.glyphs]
self.GlyphCount = len(self.AttachPoint)
return self
@@ -2191,6 +2338,7 @@ def buildMarkGlyphSetsDef(markSets, glyphMap):
class ClassDefBuilder(object):
"""Helper for building ClassDef tables."""
+
def __init__(self, useClass0):
self.classes_ = set()
self.glyphs_ = {}
@@ -2380,7 +2528,7 @@ def _buildAxisRecords(axes, nameTable):
axisValRec = ot.AxisValue()
axisValRec.AxisIndex = axisRecordIndex
axisValRec.Flags = axisVal.get("flags", 0)
- axisValRec.ValueNameID = _addName(nameTable, axisVal['name'])
+ axisValRec.ValueNameID = _addName(nameTable, axisVal["name"])
if "value" in axisVal:
axisValRec.Value = axisVal["value"]
@@ -2392,8 +2540,12 @@ def _buildAxisRecords(axes, nameTable):
elif "nominalValue" in axisVal:
axisValRec.Format = 2
axisValRec.NominalValue = axisVal["nominalValue"]
- axisValRec.RangeMinValue = axisVal.get("rangeMinValue", AXIS_VALUE_NEGATIVE_INFINITY)
- axisValRec.RangeMaxValue = axisVal.get("rangeMaxValue", AXIS_VALUE_POSITIVE_INFINITY)
+ axisValRec.RangeMinValue = axisVal.get(
+ "rangeMinValue", AXIS_VALUE_NEGATIVE_INFINITY
+ )
+ axisValRec.RangeMaxValue = axisVal.get(
+ "rangeMaxValue", AXIS_VALUE_POSITIVE_INFINITY
+ )
else:
raise ValueError("Can't determine format for AxisValue")
@@ -2410,7 +2562,7 @@ def _buildAxisValuesFormat4(locations, axes, nameTable):
for axisLocationDict in locations:
axisValRec = ot.AxisValue()
axisValRec.Format = 4
- axisValRec.ValueNameID = _addName(nameTable, axisLocationDict['name'])
+ axisValRec.ValueNameID = _addName(nameTable, axisLocationDict["name"])
axisValRec.Flags = axisLocationDict.get("flags", 0)
axisValueRecords = []
for tag, value in axisLocationDict["location"].items():
diff --git a/Lib/fontTools/otlLib/error.py b/Lib/fontTools/otlLib/error.py
index 177f2ea8..1cbef578 100644
--- a/Lib/fontTools/otlLib/error.py
+++ b/Lib/fontTools/otlLib/error.py
@@ -1,5 +1,3 @@
-
-
class OpenTypeLibError(Exception):
def __init__(self, message, location):
Exception.__init__(self, message)
diff --git a/Lib/fontTools/otlLib/maxContextCalc.py b/Lib/fontTools/otlLib/maxContextCalc.py
index 40c3d6db..03e7561b 100644
--- a/Lib/fontTools/otlLib/maxContextCalc.py
+++ b/Lib/fontTools/otlLib/maxContextCalc.py
@@ -1,12 +1,11 @@
-
-__all__ = ['maxCtxFont']
+__all__ = ["maxCtxFont"]
def maxCtxFont(font):
"""Calculate the usMaxContext value for an entire font."""
maxCtx = 0
- for tag in ('GSUB', 'GPOS'):
+ for tag in ("GSUB", "GPOS"):
if tag not in font:
continue
table = font[tag].table
@@ -24,62 +23,59 @@ def maxCtxSubtable(maxCtx, tag, lookupType, st):
"""
# single positioning, single / multiple substitution
- if (tag == 'GPOS' and lookupType == 1) or (
- tag == 'GSUB' and lookupType in (1, 2, 3)):
+ if (tag == "GPOS" and lookupType == 1) or (
+ tag == "GSUB" and lookupType in (1, 2, 3)
+ ):
maxCtx = max(maxCtx, 1)
# pair positioning
- elif tag == 'GPOS' and lookupType == 2:
+ elif tag == "GPOS" and lookupType == 2:
maxCtx = max(maxCtx, 2)
# ligatures
- elif tag == 'GSUB' and lookupType == 4:
+ elif tag == "GSUB" and lookupType == 4:
for ligatures in st.ligatures.values():
for ligature in ligatures:
maxCtx = max(maxCtx, ligature.CompCount)
# context
- elif (tag == 'GPOS' and lookupType == 7) or (
- tag == 'GSUB' and lookupType == 5):
- maxCtx = maxCtxContextualSubtable(
- maxCtx, st, 'Pos' if tag == 'GPOS' else 'Sub')
+ elif (tag == "GPOS" and lookupType == 7) or (tag == "GSUB" and lookupType == 5):
+ maxCtx = maxCtxContextualSubtable(maxCtx, st, "Pos" if tag == "GPOS" else "Sub")
# chained context
- elif (tag == 'GPOS' and lookupType == 8) or (
- tag == 'GSUB' and lookupType == 6):
+ elif (tag == "GPOS" and lookupType == 8) or (tag == "GSUB" and lookupType == 6):
maxCtx = maxCtxContextualSubtable(
- maxCtx, st, 'Pos' if tag == 'GPOS' else 'Sub', 'Chain')
+ maxCtx, st, "Pos" if tag == "GPOS" else "Sub", "Chain"
+ )
# extensions
- elif (tag == 'GPOS' and lookupType == 9) or (
- tag == 'GSUB' and lookupType == 7):
- maxCtx = maxCtxSubtable(
- maxCtx, tag, st.ExtensionLookupType, st.ExtSubTable)
+ elif (tag == "GPOS" and lookupType == 9) or (tag == "GSUB" and lookupType == 7):
+ maxCtx = maxCtxSubtable(maxCtx, tag, st.ExtensionLookupType, st.ExtSubTable)
# reverse-chained context
- elif tag == 'GSUB' and lookupType == 8:
- maxCtx = maxCtxContextualRule(maxCtx, st, 'Reverse')
+ elif tag == "GSUB" and lookupType == 8:
+ maxCtx = maxCtxContextualRule(maxCtx, st, "Reverse")
return maxCtx
-def maxCtxContextualSubtable(maxCtx, st, ruleType, chain=''):
+def maxCtxContextualSubtable(maxCtx, st, ruleType, chain=""):
"""Calculate usMaxContext based on a contextual feature subtable."""
if st.Format == 1:
- for ruleset in getattr(st, '%s%sRuleSet' % (chain, ruleType)):
+ for ruleset in getattr(st, "%s%sRuleSet" % (chain, ruleType)):
if ruleset is None:
continue
- for rule in getattr(ruleset, '%s%sRule' % (chain, ruleType)):
+ for rule in getattr(ruleset, "%s%sRule" % (chain, ruleType)):
if rule is None:
continue
maxCtx = maxCtxContextualRule(maxCtx, rule, chain)
elif st.Format == 2:
- for ruleset in getattr(st, '%s%sClassSet' % (chain, ruleType)):
+ for ruleset in getattr(st, "%s%sClassSet" % (chain, ruleType)):
if ruleset is None:
continue
- for rule in getattr(ruleset, '%s%sClassRule' % (chain, ruleType)):
+ for rule in getattr(ruleset, "%s%sClassRule" % (chain, ruleType)):
if rule is None:
continue
maxCtx = maxCtxContextualRule(maxCtx, rule, chain)
@@ -95,6 +91,6 @@ def maxCtxContextualRule(maxCtx, st, chain):
if not chain:
return max(maxCtx, st.GlyphCount)
- elif chain == 'Reverse':
+ elif chain == "Reverse":
return max(maxCtx, st.GlyphCount + st.LookAheadGlyphCount)
return max(maxCtx, st.InputGlyphCount + st.LookAheadGlyphCount)
diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py
index 88636be8..aaa22f94 100644
--- a/Lib/fontTools/subset/__init__.py
+++ b/Lib/fontTools/subset/__init__.py
@@ -2,7 +2,6 @@
#
# Google Author(s): Behdad Esfahbod
-from fontTools.misc.py23 import *
from fontTools.misc.fixedTools import otRound
from fontTools import ttLib
from fontTools.ttLib.tables import otTables
@@ -1635,9 +1634,13 @@ def prune_post_subset(self, font, options):
# table.ScriptList = None
if hasattr(table, 'FeatureVariations'):
- if not (table.FeatureList and table.FeatureVariations.FeatureVariationRecord):
+ # drop FeatureVariations if there are no features to substitute
+ if table.FeatureVariations and not (
+ table.FeatureList and table.FeatureVariations.FeatureVariationRecord
+ ):
table.FeatureVariations = None
+ # downgrade table version if there are no FeatureVariations
if not table.FeatureVariations and table.Version == 0x00010001:
table.Version = 0x00010000
diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
index a6cd1fc4..e12969e4 100644
--- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py
+++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
@@ -650,6 +650,7 @@ class Glyph(object):
assert self.isComposite()
nContours = 0
nPoints = 0
+ initialMaxComponentDepth = maxComponentDepth
for compo in self.components:
baseGlyph = glyfTable[compo.glyphName]
if baseGlyph.numberOfContours == 0:
@@ -657,8 +658,9 @@ class Glyph(object):
elif baseGlyph.numberOfContours > 0:
nP, nC = baseGlyph.getMaxpValues()
else:
- nP, nC, maxComponentDepth = baseGlyph.getCompositeMaxpValues(
- glyfTable, maxComponentDepth + 1)
+ nP, nC, componentDepth = baseGlyph.getCompositeMaxpValues(
+ glyfTable, initialMaxComponentDepth + 1)
+ maxComponentDepth = max(maxComponentDepth, componentDepth)
nPoints = nPoints + nP
nContours = nContours + nC
return CompositeMaxpValues(nPoints, nContours, maxComponentDepth)
diff --git a/Lib/fontTools/ufoLib/filenames.py b/Lib/fontTools/ufoLib/filenames.py
index e8e4404b..2815469f 100644
--- a/Lib/fontTools/ufoLib/filenames.py
+++ b/Lib/fontTools/ufoLib/filenames.py
@@ -1,6 +1,6 @@
"""
User name to file name conversion.
-This was taken form the UFO 3 spec.
+This was taken from the UFO 3 spec.
"""
illegalCharacters = r"\" * + / : < > ? [ \ ] | \0".split(" ")
diff --git a/Lib/fontTools/varLib/models.py b/Lib/fontTools/varLib/models.py
index eddf3b21..9cc40b1c 100644
--- a/Lib/fontTools/varLib/models.py
+++ b/Lib/fontTools/varLib/models.py
@@ -444,10 +444,10 @@ def main(args=None):
configLogger(level=args.loglevel)
from pprint import pprint
- if args.designspacefile:
+ if args.designspace:
from fontTools.designspaceLib import DesignSpaceDocument
doc = DesignSpaceDocument()
- doc.read(args.designspacefile)
+ doc.read(args.designspace)
locs = [s.location for s in doc.sources]
print("Original locations:")
pprint(locs)
diff --git a/Lib/fonttools.egg-info/PKG-INFO b/Lib/fonttools.egg-info/PKG-INFO
index 649d39d9..c2bfa255 100644
--- a/Lib/fonttools.egg-info/PKG-INFO
+++ b/Lib/fonttools.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fonttools
-Version: 4.13.0
+Version: 4.14.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -21,8 +21,9 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
licence <LICENSE>`__.
| Among other things this means you can use it free of charge.
- `User documentation <https://fonttools.readthedocs.io/en/latest/>` and
- `developer documentation <https://fonttools.readthedocs.io/en/latest/developer.html>` are available at `Read the Docs <https://fonttools.readthedocs.io/>`.
+ `User documentation <https://fonttools.readthedocs.io/en/latest/>`_ and
+ `developer documentation <https://fonttools.readthedocs.io/en/latest/developer.html>`_
+ are available at `Read the Docs <https://fonttools.readthedocs.io/>`_.
Installation
~~~~~~~~~~~~
@@ -253,6 +254,20 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
Changelog
~~~~~~~~~
+ 4.14.0 (released 2020-08-19)
+ ----------------------------
+
+ - [feaLib] Allow anonymous classes in LookupFlags definitions (#2037).
+ - [Docs] Better document DesignSpace rules processing order (#2041).
+ - [ttLib] Fixed 21-year old bug in ``maxp.maxComponentDepth`` calculation (#2044,
+ #2045).
+ - [varLib.models] Fixed misspelled argument name in CLI entry point (81d0042a).
+ - [subset] When subsetting GSUB v1.1, fixed TypeError by checking whether the
+ optional FeatureVariations table is present (e63ecc5b).
+ - [Snippets] Added snippet to show how to decompose glyphs in a TTF (#2030).
+ - [otlLib] Generate GSUB type 5 and GPOS type 7 contextual lookups where appropriate
+ (#2016).
+
4.13.0 (released 2020-07-10)
----------------------------
@@ -2006,13 +2021,13 @@ Classifier: Topic :: Text Processing :: Fonts
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
Requires-Python: >=3.6
-Provides-Extra: type1
-Provides-Extra: lxml
-Provides-Extra: graphite
-Provides-Extra: symfont
+Provides-Extra: ufo
+Provides-Extra: unicode
Provides-Extra: interpolatable
Provides-Extra: plot
-Provides-Extra: unicode
+Provides-Extra: symfont
Provides-Extra: all
+Provides-Extra: lxml
Provides-Extra: woff
-Provides-Extra: ufo
+Provides-Extra: type1
+Provides-Extra: graphite
diff --git a/Lib/fonttools.egg-info/SOURCES.txt b/Lib/fonttools.egg-info/SOURCES.txt
index 74a75910..9fd87b8f 100644
--- a/Lib/fonttools.egg-info/SOURCES.txt
+++ b/Lib/fonttools.egg-info/SOURCES.txt
@@ -402,6 +402,7 @@ MetaTools/roundTrip.py
Snippets/README.md
Snippets/checksum.py
Snippets/cmap-format.py
+Snippets/decompose-ttf.py
Snippets/dump_woff_metadata.py
Snippets/edit_raw_table_data.py
Snippets/fix-dflt-langsys.py
@@ -610,6 +611,8 @@ Tests/feaLib/data/PairPosSubtable.fea
Tests/feaLib/data/PairPosSubtable.ttx
Tests/feaLib/data/SingleSubstSubtable.fea
Tests/feaLib/data/SingleSubstSubtable.ttx
+Tests/feaLib/data/SubstSubtable.fea
+Tests/feaLib/data/SubstSubtable.ttx
Tests/feaLib/data/ZeroValue_ChainSinglePos_horizontal.fea
Tests/feaLib/data/ZeroValue_ChainSinglePos_horizontal.ttx
Tests/feaLib/data/ZeroValue_ChainSinglePos_vertical.fea
@@ -2051,8 +2054,8 @@ Tests/varLib/data/test_results/InterpolateLayoutGPOS_5_diff.ttx
Tests/varLib/data/test_results/InterpolateLayoutGPOS_5_same.ttx
Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_diff.ttx
Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_same.ttx
-Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx
-Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx
+Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_diff.ttx
+Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_same.ttx
Tests/varLib/data/test_results/InterpolateLayoutGPOS_size_feat_same.ttx
Tests/varLib/data/test_results/InterpolateLayoutMain.ttx
Tests/varLib/data/test_results/InterpolateTestCFF2VF.ttx
diff --git a/METADATA b/METADATA
index dc735319..87611a58 100644
--- a/METADATA
+++ b/METADATA
@@ -7,13 +7,13 @@ third_party {
}
url {
type: ARCHIVE
- value: "https://github.com/fonttools/fonttools/releases/download/4.13.0/fonttools-4.13.0.zip"
+ value: "https://github.com/fonttools/fonttools/releases/download/4.14.0/fonttools-4.14.0.zip"
}
- version: "4.13.0"
+ version: "4.14.0"
license_type: NOTICE
last_upgrade_date {
year: 2020
- month: 7
- day: 10
+ month: 8
+ day: 19
}
}
diff --git a/NEWS.rst b/NEWS.rst
index b750958a..31cb76bf 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,17 @@
+4.14.0 (released 2020-08-19)
+----------------------------
+
+- [feaLib] Allow anonymous classes in LookupFlags definitions (#2037).
+- [Docs] Better document DesignSpace rules processing order (#2041).
+- [ttLib] Fixed 21-year old bug in ``maxp.maxComponentDepth`` calculation (#2044,
+ #2045).
+- [varLib.models] Fixed misspelled argument name in CLI entry point (81d0042a).
+- [subset] When subsetting GSUB v1.1, fixed TypeError by checking whether the
+ optional FeatureVariations table is present (e63ecc5b).
+- [Snippets] Added snippet to show how to decompose glyphs in a TTF (#2030).
+- [otlLib] Generate GSUB type 5 and GPOS type 7 contextual lookups where appropriate
+ (#2016).
+
4.13.0 (released 2020-07-10)
----------------------------
diff --git a/PKG-INFO b/PKG-INFO
index 649d39d9..c2bfa255 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fonttools
-Version: 4.13.0
+Version: 4.14.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -21,8 +21,9 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
licence <LICENSE>`__.
| Among other things this means you can use it free of charge.
- `User documentation <https://fonttools.readthedocs.io/en/latest/>` and
- `developer documentation <https://fonttools.readthedocs.io/en/latest/developer.html>` are available at `Read the Docs <https://fonttools.readthedocs.io/>`.
+ `User documentation <https://fonttools.readthedocs.io/en/latest/>`_ and
+ `developer documentation <https://fonttools.readthedocs.io/en/latest/developer.html>`_
+ are available at `Read the Docs <https://fonttools.readthedocs.io/>`_.
Installation
~~~~~~~~~~~~
@@ -253,6 +254,20 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
Changelog
~~~~~~~~~
+ 4.14.0 (released 2020-08-19)
+ ----------------------------
+
+ - [feaLib] Allow anonymous classes in LookupFlags definitions (#2037).
+ - [Docs] Better document DesignSpace rules processing order (#2041).
+ - [ttLib] Fixed 21-year old bug in ``maxp.maxComponentDepth`` calculation (#2044,
+ #2045).
+ - [varLib.models] Fixed misspelled argument name in CLI entry point (81d0042a).
+ - [subset] When subsetting GSUB v1.1, fixed TypeError by checking whether the
+ optional FeatureVariations table is present (e63ecc5b).
+ - [Snippets] Added snippet to show how to decompose glyphs in a TTF (#2030).
+ - [otlLib] Generate GSUB type 5 and GPOS type 7 contextual lookups where appropriate
+ (#2016).
+
4.13.0 (released 2020-07-10)
----------------------------
@@ -2006,13 +2021,13 @@ Classifier: Topic :: Text Processing :: Fonts
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
Requires-Python: >=3.6
-Provides-Extra: type1
-Provides-Extra: lxml
-Provides-Extra: graphite
-Provides-Extra: symfont
+Provides-Extra: ufo
+Provides-Extra: unicode
Provides-Extra: interpolatable
Provides-Extra: plot
-Provides-Extra: unicode
+Provides-Extra: symfont
Provides-Extra: all
+Provides-Extra: lxml
Provides-Extra: woff
-Provides-Extra: ufo
+Provides-Extra: type1
+Provides-Extra: graphite
diff --git a/README.rst b/README.rst
index abcaa2f3..7ee99331 100644
--- a/README.rst
+++ b/README.rst
@@ -11,8 +11,9 @@ What is this?
licence <LICENSE>`__.
| Among other things this means you can use it free of charge.
-`User documentation <https://fonttools.readthedocs.io/en/latest/>` and
-`developer documentation <https://fonttools.readthedocs.io/en/latest/developer.html>` are available at `Read the Docs <https://fonttools.readthedocs.io/>`.
+`User documentation <https://fonttools.readthedocs.io/en/latest/>`_ and
+`developer documentation <https://fonttools.readthedocs.io/en/latest/developer.html>`_
+are available at `Read the Docs <https://fonttools.readthedocs.io/>`_.
Installation
~~~~~~~~~~~~
diff --git a/Snippets/decompose-ttf.py b/Snippets/decompose-ttf.py
new file mode 100755
index 00000000..bccaf72e
--- /dev/null
+++ b/Snippets/decompose-ttf.py
@@ -0,0 +1,53 @@
+#! /usr/bin/env python3
+
+# Example script to decompose the composite glyphs in a TTF into
+# non-composite outlines.
+
+
+import sys
+from fontTools.ttLib import TTFont
+from fontTools.pens.recordingPen import DecomposingRecordingPen
+from fontTools.pens.ttGlyphPen import TTGlyphPen
+
+try:
+ import pathops
+except ImportError:
+ sys.exit(
+ "This script requires the skia-pathops module. "
+ "`pip install skia-pathops` and then retry."
+ )
+
+
+if len(sys.argv) != 3:
+ print("usage: decompose-ttf.py fontfile.ttf outfile.ttf")
+ sys.exit(1)
+
+src = sys.argv[1]
+dst = sys.argv[2]
+
+with TTFont(src) as f:
+ glyfTable = f["glyf"]
+ glyphSet = f.getGlyphSet()
+
+ for glyphName in glyphSet.keys():
+ if not glyfTable[glyphName].isComposite():
+ continue
+
+ # record TTGlyph outlines without components
+ dcPen = DecomposingRecordingPen(glyphSet)
+ glyphSet[glyphName].draw(dcPen)
+
+ # replay recording onto a skia-pathops Path
+ path = pathops.Path()
+ pathPen = path.getPen()
+ dcPen.replay(pathPen)
+
+ # remove overlaps
+ path.simplify()
+
+ # create new TTGlyph from Path
+ ttPen = TTGlyphPen(None)
+ path.draw(ttPen)
+ glyfTable[glyphName] = ttPen.glyph()
+
+ f.save(dst)
diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py
index 512f6082..5a8c562d 100644
--- a/Tests/feaLib/builder_test.py
+++ b/Tests/feaLib/builder_test.py
@@ -69,10 +69,10 @@ class BuilderTest(unittest.TestCase):
ZeroValue_SinglePos_horizontal ZeroValue_SinglePos_vertical
ZeroValue_PairPos_horizontal ZeroValue_PairPos_vertical
ZeroValue_ChainSinglePos_horizontal ZeroValue_ChainSinglePos_vertical
- PairPosSubtable ChainSubstSubtable ChainPosSubtable LigatureSubtable
- AlternateSubtable MultipleSubstSubtable SingleSubstSubtable
- aalt_chain_contextual_subst AlternateChained MultipleLookupsPerGlyph
- MultipleLookupsPerGlyph2
+ PairPosSubtable ChainSubstSubtable SubstSubtable ChainPosSubtable
+ LigatureSubtable AlternateSubtable MultipleSubstSubtable
+ SingleSubstSubtable aalt_chain_contextual_subst AlternateChained
+ MultipleLookupsPerGlyph MultipleLookupsPerGlyph2
""".split()
def __init__(self, methodName):
diff --git a/Tests/feaLib/data/ChainSubstSubtable.fea b/Tests/feaLib/data/ChainSubstSubtable.fea
index b6e959a2..ec248805 100644
--- a/Tests/feaLib/data/ChainSubstSubtable.fea
+++ b/Tests/feaLib/data/ChainSubstSubtable.fea
@@ -1,9 +1,9 @@
feature test {
- sub G' by G.swash;
+ sub A G' by G.swash;
subtable;
- sub H' by H.swash;
+ sub A H' by H.swash;
subtable;
- sub G' by g;
+ sub A G' by g;
subtable;
- sub H' by H.swash;
+ sub A H' by H.swash;
} test;
diff --git a/Tests/feaLib/data/ChainSubstSubtable.ttx b/Tests/feaLib/data/ChainSubstSubtable.ttx
index f7a09c7f..75348dc2 100644
--- a/Tests/feaLib/data/ChainSubstSubtable.ttx
+++ b/Tests/feaLib/data/ChainSubstSubtable.ttx
@@ -34,7 +34,10 @@
<LookupFlag value="0"/>
<!-- SubTableCount=4 -->
<ChainContextSubst index="0" Format="3">
- <!-- BacktrackGlyphCount=0 -->
+ <!-- BacktrackGlyphCount=1 -->
+ <BacktrackCoverage index="0">
+ <Glyph value="A"/>
+ </BacktrackCoverage>
<!-- InputGlyphCount=1 -->
<InputCoverage index="0">
<Glyph value="G"/>
@@ -47,7 +50,10 @@
</SubstLookupRecord>
</ChainContextSubst>
<ChainContextSubst index="1" Format="3">
- <!-- BacktrackGlyphCount=0 -->
+ <!-- BacktrackGlyphCount=1 -->
+ <BacktrackCoverage index="0">
+ <Glyph value="A"/>
+ </BacktrackCoverage>
<!-- InputGlyphCount=1 -->
<InputCoverage index="0">
<Glyph value="H"/>
@@ -60,7 +66,10 @@
</SubstLookupRecord>
</ChainContextSubst>
<ChainContextSubst index="2" Format="3">
- <!-- BacktrackGlyphCount=0 -->
+ <!-- BacktrackGlyphCount=1 -->
+ <BacktrackCoverage index="0">
+ <Glyph value="A"/>
+ </BacktrackCoverage>
<!-- InputGlyphCount=1 -->
<InputCoverage index="0">
<Glyph value="G"/>
@@ -73,7 +82,10 @@
</SubstLookupRecord>
</ChainContextSubst>
<ChainContextSubst index="3" Format="3">
- <!-- BacktrackGlyphCount=0 -->
+ <!-- BacktrackGlyphCount=1 -->
+ <BacktrackCoverage index="0">
+ <Glyph value="A"/>
+ </BacktrackCoverage>
<!-- InputGlyphCount=1 -->
<InputCoverage index="0">
<Glyph value="H"/>
diff --git a/Tests/feaLib/data/SubstSubtable.fea b/Tests/feaLib/data/SubstSubtable.fea
new file mode 100644
index 00000000..b6e959a2
--- /dev/null
+++ b/Tests/feaLib/data/SubstSubtable.fea
@@ -0,0 +1,9 @@
+feature test {
+ sub G' by G.swash;
+ subtable;
+ sub H' by H.swash;
+ subtable;
+ sub G' by g;
+ subtable;
+ sub H' by H.swash;
+} test;
diff --git a/Tests/feaLib/data/SubstSubtable.ttx b/Tests/feaLib/data/SubstSubtable.ttx
new file mode 100644
index 00000000..8718f46f
--- /dev/null
+++ b/Tests/feaLib/data/SubstSubtable.ttx
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont>
+
+ <GSUB>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="test"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=5 -->
+ <Lookup index="0">
+ <LookupType value="5"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=4 -->
+ <ContextSubst index="0" Format="3">
+ <!-- GlyphCount=1 -->
+ <!-- SubstCount=1 -->
+ <Coverage index="0">
+ <Glyph value="G"/>
+ </Coverage>
+ <SubstLookupRecord index="0">
+ <SequenceIndex value="0"/>
+ <LookupListIndex value="1"/>
+ </SubstLookupRecord>
+ </ContextSubst>
+ <ContextSubst index="1" Format="3">
+ <!-- GlyphCount=1 -->
+ <!-- SubstCount=1 -->
+ <Coverage index="0">
+ <Glyph value="H"/>
+ </Coverage>
+ <SubstLookupRecord index="0">
+ <SequenceIndex value="0"/>
+ <LookupListIndex value="2"/>
+ </SubstLookupRecord>
+ </ContextSubst>
+ <ContextSubst index="2" Format="3">
+ <!-- GlyphCount=1 -->
+ <!-- SubstCount=1 -->
+ <Coverage index="0">
+ <Glyph value="G"/>
+ </Coverage>
+ <SubstLookupRecord index="0">
+ <SequenceIndex value="0"/>
+ <LookupListIndex value="3"/>
+ </SubstLookupRecord>
+ </ContextSubst>
+ <ContextSubst index="3" Format="3">
+ <!-- GlyphCount=1 -->
+ <!-- SubstCount=1 -->
+ <Coverage index="0">
+ <Glyph value="H"/>
+ </Coverage>
+ <SubstLookupRecord index="0">
+ <SequenceIndex value="0"/>
+ <LookupListIndex value="4"/>
+ </SubstLookupRecord>
+ </ContextSubst>
+ </Lookup>
+ <Lookup index="1">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="G" out="G.swash"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="2">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="H" out="H.swash"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="3">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="G" out="g"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="4">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="H" out="H.swash"/>
+ </SingleSubst>
+ </Lookup>
+ </LookupList>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/feaLib/data/bug506.ttx b/Tests/feaLib/data/bug506.ttx
index 9aff9135..4daba45b 100644
--- a/Tests/feaLib/data/bug506.ttx
+++ b/Tests/feaLib/data/bug506.ttx
@@ -30,25 +30,23 @@
<LookupList>
<!-- LookupCount=2 -->
<Lookup index="0">
- <LookupType value="6"/>
+ <LookupType value="5"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
- <ChainContextSubst index="0" Format="3">
- <!-- BacktrackGlyphCount=0 -->
- <!-- InputGlyphCount=2 -->
- <InputCoverage index="0">
+ <ContextSubst index="0" Format="3">
+ <!-- GlyphCount=2 -->
+ <!-- SubstCount=1 -->
+ <Coverage index="0">
<Glyph value="f"/>
- </InputCoverage>
- <InputCoverage index="1">
+ </Coverage>
+ <Coverage index="1">
<Glyph value="i"/>
- </InputCoverage>
- <!-- LookAheadGlyphCount=0 -->
- <!-- SubstCount=1 -->
+ </Coverage>
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="1"/>
</SubstLookupRecord>
- </ChainContextSubst>
+ </ContextSubst>
</Lookup>
<Lookup index="1">
<LookupType value="4"/>
diff --git a/Tests/feaLib/data/bug509.ttx b/Tests/feaLib/data/bug509.ttx
index e5a36afd..52fba201 100644
--- a/Tests/feaLib/data/bug509.ttx
+++ b/Tests/feaLib/data/bug509.ttx
@@ -30,33 +30,29 @@
<LookupList>
<!-- LookupCount=2 -->
<Lookup index="0">
- <LookupType value="6"/>
+ <LookupType value="5"/>
<LookupFlag value="0"/>
<!-- SubTableCount=2 -->
- <ChainContextSubst index="0" Format="3">
- <!-- BacktrackGlyphCount=0 -->
- <!-- InputGlyphCount=1 -->
- <InputCoverage index="0">
- <Glyph value="A"/>
- </InputCoverage>
- <!-- LookAheadGlyphCount=0 -->
+ <ContextSubst index="0" Format="3">
+ <!-- GlyphCount=1 -->
<!-- SubstCount=0 -->
- </ChainContextSubst>
- <ChainContextSubst index="1" Format="3">
- <!-- BacktrackGlyphCount=0 -->
- <!-- InputGlyphCount=1 -->
- <InputCoverage index="0">
+ <Coverage index="0">
+ <Glyph value="A"/>
+ </Coverage>
+ </ContextSubst>
+ <ContextSubst index="1" Format="3">
+ <!-- GlyphCount=1 -->
+ <!-- SubstCount=1 -->
+ <Coverage index="0">
<Glyph value="A"/>
<Glyph value="A.sc"/>
<Glyph value="A.alt1"/>
- </InputCoverage>
- <!-- LookAheadGlyphCount=0 -->
- <!-- SubstCount=1 -->
+ </Coverage>
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="1"/>
</SubstLookupRecord>
- </ChainContextSubst>
+ </ContextSubst>
</Lookup>
<Lookup index="1">
<LookupType value="1"/>
diff --git a/Tests/feaLib/data/bug512.ttx b/Tests/feaLib/data/bug512.ttx
index 1dfe63fe..50b76b46 100644
--- a/Tests/feaLib/data/bug512.ttx
+++ b/Tests/feaLib/data/bug512.ttx
@@ -30,61 +30,53 @@
<LookupList>
<!-- LookupCount=3 -->
<Lookup index="0">
- <LookupType value="6"/>
+ <LookupType value="5"/>
<LookupFlag value="0"/>
<!-- SubTableCount=4 -->
- <ChainContextSubst index="0" Format="3">
- <!-- BacktrackGlyphCount=0 -->
- <!-- InputGlyphCount=1 -->
- <InputCoverage index="0">
- <Glyph value="G"/>
- </InputCoverage>
- <!-- LookAheadGlyphCount=0 -->
+ <ContextSubst index="0" Format="3">
+ <!-- GlyphCount=1 -->
<!-- SubstCount=1 -->
+ <Coverage index="0">
+ <Glyph value="G"/>
+ </Coverage>
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="1"/>
</SubstLookupRecord>
- </ChainContextSubst>
- <ChainContextSubst index="1" Format="3">
- <!-- BacktrackGlyphCount=0 -->
- <!-- InputGlyphCount=1 -->
- <InputCoverage index="0">
- <Glyph value="H"/>
- </InputCoverage>
- <!-- LookAheadGlyphCount=0 -->
+ </ContextSubst>
+ <ContextSubst index="1" Format="3">
+ <!-- GlyphCount=1 -->
<!-- SubstCount=1 -->
+ <Coverage index="0">
+ <Glyph value="H"/>
+ </Coverage>
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="1"/>
</SubstLookupRecord>
- </ChainContextSubst>
- <ChainContextSubst index="2" Format="3">
- <!-- BacktrackGlyphCount=0 -->
- <!-- InputGlyphCount=1 -->
- <InputCoverage index="0">
- <Glyph value="G"/>
- </InputCoverage>
- <!-- LookAheadGlyphCount=0 -->
+ </ContextSubst>
+ <ContextSubst index="2" Format="3">
+ <!-- GlyphCount=1 -->
<!-- SubstCount=1 -->
+ <Coverage index="0">
+ <Glyph value="G"/>
+ </Coverage>
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="2"/>
</SubstLookupRecord>
- </ChainContextSubst>
- <ChainContextSubst index="3" Format="3">
- <!-- BacktrackGlyphCount=0 -->
- <!-- InputGlyphCount=1 -->
- <InputCoverage index="0">
- <Glyph value="H"/>
- </InputCoverage>
- <!-- LookAheadGlyphCount=0 -->
+ </ContextSubst>
+ <ContextSubst index="3" Format="3">
+ <!-- GlyphCount=1 -->
<!-- SubstCount=1 -->
+ <Coverage index="0">
+ <Glyph value="H"/>
+ </Coverage>
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="2"/>
</SubstLookupRecord>
- </ChainContextSubst>
+ </ContextSubst>
</Lookup>
<Lookup index="1">
<LookupType value="1"/>
diff --git a/Tests/feaLib/data/lookupflag.fea b/Tests/feaLib/data/lookupflag.fea
index 1828c43e..0210ab42 100644
--- a/Tests/feaLib/data/lookupflag.fea
+++ b/Tests/feaLib/data/lookupflag.fea
@@ -147,3 +147,13 @@ feature test {
pos two 2;
} X;
} test;
+
+lookup Y {
+ lookupflag UseMarkFilteringSet [acute grave macron];
+ pos Y 1;
+} Y;
+
+lookup Z {
+ lookupflag MarkAttachmentType [acute grave macron];
+ pos Z 1;
+} Z;
diff --git a/Tests/feaLib/data/lookupflag.ttx b/Tests/feaLib/data/lookupflag.ttx
index 760eab31..16ea751f 100644
--- a/Tests/feaLib/data/lookupflag.ttx
+++ b/Tests/feaLib/data/lookupflag.ttx
@@ -107,7 +107,7 @@
</FeatureRecord>
</FeatureList>
<LookupList>
- <!-- LookupCount=24 -->
+ <!-- LookupCount=26 -->
<Lookup index="0">
<LookupType value="1"/>
<LookupFlag value="1"/><!-- rightToLeft -->
@@ -400,6 +400,31 @@
<Value XAdvance="2"/>
</SinglePos>
</Lookup>
+ <Lookup index="24">
+ <LookupType value="1"/>
+ <LookupFlag value="16"/><!-- useMarkFilteringSet -->
+ <!-- SubTableCount=1 -->
+ <SinglePos index="0" Format="1">
+ <Coverage>
+ <Glyph value="Y"/>
+ </Coverage>
+ <ValueFormat value="4"/>
+ <Value XAdvance="1"/>
+ </SinglePos>
+ <MarkFilteringSet value="0"/>
+ </Lookup>
+ <Lookup index="25">
+ <LookupType value="1"/>
+ <LookupFlag value="256"/><!-- markAttachmentType[1] -->
+ <!-- SubTableCount=1 -->
+ <SinglePos index="0" Format="1">
+ <Coverage>
+ <Glyph value="Z"/>
+ </Coverage>
+ <ValueFormat value="4"/>
+ <Value XAdvance="1"/>
+ </SinglePos>
+ </Lookup>
</LookupList>
</GPOS>
diff --git a/Tests/feaLib/data/spec6h_ii.ttx b/Tests/feaLib/data/spec6h_ii.ttx
index e8ec85f2..2f0efc6f 100644
--- a/Tests/feaLib/data/spec6h_ii.ttx
+++ b/Tests/feaLib/data/spec6h_ii.ttx
@@ -112,25 +112,23 @@
</MarkBasePos>
</Lookup>
<Lookup index="2">
- <LookupType value="8"/>
+ <LookupType value="7"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
- <ChainContextPos index="0" Format="3">
- <!-- BacktrackGlyphCount=0 -->
- <!-- InputGlyphCount=3 -->
- <InputCoverage index="0">
+ <ContextPos index="0" Format="3">
+ <!-- GlyphCount=3 -->
+ <!-- PosCount=2 -->
+ <Coverage index="0">
<Glyph value="T"/>
- </InputCoverage>
- <InputCoverage index="1">
+ </Coverage>
+ <Coverage index="1">
<Glyph value="c"/>
<Glyph value="o"/>
- </InputCoverage>
- <InputCoverage index="2">
+ </Coverage>
+ <Coverage index="2">
<Glyph value="grave"/>
<Glyph value="acute"/>
- </InputCoverage>
- <!-- LookAheadGlyphCount=0 -->
- <!-- PosCount=2 -->
+ </Coverage>
<PosLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="0"/>
@@ -139,7 +137,7 @@
<SequenceIndex value="2"/>
<LookupListIndex value="1"/>
</PosLookupRecord>
- </ChainContextPos>
+ </ContextPos>
</Lookup>
</LookupList>
</GPOS>
diff --git a/Tests/feaLib/data/spec6h_iii_3d.ttx b/Tests/feaLib/data/spec6h_iii_3d.ttx
index a608f0e6..2335dd0b 100644
--- a/Tests/feaLib/data/spec6h_iii_3d.ttx
+++ b/Tests/feaLib/data/spec6h_iii_3d.ttx
@@ -30,25 +30,23 @@
<LookupList>
<!-- LookupCount=2 -->
<Lookup index="0">
- <LookupType value="8"/>
+ <LookupType value="7"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
- <ChainContextPos index="0" Format="3">
- <!-- BacktrackGlyphCount=0 -->
- <!-- InputGlyphCount=2 -->
- <InputCoverage index="0">
+ <ContextPos index="0" Format="3">
+ <!-- GlyphCount=2 -->
+ <!-- PosCount=1 -->
+ <Coverage index="0">
<Glyph value="L"/>
- </InputCoverage>
- <InputCoverage index="1">
+ </Coverage>
+ <Coverage index="1">
<Glyph value="quoteright"/>
- </InputCoverage>
- <!-- LookAheadGlyphCount=0 -->
- <!-- PosCount=1 -->
+ </Coverage>
<PosLookupRecord index="0">
<SequenceIndex value="1"/>
<LookupListIndex value="1"/>
</PosLookupRecord>
- </ChainContextPos>
+ </ContextPos>
</Lookup>
<Lookup index="1">
<LookupType value="1"/>
diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py
index 58090296..db505950 100644
--- a/Tests/feaLib/parser_test.py
+++ b/Tests/feaLib/parser_test.py
@@ -718,6 +718,18 @@ class ParserTest(unittest.TestCase):
self.assertEqual(flag.asFea(),
"lookupflag RightToLeft MarkAttachmentType @TOP_MARKS;")
+ def test_lookupflag_format_A_MarkAttachmentType_glyphClass(self):
+ flag = self.parse_lookupflag_(
+ "lookupflag RightToLeft MarkAttachmentType [acute grave macron];")
+ self.assertIsInstance(flag, ast.LookupFlagStatement)
+ self.assertEqual(flag.value, 1)
+ self.assertIsInstance(flag.markAttachment, ast.GlyphClass)
+ self.assertEqual(flag.markAttachment.glyphSet(),
+ ("acute", "grave", "macron"))
+ self.assertIsNone(flag.markFilteringSet)
+ self.assertEqual(flag.asFea(),
+ "lookupflag RightToLeft MarkAttachmentType [acute grave macron];")
+
def test_lookupflag_format_A_UseMarkFilteringSet(self):
flag = self.parse_lookupflag_(
"@BOTTOM_MARKS = [cedilla ogonek];"
@@ -731,6 +743,18 @@ class ParserTest(unittest.TestCase):
self.assertEqual(flag.asFea(),
"lookupflag IgnoreLigatures UseMarkFilteringSet @BOTTOM_MARKS;")
+ def test_lookupflag_format_A_UseMarkFilteringSet_glyphClass(self):
+ flag = self.parse_lookupflag_(
+ "lookupflag UseMarkFilteringSet [cedilla ogonek] IgnoreLigatures;")
+ self.assertIsInstance(flag, ast.LookupFlagStatement)
+ self.assertEqual(flag.value, 4)
+ self.assertIsNone(flag.markAttachment)
+ self.assertIsInstance(flag.markFilteringSet, ast.GlyphClass)
+ self.assertEqual(flag.markFilteringSet.glyphSet(),
+ ("cedilla", "ogonek"))
+ self.assertEqual(flag.asFea(),
+ "lookupflag IgnoreLigatures UseMarkFilteringSet [cedilla ogonek];")
+
def test_lookupflag_format_B(self):
flag = self.parse_lookupflag_("lookupflag 7;")
self.assertIsInstance(flag, ast.LookupFlagStatement)
diff --git a/Tests/otlLib/builder_test.py b/Tests/otlLib/builder_test.py
index 667e0826..bdfc6450 100644
--- a/Tests/otlLib/builder_test.py
+++ b/Tests/otlLib/builder_test.py
@@ -1392,6 +1392,57 @@ def test_stat_infinities():
assert struct.pack(">l", posInf) == b"\x7f\xff\xff\xff"
+class ChainContextualRulesetTest(object):
+ def test_makeRulesets(self):
+ font = ttLib.TTFont()
+ font.setGlyphOrder(["a","b","c","d","A","B","C","D","E"])
+ sb = builder.ChainContextSubstBuilder(font, None)
+ prefix, input_, suffix, lookups = [["a"], ["b"]], [["c"]], [], [None]
+ sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
+
+ prefix, input_, suffix, lookups = [["a"], ["d"]], [["c"]], [], [None]
+ sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
+
+ sb.add_subtable_break(None)
+
+ # Second subtable has some glyph classes
+ prefix, input_, suffix, lookups = [["A"]], [["E"]], [], [None]
+ sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
+ prefix, input_, suffix, lookups = [["A"]], [["C","D"]], [], [None]
+ sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
+ prefix, input_, suffix, lookups = [["A", "B"]], [["E"]], [], [None]
+ sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
+
+ sb.add_subtable_break(None)
+
+ # Third subtable has no pre/post context
+ prefix, input_, suffix, lookups = [], [["E"]], [], [None]
+ sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
+ prefix, input_, suffix, lookups = [], [["C","D"]], [], [None]
+ sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
+
+ rulesets = sb.rulesets()
+ assert len(rulesets) == 3
+ assert rulesets[0].hasPrefixOrSuffix
+ assert not rulesets[0].hasAnyGlyphClasses
+ cd = rulesets[0].format2ClassDefs()
+ assert set(cd[0].classes()[1:]) == set([("d",),("b",),("a",)])
+ assert set(cd[1].classes()[1:]) == set([("c",)])
+ assert set(cd[2].classes()[1:]) == set()
+
+ assert rulesets[1].hasPrefixOrSuffix
+ assert rulesets[1].hasAnyGlyphClasses
+ assert not rulesets[1].format2ClassDefs()
+
+ assert not rulesets[2].hasPrefixOrSuffix
+ assert rulesets[2].hasAnyGlyphClasses
+ assert rulesets[2].format2ClassDefs()
+ cd = rulesets[2].format2ClassDefs()
+ assert set(cd[0].classes()[1:]) == set()
+ assert set(cd[1].classes()[1:]) == set([("C","D"), ("E",)])
+ assert set(cd[2].classes()[1:]) == set()
+
+
if __name__ == "__main__":
import sys
diff --git a/Tests/otlLib/mock_builder_test.py b/Tests/otlLib/mock_builder_test.py
index 2b697ac7..b3fecd83 100644
--- a/Tests/otlLib/mock_builder_test.py
+++ b/Tests/otlLib/mock_builder_test.py
@@ -13,6 +13,7 @@ from fontTools.otlLib.builder import (
ClassPairPosSubtableBuilder,
PairPosBuilder,
SinglePosBuilder,
+ ChainContextualRule
)
from fontTools.otlLib.error import OpenTypeLibError
from fontTools.ttLib import TTFont
@@ -79,7 +80,7 @@ def test_chain_pos_references_GSUB_lookup(ttfont):
location = MockBuilderLocation((0, "alpha"))
builder = ChainContextPosBuilder(ttfont, location)
builder2 = SingleSubstBuilder(ttfont, location)
- builder.rules.append(([], [], [], [[builder2]]))
+ builder.rules.append(ChainContextualRule([], [], [], [[builder2]]))
with pytest.raises(OpenTypeLibError, match="0:alpha: Missing index of the specified lookup, might be a substitution lookup"):
builder.build()
diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py
index cec15cce..79addcef 100644
--- a/Tests/ttLib/tables/_g_l_y_f_test.py
+++ b/Tests/ttLib/tables/_g_l_y_f_test.py
@@ -471,6 +471,30 @@ class GlyphTest:
]
)
+ def test_getCompositeMaxpValues(self):
+ # https://github.com/fonttools/fonttools/issues/2044
+ glyphSet = {}
+ pen = TTGlyphPen(glyphSet) # empty non-composite glyph
+ glyphSet["fraction"] = pen.glyph()
+ glyphSet["zero.numr"] = pen.glyph()
+ pen = TTGlyphPen(glyphSet)
+ pen.addComponent("zero.numr", (1, 0, 0, 1, 0, 0))
+ glyphSet["zero.dnom"] = pen.glyph()
+ pen = TTGlyphPen(glyphSet)
+ pen.addComponent("zero.numr", (1, 0, 0, 1, 0, 0))
+ pen.addComponent("fraction", (1, 0, 0, 1, 0, 0))
+ pen.addComponent("zero.dnom", (1, 0, 0, 1, 0, 0))
+ glyphSet["percent"] = pen.glyph()
+ pen = TTGlyphPen(glyphSet)
+ pen.addComponent("zero.numr", (1, 0, 0, 1, 0, 0))
+ pen.addComponent("fraction", (1, 0, 0, 1, 0, 0))
+ pen.addComponent("zero.dnom", (1, 0, 0, 1, 0, 0))
+ pen.addComponent("zero.dnom", (1, 0, 0, 1, 0, 0))
+ glyphSet["perthousand"] = pen.glyph()
+ assert glyphSet["zero.dnom"].getCompositeMaxpValues(glyphSet)[2] == 1
+ assert glyphSet["percent"].getCompositeMaxpValues(glyphSet)[2] == 2
+ assert glyphSet["perthousand"].getCompositeMaxpValues(glyphSet)[2] == 2
+
class GlyphComponentTest:
diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_diff.ttx
index 12f42698..8112d0f6 100644
--- a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx
+++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_diff.ttx
@@ -83,23 +83,21 @@
</MarkBasePos>
</Lookup>
<Lookup index="2">
- <LookupType value="8"/>
+ <LookupType value="7"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
- <ChainContextPos index="0" Format="3">
- <!-- BacktrackGlyphCount=0 -->
- <!-- InputGlyphCount=3 -->
- <InputCoverage index="0" Format="1">
+ <ContextPos index="0" Format="3">
+ <!-- GlyphCount=3 -->
+ <!-- PosCount=2 -->
+ <Coverage index="0" Format="1">
<Glyph value="A"/>
- </InputCoverage>
- <InputCoverage index="1" Format="1">
+ </Coverage>
+ <Coverage index="1" Format="1">
<Glyph value="a"/>
- </InputCoverage>
- <InputCoverage index="2" Format="1">
+ </Coverage>
+ <Coverage index="2" Format="1">
<Glyph value="uni0303"/>
- </InputCoverage>
- <!-- LookAheadGlyphCount=0 -->
- <!-- PosCount=2 -->
+ </Coverage>
<PosLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="0"/>
@@ -108,7 +106,7 @@
<SequenceIndex value="2"/>
<LookupListIndex value="1"/>
</PosLookupRecord>
- </ChainContextPos>
+ </ContextPos>
</Lookup>
</LookupList>
</GPOS>
diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_same.ttx
index b7e86ba2..f56b0503 100644
--- a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx
+++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_same.ttx
@@ -83,23 +83,21 @@
</MarkBasePos>
</Lookup>
<Lookup index="2">
- <LookupType value="8"/>
+ <LookupType value="7"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
- <ChainContextPos index="0" Format="3">
- <!-- BacktrackGlyphCount=0 -->
- <!-- InputGlyphCount=3 -->
- <InputCoverage index="0" Format="1">
+ <ContextPos index="0" Format="3">
+ <!-- GlyphCount=3 -->
+ <!-- PosCount=2 -->
+ <Coverage index="0" Format="1">
<Glyph value="A"/>
- </InputCoverage>
- <InputCoverage index="1" Format="1">
+ </Coverage>
+ <Coverage index="1" Format="1">
<Glyph value="a"/>
- </InputCoverage>
- <InputCoverage index="2" Format="1">
+ </Coverage>
+ <Coverage index="2" Format="1">
<Glyph value="uni0303"/>
- </InputCoverage>
- <!-- LookAheadGlyphCount=0 -->
- <!-- PosCount=2 -->
+ </Coverage>
<PosLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="0"/>
@@ -108,7 +106,7 @@
<SequenceIndex value="2"/>
<LookupListIndex value="1"/>
</PosLookupRecord>
- </ChainContextPos>
+ </ContextPos>
</Lookup>
</LookupList>
</GPOS>
diff --git a/Tests/varLib/interpolate_layout_test.py b/Tests/varLib/interpolate_layout_test.py
index 572dd3ec..8fdf60f5 100644
--- a/Tests/varLib/interpolate_layout_test.py
+++ b/Tests/varLib/interpolate_layout_test.py
@@ -748,8 +748,8 @@ class InterpolateLayoutTest(unittest.TestCase):
self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix)
- def test_varlib_interpolate_layout_GPOS_only_LookupType_8_same_val_ttf(self):
- """Only GPOS; LookupType 8; same values in all masters.
+ def test_varlib_interpolate_layout_GPOS_only_LookupType_7_same_val_ttf(self):
+ """Only GPOS; LookupType 7; same values in all masters.
"""
suffix = '.ttf'
ds_path = self.get_test_input('InterpolateLayout.designspace')
@@ -781,13 +781,13 @@ class InterpolateLayoutTest(unittest.TestCase):
instfont = interpolate_layout(ds_path, {'weight': 500}, finder)
tables = ['GPOS']
- expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_8_same.ttx')
+ expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_7_same.ttx')
self.expect_ttx(instfont, expected_ttx_path, tables)
self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix)
- def test_varlib_interpolate_layout_GPOS_only_LookupType_8_diff_val_ttf(self):
- """Only GPOS; LookupType 8; different values in each master.
+ def test_varlib_interpolate_layout_GPOS_only_LookupType_7_diff_val_ttf(self):
+ """Only GPOS; LookupType 7; different values in each master.
"""
suffix = '.ttf'
ds_path = self.get_test_input('InterpolateLayout.designspace')
@@ -833,7 +833,7 @@ class InterpolateLayoutTest(unittest.TestCase):
instfont = interpolate_layout(ds_path, {'weight': 500}, finder)
tables = ['GPOS']
- expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_8_diff.ttx')
+ expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_7_diff.ttx')
self.expect_ttx(instfont, expected_ttx_path, tables)
self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix)
diff --git a/requirements.txt b/requirements.txt
index b5be9db5..9e858085 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,7 @@
brotli==1.0.7; platform_python_implementation != "PyPy"
brotlipy==0.7.0; platform_python_implementation == "PyPy"
unicodedata2==13.0.0.post2; python_version < '3.9' and platform_python_implementation != "PyPy"
-scipy==1.4.1; platform_python_implementation != "PyPy"
+scipy==1.5.2; platform_python_implementation != "PyPy"
munkres==1.1.2; platform_python_implementation == "PyPy"
zopfli==0.1.6
fs==2.4.11
diff --git a/setup.cfg b/setup.cfg
index 5c753761..b4c626fb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 4.13.0
+current_version = 4.14.0
commit = True
tag = False
tag_name = {new_version}
diff --git a/setup.py b/setup.py
index 80bcc83e..e91eb5dc 100755
--- a/setup.py
+++ b/setup.py
@@ -437,7 +437,7 @@ if ext_modules:
setup_params = dict(
name="fonttools",
- version="4.13.0",
+ version="4.14.0",
description="Tools to manipulate font files",
author="Just van Rossum",
author_email="just@letterror.com",