diff -Nru fonttools-4.13.0/debian/changelog fonttools-4.16.1/debian/changelog --- fonttools-4.13.0/debian/changelog 2020-07-25 04:53:08.000000000 +0000 +++ fonttools-4.16.1/debian/changelog 2020-10-10 09:58:26.000000000 +0000 @@ -1,3 +1,10 @@ +fonttools (4.16.1-1) unstable; urgency=medium + + * New upstream version 4.16.1 + * debian/control: Update dependencies + + -- Yao Wei (魏銘廷) Sat, 10 Oct 2020 17:58:26 +0800 + fonttools (4.13.0-1) unstable; urgency=medium * New upstream version 4.13.0 diff -Nru fonttools-4.13.0/debian/control fonttools-4.16.1/debian/control --- fonttools-4.13.0/debian/control 2020-07-25 04:53:08.000000000 +0000 +++ fonttools-4.16.1/debian/control 2020-10-10 09:58:26.000000000 +0000 @@ -9,9 +9,9 @@ debhelper-compat (= 13), dh-python, python3-all, - python3-brotli (>= 1.0.7), + python3-brotli (>= 1.0.9), python3-pytest, - python3-scipy (>= 1.4.1) | python3-munkres (>= 1.1.2), + python3-scipy (>= 1.5.2) | python3-munkres (>= 1.1.2), python3-setuptools, python3-sympy, python3-fs (>= 2.4.11), @@ -29,9 +29,9 @@ Section: python Architecture: all Depends: - python3-brotli (>= 1.0.7), + python3-brotli (>= 1.0.9), python3-pkg-resources, - python3-scipy (>= 1.4.1) | python3-munkres (>= 1.1.2), + python3-scipy (>= 1.5.2) | python3-munkres (>= 1.1.2), python3-sympy, python3-fs (>= 2.4.11), ${misc:Depends}, diff -Nru fonttools-4.13.0/dev-requirements.txt fonttools-4.16.1/dev-requirements.txt --- fonttools-4.13.0/dev-requirements.txt 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/dev-requirements.txt 2020-10-05 18:25:14.000000000 +0000 @@ -2,3 +2,4 @@ tox>=2.5 bump2version>=0.5.6 sphinx>=1.5.5 +mypy>=0.782 diff -Nru fonttools-4.13.0/Doc/docs-requirements.txt fonttools-4.16.1/Doc/docs-requirements.txt --- fonttools-4.13.0/Doc/docs-requirements.txt 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Doc/docs-requirements.txt 2020-10-05 18:25:14.000000000 +0000 @@ -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.49 diff -Nru fonttools-4.13.0/Doc/source/designspaceLib/readme.rst fonttools-4.16.1/Doc/source/designspaceLib/readme.rst --- fonttools-4.13.0/Doc/source/designspaceLib/readme.rst 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Doc/source/designspaceLib/readme.rst 2020-10-05 18:25:14.000000000 +0000 @@ -81,7 +81,7 @@ 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 @@ - 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 `_. See `5.0 rules element`_ § Attributes. Evaluating rules ---------------- @@ -312,8 +313,8 @@ 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 @@ **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 -Nru fonttools-4.13.0/Doc/source/mtiLib.rst fonttools-4.16.1/Doc/source/mtiLib.rst --- fonttools-4.13.0/Doc/source/mtiLib.rst 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Doc/source/mtiLib.rst 2020-10-05 18:25:14.000000000 +0000 @@ -1,8 +1,14 @@ -###### -mtiLib -###### +########################################### +mtiLib: Read Monotype FontDame source files +########################################### + +FontTools provides support for reading the OpenType layout tables produced by +Monotype's FontDame and Font Chef font editors. These tables are written in a +simple textual format. The ``mtiLib`` library parses these text files and creates +table objects representing their contents. + +Additionally, ``fonttools mtiLib`` will convert a text file to TTX XML. + .. automodule:: fontTools.mtiLib - :inherited-members: - :members: - :undoc-members: + :members: build, main diff -Nru fonttools-4.13.0/.gitignore fonttools-4.16.1/.gitignore --- fonttools-4.13.0/.gitignore 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/.gitignore 2020-10-05 18:25:14.000000000 +0000 @@ -54,3 +54,6 @@ # Cython sources (e.g. cu2qu) Lib/**/*.c + +# Ctags +tags diff -Nru fonttools-4.13.0/Lib/fontTools/feaLib/ast.py fonttools-4.16.1/Lib/fontTools/feaLib/ast.py --- fonttools-4.13.0/Lib/fontTools/feaLib/ast.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/feaLib/ast.py 2020-10-05 18:25:14.000000000 +0000 @@ -8,65 +8,65 @@ 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 @@ return "" % ", ".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 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 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 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 @@ 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 @@ 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 @@ 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 @@ """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 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 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 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 @@ 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 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 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 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 TableBlock(Block): """A ``table ... { }`` block.""" + def __init__(self, name, location=None): Block.__init__(self, location) self.name = name @@ -391,6 +442,7 @@ 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 @@ """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 @@ 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 @@ 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 @@ 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 @@ # markClass [cedilla] @FRENCH_ACCENTS; """ + def __init__(self, markClass, anchor, glyphs, location=None): Statement.__init__(self, location) assert isinstance(markClass, MarkClass) @@ -512,8 +566,8 @@ 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 @@ 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 @@ 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 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 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 @@ 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 @@ 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 @@ 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 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 @@ 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 "" @@ -740,6 +817,7 @@ 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 @@ 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 @@ ``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 @@ 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 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 @@ 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 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 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 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 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ """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 @@ """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 @@ """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 @@ 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 @@ 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 @@ If ``enumerated`` is true, then this is expressed as an `enumerated pair `_. """ - 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 @@ 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 @@ 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 @@ 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 @@ ``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 @@ 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 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 @@ 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 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 @@ 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 @@ 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 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 @@ class NameRecord(Statement): """Represents a name record. (`Section 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 @@ 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 @@ 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 @@ 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 @@ 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 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 @@ 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 @@ 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 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 @@ 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 @@ 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 VheaField(Statement): """An entry in the ``vhea`` table.""" + def __init__(self, key, value, location=None): Statement.__init__(self, location) self.key = key diff -Nru fonttools-4.13.0/Lib/fontTools/feaLib/builder.py fonttools-4.16.1/Lib/fontTools/feaLib/builder.py --- fonttools-4.13.0/Lib/fontTools/feaLib/builder.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/feaLib/builder.py 2020-10-05 18:25:14.000000000 +0000 @@ -2,6 +2,7 @@ from fontTools.misc import sstruct from fontTools.misc.textTools import binary2num, safeEval from fontTools.feaLib.error import FeatureLibError +from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY from fontTools.feaLib.parser import Parser from fontTools.feaLib.ast import FeatureFile from fontTools.otlLib import builder as otl @@ -23,6 +24,7 @@ ClassPairPosSubtableBuilder, PairPosBuilder, SinglePosBuilder, + ChainContextualRule, ) from fontTools.otlLib.error import OpenTypeLibError from collections import defaultdict @@ -33,7 +35,7 @@ log = logging.getLogger(__name__) -def addOpenTypeFeatures(font, featurefile, tables=None): +def addOpenTypeFeatures(font, featurefile, tables=None, debug=False): """Add features from a file to a font. Note that this replaces any features currently present. @@ -43,13 +45,17 @@ parse it into an AST), or a pre-parsed AST instance. tables: If passed, restrict the set of affected tables to those in the list. + debug: Whether to add source debugging information to the font in the + ``Debg`` table """ builder = Builder(font, featurefile) - builder.build(tables=tables) + builder.build(tables=tables, debug=debug) -def addOpenTypeFeaturesFromString(font, features, filename=None, tables=None): +def addOpenTypeFeaturesFromString( + font, features, filename=None, tables=None, debug=False +): """Add features from a string to a font. Note that this replaces any features currently present. @@ -61,28 +67,33 @@ directory is assumed. tables: If passed, restrict the set of affected tables to those in the list. + debug: Whether to add source debugging information to the font in the + ``Debg`` table """ featurefile = UnicodeIO(tounicode(features)) if filename: featurefile.name = filename - addOpenTypeFeatures(font, featurefile, tables=tables) + addOpenTypeFeatures(font, featurefile, tables=tables, debug=debug) 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 @@ -104,6 +115,7 @@ self.cur_lookup_name_ = None self.cur_feature_name_ = None self.lookups_ = [] + self.lookup_locations = {"GSUB": {}, "GPOS": {}} self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*] self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp' # for feature 'aalt' @@ -142,7 +154,7 @@ # for table 'vhea' self.vhea_ = {} - def build(self, tables=None): + def build(self, tables=None, debug=False): if self.parseTree is None: self.parseTree = Parser(self.file, self.glyphMap).parse() self.parseTree.build(self) @@ -170,19 +182,20 @@ 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() @@ -196,6 +209,8 @@ self.font["BASE"] = base elif "BASE" in self.font: del self.font["BASE"] + if debug: + self.buildDebg() def get_chained_lookup_(self, location, builder_class): result = builder_class(self.font, location) @@ -210,16 +225,19 @@ 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 +248,7 @@ 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 +256,16 @@ 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 +273,23 @@ 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 +356,12 @@ 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 +379,18 @@ 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 +433,18 @@ 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 +480,63 @@ 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 +592,22 @@ 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 +642,20 @@ 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 buildDebg(self): + if "Debg" not in self.font: + self.font["Debg"] = newTable("Debg") + self.font["Debg"].data = {} + self.font["Debg"].data[LOOKUP_DEBUG_INFO_KEY] = self.lookup_locations + 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 = [] @@ -576,6 +663,11 @@ if lookup.table != tag: continue lookup.lookup_index = len(lookups) + self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo( + location=str(lookup.location), + name=self.get_lookup_name_(lookup), + feature=None, + ) lookups.append(lookup) try: otLookups = [l.build() for l in lookups] @@ -606,13 +698,19 @@ # 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 + for ix in lookup_indices: + self.lookup_locations[tag][str(ix)] = self.lookup_locations[tag][ + str(ix) + ]._replace(feature=key) + feature_key = (feature_tag, lookup_indices) feature_index = feature_indices.get(feature_key) if feature_index is None: @@ -620,14 +718,12 @@ 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 +739,16 @@ 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 @@ -668,26 +763,35 @@ table.LookupList.LookupCount = len(table.LookupList.Lookup) return table + def get_lookup_name_(self, lookup): + rev = {v: k for k, v in self.named_lookups_.items()} + if lookup in rev: + return rev[lookup] + return None + 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 +803,11 @@ 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 +826,14 @@ 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 +859,24 @@ 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 +887,14 @@ 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 +910,8 @@ 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 +938,25 @@ 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 +967,9 @@ 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 +980,21 @@ 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 +1002,20 @@ 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 +1041,27 @@ 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 +1073,32 @@ 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 +1116,18 @@ 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 +1137,18 @@ 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 +1157,15 @@ 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 +1173,7 @@ 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 +1193,11 @@ 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 +1246,22 @@ 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 +1281,7 @@ 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 +1303,7 @@ 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 +1327,3 @@ vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0} valRec = otl.buildValue(vr) return valRec - - - diff -Nru fonttools-4.13.0/Lib/fontTools/feaLib/error.py fonttools-4.16.1/Lib/fontTools/feaLib/error.py --- fonttools-4.13.0/Lib/fontTools/feaLib/error.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/feaLib/error.py 2020-10-05 18:25:14.000000000 +0000 @@ -1,5 +1,3 @@ - - class FeatureLibError(Exception): def __init__(self, message, location): Exception.__init__(self, message) diff -Nru fonttools-4.13.0/Lib/fontTools/feaLib/lexer.py fonttools-4.16.1/Lib/fontTools/feaLib/lexer.py --- fonttools-4.13.0/Lib/fontTools/feaLib/lexer.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/feaLib/lexer.py 2020-10-05 18:25:14.000000000 +0000 @@ -77,75 +77,75 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 NonIncludingLexer(IncludingLexer): """Lexer that does not follow `include` statements, emits them as-is.""" + def __next__(self): # Python 3 return next(self.lexers_[0]) diff -Nru fonttools-4.13.0/Lib/fontTools/feaLib/location.py fonttools-4.16.1/Lib/fontTools/feaLib/location.py --- fonttools-4.13.0/Lib/fontTools/feaLib/location.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/feaLib/location.py 2020-10-05 18:25:14.000000000 +0000 @@ -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 -Nru fonttools-4.13.0/Lib/fontTools/feaLib/lookupDebugInfo.py fonttools-4.16.1/Lib/fontTools/feaLib/lookupDebugInfo.py --- fonttools-4.13.0/Lib/fontTools/feaLib/lookupDebugInfo.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/feaLib/lookupDebugInfo.py 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,10 @@ +from typing import NamedTuple + +LOOKUP_DEBUG_INFO_KEY = "com.github.fonttools.feaLib" + +class LookupDebugInfo(NamedTuple): + """Information about where a lookup came from, to be embedded in a font""" + + location: str + name: str + feature: list diff -Nru fonttools-4.13.0/Lib/fontTools/feaLib/__main__.py fonttools-4.16.1/Lib/fontTools/feaLib/__main__.py --- fonttools-4.13.0/Lib/fontTools/feaLib/__main__.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/feaLib/__main__.py 2020-10-05 18:25:14.000000000 +0000 @@ -15,23 +15,45 @@ 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.", + ) + parser.add_argument( + "-d", + "--debug", + action="store_true", + help="Add source-level debugging information to font.", + ) + parser.add_argument( + "-v", + "--verbose", + help="increase the logger verbosity. Multiple -v " "options are allowed.", + action="count", + default=0, + ) parser.add_argument( - "-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) - 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"] @@ -42,7 +64,9 @@ font = TTFont(options.input_font) try: - addOpenTypeFeatures(font, options.input_fea, tables=options.tables) + addOpenTypeFeatures( + font, options.input_fea, tables=options.tables, debug=options.debug + ) except FeatureLibError as e: if options.traceback: raise @@ -50,5 +74,5 @@ font.save(output_font) -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(main()) diff -Nru fonttools-4.13.0/Lib/fontTools/feaLib/parser.py fonttools-4.16.1/Lib/fontTools/feaLib/parser.py --- fonttools-4.13.0/Lib/fontTools/feaLib/parser.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/feaLib/parser.py 2020-10-05 18:25:14.000000000 +0000 @@ -35,25 +35,30 @@ ``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 @@ 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 @@ 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 @@ 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 @@ 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 ``[ mark @MARKCLASS]*.`` @@ -183,9 +199,9 @@ 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 @@ 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 @@ 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 `_). @@ -243,9 +258,9 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 `_.""" @@ -1061,12 +1133,12 @@ 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 @@ 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 @@ 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 @@ 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 `_) - 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 @@ 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 @@ 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 @@ 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 `_) @@ -1290,14 +1408,13 @@ 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 @@ 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 @@ 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 `_.""" 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 @@ # Parses a ``cvParameters`` block found in Character Variant features. # See section `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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ @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 @@ 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 -Nru fonttools-4.13.0/Lib/fontTools/__init__.py fonttools-4.16.1/Lib/fontTools/__init__.py --- fonttools-4.13.0/Lib/fontTools/__init__.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/__init__.py 2020-10-05 18:25:14.000000000 +0000 @@ -4,6 +4,6 @@ log = logging.getLogger(__name__) -version = __version__ = "4.13.0" +version = __version__ = "4.16.1" __all__ = ["version", "log", "configLogger"] diff -Nru fonttools-4.13.0/Lib/fontTools/misc/plistlib/__init__.py fonttools-4.16.1/Lib/fontTools/misc/plistlib/__init__.py --- fonttools-4.13.0/Lib/fontTools/misc/plistlib/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/misc/plistlib/__init__.py 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,680 @@ +import collections.abc +import sys +import re +from typing import ( + Any, + Callable, + Dict, + List, + Mapping, + MutableMapping, + Optional, + Sequence, + Type, + Union, + IO, +) +import warnings +from io import BytesIO +from datetime import datetime +from base64 import b64encode, b64decode +from numbers import Integral +from types import SimpleNamespace +from functools import singledispatch + +from fontTools.misc import etree + +from fontTools.misc.py23 import ( + tounicode, + tobytes, +) + +# By default, we +# - deserialize elements as bytes and +# - serialize bytes as elements. +# Before, on Python 2, we +# - deserialized elements as plistlib.Data objects, in order to +# distinguish them from the built-in str type (which is bytes on python2) +# - serialized bytes as elements (they must have only contained +# ASCII characters in this case) +# You can pass use_builtin_types=[True|False] to the load/dump etc. functions +# to enforce a specific treatment. +# NOTE that unicode type always maps to element, and plistlib.Data +# always maps to element, regardless of use_builtin_types. +USE_BUILTIN_TYPES = True + +XML_DECLARATION = b"""""" + +PLIST_DOCTYPE = ( + b'' +) + + +# Date should conform to a subset of ISO 8601: +# YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z' +_date_parser = re.compile( + r"(?P\d\d\d\d)" + r"(?:-(?P\d\d)" + r"(?:-(?P\d\d)" + r"(?:T(?P\d\d)" + r"(?::(?P\d\d)" + r"(?::(?P\d\d))" + r"?)?)?)?)?Z", + re.ASCII, +) + + +def _date_from_string(s: str) -> datetime: + order = ("year", "month", "day", "hour", "minute", "second") + m = _date_parser.match(s) + if m is None: + raise ValueError(f"Expected ISO 8601 date string, but got '{s:r}'.") + gd = m.groupdict() + lst = [] + for key in order: + val = gd[key] + if val is None: + break + lst.append(int(val)) + # NOTE: mypy doesn't know that lst is 6 elements long. + return datetime(*lst) # type:ignore + + +def _date_to_string(d: datetime) -> str: + return "%04d-%02d-%02dT%02d:%02d:%02dZ" % ( + d.year, + d.month, + d.day, + d.hour, + d.minute, + d.second, + ) + + +class Data: + """Represents binary data when ``use_builtin_types=False.`` + + This class wraps binary data loaded from a plist file when the + ``use_builtin_types`` argument to the loading function (:py:func:`fromtree`, + :py:func:`load`, :py:func:`loads`) is false. + + The actual binary data is retrieved using the ``data`` attribute. + """ + + def __init__(self, data: bytes) -> None: + if not isinstance(data, bytes): + raise TypeError("Expected bytes, found %s" % type(data).__name__) + self.data = data + + @classmethod + def fromBase64(cls, data: Union[bytes, str]) -> "Data": + return cls(b64decode(data)) + + def asBase64(self, maxlinelength: int = 76, indent_level: int = 1) -> bytes: + return _encode_base64( + self.data, maxlinelength=maxlinelength, indent_level=indent_level + ) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, self.__class__): + return self.data == other.data + elif isinstance(other, bytes): + return self.data == other + else: + return NotImplemented + + def __repr__(self) -> str: + return "%s(%s)" % (self.__class__.__name__, repr(self.data)) + + +def _encode_base64( + data: bytes, maxlinelength: Optional[int] = 76, indent_level: int = 1 +) -> bytes: + data = b64encode(data) + if data and maxlinelength: + # split into multiple lines right-justified to 'maxlinelength' chars + indent = b"\n" + b" " * indent_level + max_length = max(16, maxlinelength - len(indent)) + chunks = [] + for i in range(0, len(data), max_length): + chunks.append(indent) + chunks.append(data[i : i + max_length]) + chunks.append(indent) + data = b"".join(chunks) + return data + + +# Mypy does not support recursive type aliases as of 0.782, Pylance does. +# https://github.com/python/mypy/issues/731 +# https://devblogs.microsoft.com/python/pylance-introduces-five-new-features-that-enable-type-magic-for-python-developers/#1-support-for-recursive-type-aliases +PlistEncodable = Union[ + bool, + bytes, + Data, + datetime, + float, + int, + Mapping[str, Any], + Sequence[Any], + str, +] + + +class PlistTarget: + """Event handler using the ElementTree Target API that can be + passed to a XMLParser to produce property list objects from XML. + It is based on the CPython plistlib module's _PlistParser class, + but does not use the expat parser. + + >>> from fontTools.misc import etree + >>> parser = etree.XMLParser(target=PlistTarget()) + >>> result = etree.XML( + ... "" + ... " something" + ... " blah" + ... "", + ... parser=parser) + >>> result == {"something": "blah"} + True + + Links: + https://github.com/python/cpython/blob/master/Lib/plistlib.py + http://lxml.de/parsing.html#the-target-parser-interface + """ + + def __init__( + self, + use_builtin_types: Optional[bool] = None, + dict_type: Type[MutableMapping[str, Any]] = dict, + ) -> None: + self.stack: List[PlistEncodable] = [] + self.current_key: Optional[str] = None + self.root: Optional[PlistEncodable] = None + if use_builtin_types is None: + self._use_builtin_types = USE_BUILTIN_TYPES + else: + if use_builtin_types is False: + warnings.warn( + "Setting use_builtin_types to False is deprecated and will be " + "removed soon.", + DeprecationWarning, + ) + self._use_builtin_types = use_builtin_types + self._dict_type = dict_type + + def start(self, tag: str, attrib: Mapping[str, str]) -> None: + self._data: List[str] = [] + handler = _TARGET_START_HANDLERS.get(tag) + if handler is not None: + handler(self) + + def end(self, tag: str) -> None: + handler = _TARGET_END_HANDLERS.get(tag) + if handler is not None: + handler(self) + + def data(self, data: str) -> None: + self._data.append(data) + + def close(self) -> PlistEncodable: + if self.root is None: + raise ValueError("No root set.") + return self.root + + # helpers + + def add_object(self, value: PlistEncodable) -> None: + if self.current_key is not None: + stack_top = self.stack[-1] + if not isinstance(stack_top, collections.abc.MutableMapping): + raise ValueError("unexpected element: %r" % stack_top) + stack_top[self.current_key] = value + self.current_key = None + elif not self.stack: + # this is the root object + self.root = value + else: + stack_top = self.stack[-1] + if not isinstance(stack_top, list): + raise ValueError("unexpected element: %r" % stack_top) + stack_top.append(value) + + def get_data(self) -> str: + data = "".join(self._data) + self._data = [] + return data + + +# event handlers + + +def start_dict(self: PlistTarget) -> None: + d = self._dict_type() + self.add_object(d) + self.stack.append(d) + + +def end_dict(self: PlistTarget) -> None: + if self.current_key: + raise ValueError("missing value for key '%s'" % self.current_key) + self.stack.pop() + + +def end_key(self: PlistTarget) -> None: + if self.current_key or not isinstance(self.stack[-1], collections.abc.Mapping): + raise ValueError("unexpected key") + self.current_key = self.get_data() + + +def start_array(self: PlistTarget) -> None: + a: List[PlistEncodable] = [] + self.add_object(a) + self.stack.append(a) + + +def end_array(self: PlistTarget) -> None: + self.stack.pop() + + +def end_true(self: PlistTarget) -> None: + self.add_object(True) + + +def end_false(self: PlistTarget) -> None: + self.add_object(False) + + +def end_integer(self: PlistTarget) -> None: + self.add_object(int(self.get_data())) + + +def end_real(self: PlistTarget) -> None: + self.add_object(float(self.get_data())) + + +def end_string(self: PlistTarget) -> None: + self.add_object(self.get_data()) + + +def end_data(self: PlistTarget) -> None: + if self._use_builtin_types: + self.add_object(b64decode(self.get_data())) + else: + self.add_object(Data.fromBase64(self.get_data())) + + +def end_date(self: PlistTarget) -> None: + self.add_object(_date_from_string(self.get_data())) + + +_TARGET_START_HANDLERS: Dict[str, Callable[[PlistTarget], None]] = { + "dict": start_dict, + "array": start_array, +} + +_TARGET_END_HANDLERS: Dict[str, Callable[[PlistTarget], None]] = { + "dict": end_dict, + "array": end_array, + "key": end_key, + "true": end_true, + "false": end_false, + "integer": end_integer, + "real": end_real, + "string": end_string, + "data": end_data, + "date": end_date, +} + + +# functions to build element tree from plist data + + +def _string_element(value: str, ctx: SimpleNamespace) -> etree.Element: + el = etree.Element("string") + el.text = value + return el + + +def _bool_element(value: bool, ctx: SimpleNamespace) -> etree.Element: + if value: + return etree.Element("true") + return etree.Element("false") + + +def _integer_element(value: int, ctx: SimpleNamespace) -> etree.Element: + if -1 << 63 <= value < 1 << 64: + el = etree.Element("integer") + el.text = "%d" % value + return el + raise OverflowError(value) + + +def _real_element(value: float, ctx: SimpleNamespace) -> etree.Element: + el = etree.Element("real") + el.text = repr(value) + return el + + +def _dict_element(d: Mapping[str, PlistEncodable], ctx: SimpleNamespace) -> etree.Element: + el = etree.Element("dict") + items = d.items() + if ctx.sort_keys: + items = sorted(items) # type: ignore + ctx.indent_level += 1 + for key, value in items: + if not isinstance(key, str): + if ctx.skipkeys: + continue + raise TypeError("keys must be strings") + k = etree.SubElement(el, "key") + k.text = tounicode(key, "utf-8") + el.append(_make_element(value, ctx)) + ctx.indent_level -= 1 + return el + + +def _array_element(array: Sequence[PlistEncodable], ctx: SimpleNamespace) -> etree.Element: + el = etree.Element("array") + if len(array) == 0: + return el + ctx.indent_level += 1 + for value in array: + el.append(_make_element(value, ctx)) + ctx.indent_level -= 1 + return el + + +def _date_element(date: datetime, ctx: SimpleNamespace) -> etree.Element: + el = etree.Element("date") + el.text = _date_to_string(date) + return el + + +def _data_element(data: bytes, ctx: SimpleNamespace) -> etree.Element: + el = etree.Element("data") + # NOTE: mypy is confused about whether el.text should be str or bytes. + el.text = _encode_base64( # type: ignore + data, + maxlinelength=(76 if ctx.pretty_print else None), + indent_level=ctx.indent_level, + ) + return el + + +def _string_or_data_element(raw_bytes: bytes, ctx: SimpleNamespace) -> etree.Element: + if ctx.use_builtin_types: + return _data_element(raw_bytes, ctx) + else: + try: + string = raw_bytes.decode(encoding="ascii", errors="strict") + except UnicodeDecodeError: + raise ValueError( + "invalid non-ASCII bytes; use unicode string instead: %r" % raw_bytes + ) + return _string_element(string, ctx) + + +# The following is probably not entirely correct. The signature should take `Any` +# and return `NoReturn`. At the time of this writing, neither mypy nor Pyright +# can deal with singledispatch properly and will apply the signature of the base +# function to all others. Being slightly dishonest makes it type-check and return +# usable typing information for the optimistic case. +@singledispatch +def _make_element(value: PlistEncodable, ctx: SimpleNamespace) -> etree.Element: + raise TypeError("unsupported type: %s" % type(value)) + + +_make_element.register(str)(_string_element) +_make_element.register(bool)(_bool_element) +_make_element.register(Integral)(_integer_element) +_make_element.register(float)(_real_element) +_make_element.register(collections.abc.Mapping)(_dict_element) +_make_element.register(list)(_array_element) +_make_element.register(tuple)(_array_element) +_make_element.register(datetime)(_date_element) +_make_element.register(bytes)(_string_or_data_element) +_make_element.register(bytearray)(_data_element) +_make_element.register(Data)(lambda v, ctx: _data_element(v.data, ctx)) + + +# Public functions to create element tree from plist-compatible python +# data structures and viceversa, for use when (de)serializing GLIF xml. + + +def totree( + value: PlistEncodable, + sort_keys: bool = True, + skipkeys: bool = False, + use_builtin_types: Optional[bool] = None, + pretty_print: bool = True, + indent_level: int = 1, +) -> etree.Element: + """Convert a value derived from a plist into an XML tree. + + Args: + value: Any kind of value to be serialized to XML. + sort_keys: Whether keys of dictionaries should be sorted. + skipkeys (bool): Whether to silently skip non-string dictionary + keys. + use_builtin_types (bool): If true, byte strings will be + encoded in Base-64 and wrapped in a ``data`` tag; if + false, they will be either stored as ASCII strings or an + exception raised if they cannot be decoded as such. Defaults + to ``True`` if not present. Deprecated. + pretty_print (bool): Whether to indent the output. + indent_level (int): Level of indentation when serializing. + + Returns: an ``etree`` ``Element`` object. + + Raises: + ``TypeError`` + if non-string dictionary keys are serialized + and ``skipkeys`` is false. + ``ValueError`` + if non-ASCII binary data is present + and `use_builtin_types` is false. + """ + if use_builtin_types is None: + use_builtin_types = USE_BUILTIN_TYPES + else: + use_builtin_types = use_builtin_types + context = SimpleNamespace( + sort_keys=sort_keys, + skipkeys=skipkeys, + use_builtin_types=use_builtin_types, + pretty_print=pretty_print, + indent_level=indent_level, + ) + return _make_element(value, context) + + +def fromtree( + tree: etree.Element, + use_builtin_types: Optional[bool] = None, + dict_type: Type[MutableMapping[str, Any]] = dict, +) -> Any: + """Convert an XML tree to a plist structure. + + Args: + tree: An ``etree`` ``Element``. + use_builtin_types: If True, binary data is deserialized to + bytes strings. If False, it is wrapped in :py:class:`Data` + objects. Defaults to True if not provided. Deprecated. + dict_type: What type to use for dictionaries. + + Returns: An object (usually a dictionary). + """ + target = PlistTarget(use_builtin_types=use_builtin_types, dict_type=dict_type) + for action, element in etree.iterwalk(tree, events=("start", "end")): + if action == "start": + target.start(element.tag, element.attrib) + elif action == "end": + # if there are no children, parse the leaf's data + if not len(element): + # always pass str, not None + target.data(element.text or "") + target.end(element.tag) + return target.close() + + +# python3 plistlib API + + +def load( + fp: IO[bytes], + use_builtin_types: Optional[bool] = None, + dict_type: Type[MutableMapping[str, Any]] = dict, +) -> Any: + """Load a plist file into an object. + + Args: + fp: An opened file. + use_builtin_types: If True, binary data is deserialized to + bytes strings. If False, it is wrapped in :py:class:`Data` + objects. Defaults to True if not provided. Deprecated. + dict_type: What type to use for dictionaries. + + Returns: + An object (usually a dictionary) representing the top level of + the plist file. + """ + + if not hasattr(fp, "read"): + raise AttributeError("'%s' object has no attribute 'read'" % type(fp).__name__) + target = PlistTarget(use_builtin_types=use_builtin_types, dict_type=dict_type) + parser = etree.XMLParser(target=target) # type: ignore + result = etree.parse(fp, parser=parser) + # lxml returns the target object directly, while ElementTree wraps + # it as the root of an ElementTree object + try: + return result.getroot() + except AttributeError: + return result + + +def loads( + value: bytes, + use_builtin_types: Optional[bool] = None, + dict_type: Type[MutableMapping[str, Any]] = dict, +) -> Any: + """Load a plist file from a string into an object. + + Args: + value: A bytes string containing a plist. + use_builtin_types: If True, binary data is deserialized to + bytes strings. If False, it is wrapped in :py:class:`Data` + objects. Defaults to True if not provided. Deprecated. + dict_type: What type to use for dictionaries. + + Returns: + An object (usually a dictionary) representing the top level of + the plist file. + """ + + fp = BytesIO(value) + return load(fp, use_builtin_types=use_builtin_types, dict_type=dict_type) + + +def dump( + value: PlistEncodable, + fp: IO[bytes], + sort_keys: bool = True, + skipkeys: bool = False, + use_builtin_types: Optional[bool] = None, + pretty_print: bool = True, +) -> None: + """Write a Python object to a plist file. + + Args: + value: An object to write. + fp: A file opened for writing. + sort_keys (bool): Whether keys of dictionaries should be sorted. + skipkeys (bool): Whether to silently skip non-string dictionary + keys. + use_builtin_types (bool): If true, byte strings will be + encoded in Base-64 and wrapped in a ``data`` tag; if + false, they will be either stored as ASCII strings or an + exception raised if they cannot be represented. Defaults + pretty_print (bool): Whether to indent the output. + indent_level (int): Level of indentation when serializing. + + Raises: + ``TypeError`` + if non-string dictionary keys are serialized + and ``skipkeys`` is false. + ``ValueError`` + if non-representable binary data is present + and `use_builtin_types` is false. + """ + + if not hasattr(fp, "write"): + raise AttributeError("'%s' object has no attribute 'write'" % type(fp).__name__) + root = etree.Element("plist", version="1.0") + el = totree( + value, + sort_keys=sort_keys, + skipkeys=skipkeys, + use_builtin_types=use_builtin_types, + pretty_print=pretty_print, + ) + root.append(el) + tree = etree.ElementTree(root) + # we write the doctype ourselves instead of using the 'doctype' argument + # of 'write' method, becuse lxml will force adding a '\n' even when + # pretty_print is False. + if pretty_print: + header = b"\n".join((XML_DECLARATION, PLIST_DOCTYPE, b"")) + else: + header = XML_DECLARATION + PLIST_DOCTYPE + fp.write(header) + tree.write( # type: ignore + fp, + encoding="utf-8", + pretty_print=pretty_print, + xml_declaration=False, + ) + + +def dumps( + value: PlistEncodable, + sort_keys: bool = True, + skipkeys: bool = False, + use_builtin_types: Optional[bool] = None, + pretty_print: bool = True, +) -> bytes: + """Write a Python object to a string in plist format. + + Args: + value: An object to write. + sort_keys (bool): Whether keys of dictionaries should be sorted. + skipkeys (bool): Whether to silently skip non-string dictionary + keys. + use_builtin_types (bool): If true, byte strings will be + encoded in Base-64 and wrapped in a ``data`` tag; if + false, they will be either stored as strings or an + exception raised if they cannot be represented. Defaults + pretty_print (bool): Whether to indent the output. + indent_level (int): Level of indentation when serializing. + + Returns: + string: A plist representation of the Python object. + + Raises: + ``TypeError`` + if non-string dictionary keys are serialized + and ``skipkeys`` is false. + ``ValueError`` + if non-representable binary data is present + and `use_builtin_types` is false. + """ + fp = BytesIO() + dump( + value, + fp, + sort_keys=sort_keys, + skipkeys=skipkeys, + use_builtin_types=use_builtin_types, + pretty_print=pretty_print, + ) + return fp.getvalue() diff -Nru fonttools-4.13.0/Lib/fontTools/misc/plistlib.py fonttools-4.16.1/Lib/fontTools/misc/plistlib.py --- fonttools-4.13.0/Lib/fontTools/misc/plistlib.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/misc/plistlib.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,623 +0,0 @@ -import sys -import re -import warnings -from io import BytesIO -from datetime import datetime -from base64 import b64encode, b64decode -from numbers import Integral - -from types import SimpleNamespace -from collections.abc import Mapping -from functools import singledispatch - -from fontTools.misc import etree - -from fontTools.misc.py23 import ( - tounicode, - tobytes, -) - -# By default, we -# - deserialize elements as bytes and -# - serialize bytes as elements. -# Before, on Python 2, we -# - deserialized elements as plistlib.Data objects, in order to -# distinguish them from the built-in str type (which is bytes on python2) -# - serialized bytes as elements (they must have only contained -# ASCII characters in this case) -# You can pass use_builtin_types=[True|False] to the load/dump etc. functions -# to enforce a specific treatment. -# NOTE that unicode type always maps to element, and plistlib.Data -# always maps to element, regardless of use_builtin_types. -USE_BUILTIN_TYPES = True - -XML_DECLARATION = b"""""" - -PLIST_DOCTYPE = ( - b'' -) - -# Date should conform to a subset of ISO 8601: -# YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z' -_date_parser = re.compile( - r"(?P\d\d\d\d)" - r"(?:-(?P\d\d)" - r"(?:-(?P\d\d)" - r"(?:T(?P\d\d)" - r"(?::(?P\d\d)" - r"(?::(?P\d\d))" - r"?)?)?)?)?Z", - re.ASCII -) - - -def _date_from_string(s): - order = ("year", "month", "day", "hour", "minute", "second") - gd = _date_parser.match(s).groupdict() - lst = [] - for key in order: - val = gd[key] - if val is None: - break - lst.append(int(val)) - return datetime(*lst) - - -def _date_to_string(d): - return "%04d-%02d-%02dT%02d:%02d:%02dZ" % ( - d.year, - d.month, - d.day, - d.hour, - d.minute, - d.second, - ) - - -def _encode_base64(data, maxlinelength=76, indent_level=1): - data = b64encode(data) - if data and maxlinelength: - # split into multiple lines right-justified to 'maxlinelength' chars - indent = b"\n" + b" " * indent_level - max_length = max(16, maxlinelength - len(indent)) - chunks = [] - for i in range(0, len(data), max_length): - chunks.append(indent) - chunks.append(data[i : i + max_length]) - chunks.append(indent) - data = b"".join(chunks) - return data - - -class Data: - """Represents binary data when ``use_builtin_types=False.`` - - This class wraps binary data loaded from a plist file when the - ``use_builtin_types`` argument to the loading function (:py:func:`fromtree`, - :py:func:`load`, :py:func:`loads`) is false. - - The actual binary data is retrieved using the ``data`` attribute. - """ - - def __init__(self, data): - if not isinstance(data, bytes): - raise TypeError("Expected bytes, found %s" % type(data).__name__) - self.data = data - - @classmethod - def fromBase64(cls, data): - return cls(b64decode(data)) - - def asBase64(self, maxlinelength=76, indent_level=1): - return _encode_base64( - self.data, maxlinelength=maxlinelength, indent_level=indent_level - ) - - def __eq__(self, other): - if isinstance(other, self.__class__): - return self.data == other.data - elif isinstance(other, bytes): - return self.data == other - else: - return NotImplemented - - def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, repr(self.data)) - - -class PlistTarget: - """ Event handler using the ElementTree Target API that can be - passed to a XMLParser to produce property list objects from XML. - It is based on the CPython plistlib module's _PlistParser class, - but does not use the expat parser. - - >>> from fontTools.misc import etree - >>> parser = etree.XMLParser(target=PlistTarget()) - >>> result = etree.XML( - ... "" - ... " something" - ... " blah" - ... "", - ... parser=parser) - >>> result == {"something": "blah"} - True - - Links: - https://github.com/python/cpython/blob/master/Lib/plistlib.py - http://lxml.de/parsing.html#the-target-parser-interface - """ - - def __init__(self, use_builtin_types=None, dict_type=dict): - self.stack = [] - self.current_key = None - self.root = None - if use_builtin_types is None: - self._use_builtin_types = USE_BUILTIN_TYPES - else: - if use_builtin_types is False: - warnings.warn( - "Setting use_builtin_types to False is deprecated and will be " - "removed soon.", - DeprecationWarning, - ) - self._use_builtin_types = use_builtin_types - self._dict_type = dict_type - - def start(self, tag, attrib): - self._data = [] - handler = _TARGET_START_HANDLERS.get(tag) - if handler is not None: - handler(self) - - def end(self, tag): - handler = _TARGET_END_HANDLERS.get(tag) - if handler is not None: - handler(self) - - def data(self, data): - self._data.append(data) - - def close(self): - return self.root - - # helpers - - def add_object(self, value): - if self.current_key is not None: - if not isinstance(self.stack[-1], type({})): - raise ValueError("unexpected element: %r" % self.stack[-1]) - self.stack[-1][self.current_key] = value - self.current_key = None - elif not self.stack: - # this is the root object - self.root = value - else: - if not isinstance(self.stack[-1], type([])): - raise ValueError("unexpected element: %r" % self.stack[-1]) - self.stack[-1].append(value) - - def get_data(self): - data = "".join(self._data) - self._data = [] - return data - - -# event handlers - - -def start_dict(self): - d = self._dict_type() - self.add_object(d) - self.stack.append(d) - - -def end_dict(self): - if self.current_key: - raise ValueError("missing value for key '%s'" % self.current_key) - self.stack.pop() - - -def end_key(self): - if self.current_key or not isinstance(self.stack[-1], type({})): - raise ValueError("unexpected key") - self.current_key = self.get_data() - - -def start_array(self): - a = [] - self.add_object(a) - self.stack.append(a) - - -def end_array(self): - self.stack.pop() - - -def end_true(self): - self.add_object(True) - - -def end_false(self): - self.add_object(False) - - -def end_integer(self): - self.add_object(int(self.get_data())) - - -def end_real(self): - self.add_object(float(self.get_data())) - - -def end_string(self): - self.add_object(self.get_data()) - - -def end_data(self): - if self._use_builtin_types: - self.add_object(b64decode(self.get_data())) - else: - self.add_object(Data.fromBase64(self.get_data())) - - -def end_date(self): - self.add_object(_date_from_string(self.get_data())) - - -_TARGET_START_HANDLERS = {"dict": start_dict, "array": start_array} - -_TARGET_END_HANDLERS = { - "dict": end_dict, - "array": end_array, - "key": end_key, - "true": end_true, - "false": end_false, - "integer": end_integer, - "real": end_real, - "string": end_string, - "data": end_data, - "date": end_date, -} - - -# functions to build element tree from plist data - - -def _string_element(value, ctx): - el = etree.Element("string") - el.text = value - return el - - -def _bool_element(value, ctx): - if value: - return etree.Element("true") - else: - return etree.Element("false") - - -def _integer_element(value, ctx): - if -1 << 63 <= value < 1 << 64: - el = etree.Element("integer") - el.text = "%d" % value - return el - else: - raise OverflowError(value) - - -def _real_element(value, ctx): - el = etree.Element("real") - el.text = repr(value) - return el - - -def _dict_element(d, ctx): - el = etree.Element("dict") - items = d.items() - if ctx.sort_keys: - items = sorted(items) - ctx.indent_level += 1 - for key, value in items: - if not isinstance(key, str): - if ctx.skipkeys: - continue - raise TypeError("keys must be strings") - k = etree.SubElement(el, "key") - k.text = tounicode(key, "utf-8") - el.append(_make_element(value, ctx)) - ctx.indent_level -= 1 - return el - - -def _array_element(array, ctx): - el = etree.Element("array") - if len(array) == 0: - return el - ctx.indent_level += 1 - for value in array: - el.append(_make_element(value, ctx)) - ctx.indent_level -= 1 - return el - - -def _date_element(date, ctx): - el = etree.Element("date") - el.text = _date_to_string(date) - return el - - -def _data_element(data, ctx): - el = etree.Element("data") - el.text = _encode_base64( - data, - maxlinelength=(76 if ctx.pretty_print else None), - indent_level=ctx.indent_level, - ) - return el - - -def _string_or_data_element(raw_bytes, ctx): - if ctx.use_builtin_types: - return _data_element(raw_bytes, ctx) - else: - try: - string = raw_bytes.decode(encoding="ascii", errors="strict") - except UnicodeDecodeError: - raise ValueError( - "invalid non-ASCII bytes; use unicode string instead: %r" - % raw_bytes - ) - return _string_element(string, ctx) - - -@singledispatch -def _make_element(value, ctx): - raise TypeError("unsupported type: %s" % type(value)) - -_make_element.register(str)(_string_element) -_make_element.register(bool)(_bool_element) -_make_element.register(Integral)(_integer_element) -_make_element.register(float)(_real_element) -_make_element.register(Mapping)(_dict_element) -_make_element.register(list)(_array_element) -_make_element.register(tuple)(_array_element) -_make_element.register(datetime)(_date_element) -_make_element.register(bytes)(_string_or_data_element) -_make_element.register(bytearray)(_data_element) -_make_element.register(Data)(lambda v, ctx: _data_element(v.data, ctx)) - - -# Public functions to create element tree from plist-compatible python -# data structures and viceversa, for use when (de)serializing GLIF xml. - - -def totree( - value, - sort_keys=True, - skipkeys=False, - use_builtin_types=None, - pretty_print=True, - indent_level=1, -): - """Convert a value derived from a plist into an XML tree. - - Args: - value: Any kind of value to be serialized to XML. - sort_keys: Whether keys of dictionaries should be sorted. - skipkeys (bool): Whether to silently skip non-string dictionary - keys. - use_builtin_types (bool): If true, byte strings will be - encoded in Base-64 and wrapped in a ``data`` tag; if - false, they will be either stored as ASCII strings or an - exception raised if they cannot be decoded as such. Defaults - to ``True`` if not present. Deprecated. - pretty_print (bool): Whether to indent the output. - indent_level (int): Level of indentation when serializing. - - Returns: an ``etree`` ``Element`` object. - - Raises: - ``TypeError`` - if non-string dictionary keys are serialized - and ``skipkeys`` is false. - ``ValueError`` - if non-ASCII binary data is present - and `use_builtin_types` is false. - """ - if use_builtin_types is None: - use_builtin_types = USE_BUILTIN_TYPES - else: - use_builtin_types = use_builtin_types - context = SimpleNamespace( - sort_keys=sort_keys, - skipkeys=skipkeys, - use_builtin_types=use_builtin_types, - pretty_print=pretty_print, - indent_level=indent_level, - ) - return _make_element(value, context) - - -def fromtree(tree, use_builtin_types=None, dict_type=dict): - """Convert an XML tree to a plist structure. - - Args: - tree: An ``etree`` ``Element``. - use_builtin_types: If True, binary data is deserialized to - bytes strings. If False, it is wrapped in :py:class:`Data` - objects. Defaults to True if not provided. Deprecated. - dict_type: What type to use for dictionaries. - - Returns: An object (usually a dictionary). - """ - target = PlistTarget( - use_builtin_types=use_builtin_types, dict_type=dict_type - ) - for action, element in etree.iterwalk(tree, events=("start", "end")): - if action == "start": - target.start(element.tag, element.attrib) - elif action == "end": - # if there are no children, parse the leaf's data - if not len(element): - # always pass str, not None - target.data(element.text or "") - target.end(element.tag) - return target.close() - - -# python3 plistlib API - - -def load(fp, use_builtin_types=None, dict_type=dict): - """Load a plist file into an object. - - Args: - fp: An opened file. - use_builtin_types: If True, binary data is deserialized to - bytes strings. If False, it is wrapped in :py:class:`Data` - objects. Defaults to True if not provided. Deprecated. - dict_type: What type to use for dictionaries. - - Returns: - An object (usually a dictionary) representing the top level of - the plist file. - """ - - if not hasattr(fp, "read"): - raise AttributeError( - "'%s' object has no attribute 'read'" % type(fp).__name__ - ) - target = PlistTarget( - use_builtin_types=use_builtin_types, dict_type=dict_type - ) - parser = etree.XMLParser(target=target) - result = etree.parse(fp, parser=parser) - # lxml returns the target object directly, while ElementTree wraps - # it as the root of an ElementTree object - try: - return result.getroot() - except AttributeError: - return result - - -def loads(value, use_builtin_types=None, dict_type=dict): - """Load a plist file from a string into an object. - - Args: - value: A string containing a plist. - use_builtin_types: If True, binary data is deserialized to - bytes strings. If False, it is wrapped in :py:class:`Data` - objects. Defaults to True if not provided. Deprecated. - dict_type: What type to use for dictionaries. - - Returns: - An object (usually a dictionary) representing the top level of - the plist file. - """ - - fp = BytesIO(value) - return load(fp, use_builtin_types=use_builtin_types, dict_type=dict_type) - - -def dump( - value, - fp, - sort_keys=True, - skipkeys=False, - use_builtin_types=None, - pretty_print=True, -): - """Write a Python object to a plist file. - - Args: - value: An object to write. - fp: A file opened for writing. - sort_keys (bool): Whether keys of dictionaries should be sorted. - skipkeys (bool): Whether to silently skip non-string dictionary - keys. - use_builtin_types (bool): If true, byte strings will be - encoded in Base-64 and wrapped in a ``data`` tag; if - false, they will be either stored as ASCII strings or an - exception raised if they cannot be represented. Defaults - pretty_print (bool): Whether to indent the output. - indent_level (int): Level of indentation when serializing. - - Raises: - ``TypeError`` - if non-string dictionary keys are serialized - and ``skipkeys`` is false. - ``ValueError`` - if non-representable binary data is present - and `use_builtin_types` is false. - """ - - if not hasattr(fp, "write"): - raise AttributeError( - "'%s' object has no attribute 'write'" % type(fp).__name__ - ) - root = etree.Element("plist", version="1.0") - el = totree( - value, - sort_keys=sort_keys, - skipkeys=skipkeys, - use_builtin_types=use_builtin_types, - pretty_print=pretty_print, - ) - root.append(el) - tree = etree.ElementTree(root) - # we write the doctype ourselves instead of using the 'doctype' argument - # of 'write' method, becuse lxml will force adding a '\n' even when - # pretty_print is False. - if pretty_print: - header = b"\n".join((XML_DECLARATION, PLIST_DOCTYPE, b"")) - else: - header = XML_DECLARATION + PLIST_DOCTYPE - fp.write(header) - tree.write( - fp, encoding="utf-8", pretty_print=pretty_print, xml_declaration=False - ) - - -def dumps( - value, - sort_keys=True, - skipkeys=False, - use_builtin_types=None, - pretty_print=True, -): - """Write a Python object to a string in plist format. - - Args: - value: An object to write. - sort_keys (bool): Whether keys of dictionaries should be sorted. - skipkeys (bool): Whether to silently skip non-string dictionary - keys. - use_builtin_types (bool): If true, byte strings will be - encoded in Base-64 and wrapped in a ``data`` tag; if - false, they will be either stored as strings or an - exception raised if they cannot be represented. Defaults - pretty_print (bool): Whether to indent the output. - indent_level (int): Level of indentation when serializing. - - Returns: - string: A plist representation of the Python object. - - Raises: - ``TypeError`` - if non-string dictionary keys are serialized - and ``skipkeys`` is false. - ``ValueError`` - if non-representable binary data is present - and `use_builtin_types` is false. - """ - fp = BytesIO() - dump( - value, - fp, - sort_keys=sort_keys, - skipkeys=skipkeys, - use_builtin_types=use_builtin_types, - pretty_print=pretty_print, - ) - return fp.getvalue() diff -Nru fonttools-4.13.0/Lib/fontTools/mtiLib/__init__.py fonttools-4.16.1/Lib/fontTools/mtiLib/__init__.py --- fonttools-4.13.0/Lib/fontTools/mtiLib/__init__.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/mtiLib/__init__.py 2020-10-05 18:25:14.000000000 +0000 @@ -1146,12 +1146,33 @@ return line def build(f, font, tableTag=None): + """Convert a Monotype font layout file to an OpenType layout object + + A font object must be passed, but this may be a "dummy" font; it is only + used for sorting glyph sets when making coverage tables and to hold the + OpenType layout table while it is being built. + + Args: + f: A file object. + font (TTFont): A font object. + tableTag (string): If provided, asserts that the file contains data for the + given OpenType table. + + Returns: + An object representing the table. (e.g. ``table_G_S_U_B_``) + """ lines = Tokenizer(f) return parseTable(lines, font, tableTag=tableTag) def main(args=None, font=None): - """Convert a FontDame OTL file to TTX XML""" + """Convert a FontDame OTL file to TTX XML. + + Writes XML output to stdout. + + Args: + args: Command line arguments (``--font``, ``--table``, input files). + """ import sys from fontTools import configLogger from fontTools.misc.testTools import MockFont diff -Nru fonttools-4.13.0/Lib/fontTools/otlLib/builder.py fonttools-4.16.1/Lib/fontTools/otlLib/builder.py --- fonttools-4.13.0/Lib/fontTools/otlLib/builder.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/otlLib/builder.py 2020-10-05 18:25:14.000000000 +0000 @@ -88,9 +88,10 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ `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 @@ 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 @@ 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 @@ 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 @@ `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 @@ 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 @@ `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 @@ 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 @@ `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 @@ 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 @@ `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 @@ `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 @@ `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 @@ 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 @@ `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 @@ 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 @@ `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 @@ """ 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 @@ `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 @@ `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 @@ 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 @@ 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 @@ 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 @@ 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 @@ `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 @@ # 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 @@ 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 @@ `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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ count += 1 return count + def buildValue(value): """Builds a positioning value record. @@ -2042,6 +2189,7 @@ # GDEF + def buildAttachList(attachPoints, glyphMap): """Builds an AttachList subtable. @@ -2061,8 +2209,7 @@ 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 @@ class ClassDefBuilder(object): """Helper for building ClassDef tables.""" + def __init__(self, useClass0): self.classes_ = set() self.glyphs_ = {} @@ -2380,7 +2528,7 @@ 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 @@ 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 @@ 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 -Nru fonttools-4.13.0/Lib/fontTools/otlLib/error.py fonttools-4.16.1/Lib/fontTools/otlLib/error.py --- fonttools-4.13.0/Lib/fontTools/otlLib/error.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/otlLib/error.py 2020-10-05 18:25:14.000000000 +0000 @@ -1,5 +1,3 @@ - - class OpenTypeLibError(Exception): def __init__(self, message, location): Exception.__init__(self, message) diff -Nru fonttools-4.13.0/Lib/fontTools/otlLib/maxContextCalc.py fonttools-4.16.1/Lib/fontTools/otlLib/maxContextCalc.py --- fonttools-4.13.0/Lib/fontTools/otlLib/maxContextCalc.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/otlLib/maxContextCalc.py 2020-10-05 18:25:14.000000000 +0000 @@ -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 @@ """ # 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 @@ 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 -Nru fonttools-4.13.0/Lib/fontTools/subset/__init__.py fonttools-4.16.1/Lib/fontTools/subset/__init__.py --- fonttools-4.13.0/Lib/fontTools/subset/__init__.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/subset/__init__.py 2020-10-05 18:25:14.000000000 +0000 @@ -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 @@ # 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 @@ -2708,7 +2711,7 @@ def load_font(fontFile, options, allowVID=False, - checkChecksums=False, + checkChecksums=0, dontLoadGlyphNames=False, lazy=True): diff -Nru fonttools-4.13.0/Lib/fontTools/ttLib/removeOverlaps.py fonttools-4.16.1/Lib/fontTools/ttLib/removeOverlaps.py --- fonttools-4.13.0/Lib/fontTools/ttLib/removeOverlaps.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/ttLib/removeOverlaps.py 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,192 @@ +""" Simplify TrueType glyphs by merging overlapping contours/components. + +Requires https://github.com/fonttools/skia-pathops +""" + +import itertools +import logging +from typing import Iterable, Optional, Mapping + +from fontTools.ttLib import ttFont +from fontTools.ttLib.tables import _g_l_y_f +from fontTools.ttLib.tables import _h_m_t_x +from fontTools.pens.ttGlyphPen import TTGlyphPen + +import pathops + + +__all__ = ["removeOverlaps"] + + +log = logging.getLogger("fontTools.ttLib.removeOverlaps") + +_TTGlyphMapping = Mapping[str, ttFont._TTGlyph] + + +def skPathFromGlyph(glyphName: str, glyphSet: _TTGlyphMapping) -> pathops.Path: + path = pathops.Path() + pathPen = path.getPen(glyphSet=glyphSet) + glyphSet[glyphName].draw(pathPen) + return path + + +def skPathFromGlyphComponent( + component: _g_l_y_f.GlyphComponent, glyphSet: _TTGlyphMapping +): + baseGlyphName, transformation = component.getComponentInfo() + path = skPathFromGlyph(baseGlyphName, glyphSet) + return path.transform(*transformation) + + +def componentsOverlap(glyph: _g_l_y_f.Glyph, glyphSet: _TTGlyphMapping) -> bool: + if not glyph.isComposite(): + raise ValueError("This method only works with TrueType composite glyphs") + if len(glyph.components) < 2: + return False # single component, no overlaps + + component_paths = {} + + def _get_nth_component_path(index: int) -> pathops.Path: + if index not in component_paths: + component_paths[index] = skPathFromGlyphComponent( + glyph.components[index], glyphSet + ) + return component_paths[index] + + return any( + pathops.op( + _get_nth_component_path(i), + _get_nth_component_path(j), + pathops.PathOp.INTERSECTION, + fix_winding=False, + keep_starting_points=False, + ) + for i, j in itertools.combinations(range(len(glyph.components)), 2) + ) + + +def ttfGlyphFromSkPath(path: pathops.Path) -> _g_l_y_f.Glyph: + # Skia paths have no 'components', no need for glyphSet + ttPen = TTGlyphPen(glyphSet=None) + path.draw(ttPen) + glyph = ttPen.glyph() + assert not glyph.isComposite() + # compute glyph.xMin (glyfTable parameter unused for non composites) + glyph.recalcBounds(glyfTable=None) + return glyph + + +def removeTTGlyphOverlaps( + glyphName: str, + glyphSet: _TTGlyphMapping, + glyfTable: _g_l_y_f.table__g_l_y_f, + hmtxTable: _h_m_t_x.table__h_m_t_x, + removeHinting: bool = True, +) -> bool: + glyph = glyfTable[glyphName] + # decompose composite glyphs only if components overlap each other + if ( + glyph.numberOfContours > 0 + or glyph.isComposite() + and componentsOverlap(glyph, glyphSet) + ): + path = skPathFromGlyph(glyphName, glyphSet) + + # remove overlaps + path2 = pathops.simplify(path, clockwise=path.clockwise) + + # replace TTGlyph if simplified path is different (ignoring contour order) + if {tuple(c) for c in path.contours} != {tuple(c) for c in path2.contours}: + glyfTable[glyphName] = glyph = ttfGlyphFromSkPath(path2) + # simplified glyph is always unhinted + assert not glyph.program + # also ensure hmtx LSB == glyph.xMin so glyph origin is at x=0 + width, lsb = hmtxTable[glyphName] + if lsb != glyph.xMin: + hmtxTable[glyphName] = (width, glyph.xMin) + return True + + if removeHinting: + glyph.removeHinting() + return False + + +def removeOverlaps( + font: ttFont.TTFont, + glyphNames: Optional[Iterable[str]] = None, + removeHinting: bool = True, +) -> None: + """Simplify glyphs in TTFont by merging overlapping contours. + + Overlapping components are first decomposed to simple contours, then merged. + + Currently this only works with TrueType fonts with 'glyf' table. + Raises NotImplementedError if 'glyf' table is absent. + + Note that removing overlaps invalidates the hinting. By default we drop hinting + from all glyphs whether or not overlaps are removed from a given one, as it would + look weird if only some glyphs are left (un)hinted. + + Args: + font: input TTFont object, modified in place. + glyphNames: optional iterable of glyph names (str) to remove overlaps from. + By default, all glyphs in the font are processed. + removeHinting (bool): set to False to keep hinting for unmodified glyphs. + """ + try: + glyfTable = font["glyf"] + except KeyError: + raise NotImplementedError("removeOverlaps currently only works with TTFs") + + hmtxTable = font["hmtx"] + # wraps the underlying glyf Glyphs, takes care of interfacing with drawing pens + glyphSet = font.getGlyphSet() + + if glyphNames is None: + glyphNames = font.getGlyphOrder() + + # process all simple glyphs first, then composites with increasing component depth, + # so that by the time we test for component intersections the respective base glyphs + # have already been simplified + glyphNames = sorted( + glyphNames, + key=lambda name: ( + glyfTable[name].getCompositeMaxpValues(glyfTable).maxComponentDepth + if glyfTable[name].isComposite() + else 0, + name, + ), + ) + modified = set() + for glyphName in glyphNames: + if removeTTGlyphOverlaps( + glyphName, glyphSet, glyfTable, hmtxTable, removeHinting + ): + modified.add(glyphName) + + log.debug("Removed overlaps for %s glyphs:\n%s", len(modified), " ".join(modified)) + + +def main(args=None): + import sys + + if args is None: + args = sys.argv[1:] + + if len(args) < 2: + print( + f"usage: fonttools ttLib.removeOverlaps INPUT.ttf OUTPUT.ttf [GLYPHS ...]" + ) + sys.exit(1) + + src = args[0] + dst = args[1] + glyphNames = args[2:] or None + + with ttFont.TTFont(src) as f: + removeOverlaps(f, glyphNames) + f.save(dst) + + +if __name__ == "__main__": + main() diff -Nru fonttools-4.13.0/Lib/fontTools/ttLib/sfnt.py fonttools-4.16.1/Lib/fontTools/ttLib/sfnt.py --- fonttools-4.13.0/Lib/fontTools/ttLib/sfnt.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/ttLib/sfnt.py 2020-10-05 18:25:14.000000000 +0000 @@ -43,7 +43,7 @@ # return default object return object.__new__(cls) - def __init__(self, file, checkChecksums=1, fontNumber=-1): + def __init__(self, file, checkChecksums=0, fontNumber=-1): self.file = file self.checkChecksums = checkChecksums diff -Nru fonttools-4.13.0/Lib/fontTools/ttLib/tables/D__e_b_g.py fonttools-4.16.1/Lib/fontTools/ttLib/tables/D__e_b_g.py --- fonttools-4.13.0/Lib/fontTools/ttLib/tables/D__e_b_g.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/ttLib/tables/D__e_b_g.py 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,17 @@ +import json + +from . import DefaultTable + + +class table_D__e_b_g(DefaultTable.DefaultTable): + def decompile(self, data, ttFont): + self.data = json.loads(data) + + def compile(self, ttFont): + return json.dumps(self.data).encode("utf-8") + + def toXML(self, writer, ttFont): + writer.writecdata(json.dumps(self.data)) + + def fromXML(self, name, attrs, content, ttFont): + self.data = json.loads(content) diff -Nru fonttools-4.13.0/Lib/fontTools/ttLib/tables/_g_l_y_f.py fonttools-4.16.1/Lib/fontTools/ttLib/tables/_g_l_y_f.py --- fonttools-4.13.0/Lib/fontTools/ttLib/tables/_g_l_y_f.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/ttLib/tables/_g_l_y_f.py 2020-10-05 18:25:14.000000000 +0000 @@ -650,6 +650,7 @@ 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 @@ 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 -Nru fonttools-4.13.0/Lib/fontTools/ttLib/tables/otTables.py fonttools-4.16.1/Lib/fontTools/ttLib/tables/otTables.py --- fonttools-4.13.0/Lib/fontTools/ttLib/tables/otTables.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/ttLib/tables/otTables.py 2020-10-05 18:25:14.000000000 +0000 @@ -12,6 +12,7 @@ from fontTools.misc.fixedTools import otRound from fontTools.misc.textTools import pad, safeEval from .otBase import BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference +from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY import logging import struct @@ -1187,6 +1188,44 @@ } +class LookupList(BaseTable): + @property + def table(self): + for l in self.Lookup: + for st in l.SubTable: + if type(st).__name__.endswith("Subst"): + return "GSUB" + if type(st).__name__.endswith("Pos"): + return "GPOS" + raise ValueError + + def toXML2(self, xmlWriter, font): + if not font or "Debg" not in font or LOOKUP_DEBUG_INFO_KEY not in font["Debg"].data: + return super().toXML2(xmlWriter, font) + debugData = font["Debg"].data[LOOKUP_DEBUG_INFO_KEY][self.table] + for conv in self.getConverters(): + if conv.repeat: + value = getattr(self, conv.name, []) + for lookupIndex, item in enumerate(value): + if str(lookupIndex) in debugData: + info = LookupDebugInfo(*debugData[str(lookupIndex)]) + tag = info.location + if info.name: + tag = f'{info.name}: {tag}' + if info.feature: + script,language,feature = info.feature + tag = f'{tag} in {feature} ({script}/{language})' + xmlWriter.comment(tag) + xmlWriter.newline() + + conv.xmlWrite(xmlWriter, font, item, conv.name, + [("index", lookupIndex)]) + else: + if conv.aux and not eval(conv.aux, None, vars(self)): + continue + value = getattr(self, conv.name, None) # TODO Handle defaults instead of defaulting to None! + conv.xmlWrite(xmlWriter, font, value, conv.name, []) + class BaseGlyphRecordArray(BaseTable): def preWrite(self, font): diff -Nru fonttools-4.13.0/Lib/fontTools/ttLib/ttFont.py fonttools-4.16.1/Lib/fontTools/ttLib/ttFont.py --- fonttools-4.13.0/Lib/fontTools/ttLib/ttFont.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/ttLib/ttFont.py 2020-10-05 18:25:14.000000000 +0000 @@ -18,7 +18,7 @@ """ def __init__(self, file=None, res_name_or_index=None, - sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False, + sfntVersion="\000\001\000\000", flavor=None, checkChecksums=0, verbose=None, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False, recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None, _tableCache=None): @@ -830,10 +830,48 @@ return getattr(tables, pyTag) -def getTableClass(tag): - """Fetch the packer/unpacker class for a table. - Return None when no class is found. +# Registry for custom table packer/unpacker classes. Keys are table +# tags, values are (moduleName, className) tuples. +# See registerCustomTableClass() and getCustomTableClass() +_customTableRegistry = {} + + +def registerCustomTableClass(tag, moduleName, className=None): + """Register a custom packer/unpacker class for a table. + The 'moduleName' must be an importable module. If no 'className' + is given, it is derived from the tag, for example it will be + table_C_U_S_T_ for a 'CUST' tag. + + The registered table class should be a subclass of + fontTools.ttLib.tables.DefaultTable.DefaultTable + """ + if className is None: + className = "table_" + tagToIdentifier(tag) + _customTableRegistry[tag] = (moduleName, className) + + +def unregisterCustomTableClass(tag): + """Unregister the custom packer/unpacker class for a table.""" + del _customTableRegistry[tag] + + +def getCustomTableClass(tag): + """Return the custom table class for tag, if one has been registered + with 'registerCustomTableClass()'. Else return None. """ + if tag not in _customTableRegistry: + return None + import importlib + moduleName, className = _customTableRegistry[tag] + module = importlib.import_module(moduleName) + return getattr(module, className) + + +def getTableClass(tag): + """Fetch the packer/unpacker class for a table.""" + tableClass = getCustomTableClass(tag) + if tableClass is not None: + return tableClass module = getTableModule(tag) if module is None: from .tables.DefaultTable import DefaultTable diff -Nru fonttools-4.13.0/Lib/fontTools/ttLib/woff2.py fonttools-4.16.1/Lib/fontTools/ttLib/woff2.py --- fonttools-4.13.0/Lib/fontTools/ttLib/woff2.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/ttLib/woff2.py 2020-10-05 18:25:14.000000000 +0000 @@ -29,7 +29,7 @@ flavor = "woff2" - def __init__(self, file, checkChecksums=1, fontNumber=-1): + def __init__(self, file, checkChecksums=0, fontNumber=-1): if not haveBrotli: log.error( 'The WOFF2 decoder requires the Brotli Python extension, available at: ' diff -Nru fonttools-4.13.0/Lib/fontTools/ufoLib/filenames.py fonttools-4.16.1/Lib/fontTools/ufoLib/filenames.py --- fonttools-4.13.0/Lib/fontTools/ufoLib/filenames.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/ufoLib/filenames.py 2020-10-05 18:25:14.000000000 +0000 @@ -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 -Nru fonttools-4.13.0/Lib/fontTools/ufoLib/glifLib.py fonttools-4.16.1/Lib/fontTools/ufoLib/glifLib.py --- fonttools-4.13.0/Lib/fontTools/ufoLib/glifLib.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/ufoLib/glifLib.py 2020-10-05 18:25:14.000000000 +0000 @@ -1416,10 +1416,10 @@ raise GlifLibError("Unknown child elements in point element.") # x and y are required for attr in ("x", "y"): - value = element.get(attr) - if validate and value is None: - raise GlifLibError("Required %s attribute is missing in point element." % attr) - point[attr] = _number(value) + try: + point[attr] = _number(point[attr]) + except KeyError as e: + raise GlifLibError(f"Required {attr} attribute is missing in point element.") from e # segment type pointType = point.pop("type", "offcurve") if validate and pointType not in pointTypeOptions: diff -Nru fonttools-4.13.0/Lib/fontTools/varLib/instancer.py fonttools-4.16.1/Lib/fontTools/varLib/instancer.py --- fonttools-4.13.0/Lib/fontTools/varLib/instancer.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/varLib/instancer.py 2020-10-05 18:25:14.000000000 +0000 @@ -87,6 +87,7 @@ from contextlib import contextmanager import collections from copy import deepcopy +from enum import IntEnum import logging from itertools import islice import os @@ -121,6 +122,12 @@ return self +class OverlapMode(IntEnum): + KEEP_AND_DONT_SET_FLAGS = 0 + KEEP_AND_SET_FLAGS = 1 + REMOVE = 2 + + def instantiateTupleVariationStore( variations, axisLimits, origCoords=None, endPts=None ): @@ -578,7 +585,7 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, axisLimits): - """ Compute deltas at partial location, and update varStore in-place. + """Compute deltas at partial location, and update varStore in-place. Remove regions in which all axes were instanced, or fall outside the new axis limits. Scale the deltas of the remaining regions where only some of the axes @@ -676,8 +683,8 @@ def instantiateFeatureVariations(varfont, axisLimits): for tableTag in ("GPOS", "GSUB"): - if tableTag not in varfont or not hasattr( - varfont[tableTag].table, "FeatureVariations" + if tableTag not in varfont or not getattr( + varfont[tableTag].table, "FeatureVariations", None ): continue log.info("Instantiating FeatureVariations of %s table", tableTag) @@ -1175,9 +1182,13 @@ def instantiateVariableFont( - varfont, axisLimits, inplace=False, optimize=True, overlap=True + varfont, + axisLimits, + inplace=False, + optimize=True, + overlap=OverlapMode.KEEP_AND_SET_FLAGS, ): - """ Instantiate variable font, either fully or partially. + """Instantiate variable font, either fully or partially. Depending on whether the `axisLimits` dictionary references all or some of the input varfont's axes, the output font will either be a full instance (static @@ -1198,13 +1209,20 @@ remaining 'gvar' table's deltas. Possibly faster, and might work around rendering issues in some buggy environments, at the cost of a slightly larger file size. - overlap (bool): variable fonts usually contain overlapping contours, and some - font rendering engines on Apple platforms require that the `OVERLAP_SIMPLE` - and `OVERLAP_COMPOUND` flags in the 'glyf' table be set to force rendering - using a non-zero fill rule. Thus we always set these flags on all glyphs - to maximise cross-compatibility of the generated instance. You can disable - this by setting `overalap` to False. + overlap (OverlapMode): variable fonts usually contain overlapping contours, and + some font rendering engines on Apple platforms require that the + `OVERLAP_SIMPLE` and `OVERLAP_COMPOUND` flags in the 'glyf' table be set to + force rendering using a non-zero fill rule. Thus we always set these flags + on all glyphs to maximise cross-compatibility of the generated instance. + You can disable this by passing OverlapMode.KEEP_AND_DONT_SET_FLAGS. + If you want to remove the overlaps altogether and merge overlapping + contours and components, you can pass OverlapMode.REMOVE. Note that this + requires the skia-pathops package (available to pip install). + The overlap parameter only has effect when generating full static instances. """ + # 'overlap' used to be bool and is now enum; for backward compat keep accepting bool + overlap = OverlapMode(int(overlap)) + sanityCheckVariableTables(varfont) axisLimits = populateAxisDefaults(varfont, axisLimits) @@ -1245,8 +1263,14 @@ instantiateFvar(varfont, axisLimits) if "fvar" not in varfont: - if "glyf" in varfont and overlap: - setMacOverlapFlags(varfont["glyf"]) + if "glyf" in varfont: + if overlap == OverlapMode.KEEP_AND_SET_FLAGS: + setMacOverlapFlags(varfont["glyf"]) + elif overlap == OverlapMode.REMOVE: + from fontTools.ttLib.removeOverlaps import removeOverlaps + + log.info("Removing overlaps from glyf table") + removeOverlaps(varfont) varLib.set_default_weight_width_slant( varfont, @@ -1346,6 +1370,13 @@ help="Don't set OVERLAP_SIMPLE/OVERLAP_COMPOUND glyf flags (only applicable " "when generating a full instance)", ) + parser.add_argument( + "--remove-overlaps", + dest="remove_overlaps", + action="store_true", + help="Merge overlapping contours and components (only applicable " + "when generating a full instance). Requires skia-pathops", + ) loggingGroup = parser.add_mutually_exclusive_group(required=False) loggingGroup.add_argument( "-v", "--verbose", action="store_true", help="Run more verbosely." @@ -1355,6 +1386,11 @@ ) options = parser.parse_args(args) + if options.remove_overlaps: + options.overlap = OverlapMode.REMOVE + else: + options.overlap = OverlapMode(int(options.overlap)) + infile = options.input if not os.path.isfile(infile): parser.error("No such file '{}'".format(infile)) diff -Nru fonttools-4.13.0/Lib/fontTools/varLib/interpolatable.py fonttools-4.16.1/Lib/fontTools/varLib/interpolatable.py --- fonttools-4.13.0/Lib/fontTools/varLib/interpolatable.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/varLib/interpolatable.py 2020-10-05 18:25:14.000000000 +0000 @@ -104,6 +104,7 @@ try: allVectors = [] + allNodeTypes = [] for glyphset,name in zip(glyphsets, names): #print('.', end='') glyph = glyphset[glyph_name] @@ -114,8 +115,11 @@ del perContourPen contourVectors = [] + nodeTypes = [] + allNodeTypes.append(nodeTypes) allVectors.append(contourVectors) for contour in contourPens: + nodeTypes.append(tuple(instruction[0] for instruction in contour.value)) stats = StatisticsPen(glyphset=glyphset) contour.replay(stats) size = abs(stats.area) ** .5 * .5 @@ -131,6 +135,23 @@ #print(vector) # Check each master against the next one in the list. + for i, (m0, m1) in enumerate(zip(allNodeTypes[:-1], allNodeTypes[1:])): + if len(m0) != len(m1): + print('%s: %s+%s: Glyphs not compatible (wrong number of paths %i+%i)!!!!!' % (glyph_name, names[i], names[i+1], len(m0), len(m1))) + if m0 == m1: + continue + for pathIx, (nodes1, nodes2) in enumerate(zip(m0, m1)): + if nodes1 == nodes2: + continue + print('%s: %s+%s: Glyphs not compatible at path %i!!!!!' % (glyph_name, names[i], names[i+1], pathIx)) + if len(nodes1) != len(nodes2): + print("%s has %i nodes, %s has %i nodes" % (names[i], len(nodes1), names[i+1], len(nodes2))) + continue + for nodeIx, (n1, n2) in enumerate(zip(nodes1, nodes2)): + if n1 != n2: + print("At node %i, %s has %s, %s has %s" % (nodeIx, names[i], n1, names[i+1], n2)) + continue + for i,(m0,m1) in enumerate(zip(allVectors[:-1],allVectors[1:])): if len(m0) != len(m1): print('%s: %s+%s: Glyphs not compatible!!!!!' % (glyph_name, names[i], names[i+1])) diff -Nru fonttools-4.13.0/Lib/fontTools/varLib/models.py fonttools-4.16.1/Lib/fontTools/varLib/models.py --- fonttools-4.13.0/Lib/fontTools/varLib/models.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/varLib/models.py 2020-10-05 18:25:14.000000000 +0000 @@ -444,10 +444,10 @@ 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 -Nru fonttools-4.13.0/Lib/fontTools/varLib/mutator.py fonttools-4.16.1/Lib/fontTools/varLib/mutator.py --- fonttools-4.13.0/Lib/fontTools/varLib/mutator.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Lib/fontTools/varLib/mutator.py 2020-10-05 18:25:14.000000000 +0000 @@ -257,7 +257,7 @@ if not tableTag in varfont: continue table = varfont[tableTag].table - if not hasattr(table, 'FeatureVariations'): + if not getattr(table, 'FeatureVariations', None): continue variations = table.FeatureVariations for record in variations.FeatureVariationRecord: diff -Nru fonttools-4.13.0/MANIFEST.in fonttools-4.16.1/MANIFEST.in --- fonttools-4.13.0/MANIFEST.in 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/MANIFEST.in 2020-10-05 18:25:14.000000000 +0000 @@ -13,6 +13,8 @@ include tox.ini include run-tests.sh +recursive-include Lib/fontTools py.typed + include .appveyor.yml include .codecov.yml include .coveragerc diff -Nru fonttools-4.13.0/mypy.ini fonttools-4.16.1/mypy.ini --- fonttools-4.13.0/mypy.ini 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/mypy.ini 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,21 @@ +[mypy] +python_version = 3.6 +files = Lib/fontTools/misc/plistlib +follow_imports = silent +ignore_missing_imports = True +warn_redundant_casts = True +warn_unused_configs = True +warn_unused_ignores = True + +[mypy-fontTools.misc.plistlib] +check_untyped_defs = True +disallow_any_generics = True +disallow_incomplete_defs = True +disallow_subclassing_any = True +disallow_untyped_decorators = True +disallow_untyped_calls = False +disallow_untyped_defs = True +no_implicit_optional = True +no_implicit_reexport = True +strict_equality = True +warn_return_any = True diff -Nru fonttools-4.13.0/NEWS.rst fonttools-4.16.1/NEWS.rst --- fonttools-4.13.0/NEWS.rst 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/NEWS.rst 2020-10-05 18:25:14.000000000 +0000 @@ -1,3 +1,57 @@ +4.16.1 (released 2020-10-05) +---------------------------- + +- [varLib.instancer] Fixed ``TypeError`` exception when instantiating a VF with + a GSUB table 1.1 in which ``FeatureVariations`` attribute is present but set to + ``None`` -- indicating that optional ``FeatureVariations`` is missing (#2077). +- [glifLib] Make ``x`` and ``y`` attributes of the ``point`` element required + even when validation is turned off, and raise a meaningful ``GlifLibError`` + message when that happens (#2075). + +4.16.0 (released 2020-09-30) +---------------------------- + +- [removeOverlaps] Added new module and ``removeOverlaps`` function that merges + overlapping contours and components in TrueType glyphs. It requires the + `skia-pathops `__ module. + Note that removing overlaps invalidates the TrueType hinting (#2068). +- [varLib.instancer] Added ``--remove-overlaps`` command-line option. + The ``overlap`` option in ``instantiateVariableFont`` now takes an ``OverlapMode`` + enum: 0: KEEP_AND_DONT_SET_FLAGS, 1: KEEP_AND_SET_FLAGS (default), and 2: REMOVE. + The latter is equivalent to calling ``removeOverlaps`` on the generated static + instance. The option continues to accept ``bool`` value for backward compatibility. + + +4.15.0 (released 2020-09-21) +---------------------------- + +- [plistlib] Added typing annotations to plistlib module. Set up mypy static + typechecker to run automatically on CI (#2061). +- [ttLib] Implement private ``Debg`` table, a reverse-DNS namespaced JSON dict. +- [feaLib] Optionally add an entry into the ``Debg`` table with the original + lookup name (if any), feature name / script / language combination (if any), + and original source filename and line location. Annotate the ttx output for + a lookup with the information from the Debg table (#2052). +- [sfnt] Disabled checksum checking by default in ``SFNTReader`` (#2058). +- [Docs] Document ``mtiLib`` module (#2027). +- [varLib.interpolatable] Added checks for contour node count and operation type + of each node (#2054). +- [ttLib] Added API to register custom table packer/unpacker classes (#2055). + +4.14.0 (released 2020-08-19) +---------------------------- + +- [feaLib] Allow anonymous classes in LookupFlags definitions (#2037). +- [Docs] Better document DesignSpace rules processing order (#2041). +- [ttLib] Fixed 21-year old bug in ``maxp.maxComponentDepth`` calculation (#2044, + #2045). +- [varLib.models] Fixed misspelled argument name in CLI entry point (81d0042a). +- [subset] When subsetting GSUB v1.1, fixed TypeError by checking whether the + optional FeatureVariations table is present (e63ecc5b). +- [Snippets] Added snippet to show how to decompose glyphs in a TTF (#2030). +- [otlLib] Generate GSUB type 5 and GPOS type 7 contextual lookups where appropriate + (#2016). + 4.13.0 (released 2020-07-10) ---------------------------- diff -Nru fonttools-4.13.0/README.rst fonttools-4.16.1/README.rst --- fonttools-4.13.0/README.rst 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/README.rst 2020-10-05 18:25:14.000000000 +0000 @@ -11,8 +11,9 @@ licence `__. | Among other things this means you can use it free of charge. -`User documentation ` and -`developer documentation ` are available at `Read the Docs `. +`User documentation `_ and +`developer documentation `_ +are available at `Read the Docs `_. Installation ~~~~~~~~~~~~ @@ -173,6 +174,16 @@ *Extra:* ``type1`` +- ``Lib/fontTools/ttLib/removeOverlaps.py`` + + Simplify TrueType glyphs by merging overlapping contours and components. + + * `skia-pathops `__: Python + bindings for the Skia library's PathOps module, performing boolean + operations on paths (union, intersection, etc.). + + *Extra:* ``pathops`` + - ``Lib/fontTools/pens/cocoaPen.py`` Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires: diff -Nru fonttools-4.13.0/requirements.txt fonttools-4.16.1/requirements.txt --- fonttools-4.13.0/requirements.txt 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/requirements.txt 2020-10-05 18:25:14.000000000 +0000 @@ -1,11 +1,12 @@ # we use the official Brotli module on CPython and the CFFI-based # extension 'brotlipy' on PyPy -brotli==1.0.7; platform_python_implementation != "PyPy" +brotli==1.0.9; 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 +skia-pathops==0.5.0; platform_python_implementation != "PyPy" # this is only required to run Tests/cu2qu/{ufo,cli}_test.py ufoLib2==0.6.2 diff -Nru fonttools-4.13.0/setup.cfg fonttools-4.16.1/setup.cfg --- fonttools-4.13.0/setup.cfg 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/setup.cfg 2020-10-05 18:25:14.000000000 +0000 @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.13.0 +current_version = 4.16.1 commit = True tag = False tag_name = {new_version} diff -Nru fonttools-4.13.0/setup.py fonttools-4.16.1/setup.py --- fonttools-4.13.0/setup.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/setup.py 2020-10-05 18:25:14.000000000 +0000 @@ -122,6 +122,10 @@ "type1": [ "xattr; sys_platform == 'darwin'", ], + # for fontTools.ttLib.removeOverlaps, to remove overlaps in TTF fonts + "pathops": [ + "skia-pathops >= 0.5.0", + ], } # use a special 'all' key as shorthand to includes all the extra dependencies extras_require["all"] = sum(extras_require.values(), []) @@ -437,7 +441,7 @@ setup_params = dict( name="fonttools", - version="4.13.0", + version="4.16.1", description="Tools to manipulate font files", author="Just van Rossum", author_email="just@letterror.com", diff -Nru fonttools-4.13.0/Snippets/decompose-ttf.py fonttools-4.16.1/Snippets/decompose-ttf.py --- fonttools-4.13.0/Snippets/decompose-ttf.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Snippets/decompose-ttf.py 2020-10-05 18:25:14.000000000 +0000 @@ -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 -Nru fonttools-4.13.0/Snippets/fontTools/feaLib/ast.py fonttools-4.16.1/Snippets/fontTools/feaLib/ast.py --- fonttools-4.13.0/Snippets/fontTools/feaLib/ast.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/feaLib/ast.py 2020-10-05 18:25:14.000000000 +0000 @@ -8,65 +8,65 @@ 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 @@ return "" % ", ".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 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 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 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 @@ 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 @@ 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 @@ 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 @@ """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 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 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 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 @@ 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 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 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 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 TableBlock(Block): """A ``table ... { }`` block.""" + def __init__(self, name, location=None): Block.__init__(self, location) self.name = name @@ -391,6 +442,7 @@ 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 @@ """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 @@ 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 @@ 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 @@ 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 @@ # markClass [cedilla] @FRENCH_ACCENTS; """ + def __init__(self, markClass, anchor, glyphs, location=None): Statement.__init__(self, location) assert isinstance(markClass, MarkClass) @@ -512,8 +566,8 @@ 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 @@ 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 @@ 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 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 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 @@ 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 @@ 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 @@ 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 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 @@ 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 "" @@ -740,6 +817,7 @@ 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 @@ 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 @@ ``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 @@ 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 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 @@ 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 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 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 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 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ """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 @@ """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 @@ """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 @@ 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 @@ 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 @@ If ``enumerated`` is true, then this is expressed as an `enumerated pair `_. """ - 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 @@ 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 @@ 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 @@ 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 @@ ``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 @@ 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 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 @@ 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 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 @@ 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 @@ 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 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 @@ class NameRecord(Statement): """Represents a name record. (`Section 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 @@ 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 @@ 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 @@ 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 @@ 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 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 @@ 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 @@ 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 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 @@ 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 @@ 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 VheaField(Statement): """An entry in the ``vhea`` table.""" + def __init__(self, key, value, location=None): Statement.__init__(self, location) self.key = key diff -Nru fonttools-4.13.0/Snippets/fontTools/feaLib/builder.py fonttools-4.16.1/Snippets/fontTools/feaLib/builder.py --- fonttools-4.13.0/Snippets/fontTools/feaLib/builder.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/feaLib/builder.py 2020-10-05 18:25:14.000000000 +0000 @@ -2,6 +2,7 @@ from fontTools.misc import sstruct from fontTools.misc.textTools import binary2num, safeEval from fontTools.feaLib.error import FeatureLibError +from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY from fontTools.feaLib.parser import Parser from fontTools.feaLib.ast import FeatureFile from fontTools.otlLib import builder as otl @@ -23,6 +24,7 @@ ClassPairPosSubtableBuilder, PairPosBuilder, SinglePosBuilder, + ChainContextualRule, ) from fontTools.otlLib.error import OpenTypeLibError from collections import defaultdict @@ -33,7 +35,7 @@ log = logging.getLogger(__name__) -def addOpenTypeFeatures(font, featurefile, tables=None): +def addOpenTypeFeatures(font, featurefile, tables=None, debug=False): """Add features from a file to a font. Note that this replaces any features currently present. @@ -43,13 +45,17 @@ parse it into an AST), or a pre-parsed AST instance. tables: If passed, restrict the set of affected tables to those in the list. + debug: Whether to add source debugging information to the font in the + ``Debg`` table """ builder = Builder(font, featurefile) - builder.build(tables=tables) + builder.build(tables=tables, debug=debug) -def addOpenTypeFeaturesFromString(font, features, filename=None, tables=None): +def addOpenTypeFeaturesFromString( + font, features, filename=None, tables=None, debug=False +): """Add features from a string to a font. Note that this replaces any features currently present. @@ -61,28 +67,33 @@ directory is assumed. tables: If passed, restrict the set of affected tables to those in the list. + debug: Whether to add source debugging information to the font in the + ``Debg`` table """ featurefile = UnicodeIO(tounicode(features)) if filename: featurefile.name = filename - addOpenTypeFeatures(font, featurefile, tables=tables) + addOpenTypeFeatures(font, featurefile, tables=tables, debug=debug) 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 @@ -104,6 +115,7 @@ self.cur_lookup_name_ = None self.cur_feature_name_ = None self.lookups_ = [] + self.lookup_locations = {"GSUB": {}, "GPOS": {}} self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*] self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp' # for feature 'aalt' @@ -142,7 +154,7 @@ # for table 'vhea' self.vhea_ = {} - def build(self, tables=None): + def build(self, tables=None, debug=False): if self.parseTree is None: self.parseTree = Parser(self.file, self.glyphMap).parse() self.parseTree.build(self) @@ -170,19 +182,20 @@ 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() @@ -196,6 +209,8 @@ self.font["BASE"] = base elif "BASE" in self.font: del self.font["BASE"] + if debug: + self.buildDebg() def get_chained_lookup_(self, location, builder_class): result = builder_class(self.font, location) @@ -210,16 +225,19 @@ 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 +248,7 @@ 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 +256,16 @@ 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 +273,23 @@ 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 +356,12 @@ 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 +379,18 @@ 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 +433,18 @@ 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 +480,63 @@ 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 +592,22 @@ 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 +642,20 @@ 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 buildDebg(self): + if "Debg" not in self.font: + self.font["Debg"] = newTable("Debg") + self.font["Debg"].data = {} + self.font["Debg"].data[LOOKUP_DEBUG_INFO_KEY] = self.lookup_locations + 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 = [] @@ -576,6 +663,11 @@ if lookup.table != tag: continue lookup.lookup_index = len(lookups) + self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo( + location=str(lookup.location), + name=self.get_lookup_name_(lookup), + feature=None, + ) lookups.append(lookup) try: otLookups = [l.build() for l in lookups] @@ -606,13 +698,19 @@ # 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 + for ix in lookup_indices: + self.lookup_locations[tag][str(ix)] = self.lookup_locations[tag][ + str(ix) + ]._replace(feature=key) + feature_key = (feature_tag, lookup_indices) feature_index = feature_indices.get(feature_key) if feature_index is None: @@ -620,14 +718,12 @@ 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 +739,16 @@ 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 @@ -668,26 +763,35 @@ table.LookupList.LookupCount = len(table.LookupList.Lookup) return table + def get_lookup_name_(self, lookup): + rev = {v: k for k, v in self.named_lookups_.items()} + if lookup in rev: + return rev[lookup] + return None + 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 +803,11 @@ 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 +826,14 @@ 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 +859,24 @@ 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 +887,14 @@ 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 +910,8 @@ 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 +938,25 @@ 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 +967,9 @@ 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 +980,21 @@ 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 +1002,20 @@ 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 +1041,27 @@ 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 +1073,32 @@ 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 +1116,18 @@ 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 +1137,18 @@ 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 +1157,15 @@ 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 +1173,7 @@ 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 +1193,11 @@ 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 +1246,22 @@ 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 +1281,7 @@ 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 +1303,7 @@ 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 +1327,3 @@ vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0} valRec = otl.buildValue(vr) return valRec - - - diff -Nru fonttools-4.13.0/Snippets/fontTools/feaLib/error.py fonttools-4.16.1/Snippets/fontTools/feaLib/error.py --- fonttools-4.13.0/Snippets/fontTools/feaLib/error.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/feaLib/error.py 2020-10-05 18:25:14.000000000 +0000 @@ -1,5 +1,3 @@ - - class FeatureLibError(Exception): def __init__(self, message, location): Exception.__init__(self, message) diff -Nru fonttools-4.13.0/Snippets/fontTools/feaLib/lexer.py fonttools-4.16.1/Snippets/fontTools/feaLib/lexer.py --- fonttools-4.13.0/Snippets/fontTools/feaLib/lexer.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/feaLib/lexer.py 2020-10-05 18:25:14.000000000 +0000 @@ -77,75 +77,75 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 NonIncludingLexer(IncludingLexer): """Lexer that does not follow `include` statements, emits them as-is.""" + def __next__(self): # Python 3 return next(self.lexers_[0]) diff -Nru fonttools-4.13.0/Snippets/fontTools/feaLib/location.py fonttools-4.16.1/Snippets/fontTools/feaLib/location.py --- fonttools-4.13.0/Snippets/fontTools/feaLib/location.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/feaLib/location.py 2020-10-05 18:25:14.000000000 +0000 @@ -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 -Nru fonttools-4.13.0/Snippets/fontTools/feaLib/lookupDebugInfo.py fonttools-4.16.1/Snippets/fontTools/feaLib/lookupDebugInfo.py --- fonttools-4.13.0/Snippets/fontTools/feaLib/lookupDebugInfo.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/feaLib/lookupDebugInfo.py 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,10 @@ +from typing import NamedTuple + +LOOKUP_DEBUG_INFO_KEY = "com.github.fonttools.feaLib" + +class LookupDebugInfo(NamedTuple): + """Information about where a lookup came from, to be embedded in a font""" + + location: str + name: str + feature: list diff -Nru fonttools-4.13.0/Snippets/fontTools/feaLib/__main__.py fonttools-4.16.1/Snippets/fontTools/feaLib/__main__.py --- fonttools-4.13.0/Snippets/fontTools/feaLib/__main__.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/feaLib/__main__.py 2020-10-05 18:25:14.000000000 +0000 @@ -15,23 +15,45 @@ 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.", + ) + parser.add_argument( + "-d", + "--debug", + action="store_true", + help="Add source-level debugging information to font.", + ) + parser.add_argument( + "-v", + "--verbose", + help="increase the logger verbosity. Multiple -v " "options are allowed.", + action="count", + default=0, + ) parser.add_argument( - "-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) - 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"] @@ -42,7 +64,9 @@ font = TTFont(options.input_font) try: - addOpenTypeFeatures(font, options.input_fea, tables=options.tables) + addOpenTypeFeatures( + font, options.input_fea, tables=options.tables, debug=options.debug + ) except FeatureLibError as e: if options.traceback: raise @@ -50,5 +74,5 @@ font.save(output_font) -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(main()) diff -Nru fonttools-4.13.0/Snippets/fontTools/feaLib/parser.py fonttools-4.16.1/Snippets/fontTools/feaLib/parser.py --- fonttools-4.13.0/Snippets/fontTools/feaLib/parser.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/feaLib/parser.py 2020-10-05 18:25:14.000000000 +0000 @@ -35,25 +35,30 @@ ``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 @@ 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 @@ 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 @@ 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 @@ 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 ``[ mark @MARKCLASS]*.`` @@ -183,9 +199,9 @@ 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 @@ 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 @@ 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 `_). @@ -243,9 +258,9 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 `_.""" @@ -1061,12 +1133,12 @@ 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 @@ 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 @@ 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 @@ 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 `_) - 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 @@ 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 @@ 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 @@ 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 `_) @@ -1290,14 +1408,13 @@ 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 @@ 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 @@ 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 `_.""" 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 @@ # Parses a ``cvParameters`` block found in Character Variant features. # See section `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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ @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 @@ 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 -Nru fonttools-4.13.0/Snippets/fontTools/__init__.py fonttools-4.16.1/Snippets/fontTools/__init__.py --- fonttools-4.13.0/Snippets/fontTools/__init__.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/__init__.py 2020-10-05 18:25:14.000000000 +0000 @@ -4,6 +4,6 @@ log = logging.getLogger(__name__) -version = __version__ = "4.13.0" +version = __version__ = "4.16.1" __all__ = ["version", "log", "configLogger"] diff -Nru fonttools-4.13.0/Snippets/fontTools/misc/plistlib/__init__.py fonttools-4.16.1/Snippets/fontTools/misc/plistlib/__init__.py --- fonttools-4.13.0/Snippets/fontTools/misc/plistlib/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/misc/plistlib/__init__.py 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,680 @@ +import collections.abc +import sys +import re +from typing import ( + Any, + Callable, + Dict, + List, + Mapping, + MutableMapping, + Optional, + Sequence, + Type, + Union, + IO, +) +import warnings +from io import BytesIO +from datetime import datetime +from base64 import b64encode, b64decode +from numbers import Integral +from types import SimpleNamespace +from functools import singledispatch + +from fontTools.misc import etree + +from fontTools.misc.py23 import ( + tounicode, + tobytes, +) + +# By default, we +# - deserialize elements as bytes and +# - serialize bytes as elements. +# Before, on Python 2, we +# - deserialized elements as plistlib.Data objects, in order to +# distinguish them from the built-in str type (which is bytes on python2) +# - serialized bytes as elements (they must have only contained +# ASCII characters in this case) +# You can pass use_builtin_types=[True|False] to the load/dump etc. functions +# to enforce a specific treatment. +# NOTE that unicode type always maps to element, and plistlib.Data +# always maps to element, regardless of use_builtin_types. +USE_BUILTIN_TYPES = True + +XML_DECLARATION = b"""""" + +PLIST_DOCTYPE = ( + b'' +) + + +# Date should conform to a subset of ISO 8601: +# YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z' +_date_parser = re.compile( + r"(?P\d\d\d\d)" + r"(?:-(?P\d\d)" + r"(?:-(?P\d\d)" + r"(?:T(?P\d\d)" + r"(?::(?P\d\d)" + r"(?::(?P\d\d))" + r"?)?)?)?)?Z", + re.ASCII, +) + + +def _date_from_string(s: str) -> datetime: + order = ("year", "month", "day", "hour", "minute", "second") + m = _date_parser.match(s) + if m is None: + raise ValueError(f"Expected ISO 8601 date string, but got '{s:r}'.") + gd = m.groupdict() + lst = [] + for key in order: + val = gd[key] + if val is None: + break + lst.append(int(val)) + # NOTE: mypy doesn't know that lst is 6 elements long. + return datetime(*lst) # type:ignore + + +def _date_to_string(d: datetime) -> str: + return "%04d-%02d-%02dT%02d:%02d:%02dZ" % ( + d.year, + d.month, + d.day, + d.hour, + d.minute, + d.second, + ) + + +class Data: + """Represents binary data when ``use_builtin_types=False.`` + + This class wraps binary data loaded from a plist file when the + ``use_builtin_types`` argument to the loading function (:py:func:`fromtree`, + :py:func:`load`, :py:func:`loads`) is false. + + The actual binary data is retrieved using the ``data`` attribute. + """ + + def __init__(self, data: bytes) -> None: + if not isinstance(data, bytes): + raise TypeError("Expected bytes, found %s" % type(data).__name__) + self.data = data + + @classmethod + def fromBase64(cls, data: Union[bytes, str]) -> "Data": + return cls(b64decode(data)) + + def asBase64(self, maxlinelength: int = 76, indent_level: int = 1) -> bytes: + return _encode_base64( + self.data, maxlinelength=maxlinelength, indent_level=indent_level + ) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, self.__class__): + return self.data == other.data + elif isinstance(other, bytes): + return self.data == other + else: + return NotImplemented + + def __repr__(self) -> str: + return "%s(%s)" % (self.__class__.__name__, repr(self.data)) + + +def _encode_base64( + data: bytes, maxlinelength: Optional[int] = 76, indent_level: int = 1 +) -> bytes: + data = b64encode(data) + if data and maxlinelength: + # split into multiple lines right-justified to 'maxlinelength' chars + indent = b"\n" + b" " * indent_level + max_length = max(16, maxlinelength - len(indent)) + chunks = [] + for i in range(0, len(data), max_length): + chunks.append(indent) + chunks.append(data[i : i + max_length]) + chunks.append(indent) + data = b"".join(chunks) + return data + + +# Mypy does not support recursive type aliases as of 0.782, Pylance does. +# https://github.com/python/mypy/issues/731 +# https://devblogs.microsoft.com/python/pylance-introduces-five-new-features-that-enable-type-magic-for-python-developers/#1-support-for-recursive-type-aliases +PlistEncodable = Union[ + bool, + bytes, + Data, + datetime, + float, + int, + Mapping[str, Any], + Sequence[Any], + str, +] + + +class PlistTarget: + """Event handler using the ElementTree Target API that can be + passed to a XMLParser to produce property list objects from XML. + It is based on the CPython plistlib module's _PlistParser class, + but does not use the expat parser. + + >>> from fontTools.misc import etree + >>> parser = etree.XMLParser(target=PlistTarget()) + >>> result = etree.XML( + ... "" + ... " something" + ... " blah" + ... "", + ... parser=parser) + >>> result == {"something": "blah"} + True + + Links: + https://github.com/python/cpython/blob/master/Lib/plistlib.py + http://lxml.de/parsing.html#the-target-parser-interface + """ + + def __init__( + self, + use_builtin_types: Optional[bool] = None, + dict_type: Type[MutableMapping[str, Any]] = dict, + ) -> None: + self.stack: List[PlistEncodable] = [] + self.current_key: Optional[str] = None + self.root: Optional[PlistEncodable] = None + if use_builtin_types is None: + self._use_builtin_types = USE_BUILTIN_TYPES + else: + if use_builtin_types is False: + warnings.warn( + "Setting use_builtin_types to False is deprecated and will be " + "removed soon.", + DeprecationWarning, + ) + self._use_builtin_types = use_builtin_types + self._dict_type = dict_type + + def start(self, tag: str, attrib: Mapping[str, str]) -> None: + self._data: List[str] = [] + handler = _TARGET_START_HANDLERS.get(tag) + if handler is not None: + handler(self) + + def end(self, tag: str) -> None: + handler = _TARGET_END_HANDLERS.get(tag) + if handler is not None: + handler(self) + + def data(self, data: str) -> None: + self._data.append(data) + + def close(self) -> PlistEncodable: + if self.root is None: + raise ValueError("No root set.") + return self.root + + # helpers + + def add_object(self, value: PlistEncodable) -> None: + if self.current_key is not None: + stack_top = self.stack[-1] + if not isinstance(stack_top, collections.abc.MutableMapping): + raise ValueError("unexpected element: %r" % stack_top) + stack_top[self.current_key] = value + self.current_key = None + elif not self.stack: + # this is the root object + self.root = value + else: + stack_top = self.stack[-1] + if not isinstance(stack_top, list): + raise ValueError("unexpected element: %r" % stack_top) + stack_top.append(value) + + def get_data(self) -> str: + data = "".join(self._data) + self._data = [] + return data + + +# event handlers + + +def start_dict(self: PlistTarget) -> None: + d = self._dict_type() + self.add_object(d) + self.stack.append(d) + + +def end_dict(self: PlistTarget) -> None: + if self.current_key: + raise ValueError("missing value for key '%s'" % self.current_key) + self.stack.pop() + + +def end_key(self: PlistTarget) -> None: + if self.current_key or not isinstance(self.stack[-1], collections.abc.Mapping): + raise ValueError("unexpected key") + self.current_key = self.get_data() + + +def start_array(self: PlistTarget) -> None: + a: List[PlistEncodable] = [] + self.add_object(a) + self.stack.append(a) + + +def end_array(self: PlistTarget) -> None: + self.stack.pop() + + +def end_true(self: PlistTarget) -> None: + self.add_object(True) + + +def end_false(self: PlistTarget) -> None: + self.add_object(False) + + +def end_integer(self: PlistTarget) -> None: + self.add_object(int(self.get_data())) + + +def end_real(self: PlistTarget) -> None: + self.add_object(float(self.get_data())) + + +def end_string(self: PlistTarget) -> None: + self.add_object(self.get_data()) + + +def end_data(self: PlistTarget) -> None: + if self._use_builtin_types: + self.add_object(b64decode(self.get_data())) + else: + self.add_object(Data.fromBase64(self.get_data())) + + +def end_date(self: PlistTarget) -> None: + self.add_object(_date_from_string(self.get_data())) + + +_TARGET_START_HANDLERS: Dict[str, Callable[[PlistTarget], None]] = { + "dict": start_dict, + "array": start_array, +} + +_TARGET_END_HANDLERS: Dict[str, Callable[[PlistTarget], None]] = { + "dict": end_dict, + "array": end_array, + "key": end_key, + "true": end_true, + "false": end_false, + "integer": end_integer, + "real": end_real, + "string": end_string, + "data": end_data, + "date": end_date, +} + + +# functions to build element tree from plist data + + +def _string_element(value: str, ctx: SimpleNamespace) -> etree.Element: + el = etree.Element("string") + el.text = value + return el + + +def _bool_element(value: bool, ctx: SimpleNamespace) -> etree.Element: + if value: + return etree.Element("true") + return etree.Element("false") + + +def _integer_element(value: int, ctx: SimpleNamespace) -> etree.Element: + if -1 << 63 <= value < 1 << 64: + el = etree.Element("integer") + el.text = "%d" % value + return el + raise OverflowError(value) + + +def _real_element(value: float, ctx: SimpleNamespace) -> etree.Element: + el = etree.Element("real") + el.text = repr(value) + return el + + +def _dict_element(d: Mapping[str, PlistEncodable], ctx: SimpleNamespace) -> etree.Element: + el = etree.Element("dict") + items = d.items() + if ctx.sort_keys: + items = sorted(items) # type: ignore + ctx.indent_level += 1 + for key, value in items: + if not isinstance(key, str): + if ctx.skipkeys: + continue + raise TypeError("keys must be strings") + k = etree.SubElement(el, "key") + k.text = tounicode(key, "utf-8") + el.append(_make_element(value, ctx)) + ctx.indent_level -= 1 + return el + + +def _array_element(array: Sequence[PlistEncodable], ctx: SimpleNamespace) -> etree.Element: + el = etree.Element("array") + if len(array) == 0: + return el + ctx.indent_level += 1 + for value in array: + el.append(_make_element(value, ctx)) + ctx.indent_level -= 1 + return el + + +def _date_element(date: datetime, ctx: SimpleNamespace) -> etree.Element: + el = etree.Element("date") + el.text = _date_to_string(date) + return el + + +def _data_element(data: bytes, ctx: SimpleNamespace) -> etree.Element: + el = etree.Element("data") + # NOTE: mypy is confused about whether el.text should be str or bytes. + el.text = _encode_base64( # type: ignore + data, + maxlinelength=(76 if ctx.pretty_print else None), + indent_level=ctx.indent_level, + ) + return el + + +def _string_or_data_element(raw_bytes: bytes, ctx: SimpleNamespace) -> etree.Element: + if ctx.use_builtin_types: + return _data_element(raw_bytes, ctx) + else: + try: + string = raw_bytes.decode(encoding="ascii", errors="strict") + except UnicodeDecodeError: + raise ValueError( + "invalid non-ASCII bytes; use unicode string instead: %r" % raw_bytes + ) + return _string_element(string, ctx) + + +# The following is probably not entirely correct. The signature should take `Any` +# and return `NoReturn`. At the time of this writing, neither mypy nor Pyright +# can deal with singledispatch properly and will apply the signature of the base +# function to all others. Being slightly dishonest makes it type-check and return +# usable typing information for the optimistic case. +@singledispatch +def _make_element(value: PlistEncodable, ctx: SimpleNamespace) -> etree.Element: + raise TypeError("unsupported type: %s" % type(value)) + + +_make_element.register(str)(_string_element) +_make_element.register(bool)(_bool_element) +_make_element.register(Integral)(_integer_element) +_make_element.register(float)(_real_element) +_make_element.register(collections.abc.Mapping)(_dict_element) +_make_element.register(list)(_array_element) +_make_element.register(tuple)(_array_element) +_make_element.register(datetime)(_date_element) +_make_element.register(bytes)(_string_or_data_element) +_make_element.register(bytearray)(_data_element) +_make_element.register(Data)(lambda v, ctx: _data_element(v.data, ctx)) + + +# Public functions to create element tree from plist-compatible python +# data structures and viceversa, for use when (de)serializing GLIF xml. + + +def totree( + value: PlistEncodable, + sort_keys: bool = True, + skipkeys: bool = False, + use_builtin_types: Optional[bool] = None, + pretty_print: bool = True, + indent_level: int = 1, +) -> etree.Element: + """Convert a value derived from a plist into an XML tree. + + Args: + value: Any kind of value to be serialized to XML. + sort_keys: Whether keys of dictionaries should be sorted. + skipkeys (bool): Whether to silently skip non-string dictionary + keys. + use_builtin_types (bool): If true, byte strings will be + encoded in Base-64 and wrapped in a ``data`` tag; if + false, they will be either stored as ASCII strings or an + exception raised if they cannot be decoded as such. Defaults + to ``True`` if not present. Deprecated. + pretty_print (bool): Whether to indent the output. + indent_level (int): Level of indentation when serializing. + + Returns: an ``etree`` ``Element`` object. + + Raises: + ``TypeError`` + if non-string dictionary keys are serialized + and ``skipkeys`` is false. + ``ValueError`` + if non-ASCII binary data is present + and `use_builtin_types` is false. + """ + if use_builtin_types is None: + use_builtin_types = USE_BUILTIN_TYPES + else: + use_builtin_types = use_builtin_types + context = SimpleNamespace( + sort_keys=sort_keys, + skipkeys=skipkeys, + use_builtin_types=use_builtin_types, + pretty_print=pretty_print, + indent_level=indent_level, + ) + return _make_element(value, context) + + +def fromtree( + tree: etree.Element, + use_builtin_types: Optional[bool] = None, + dict_type: Type[MutableMapping[str, Any]] = dict, +) -> Any: + """Convert an XML tree to a plist structure. + + Args: + tree: An ``etree`` ``Element``. + use_builtin_types: If True, binary data is deserialized to + bytes strings. If False, it is wrapped in :py:class:`Data` + objects. Defaults to True if not provided. Deprecated. + dict_type: What type to use for dictionaries. + + Returns: An object (usually a dictionary). + """ + target = PlistTarget(use_builtin_types=use_builtin_types, dict_type=dict_type) + for action, element in etree.iterwalk(tree, events=("start", "end")): + if action == "start": + target.start(element.tag, element.attrib) + elif action == "end": + # if there are no children, parse the leaf's data + if not len(element): + # always pass str, not None + target.data(element.text or "") + target.end(element.tag) + return target.close() + + +# python3 plistlib API + + +def load( + fp: IO[bytes], + use_builtin_types: Optional[bool] = None, + dict_type: Type[MutableMapping[str, Any]] = dict, +) -> Any: + """Load a plist file into an object. + + Args: + fp: An opened file. + use_builtin_types: If True, binary data is deserialized to + bytes strings. If False, it is wrapped in :py:class:`Data` + objects. Defaults to True if not provided. Deprecated. + dict_type: What type to use for dictionaries. + + Returns: + An object (usually a dictionary) representing the top level of + the plist file. + """ + + if not hasattr(fp, "read"): + raise AttributeError("'%s' object has no attribute 'read'" % type(fp).__name__) + target = PlistTarget(use_builtin_types=use_builtin_types, dict_type=dict_type) + parser = etree.XMLParser(target=target) # type: ignore + result = etree.parse(fp, parser=parser) + # lxml returns the target object directly, while ElementTree wraps + # it as the root of an ElementTree object + try: + return result.getroot() + except AttributeError: + return result + + +def loads( + value: bytes, + use_builtin_types: Optional[bool] = None, + dict_type: Type[MutableMapping[str, Any]] = dict, +) -> Any: + """Load a plist file from a string into an object. + + Args: + value: A bytes string containing a plist. + use_builtin_types: If True, binary data is deserialized to + bytes strings. If False, it is wrapped in :py:class:`Data` + objects. Defaults to True if not provided. Deprecated. + dict_type: What type to use for dictionaries. + + Returns: + An object (usually a dictionary) representing the top level of + the plist file. + """ + + fp = BytesIO(value) + return load(fp, use_builtin_types=use_builtin_types, dict_type=dict_type) + + +def dump( + value: PlistEncodable, + fp: IO[bytes], + sort_keys: bool = True, + skipkeys: bool = False, + use_builtin_types: Optional[bool] = None, + pretty_print: bool = True, +) -> None: + """Write a Python object to a plist file. + + Args: + value: An object to write. + fp: A file opened for writing. + sort_keys (bool): Whether keys of dictionaries should be sorted. + skipkeys (bool): Whether to silently skip non-string dictionary + keys. + use_builtin_types (bool): If true, byte strings will be + encoded in Base-64 and wrapped in a ``data`` tag; if + false, they will be either stored as ASCII strings or an + exception raised if they cannot be represented. Defaults + pretty_print (bool): Whether to indent the output. + indent_level (int): Level of indentation when serializing. + + Raises: + ``TypeError`` + if non-string dictionary keys are serialized + and ``skipkeys`` is false. + ``ValueError`` + if non-representable binary data is present + and `use_builtin_types` is false. + """ + + if not hasattr(fp, "write"): + raise AttributeError("'%s' object has no attribute 'write'" % type(fp).__name__) + root = etree.Element("plist", version="1.0") + el = totree( + value, + sort_keys=sort_keys, + skipkeys=skipkeys, + use_builtin_types=use_builtin_types, + pretty_print=pretty_print, + ) + root.append(el) + tree = etree.ElementTree(root) + # we write the doctype ourselves instead of using the 'doctype' argument + # of 'write' method, becuse lxml will force adding a '\n' even when + # pretty_print is False. + if pretty_print: + header = b"\n".join((XML_DECLARATION, PLIST_DOCTYPE, b"")) + else: + header = XML_DECLARATION + PLIST_DOCTYPE + fp.write(header) + tree.write( # type: ignore + fp, + encoding="utf-8", + pretty_print=pretty_print, + xml_declaration=False, + ) + + +def dumps( + value: PlistEncodable, + sort_keys: bool = True, + skipkeys: bool = False, + use_builtin_types: Optional[bool] = None, + pretty_print: bool = True, +) -> bytes: + """Write a Python object to a string in plist format. + + Args: + value: An object to write. + sort_keys (bool): Whether keys of dictionaries should be sorted. + skipkeys (bool): Whether to silently skip non-string dictionary + keys. + use_builtin_types (bool): If true, byte strings will be + encoded in Base-64 and wrapped in a ``data`` tag; if + false, they will be either stored as strings or an + exception raised if they cannot be represented. Defaults + pretty_print (bool): Whether to indent the output. + indent_level (int): Level of indentation when serializing. + + Returns: + string: A plist representation of the Python object. + + Raises: + ``TypeError`` + if non-string dictionary keys are serialized + and ``skipkeys`` is false. + ``ValueError`` + if non-representable binary data is present + and `use_builtin_types` is false. + """ + fp = BytesIO() + dump( + value, + fp, + sort_keys=sort_keys, + skipkeys=skipkeys, + use_builtin_types=use_builtin_types, + pretty_print=pretty_print, + ) + return fp.getvalue() diff -Nru fonttools-4.13.0/Snippets/fontTools/misc/plistlib.py fonttools-4.16.1/Snippets/fontTools/misc/plistlib.py --- fonttools-4.13.0/Snippets/fontTools/misc/plistlib.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/misc/plistlib.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,623 +0,0 @@ -import sys -import re -import warnings -from io import BytesIO -from datetime import datetime -from base64 import b64encode, b64decode -from numbers import Integral - -from types import SimpleNamespace -from collections.abc import Mapping -from functools import singledispatch - -from fontTools.misc import etree - -from fontTools.misc.py23 import ( - tounicode, - tobytes, -) - -# By default, we -# - deserialize elements as bytes and -# - serialize bytes as elements. -# Before, on Python 2, we -# - deserialized elements as plistlib.Data objects, in order to -# distinguish them from the built-in str type (which is bytes on python2) -# - serialized bytes as elements (they must have only contained -# ASCII characters in this case) -# You can pass use_builtin_types=[True|False] to the load/dump etc. functions -# to enforce a specific treatment. -# NOTE that unicode type always maps to element, and plistlib.Data -# always maps to element, regardless of use_builtin_types. -USE_BUILTIN_TYPES = True - -XML_DECLARATION = b"""""" - -PLIST_DOCTYPE = ( - b'' -) - -# Date should conform to a subset of ISO 8601: -# YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z' -_date_parser = re.compile( - r"(?P\d\d\d\d)" - r"(?:-(?P\d\d)" - r"(?:-(?P\d\d)" - r"(?:T(?P\d\d)" - r"(?::(?P\d\d)" - r"(?::(?P\d\d))" - r"?)?)?)?)?Z", - re.ASCII -) - - -def _date_from_string(s): - order = ("year", "month", "day", "hour", "minute", "second") - gd = _date_parser.match(s).groupdict() - lst = [] - for key in order: - val = gd[key] - if val is None: - break - lst.append(int(val)) - return datetime(*lst) - - -def _date_to_string(d): - return "%04d-%02d-%02dT%02d:%02d:%02dZ" % ( - d.year, - d.month, - d.day, - d.hour, - d.minute, - d.second, - ) - - -def _encode_base64(data, maxlinelength=76, indent_level=1): - data = b64encode(data) - if data and maxlinelength: - # split into multiple lines right-justified to 'maxlinelength' chars - indent = b"\n" + b" " * indent_level - max_length = max(16, maxlinelength - len(indent)) - chunks = [] - for i in range(0, len(data), max_length): - chunks.append(indent) - chunks.append(data[i : i + max_length]) - chunks.append(indent) - data = b"".join(chunks) - return data - - -class Data: - """Represents binary data when ``use_builtin_types=False.`` - - This class wraps binary data loaded from a plist file when the - ``use_builtin_types`` argument to the loading function (:py:func:`fromtree`, - :py:func:`load`, :py:func:`loads`) is false. - - The actual binary data is retrieved using the ``data`` attribute. - """ - - def __init__(self, data): - if not isinstance(data, bytes): - raise TypeError("Expected bytes, found %s" % type(data).__name__) - self.data = data - - @classmethod - def fromBase64(cls, data): - return cls(b64decode(data)) - - def asBase64(self, maxlinelength=76, indent_level=1): - return _encode_base64( - self.data, maxlinelength=maxlinelength, indent_level=indent_level - ) - - def __eq__(self, other): - if isinstance(other, self.__class__): - return self.data == other.data - elif isinstance(other, bytes): - return self.data == other - else: - return NotImplemented - - def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, repr(self.data)) - - -class PlistTarget: - """ Event handler using the ElementTree Target API that can be - passed to a XMLParser to produce property list objects from XML. - It is based on the CPython plistlib module's _PlistParser class, - but does not use the expat parser. - - >>> from fontTools.misc import etree - >>> parser = etree.XMLParser(target=PlistTarget()) - >>> result = etree.XML( - ... "" - ... " something" - ... " blah" - ... "", - ... parser=parser) - >>> result == {"something": "blah"} - True - - Links: - https://github.com/python/cpython/blob/master/Lib/plistlib.py - http://lxml.de/parsing.html#the-target-parser-interface - """ - - def __init__(self, use_builtin_types=None, dict_type=dict): - self.stack = [] - self.current_key = None - self.root = None - if use_builtin_types is None: - self._use_builtin_types = USE_BUILTIN_TYPES - else: - if use_builtin_types is False: - warnings.warn( - "Setting use_builtin_types to False is deprecated and will be " - "removed soon.", - DeprecationWarning, - ) - self._use_builtin_types = use_builtin_types - self._dict_type = dict_type - - def start(self, tag, attrib): - self._data = [] - handler = _TARGET_START_HANDLERS.get(tag) - if handler is not None: - handler(self) - - def end(self, tag): - handler = _TARGET_END_HANDLERS.get(tag) - if handler is not None: - handler(self) - - def data(self, data): - self._data.append(data) - - def close(self): - return self.root - - # helpers - - def add_object(self, value): - if self.current_key is not None: - if not isinstance(self.stack[-1], type({})): - raise ValueError("unexpected element: %r" % self.stack[-1]) - self.stack[-1][self.current_key] = value - self.current_key = None - elif not self.stack: - # this is the root object - self.root = value - else: - if not isinstance(self.stack[-1], type([])): - raise ValueError("unexpected element: %r" % self.stack[-1]) - self.stack[-1].append(value) - - def get_data(self): - data = "".join(self._data) - self._data = [] - return data - - -# event handlers - - -def start_dict(self): - d = self._dict_type() - self.add_object(d) - self.stack.append(d) - - -def end_dict(self): - if self.current_key: - raise ValueError("missing value for key '%s'" % self.current_key) - self.stack.pop() - - -def end_key(self): - if self.current_key or not isinstance(self.stack[-1], type({})): - raise ValueError("unexpected key") - self.current_key = self.get_data() - - -def start_array(self): - a = [] - self.add_object(a) - self.stack.append(a) - - -def end_array(self): - self.stack.pop() - - -def end_true(self): - self.add_object(True) - - -def end_false(self): - self.add_object(False) - - -def end_integer(self): - self.add_object(int(self.get_data())) - - -def end_real(self): - self.add_object(float(self.get_data())) - - -def end_string(self): - self.add_object(self.get_data()) - - -def end_data(self): - if self._use_builtin_types: - self.add_object(b64decode(self.get_data())) - else: - self.add_object(Data.fromBase64(self.get_data())) - - -def end_date(self): - self.add_object(_date_from_string(self.get_data())) - - -_TARGET_START_HANDLERS = {"dict": start_dict, "array": start_array} - -_TARGET_END_HANDLERS = { - "dict": end_dict, - "array": end_array, - "key": end_key, - "true": end_true, - "false": end_false, - "integer": end_integer, - "real": end_real, - "string": end_string, - "data": end_data, - "date": end_date, -} - - -# functions to build element tree from plist data - - -def _string_element(value, ctx): - el = etree.Element("string") - el.text = value - return el - - -def _bool_element(value, ctx): - if value: - return etree.Element("true") - else: - return etree.Element("false") - - -def _integer_element(value, ctx): - if -1 << 63 <= value < 1 << 64: - el = etree.Element("integer") - el.text = "%d" % value - return el - else: - raise OverflowError(value) - - -def _real_element(value, ctx): - el = etree.Element("real") - el.text = repr(value) - return el - - -def _dict_element(d, ctx): - el = etree.Element("dict") - items = d.items() - if ctx.sort_keys: - items = sorted(items) - ctx.indent_level += 1 - for key, value in items: - if not isinstance(key, str): - if ctx.skipkeys: - continue - raise TypeError("keys must be strings") - k = etree.SubElement(el, "key") - k.text = tounicode(key, "utf-8") - el.append(_make_element(value, ctx)) - ctx.indent_level -= 1 - return el - - -def _array_element(array, ctx): - el = etree.Element("array") - if len(array) == 0: - return el - ctx.indent_level += 1 - for value in array: - el.append(_make_element(value, ctx)) - ctx.indent_level -= 1 - return el - - -def _date_element(date, ctx): - el = etree.Element("date") - el.text = _date_to_string(date) - return el - - -def _data_element(data, ctx): - el = etree.Element("data") - el.text = _encode_base64( - data, - maxlinelength=(76 if ctx.pretty_print else None), - indent_level=ctx.indent_level, - ) - return el - - -def _string_or_data_element(raw_bytes, ctx): - if ctx.use_builtin_types: - return _data_element(raw_bytes, ctx) - else: - try: - string = raw_bytes.decode(encoding="ascii", errors="strict") - except UnicodeDecodeError: - raise ValueError( - "invalid non-ASCII bytes; use unicode string instead: %r" - % raw_bytes - ) - return _string_element(string, ctx) - - -@singledispatch -def _make_element(value, ctx): - raise TypeError("unsupported type: %s" % type(value)) - -_make_element.register(str)(_string_element) -_make_element.register(bool)(_bool_element) -_make_element.register(Integral)(_integer_element) -_make_element.register(float)(_real_element) -_make_element.register(Mapping)(_dict_element) -_make_element.register(list)(_array_element) -_make_element.register(tuple)(_array_element) -_make_element.register(datetime)(_date_element) -_make_element.register(bytes)(_string_or_data_element) -_make_element.register(bytearray)(_data_element) -_make_element.register(Data)(lambda v, ctx: _data_element(v.data, ctx)) - - -# Public functions to create element tree from plist-compatible python -# data structures and viceversa, for use when (de)serializing GLIF xml. - - -def totree( - value, - sort_keys=True, - skipkeys=False, - use_builtin_types=None, - pretty_print=True, - indent_level=1, -): - """Convert a value derived from a plist into an XML tree. - - Args: - value: Any kind of value to be serialized to XML. - sort_keys: Whether keys of dictionaries should be sorted. - skipkeys (bool): Whether to silently skip non-string dictionary - keys. - use_builtin_types (bool): If true, byte strings will be - encoded in Base-64 and wrapped in a ``data`` tag; if - false, they will be either stored as ASCII strings or an - exception raised if they cannot be decoded as such. Defaults - to ``True`` if not present. Deprecated. - pretty_print (bool): Whether to indent the output. - indent_level (int): Level of indentation when serializing. - - Returns: an ``etree`` ``Element`` object. - - Raises: - ``TypeError`` - if non-string dictionary keys are serialized - and ``skipkeys`` is false. - ``ValueError`` - if non-ASCII binary data is present - and `use_builtin_types` is false. - """ - if use_builtin_types is None: - use_builtin_types = USE_BUILTIN_TYPES - else: - use_builtin_types = use_builtin_types - context = SimpleNamespace( - sort_keys=sort_keys, - skipkeys=skipkeys, - use_builtin_types=use_builtin_types, - pretty_print=pretty_print, - indent_level=indent_level, - ) - return _make_element(value, context) - - -def fromtree(tree, use_builtin_types=None, dict_type=dict): - """Convert an XML tree to a plist structure. - - Args: - tree: An ``etree`` ``Element``. - use_builtin_types: If True, binary data is deserialized to - bytes strings. If False, it is wrapped in :py:class:`Data` - objects. Defaults to True if not provided. Deprecated. - dict_type: What type to use for dictionaries. - - Returns: An object (usually a dictionary). - """ - target = PlistTarget( - use_builtin_types=use_builtin_types, dict_type=dict_type - ) - for action, element in etree.iterwalk(tree, events=("start", "end")): - if action == "start": - target.start(element.tag, element.attrib) - elif action == "end": - # if there are no children, parse the leaf's data - if not len(element): - # always pass str, not None - target.data(element.text or "") - target.end(element.tag) - return target.close() - - -# python3 plistlib API - - -def load(fp, use_builtin_types=None, dict_type=dict): - """Load a plist file into an object. - - Args: - fp: An opened file. - use_builtin_types: If True, binary data is deserialized to - bytes strings. If False, it is wrapped in :py:class:`Data` - objects. Defaults to True if not provided. Deprecated. - dict_type: What type to use for dictionaries. - - Returns: - An object (usually a dictionary) representing the top level of - the plist file. - """ - - if not hasattr(fp, "read"): - raise AttributeError( - "'%s' object has no attribute 'read'" % type(fp).__name__ - ) - target = PlistTarget( - use_builtin_types=use_builtin_types, dict_type=dict_type - ) - parser = etree.XMLParser(target=target) - result = etree.parse(fp, parser=parser) - # lxml returns the target object directly, while ElementTree wraps - # it as the root of an ElementTree object - try: - return result.getroot() - except AttributeError: - return result - - -def loads(value, use_builtin_types=None, dict_type=dict): - """Load a plist file from a string into an object. - - Args: - value: A string containing a plist. - use_builtin_types: If True, binary data is deserialized to - bytes strings. If False, it is wrapped in :py:class:`Data` - objects. Defaults to True if not provided. Deprecated. - dict_type: What type to use for dictionaries. - - Returns: - An object (usually a dictionary) representing the top level of - the plist file. - """ - - fp = BytesIO(value) - return load(fp, use_builtin_types=use_builtin_types, dict_type=dict_type) - - -def dump( - value, - fp, - sort_keys=True, - skipkeys=False, - use_builtin_types=None, - pretty_print=True, -): - """Write a Python object to a plist file. - - Args: - value: An object to write. - fp: A file opened for writing. - sort_keys (bool): Whether keys of dictionaries should be sorted. - skipkeys (bool): Whether to silently skip non-string dictionary - keys. - use_builtin_types (bool): If true, byte strings will be - encoded in Base-64 and wrapped in a ``data`` tag; if - false, they will be either stored as ASCII strings or an - exception raised if they cannot be represented. Defaults - pretty_print (bool): Whether to indent the output. - indent_level (int): Level of indentation when serializing. - - Raises: - ``TypeError`` - if non-string dictionary keys are serialized - and ``skipkeys`` is false. - ``ValueError`` - if non-representable binary data is present - and `use_builtin_types` is false. - """ - - if not hasattr(fp, "write"): - raise AttributeError( - "'%s' object has no attribute 'write'" % type(fp).__name__ - ) - root = etree.Element("plist", version="1.0") - el = totree( - value, - sort_keys=sort_keys, - skipkeys=skipkeys, - use_builtin_types=use_builtin_types, - pretty_print=pretty_print, - ) - root.append(el) - tree = etree.ElementTree(root) - # we write the doctype ourselves instead of using the 'doctype' argument - # of 'write' method, becuse lxml will force adding a '\n' even when - # pretty_print is False. - if pretty_print: - header = b"\n".join((XML_DECLARATION, PLIST_DOCTYPE, b"")) - else: - header = XML_DECLARATION + PLIST_DOCTYPE - fp.write(header) - tree.write( - fp, encoding="utf-8", pretty_print=pretty_print, xml_declaration=False - ) - - -def dumps( - value, - sort_keys=True, - skipkeys=False, - use_builtin_types=None, - pretty_print=True, -): - """Write a Python object to a string in plist format. - - Args: - value: An object to write. - sort_keys (bool): Whether keys of dictionaries should be sorted. - skipkeys (bool): Whether to silently skip non-string dictionary - keys. - use_builtin_types (bool): If true, byte strings will be - encoded in Base-64 and wrapped in a ``data`` tag; if - false, they will be either stored as strings or an - exception raised if they cannot be represented. Defaults - pretty_print (bool): Whether to indent the output. - indent_level (int): Level of indentation when serializing. - - Returns: - string: A plist representation of the Python object. - - Raises: - ``TypeError`` - if non-string dictionary keys are serialized - and ``skipkeys`` is false. - ``ValueError`` - if non-representable binary data is present - and `use_builtin_types` is false. - """ - fp = BytesIO() - dump( - value, - fp, - sort_keys=sort_keys, - skipkeys=skipkeys, - use_builtin_types=use_builtin_types, - pretty_print=pretty_print, - ) - return fp.getvalue() diff -Nru fonttools-4.13.0/Snippets/fontTools/mtiLib/__init__.py fonttools-4.16.1/Snippets/fontTools/mtiLib/__init__.py --- fonttools-4.13.0/Snippets/fontTools/mtiLib/__init__.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/mtiLib/__init__.py 2020-10-05 18:25:14.000000000 +0000 @@ -1146,12 +1146,33 @@ return line def build(f, font, tableTag=None): + """Convert a Monotype font layout file to an OpenType layout object + + A font object must be passed, but this may be a "dummy" font; it is only + used for sorting glyph sets when making coverage tables and to hold the + OpenType layout table while it is being built. + + Args: + f: A file object. + font (TTFont): A font object. + tableTag (string): If provided, asserts that the file contains data for the + given OpenType table. + + Returns: + An object representing the table. (e.g. ``table_G_S_U_B_``) + """ lines = Tokenizer(f) return parseTable(lines, font, tableTag=tableTag) def main(args=None, font=None): - """Convert a FontDame OTL file to TTX XML""" + """Convert a FontDame OTL file to TTX XML. + + Writes XML output to stdout. + + Args: + args: Command line arguments (``--font``, ``--table``, input files). + """ import sys from fontTools import configLogger from fontTools.misc.testTools import MockFont diff -Nru fonttools-4.13.0/Snippets/fontTools/otlLib/builder.py fonttools-4.16.1/Snippets/fontTools/otlLib/builder.py --- fonttools-4.13.0/Snippets/fontTools/otlLib/builder.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/otlLib/builder.py 2020-10-05 18:25:14.000000000 +0000 @@ -88,9 +88,10 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ `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 @@ 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 @@ 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 @@ 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 @@ `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 @@ 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 @@ `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 @@ 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 @@ `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 @@ 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 @@ `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 @@ `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 @@ `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 @@ 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 @@ `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 @@ 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 @@ `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 @@ """ 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 @@ `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 @@ `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 @@ 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 @@ 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 @@ 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 @@ 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 @@ `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 @@ # 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 @@ 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 @@ `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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ count += 1 return count + def buildValue(value): """Builds a positioning value record. @@ -2042,6 +2189,7 @@ # GDEF + def buildAttachList(attachPoints, glyphMap): """Builds an AttachList subtable. @@ -2061,8 +2209,7 @@ 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 @@ class ClassDefBuilder(object): """Helper for building ClassDef tables.""" + def __init__(self, useClass0): self.classes_ = set() self.glyphs_ = {} @@ -2380,7 +2528,7 @@ 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 @@ 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 @@ 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 -Nru fonttools-4.13.0/Snippets/fontTools/otlLib/error.py fonttools-4.16.1/Snippets/fontTools/otlLib/error.py --- fonttools-4.13.0/Snippets/fontTools/otlLib/error.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/otlLib/error.py 2020-10-05 18:25:14.000000000 +0000 @@ -1,5 +1,3 @@ - - class OpenTypeLibError(Exception): def __init__(self, message, location): Exception.__init__(self, message) diff -Nru fonttools-4.13.0/Snippets/fontTools/otlLib/maxContextCalc.py fonttools-4.16.1/Snippets/fontTools/otlLib/maxContextCalc.py --- fonttools-4.13.0/Snippets/fontTools/otlLib/maxContextCalc.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/otlLib/maxContextCalc.py 2020-10-05 18:25:14.000000000 +0000 @@ -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 @@ """ # 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 @@ 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 -Nru fonttools-4.13.0/Snippets/fontTools/subset/__init__.py fonttools-4.16.1/Snippets/fontTools/subset/__init__.py --- fonttools-4.13.0/Snippets/fontTools/subset/__init__.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/subset/__init__.py 2020-10-05 18:25:14.000000000 +0000 @@ -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 @@ # 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 @@ -2708,7 +2711,7 @@ def load_font(fontFile, options, allowVID=False, - checkChecksums=False, + checkChecksums=0, dontLoadGlyphNames=False, lazy=True): diff -Nru fonttools-4.13.0/Snippets/fontTools/ttLib/removeOverlaps.py fonttools-4.16.1/Snippets/fontTools/ttLib/removeOverlaps.py --- fonttools-4.13.0/Snippets/fontTools/ttLib/removeOverlaps.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/ttLib/removeOverlaps.py 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,192 @@ +""" Simplify TrueType glyphs by merging overlapping contours/components. + +Requires https://github.com/fonttools/skia-pathops +""" + +import itertools +import logging +from typing import Iterable, Optional, Mapping + +from fontTools.ttLib import ttFont +from fontTools.ttLib.tables import _g_l_y_f +from fontTools.ttLib.tables import _h_m_t_x +from fontTools.pens.ttGlyphPen import TTGlyphPen + +import pathops + + +__all__ = ["removeOverlaps"] + + +log = logging.getLogger("fontTools.ttLib.removeOverlaps") + +_TTGlyphMapping = Mapping[str, ttFont._TTGlyph] + + +def skPathFromGlyph(glyphName: str, glyphSet: _TTGlyphMapping) -> pathops.Path: + path = pathops.Path() + pathPen = path.getPen(glyphSet=glyphSet) + glyphSet[glyphName].draw(pathPen) + return path + + +def skPathFromGlyphComponent( + component: _g_l_y_f.GlyphComponent, glyphSet: _TTGlyphMapping +): + baseGlyphName, transformation = component.getComponentInfo() + path = skPathFromGlyph(baseGlyphName, glyphSet) + return path.transform(*transformation) + + +def componentsOverlap(glyph: _g_l_y_f.Glyph, glyphSet: _TTGlyphMapping) -> bool: + if not glyph.isComposite(): + raise ValueError("This method only works with TrueType composite glyphs") + if len(glyph.components) < 2: + return False # single component, no overlaps + + component_paths = {} + + def _get_nth_component_path(index: int) -> pathops.Path: + if index not in component_paths: + component_paths[index] = skPathFromGlyphComponent( + glyph.components[index], glyphSet + ) + return component_paths[index] + + return any( + pathops.op( + _get_nth_component_path(i), + _get_nth_component_path(j), + pathops.PathOp.INTERSECTION, + fix_winding=False, + keep_starting_points=False, + ) + for i, j in itertools.combinations(range(len(glyph.components)), 2) + ) + + +def ttfGlyphFromSkPath(path: pathops.Path) -> _g_l_y_f.Glyph: + # Skia paths have no 'components', no need for glyphSet + ttPen = TTGlyphPen(glyphSet=None) + path.draw(ttPen) + glyph = ttPen.glyph() + assert not glyph.isComposite() + # compute glyph.xMin (glyfTable parameter unused for non composites) + glyph.recalcBounds(glyfTable=None) + return glyph + + +def removeTTGlyphOverlaps( + glyphName: str, + glyphSet: _TTGlyphMapping, + glyfTable: _g_l_y_f.table__g_l_y_f, + hmtxTable: _h_m_t_x.table__h_m_t_x, + removeHinting: bool = True, +) -> bool: + glyph = glyfTable[glyphName] + # decompose composite glyphs only if components overlap each other + if ( + glyph.numberOfContours > 0 + or glyph.isComposite() + and componentsOverlap(glyph, glyphSet) + ): + path = skPathFromGlyph(glyphName, glyphSet) + + # remove overlaps + path2 = pathops.simplify(path, clockwise=path.clockwise) + + # replace TTGlyph if simplified path is different (ignoring contour order) + if {tuple(c) for c in path.contours} != {tuple(c) for c in path2.contours}: + glyfTable[glyphName] = glyph = ttfGlyphFromSkPath(path2) + # simplified glyph is always unhinted + assert not glyph.program + # also ensure hmtx LSB == glyph.xMin so glyph origin is at x=0 + width, lsb = hmtxTable[glyphName] + if lsb != glyph.xMin: + hmtxTable[glyphName] = (width, glyph.xMin) + return True + + if removeHinting: + glyph.removeHinting() + return False + + +def removeOverlaps( + font: ttFont.TTFont, + glyphNames: Optional[Iterable[str]] = None, + removeHinting: bool = True, +) -> None: + """Simplify glyphs in TTFont by merging overlapping contours. + + Overlapping components are first decomposed to simple contours, then merged. + + Currently this only works with TrueType fonts with 'glyf' table. + Raises NotImplementedError if 'glyf' table is absent. + + Note that removing overlaps invalidates the hinting. By default we drop hinting + from all glyphs whether or not overlaps are removed from a given one, as it would + look weird if only some glyphs are left (un)hinted. + + Args: + font: input TTFont object, modified in place. + glyphNames: optional iterable of glyph names (str) to remove overlaps from. + By default, all glyphs in the font are processed. + removeHinting (bool): set to False to keep hinting for unmodified glyphs. + """ + try: + glyfTable = font["glyf"] + except KeyError: + raise NotImplementedError("removeOverlaps currently only works with TTFs") + + hmtxTable = font["hmtx"] + # wraps the underlying glyf Glyphs, takes care of interfacing with drawing pens + glyphSet = font.getGlyphSet() + + if glyphNames is None: + glyphNames = font.getGlyphOrder() + + # process all simple glyphs first, then composites with increasing component depth, + # so that by the time we test for component intersections the respective base glyphs + # have already been simplified + glyphNames = sorted( + glyphNames, + key=lambda name: ( + glyfTable[name].getCompositeMaxpValues(glyfTable).maxComponentDepth + if glyfTable[name].isComposite() + else 0, + name, + ), + ) + modified = set() + for glyphName in glyphNames: + if removeTTGlyphOverlaps( + glyphName, glyphSet, glyfTable, hmtxTable, removeHinting + ): + modified.add(glyphName) + + log.debug("Removed overlaps for %s glyphs:\n%s", len(modified), " ".join(modified)) + + +def main(args=None): + import sys + + if args is None: + args = sys.argv[1:] + + if len(args) < 2: + print( + f"usage: fonttools ttLib.removeOverlaps INPUT.ttf OUTPUT.ttf [GLYPHS ...]" + ) + sys.exit(1) + + src = args[0] + dst = args[1] + glyphNames = args[2:] or None + + with ttFont.TTFont(src) as f: + removeOverlaps(f, glyphNames) + f.save(dst) + + +if __name__ == "__main__": + main() diff -Nru fonttools-4.13.0/Snippets/fontTools/ttLib/sfnt.py fonttools-4.16.1/Snippets/fontTools/ttLib/sfnt.py --- fonttools-4.13.0/Snippets/fontTools/ttLib/sfnt.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/ttLib/sfnt.py 2020-10-05 18:25:14.000000000 +0000 @@ -43,7 +43,7 @@ # return default object return object.__new__(cls) - def __init__(self, file, checkChecksums=1, fontNumber=-1): + def __init__(self, file, checkChecksums=0, fontNumber=-1): self.file = file self.checkChecksums = checkChecksums diff -Nru fonttools-4.13.0/Snippets/fontTools/ttLib/tables/D__e_b_g.py fonttools-4.16.1/Snippets/fontTools/ttLib/tables/D__e_b_g.py --- fonttools-4.13.0/Snippets/fontTools/ttLib/tables/D__e_b_g.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/ttLib/tables/D__e_b_g.py 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,17 @@ +import json + +from . import DefaultTable + + +class table_D__e_b_g(DefaultTable.DefaultTable): + def decompile(self, data, ttFont): + self.data = json.loads(data) + + def compile(self, ttFont): + return json.dumps(self.data).encode("utf-8") + + def toXML(self, writer, ttFont): + writer.writecdata(json.dumps(self.data)) + + def fromXML(self, name, attrs, content, ttFont): + self.data = json.loads(content) diff -Nru fonttools-4.13.0/Snippets/fontTools/ttLib/tables/_g_l_y_f.py fonttools-4.16.1/Snippets/fontTools/ttLib/tables/_g_l_y_f.py --- fonttools-4.13.0/Snippets/fontTools/ttLib/tables/_g_l_y_f.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/ttLib/tables/_g_l_y_f.py 2020-10-05 18:25:14.000000000 +0000 @@ -650,6 +650,7 @@ 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 @@ 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 -Nru fonttools-4.13.0/Snippets/fontTools/ttLib/tables/otTables.py fonttools-4.16.1/Snippets/fontTools/ttLib/tables/otTables.py --- fonttools-4.13.0/Snippets/fontTools/ttLib/tables/otTables.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/ttLib/tables/otTables.py 2020-10-05 18:25:14.000000000 +0000 @@ -12,6 +12,7 @@ from fontTools.misc.fixedTools import otRound from fontTools.misc.textTools import pad, safeEval from .otBase import BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference +from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY import logging import struct @@ -1187,6 +1188,44 @@ } +class LookupList(BaseTable): + @property + def table(self): + for l in self.Lookup: + for st in l.SubTable: + if type(st).__name__.endswith("Subst"): + return "GSUB" + if type(st).__name__.endswith("Pos"): + return "GPOS" + raise ValueError + + def toXML2(self, xmlWriter, font): + if not font or "Debg" not in font or LOOKUP_DEBUG_INFO_KEY not in font["Debg"].data: + return super().toXML2(xmlWriter, font) + debugData = font["Debg"].data[LOOKUP_DEBUG_INFO_KEY][self.table] + for conv in self.getConverters(): + if conv.repeat: + value = getattr(self, conv.name, []) + for lookupIndex, item in enumerate(value): + if str(lookupIndex) in debugData: + info = LookupDebugInfo(*debugData[str(lookupIndex)]) + tag = info.location + if info.name: + tag = f'{info.name}: {tag}' + if info.feature: + script,language,feature = info.feature + tag = f'{tag} in {feature} ({script}/{language})' + xmlWriter.comment(tag) + xmlWriter.newline() + + conv.xmlWrite(xmlWriter, font, item, conv.name, + [("index", lookupIndex)]) + else: + if conv.aux and not eval(conv.aux, None, vars(self)): + continue + value = getattr(self, conv.name, None) # TODO Handle defaults instead of defaulting to None! + conv.xmlWrite(xmlWriter, font, value, conv.name, []) + class BaseGlyphRecordArray(BaseTable): def preWrite(self, font): diff -Nru fonttools-4.13.0/Snippets/fontTools/ttLib/ttFont.py fonttools-4.16.1/Snippets/fontTools/ttLib/ttFont.py --- fonttools-4.13.0/Snippets/fontTools/ttLib/ttFont.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/ttLib/ttFont.py 2020-10-05 18:25:14.000000000 +0000 @@ -18,7 +18,7 @@ """ def __init__(self, file=None, res_name_or_index=None, - sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False, + sfntVersion="\000\001\000\000", flavor=None, checkChecksums=0, verbose=None, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False, recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None, _tableCache=None): @@ -830,10 +830,48 @@ return getattr(tables, pyTag) -def getTableClass(tag): - """Fetch the packer/unpacker class for a table. - Return None when no class is found. +# Registry for custom table packer/unpacker classes. Keys are table +# tags, values are (moduleName, className) tuples. +# See registerCustomTableClass() and getCustomTableClass() +_customTableRegistry = {} + + +def registerCustomTableClass(tag, moduleName, className=None): + """Register a custom packer/unpacker class for a table. + The 'moduleName' must be an importable module. If no 'className' + is given, it is derived from the tag, for example it will be + table_C_U_S_T_ for a 'CUST' tag. + + The registered table class should be a subclass of + fontTools.ttLib.tables.DefaultTable.DefaultTable + """ + if className is None: + className = "table_" + tagToIdentifier(tag) + _customTableRegistry[tag] = (moduleName, className) + + +def unregisterCustomTableClass(tag): + """Unregister the custom packer/unpacker class for a table.""" + del _customTableRegistry[tag] + + +def getCustomTableClass(tag): + """Return the custom table class for tag, if one has been registered + with 'registerCustomTableClass()'. Else return None. """ + if tag not in _customTableRegistry: + return None + import importlib + moduleName, className = _customTableRegistry[tag] + module = importlib.import_module(moduleName) + return getattr(module, className) + + +def getTableClass(tag): + """Fetch the packer/unpacker class for a table.""" + tableClass = getCustomTableClass(tag) + if tableClass is not None: + return tableClass module = getTableModule(tag) if module is None: from .tables.DefaultTable import DefaultTable diff -Nru fonttools-4.13.0/Snippets/fontTools/ttLib/woff2.py fonttools-4.16.1/Snippets/fontTools/ttLib/woff2.py --- fonttools-4.13.0/Snippets/fontTools/ttLib/woff2.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/ttLib/woff2.py 2020-10-05 18:25:14.000000000 +0000 @@ -29,7 +29,7 @@ flavor = "woff2" - def __init__(self, file, checkChecksums=1, fontNumber=-1): + def __init__(self, file, checkChecksums=0, fontNumber=-1): if not haveBrotli: log.error( 'The WOFF2 decoder requires the Brotli Python extension, available at: ' diff -Nru fonttools-4.13.0/Snippets/fontTools/ufoLib/filenames.py fonttools-4.16.1/Snippets/fontTools/ufoLib/filenames.py --- fonttools-4.13.0/Snippets/fontTools/ufoLib/filenames.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/ufoLib/filenames.py 2020-10-05 18:25:14.000000000 +0000 @@ -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 -Nru fonttools-4.13.0/Snippets/fontTools/ufoLib/glifLib.py fonttools-4.16.1/Snippets/fontTools/ufoLib/glifLib.py --- fonttools-4.13.0/Snippets/fontTools/ufoLib/glifLib.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/ufoLib/glifLib.py 2020-10-05 18:25:14.000000000 +0000 @@ -1416,10 +1416,10 @@ raise GlifLibError("Unknown child elements in point element.") # x and y are required for attr in ("x", "y"): - value = element.get(attr) - if validate and value is None: - raise GlifLibError("Required %s attribute is missing in point element." % attr) - point[attr] = _number(value) + try: + point[attr] = _number(point[attr]) + except KeyError as e: + raise GlifLibError(f"Required {attr} attribute is missing in point element.") from e # segment type pointType = point.pop("type", "offcurve") if validate and pointType not in pointTypeOptions: diff -Nru fonttools-4.13.0/Snippets/fontTools/varLib/instancer.py fonttools-4.16.1/Snippets/fontTools/varLib/instancer.py --- fonttools-4.13.0/Snippets/fontTools/varLib/instancer.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/varLib/instancer.py 2020-10-05 18:25:14.000000000 +0000 @@ -87,6 +87,7 @@ from contextlib import contextmanager import collections from copy import deepcopy +from enum import IntEnum import logging from itertools import islice import os @@ -121,6 +122,12 @@ return self +class OverlapMode(IntEnum): + KEEP_AND_DONT_SET_FLAGS = 0 + KEEP_AND_SET_FLAGS = 1 + REMOVE = 2 + + def instantiateTupleVariationStore( variations, axisLimits, origCoords=None, endPts=None ): @@ -578,7 +585,7 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, axisLimits): - """ Compute deltas at partial location, and update varStore in-place. + """Compute deltas at partial location, and update varStore in-place. Remove regions in which all axes were instanced, or fall outside the new axis limits. Scale the deltas of the remaining regions where only some of the axes @@ -676,8 +683,8 @@ def instantiateFeatureVariations(varfont, axisLimits): for tableTag in ("GPOS", "GSUB"): - if tableTag not in varfont or not hasattr( - varfont[tableTag].table, "FeatureVariations" + if tableTag not in varfont or not getattr( + varfont[tableTag].table, "FeatureVariations", None ): continue log.info("Instantiating FeatureVariations of %s table", tableTag) @@ -1175,9 +1182,13 @@ def instantiateVariableFont( - varfont, axisLimits, inplace=False, optimize=True, overlap=True + varfont, + axisLimits, + inplace=False, + optimize=True, + overlap=OverlapMode.KEEP_AND_SET_FLAGS, ): - """ Instantiate variable font, either fully or partially. + """Instantiate variable font, either fully or partially. Depending on whether the `axisLimits` dictionary references all or some of the input varfont's axes, the output font will either be a full instance (static @@ -1198,13 +1209,20 @@ remaining 'gvar' table's deltas. Possibly faster, and might work around rendering issues in some buggy environments, at the cost of a slightly larger file size. - overlap (bool): variable fonts usually contain overlapping contours, and some - font rendering engines on Apple platforms require that the `OVERLAP_SIMPLE` - and `OVERLAP_COMPOUND` flags in the 'glyf' table be set to force rendering - using a non-zero fill rule. Thus we always set these flags on all glyphs - to maximise cross-compatibility of the generated instance. You can disable - this by setting `overalap` to False. + overlap (OverlapMode): variable fonts usually contain overlapping contours, and + some font rendering engines on Apple platforms require that the + `OVERLAP_SIMPLE` and `OVERLAP_COMPOUND` flags in the 'glyf' table be set to + force rendering using a non-zero fill rule. Thus we always set these flags + on all glyphs to maximise cross-compatibility of the generated instance. + You can disable this by passing OverlapMode.KEEP_AND_DONT_SET_FLAGS. + If you want to remove the overlaps altogether and merge overlapping + contours and components, you can pass OverlapMode.REMOVE. Note that this + requires the skia-pathops package (available to pip install). + The overlap parameter only has effect when generating full static instances. """ + # 'overlap' used to be bool and is now enum; for backward compat keep accepting bool + overlap = OverlapMode(int(overlap)) + sanityCheckVariableTables(varfont) axisLimits = populateAxisDefaults(varfont, axisLimits) @@ -1245,8 +1263,14 @@ instantiateFvar(varfont, axisLimits) if "fvar" not in varfont: - if "glyf" in varfont and overlap: - setMacOverlapFlags(varfont["glyf"]) + if "glyf" in varfont: + if overlap == OverlapMode.KEEP_AND_SET_FLAGS: + setMacOverlapFlags(varfont["glyf"]) + elif overlap == OverlapMode.REMOVE: + from fontTools.ttLib.removeOverlaps import removeOverlaps + + log.info("Removing overlaps from glyf table") + removeOverlaps(varfont) varLib.set_default_weight_width_slant( varfont, @@ -1346,6 +1370,13 @@ help="Don't set OVERLAP_SIMPLE/OVERLAP_COMPOUND glyf flags (only applicable " "when generating a full instance)", ) + parser.add_argument( + "--remove-overlaps", + dest="remove_overlaps", + action="store_true", + help="Merge overlapping contours and components (only applicable " + "when generating a full instance). Requires skia-pathops", + ) loggingGroup = parser.add_mutually_exclusive_group(required=False) loggingGroup.add_argument( "-v", "--verbose", action="store_true", help="Run more verbosely." @@ -1355,6 +1386,11 @@ ) options = parser.parse_args(args) + if options.remove_overlaps: + options.overlap = OverlapMode.REMOVE + else: + options.overlap = OverlapMode(int(options.overlap)) + infile = options.input if not os.path.isfile(infile): parser.error("No such file '{}'".format(infile)) diff -Nru fonttools-4.13.0/Snippets/fontTools/varLib/interpolatable.py fonttools-4.16.1/Snippets/fontTools/varLib/interpolatable.py --- fonttools-4.13.0/Snippets/fontTools/varLib/interpolatable.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/varLib/interpolatable.py 2020-10-05 18:25:14.000000000 +0000 @@ -104,6 +104,7 @@ try: allVectors = [] + allNodeTypes = [] for glyphset,name in zip(glyphsets, names): #print('.', end='') glyph = glyphset[glyph_name] @@ -114,8 +115,11 @@ del perContourPen contourVectors = [] + nodeTypes = [] + allNodeTypes.append(nodeTypes) allVectors.append(contourVectors) for contour in contourPens: + nodeTypes.append(tuple(instruction[0] for instruction in contour.value)) stats = StatisticsPen(glyphset=glyphset) contour.replay(stats) size = abs(stats.area) ** .5 * .5 @@ -131,6 +135,23 @@ #print(vector) # Check each master against the next one in the list. + for i, (m0, m1) in enumerate(zip(allNodeTypes[:-1], allNodeTypes[1:])): + if len(m0) != len(m1): + print('%s: %s+%s: Glyphs not compatible (wrong number of paths %i+%i)!!!!!' % (glyph_name, names[i], names[i+1], len(m0), len(m1))) + if m0 == m1: + continue + for pathIx, (nodes1, nodes2) in enumerate(zip(m0, m1)): + if nodes1 == nodes2: + continue + print('%s: %s+%s: Glyphs not compatible at path %i!!!!!' % (glyph_name, names[i], names[i+1], pathIx)) + if len(nodes1) != len(nodes2): + print("%s has %i nodes, %s has %i nodes" % (names[i], len(nodes1), names[i+1], len(nodes2))) + continue + for nodeIx, (n1, n2) in enumerate(zip(nodes1, nodes2)): + if n1 != n2: + print("At node %i, %s has %s, %s has %s" % (nodeIx, names[i], n1, names[i+1], n2)) + continue + for i,(m0,m1) in enumerate(zip(allVectors[:-1],allVectors[1:])): if len(m0) != len(m1): print('%s: %s+%s: Glyphs not compatible!!!!!' % (glyph_name, names[i], names[i+1])) diff -Nru fonttools-4.13.0/Snippets/fontTools/varLib/models.py fonttools-4.16.1/Snippets/fontTools/varLib/models.py --- fonttools-4.13.0/Snippets/fontTools/varLib/models.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/varLib/models.py 2020-10-05 18:25:14.000000000 +0000 @@ -444,10 +444,10 @@ 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 -Nru fonttools-4.13.0/Snippets/fontTools/varLib/mutator.py fonttools-4.16.1/Snippets/fontTools/varLib/mutator.py --- fonttools-4.13.0/Snippets/fontTools/varLib/mutator.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Snippets/fontTools/varLib/mutator.py 2020-10-05 18:25:14.000000000 +0000 @@ -257,7 +257,7 @@ if not tableTag in varfont: continue table = varfont[tableTag].table - if not hasattr(table, 'FeatureVariations'): + if not getattr(table, 'FeatureVariations', None): continue variations = table.FeatureVariations for record in variations.FeatureVariationRecord: diff -Nru fonttools-4.13.0/Tests/feaLib/builder_test.py fonttools-4.16.1/Tests/feaLib/builder_test.py --- fonttools-4.13.0/Tests/feaLib/builder_test.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/feaLib/builder_test.py 2020-10-05 18:25:14.000000000 +0000 @@ -69,10 +69,10 @@ 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): @@ -114,12 +114,16 @@ lines.append(line.rstrip() + os.linesep) return lines - def expect_ttx(self, font, expected_ttx): + def expect_ttx(self, font, expected_ttx, replace=None): path = self.temp_path(suffix=".ttx") font.saveXML(path, tables=['head', 'name', 'BASE', 'GDEF', 'GSUB', 'GPOS', 'OS/2', 'hhea', 'vhea']) actual = self.read_ttx(path) expected = self.read_ttx(expected_ttx) + if replace: + for i in range(len(expected)): + for k, v in replace.items(): + expected[i] = expected[i].replace(k, v) if actual != expected: for line in difflib.unified_diff( expected, actual, fromfile=expected_ttx, tofile=path): @@ -133,12 +137,17 @@ def check_feature_file(self, name): font = makeTTFont() - addOpenTypeFeatures(font, self.getpath("%s.fea" % name)) + feapath = self.getpath("%s.fea" % name) + addOpenTypeFeatures(font, feapath) self.expect_ttx(font, self.getpath("%s.ttx" % name)) # Make sure we can produce binary OpenType tables, not just XML. for tag in ('GDEF', 'GSUB', 'GPOS'): if tag in font: font[tag].compile(font) + debugttx = self.getpath("%s-debug.ttx" % name) + if os.path.exists(debugttx): + addOpenTypeFeatures(font, feapath, debug=True) + self.expect_ttx(font, debugttx, replace = {"__PATH__": feapath}) def check_fea2fea_file(self, name, base=None, parser=Parser): font = makeTTFont() diff -Nru fonttools-4.13.0/Tests/feaLib/data/bug506.ttx fonttools-4.16.1/Tests/feaLib/data/bug506.ttx --- fonttools-4.13.0/Tests/feaLib/data/bug506.ttx 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/feaLib/data/bug506.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -30,25 +30,23 @@ - + - - - - + + + + - - + + - - - + - + diff -Nru fonttools-4.13.0/Tests/feaLib/data/bug509.ttx fonttools-4.16.1/Tests/feaLib/data/bug509.ttx --- fonttools-4.13.0/Tests/feaLib/data/bug509.ttx 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/feaLib/data/bug509.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -30,33 +30,29 @@ - + - - - - - - - + + - - - - - + + + + + + + + - - - + - + diff -Nru fonttools-4.13.0/Tests/feaLib/data/bug512.ttx fonttools-4.16.1/Tests/feaLib/data/bug512.ttx --- fonttools-4.13.0/Tests/feaLib/data/bug512.ttx 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/feaLib/data/bug512.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -30,61 +30,53 @@ - + - - - - - - - + + + + + - - - - - - - - + + + + + + - - - - - - - - + + + + + + - - - - - - - - + + + + + + - + diff -Nru fonttools-4.13.0/Tests/feaLib/data/ChainSubstSubtable.fea fonttools-4.16.1/Tests/feaLib/data/ChainSubstSubtable.fea --- fonttools-4.13.0/Tests/feaLib/data/ChainSubstSubtable.fea 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/feaLib/data/ChainSubstSubtable.fea 2020-10-05 18:25:14.000000000 +0000 @@ -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 -Nru fonttools-4.13.0/Tests/feaLib/data/ChainSubstSubtable.ttx fonttools-4.16.1/Tests/feaLib/data/ChainSubstSubtable.ttx --- fonttools-4.13.0/Tests/feaLib/data/ChainSubstSubtable.ttx 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/feaLib/data/ChainSubstSubtable.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -34,7 +34,10 @@ - + + + + @@ -47,7 +50,10 @@ - + + + + @@ -60,7 +66,10 @@ - + + + + @@ -73,7 +82,10 @@ - + + + + diff -Nru fonttools-4.13.0/Tests/feaLib/data/lookup-debug.ttx fonttools-4.16.1/Tests/feaLib/data/lookup-debug.ttx --- fonttools-4.13.0/Tests/feaLib/data/lookup-debug.ttx 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Tests/feaLib/data/lookup-debug.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru fonttools-4.13.0/Tests/feaLib/data/lookupflag.fea fonttools-4.16.1/Tests/feaLib/data/lookupflag.fea --- fonttools-4.13.0/Tests/feaLib/data/lookupflag.fea 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/feaLib/data/lookupflag.fea 2020-10-05 18:25:14.000000000 +0000 @@ -147,3 +147,13 @@ 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 -Nru fonttools-4.13.0/Tests/feaLib/data/lookupflag.ttx fonttools-4.16.1/Tests/feaLib/data/lookupflag.ttx --- fonttools-4.13.0/Tests/feaLib/data/lookupflag.ttx 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/feaLib/data/lookupflag.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -107,7 +107,7 @@ - + @@ -400,6 +400,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru fonttools-4.13.0/Tests/feaLib/data/spec6h_iii_3d.ttx fonttools-4.16.1/Tests/feaLib/data/spec6h_iii_3d.ttx --- fonttools-4.13.0/Tests/feaLib/data/spec6h_iii_3d.ttx 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/feaLib/data/spec6h_iii_3d.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -30,25 +30,23 @@ - + - - - - + + + + - - + + - - - + - + diff -Nru fonttools-4.13.0/Tests/feaLib/data/spec6h_ii.ttx fonttools-4.16.1/Tests/feaLib/data/spec6h_ii.ttx --- fonttools-4.13.0/Tests/feaLib/data/spec6h_ii.ttx 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/feaLib/data/spec6h_ii.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -112,25 +112,23 @@ - + - - - - + + + + - - + + - - + + - - - + @@ -139,7 +137,7 @@ - + diff -Nru fonttools-4.13.0/Tests/feaLib/data/SubstSubtable.fea fonttools-4.16.1/Tests/feaLib/data/SubstSubtable.fea --- fonttools-4.13.0/Tests/feaLib/data/SubstSubtable.fea 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Tests/feaLib/data/SubstSubtable.fea 2020-10-05 18:25:14.000000000 +0000 @@ -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 -Nru fonttools-4.13.0/Tests/feaLib/data/SubstSubtable.ttx fonttools-4.16.1/Tests/feaLib/data/SubstSubtable.ttx --- fonttools-4.13.0/Tests/feaLib/data/SubstSubtable.ttx 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Tests/feaLib/data/SubstSubtable.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru fonttools-4.13.0/Tests/feaLib/parser_test.py fonttools-4.16.1/Tests/feaLib/parser_test.py --- fonttools-4.13.0/Tests/feaLib/parser_test.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/feaLib/parser_test.py 2020-10-05 18:25:14.000000000 +0000 @@ -718,6 +718,18 @@ 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 @@ 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 -Nru fonttools-4.13.0/Tests/otlLib/builder_test.py fonttools-4.16.1/Tests/otlLib/builder_test.py --- fonttools-4.13.0/Tests/otlLib/builder_test.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/otlLib/builder_test.py 2020-10-05 18:25:14.000000000 +0000 @@ -1392,6 +1392,57 @@ 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 -Nru fonttools-4.13.0/Tests/otlLib/mock_builder_test.py fonttools-4.16.1/Tests/otlLib/mock_builder_test.py --- fonttools-4.13.0/Tests/otlLib/mock_builder_test.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/otlLib/mock_builder_test.py 2020-10-05 18:25:14.000000000 +0000 @@ -13,6 +13,7 @@ ClassPairPosSubtableBuilder, PairPosBuilder, SinglePosBuilder, + ChainContextualRule ) from fontTools.otlLib.error import OpenTypeLibError from fontTools.ttLib import TTFont @@ -79,7 +80,7 @@ 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 -Nru fonttools-4.13.0/Tests/ttLib/tables/_g_l_y_f_test.py fonttools-4.16.1/Tests/ttLib/tables/_g_l_y_f_test.py --- fonttools-4.13.0/Tests/ttLib/tables/_g_l_y_f_test.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/ttLib/tables/_g_l_y_f_test.py 2020-10-05 18:25:14.000000000 +0000 @@ -471,6 +471,30 @@ ] ) + 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 -Nru fonttools-4.13.0/Tests/ttLib/ttFont_test.py fonttools-4.16.1/Tests/ttLib/ttFont_test.py --- fonttools-4.13.0/Tests/ttLib/ttFont_test.py 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Tests/ttLib/ttFont_test.py 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,48 @@ +import io +from fontTools.ttLib import TTFont, newTable, registerCustomTableClass, unregisterCustomTableClass +from fontTools.ttLib.tables.DefaultTable import DefaultTable + + +class CustomTableClass(DefaultTable): + + def decompile(self, data, ttFont): + self.numbers = list(data) + + def compile(self, ttFont): + return bytes(self.numbers) + + # not testing XML read/write + + +table_C_U_S_T_ = CustomTableClass # alias for testing + + +TABLETAG = "CUST" + + +def test_registerCustomTableClass(): + font = TTFont() + font[TABLETAG] = newTable(TABLETAG) + font[TABLETAG].data = b"\x00\x01\xff" + f = io.BytesIO() + font.save(f) + f.seek(0) + assert font[TABLETAG].data == b"\x00\x01\xff" + registerCustomTableClass(TABLETAG, "ttFont_test", "CustomTableClass") + try: + font = TTFont(f) + assert font[TABLETAG].numbers == [0, 1, 255] + assert font[TABLETAG].compile(font) == b"\x00\x01\xff" + finally: + unregisterCustomTableClass(TABLETAG) + + +def test_registerCustomTableClassStandardName(): + registerCustomTableClass(TABLETAG, "ttFont_test") + try: + font = TTFont() + font[TABLETAG] = newTable(TABLETAG) + font[TABLETAG].numbers = [4, 5, 6] + assert font[TABLETAG].compile(font) == b"\x04\x05\x06" + finally: + unregisterCustomTableClass(TABLETAG) diff -Nru fonttools-4.13.0/Tests/ufoLib/glifLib_test.py fonttools-4.16.1/Tests/ufoLib/glifLib_test.py --- fonttools-4.13.0/Tests/ufoLib/glifLib_test.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/ufoLib/glifLib_test.py 2020-10-05 18:25:14.000000000 +0000 @@ -10,6 +10,7 @@ ) from fontTools.ufoLib.errors import GlifLibError, UnsupportedGLIFFormat, UnsupportedUFOFormat from fontTools.misc.etree import XML_DECLARATION +from fontTools.pens.recordingPen import RecordingPointPen import pytest GLYPHSETDIR = getDemoFontGlyphSetPath() @@ -250,6 +251,27 @@ with pytest.raises(GlifLibError, match="Forbidden GLIF format version"): readGlyphFromString(s, _Glyph(), formatVersions=[1]) + def test_read_ensure_x_y(self): + """Ensure that a proper GlifLibError is raised when point coordinates are + missing, regardless of validation setting.""" + + s = """ + + + + + + + + + """ + pen = RecordingPointPen() + + with pytest.raises(GlifLibError, match="Required y attribute"): + readGlyphFromString(s, _Glyph(), pen) + + with pytest.raises(GlifLibError, match="Required y attribute"): + readGlyphFromString(s, _Glyph(), pen, validate=False) def test_GlyphSet_unsupported_ufoFormatVersion(tmp_path, caplog): with pytest.raises(UnsupportedUFOFormat): diff -Nru fonttools-4.13.0/Tests/varLib/data/PartialInstancerTest3-VF.ttx fonttools-4.16.1/Tests/varLib/data/PartialInstancerTest3-VF.ttx --- fonttools-4.13.0/Tests/varLib/data/PartialInstancerTest3-VF.ttx 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Tests/varLib/data/PartialInstancerTest3-VF.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Weight + + + Regular + + + Remove Overlaps Test + + + Regular + + + 1.000;NONE;RemoveOverlapsTest-Regular + + + Remove Overlaps Test Regular + + + Version 1.000 + + + RemoveOverlapsTest-Regular + + + Weight + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wght + 0x0 + 400.0 + 400.0 + 700.0 + 256 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru fonttools-4.13.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_diff.ttx fonttools-4.16.1/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_diff.ttx --- fonttools-4.13.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_diff.ttx 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_diff.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru fonttools-4.13.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_same.ttx fonttools-4.16.1/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_same.ttx --- fonttools-4.13.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_same.ttx 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_same.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru fonttools-4.13.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx fonttools-4.16.1/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx --- fonttools-4.13.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx 1970-01-01 00:00:00.000000000 +0000 @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff -Nru fonttools-4.13.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx fonttools-4.16.1/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx --- fonttools-4.13.0/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx 1970-01-01 00:00:00.000000000 +0000 @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff -Nru fonttools-4.13.0/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlap-flags.ttx fonttools-4.16.1/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlap-flags.ttx --- fonttools-4.13.0/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlap-flags.ttx 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlap-flags.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,305 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Weight + + + Remove Overlaps Test + + + Regular + + + 1.000;NONE;RemoveOverlapsTest-Regular + + + Remove Overlaps Test Regular + + + Version 1.000 + + + RemoveOverlapsTest-Regular + + + Weight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru fonttools-4.13.0/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlaps.ttx fonttools-4.16.1/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlaps.ttx --- fonttools-4.13.0/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlaps.ttx 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-400-no-overlaps.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Weight + + + Remove Overlaps Test + + + Regular + + + 1.000;NONE;RemoveOverlapsTest-Regular + + + Remove Overlaps Test Regular + + + Version 1.000 + + + RemoveOverlapsTest-Regular + + + Weight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru fonttools-4.13.0/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-700-no-overlaps.ttx fonttools-4.16.1/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-700-no-overlaps.ttx --- fonttools-4.13.0/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-700-no-overlaps.ttx 1970-01-01 00:00:00.000000000 +0000 +++ fonttools-4.16.1/Tests/varLib/data/test_results/PartialInstancerTest3-VF-instance-700-no-overlaps.ttx 2020-10-05 18:25:14.000000000 +0000 @@ -0,0 +1,367 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Weight + + + Remove Overlaps Test + + + Regular + + + 1.000;NONE;RemoveOverlapsTest-Regular + + + Remove Overlaps Test Regular + + + Version 1.000 + + + RemoveOverlapsTest-Regular + + + Weight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru fonttools-4.13.0/Tests/varLib/instancer_test.py fonttools-4.16.1/Tests/varLib/instancer_test.py --- fonttools-4.13.0/Tests/varLib/instancer_test.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/varLib/instancer_test.py 2020-10-05 18:25:14.000000000 +0000 @@ -1400,6 +1400,13 @@ return f +@pytest.fixture +def varfont3(): + f = ttLib.TTFont(recalcTimestamp=False) + f.importXML(os.path.join(TESTDATA, "PartialInstancerTest3-VF.ttx")) + return f + + def _dump_ttx(ttFont): # compile to temporary bytes stream, reload and dump to XML tmp = BytesIO() @@ -1411,13 +1418,16 @@ return _strip_ttLibVersion(s.getvalue()) -def _get_expected_instance_ttx(wght, wdth): +def _get_expected_instance_ttx( + name, *locations, overlap=instancer.OverlapMode.KEEP_AND_SET_FLAGS +): + filename = f"{name}-VF-instance-{','.join(str(loc) for loc in locations)}" + if overlap == instancer.OverlapMode.KEEP_AND_DONT_SET_FLAGS: + filename += "-no-overlap-flags" + elif overlap == instancer.OverlapMode.REMOVE: + filename += "-no-overlaps" with open( - os.path.join( - TESTDATA, - "test_results", - "PartialInstancerTest2-VF-instance-{0},{1}.ttx".format(wght, wdth), - ), + os.path.join(TESTDATA, "test_results", f"{filename}.ttx"), "r", encoding="utf-8", ) as fp: @@ -1433,7 +1443,7 @@ partial = instancer.instantiateVariableFont(varfont2, {"wght": wght}) instance = instancer.instantiateVariableFont(partial, {"wdth": wdth}) - expected = _get_expected_instance_ttx(wght, wdth) + expected = _get_expected_instance_ttx("PartialInstancerTest2", wght, wdth) assert _dump_ttx(instance) == expected @@ -1442,7 +1452,30 @@ varfont2, {"wght": None, "wdth": None} ) - expected = _get_expected_instance_ttx(400, 100) + expected = _get_expected_instance_ttx("PartialInstancerTest2", 400, 100) + + assert _dump_ttx(instance) == expected + + @pytest.mark.parametrize( + "overlap, wght", + [ + (instancer.OverlapMode.KEEP_AND_DONT_SET_FLAGS, 400), + (instancer.OverlapMode.REMOVE, 400), + (instancer.OverlapMode.REMOVE, 700), + ], + ) + def test_overlap(self, varfont3, wght, overlap): + pytest.importorskip("pathops") + + location = {"wght": wght} + + instance = instancer.instantiateVariableFont( + varfont3, location, overlap=overlap + ) + + expected = _get_expected_instance_ttx( + "PartialInstancerTest3", wght, overlap=overlap + ) assert _dump_ttx(instance) == expected @@ -1633,6 +1666,19 @@ assert len(rec1.ConditionSet.ConditionTable) == 2 assert rec1.ConditionSet.ConditionTable[0].Format == 2 + def test_GSUB_FeatureVariations_is_None(self, varfont2): + varfont2["GSUB"].table.Version = 0x00010001 + varfont2["GSUB"].table.FeatureVariations = None + tmp = BytesIO() + varfont2.save(tmp) + varfont = ttLib.TTFont(tmp) + + # DO NOT raise an exception when the optional 'FeatureVariations' attribute is + # present but is set to None (e.g. with GSUB 1.1); skip and do nothing. + assert varfont["GSUB"].table.FeatureVariations is None + instancer.instantiateFeatureVariations(varfont, {"wght": 400, "wdth": 100}) + assert varfont["GSUB"].table.FeatureVariations is None + class LimitTupleVariationAxisRangesTest: def check_limit_single_var_axis_range(self, var, axisTag, axisRange, expected): diff -Nru fonttools-4.13.0/Tests/varLib/interpolate_layout_test.py fonttools-4.16.1/Tests/varLib/interpolate_layout_test.py --- fonttools-4.13.0/Tests/varLib/interpolate_layout_test.py 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/Tests/varLib/interpolate_layout_test.py 2020-10-05 18:25:14.000000000 +0000 @@ -748,8 +748,8 @@ 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 @@ 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 @@ 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 -Nru fonttools-4.13.0/tox.ini fonttools-4.16.1/tox.ini --- fonttools-4.13.0/tox.ini 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/tox.ini 2020-10-05 18:25:14.000000000 +0000 @@ -1,11 +1,16 @@ [tox] minversion = 3.0 -envlist = py3{6,7,8}-cov, htmlcov +envlist = mypy, py3{6,7,8}-cov, htmlcov skip_missing_interpreters=true [testenv] setenv = cy: FONTTOOLS_WITH_CYTHON=1 +# use 'download = true' to have tox install the latest pip inside the virtualenv. +# We need this to be able to install skia-pathops on Linux, which uses a +# relatively recent 'manylinux2014' platform tag. +# https://github.com/tox-dev/tox/issues/791#issuecomment-518713438 +download = true deps = cov: coverage>=4.3 pytest @@ -33,6 +38,13 @@ coverage combine coverage html +[testenv:mypy] +deps = + -r dev-requirements.txt +skip_install = true +commands = + mypy + [testenv:codecov] passenv = * deps = diff -Nru fonttools-4.13.0/.travis/install.sh fonttools-4.16.1/.travis/install.sh --- fonttools-4.13.0/.travis/install.sh 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/.travis/install.sh 2020-10-05 18:25:14.000000000 +0000 @@ -27,4 +27,4 @@ source .venv/bin/activate fi -python -m pip install $ci_requirements +python -m pip install --upgrade $ci_requirements diff -Nru fonttools-4.13.0/.travis.yml fonttools-4.16.1/.travis.yml --- fonttools-4.13.0/.travis.yml 2020-07-10 11:47:59.000000000 +0000 +++ fonttools-4.16.1/.travis.yml 2020-10-05 18:25:14.000000000 +0000 @@ -20,6 +20,9 @@ include: - python: 3.6 env: + - TOXENV=mypy + - python: 3.6 + env: - TOXENV=py36-cov,package_readme - BUILD_DIST=true - python: 3.7