diff options
author | robot-contrib <robot-contrib@yandex-team.com> | 2023-11-18 09:39:00 +0300 |
---|---|---|
committer | robot-contrib <robot-contrib@yandex-team.com> | 2023-11-18 09:58:55 +0300 |
commit | 02af688a175993fc8169edf79bef767977b91764 (patch) | |
tree | a08abcf2e33fea2290da638dab525819d9eedd40 /contrib | |
parent | 45d2a1a9ad08d7bb7ecb28ea9fae9f59c67768b7 (diff) | |
download | ydb-02af688a175993fc8169edf79bef767977b91764.tar.gz |
Update contrib/python/fonttools to 4.44.0
Diffstat (limited to 'contrib')
68 files changed, 506 insertions, 364 deletions
diff --git a/contrib/python/fonttools/.dist-info/METADATA b/contrib/python/fonttools/.dist-info/METADATA index b6ed83e9b0..996d69838a 100644 --- a/contrib/python/fonttools/.dist-info/METADATA +++ b/contrib/python/fonttools/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: fonttools -Version: 4.43.1 +Version: 4.44.0 Summary: Tools to manipulate font files Home-page: http://github.com/fonttools/fonttools Author: Just van Rossum @@ -42,7 +42,7 @@ Requires-Dist: brotlicffi >=0.8.0 ; (platform_python_implementation != "CPython" Requires-Dist: scipy ; (platform_python_implementation != "PyPy") and extra == 'all' Requires-Dist: brotli >=1.0.1 ; (platform_python_implementation == "CPython") and extra == 'all' Requires-Dist: munkres ; (platform_python_implementation == "PyPy") and extra == 'all' -Requires-Dist: unicodedata2 >=15.0.0 ; (python_version <= "3.11") and extra == 'all' +Requires-Dist: unicodedata2 >=15.1.0 ; (python_version <= "3.12") and extra == 'all' Requires-Dist: xattr ; (sys_platform == "darwin") and extra == 'all' Provides-Extra: graphite Requires-Dist: lz4 >=1.7.4.2 ; extra == 'graphite' @@ -64,7 +64,7 @@ Requires-Dist: xattr ; (sys_platform == "darwin") and extra == 'type1' Provides-Extra: ufo Requires-Dist: fs <3,>=2.2.0 ; extra == 'ufo' Provides-Extra: unicode -Requires-Dist: unicodedata2 >=15.0.0 ; (python_version <= "3.11") and extra == 'unicode' +Requires-Dist: unicodedata2 >=15.1.0 ; (python_version <= "3.12") and extra == 'unicode' Provides-Extra: woff Requires-Dist: zopfli >=0.1.4 ; extra == 'woff' Requires-Dist: brotlicffi >=0.8.0 ; (platform_python_implementation != "CPython") and extra == 'woff' @@ -366,6 +366,27 @@ Have fun! Changelog ~~~~~~~~~ +4.44.0 (released 2023-11-03) +---------------------------- + +- [instancer] Recalc OS/2 AvgCharWidth after instancing if default changes (#3317). +- [otlLib] Make ClassDefBuilder class order match varLib.merger's, i.e. large + classes first, then glyph lexicographic order (#3321, #3324). +- [instancer] Allow not specifying any of min:default:max values and let be filled + up with fvar's values (#3322, #3323). +- [instancer] When running --update-name-table ignore axes that have no STAT axis + values (#3318, #3319). +- [Debg] When dumping to ttx, write the embedded JSON as multi-line string with + indentation (92cbfee0d). +- [varStore] Handle > 65535 items per encoding by splitting VarData subtable (#3310). +- [subset] Handle null-offsets in MarkLigPos subtables. +- [subset] Keep East Asian spacing fatures vhal, halt, chws, vchw by default (#3305). +- [instancer.solver] Fixed case where axisDef < lower and upper < axisMax (#3304). +- [glyf] Speed up compilation, mostly around ``recalcBounds`` (#3301). +- [varLib.interpolatable] Speed it up when working on variable fonts, plus various + micro-optimizations (#3300). +- Require unicodedata2 >= 15.1.0 when installed with 'unicode' extra, contains UCD 15.1. + 4.43.1 (released 2023-10-06) ---------------------------- diff --git a/contrib/python/fonttools/fontTools/__init__.py b/contrib/python/fonttools/fontTools/__init__.py index 765000c5dc..9a59504e44 100644 --- a/contrib/python/fonttools/fontTools/__init__.py +++ b/contrib/python/fonttools/fontTools/__init__.py @@ -3,6 +3,6 @@ from fontTools.misc.loggingTools import configLogger log = logging.getLogger(__name__) -version = __version__ = "4.43.1" +version = __version__ = "4.44.0" __all__ = ["version", "log", "configLogger"] diff --git a/contrib/python/fonttools/fontTools/afmLib.py b/contrib/python/fonttools/fontTools/afmLib.py index 394b901ff5..935a1e8e0d 100644 --- a/contrib/python/fonttools/fontTools/afmLib.py +++ b/contrib/python/fonttools/fontTools/afmLib.py @@ -119,7 +119,6 @@ class error(Exception): class AFM(object): - _attrs = None _keywords = [ diff --git a/contrib/python/fonttools/fontTools/cffLib/__init__.py b/contrib/python/fonttools/fontTools/cffLib/__init__.py index b5b859fc50..644508c155 100644 --- a/contrib/python/fonttools/fontTools/cffLib/__init__.py +++ b/contrib/python/fonttools/fontTools/cffLib/__init__.py @@ -1000,7 +1000,6 @@ class TopDictIndex(Index): class FDArrayIndex(Index): - compilerClass = FDArrayIndexCompiler def toXML(self, xmlWriter): @@ -2483,7 +2482,6 @@ def encodeNumber(num): class TopDictCompiler(DictCompiler): - opcodes = buildOpcodeDict(topDictOperators) def getChildren(self, strings): @@ -2590,7 +2588,6 @@ class FontDictCompiler(DictCompiler): class PrivateDictCompiler(DictCompiler): - maxBlendStack = maxStackLimit opcodes = buildOpcodeDict(privateDictOperators) diff --git a/contrib/python/fonttools/fontTools/cffLib/specializer.py b/contrib/python/fonttools/fontTools/cffLib/specializer.py index 3d28c82dc7..efc15af704 100644 --- a/contrib/python/fonttools/fontTools/cffLib/specializer.py +++ b/contrib/python/fonttools/fontTools/cffLib/specializer.py @@ -41,7 +41,7 @@ def programToCommands(program, getNumRegions=None): Each command is a two-tuple of commandname,arg-list. The commandname might be empty string if no commandname shall be emitted (used for glyph width, hintmask/cntrmask argument, as well as stray arguments at the end of the - program (¯\_(ツ)_/¯). + program (🤷). 'getNumRegions' may be None, or a callable object. It must return the number of regions. 'getNumRegions' takes a single argument, vsindex. If the vsindex argument is None, getNumRegions returns the default number @@ -513,7 +513,6 @@ def specializeCommands( preserveTopology=False, maxstack=48, ): - # We perform several rounds of optimizations. They are carefully ordered and are: # # 0. Generalize commands. diff --git a/contrib/python/fonttools/fontTools/cffLib/width.py b/contrib/python/fonttools/fontTools/cffLib/width.py index c0a746b692..0ba3ed39bf 100644 --- a/contrib/python/fonttools/fontTools/cffLib/width.py +++ b/contrib/python/fonttools/fontTools/cffLib/width.py @@ -22,7 +22,6 @@ class missingdict(dict): def cumSum(f, op=add, start=0, decreasing=False): - keys = sorted(f.keys()) minx, maxx = keys[0], keys[-1] @@ -46,7 +45,6 @@ def cumSum(f, op=add, start=0, decreasing=False): def byteCost(widths, default, nominal): - if not hasattr(widths, "items"): d = defaultdict(int) for w in widths: diff --git a/contrib/python/fonttools/fontTools/feaLib/parser.py b/contrib/python/fonttools/fontTools/feaLib/parser.py index 49667f4503..8ffdf644c3 100644 --- a/contrib/python/fonttools/fontTools/feaLib/parser.py +++ b/contrib/python/fonttools/fontTools/feaLib/parser.py @@ -45,7 +45,6 @@ class Parser(object): def __init__( self, featurefile, glyphNames=(), followIncludes=True, includeDir=None, **kwargs ): - if "glyphMap" in kwargs: from fontTools.misc.loggingTools import deprecateArgument @@ -1754,7 +1753,8 @@ class Parser(object): def parse_featureNames_(self, tag): """Parses a ``featureNames`` statement found in stylistic set features. - See section `8.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.c>`_.""" + See section `8.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.c>`_. + """ assert self.cur_token_ == "featureNames", self.cur_token_ block = self.ast.NestedBlock( tag, self.cur_token_, location=self.cur_token_location_ diff --git a/contrib/python/fonttools/fontTools/merge/__init__.py b/contrib/python/fonttools/fontTools/merge/__init__.py index 10eff133fa..8d8a5213e8 100644 --- a/contrib/python/fonttools/fontTools/merge/__init__.py +++ b/contrib/python/fonttools/fontTools/merge/__init__.py @@ -51,7 +51,6 @@ class Merger(object): """ def __init__(self, options=None): - if not options: options = Options() diff --git a/contrib/python/fonttools/fontTools/merge/options.py b/contrib/python/fonttools/fontTools/merge/options.py index 0c4cfb9988..f134009368 100644 --- a/contrib/python/fonttools/fontTools/merge/options.py +++ b/contrib/python/fonttools/fontTools/merge/options.py @@ -8,7 +8,6 @@ class Options(object): pass def __init__(self, **kwargs): - self.verbose = False self.timing = False self.drop_tables = [] diff --git a/contrib/python/fonttools/fontTools/merge/tables.py b/contrib/python/fonttools/fontTools/merge/tables.py index 394541b8a4..57ed64d337 100644 --- a/contrib/python/fonttools/fontTools/merge/tables.py +++ b/contrib/python/fonttools/fontTools/merge/tables.py @@ -210,6 +210,7 @@ ttLib.getTableClass("glyf").mergeMap = { "tableTag": equal, "glyphs": sumDicts, "glyphOrder": sumLists, + "_reverseGlyphOrder": recalculate, "axisTags": equal, } diff --git a/contrib/python/fonttools/fontTools/misc/arrayTools.py b/contrib/python/fonttools/fontTools/misc/arrayTools.py index 5fb01a838a..ced8d87a61 100644 --- a/contrib/python/fonttools/fontTools/misc/arrayTools.py +++ b/contrib/python/fonttools/fontTools/misc/arrayTools.py @@ -47,7 +47,7 @@ def updateBounds(bounds, p, min=min, max=max): Args: bounds: A bounding rectangle expressed as a tuple - ``(xMin, yMin, xMax, yMax)``. + ``(xMin, yMin, xMax, yMax), or None``. p: A 2D tuple representing a point. min,max: functions to compute the minimum and maximum. @@ -55,6 +55,8 @@ def updateBounds(bounds, p, min=min, max=max): The updated bounding rectangle ``(xMin, yMin, xMax, yMax)``. """ (x, y) = p + if bounds is None: + return x, y, x, y xMin, yMin, xMax, yMax = bounds return min(xMin, x), min(yMin, y), max(xMax, x), max(yMax, y) diff --git a/contrib/python/fonttools/fontTools/misc/classifyTools.py b/contrib/python/fonttools/fontTools/misc/classifyTools.py index e46386230e..2235bbd7f8 100644 --- a/contrib/python/fonttools/fontTools/misc/classifyTools.py +++ b/contrib/python/fonttools/fontTools/misc/classifyTools.py @@ -9,7 +9,6 @@ class Classifier(object): """ def __init__(self, sort=True): - self._things = set() # set of all things known so far self._sets = [] # list of class sets produced so far self._mapping = {} # map from things to their class set diff --git a/contrib/python/fonttools/fontTools/misc/dictTools.py b/contrib/python/fonttools/fontTools/misc/dictTools.py index 259613b270..e3c0df7355 100644 --- a/contrib/python/fonttools/fontTools/misc/dictTools.py +++ b/contrib/python/fonttools/fontTools/misc/dictTools.py @@ -3,6 +3,7 @@ __all__ = ["hashdict"] + # https://stackoverflow.com/questions/1151658/python-hashable-dicts class hashdict(dict): """ diff --git a/contrib/python/fonttools/fontTools/misc/psCharStrings.py b/contrib/python/fonttools/fontTools/misc/psCharStrings.py index 0092a98ece..cc9ca01c7f 100644 --- a/contrib/python/fonttools/fontTools/misc/psCharStrings.py +++ b/contrib/python/fonttools/fontTools/misc/psCharStrings.py @@ -1092,7 +1092,6 @@ class T1OutlineExtractor(T2OutlineExtractor): class T2CharString(object): - operandEncoding = t2OperandEncoding operators, opcodes = buildOperatorDict(t2Operators) decompilerClass = SimpleT2Decompiler @@ -1313,7 +1312,6 @@ class T2CharString(object): class T1CharString(T2CharString): - operandEncoding = t1OperandEncoding operators, opcodes = buildOperatorDict(t1Operators) @@ -1347,7 +1345,6 @@ class T1CharString(T2CharString): class DictDecompiler(object): - operandEncoding = cffDictOperandEncoding def __init__(self, strings, parent=None): diff --git a/contrib/python/fonttools/fontTools/misc/psLib.py b/contrib/python/fonttools/fontTools/misc/psLib.py index 1e0408ce9c..3bfdb4ae9f 100644 --- a/contrib/python/fonttools/fontTools/misc/psLib.py +++ b/contrib/python/fonttools/fontTools/misc/psLib.py @@ -100,7 +100,6 @@ class PSTokenizer(object): commentmatch=commentRE.match, endmatch=endofthingRE.match, ): - self.skipwhite() if self.pos >= self.len: return None, None diff --git a/contrib/python/fonttools/fontTools/misc/psOperators.py b/contrib/python/fonttools/fontTools/misc/psOperators.py index d0ef432f52..d0b975eab1 100644 --- a/contrib/python/fonttools/fontTools/misc/psOperators.py +++ b/contrib/python/fonttools/fontTools/misc/psOperators.py @@ -2,7 +2,6 @@ _accessstrings = {0: "", 1: "readonly", 2: "executeonly", 3: "noaccess"} class ps_object(object): - literal = 1 access = 0 value = None @@ -16,7 +15,6 @@ class ps_object(object): class ps_operator(ps_object): - literal = 0 def __init__(self, name, function): diff --git a/contrib/python/fonttools/fontTools/misc/roundTools.py b/contrib/python/fonttools/fontTools/misc/roundTools.py index 48a47c07c8..a4d45c31b2 100644 --- a/contrib/python/fonttools/fontTools/misc/roundTools.py +++ b/contrib/python/fonttools/fontTools/misc/roundTools.py @@ -13,6 +13,7 @@ __all__ = [ "otRound", "maybeRound", "roundFunc", + "nearestMultipleShortestRepr", ] diff --git a/contrib/python/fonttools/fontTools/misc/symfont.py b/contrib/python/fonttools/fontTools/misc/symfont.py index 0bd69a386e..fb9e20a46e 100644 --- a/contrib/python/fonttools/fontTools/misc/symfont.py +++ b/contrib/python/fonttools/fontTools/misc/symfont.py @@ -61,7 +61,6 @@ class _BezierFuncsLazy(dict): class GreenPen(BasePen): - _BezierFuncs = {} @classmethod @@ -115,7 +114,6 @@ MomentXYPen = partial(GreenPen, func=x * y) def printGreenPen(penName, funcs, file=sys.stdout, docstring=None): - if docstring is not None: print('"""%s"""' % docstring) @@ -167,7 +165,6 @@ class %s(BasePen): ) for n in (1, 2, 3): - subs = {P[i][j]: [X, Y][j][i] for i in range(n + 1) for j in range(2)} greens = [green(f, BezierCurve[n]) for name, f in funcs] greens = [sp.gcd_terms(f.collect(sum(P, ()))) for f in greens] # Optimize diff --git a/contrib/python/fonttools/fontTools/misc/visitor.py b/contrib/python/fonttools/fontTools/misc/visitor.py index 3d28135fad..d289895467 100644 --- a/contrib/python/fonttools/fontTools/misc/visitor.py +++ b/contrib/python/fonttools/fontTools/misc/visitor.py @@ -4,7 +4,6 @@ import enum class Visitor(object): - defaultStop = False @classmethod @@ -58,7 +57,6 @@ class Visitor(object): typ = type(thing) for celf in celf.mro(): - _visitors = getattr(celf, "_visitors", None) if _visitors is None: break diff --git a/contrib/python/fonttools/fontTools/otlLib/builder.py b/contrib/python/fonttools/fontTools/otlLib/builder.py index 94628bff11..3508a7e28d 100644 --- a/contrib/python/fonttools/fontTools/otlLib/builder.py +++ b/contrib/python/fonttools/fontTools/otlLib/builder.py @@ -2674,7 +2674,7 @@ class ClassDefBuilder(object): # class form a contiguous range, the encoding is actually quite # compact, whereas a non-contiguous set might need a lot of bytes # in the output file. We don't get this right with the key below. - result = sorted(self.classes_, key=lambda s: (len(s), s), reverse=True) + result = sorted(self.classes_, key=lambda s: (-len(s), s)) if not self.useClass0_: result.insert(0, frozenset()) return result diff --git a/contrib/python/fonttools/fontTools/otlLib/optimize/gpos.py b/contrib/python/fonttools/fontTools/otlLib/optimize/gpos.py index 0acd9ed04c..01c2257cd7 100644 --- a/contrib/python/fonttools/fontTools/otlLib/optimize/gpos.py +++ b/contrib/python/fonttools/fontTools/otlLib/optimize/gpos.py @@ -135,6 +135,7 @@ Pairs = Dict[ Tuple[otBase.ValueRecord, otBase.ValueRecord], ] + # Adapted from https://github.com/fonttools/fonttools/blob/f64f0b42f2d1163b2d85194e0979def539f5dca3/Lib/fontTools/ttLib/tables/otTables.py#L935-L958 def _getClassRanges(glyphIDs: Iterable[int]): glyphIDs = sorted(glyphIDs) @@ -274,7 +275,7 @@ class Cluster: ) merged_range_count = 0 last = None - for (start, end) in ranges: + for start, end in ranges: if last is not None and start != last + 1: merged_range_count += 1 last = end diff --git a/contrib/python/fonttools/fontTools/pens/cu2quPen.py b/contrib/python/fonttools/fontTools/pens/cu2quPen.py index f182aed44a..5730b325cf 100644 --- a/contrib/python/fonttools/fontTools/pens/cu2quPen.py +++ b/contrib/python/fonttools/fontTools/pens/cu2quPen.py @@ -199,7 +199,7 @@ class Cu2QuPointPen(BasePointToSegmentPen): # will be appended at the end of the contour last_offcurves = offcurves else: - for (pt, smooth, name, kwargs) in offcurves: + for pt, smooth, name, kwargs in offcurves: pen.addPoint(pt, None, smooth, name, **kwargs) pt, smooth, name, kwargs = points[-1] if pt is None: @@ -212,7 +212,7 @@ class Cu2QuPointPen(BasePointToSegmentPen): pen.addPoint(pt, segment_type, smooth, name, **kwargs) else: raise AssertionError("unexpected segment type: %r" % segment_type) - for (pt, smooth, name, kwargs) in last_offcurves: + for pt, smooth, name, kwargs in last_offcurves: pen.addPoint(pt, None, smooth, name, **kwargs) pen.endPath() diff --git a/contrib/python/fonttools/fontTools/pens/statisticsPen.py b/contrib/python/fonttools/fontTools/pens/statisticsPen.py index 8e00aa44da..39f319e02d 100644 --- a/contrib/python/fonttools/fontTools/pens/statisticsPen.py +++ b/contrib/python/fonttools/fontTools/pens/statisticsPen.py @@ -85,11 +85,13 @@ def _test(glyphset, upem, glyphs, quiet=False): transformer = TransformPen(pen, Scale(1.0 / upem)) glyph.draw(transformer) - wght_sum += abs(pen.area) - wght_sum_perceptual += abs(pen.area) * glyph.width - wdth_sum += glyph.width + area = abs(pen.area) + width = glyph.width + wght_sum += area + wght_sum_perceptual += pen.area * width + wdth_sum += width slnt_sum += pen.slant - slnt_sum_perceptual += pen.slant * glyph.width + slnt_sum_perceptual += pen.slant * width if quiet: continue diff --git a/contrib/python/fonttools/fontTools/subset/__init__.py b/contrib/python/fonttools/fontTools/subset/__init__.py index 4b9cb00f60..bd826ed224 100644 --- a/contrib/python/fonttools/fontTools/subset/__init__.py +++ b/contrib/python/fonttools/fontTools/subset/__init__.py @@ -870,6 +870,8 @@ def subset_glyphs(self, s): for m in self.MarkArray.MarkRecord: m.Class = class_indices.index(m.Class) for l in self.LigatureArray.LigatureAttach: + if l is None: + continue for c in l.ComponentRecord: c.LigatureAnchor = _list_subset(c.LigatureAnchor, class_indices) return bool( @@ -888,6 +890,8 @@ def prune_post_subset(self, font, options): if m.MarkAnchor: m.MarkAnchor.prune_hints() for l in self.LigatureArray.LigatureAttach: + if l is None: + continue for c in l.ComponentRecord: for a in c.LigatureAnchor: if a: @@ -3004,6 +3008,7 @@ class Options(object): "rand": ["rand"], "justify": ["jalt"], "private": ["Harf", "HARF", "Buzz", "BUZZ"], + "east_asian_spacing": ["chws", "vchw", "halt", "vhal"], # Complex shapers "arabic": [ "init", @@ -3666,7 +3671,10 @@ def main(args=None): ) if outfile is None: - outfile = makeOutputFileName(fontfile, overWrite=True, suffix=".subset") + ext = "." + options.flavor.lower() if options.flavor is not None else None + outfile = makeOutputFileName( + fontfile, extension=ext, overWrite=True, suffix=".subset" + ) with timer("compile glyph list"): if wildcard_glyphs: diff --git a/contrib/python/fonttools/fontTools/subset/svg.py b/contrib/python/fonttools/fontTools/subset/svg.py index 9daac6470c..329c68fb0a 100644 --- a/contrib/python/fonttools/fontTools/subset/svg.py +++ b/contrib/python/fonttools/fontTools/subset/svg.py @@ -204,7 +204,6 @@ def subset_glyphs(self, s) -> bool: new_docs: List[SVGDocument] = [] for doc in self.docList: - glyphs = { glyph_order[i] for i in range(doc.startGlyphID, doc.endGlyphID + 1) }.intersection(s.glyphs) diff --git a/contrib/python/fonttools/fontTools/svgLib/path/parser.py b/contrib/python/fonttools/fontTools/svgLib/path/parser.py index 70ae4c17ea..fa53474558 100644 --- a/contrib/python/fonttools/fontTools/svgLib/path/parser.py +++ b/contrib/python/fonttools/fontTools/svgLib/path/parser.py @@ -125,7 +125,6 @@ def parse_path(pathdef, pen, current_pos=(0, 0), arc_class=EllipticalArc): have_arcTo = hasattr(pen, "arcTo") while elements: - if elements[-1] in COMMANDS: # New command. last_command = command # Used by S and T diff --git a/contrib/python/fonttools/fontTools/ttLib/__main__.py b/contrib/python/fonttools/fontTools/ttLib/__main__.py index d9b2a465d7..2733444d8b 100644 --- a/contrib/python/fonttools/fontTools/ttLib/__main__.py +++ b/contrib/python/fonttools/fontTools/ttLib/__main__.py @@ -51,6 +51,9 @@ def main(args=None): ) parser.add_argument("font", metavar="font", nargs="*", help="Font file.") parser.add_argument( + "-t", "--table", metavar="table", nargs="*", help="Tables to decompile." + ) + parser.add_argument( "-o", "--output", metavar="FILE", default=None, help="Output file." ) parser.add_argument( @@ -74,6 +77,7 @@ def main(args=None): outFile = options.output lazy = options.lazy flavor = options.flavor + tables = options.table if options.table is not None else [] fonts = [] for f in options.font: @@ -84,6 +88,10 @@ def main(args=None): collection = TTCollection(f, lazy=lazy) fonts.extend(collection.fonts) + for font in fonts: + for table in tables if "*" not in tables else font.keys(): + font[table] # Decompiles + if outFile is not None: if len(fonts) == 1: fonts[0].flavor = flavor diff --git a/contrib/python/fonttools/fontTools/ttLib/scaleUpem.py b/contrib/python/fonttools/fontTools/ttLib/scaleUpem.py index 7018f27a7c..3f9b22af8f 100644 --- a/contrib/python/fonttools/fontTools/ttLib/scaleUpem.py +++ b/contrib/python/fonttools/fontTools/ttLib/scaleUpem.py @@ -139,7 +139,6 @@ def visit(visitor, obj, attr, glyphs): @ScalerVisitor.register_attr(ttLib.getTableClass("gvar"), "variations") def visit(visitor, obj, attr, variations): - # VarComposites are a pain to handle :-( glyfTable = visitor.font["glyf"] diff --git a/contrib/python/fonttools/fontTools/ttLib/sfnt.py b/contrib/python/fonttools/fontTools/ttLib/sfnt.py index 354fb85ea2..b1569423c4 100644 --- a/contrib/python/fonttools/fontTools/ttLib/sfnt.py +++ b/contrib/python/fonttools/fontTools/ttLib/sfnt.py @@ -524,13 +524,11 @@ class DirectoryEntry(object): class SFNTDirectoryEntry(DirectoryEntry): - format = sfntDirectoryEntryFormat formatSize = sfntDirectoryEntrySize class WOFFDirectoryEntry(DirectoryEntry): - format = woffDirectoryEntryFormat formatSize = woffDirectoryEntrySize @@ -571,7 +569,6 @@ class WOFFDirectoryEntry(DirectoryEntry): class WOFFFlavorData: - Flavor = "woff" def __init__(self, reader=None): diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/C_B_D_T_.py b/contrib/python/fonttools/fontTools/ttLib/tables/C_B_D_T_.py index e9e2d5fde9..2b87ac8628 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/C_B_D_T_.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/C_B_D_T_.py @@ -21,7 +21,6 @@ import struct class table_C_B_D_T_(E_B_D_T_.table_E_B_D_T_): - # Change the data locator table being referenced. locatorName = "CBLC" @@ -42,7 +41,6 @@ def _removeUnsupportedForColor(dataFunctions): class ColorBitmapGlyph(BitmapGlyph): - fileExtension = ".png" xmlDataFunctions = _removeUnsupportedForColor(BitmapGlyph.xmlDataFunctions) diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/C_B_L_C_.py b/contrib/python/fonttools/fontTools/ttLib/tables/C_B_L_C_.py index e9ed58e582..fc3974ece0 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/C_B_L_C_.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/C_B_L_C_.py @@ -6,5 +6,4 @@ from . import E_B_L_C_ class table_C_B_L_C_(E_B_L_C_.table_E_B_L_C_): - dependencies = ["CBDT"] diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/C_O_L_R_.py b/contrib/python/fonttools/fontTools/ttLib/tables/C_O_L_R_.py index b4bc5d0c20..2f03ec054f 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/C_O_L_R_.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/C_O_L_R_.py @@ -151,7 +151,7 @@ class LayerRecord(object): writer.newline() def fromXML(self, eltname, attrs, content, ttFont): - for (name, value) in attrs.items(): + for name, value in attrs.items(): if name == "name": setattr(self, name, value) else: diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/C_P_A_L_.py b/contrib/python/fonttools/fontTools/ttLib/tables/C_P_A_L_.py index 03eb851e8c..9fb2074afc 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/C_P_A_L_.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/C_P_A_L_.py @@ -11,7 +11,6 @@ import sys class table_C_P_A_L_(DefaultTable.DefaultTable): - NO_NAME_ID = 0xFFFF DEFAULT_PALETTE_TYPE = 0 diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/D__e_b_g.py b/contrib/python/fonttools/fontTools/ttLib/tables/D__e_b_g.py index ff64a9b519..54449a5fd6 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/D__e_b_g.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/D__e_b_g.py @@ -11,7 +11,7 @@ class table_D__e_b_g(DefaultTable.DefaultTable): return json.dumps(self.data).encode("utf-8") def toXML(self, writer, ttFont): - writer.writecdata(json.dumps(self.data)) + writer.writecdata(json.dumps(self.data, indent=2)) def fromXML(self, name, attrs, content, ttFont): self.data = json.loads(content) diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/DefaultTable.py b/contrib/python/fonttools/fontTools/ttLib/tables/DefaultTable.py index 32a4b1f258..92f2aa6523 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/DefaultTable.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/DefaultTable.py @@ -3,7 +3,6 @@ from fontTools.ttLib import getClassTag class DefaultTable(object): - dependencies = [] def __init__(self, tag=None): diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/E_B_D_T_.py b/contrib/python/fonttools/fontTools/ttLib/tables/E_B_D_T_.py index 535f98fd80..9f7f82efd5 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/E_B_D_T_.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/E_B_D_T_.py @@ -38,7 +38,6 @@ ebdtComponentFormat = """ class table_E_B_D_T_(DefaultTable.DefaultTable): - # Keep a reference to the name of the data locator table. locatorName = "EBLC" @@ -84,7 +83,6 @@ class table_E_B_D_T_(DefaultTable.DefaultTable): bitmapGlyphDict[curName] = curGlyph def compile(self, ttFont): - dataList = [] dataList.append(sstruct.pack(ebdtTableVersionFormat, self)) dataSize = len(dataList[0]) @@ -428,7 +426,6 @@ _bitmapGlyphSubclassPrefix = "ebdt_bitmap_format_" class BitmapGlyph(object): - # For the external file format. This can be changed in subclasses. This way # when the extfile option is turned on files have the form: glyphName.ext # The default is just a flat binary file with no meaning. @@ -555,6 +552,7 @@ def _createBitmapPlusMetricsMixin(metricsClass): BitmapPlusBigMetricsMixin = _createBitmapPlusMetricsMixin(BigGlyphMetrics) BitmapPlusSmallMetricsMixin = _createBitmapPlusMetricsMixin(SmallGlyphMetrics) + # Data that is bit aligned can be tricky to deal with. These classes implement # helper functionality for dealing with the data and getting a particular row # of bitwise data. Also helps implement fancy data export/import in XML. diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/E_B_L_C_.py b/contrib/python/fonttools/fontTools/ttLib/tables/E_B_L_C_.py index 9cc60ff82d..6046d9100b 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/E_B_L_C_.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/E_B_L_C_.py @@ -66,7 +66,6 @@ codeOffsetPairSize = struct.calcsize(codeOffsetPairFormat) class table_E_B_L_C_(DefaultTable.DefaultTable): - dependencies = ["EBDT"] # This method can be overridden in subclasses to support new formats @@ -76,7 +75,6 @@ class table_E_B_L_C_(DefaultTable.DefaultTable): return eblc_sub_table_classes[indexFormat] def decompile(self, data, ttFont): - # Save the original data because offsets are from the start of the table. origData = data i = 0 @@ -138,7 +136,6 @@ class table_E_B_L_C_(DefaultTable.DefaultTable): curStrike.indexSubTables.append(indexSubTable) def compile(self, ttFont): - dataList = [] self.numSizes = len(self.strikes) dataList.append(sstruct.pack(eblcHeaderFormat, self)) @@ -297,7 +294,6 @@ class Strike(object): class BitmapSizeTable(object): - # Returns all the simple metric names that bitmap size table # cares about in terms of XML creation. def _getXMLMetricNames(self): @@ -476,14 +472,12 @@ class EblcIndexSubTable(object): # are very similar. The only difference between them is the size per offset # value. Code put in here should handle both cases generally. def _createOffsetArrayIndexSubTableMixin(formatStringForDataType): - # Prep the data size for the offset array data format. dataFormat = ">" + formatStringForDataType offsetDataSize = struct.calcsize(dataFormat) class OffsetArrayIndexSubTableMixin(object): def decompile(self): - numGlyphs = self.lastGlyphIndex - self.firstGlyphIndex + 1 indexingOffsets = [ glyphIndex * offsetDataSize for glyphIndex in range(numGlyphs + 2) @@ -625,7 +619,6 @@ class eblc_index_sub_table_3( class eblc_index_sub_table_4(EblcIndexSubTable): def decompile(self): - (numGlyphs,) = struct.unpack(">L", self.data[:4]) data = self.data[4:] indexingOffsets = [ diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/G_M_A_P_.py b/contrib/python/fonttools/fontTools/ttLib/tables/G_M_A_P_.py index 39b0050c5f..949ef842ef 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/G_M_A_P_.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/G_M_A_P_.py @@ -81,7 +81,6 @@ class GMAPRecord(object): class table_G_M_A_P_(DefaultTable.DefaultTable): - dependencies = [] def decompile(self, data, ttFont): diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/M_E_T_A_.py b/contrib/python/fonttools/fontTools/ttLib/tables/M_E_T_A_.py index 6631e2f30c..445aeb4dea 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/M_E_T_A_.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/M_E_T_A_.py @@ -68,7 +68,6 @@ def getLabelString(labelID): class table_M_E_T_A_(DefaultTable.DefaultTable): - dependencies = [] def decompile(self, data, ttFont): diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/S_I_N_G_.py b/contrib/python/fonttools/fontTools/ttLib/tables/S_I_N_G_.py index 7420da7e5d..4522c06c6b 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/S_I_N_G_.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/S_I_N_G_.py @@ -20,7 +20,6 @@ SINGFormat = """ class table_S_I_N_G_(DefaultTable.DefaultTable): - dependencies = [] def decompile(self, data, ttFont): diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__0.py b/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__0.py index 4112937d45..f15fc67bce 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__0.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__0.py @@ -16,7 +16,6 @@ def fixlongs(glyphID, textLength, textOffset): class table_T_S_I__0(DefaultTable.DefaultTable): - dependencies = ["TSI1"] def decompile(self, data, ttFont): diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__1.py b/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__1.py index 57163d726c..55aca33991 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__1.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__1.py @@ -10,7 +10,6 @@ from fontTools.misc.textTools import strjoin, tobytes, tostr class table_T_S_I__1(LogMixin, DefaultTable.DefaultTable): - extras = {0xFFFA: "ppgm", 0xFFFB: "cvt", 0xFFFC: "reserved", 0xFFFD: "fpgm"} indextable = "TSI0" diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__2.py b/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__2.py index 43a17f6f1f..4278be1556 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__2.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__2.py @@ -11,5 +11,4 @@ superclass = ttLib.getTableClass("TSI0") class table_T_S_I__2(superclass): - dependencies = ["TSI3"] diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__3.py b/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__3.py index 8ef3c5ade2..785ca23152 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__3.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__3.py @@ -9,7 +9,6 @@ superclass = ttLib.getTableClass("TSI1") class table_T_S_I__3(superclass): - extras = { 0xFFFA: "reserved0", 0xFFFB: "reserved1", diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py b/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py index 13ff867874..30d009906d 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py @@ -166,15 +166,15 @@ class TupleVariation(object): return b"".join(tupleData), auxData def compileCoord(self, axisTags): - result = bytearray() + result = [] axes = self.axes for axis in axisTags: triple = axes.get(axis) if triple is None: - result.extend(b"\0\0") + result.append(b"\0\0") else: - result.extend(struct.pack(">h", fl2fi(triple[1], 14))) - return bytes(result) + result.append(struct.pack(">h", fl2fi(triple[1], 14))) + return b"".join(result) def compileIntermediateCoord(self, axisTags): needed = False @@ -187,13 +187,13 @@ class TupleVariation(object): break if not needed: return None - minCoords = bytearray() - maxCoords = bytearray() + minCoords = [] + maxCoords = [] for axis in axisTags: minValue, value, maxValue = self.axes.get(axis, (0.0, 0.0, 0.0)) - minCoords.extend(struct.pack(">h", fl2fi(minValue, 14))) - maxCoords.extend(struct.pack(">h", fl2fi(maxValue, 14))) - return minCoords + maxCoords + minCoords.append(struct.pack(">h", fl2fi(minValue, 14))) + maxCoords.append(struct.pack(">h", fl2fi(maxValue, 14))) + return b"".join(minCoords + maxCoords) @staticmethod def decompileCoord_(axisTags, data, offset): @@ -802,7 +802,7 @@ def inferRegion_(peak): intermediateEndTuple fields. """ start, end = {}, {} - for (axis, value) in peak.items(): + for axis, value in peak.items(): start[axis] = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0 end[axis] = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7 return (start, end) diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_c_m_a_p.py b/contrib/python/fonttools/fontTools/ttLib/tables/_c_m_a_p.py index 6c00aaf63d..484c331cb7 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_c_m_a_p.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_c_m_a_p.py @@ -1291,7 +1291,6 @@ class cmap_format_12_or_13(CmapSubtable): class cmap_format_12(cmap_format_12_or_13): - _format_step = 1 def __init__(self, format=12): @@ -1305,7 +1304,6 @@ class cmap_format_12(cmap_format_12_or_13): class cmap_format_13(cmap_format_12_or_13): - _format_step = 0 def __init__(self, format=13): diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py b/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py index 341c4ed646..bff0d92c32 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py @@ -6,7 +6,7 @@ from fontTools import ttLib from fontTools import version from fontTools.misc.transform import DecomposedTransform from fontTools.misc.textTools import tostr, safeEval, pad -from fontTools.misc.arrayTools import calcIntBounds, pointInRect +from fontTools.misc.arrayTools import updateBounds, pointInRect from fontTools.misc.bezierTools import calcQuadraticBounds from fontTools.misc.fixedTools import ( fixedToFloat as fi2fl, @@ -102,6 +102,7 @@ class table__g_l_y_f(DefaultTable.DefaultTable): noname = 0 self.glyphs = {} self.glyphOrder = glyphOrder = ttFont.getGlyphOrder() + self._reverseGlyphOrder = {} for i in range(0, len(loca) - 1): try: glyphName = glyphOrder[i] @@ -144,9 +145,10 @@ class table__g_l_y_f(DefaultTable.DefaultTable): currentLocation = 0 dataList = [] recalcBBoxes = ttFont.recalcBBoxes + boundsDone = set() for glyphName in self.glyphOrder: glyph = self.glyphs[glyphName] - glyphData = glyph.compile(self, recalcBBoxes) + glyphData = glyph.compile(self, recalcBBoxes, boundsDone=boundsDone) if padding > 1: glyphData = pad(glyphData, size=padding) locations.append(currentLocation) @@ -281,6 +283,7 @@ class table__g_l_y_f(DefaultTable.DefaultTable): glyphOrder ([str]): List of glyph names in order. """ self.glyphOrder = glyphOrder + self._reverseGlyphOrder = {} def getGlyphName(self, glyphID): """Returns the name for the glyph with the given ID. @@ -289,13 +292,24 @@ class table__g_l_y_f(DefaultTable.DefaultTable): """ return self.glyphOrder[glyphID] + def _buildReverseGlyphOrderDict(self): + self._reverseGlyphOrder = d = {} + for glyphID, glyphName in enumerate(self.glyphOrder): + d[glyphName] = glyphID + def getGlyphID(self, glyphName): """Returns the ID of the glyph with the given name. Raises a ``ValueError`` if the glyph is not found in the font. """ - # XXX optimize with reverse dict!!! - return self.glyphOrder.index(glyphName) + glyphOrder = self.glyphOrder + id = getattr(self, "_reverseGlyphOrder", {}).get(glyphName) + if id is None or id >= len(glyphOrder) or glyphOrder[id] != glyphName: + self._buildReverseGlyphOrderDict() + id = self._reverseGlyphOrder.get(glyphName) + if id is None: + raise ValueError(glyphName) + return id def removeHinting(self): """Removes TrueType hints from all glyphs in the glyphset. @@ -488,7 +502,7 @@ class table__g_l_y_f(DefaultTable.DefaultTable): assert len(coord) == len(glyph.coordinates) glyph.coordinates = GlyphCoordinates(coord) - glyph.recalcBounds(self) + glyph.recalcBounds(self, boundsDone=set()) horizontalAdvanceWidth = otRound(rightSideX - leftSideX) if horizontalAdvanceWidth < 0: @@ -728,7 +742,7 @@ class Glyph(object): else: self.decompileCoordinates(data) - def compile(self, glyfTable, recalcBBoxes=True): + def compile(self, glyfTable, recalcBBoxes=True, *, boundsDone=None): if hasattr(self, "data"): if recalcBBoxes: # must unpack glyph in order to recalculate bounding box @@ -737,8 +751,10 @@ class Glyph(object): return self.data if self.numberOfContours == 0: return b"" + if recalcBBoxes: - self.recalcBounds(glyfTable) + self.recalcBounds(glyfTable, boundsDone=boundsDone) + data = sstruct.pack(glyphHeaderFormat, self) if self.isComposite(): data = data + self.compileComponents(glyfTable) @@ -1148,7 +1164,7 @@ class Glyph(object): return (compressedFlags, compressedXs, compressedYs) - def recalcBounds(self, glyfTable): + def recalcBounds(self, glyfTable, *, boundsDone=None): """Recalculates the bounds of the glyph. Each glyph object stores its bounding box in the @@ -1156,12 +1172,55 @@ class Glyph(object): recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds must be provided to resolve component bounds. """ + if self.isComposite() and self.tryRecalcBoundsComposite( + glyfTable, boundsDone=boundsDone + ): + return try: coords, endPts, flags = self.getCoordinates(glyfTable) - self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(coords) + self.xMin, self.yMin, self.xMax, self.yMax = coords.calcIntBounds() except NotImplementedError: pass + def tryRecalcBoundsComposite(self, glyfTable, *, boundsDone=None): + """Try recalculating the bounds of a composite glyph that has + certain constrained properties. Namely, none of the components + have a transform other than an integer translate, and none + uses the anchor points. + + Each glyph object stores its bounding box in the + ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be + recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds + must be provided to resolve component bounds. + + Return True if bounds were calculated, False otherwise. + """ + for compo in self.components: + if hasattr(compo, "firstPt") or hasattr(compo, "transform"): + return False + if not float(compo.x).is_integer() or not float(compo.y).is_integer(): + return False + + # All components are untransformed and have an integer x/y translate + bounds = None + for compo in self.components: + glyphName = compo.glyphName + g = glyfTable[glyphName] + + if boundsDone is None or glyphName not in boundsDone: + g.recalcBounds(glyfTable, boundsDone=boundsDone) + if boundsDone is not None: + boundsDone.add(glyphName) + + x, y = compo.x, compo.y + bounds = updateBounds(bounds, (g.xMin + x, g.yMin + y)) + bounds = updateBounds(bounds, (g.xMax + x, g.yMax + y)) + + if bounds is None: + bounds = (0, 0, 0, 0) + self.xMin, self.yMin, self.xMax, self.yMax = bounds + return True + def isComposite(self): """Test whether a glyph has components""" if hasattr(self, "data"): @@ -2300,10 +2359,18 @@ class GlyphCoordinates(object): def __getitem__(self, k): """Returns a two element tuple (x,y)""" + a = self._a if isinstance(k, slice): indices = range(*k.indices(len(self))) - return [self[i] for i in indices] - a = self._a + # Instead of calling ourselves recursively, duplicate code; faster + ret = [] + for k in indices: + x = a[2 * k] + y = a[2 * k + 1] + ret.append( + (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y) + ) + return ret x = a[2 * k] y = a[2 * k + 1] return (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y) @@ -2341,6 +2408,17 @@ class GlyphCoordinates(object): for i in range(len(a)): a[i] = round(a[i]) + def calcBounds(self): + a = self._a + if not a: + return 0, 0, 0, 0 + xs = a[0::2] + ys = a[1::2] + return min(xs), min(ys), max(xs), max(ys) + + def calcIntBounds(self, round=otRound): + return tuple(round(v) for v in self.calcBounds()) + def relativeToAbsolute(self): a = self._a x, y = 0, 0 diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_h_e_a_d.py b/contrib/python/fonttools/fontTools/ttLib/tables/_h_e_a_d.py index 04505e8250..fe29c8fc4f 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_h_e_a_d.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_h_e_a_d.py @@ -37,7 +37,6 @@ headFormat = """ class table__h_e_a_d(DefaultTable.DefaultTable): - dependencies = ["maxp", "loca", "CFF ", "CFF2"] def decompile(self, data, ttFont): diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_h_h_e_a.py b/contrib/python/fonttools/fontTools/ttLib/tables/_h_h_e_a.py index 5500cf6a93..43e464f74f 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_h_h_e_a.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_h_h_e_a.py @@ -31,7 +31,6 @@ hheaFormat = """ class table__h_h_e_a(DefaultTable.DefaultTable): - # Note: Keep in sync with table__v_h_e_a dependencies = ["hmtx", "glyf", "CFF ", "CFF2"] diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_h_m_t_x.py b/contrib/python/fonttools/fontTools/ttLib/tables/_h_m_t_x.py index 2dafe617a0..2dbdd7f985 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_h_m_t_x.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_h_m_t_x.py @@ -12,7 +12,6 @@ log = logging.getLogger(__name__) class table__h_m_t_x(DefaultTable.DefaultTable): - headerTag = "hhea" advanceName = "width" sideBearingName = "lsb" diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_k_e_r_n.py b/contrib/python/fonttools/fontTools/ttLib/tables/_k_e_r_n.py index 94183c8a0a..8f55a311cd 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_k_e_r_n.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_k_e_r_n.py @@ -101,7 +101,6 @@ class table__k_e_r_n(DefaultTable.DefaultTable): class KernTable_format_0(object): - # 'version' is kept for backward compatibility version = format = 0 diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py b/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py index ad1b715133..5884cef45f 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py @@ -8,7 +8,6 @@ log = logging.getLogger(__name__) class table__l_o_c_a(DefaultTable.DefaultTable): - dependencies = ["glyf"] def decompile(self, data, ttFont): diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_m_a_x_p.py b/contrib/python/fonttools/fontTools/ttLib/tables/_m_a_x_p.py index 2934149773..f0e6c33ae3 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_m_a_x_p.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_m_a_x_p.py @@ -27,7 +27,6 @@ maxpFormat_1_0_add = """ class table__m_a_x_p(DefaultTable.DefaultTable): - dependencies = ["glyf"] def decompile(self, data, ttFont): diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_v_h_e_a.py b/contrib/python/fonttools/fontTools/ttLib/tables/_v_h_e_a.py index 934f2f12d4..de7ce245ad 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_v_h_e_a.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_v_h_e_a.py @@ -31,7 +31,6 @@ vheaFormat = """ class table__v_h_e_a(DefaultTable.DefaultTable): - # Note: Keep in sync with table__h_h_e_a dependencies = ["vmtx", "glyf", "CFF ", "CFF2"] diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_v_m_t_x.py b/contrib/python/fonttools/fontTools/ttLib/tables/_v_m_t_x.py index c965c94ee5..a13304c321 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_v_m_t_x.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_v_m_t_x.py @@ -4,7 +4,6 @@ superclass = ttLib.getTableClass("hmtx") class table__v_m_t_x(superclass): - headerTag = "vhea" advanceName = "height" sideBearingName = "tsb" diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py b/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py index 9470873440..262f8d4187 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py @@ -538,7 +538,6 @@ class FeatureParamsCharacterVariants(FeatureParams): class Coverage(FormatSwitchingBaseTable): - # manual implementation to get rid of glyphID dependencies def populateDefaults(self, propagator=None): diff --git a/contrib/python/fonttools/fontTools/ttLib/ttCollection.py b/contrib/python/fonttools/fontTools/ttLib/ttCollection.py index 3ab579ee00..70ed4b7a0d 100644 --- a/contrib/python/fonttools/fontTools/ttLib/ttCollection.py +++ b/contrib/python/fonttools/fontTools/ttLib/ttCollection.py @@ -87,7 +87,6 @@ class TTCollection(object): file.close() def saveXML(self, fileOrPath, newlinestr="\n", writeVersion=True, **kwargs): - from fontTools.misc import xmlWriter writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr) diff --git a/contrib/python/fonttools/fontTools/ttLib/ttFont.py b/contrib/python/fonttools/fontTools/ttLib/ttFont.py index 1bece8e5e4..6a9ca09820 100644 --- a/contrib/python/fonttools/fontTools/ttLib/ttFont.py +++ b/contrib/python/fonttools/fontTools/ttLib/ttFont.py @@ -287,7 +287,6 @@ class TTFont(object): disassembleInstructions=True, bitmapGlyphDataFormat="raw", ): - if quiet is not None: deprecateArgument("quiet", "configure logging instead") diff --git a/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py b/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py index fa7fbd4f23..d4384c89f6 100644 --- a/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py +++ b/contrib/python/fonttools/fontTools/ttLib/ttGlyphSet.py @@ -170,7 +170,6 @@ class _TTGlyphGlyf(_TTGlyph): glyph, offset = self._getGlyphAndOffset() with self.glyphSet.pushDepth() as depth: - if depth: offset = 0 # Offset should only apply at top-level @@ -187,7 +186,6 @@ class _TTGlyphGlyf(_TTGlyph): glyph, offset = self._getGlyphAndOffset() with self.glyphSet.pushDepth() as depth: - if depth: offset = 0 # Offset should only apply at top-level @@ -198,14 +196,12 @@ class _TTGlyphGlyf(_TTGlyph): glyph.drawPoints(pen, self.glyphSet.glyfTable, offset) def _drawVarComposite(self, glyph, pen, isPointPen): - from fontTools.ttLib.tables._g_l_y_f import ( VarComponentFlags, VAR_COMPONENT_TRANSFORM_MAPPING, ) for comp in glyph.components: - with self.glyphSet.pushLocation( comp.location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES ): diff --git a/contrib/python/fonttools/fontTools/ttLib/woff2.py b/contrib/python/fonttools/fontTools/ttLib/woff2.py index 3e247b02e9..9da2f7e6d6 100644 --- a/contrib/python/fonttools/fontTools/ttLib/woff2.py +++ b/contrib/python/fonttools/fontTools/ttLib/woff2.py @@ -42,7 +42,6 @@ except ImportError: class WOFF2Reader(SFNTReader): - flavor = "woff2" def __init__(self, file, checkChecksums=0, fontNumber=-1): @@ -177,7 +176,6 @@ class WOFF2Reader(SFNTReader): class WOFF2Writer(SFNTWriter): - flavor = "woff2" def __init__( @@ -1291,7 +1289,6 @@ class WOFF2HmtxTable(getTableClass("hmtx")): class WOFF2FlavorData(WOFFFlavorData): - Flavor = "woff2" def __init__(self, reader=None, data=None, transformedTables=None): diff --git a/contrib/python/fonttools/fontTools/ttx.py b/contrib/python/fonttools/fontTools/ttx.py index 65a3c7a808..d8c2a3a758 100644 --- a/contrib/python/fonttools/fontTools/ttx.py +++ b/contrib/python/fonttools/fontTools/ttx.py @@ -124,7 +124,6 @@ opentypeheaderRE = re.compile("""sfntVersion=['"]OTTO["']""") class Options(object): - listTables = False outputDir = None outputFile = None diff --git a/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py b/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py index a8663ec422..cde1d39f6f 100644 --- a/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py +++ b/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py @@ -87,7 +87,7 @@ from fontTools.misc.fixedTools import ( strToFixedToFloat, otRound, ) -from fontTools.varLib.models import supportScalar, normalizeValue, piecewiseLinearMap +from fontTools.varLib.models import normalizeValue, piecewiseLinearMap from fontTools.ttLib import TTFont from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.ttLib.tables import _g_l_y_f @@ -139,26 +139,36 @@ def NormalizedAxisRange(minimum, maximum): class AxisTriple(Sequence): """A triple of (min, default, max) axis values. - The default value can be None, in which case the limitRangeAndPopulateDefault() - method can be used to fill in the missing default value based on the fvar axis - default. + Any of the values can be None, in which case the limitRangeAndPopulateDefaults() + method can be used to fill in the missing values based on the fvar axis values. """ - minimum: float - default: Optional[float] # if None, filled with by limitRangeAndPopulateDefault - maximum: float + minimum: Optional[float] + default: Optional[float] + maximum: Optional[float] def __post_init__(self): if self.default is None and self.minimum == self.maximum: object.__setattr__(self, "default", self.minimum) - if not ( - (self.minimum <= self.default <= self.maximum) - if self.default is not None - else (self.minimum <= self.maximum) + if ( + ( + self.minimum is not None + and self.default is not None + and self.minimum > self.default + ) + or ( + self.default is not None + and self.maximum is not None + and self.default > self.maximum + ) + or ( + self.minimum is not None + and self.maximum is not None + and self.minimum > self.maximum + ) ): raise ValueError( - f"{type(self).__name__} minimum ({self.minimum}) must be <= default " - f"({self.default}) which must be <= maximum ({self.maximum})" + f"{type(self).__name__} minimum ({self.minimum}), default ({self.default}), maximum ({self.maximum}) must be in sorted order" ) def __getitem__(self, i): @@ -210,7 +220,7 @@ class AxisTriple(Sequence): raise ValueError(f"expected sequence of 2 or 3; got {n}: {v!r}") return cls(minimum, default, maximum) - def limitRangeAndPopulateDefault(self, fvarTriple) -> "AxisTriple": + def limitRangeAndPopulateDefaults(self, fvarTriple) -> "AxisTriple": """Return a new AxisTriple with the default value filled in. Set default to fvar axis default if the latter is within the min/max range, @@ -219,13 +229,17 @@ class AxisTriple(Sequence): If the default value is already set, return self. """ minimum = self.minimum - maximum = self.maximum + if minimum is None: + minimum = fvarTriple[0] default = self.default if default is None: default = fvarTriple[1] + maximum = self.maximum + if maximum is None: + maximum = fvarTriple[2] - minimum = max(self.minimum, fvarTriple[0]) - maximum = max(self.maximum, fvarTriple[0]) + minimum = max(minimum, fvarTriple[0]) + maximum = max(maximum, fvarTriple[0]) minimum = min(minimum, fvarTriple[2]) maximum = min(maximum, fvarTriple[2]) default = max(minimum, min(maximum, default)) @@ -372,7 +386,7 @@ class AxisLimits(_BaseAxisLimits): if triple is None: newLimits[axisTag] = AxisTriple(default, default, default) else: - newLimits[axisTag] = triple.limitRangeAndPopulateDefault(fvarTriple) + newLimits[axisTag] = triple.limitRangeAndPopulateDefaults(fvarTriple) return type(self)(newLimits) def normalize(self, varfont, usingAvar=True) -> "NormalizedAxisLimits": @@ -1273,6 +1287,9 @@ def instantiateVariableFont( ignoreErrors=(overlap == OverlapMode.REMOVE_AND_IGNORE_ERRORS), ) + if "OS/2" in varfont: + varfont["OS/2"].recalcAvgCharWidth(varfont) + varLib.set_default_weight_width_slant( varfont, location=axisLimits.defaultLocation() ) @@ -1326,29 +1343,27 @@ def parseLimits(limits: Iterable[str]) -> Dict[str, Optional[AxisTriple]]: result = {} for limitString in limits: match = re.match( - r"^(\w{1,4})=(?:(drop)|(?:([^:]+)(?:[:]([^:]+))?(?:[:]([^:]+))?))$", + r"^(\w{1,4})=(?:(drop)|(?:([^:]*)(?:[:]([^:]*))?(?:[:]([^:]*))?))$", limitString, ) if not match: raise ValueError("invalid location format: %r" % limitString) tag = match.group(1).ljust(4) + if match.group(2): # 'drop' - lbound = None - else: - lbound = strToFixedToFloat(match.group(3), precisionBits=16) - ubound = default = lbound - if match.group(4): - ubound = default = strToFixedToFloat(match.group(4), precisionBits=16) - default = None - if match.group(5): - default = ubound - ubound = strToFixedToFloat(match.group(5), precisionBits=16) - - if all(v is None for v in (lbound, default, ubound)): result[tag] = None continue - result[tag] = AxisTriple(lbound, default, ubound) + triple = match.group(3, 4, 5) + + if triple[1] is None: # "value" syntax + triple = (triple[0], triple[0], triple[0]) + elif triple[2] is None: # "min:max" syntax + triple = (triple[0], None, triple[1]) + + triple = tuple(float(v) if v else None for v in triple) + + result[tag] = AxisTriple(*triple) return result @@ -1377,9 +1392,11 @@ def parseArgs(args): metavar="AXIS=LOC", nargs="*", help="List of space separated locations. A location consists of " - "the tag of a variation axis, followed by '=' and one of number, " - "number:number or the literal string 'drop'. " - "E.g.: wdth=100 or wght=75.0:125.0 or wght=drop", + "the tag of a variation axis, followed by '=' and the literal, " + "string 'drop', or comma-separate list of one to three values, " + "each of which is the empty string, or a number. " + "E.g.: wdth=100 or wght=75.0:125.0 or wght=100:400:700 or wght=:500: " + "or wght=drop", ) parser.add_argument( "-o", diff --git a/contrib/python/fonttools/fontTools/varLib/instancer/names.py b/contrib/python/fonttools/fontTools/varLib/instancer/names.py index dad3fd7e57..f9454688e0 100644 --- a/contrib/python/fonttools/fontTools/varLib/instancer/names.py +++ b/contrib/python/fonttools/fontTools/varLib/instancer/names.py @@ -134,6 +134,14 @@ def updateNameTable(varfont, axisLimits): def checkAxisValuesExist(stat, axisValues, axisCoords): seen = set() designAxes = stat.DesignAxisRecord.Axis + hasValues = set() + for value in stat.AxisValueArray.AxisValue: + if value.Format in (1, 2, 3): + hasValues.add(designAxes[value.AxisIndex].AxisTag) + elif value.Format == 4: + for rec in value.AxisValueRecord: + hasValues.add(designAxes[rec.AxisIndex].AxisTag) + for axisValueTable in axisValues: axisValueFormat = axisValueTable.Format if axisValueTable.Format in (1, 2, 3): @@ -150,7 +158,7 @@ def checkAxisValuesExist(stat, axisValues, axisCoords): if axisTag in axisCoords and rec.Value == axisCoords[axisTag]: seen.add(axisTag) - missingAxes = set(axisCoords) - seen + missingAxes = (set(axisCoords) - seen) & hasValues if missingAxes: missing = ", ".join(f"'{i}': {axisCoords[i]}" for i in missingAxes) raise ValueError(f"Cannot find Axis Values {{{missing}}}") diff --git a/contrib/python/fonttools/fontTools/varLib/instancer/solver.py b/contrib/python/fonttools/fontTools/varLib/instancer/solver.py index c991fcdcfb..9c568fe9a5 100644 --- a/contrib/python/fonttools/fontTools/varLib/instancer/solver.py +++ b/contrib/python/fonttools/fontTools/varLib/instancer/solver.py @@ -99,11 +99,13 @@ def _solve(tent, axisLimit, negative=False): # axisDef | axisMax # | # crossing - if gain > outGain: + if gain >= outGain: + # Note that this is the branch taken if both gain and outGain are 0. + # Crossing point on the axis. crossing = peak + (1 - gain) * (upper - peak) - loc = (axisDef, peak, crossing) + loc = (max(lower, axisDef), peak, crossing) scalar = 1 # The part before the crossing point. @@ -175,7 +177,7 @@ def _solve(tent, axisLimit, negative=False): # axisDef axisMax # newUpper = peak + (1 - gain) * (upper - peak) - assert axisMax <= newUpper # Because outGain >= gain + assert axisMax <= newUpper # Because outGain > gain if newUpper <= axisDef + (axisMax - axisDef) * 2: upper = newUpper if not negative and axisDef + (axisMax - axisDef) * MAX_F2DOT14 < upper: diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatable.py b/contrib/python/fonttools/fontTools/varLib/interpolatable.py index d5428c2002..c3f01f46e0 100644 --- a/contrib/python/fonttools/fontTools/varLib/interpolatable.py +++ b/contrib/python/fonttools/fontTools/varLib/interpolatable.py @@ -7,11 +7,11 @@ $ fonttools varLib.interpolatable font1 font2 ... """ from fontTools.pens.basePen import AbstractPen, BasePen -from fontTools.pens.pointPen import SegmentToPointPen +from fontTools.pens.pointPen import AbstractPointPen, SegmentToPointPen from fontTools.pens.recordingPen import RecordingPen from fontTools.pens.statisticsPen import StatisticsPen from fontTools.pens.momentsPen import OpenContourError -from collections import OrderedDict +from collections import defaultdict import math import itertools import sys @@ -20,11 +20,7 @@ import sys def _rot_list(l, k): """Rotate list by k items forward. Ie. item at position 0 will be at position k in returned list. Negative k is allowed.""" - n = len(l) - k %= n - if not k: - return l - return l[n - k :] + l[: n - k] + return l[-k:] + l[:-k] class PerContourPen(BasePen): @@ -67,7 +63,7 @@ class PerContourOrComponentPen(PerContourPen): self.value[-1].addComponent(glyphName, transformation) -class RecordingPointPen(BasePen): +class RecordingPointPen(AbstractPointPen): def __init__(self): self.value = [] @@ -81,48 +77,43 @@ class RecordingPointPen(BasePen): self.value.append((pt, False if segmentType is None else True)) -def _vdiff(v0, v1): - return tuple(b - a for a, b in zip(v0, v1)) +def _vdiff_hypot2(v0, v1): + s = 0 + for x0, x1 in zip(v0, v1): + d = x1 - x0 + s += d * d + return s -def _vlen(vec): - v = 0 - for x in vec: - v += x * x - return v - - -def _complex_vlen(vec): - v = 0 - for x in vec: - v += abs(x) * abs(x) - return v +def _vdiff_hypot2_complex(v0, v1): + s = 0 + for x0, x1 in zip(v0, v1): + d = x1 - x0 + s += d.real * d.real + d.imag * d.imag + return s def _matching_cost(G, matching): return sum(G[i][j] for i, j in enumerate(matching)) -def min_cost_perfect_bipartite_matching(G): +def min_cost_perfect_bipartite_matching_scipy(G): n = len(G) - try: - from scipy.optimize import linear_sum_assignment + rows, cols = linear_sum_assignment(G) + assert (rows == list(range(n))).all() + return list(cols), _matching_cost(G, cols) - rows, cols = linear_sum_assignment(G) - assert (rows == list(range(n))).all() - return list(cols), _matching_cost(G, cols) - except ImportError: - pass - try: - from munkres import Munkres +def min_cost_perfect_bipartite_matching_munkres(G): + n = len(G) + cols = [None] * n + for row, col in Munkres().compute(G): + cols[row] = col + return cols, _matching_cost(G, cols) - cols = [None] * n - for row, col in Munkres().compute(G): - cols[row] = col - return cols, _matching_cost(G, cols) - except ImportError: - pass + +def min_cost_perfect_bipartite_matching_bruteforce(G): + n = len(G) if n > 6: raise Exception("Install Python module 'munkres' or 'scipy >= 0.17.0'") @@ -138,7 +129,24 @@ def min_cost_perfect_bipartite_matching(G): return best, best_cost -def test(glyphsets, glyphs=None, names=None, ignore_missing=False): +try: + from scipy.optimize import linear_sum_assignment + + min_cost_perfect_bipartite_matching = min_cost_perfect_bipartite_matching_scipy +except ImportError: + try: + from munkres import Munkres + + min_cost_perfect_bipartite_matching = ( + min_cost_perfect_bipartite_matching_munkres + ) + except ImportError: + min_cost_perfect_bipartite_matching = ( + min_cost_perfect_bipartite_matching_bruteforce + ) + + +def test_gen(glyphsets, glyphs=None, names=None, ignore_missing=False): if names is None: names = glyphsets if glyphs is None: @@ -147,10 +155,6 @@ def test(glyphsets, glyphs=None, names=None, ignore_missing=False): glyphs = {g for glyphset in glyphsets for g in glyphset.keys()} hist = [] - problems = OrderedDict() - - def add_problem(glyphname, problem): - problems.setdefault(glyphname, []).append(problem) for glyph_name in glyphs: try: @@ -158,12 +162,13 @@ def test(glyphsets, glyphs=None, names=None, ignore_missing=False): allVectors = [] allNodeTypes = [] allContourIsomorphisms = [] - for glyphset, name in zip(glyphsets, names): - glyph = glyphset[glyph_name] - + allGlyphs = [glyphset[glyph_name] for glyphset in glyphsets] + if len([1 for glyph in allGlyphs if glyph is not None]) <= 1: + continue + for glyph, glyphset, name in zip(allGlyphs, glyphsets, names): if glyph is None: if not ignore_missing: - add_problem(glyph_name, {"type": "missing", "master": name}) + yield (glyph_name, {"type": "missing", "master": name}) allNodeTypes.append(None) allVectors.append(None) allContourIsomorphisms.append(None) @@ -193,7 +198,7 @@ def test(glyphsets, glyphs=None, names=None, ignore_missing=False): try: contour.replay(stats) except OpenContourError as e: - add_problem( + yield ( glyph_name, {"master": name, "contour": ix, "type": "open_path"}, ) @@ -228,36 +233,35 @@ def test(glyphsets, glyphs=None, names=None, ignore_missing=False): mask = (1 << n) - 1 isomorphisms = [] contourIsomorphisms.append(isomorphisms) + complexPoints = [complex(*pt) for pt, bl in points.value] for i in range(n): b = ((bits << i) & mask) | ((bits >> (n - i))) if b == bits: - isomorphisms.append( - _rot_list([complex(*pt) for pt, bl in points.value], i) - ) + isomorphisms.append(_rot_list(complexPoints, i)) # Add mirrored rotations mirrored = list(reversed(points.value)) reversed_bits = 0 for pt, b in mirrored: reversed_bits = (reversed_bits << 1) | b + complexPoints = list(reversed(complexPoints)) for i in range(n): b = ((reversed_bits << i) & mask) | ((reversed_bits >> (n - i))) if b == bits: - isomorphisms.append( - _rot_list([complex(*pt) for pt, bl in mirrored], i) - ) + isomorphisms.append(_rot_list(complexPoints, i)) # m0idx should be the index of the first non-None item in allNodeTypes, - # else give it the first index of None, which is likely 0 - m0idx = allNodeTypes.index( - next((x for x in allNodeTypes if x is not None), None) + # else give it the last item. + m0idx = next( + (i for i, x in enumerate(allNodeTypes) if x is not None), + len(allNodeTypes) - 1, ) - # m0 is the first non-None item in allNodeTypes, or the first item if all are None + # m0 is the first non-None item in allNodeTypes, or last one if all None m0 = allNodeTypes[m0idx] for i, m1 in enumerate(allNodeTypes[m0idx + 1 :]): if m1 is None: continue if len(m0) != len(m1): - add_problem( + yield ( glyph_name, { "type": "path_count", @@ -273,7 +277,7 @@ def test(glyphsets, glyphs=None, names=None, ignore_missing=False): if nodes1 == nodes2: continue if len(nodes1) != len(nodes2): - add_problem( + yield ( glyph_name, { "type": "node_count", @@ -287,7 +291,7 @@ def test(glyphsets, glyphs=None, names=None, ignore_missing=False): continue for nodeIx, (n1, n2) in enumerate(zip(nodes1, nodes2)): if n1 != n2: - add_problem( + yield ( glyph_name, { "type": "node_incompatibility", @@ -302,81 +306,94 @@ def test(glyphsets, glyphs=None, names=None, ignore_missing=False): continue # m0idx should be the index of the first non-None item in allVectors, - # else give it the first index of None, which is likely 0 - m0idx = allVectors.index( - next((x for x in allVectors if x is not None), None) + # else give it the last item. + m0idx = next( + (i for i, x in enumerate(allVectors) if x is not None), + len(allVectors) - 1, ) - # m0 is the first non-None item in allVectors, or the first item if all are None + # m0 is the first non-None item in allVectors, or last one if all None m0 = allVectors[m0idx] - for i, m1 in enumerate(allVectors[m0idx + 1 :]): - if m1 is None: - continue - if len(m0) != len(m1): - # We already reported this - continue - if not m0: - continue - costs = [[_vlen(_vdiff(v0, v1)) for v1 in m1] for v0 in m0] - matching, matching_cost = min_cost_perfect_bipartite_matching(costs) - identity_matching = list(range(len(m0))) - identity_cost = sum(costs[i][i] for i in range(len(m0))) - if ( - matching != identity_matching - and matching_cost < identity_cost * 0.95 - ): - add_problem( - glyph_name, - { - "type": "contour_order", - "master_1": names[m0idx], - "master_2": names[m0idx + i + 1], - "value_1": list(range(len(m0))), - "value_2": matching, - }, - ) - break - - # m0idx should be the index of the first non-None item in allContourIsomorphisms, - # else give it the first index of None, which is likely 0 - m0idx = allContourIsomorphisms.index( - next((x for x in allContourIsomorphisms if x is not None), None) - ) - # m0 is the first non-None item in allContourIsomorphisms, or the first item if all are None - m0 = allContourIsomorphisms[m0idx] - for i, m1 in enumerate(allContourIsomorphisms[m0idx + 1 :]): - if m1 is None: - continue - if len(m0) != len(m1): - # We already reported this - continue - if not m0: - continue - for ix, (contour0, contour1) in enumerate(zip(m0, m1)): - c0 = contour0[0] - costs = [ - v for v in (_complex_vlen(_vdiff(c0, c1)) for c1 in contour1) - ] - min_cost = min(costs) - first_cost = costs[0] - if min_cost < first_cost * 0.95: - add_problem( + if m0 is not None and len(m0) > 1: + for i, m1 in enumerate(allVectors[m0idx + 1 :]): + if m1 is None: + continue + if len(m0) != len(m1): + # We already reported this + continue + costs = [[_vdiff_hypot2(v0, v1) for v1 in m1] for v0 in m0] + matching, matching_cost = min_cost_perfect_bipartite_matching(costs) + identity_matching = list(range(len(m0))) + identity_cost = sum(costs[i][i] for i in range(len(m0))) + if ( + matching != identity_matching + and matching_cost < identity_cost * 0.95 + ): + yield ( glyph_name, { - "type": "wrong_start_point", - "contour": ix, + "type": "contour_order", "master_1": names[m0idx], "master_2": names[m0idx + i + 1], + "value_1": list(range(len(m0))), + "value_2": matching, }, ) + break + + # m0idx should be the index of the first non-None item in allContourIsomorphisms, + # else give it the last item. + m0idx = next( + (i for i, x in enumerate(allContourIsomorphisms) if x is not None), + len(allVectors) - 1, + ) + # m0 is the first non-None item in allContourIsomorphisms, or last one if all None + m0 = allContourIsomorphisms[m0idx] + if m0: + for i, m1 in enumerate(allContourIsomorphisms[m0idx + 1 :]): + if m1 is None: + continue + if len(m0) != len(m1): + # We already reported this + continue + for ix, (contour0, contour1) in enumerate(zip(m0, m1)): + c0 = contour0[0] + costs = [_vdiff_hypot2_complex(c0, c1) for c1 in contour1] + min_cost = min(costs) + first_cost = costs[0] + if min_cost < first_cost * 0.95: + yield ( + glyph_name, + { + "type": "wrong_start_point", + "contour": ix, + "master_1": names[m0idx], + "master_2": names[m0idx + i + 1], + }, + ) except ValueError as e: - add_problem( + yield ( glyph_name, {"type": "math_error", "master": name, "error": e}, ) + + +def test(glyphsets, glyphs=None, names=None, ignore_missing=False): + problems = defaultdict(list) + for glyphname, problem in test_gen(glyphsets, glyphs, names, ignore_missing): + problems[glyphname].append(problem) return problems +def recursivelyAddGlyph(glyphname, glyphset, ttGlyphSet, glyf): + if glyphname in glyphset: + return + glyphset[glyphname] = ttGlyphSet[glyphname] + + for component in getattr(glyf[glyphname], "components", []): + recursivelyAddGlyph(component.glyphName, glyphset, ttGlyphSet, glyf) + + def main(args=None): """Test for interpolatability issues between fonts""" import argparse @@ -410,12 +427,12 @@ def main(args=None): metavar="FILE", type=str, nargs="+", - help="Input a single DesignSpace/Glyphs file, or multiple TTF/UFO files", + help="Input a single variable font / DesignSpace / Glyphs file, or multiple TTF/UFO files", ) args = parser.parse_args(args) - glyphs = set(args.glyphs.split()) if args.glyphs else None + glyphs = args.glyphs.split() if args.glyphs else None from os.path import basename @@ -444,30 +461,37 @@ def main(args=None): if "gvar" in font: # Is variable font gvar = font["gvar"] - # Gather all "master" locations - locs = set() - for variations in gvar.variations.values(): - for var in variations: + glyf = font["glyf"] + # Gather all glyphs at their "master" locations + ttGlyphSets = {} + glyphsets = defaultdict(dict) + + if glyphs is None: + glyphs = sorted(gvar.variations.keys()) + for glyphname in glyphs: + for var in gvar.variations[glyphname]: + locDict = {} loc = [] for tag, val in sorted(var.axes.items()): + locDict[tag] = val[1] loc.append((tag, val[1])) - locs.add(tuple(loc)) - # Rebuild locs as dictionaries - new_locs = [{}] - names.append("()") - for loc in sorted(locs, key=lambda v: (len(v), v)): - names.append(str(loc)) - l = {} - for tag, val in loc: - l[tag] = val - new_locs.append(l) - locs = new_locs - del new_locs - # locs is all master locations now - - for loc in locs: - fonts.append(font.getGlyphSet(location=loc, normalized=True)) + locTuple = tuple(loc) + if locTuple not in ttGlyphSets: + ttGlyphSets[locTuple] = font.getGlyphSet( + location=locDict, normalized=True + ) + + recursivelyAddGlyph( + glyphname, glyphsets[locTuple], ttGlyphSets[locTuple], glyf + ) + + names = ["()"] + fonts = [font.getGlyphSet()] + for locTuple in sorted(glyphsets.keys(), key=lambda v: (len(v), v)): + names.append(str(locTuple)) + fonts.append(glyphsets[locTuple]) + args.ignore_missing = True args.inputs = [] for filename in args.inputs: @@ -491,87 +515,101 @@ def main(args=None): glyphsets.append({k: glyphset[k] for k in glyphset.keys()}) if not glyphs: - glyphs = set([gn for glyphset in glyphsets for gn in glyphset.keys()]) + glyphs = sorted(set([gn for glyphset in glyphsets for gn in glyphset.keys()])) + glyphsSet = set(glyphs) for glyphset in glyphsets: glyphSetGlyphNames = set(glyphset.keys()) - diff = glyphs - glyphSetGlyphNames + diff = glyphsSet - glyphSetGlyphNames if diff: for gn in diff: glyphset[gn] = None - problems = test( + problems_gen = test_gen( glyphsets, glyphs=glyphs, names=names, ignore_missing=args.ignore_missing ) + problems = defaultdict(list) if not args.quiet: if args.json: import json + for glyphname, problem in problems_gen: + problems[glyphname].append(problem) + print(json.dumps(problems)) else: - for glyph, glyph_problems in problems.items(): - print(f"Glyph {glyph} was not compatible: ") - for p in glyph_problems: - if p["type"] == "missing": - print(" Glyph was missing in master %s" % p["master"]) - if p["type"] == "open_path": - print(" Glyph has an open path in master %s" % p["master"]) - if p["type"] == "path_count": - print( - " Path count differs: %i in %s, %i in %s" - % (p["value_1"], p["master_1"], p["value_2"], p["master_2"]) - ) - if p["type"] == "node_count": - print( - " Node count differs in path %i: %i in %s, %i in %s" - % ( - p["path"], - p["value_1"], - p["master_1"], - p["value_2"], - p["master_2"], - ) + last_glyphname = None + for glyphname, p in problems_gen: + problems[glyphname].append(p) + + if glyphname != last_glyphname: + print(f"Glyph {glyphname} was not compatible: ") + last_glyphname = glyphname + + if p["type"] == "missing": + print(" Glyph was missing in master %s" % p["master"]) + if p["type"] == "open_path": + print(" Glyph has an open path in master %s" % p["master"]) + if p["type"] == "path_count": + print( + " Path count differs: %i in %s, %i in %s" + % (p["value_1"], p["master_1"], p["value_2"], p["master_2"]) + ) + if p["type"] == "node_count": + print( + " Node count differs in path %i: %i in %s, %i in %s" + % ( + p["path"], + p["value_1"], + p["master_1"], + p["value_2"], + p["master_2"], ) - if p["type"] == "node_incompatibility": - print( - " Node %o incompatible in path %i: %s in %s, %s in %s" - % ( - p["node"], - p["path"], - p["value_1"], - p["master_1"], - p["value_2"], - p["master_2"], - ) + ) + if p["type"] == "node_incompatibility": + print( + " Node %o incompatible in path %i: %s in %s, %s in %s" + % ( + p["node"], + p["path"], + p["value_1"], + p["master_1"], + p["value_2"], + p["master_2"], ) - if p["type"] == "contour_order": - print( - " Contour order differs: %s in %s, %s in %s" - % ( - p["value_1"], - p["master_1"], - p["value_2"], - p["master_2"], - ) + ) + if p["type"] == "contour_order": + print( + " Contour order differs: %s in %s, %s in %s" + % ( + p["value_1"], + p["master_1"], + p["value_2"], + p["master_2"], ) - if p["type"] == "wrong_start_point": - print( - " Contour %d start point differs: %s, %s" - % ( - p["contour"], - p["master_1"], - p["master_2"], - ) + ) + if p["type"] == "wrong_start_point": + print( + " Contour %d start point differs: %s, %s" + % ( + p["contour"], + p["master_1"], + p["master_2"], ) - if p["type"] == "math_error": - print( - " Miscellaneous error in %s: %s" - % ( - p["master"], - p["error"], - ) + ) + if p["type"] == "math_error": + print( + " Miscellaneous error in %s: %s" + % ( + p["master"], + p["error"], ) + ) + else: + for glyphname, problem in problems_gen: + problems[glyphname].append(problem) + if problems: return problems diff --git a/contrib/python/fonttools/fontTools/varLib/models.py b/contrib/python/fonttools/fontTools/varLib/models.py index 954cf87bfa..5bd66dba15 100644 --- a/contrib/python/fonttools/fontTools/varLib/models.py +++ b/contrib/python/fonttools/fontTools/varLib/models.py @@ -248,7 +248,6 @@ class VariationModel(object): """ def __init__(self, locations, axisOrder=None, extrapolate=False): - if len(set(tuple(sorted(l.items())) for l in locations)) != len(locations): raise VariationModelError("Locations must be unique.") diff --git a/contrib/python/fonttools/fontTools/varLib/varStore.py b/contrib/python/fonttools/fontTools/varLib/varStore.py index 74828e407e..7805769074 100644 --- a/contrib/python/fonttools/fontTools/varLib/varStore.py +++ b/contrib/python/fonttools/fontTools/varLib/varStore.py @@ -126,11 +126,11 @@ def VarData_addItem(self, deltas, *, round=round): countUs = self.VarRegionCount countThem = len(deltas) if countUs + 1 == countThem: - deltas = tuple(deltas[1:]) + deltas = list(deltas[1:]) else: assert countUs == countThem, (countUs, countThem) - deltas = tuple(deltas) - self.Item.append(list(deltas)) + deltas = list(deltas) + self.Item.append(deltas) self.ItemCount = len(self.Item) @@ -532,6 +532,23 @@ def VarStore_optimize(self, use_NO_VARIATION_INDEX=True, quantization=1): # - Insert the new encoding into the todo list, # # - Encode all remaining items in the todo list. + # + # The output is then sorted for stability, in the following way: + # - The VarRegionList of the input is kept intact. + # - All encodings are sorted before the main algorithm, by + # gain_key_sort(), which is a tuple of the following items: + # * The gain of the encoding. + # * The characteristic bitmap of the encoding, with higher-numbered + # columns compared first. + # - The VarData is sorted by width_sort_key(), which is a tuple + # of the following items: + # * The "width" of the encoding. + # * The characteristic bitmap of the encoding, with higher-numbered + # columns compared first. + # - Within each VarData, the items are sorted as vectors of numbers. + # + # Finally, each VarData is optimized to remove the empty columns and + # reorder columns as needed. # TODO # Check that no two VarRegions are the same; if they are, fold them. @@ -619,14 +636,21 @@ def VarStore_optimize(self, use_NO_VARIATION_INDEX=True, quantization=1): back_mapping = {} # Mapping from full rows to new VarIdxes encodings.sort(key=_Encoding.width_sort_key) self.VarData = [] - for major, encoding in enumerate(encodings): - data = ot.VarData() - self.VarData.append(data) - data.VarRegionIndex = range(n) - data.VarRegionCount = len(data.VarRegionIndex) - data.Item = sorted(encoding.items) - for minor, item in enumerate(data.Item): - back_mapping[item] = (major << 16) + minor + for encoding in encodings: + items = sorted(encoding.items) + + while items: + major = len(self.VarData) + data = ot.VarData() + self.VarData.append(data) + data.VarRegionIndex = range(n) + data.VarRegionCount = len(data.VarRegionIndex) + + # Each major can only encode up to 0xFFFF entries. + data.Item, items = items[:0xFFFF], items[0xFFFF:] + + for minor, item in enumerate(data.Item): + back_mapping[item] = (major << 16) + minor # Compile final mapping. varidx_map = {NO_VARIATION_INDEX: NO_VARIATION_INDEX} diff --git a/contrib/python/fonttools/ya.make b/contrib/python/fonttools/ya.make index 4aeb2b26f6..adcfec1693 100644 --- a/contrib/python/fonttools/ya.make +++ b/contrib/python/fonttools/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(4.43.1) +VERSION(4.44.0) LICENSE(MIT) |