diff options
author | AlexSm <alex@ydb.tech> | 2024-01-09 18:56:40 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-09 18:56:40 +0100 |
commit | e95f266d2a3e48e62015220588a4fd73d5d5a5cb (patch) | |
tree | a8a784b6931fe52ad5f511cfef85af14e5f63991 /contrib/python/fonttools/fontTools/varLib | |
parent | 50a65e3b48a82d5b51f272664da389f2e0b0c99a (diff) | |
download | ydb-e95f266d2a3e48e62015220588a4fd73d5d5a5cb.tar.gz |
Library import 6 (#888)
Diffstat (limited to 'contrib/python/fonttools/fontTools/varLib')
9 files changed, 551 insertions, 408 deletions
diff --git a/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py b/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py index a887e5d38f..d1cde0df7a 100644 --- a/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py +++ b/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py @@ -1433,7 +1433,7 @@ def parseArgs(args): nargs="*", help="List of space separated locations. A location consists of " "the tag of a variation axis, followed by '=' and the literal, " - "string 'drop', or comma-separate list of one to three values, " + "string 'drop', or colon-separated 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", diff --git a/contrib/python/fonttools/fontTools/varLib/instancer/solver.py b/contrib/python/fonttools/fontTools/varLib/instancer/solver.py index 9c568fe9a5..ba5231b796 100644 --- a/contrib/python/fonttools/fontTools/varLib/instancer/solver.py +++ b/contrib/python/fonttools/fontTools/varLib/instancer/solver.py @@ -178,7 +178,9 @@ def _solve(tent, axisLimit, negative=False): # newUpper = peak + (1 - gain) * (upper - peak) assert axisMax <= newUpper # Because outGain > gain - if newUpper <= axisDef + (axisMax - axisDef) * 2: + # Disabled because ots doesn't like us: + # https://github.com/fonttools/fonttools/issues/3350 + if False and newUpper <= axisDef + (axisMax - axisDef) * 2: upper = newUpper if not negative and axisDef + (axisMax - axisDef) * MAX_F2DOT14 < upper: # we clamp +2.0 to the max F2Dot14 (~1.99994) for convenience diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatable.py b/contrib/python/fonttools/fontTools/varLib/interpolatable.py index f03e946207..0a9bbebc41 100644 --- a/contrib/python/fonttools/fontTools/varLib/interpolatable.py +++ b/contrib/python/fonttools/fontTools/varLib/interpolatable.py @@ -9,7 +9,11 @@ $ fonttools varLib.interpolatable font1 font2 ... from .interpolatableHelpers import * from .interpolatableTestContourOrder import test_contour_order from .interpolatableTestStartingPoint import test_starting_point -from fontTools.pens.recordingPen import RecordingPen, DecomposingRecordingPen +from fontTools.pens.recordingPen import ( + RecordingPen, + DecomposingRecordingPen, + lerpRecordings, +) from fontTools.pens.transformPen import TransformPen from fontTools.pens.statisticsPen import StatisticsPen, StatisticsControlPen from fontTools.pens.momentsPen import OpenContourError @@ -22,6 +26,7 @@ from functools import wraps from pprint import pformat from math import sqrt, atan2, pi import logging +import os log = logging.getLogger("fontTools.varLib.interpolatable") @@ -34,11 +39,9 @@ DEFAULT_UPEM = 1000 class Glyph: ITEMS = ( "recordings", - "recordingsNormalized", "greenStats", "controlStats", "greenVectors", - "greenVectorsNormalized", "controlVectors", "nodeTypes", "isomorphisms", @@ -90,21 +93,6 @@ class Glyph: self.greenVectors.append(contour_vector_from_stats(greenStats)) self.controlVectors.append(contour_vector_from_stats(controlStats)) - # Save a "normalized" version of the outlines - try: - rpen = DecomposingRecordingPen(glyphset) - tpen = TransformPen( - rpen, transform_from_stats(greenStats, inverse=True) - ) - contour.replay(tpen) - self.recordingsNormalized.append(rpen) - except ZeroDivisionError: - self.recordingsNormalized.append(None) - - greenStats = StatisticsPen(glyphset=glyphset) - rpen.replay(greenStats) - self.greenVectorsNormalized.append(contour_vector_from_stats(greenStats)) - # Check starting point if nodeTypes[0] == "addComponent": self._fill_in(ix) @@ -186,7 +174,11 @@ def test_gen( if not ignore_missing: yield ( glyph_name, - {"type": "missing", "master": name, "master_idx": master_idx}, + { + "type": InterpolatableProblem.MISSING, + "master": name, + "master_idx": master_idx, + }, ) continue @@ -198,10 +190,10 @@ def test_gen( yield ( glyph_name, { + "type": InterpolatableProblem.OPEN_PATH, "master": name, "master_idx": master_idx, "contour": ix, - "type": "open_path", }, ) if has_open: @@ -230,7 +222,7 @@ def test_gen( yield ( glyph_name, { - "type": "path_count", + "type": InterpolatableProblem.PATH_COUNT, "master_1": names[m0idx], "master_2": names[m1idx], "master_1_idx": m0idx, @@ -249,7 +241,7 @@ def test_gen( yield ( glyph_name, { - "type": "node_count", + "type": InterpolatableProblem.NODE_COUNT, "path": pathIx, "master_1": names[m0idx], "master_2": names[m1idx], @@ -265,7 +257,7 @@ def test_gen( yield ( glyph_name, { - "type": "node_incompatibility", + "type": InterpolatableProblem.NODE_INCOMPATIBILITY, "path": pathIx, "node": nodeIx, "master_1": names[m0idx], @@ -279,21 +271,15 @@ def test_gen( continue # - # "contour_order" check + # InterpolatableProblem.CONTOUR_ORDER check # - matching, matching_cost, identity_cost = test_contour_order(glyph0, glyph1) - if matching_cost < identity_cost * tolerance: - log.debug( - "matching_ratio %g", - matching_cost / identity_cost, - ) - this_tolerance = matching_cost / identity_cost - log.debug("tolerance: %g", this_tolerance) + this_tolerance, matching = test_contour_order(glyph0, glyph1) + if this_tolerance < tolerance: yield ( glyph_name, { - "type": "contour_order", + "type": InterpolatableProblem.CONTOUR_ORDER, "master_1": names[m0idx], "master_2": names[m1idx], "master_1_idx": m0idx, @@ -306,19 +292,15 @@ def test_gen( matchings[m1idx] = matching # - # "wrong_start_point" / weight check + # wrong-start-point / weight check # m0Isomorphisms = glyph0.isomorphisms m1Isomorphisms = glyph1.isomorphisms m0Vectors = glyph0.greenVectors m1Vectors = glyph1.greenVectors - m0VectorsNormalized = glyph0.greenVectorsNormalized - m1VectorsNormalized = glyph1.greenVectorsNormalized recording0 = glyph0.recordings recording1 = glyph1.recordings - recording0Normalized = glyph0.recordingsNormalized - recording1Normalized = glyph1.recordingsNormalized # If contour-order is wrong, adjust it matching = matchings[m1idx] @@ -327,14 +309,14 @@ def test_gen( ): # m1 is empty for composite glyphs m1Isomorphisms = [m1Isomorphisms[i] for i in matching] m1Vectors = [m1Vectors[i] for i in matching] - m1VectorsNormalized = [m1VectorsNormalized[i] for i in matching] recording1 = [recording1[i] for i in matching] - recording1Normalized = [recording1Normalized[i] for i in matching] midRecording = [] for c0, c1 in zip(recording0, recording1): try: - midRecording.append(lerp_recordings(c0, c1)) + r = RecordingPen() + r.value = list(lerpRecordings(c0.value, c1.value)) + midRecording.append(r) except ValueError: # Mismatch because of the reordering above midRecording.append(None) @@ -352,118 +334,100 @@ def test_gen( # after reordering above. continue - proposed_point, reverse, min_cost, first_cost = test_starting_point( + this_tolerance, proposed_point, reverse = test_starting_point( glyph0, glyph1, ix, tolerance, matching ) - if proposed_point or reverse: - this_tolerance = min_cost / first_cost - log.debug("tolerance: %g", this_tolerance) - if min_cost < first_cost * tolerance: - yield ( - glyph_name, - { - "type": "wrong_start_point", - "contour": ix, - "master_1": names[m0idx], - "master_2": names[m1idx], - "master_1_idx": m0idx, - "master_2_idx": m1idx, - "value_1": 0, - "value_2": proposed_point, - "reversed": reverse, - "tolerance": this_tolerance, - }, - ) - else: - # Weight check. - # - # If contour could be mid-interpolated, and the two - # contours have the same area sign, proceeed. - # - # The sign difference can happen if it's a werido - # self-intersecting contour; ignore it. - contour = midRecording[ix] - - normalized = False - if contour and (m0Vectors[ix][0] < 0) == (m1Vectors[ix][0] < 0): - if normalized: - midStats = StatisticsPen(glyphset=None) - tpen = TransformPen( - midStats, transform_from_stats(midStats, inverse=True) - ) - contour.replay(tpen) - else: - midStats = StatisticsPen(glyphset=None) - contour.replay(midStats) - - midVector = contour_vector_from_stats(midStats) + if this_tolerance < tolerance: + yield ( + glyph_name, + { + "type": InterpolatableProblem.WRONG_START_POINT, + "contour": ix, + "master_1": names[m0idx], + "master_2": names[m1idx], + "master_1_idx": m0idx, + "master_2_idx": m1idx, + "value_1": 0, + "value_2": proposed_point, + "reversed": reverse, + "tolerance": this_tolerance, + }, + ) - m0Vec = ( - m0Vectors[ix] if not normalized else m0VectorsNormalized[ix] + # Weight check. + # + # If contour could be mid-interpolated, and the two + # contours have the same area sign, proceeed. + # + # The sign difference can happen if it's a weirdo + # self-intersecting contour; ignore it. + contour = midRecording[ix] + + if contour and (m0Vectors[ix][0] < 0) == (m1Vectors[ix][0] < 0): + midStats = StatisticsPen(glyphset=None) + contour.replay(midStats) + + midVector = contour_vector_from_stats(midStats) + + m0Vec = m0Vectors[ix] + m1Vec = m1Vectors[ix] + size0 = m0Vec[0] * m0Vec[0] + size1 = m1Vec[0] * m1Vec[0] + midSize = midVector[0] * midVector[0] + + power = 1 + t = tolerance**power + + for overweight, problem_type in enumerate( + ( + InterpolatableProblem.UNDERWEIGHT, + InterpolatableProblem.OVERWEIGHT, ) - m1Vec = ( - m1Vectors[ix] if not normalized else m1VectorsNormalized[ix] + ): + if overweight: + expectedSize = sqrt(size0 * size1) + expectedSize = (size0 + size1) - expectedSize + continue + else: + expectedSize = sqrt(size0 * size1) + + log.debug( + "%s: actual size %g; threshold size %g, master sizes: %g, %g", + problem_type, + midSize, + expectedSize, + size0, + size1, ) - size0 = m0Vec[0] * m0Vec[0] - size1 = m1Vec[0] * m1Vec[0] - midSize = midVector[0] * midVector[0] - - power = 1 - t = tolerance**power - - for overweight, problem_type in enumerate( - ("underweight", "overweight") - ): - if overweight: - expectedSize = sqrt(size0 * size1) - expectedSize = (size0 + size1) - expectedSize - expectedSize = size1 + (midSize - size1) - continue - else: - expectedSize = sqrt(size0 * size1) - - log.debug( - "%s: actual size %g; threshold size %g, master sizes: %g, %g", - problem_type, - midSize, - expectedSize, - size0, - size1, - ) - size0, size1 = sorted((size0, size1)) - - if ( - not overweight - and expectedSize * tolerance > midSize + 1e-5 - ) or ( - overweight and 1e-5 + expectedSize / tolerance < midSize - ): - try: - if overweight: - this_tolerance = (expectedSize / midSize) ** ( - 1 / power - ) - else: - this_tolerance = (midSize / expectedSize) ** ( - 1 / power - ) - except ZeroDivisionError: - this_tolerance = 0 - log.debug("tolerance %g", this_tolerance) - yield ( - glyph_name, - { - "type": problem_type, - "contour": ix, - "master_1": names[m0idx], - "master_2": names[m1idx], - "master_1_idx": m0idx, - "master_2_idx": m1idx, - "tolerance": this_tolerance, - }, - ) + if ( + not overweight and expectedSize * tolerance > midSize + 1e-5 + ) or (overweight and 1e-5 + expectedSize / tolerance < midSize): + try: + if overweight: + this_tolerance = (expectedSize / midSize) ** ( + 1 / power + ) + else: + this_tolerance = (midSize / expectedSize) ** ( + 1 / power + ) + except ZeroDivisionError: + this_tolerance = 0 + log.debug("tolerance %g", this_tolerance) + yield ( + glyph_name, + { + "type": problem_type, + "contour": ix, + "master_1": names[m0idx], + "master_2": names[m1idx], + "master_1_idx": m0idx, + "master_2_idx": m1idx, + "tolerance": this_tolerance, + }, + ) # # "kink" detector @@ -585,7 +549,7 @@ def test_gen( this_tolerance = t / (abs(sin_mid) * kinkiness) log.debug( - "deviation %g; deviation_ratio %g; sin_mid %g; r_diff %g", + "kink: deviation %g; deviation_ratio %g; sin_mid %g; r_diff %g", deviation, deviation_ratio, sin_mid, @@ -595,7 +559,7 @@ def test_gen( yield ( glyph_name, { - "type": "kink", + "type": InterpolatableProblem.KINK, "contour": ix, "master_1": names[m0idx], "master_2": names[m1idx], @@ -614,7 +578,7 @@ def test_gen( yield ( glyph_name, { - "type": "nothing", + "type": InterpolatableProblem.NOTHING, "master_1": names[m0idx], "master_2": names[m1idx], "master_1_idx": m0idx, @@ -640,6 +604,13 @@ def recursivelyAddGlyph(glyphname, glyphset, ttGlyphSet, glyf): recursivelyAddGlyph(component.glyphName, glyphset, ttGlyphSet, glyf) +def ensure_parent_dir(path): + dirname = os.path.dirname(path) + if dirname: + os.makedirs(dirname, exist_ok=True) + return path + + def main(args=None): """Test for interpolatability issues between fonts""" import argparse @@ -759,7 +730,7 @@ def main(args=None): for k, vv in axis_triples.items() } - elif args.inputs[0].endswith(".glyphs"): + elif args.inputs[0].endswith((".glyphs", ".glyphspackage")): from glyphsLib import GSFont, to_designspace gsfont = GSFont(args.inputs[0]) @@ -929,7 +900,11 @@ def main(args=None): ) problems = defaultdict(list) - f = sys.stdout if args.output is None else open(args.output, "w") + f = ( + sys.stdout + if args.output is None + else open(ensure_parent_dir(args.output), "w") + ) if not args.quiet: if args.json: @@ -963,16 +938,16 @@ def main(args=None): print(f" Masters: %s:" % ", ".join(master_names), file=f) last_master_idxs = master_idxs - if p["type"] == "missing": + if p["type"] == InterpolatableProblem.MISSING: print( " Glyph was missing in master %s" % p["master"], file=f ) - elif p["type"] == "open_path": + elif p["type"] == InterpolatableProblem.OPEN_PATH: print( " Glyph has an open path in master %s" % p["master"], file=f, ) - elif p["type"] == "path_count": + elif p["type"] == InterpolatableProblem.PATH_COUNT: print( " Path count differs: %i in %s, %i in %s" % ( @@ -983,7 +958,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "node_count": + elif p["type"] == InterpolatableProblem.NODE_COUNT: print( " Node count differs in path %i: %i in %s, %i in %s" % ( @@ -995,7 +970,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "node_incompatibility": + elif p["type"] == InterpolatableProblem.NODE_INCOMPATIBILITY: print( " Node %o incompatible in path %i: %s in %s, %s in %s" % ( @@ -1008,7 +983,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "contour_order": + elif p["type"] == InterpolatableProblem.CONTOUR_ORDER: print( " Contour order differs: %s in %s, %s in %s" % ( @@ -1019,7 +994,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "wrong_start_point": + elif p["type"] == InterpolatableProblem.WRONG_START_POINT: print( " Contour %d start point differs: %s in %s, %s in %s; reversed: %s" % ( @@ -1032,7 +1007,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "underweight": + elif p["type"] == InterpolatableProblem.UNDERWEIGHT: print( " Contour %d interpolation is underweight: %s, %s" % ( @@ -1042,7 +1017,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "overweight": + elif p["type"] == InterpolatableProblem.OVERWEIGHT: print( " Contour %d interpolation is overweight: %s, %s" % ( @@ -1052,7 +1027,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "kink": + elif p["type"] == InterpolatableProblem.KINK: print( " Contour %d has a kink at %s: %s, %s" % ( @@ -1063,7 +1038,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "nothing": + elif p["type"] == InterpolatableProblem.NOTHING: print( " Showing %s and %s" % ( @@ -1076,29 +1051,31 @@ def main(args=None): for glyphname, problem in problems_gen: problems[glyphname].append(problem) - if args.pdf: - log.info("Writing PDF to %s", args.pdf) - from .interpolatablePlot import InterpolatablePDF + problems = sort_problems(problems) - with InterpolatablePDF(args.pdf, glyphsets=glyphsets, names=names) as pdf: - pdf.add_title_page( - original_args_inputs, tolerance=tolerance, kinkiness=kinkiness - ) - pdf.add_problems(problems) - if not problems and not args.quiet: - pdf.draw_cupcake() + for p in "ps", "pdf": + arg = getattr(args, p) + if arg is None: + continue + log.info("Writing %s to %s", p.upper(), arg) + from .interpolatablePlot import InterpolatablePS, InterpolatablePDF - if args.ps: - log.info("Writing PS to %s", args.pdf) - from .interpolatablePlot import InterpolatablePS + PlotterClass = InterpolatablePS if p == "ps" else InterpolatablePDF - with InterpolatablePS(args.ps, glyphsets=glyphsets, names=names) as ps: - ps.add_title_page( + with PlotterClass( + ensure_parent_dir(arg), glyphsets=glyphsets, names=names + ) as doc: + doc.add_title_page( original_args_inputs, tolerance=tolerance, kinkiness=kinkiness ) - ps.add_problems(problems) + if problems: + doc.add_summary(problems) + doc.add_problems(problems) if not problems and not args.quiet: - ps.draw_cupcake() + doc.draw_cupcake() + if problems: + doc.add_index() + doc.add_table_of_contents() if args.html: log.info("Writing HTML to %s", args.html) @@ -1125,7 +1102,7 @@ def main(args=None): import base64 - with open(args.html, "wb") as f: + with open(ensure_parent_dir(args.html), "wb") as f: f.write(b"<!DOCTYPE html>\n") f.write( b'<html><body align="center" style="font-family: sans-serif; text-color: #222">\n' diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatableHelpers.py b/contrib/python/fonttools/fontTools/varLib/interpolatableHelpers.py index 513e5f7409..2a3540fff2 100644 --- a/contrib/python/fonttools/fontTools/varLib/interpolatableHelpers.py +++ b/contrib/python/fonttools/fontTools/varLib/interpolatableHelpers.py @@ -1,9 +1,11 @@ +from fontTools.ttLib.ttGlyphSet import LerpGlyphSet from fontTools.pens.basePen import AbstractPen, BasePen, DecomposingPen from fontTools.pens.pointPen import AbstractPointPen, SegmentToPointPen from fontTools.pens.recordingPen import RecordingPen, DecomposingRecordingPen from fontTools.misc.transform import Transform from collections import defaultdict, deque from math import sqrt, copysign, atan2, pi +from enum import Enum import itertools import logging @@ -11,6 +13,50 @@ import logging log = logging.getLogger("fontTools.varLib.interpolatable") +class InterpolatableProblem: + NOTHING = "nothing" + MISSING = "missing" + OPEN_PATH = "open_path" + PATH_COUNT = "path_count" + NODE_COUNT = "node_count" + NODE_INCOMPATIBILITY = "node_incompatibility" + CONTOUR_ORDER = "contour_order" + WRONG_START_POINT = "wrong_start_point" + KINK = "kink" + UNDERWEIGHT = "underweight" + OVERWEIGHT = "overweight" + + severity = { + MISSING: 1, + OPEN_PATH: 2, + PATH_COUNT: 3, + NODE_COUNT: 4, + NODE_INCOMPATIBILITY: 5, + CONTOUR_ORDER: 6, + WRONG_START_POINT: 7, + KINK: 8, + UNDERWEIGHT: 9, + OVERWEIGHT: 10, + NOTHING: 11, + } + + +def sort_problems(problems): + """Sort problems by severity, then by glyph name, then by problem message.""" + return dict( + sorted( + problems.items(), + key=lambda _: -min( + ( + (InterpolatableProblem.severity[p["type"]] + p.get("tolerance", 0)) + for p in _[1] + ), + ), + reverse=True, + ) + ) + + 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.""" @@ -332,52 +378,3 @@ def transform_from_stats(stats, inverse=False): trans = trans.translate(stats.meanX, stats.meanY) return trans - - -class LerpGlyphSet: - def __init__(self, glyphset1, glyphset2, factor=0.5): - self.glyphset1 = glyphset1 - self.glyphset2 = glyphset2 - self.factor = factor - - def __getitem__(self, glyphname): - return LerpGlyph(glyphname, self) - - -class LerpGlyph: - def __init__(self, glyphname, glyphset): - self.glyphset = glyphset - self.glyphname = glyphname - - def draw(self, pen): - recording1 = DecomposingRecordingPen(self.glyphset.glyphset1) - self.glyphset.glyphset1[self.glyphname].draw(recording1) - recording2 = DecomposingRecordingPen(self.glyphset.glyphset2) - self.glyphset.glyphset2[self.glyphname].draw(recording2) - - factor = self.glyphset.factor - for (op1, args1), (op2, args2) in zip(recording1.value, recording2.value): - if op1 != op2: - raise ValueError("Mismatching operations: %s, %s" % (op1, op2)) - mid_args = [ - (x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor) - for (x1, y1), (x2, y2) in zip(args1, args2) - ] - getattr(pen, op1)(*mid_args) - - -def lerp_recordings(recording1, recording2, factor=0.5): - pen = RecordingPen() - value = pen.value - for (op1, args1), (op2, args2) in zip(recording1.value, recording2.value): - if op1 != op2: - raise ValueError("Mismatched operations: %s, %s" % (op1, op2)) - if op1 == "addComponent": - mid_args = args1 # XXX Interpolate transformation? - else: - mid_args = [ - (x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor) - for (x1, y1), (x2, y2) in zip(args1, args2) - ] - value.append((op1, mid_args)) - return pen diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py b/contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py index eef4a47160..3c206c6ee2 100644 --- a/contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py +++ b/contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py @@ -1,4 +1,6 @@ +from .interpolatableHelpers import * from fontTools.ttLib import TTFont +from fontTools.ttLib.ttGlyphSet import LerpGlyphSet from fontTools.pens.recordingPen import ( RecordingPen, DecomposingRecordingPen, @@ -11,10 +13,9 @@ from fontTools.pens.pointPen import ( PointToSegmentPen, ReverseContourPointPen, ) -from fontTools.varLib.interpolatable import ( +from fontTools.varLib.interpolatableHelpers import ( PerContourOrComponentPen, SimpleRecordingPointPen, - LerpGlyphSet, ) from itertools import cycle from functools import wraps @@ -36,33 +37,34 @@ class OverridingDict(dict): class InterpolatablePlot: - width = 640 - height = 480 - pad = 16 - line_height = 36 + width = 8.5 * 72 + height = 11 * 72 + pad = 0.1 * 72 + title_font_size = 24 + font_size = 16 page_number = 1 head_color = (0.3, 0.3, 0.3) label_color = (0.2, 0.2, 0.2) border_color = (0.9, 0.9, 0.9) - border_width = 1 + border_width = 0.5 fill_color = (0.8, 0.8, 0.8) stroke_color = (0.1, 0.1, 0.1) - stroke_width = 2 + stroke_width = 1 oncurve_node_color = (0, 0.8, 0, 0.7) - oncurve_node_diameter = 10 + oncurve_node_diameter = 6 offcurve_node_color = (0, 0.5, 0, 0.7) - offcurve_node_diameter = 8 + offcurve_node_diameter = 4 handle_color = (0, 0.5, 0, 0.7) - handle_width = 1 + handle_width = 0.5 corrected_start_point_color = (0, 0.9, 0, 0.7) - corrected_start_point_size = 15 + corrected_start_point_size = 7 wrong_start_point_color = (1, 0, 0, 0.7) start_point_color = (0, 0, 1, 0.7) - start_arrow_length = 20 - kink_point_size = 10 + start_arrow_length = 9 + kink_point_size = 7 kink_point_color = (1, 0, 1, 0.7) - kink_circle_size = 25 - kink_circle_stroke_width = 1.5 + kink_circle_size = 15 + kink_circle_stroke_width = 1 kink_circle_color = (1, 0, 1, 0.7) contour_colors = ((1, 0, 0), (0, 0, 1), (0, 1, 0), (1, 1, 0), (1, 0, 1), (0, 1, 1)) contour_alpha = 0.5 @@ -113,62 +115,59 @@ class InterpolatablePlot: self.out = out self.glyphsets = glyphsets self.names = names or [repr(g) for g in glyphsets] + self.toc = {} for k, v in kwargs.items(): if not hasattr(self, k): raise TypeError("Unknown keyword argument: %s" % k) setattr(self, k, v) + self.panel_width = self.width / 2 - self.pad * 3 + self.panel_height = ( + self.height / 2 - self.pad * 6 - self.font_size * 2 - self.title_font_size + ) + def __enter__(self): return self def __exit__(self, type, value, traceback): pass - def set_size(self, width, height): - raise NotImplementedError - def show_page(self): self.page_number += 1 - def total_width(self): - return self.width * 2 + self.pad * 3 - - def total_height(self): - return ( - self.pad - + self.line_height - + self.pad - + self.line_height - + self.pad - + 2 * (self.height + self.pad * 2 + self.line_height) - + self.pad - ) - def add_title_page( self, files, *, show_tolerance=True, tolerance=None, kinkiness=None ): - self.set_size(self.total_width(), self.total_height()) - pad = self.pad - width = self.total_width() - 3 * self.pad - height = self.total_height() - 2 * self.pad + width = self.width - 3 * self.pad + height = self.height - 2 * self.pad x = y = pad - self.draw_label("Problem report for:", x=x, y=y, bold=True, width=width) - y += self.line_height + self.draw_label( + "Problem report for:", + x=x, + y=y, + bold=True, + width=width, + font_size=self.title_font_size, + ) + y += self.title_font_size import hashlib for file in files: base_file = os.path.basename(file) - y += self.line_height + y += self.font_size + self.pad self.draw_label(base_file, x=x, y=y, bold=True, width=width) - y += self.line_height + y += self.font_size + self.pad - h = hashlib.sha1(open(file, "rb").read()).hexdigest() - self.draw_label("sha1: %s" % h, x=x + pad, y=y, width=width) - y += self.line_height + try: + h = hashlib.sha1(open(file, "rb").read()).hexdigest() + self.draw_label("sha1: %s" % h, x=x + pad, y=y, width=width) + y += self.font_size + except IsADirectoryError: + pass if file.endswith(".ttf"): ttFont = TTFont(file) @@ -184,8 +183,8 @@ class InterpolatablePlot: self.draw_label( "%s: %s" % (what, n), x=x + pad, y=y, width=width ) - y += self.line_height - elif file.endswith(".glyphs"): + y += self.font_size + self.pad + elif file.endswith((".glyphs", ".glyphspackage")): from glyphsLib import GSFont f = GSFont(file) @@ -200,7 +199,7 @@ class InterpolatablePlot: y=y, width=width, ) - y += self.line_height + y += self.font_size + self.pad self.draw_legend( show_tolerance=show_tolerance, tolerance=tolerance, kinkiness=kinkiness @@ -211,8 +210,8 @@ class InterpolatablePlot: cr = cairo.Context(self.surface) x = self.pad - y = self.total_height() - self.pad - self.line_height * 2 - width = self.total_width() - 2 * self.pad + y = self.height - self.pad - self.font_size * 2 + width = self.width - 2 * self.pad xx = x + self.pad * 2 xxx = x + self.pad * 4 @@ -221,10 +220,10 @@ class InterpolatablePlot: self.draw_label( "Tolerance: badness; closer to zero the worse", x=xxx, y=y, width=width ) - y -= self.pad + self.line_height + y -= self.pad + self.font_size self.draw_label("Underweight contours", x=xxx, y=y, width=width) - cr.rectangle(xx - self.pad * 0.7, y, 1.5 * self.pad, self.line_height) + cr.rectangle(xx - self.pad * 0.7, y, 1.5 * self.pad, self.font_size) cr.set_source_rgb(*self.fill_color) cr.fill_preserve() if self.stroke_color: @@ -233,12 +232,12 @@ class InterpolatablePlot: cr.stroke_preserve() cr.set_source_rgba(*self.weight_issue_contour_color) cr.fill() - y -= self.pad + self.line_height + y -= self.pad + self.font_size self.draw_label( "Colored contours: contours with the wrong order", x=xxx, y=y, width=width ) - cr.rectangle(xx - self.pad * 0.7, y, 1.5 * self.pad, self.line_height) + cr.rectangle(xx - self.pad * 0.7, y, 1.5 * self.pad, self.font_size) if self.fill_color: cr.set_source_rgb(*self.fill_color) cr.fill_preserve() @@ -248,38 +247,38 @@ class InterpolatablePlot: cr.stroke_preserve() cr.set_source_rgba(*self.contour_colors[0], self.contour_alpha) cr.fill() - y -= self.pad + self.line_height + y -= self.pad + self.font_size self.draw_label("Kink artifact", x=xxx, y=y, width=width) self.draw_circle( cr, x=xx, - y=y + self.line_height * 0.5, + y=y + self.font_size * 0.5, diameter=self.kink_circle_size, stroke_width=self.kink_circle_stroke_width, color=self.kink_circle_color, ) - y -= self.pad + self.line_height + y -= self.pad + self.font_size self.draw_label("Point causing kink in the contour", x=xxx, y=y, width=width) self.draw_dot( cr, x=xx, - y=y + self.line_height * 0.5, + y=y + self.font_size * 0.5, diameter=self.kink_point_size, color=self.kink_point_color, ) - y -= self.pad + self.line_height + y -= self.pad + self.font_size self.draw_label("Suggested new contour start point", x=xxx, y=y, width=width) self.draw_dot( cr, x=xx, - y=y + self.line_height * 0.5, + y=y + self.font_size * 0.5, diameter=self.corrected_start_point_size, color=self.corrected_start_point_color, ) - y -= self.pad + self.line_height + y -= self.pad + self.font_size self.draw_label( "Contour start point in contours with wrong direction", @@ -290,10 +289,10 @@ class InterpolatablePlot: self.draw_arrow( cr, x=xx - self.start_arrow_length * 0.3, - y=y + self.line_height * 0.5, + y=y + self.font_size * 0.5, color=self.wrong_start_point_color, ) - y -= self.pad + self.line_height + y -= self.pad + self.font_size self.draw_label( "Contour start point when the first two points overlap", @@ -304,23 +303,23 @@ class InterpolatablePlot: self.draw_dot( cr, x=xx, - y=y + self.line_height * 0.5, + y=y + self.font_size * 0.5, diameter=self.corrected_start_point_size, color=self.start_point_color, ) - y -= self.pad + self.line_height + y -= self.pad + self.font_size self.draw_label("Contour start point and direction", x=xxx, y=y, width=width) self.draw_arrow( cr, x=xx - self.start_arrow_length * 0.3, - y=y + self.line_height * 0.5, + y=y + self.font_size * 0.5, color=self.start_point_color, ) - y -= self.pad + self.line_height + y -= self.pad + self.font_size self.draw_label("Legend:", x=x, y=y, width=width, bold=True) - y -= self.pad + self.line_height + y -= self.pad + self.font_size if kinkiness is not None: self.draw_label( @@ -329,7 +328,7 @@ class InterpolatablePlot: y=y, width=width, ) - y -= self.pad + self.line_height + y -= self.pad + self.font_size if tolerance is not None: self.draw_label( @@ -338,10 +337,87 @@ class InterpolatablePlot: y=y, width=width, ) - y -= self.pad + self.line_height + y -= self.pad + self.font_size self.draw_label("Parameters:", x=x, y=y, width=width, bold=True) - y -= self.pad + self.line_height + y -= self.pad + self.font_size + + def add_summary(self, problems): + pad = self.pad + width = self.width - 3 * self.pad + height = self.height - 2 * self.pad + x = y = pad + + self.draw_label( + "Summary of problems", + x=x, + y=y, + bold=True, + width=width, + font_size=self.title_font_size, + ) + y += self.title_font_size + + glyphs_per_problem = defaultdict(set) + for glyphname, problems in sorted(problems.items()): + for problem in problems: + glyphs_per_problem[problem["type"]].add(glyphname) + + if "nothing" in glyphs_per_problem: + del glyphs_per_problem["nothing"] + + for problem_type in sorted( + glyphs_per_problem, key=lambda x: InterpolatableProblem.severity[x] + ): + y += self.font_size + self.draw_label( + "%s: %d" % (problem_type, len(glyphs_per_problem[problem_type])), + x=x, + y=y, + width=width, + bold=True, + ) + y += self.font_size + + for glyphname in sorted(glyphs_per_problem[problem_type]): + if y + self.font_size > height: + self.show_page() + y = self.font_size + pad + self.draw_label(glyphname, x=x + 2 * pad, y=y, width=width - 2 * pad) + y += self.font_size + + self.show_page() + + def _add_listing(self, title, items): + pad = self.pad + width = self.width - 2 * self.pad + height = self.height - 2 * self.pad + x = y = pad + + self.draw_label( + title, x=x, y=y, bold=True, width=width, font_size=self.title_font_size + ) + y += self.title_font_size + self.pad + + last_glyphname = None + for page_no, (glyphname, problems) in items: + if glyphname == last_glyphname: + continue + last_glyphname = glyphname + if y + self.font_size > height: + self.show_page() + y = self.font_size + pad + self.draw_label(glyphname, x=x + 5 * pad, y=y, width=width - 2 * pad) + self.draw_label(str(page_no), x=x, y=y, width=4 * pad, align=1) + y += self.font_size + + self.show_page() + + def add_table_of_contents(self): + self._add_listing("Table of contents", sorted(self.toc.items())) + + def add_index(self): + self._add_listing("Index", sorted(self.toc.items(), key=lambda x: x[1][0])) def add_problems(self, problems, *, show_tolerance=True, show_page_number=True): for glyph, glyph_problems in problems.items(): @@ -383,6 +459,8 @@ class InterpolatablePlot: if type(problems) not in (list, tuple): problems = [problems] + self.toc[self.page_number] = (glyphname, problems) + problem_type = problems[0]["type"] problem_types = set(problem["type"] for problem in problems) if not all(pt == problem_type for pt in problem_types): @@ -397,14 +475,12 @@ class InterpolatablePlot: ) master_indices = [problems[0][k] for k in master_keys] - if problem_type == "missing": + if problem_type == InterpolatableProblem.MISSING: sample_glyph = next( i for i, m in enumerate(self.glyphsets) if m[glyphname] is not None ) master_indices.insert(0, sample_glyph) - self.set_size(self.total_width(), self.total_height()) - x = self.pad y = self.pad @@ -415,6 +491,7 @@ class InterpolatablePlot: color=self.head_color, align=0, bold=True, + font_size=self.title_font_size, ) tolerance = min(p.get("tolerance", 1) for p in problems) if tolerance < 1 and show_tolerance: @@ -422,29 +499,35 @@ class InterpolatablePlot: "tolerance: %.2f" % tolerance, x=x, y=y, - width=self.total_width() - 2 * self.pad, + width=self.width - 2 * self.pad, align=1, bold=True, ) - y += self.line_height + self.pad + y += self.title_font_size + self.pad self.draw_label( - problem_type, + "Problems: " + problem_type, x=x, y=y, - width=self.total_width() - 2 * self.pad, + width=self.width - 2 * self.pad, color=self.head_color, - align=0.5, bold=True, ) - y += self.line_height + self.pad + y += self.font_size + self.pad * 2 scales = [] for which, master_idx in enumerate(master_indices): glyphset = self.glyphsets[master_idx] name = self.names[master_idx] - self.draw_label(name, x=x, y=y, color=self.label_color, align=0.5) - y += self.line_height + self.pad + self.draw_label( + name, + x=x, + y=y, + color=self.label_color, + width=self.panel_width, + align=0.5, + ) + y += self.font_size + self.pad if glyphset[glyphname] is not None: scales.append( @@ -452,24 +535,24 @@ class InterpolatablePlot: ) else: self.draw_emoticon(self.shrug, x=x, y=y) - y += self.height + self.pad + y += self.panel_height + self.font_size + self.pad if any( pt in ( - "nothing", - "wrong_start_point", - "contour_order", - "kink", - "underweight", - "overweight", + InterpolatableProblem.NOTHING, + InterpolatableProblem.WRONG_START_POINT, + InterpolatableProblem.CONTOUR_ORDER, + InterpolatableProblem.KINK, + InterpolatableProblem.UNDERWEIGHT, + InterpolatableProblem.OVERWEIGHT, ) for pt in problem_types ): - x = self.pad + self.width + self.pad + x = self.pad + self.panel_width + self.pad y = self.pad - y += self.line_height + self.pad - y += self.line_height + self.pad + y += self.title_font_size + self.pad * 2 + y += self.font_size + self.pad glyphset1 = self.glyphsets[master_indices[0]] glyphset2 = self.glyphsets[master_indices[1]] @@ -477,9 +560,14 @@ class InterpolatablePlot: # Draw the mid-way of the two masters self.draw_label( - "midway interpolation", x=x, y=y, color=self.head_color, align=0.5 + "midway interpolation", + x=x, + y=y, + color=self.head_color, + width=self.panel_width, + align=0.5, ) - y += self.line_height + self.pad + y += self.font_size + self.pad midway_glyphset = LerpGlyphSet(glyphset1, glyphset2) self.draw_glyph( @@ -489,7 +577,12 @@ class InterpolatablePlot: + [ p for p in problems - if p["type"] in ("kink", "underweight", "overweight") + if p["type"] + in ( + InterpolatableProblem.KINK, + InterpolatableProblem.UNDERWEIGHT, + InterpolatableProblem.OVERWEIGHT, + ) ], None, x=x, @@ -497,21 +590,28 @@ class InterpolatablePlot: scale=min(scales), ) - y += self.height + self.pad + y += self.panel_height + self.font_size + self.pad if any( pt in ( - "wrong_start_point", - "contour_order", - "kink", + InterpolatableProblem.WRONG_START_POINT, + InterpolatableProblem.CONTOUR_ORDER, + InterpolatableProblem.KINK, ) for pt in problem_types ): # Draw the proposed fix - self.draw_label("proposed fix", x=x, y=y, color=self.head_color, align=0.5) - y += self.line_height + self.pad + self.draw_label( + "proposed fix", + x=x, + y=y, + color=self.head_color, + width=self.panel_width, + align=0.5, + ) + y += self.font_size + self.pad overriding1 = OverridingDict(glyphset1) overriding2 = OverridingDict(glyphset2) @@ -525,14 +625,14 @@ class InterpolatablePlot: glyphset2[glyphname].draw(perContourPen2) for problem in problems: - if problem["type"] == "contour_order": + if problem["type"] == InterpolatableProblem.CONTOUR_ORDER: fixed_contours = [ perContourPen2.value[i] for i in problems[0]["value_2"] ] perContourPen2.value = fixed_contours for problem in problems: - if problem["type"] == "wrong_start_point": + if problem["type"] == InterpolatableProblem.WRONG_START_POINT: # Save the wrong contours wrongContour1 = perContourPen1.value[problem["contour"]] wrongContour2 = perContourPen2.value[problem["contour"]] @@ -578,7 +678,7 @@ class InterpolatablePlot: for problem in problems: # If we have a kink, try to fix it. - if problem["type"] == "kink": + if problem["type"] == InterpolatableProblem.KINK: # Save the wrong contours wrongContour1 = perContourPen1.value[problem["contour"]] wrongContour2 = perContourPen2.value[problem["contour"]] @@ -669,15 +769,15 @@ class InterpolatablePlot: ) except ValueError: self.draw_emoticon(self.shrug, x=x, y=y) - y += self.height + self.pad + y += self.panel_height + self.pad else: emoticon = self.shrug - if "underweight" in problem_types: + if InterpolatableProblem.UNDERWEIGHT in problem_types: emoticon = self.underweight - elif "overweight" in problem_types: + elif InterpolatableProblem.OVERWEIGHT in problem_types: emoticon = self.overweight - elif "nothing" in problem_types: + elif InterpolatableProblem.NOTHING in problem_types: emoticon = self.yay self.draw_emoticon(emoticon, x=x, y=y) @@ -685,8 +785,8 @@ class InterpolatablePlot: self.draw_label( str(self.page_number), x=0, - y=self.total_height() - self.line_height, - width=self.total_width(), + y=self.height - self.font_size - self.pad, + width=self.width, color=self.head_color, align=0.5, ) @@ -702,20 +802,23 @@ class InterpolatablePlot: bold=False, width=None, height=None, + font_size=None, ): if width is None: width = self.width if height is None: height = self.height + if font_size is None: + font_size = self.font_size cr = cairo.Context(self.surface) cr.select_font_face( "@cairo:", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD if bold else cairo.FONT_WEIGHT_NORMAL, ) - cr.set_font_size(self.line_height) + cr.set_font_size(font_size) font_extents = cr.font_extents() - font_size = self.line_height * self.line_height / font_extents[2] + font_size = font_size * font_size / font_extents[2] cr.set_font_size(font_size) font_extents = cr.font_extents() @@ -762,14 +865,14 @@ class InterpolatablePlot: if glyph_width: if scale is None: - scale = self.width / glyph_width + scale = self.panel_width / glyph_width else: - scale = min(scale, self.height / glyph_height) + scale = min(scale, self.panel_height / glyph_height) if glyph_height: if scale is None: - scale = self.height / glyph_height + scale = self.panel_height / glyph_height else: - scale = min(scale, self.height / glyph_height) + scale = min(scale, self.panel_height / glyph_height) if scale is None: scale = 1 @@ -777,8 +880,8 @@ class InterpolatablePlot: cr.translate(x, y) # Center cr.translate( - (self.width - glyph_width * scale) / 2, - (self.height - glyph_height * scale) / 2, + (self.panel_width - glyph_width * scale) / 2, + (self.panel_height - glyph_height * scale) / 2, ) cr.scale(scale, -scale) cr.translate(-bounds[0], -bounds[3]) @@ -793,7 +896,7 @@ class InterpolatablePlot: pen = CairoPen(glyphset, cr) decomposedRecording.replay(pen) - if self.fill_color and problem_type != "open_path": + if self.fill_color and problem_type != InterpolatableProblem.OPEN_PATH: cr.set_source_rgb(*self.fill_color) cr.fill_preserve() @@ -804,11 +907,17 @@ class InterpolatablePlot: cr.new_path() - if "underweight" in problem_types or "overweight" in problem_types: + if ( + InterpolatableProblem.UNDERWEIGHT in problem_types + or InterpolatableProblem.OVERWEIGHT in problem_types + ): perContourPen = PerContourOrComponentPen(RecordingPen, glyphset=glyphset) recording.replay(perContourPen) for problem in problems: - if problem["type"] in ("underweight", "overweight"): + if problem["type"] in ( + InterpolatableProblem.UNDERWEIGHT, + InterpolatableProblem.OVERWEIGHT, + ): contour = perContourPen.value[problem["contour"]] contour.replay(CairoPen(glyphset, cr)) cr.set_source_rgba(*self.weight_issue_contour_color) @@ -817,9 +926,9 @@ class InterpolatablePlot: if any( t in problem_types for t in { - "nothing", - "node_count", - "node_incompatibility", + InterpolatableProblem.NOTHING, + InterpolatableProblem.NODE_COUNT, + InterpolatableProblem.NODE_INCOMPATIBILITY, } ): cr.set_line_cap(cairo.LINE_CAP_ROUND) @@ -873,7 +982,7 @@ class InterpolatablePlot: matching = None for problem in problems: - if problem["type"] == "contour_order": + if problem["type"] == InterpolatableProblem.CONTOUR_ORDER: matching = problem["value_2"] colors = cycle(self.contour_colors) perContourPen = PerContourOrComponentPen( @@ -889,7 +998,10 @@ class InterpolatablePlot: cr.fill() for problem in problems: - if problem["type"] in ("nothing", "wrong_start_point"): + if problem["type"] in ( + InterpolatableProblem.NOTHING, + InterpolatableProblem.WRONG_START_POINT, + ): idx = problem.get("contour") # Draw suggested point @@ -967,7 +1079,7 @@ class InterpolatablePlot: cr.restore() - if problem["type"] == "kink": + if problem["type"] == InterpolatableProblem.KINK: idx = problem.get("contour") perContourPen = PerContourOrComponentPen( RecordingPen, glyphset=glyphset @@ -1053,19 +1165,19 @@ class InterpolatablePlot: text = text.splitlines() cr = cairo.Context(self.surface) cr.set_source_rgb(*color) - cr.set_font_size(self.line_height) + cr.set_font_size(self.font_size) cr.select_font_face( "@cairo:monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL ) text_width = 0 text_height = 0 font_extents = cr.font_extents() - font_line_height = font_extents[2] + font_font_size = font_extents[2] font_ascent = font_extents[0] for line in text: extents = cr.text_extents(line) text_width = max(text_width, extents.x_advance) - text_height += font_line_height + text_height += font_font_size if not text_width: return cr.translate(x, y) @@ -1080,45 +1192,44 @@ class InterpolatablePlot: for line in text: cr.move_to(0, 0) cr.show_text(line) - cr.translate(0, font_line_height) + cr.translate(0, font_font_size) def draw_cupcake(self): - self.set_size(self.total_width(), self.total_height()) - self.draw_label( self.no_issues_label, x=self.pad, y=self.pad, color=self.no_issues_label_color, - width=self.total_width() - 2 * self.pad, + width=self.width - 2 * self.pad, align=0.5, bold=True, + font_size=self.title_font_size, ) self.draw_text( self.cupcake, x=self.pad, - y=self.pad + self.line_height, - width=self.total_width() - 2 * self.pad, - height=self.total_height() - 2 * self.pad - self.line_height, + y=self.pad + self.font_size, + width=self.width - 2 * self.pad, + height=self.height - 2 * self.pad - self.font_size, color=self.cupcake_color, ) def draw_emoticon(self, emoticon, x=0, y=0): - self.draw_text(emoticon, x=x, y=y, color=self.emoticon_color) + self.draw_text( + emoticon, + x=x, + y=y, + color=self.emoticon_color, + width=self.panel_width, + height=self.panel_height, + ) class InterpolatablePostscriptLike(InterpolatablePlot): - @wraps(InterpolatablePlot.__init__) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - def __exit__(self, type, value, traceback): self.surface.finish() - def set_size(self, width, height): - self.surface.set_size(width, height) - def show_page(self): super().show_page() self.surface.show_page() @@ -1141,24 +1252,18 @@ class InterpolatablePDF(InterpolatablePostscriptLike): class InterpolatableSVG(InterpolatablePlot): - @wraps(InterpolatablePlot.__init__) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - def __enter__(self): - self.surface = None + self.sink = BytesIO() + self.surface = cairo.SVGSurface(self.sink, self.width, self.height) return self def __exit__(self, type, value, traceback): if self.surface is not None: self.show_page() - def set_size(self, width, height): - self.sink = BytesIO() - self.surface = cairo.SVGSurface(self.sink, width, height) - def show_page(self): super().show_page() self.surface.finish() self.out.append(self.sink.getvalue()) - self.surface = None + self.sink = BytesIO() + self.surface = cairo.SVGSurface(self.sink, self.width, self.height) diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatableTestContourOrder.py b/contrib/python/fonttools/fontTools/varLib/interpolatableTestContourOrder.py index d089e43576..9edb1afcb5 100644 --- a/contrib/python/fonttools/fontTools/varLib/interpolatableTestContourOrder.py +++ b/contrib/python/fonttools/fontTools/varLib/interpolatableTestContourOrder.py @@ -1,4 +1,7 @@ from .interpolatableHelpers import * +import logging + +log = logging.getLogger("fontTools.varLib.interpolatable") def test_contour_order(glyph0, glyph1): @@ -71,4 +74,9 @@ def test_contour_order(glyph0, glyph1): matching_cost = matching_cost_green identity_cost = identity_cost_green - return matching, matching_cost, identity_cost + this_tolerance = matching_cost / identity_cost if identity_cost else 1 + log.debug( + "test-contour-order: tolerance %g", + this_tolerance, + ) + return this_tolerance, matching diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatableTestStartingPoint.py b/contrib/python/fonttools/fontTools/varLib/interpolatableTestStartingPoint.py index 9f742a14f5..e760006631 100644 --- a/contrib/python/fonttools/fontTools/varLib/interpolatableTestStartingPoint.py +++ b/contrib/python/fonttools/fontTools/varLib/interpolatableTestStartingPoint.py @@ -9,18 +9,15 @@ def test_starting_point(glyph0, glyph1, ix, tolerance, matching): m0Vectors = glyph0.greenVectors m1Vectors = [glyph1.greenVectors[i] for i in matching] - proposed_point = 0 - reverse = False - min_cost = first_cost = 1 - c0 = contour0[0] # Next few lines duplicated below. costs = [vdiff_hypot2_complex(c0[0], c1[0]) for c1 in contour1] min_cost_idx, min_cost = min(enumerate(costs), key=lambda x: x[1]) first_cost = costs[0] + proposed_point = contour1[min_cost_idx][1] + reverse = contour1[min_cost_idx][2] if min_cost < first_cost * tolerance: - this_tolerance = min_cost / first_cost # c0 is the first isomorphism of the m0 master # contour1 is list of all isomorphisms of the m1 master # @@ -37,8 +34,6 @@ def test_starting_point(glyph0, glyph1, ix, tolerance, matching): # closest point again. If it matches this time, let it # pass. - proposed_point = contour1[min_cost_idx][1] - reverse = contour1[min_cost_idx][2] num_points = len(glyph1.points[ix]) leeway = 3 if not reverse and ( @@ -102,4 +97,9 @@ def test_starting_point(glyph0, glyph1, ix, tolerance, matching): # proposed_point = 0 # new_contour1[min_cost_idx][1] pass - return proposed_point, reverse, min_cost, first_cost + this_tolerance = min_cost / first_cost if first_cost else 1 + log.debug( + "test-starting-point: tolerance %g", + this_tolerance, + ) + return this_tolerance, proposed_point, reverse diff --git a/contrib/python/fonttools/fontTools/varLib/merger.py b/contrib/python/fonttools/fontTools/varLib/merger.py index b2c34016b3..96029166a7 100644 --- a/contrib/python/fonttools/fontTools/varLib/merger.py +++ b/contrib/python/fonttools/fontTools/varLib/merger.py @@ -1059,7 +1059,7 @@ class InstancerMerger(AligningMerger): Merger.__init__(self, font) self.model = model self.location = location - self.scalars = model.getScalars(location) + self.masterScalars = model.getMasterScalars(location) @InstancerMerger.merger(ot.CaretValue) @@ -1067,8 +1067,10 @@ def merge(merger, self, lst): assert self.Format == 1 Coords = [a.Coordinate for a in lst] model = merger.model - scalars = merger.scalars - self.Coordinate = otRound(model.interpolateFromMastersAndScalars(Coords, scalars)) + masterScalars = merger.masterScalars + self.Coordinate = otRound( + model.interpolateFromValuesAndScalars(Coords, masterScalars) + ) @InstancerMerger.merger(ot.Anchor) @@ -1077,15 +1079,19 @@ def merge(merger, self, lst): XCoords = [a.XCoordinate for a in lst] YCoords = [a.YCoordinate for a in lst] model = merger.model - scalars = merger.scalars - self.XCoordinate = otRound(model.interpolateFromMastersAndScalars(XCoords, scalars)) - self.YCoordinate = otRound(model.interpolateFromMastersAndScalars(YCoords, scalars)) + masterScalars = merger.masterScalars + self.XCoordinate = otRound( + model.interpolateFromValuesAndScalars(XCoords, masterScalars) + ) + self.YCoordinate = otRound( + model.interpolateFromValuesAndScalars(YCoords, masterScalars) + ) @InstancerMerger.merger(otBase.ValueRecord) def merge(merger, self, lst): model = merger.model - scalars = merger.scalars + masterScalars = merger.masterScalars # TODO Handle differing valueformats for name, tableName in [ ("XAdvance", "XAdvDevice"), @@ -1097,7 +1103,9 @@ def merge(merger, self, lst): if hasattr(self, name): values = [getattr(a, name, 0) for a in lst] - value = otRound(model.interpolateFromMastersAndScalars(values, scalars)) + value = otRound( + model.interpolateFromValuesAndScalars(values, masterScalars) + ) setattr(self, name, value) diff --git a/contrib/python/fonttools/fontTools/varLib/models.py b/contrib/python/fonttools/fontTools/varLib/models.py index 33deabe043..59815316f8 100644 --- a/contrib/python/fonttools/fontTools/varLib/models.py +++ b/contrib/python/fonttools/fontTools/varLib/models.py @@ -271,6 +271,12 @@ class VariationModel(object): self._subModels = {} def getSubModel(self, items): + """Return a sub-model and the items that are not None. + + The sub-model is necessary for working with the subset + of items when some are None. + + The sub-model is cached.""" if None not in items: return self, items key = tuple(v is not None for v in items) @@ -465,6 +471,10 @@ class VariationModel(object): return model.getDeltas(items, round=round), model.supports def getScalars(self, loc): + """Return scalars for each delta, for the given location. + If interpolating many master-values at the same location, + this function allows speed up by fetching the scalars once + and using them with interpolateFromMastersAndScalars().""" return [ supportScalar( loc, support, extrapolate=self.extrapolate, axisRanges=self.axisRanges @@ -472,29 +482,65 @@ class VariationModel(object): for support in self.supports ] + def getMasterScalars(self, targetLocation): + """Return multipliers for each master, for the given location. + If interpolating many master-values at the same location, + this function allows speed up by fetching the scalars once + and using them with interpolateFromValuesAndScalars(). + + Note that the scalars used in interpolateFromMastersAndScalars(), + are *not* the same as the ones returned here. They are the result + of getScalars().""" + out = self.getScalars(targetLocation) + for i, weights in reversed(list(enumerate(self.deltaWeights))): + for j, weight in weights.items(): + out[j] -= out[i] * weight + + out = [out[self.mapping[i]] for i in range(len(out))] + return out + @staticmethod - def interpolateFromDeltasAndScalars(deltas, scalars): + def interpolateFromValuesAndScalars(values, scalars): + """Interpolate from values and scalars coefficients. + + If the values are master-values, then the scalars should be + fetched from getMasterScalars(). + + If the values are deltas, then the scalars should be fetched + from getScalars(); in which case this is the same as + interpolateFromDeltasAndScalars(). + """ v = None - assert len(deltas) == len(scalars) - for delta, scalar in zip(deltas, scalars): + assert len(values) == len(scalars) + for value, scalar in zip(values, scalars): if not scalar: continue - contribution = delta * scalar + contribution = value * scalar if v is None: v = contribution else: v += contribution return v + @staticmethod + def interpolateFromDeltasAndScalars(deltas, scalars): + """Interpolate from deltas and scalars fetched from getScalars().""" + return VariationModel.interpolateFromValuesAndScalars(deltas, scalars) + def interpolateFromDeltas(self, loc, deltas): + """Interpolate from deltas, at location loc.""" scalars = self.getScalars(loc) return self.interpolateFromDeltasAndScalars(deltas, scalars) def interpolateFromMasters(self, loc, masterValues, *, round=noRound): - deltas = self.getDeltas(masterValues, round=round) - return self.interpolateFromDeltas(loc, deltas) + """Interpolate from master-values, at location loc.""" + scalars = self.getMasterScalars(loc) + return self.interpolateFromValuesAndScalars(masterValues, scalars) def interpolateFromMastersAndScalars(self, masterValues, scalars, *, round=noRound): + """Interpolate from master-values, and scalars fetched from + getScalars(), which is useful when you want to interpolate + multiple master-values with the same location.""" deltas = self.getDeltas(masterValues, round=round) return self.interpolateFromDeltasAndScalars(deltas, scalars) |