diff options
author | Haibo Huang <hhb@google.com> | 2020-08-19 13:00:39 -0700 |
---|---|---|
committer | Haibo Huang <hhb@google.com> | 2020-08-19 13:00:39 -0700 |
commit | 3aedd105d78b03a1821e6b20f5ad8f096fa49c7a (patch) | |
tree | 151c2229b01f7c1c6bc424641d3498bdc93a49c2 | |
parent | a7ee29ebd4c9d39d29ee03f7746bb6af1c3f7e6b (diff) | |
download | fonttools-3aedd105d78b03a1821e6b20f5ad8f096fa49c7a.tar.gz |
Upgrade fonttools to 4.14.0
Change-Id: Ic1373449418488c7fc89c76a32892c20ab621356
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 @@ -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 } } @@ -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) ---------------------------- @@ -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 @@ -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 @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.13.0 +current_version = 4.14.0 commit = True tag = False tag_name = {new_version} @@ -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", |