You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			1270 lines
		
	
	
		
			43 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			1270 lines
		
	
	
		
			43 KiB
		
	
	
	
		
			Python
		
	
from .interpolatableHelpers import *
 | 
						|
from fontTools.ttLib import TTFont
 | 
						|
from fontTools.ttLib.ttGlyphSet import LerpGlyphSet
 | 
						|
from fontTools.pens.recordingPen import (
 | 
						|
    RecordingPen,
 | 
						|
    DecomposingRecordingPen,
 | 
						|
    RecordingPointPen,
 | 
						|
)
 | 
						|
from fontTools.pens.boundsPen import ControlBoundsPen
 | 
						|
from fontTools.pens.cairoPen import CairoPen
 | 
						|
from fontTools.pens.pointPen import (
 | 
						|
    SegmentToPointPen,
 | 
						|
    PointToSegmentPen,
 | 
						|
    ReverseContourPointPen,
 | 
						|
)
 | 
						|
from fontTools.varLib.interpolatableHelpers import (
 | 
						|
    PerContourOrComponentPen,
 | 
						|
    SimpleRecordingPointPen,
 | 
						|
)
 | 
						|
from itertools import cycle
 | 
						|
from functools import wraps
 | 
						|
from io import BytesIO
 | 
						|
import cairo
 | 
						|
import math
 | 
						|
import os
 | 
						|
import logging
 | 
						|
 | 
						|
log = logging.getLogger("fontTools.varLib.interpolatable")
 | 
						|
 | 
						|
 | 
						|
class OverridingDict(dict):
 | 
						|
    def __init__(self, parent_dict):
 | 
						|
        self.parent_dict = parent_dict
 | 
						|
 | 
						|
    def __missing__(self, key):
 | 
						|
        return self.parent_dict[key]
 | 
						|
 | 
						|
 | 
						|
class InterpolatablePlot:
 | 
						|
    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 = 0.5
 | 
						|
    fill_color = (0.8, 0.8, 0.8)
 | 
						|
    stroke_color = (0.1, 0.1, 0.1)
 | 
						|
    stroke_width = 1
 | 
						|
    oncurve_node_color = (0, 0.8, 0, 0.7)
 | 
						|
    oncurve_node_diameter = 6
 | 
						|
    offcurve_node_color = (0, 0.5, 0, 0.7)
 | 
						|
    offcurve_node_diameter = 4
 | 
						|
    handle_color = (0, 0.5, 0, 0.7)
 | 
						|
    handle_width = 0.5
 | 
						|
    corrected_start_point_color = (0, 0.9, 0, 0.7)
 | 
						|
    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 = 9
 | 
						|
    kink_point_size = 7
 | 
						|
    kink_point_color = (1, 0, 1, 0.7)
 | 
						|
    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
 | 
						|
    weight_issue_contour_color = (0, 0, 0, 0.4)
 | 
						|
    no_issues_label = "Your font's good! Have a cupcake..."
 | 
						|
    no_issues_label_color = (0, 0.5, 0)
 | 
						|
    cupcake_color = (0.3, 0, 0.3)
 | 
						|
    cupcake = r"""
 | 
						|
                          ,@.
 | 
						|
                        ,@.@@,.
 | 
						|
                  ,@@,.@@@.  @.@@@,.
 | 
						|
                ,@@. @@@.     @@. @@,.
 | 
						|
        ,@@@.@,.@.              @.  @@@@,.@.@@,.
 | 
						|
   ,@@.@.     @@.@@.            @,.    .@' @'  @@,
 | 
						|
 ,@@. @.          .@@.@@@.  @@'                  @,
 | 
						|
,@.  @@.                                          @,
 | 
						|
@.     @,@@,.     ,                             .@@,
 | 
						|
@,.       .@,@@,.         .@@,.  ,       .@@,  @, @,
 | 
						|
@.                             .@. @ @@,.    ,      @
 | 
						|
 @,.@@.     @,.      @@,.      @.           @,.    @'
 | 
						|
  @@||@,.  @'@,.       @@,.  @@ @,.        @'@@,  @'
 | 
						|
     \\@@@@'  @,.      @'@@@@'   @@,.   @@@' //@@@'
 | 
						|
      |||||||| @@,.  @@' |||||||  |@@@|@||  ||
 | 
						|
       \\\\\\\  ||@@@||  |||||||  |||||||  //
 | 
						|
        |||||||  ||||||  ||||||   ||||||  ||
 | 
						|
         \\\\\\  ||||||  ||||||  ||||||  //
 | 
						|
          ||||||  |||||  |||||   |||||  ||
 | 
						|
           \\\\\  |||||  |||||  |||||  //
 | 
						|
            |||||  ||||  |||||  ||||  ||
 | 
						|
             \\\\  ||||  ||||  ||||  //
 | 
						|
              ||||||||||||||||||||||||
 | 
						|
"""
 | 
						|
    emoticon_color = (0, 0.3, 0.3)
 | 
						|
    shrug = r"""\_(")_/"""
 | 
						|
    underweight = r"""
 | 
						|
 o
 | 
						|
/|\
 | 
						|
/ \
 | 
						|
"""
 | 
						|
    overweight = r"""
 | 
						|
 o
 | 
						|
/O\
 | 
						|
/ \
 | 
						|
"""
 | 
						|
    yay = r""" \o/ """
 | 
						|
 | 
						|
    def __init__(self, out, glyphsets, names=None, **kwargs):
 | 
						|
        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 show_page(self):
 | 
						|
        self.page_number += 1
 | 
						|
 | 
						|
    def add_title_page(
 | 
						|
        self, files, *, show_tolerance=True, tolerance=None, kinkiness=None
 | 
						|
    ):
 | 
						|
        pad = 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,
 | 
						|
            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.font_size + self.pad
 | 
						|
            self.draw_label(base_file, x=x, y=y, bold=True, width=width)
 | 
						|
            y += self.font_size + self.pad
 | 
						|
 | 
						|
            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)
 | 
						|
                name = ttFont["name"] if "name" in ttFont else None
 | 
						|
                if name:
 | 
						|
                    for what, nameIDs in (
 | 
						|
                        ("Family name", (21, 16, 1)),
 | 
						|
                        ("Version", (5,)),
 | 
						|
                    ):
 | 
						|
                        n = name.getFirstDebugName(nameIDs)
 | 
						|
                        if n is None:
 | 
						|
                            continue
 | 
						|
                        self.draw_label(
 | 
						|
                            "%s: %s" % (what, n), x=x + pad, y=y, width=width
 | 
						|
                        )
 | 
						|
                        y += self.font_size + self.pad
 | 
						|
            elif file.endswith((".glyphs", ".glyphspackage")):
 | 
						|
                from glyphsLib import GSFont
 | 
						|
 | 
						|
                f = GSFont(file)
 | 
						|
                for what, field in (
 | 
						|
                    ("Family name", "familyName"),
 | 
						|
                    ("VersionMajor", "versionMajor"),
 | 
						|
                    ("VersionMinor", "_versionMinor"),
 | 
						|
                ):
 | 
						|
                    self.draw_label(
 | 
						|
                        "%s: %s" % (what, getattr(f, field)),
 | 
						|
                        x=x + pad,
 | 
						|
                        y=y,
 | 
						|
                        width=width,
 | 
						|
                    )
 | 
						|
                    y += self.font_size + self.pad
 | 
						|
 | 
						|
        self.draw_legend(
 | 
						|
            show_tolerance=show_tolerance, tolerance=tolerance, kinkiness=kinkiness
 | 
						|
        )
 | 
						|
        self.show_page()
 | 
						|
 | 
						|
    def draw_legend(self, *, show_tolerance=True, tolerance=None, kinkiness=None):
 | 
						|
        cr = cairo.Context(self.surface)
 | 
						|
 | 
						|
        x = 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
 | 
						|
 | 
						|
        if show_tolerance:
 | 
						|
            self.draw_label(
 | 
						|
                "Tolerance: badness; closer to zero the worse", x=xxx, y=y, width=width
 | 
						|
            )
 | 
						|
            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.font_size)
 | 
						|
        cr.set_source_rgb(*self.fill_color)
 | 
						|
        cr.fill_preserve()
 | 
						|
        if self.stroke_color:
 | 
						|
            cr.set_source_rgb(*self.stroke_color)
 | 
						|
            cr.set_line_width(self.stroke_width)
 | 
						|
            cr.stroke_preserve()
 | 
						|
        cr.set_source_rgba(*self.weight_issue_contour_color)
 | 
						|
        cr.fill()
 | 
						|
        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.font_size)
 | 
						|
        if self.fill_color:
 | 
						|
            cr.set_source_rgb(*self.fill_color)
 | 
						|
            cr.fill_preserve()
 | 
						|
        if self.stroke_color:
 | 
						|
            cr.set_source_rgb(*self.stroke_color)
 | 
						|
            cr.set_line_width(self.stroke_width)
 | 
						|
            cr.stroke_preserve()
 | 
						|
        cr.set_source_rgba(*self.contour_colors[0], self.contour_alpha)
 | 
						|
        cr.fill()
 | 
						|
        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.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.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.font_size * 0.5,
 | 
						|
            diameter=self.kink_point_size,
 | 
						|
            color=self.kink_point_color,
 | 
						|
        )
 | 
						|
        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.font_size * 0.5,
 | 
						|
            diameter=self.corrected_start_point_size,
 | 
						|
            color=self.corrected_start_point_color,
 | 
						|
        )
 | 
						|
        y -= self.pad + self.font_size
 | 
						|
 | 
						|
        self.draw_label(
 | 
						|
            "Contour start point in contours with wrong direction",
 | 
						|
            x=xxx,
 | 
						|
            y=y,
 | 
						|
            width=width,
 | 
						|
        )
 | 
						|
        self.draw_arrow(
 | 
						|
            cr,
 | 
						|
            x=xx - self.start_arrow_length * 0.3,
 | 
						|
            y=y + self.font_size * 0.5,
 | 
						|
            color=self.wrong_start_point_color,
 | 
						|
        )
 | 
						|
        y -= self.pad + self.font_size
 | 
						|
 | 
						|
        self.draw_label(
 | 
						|
            "Contour start point when the first two points overlap",
 | 
						|
            x=xxx,
 | 
						|
            y=y,
 | 
						|
            width=width,
 | 
						|
        )
 | 
						|
        self.draw_dot(
 | 
						|
            cr,
 | 
						|
            x=xx,
 | 
						|
            y=y + self.font_size * 0.5,
 | 
						|
            diameter=self.corrected_start_point_size,
 | 
						|
            color=self.start_point_color,
 | 
						|
        )
 | 
						|
        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.font_size * 0.5,
 | 
						|
            color=self.start_point_color,
 | 
						|
        )
 | 
						|
        y -= self.pad + self.font_size
 | 
						|
 | 
						|
        self.draw_label("Legend:", x=x, y=y, width=width, bold=True)
 | 
						|
        y -= self.pad + self.font_size
 | 
						|
 | 
						|
        if kinkiness is not None:
 | 
						|
            self.draw_label(
 | 
						|
                "Kink-reporting aggressiveness: %g" % kinkiness,
 | 
						|
                x=xxx,
 | 
						|
                y=y,
 | 
						|
                width=width,
 | 
						|
            )
 | 
						|
            y -= self.pad + self.font_size
 | 
						|
 | 
						|
        if tolerance is not None:
 | 
						|
            self.draw_label(
 | 
						|
                "Error tolerance: %g" % tolerance,
 | 
						|
                x=xxx,
 | 
						|
                y=y,
 | 
						|
                width=width,
 | 
						|
            )
 | 
						|
            y -= self.pad + self.font_size
 | 
						|
 | 
						|
        self.draw_label("Parameters:", x=x, y=y, width=width, bold=True)
 | 
						|
        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():
 | 
						|
            last_masters = None
 | 
						|
            current_glyph_problems = []
 | 
						|
            for p in glyph_problems:
 | 
						|
                masters = (
 | 
						|
                    p["master_idx"]
 | 
						|
                    if "master_idx" in p
 | 
						|
                    else (p["master_1_idx"], p["master_2_idx"])
 | 
						|
                )
 | 
						|
                if masters == last_masters:
 | 
						|
                    current_glyph_problems.append(p)
 | 
						|
                    continue
 | 
						|
                # Flush
 | 
						|
                if current_glyph_problems:
 | 
						|
                    self.add_problem(
 | 
						|
                        glyph,
 | 
						|
                        current_glyph_problems,
 | 
						|
                        show_tolerance=show_tolerance,
 | 
						|
                        show_page_number=show_page_number,
 | 
						|
                    )
 | 
						|
                    self.show_page()
 | 
						|
                    current_glyph_problems = []
 | 
						|
                last_masters = masters
 | 
						|
                current_glyph_problems.append(p)
 | 
						|
            if current_glyph_problems:
 | 
						|
                self.add_problem(
 | 
						|
                    glyph,
 | 
						|
                    current_glyph_problems,
 | 
						|
                    show_tolerance=show_tolerance,
 | 
						|
                    show_page_number=show_page_number,
 | 
						|
                )
 | 
						|
                self.show_page()
 | 
						|
 | 
						|
    def add_problem(
 | 
						|
        self, glyphname, problems, *, show_tolerance=True, show_page_number=True
 | 
						|
    ):
 | 
						|
        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):
 | 
						|
            problem_type = ", ".join(sorted({problem["type"] for problem in problems}))
 | 
						|
 | 
						|
        log.info("Drawing %s: %s", glyphname, problem_type)
 | 
						|
 | 
						|
        master_keys = (
 | 
						|
            ("master_idx",)
 | 
						|
            if "master_idx" in problems[0]
 | 
						|
            else ("master_1_idx", "master_2_idx")
 | 
						|
        )
 | 
						|
        master_indices = [problems[0][k] for k in master_keys]
 | 
						|
 | 
						|
        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)
 | 
						|
 | 
						|
        x = self.pad
 | 
						|
        y = self.pad
 | 
						|
 | 
						|
        self.draw_label(
 | 
						|
            "Glyph name: " + glyphname,
 | 
						|
            x=x,
 | 
						|
            y=y,
 | 
						|
            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:
 | 
						|
            self.draw_label(
 | 
						|
                "tolerance: %.2f" % tolerance,
 | 
						|
                x=x,
 | 
						|
                y=y,
 | 
						|
                width=self.width - 2 * self.pad,
 | 
						|
                align=1,
 | 
						|
                bold=True,
 | 
						|
            )
 | 
						|
        y += self.title_font_size + self.pad
 | 
						|
        self.draw_label(
 | 
						|
            "Problems: " + problem_type,
 | 
						|
            x=x,
 | 
						|
            y=y,
 | 
						|
            width=self.width - 2 * self.pad,
 | 
						|
            color=self.head_color,
 | 
						|
            bold=True,
 | 
						|
        )
 | 
						|
        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,
 | 
						|
                width=self.panel_width,
 | 
						|
                align=0.5,
 | 
						|
            )
 | 
						|
            y += self.font_size + self.pad
 | 
						|
 | 
						|
            if glyphset[glyphname] is not None:
 | 
						|
                scales.append(
 | 
						|
                    self.draw_glyph(glyphset, glyphname, problems, which, x=x, y=y)
 | 
						|
                )
 | 
						|
            else:
 | 
						|
                self.draw_emoticon(self.shrug, x=x, y=y)
 | 
						|
            y += self.panel_height + self.font_size + self.pad
 | 
						|
 | 
						|
        if any(
 | 
						|
            pt
 | 
						|
            in (
 | 
						|
                InterpolatableProblem.NOTHING,
 | 
						|
                InterpolatableProblem.WRONG_START_POINT,
 | 
						|
                InterpolatableProblem.CONTOUR_ORDER,
 | 
						|
                InterpolatableProblem.KINK,
 | 
						|
                InterpolatableProblem.UNDERWEIGHT,
 | 
						|
                InterpolatableProblem.OVERWEIGHT,
 | 
						|
            )
 | 
						|
            for pt in problem_types
 | 
						|
        ):
 | 
						|
            x = self.pad + self.panel_width + self.pad
 | 
						|
            y = 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]]
 | 
						|
 | 
						|
            # Draw the mid-way of the two masters
 | 
						|
 | 
						|
            self.draw_label(
 | 
						|
                "midway interpolation",
 | 
						|
                x=x,
 | 
						|
                y=y,
 | 
						|
                color=self.head_color,
 | 
						|
                width=self.panel_width,
 | 
						|
                align=0.5,
 | 
						|
            )
 | 
						|
            y += self.font_size + self.pad
 | 
						|
 | 
						|
            midway_glyphset = LerpGlyphSet(glyphset1, glyphset2)
 | 
						|
            self.draw_glyph(
 | 
						|
                midway_glyphset,
 | 
						|
                glyphname,
 | 
						|
                [{"type": "midway"}]
 | 
						|
                + [
 | 
						|
                    p
 | 
						|
                    for p in problems
 | 
						|
                    if p["type"]
 | 
						|
                    in (
 | 
						|
                        InterpolatableProblem.KINK,
 | 
						|
                        InterpolatableProblem.UNDERWEIGHT,
 | 
						|
                        InterpolatableProblem.OVERWEIGHT,
 | 
						|
                    )
 | 
						|
                ],
 | 
						|
                None,
 | 
						|
                x=x,
 | 
						|
                y=y,
 | 
						|
                scale=min(scales),
 | 
						|
            )
 | 
						|
 | 
						|
            y += self.panel_height + self.font_size + self.pad
 | 
						|
 | 
						|
        if any(
 | 
						|
            pt
 | 
						|
            in (
 | 
						|
                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,
 | 
						|
                width=self.panel_width,
 | 
						|
                align=0.5,
 | 
						|
            )
 | 
						|
            y += self.font_size + self.pad
 | 
						|
 | 
						|
            overriding1 = OverridingDict(glyphset1)
 | 
						|
            overriding2 = OverridingDict(glyphset2)
 | 
						|
            perContourPen1 = PerContourOrComponentPen(
 | 
						|
                RecordingPen, glyphset=overriding1
 | 
						|
            )
 | 
						|
            perContourPen2 = PerContourOrComponentPen(
 | 
						|
                RecordingPen, glyphset=overriding2
 | 
						|
            )
 | 
						|
            glyphset1[glyphname].draw(perContourPen1)
 | 
						|
            glyphset2[glyphname].draw(perContourPen2)
 | 
						|
 | 
						|
            for problem in problems:
 | 
						|
                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"] == InterpolatableProblem.WRONG_START_POINT:
 | 
						|
                    # Save the wrong contours
 | 
						|
                    wrongContour1 = perContourPen1.value[problem["contour"]]
 | 
						|
                    wrongContour2 = perContourPen2.value[problem["contour"]]
 | 
						|
 | 
						|
                    # Convert the wrong contours to point pens
 | 
						|
                    points1 = RecordingPointPen()
 | 
						|
                    converter = SegmentToPointPen(points1, False)
 | 
						|
                    wrongContour1.replay(converter)
 | 
						|
                    points2 = RecordingPointPen()
 | 
						|
                    converter = SegmentToPointPen(points2, False)
 | 
						|
                    wrongContour2.replay(converter)
 | 
						|
 | 
						|
                    proposed_start = problem["value_2"]
 | 
						|
 | 
						|
                    # See if we need reversing; fragile but worth a try
 | 
						|
                    if problem["reversed"]:
 | 
						|
                        new_points2 = RecordingPointPen()
 | 
						|
                        reversedPen = ReverseContourPointPen(new_points2)
 | 
						|
                        points2.replay(reversedPen)
 | 
						|
                        points2 = new_points2
 | 
						|
                        proposed_start = len(points2.value) - 2 - proposed_start
 | 
						|
 | 
						|
                    # Rotate points2 so that the first point is the same as in points1
 | 
						|
                    beginPath = points2.value[:1]
 | 
						|
                    endPath = points2.value[-1:]
 | 
						|
                    pts = points2.value[1:-1]
 | 
						|
                    pts = pts[proposed_start:] + pts[:proposed_start]
 | 
						|
                    points2.value = beginPath + pts + endPath
 | 
						|
 | 
						|
                    # Convert the point pens back to segment pens
 | 
						|
                    segment1 = RecordingPen()
 | 
						|
                    converter = PointToSegmentPen(segment1, True)
 | 
						|
                    points1.replay(converter)
 | 
						|
                    segment2 = RecordingPen()
 | 
						|
                    converter = PointToSegmentPen(segment2, True)
 | 
						|
                    points2.replay(converter)
 | 
						|
 | 
						|
                    # Replace the wrong contours
 | 
						|
                    wrongContour1.value = segment1.value
 | 
						|
                    wrongContour2.value = segment2.value
 | 
						|
                    perContourPen1.value[problem["contour"]] = wrongContour1
 | 
						|
                    perContourPen2.value[problem["contour"]] = wrongContour2
 | 
						|
 | 
						|
            for problem in problems:
 | 
						|
                # If we have a kink, try to fix it.
 | 
						|
                if problem["type"] == InterpolatableProblem.KINK:
 | 
						|
                    # Save the wrong contours
 | 
						|
                    wrongContour1 = perContourPen1.value[problem["contour"]]
 | 
						|
                    wrongContour2 = perContourPen2.value[problem["contour"]]
 | 
						|
 | 
						|
                    # Convert the wrong contours to point pens
 | 
						|
                    points1 = RecordingPointPen()
 | 
						|
                    converter = SegmentToPointPen(points1, False)
 | 
						|
                    wrongContour1.replay(converter)
 | 
						|
                    points2 = RecordingPointPen()
 | 
						|
                    converter = SegmentToPointPen(points2, False)
 | 
						|
                    wrongContour2.replay(converter)
 | 
						|
 | 
						|
                    i = problem["value"]
 | 
						|
 | 
						|
                    # Position points to be around the same ratio
 | 
						|
                    # beginPath / endPath dance
 | 
						|
                    j = i + 1
 | 
						|
                    pt0 = points1.value[j][1][0]
 | 
						|
                    pt1 = points2.value[j][1][0]
 | 
						|
                    j_prev = (i - 1) % (len(points1.value) - 2) + 1
 | 
						|
                    pt0_prev = points1.value[j_prev][1][0]
 | 
						|
                    pt1_prev = points2.value[j_prev][1][0]
 | 
						|
                    j_next = (i + 1) % (len(points1.value) - 2) + 1
 | 
						|
                    pt0_next = points1.value[j_next][1][0]
 | 
						|
                    pt1_next = points2.value[j_next][1][0]
 | 
						|
 | 
						|
                    pt0 = complex(*pt0)
 | 
						|
                    pt1 = complex(*pt1)
 | 
						|
                    pt0_prev = complex(*pt0_prev)
 | 
						|
                    pt1_prev = complex(*pt1_prev)
 | 
						|
                    pt0_next = complex(*pt0_next)
 | 
						|
                    pt1_next = complex(*pt1_next)
 | 
						|
 | 
						|
                    # Find the ratio of the distance between the points
 | 
						|
                    r0 = abs(pt0 - pt0_prev) / abs(pt0_next - pt0_prev)
 | 
						|
                    r1 = abs(pt1 - pt1_prev) / abs(pt1_next - pt1_prev)
 | 
						|
                    r_mid = (r0 + r1) / 2
 | 
						|
 | 
						|
                    pt0 = pt0_prev + r_mid * (pt0_next - pt0_prev)
 | 
						|
                    pt1 = pt1_prev + r_mid * (pt1_next - pt1_prev)
 | 
						|
 | 
						|
                    points1.value[j] = (
 | 
						|
                        points1.value[j][0],
 | 
						|
                        (((pt0.real, pt0.imag),) + points1.value[j][1][1:]),
 | 
						|
                        points1.value[j][2],
 | 
						|
                    )
 | 
						|
                    points2.value[j] = (
 | 
						|
                        points2.value[j][0],
 | 
						|
                        (((pt1.real, pt1.imag),) + points2.value[j][1][1:]),
 | 
						|
                        points2.value[j][2],
 | 
						|
                    )
 | 
						|
 | 
						|
                    # Convert the point pens back to segment pens
 | 
						|
                    segment1 = RecordingPen()
 | 
						|
                    converter = PointToSegmentPen(segment1, True)
 | 
						|
                    points1.replay(converter)
 | 
						|
                    segment2 = RecordingPen()
 | 
						|
                    converter = PointToSegmentPen(segment2, True)
 | 
						|
                    points2.replay(converter)
 | 
						|
 | 
						|
                    # Replace the wrong contours
 | 
						|
                    wrongContour1.value = segment1.value
 | 
						|
                    wrongContour2.value = segment2.value
 | 
						|
 | 
						|
            # Assemble
 | 
						|
            fixed1 = RecordingPen()
 | 
						|
            fixed2 = RecordingPen()
 | 
						|
            for contour in perContourPen1.value:
 | 
						|
                fixed1.value.extend(contour.value)
 | 
						|
            for contour in perContourPen2.value:
 | 
						|
                fixed2.value.extend(contour.value)
 | 
						|
            fixed1.draw = fixed1.replay
 | 
						|
            fixed2.draw = fixed2.replay
 | 
						|
 | 
						|
            overriding1[glyphname] = fixed1
 | 
						|
            overriding2[glyphname] = fixed2
 | 
						|
 | 
						|
            try:
 | 
						|
                midway_glyphset = LerpGlyphSet(overriding1, overriding2)
 | 
						|
                self.draw_glyph(
 | 
						|
                    midway_glyphset,
 | 
						|
                    glyphname,
 | 
						|
                    {"type": "fixed"},
 | 
						|
                    None,
 | 
						|
                    x=x,
 | 
						|
                    y=y,
 | 
						|
                    scale=min(scales),
 | 
						|
                )
 | 
						|
            except ValueError:
 | 
						|
                self.draw_emoticon(self.shrug, x=x, y=y)
 | 
						|
            y += self.panel_height + self.pad
 | 
						|
 | 
						|
        else:
 | 
						|
            emoticon = self.shrug
 | 
						|
            if InterpolatableProblem.UNDERWEIGHT in problem_types:
 | 
						|
                emoticon = self.underweight
 | 
						|
            elif InterpolatableProblem.OVERWEIGHT in problem_types:
 | 
						|
                emoticon = self.overweight
 | 
						|
            elif InterpolatableProblem.NOTHING in problem_types:
 | 
						|
                emoticon = self.yay
 | 
						|
            self.draw_emoticon(emoticon, x=x, y=y)
 | 
						|
 | 
						|
        if show_page_number:
 | 
						|
            self.draw_label(
 | 
						|
                str(self.page_number),
 | 
						|
                x=0,
 | 
						|
                y=self.height - self.font_size - self.pad,
 | 
						|
                width=self.width,
 | 
						|
                color=self.head_color,
 | 
						|
                align=0.5,
 | 
						|
            )
 | 
						|
 | 
						|
    def draw_label(
 | 
						|
        self,
 | 
						|
        label,
 | 
						|
        *,
 | 
						|
        x=0,
 | 
						|
        y=0,
 | 
						|
        color=(0, 0, 0),
 | 
						|
        align=0,
 | 
						|
        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(font_size)
 | 
						|
        font_extents = cr.font_extents()
 | 
						|
        font_size = font_size * font_size / font_extents[2]
 | 
						|
        cr.set_font_size(font_size)
 | 
						|
        font_extents = cr.font_extents()
 | 
						|
 | 
						|
        cr.set_source_rgb(*color)
 | 
						|
 | 
						|
        extents = cr.text_extents(label)
 | 
						|
        if extents.width > width:
 | 
						|
            # Shrink
 | 
						|
            font_size *= width / extents.width
 | 
						|
            cr.set_font_size(font_size)
 | 
						|
            font_extents = cr.font_extents()
 | 
						|
            extents = cr.text_extents(label)
 | 
						|
 | 
						|
        # Center
 | 
						|
        label_x = x + (width - extents.width) * align
 | 
						|
        label_y = y + font_extents[0]
 | 
						|
        cr.move_to(label_x, label_y)
 | 
						|
        cr.show_text(label)
 | 
						|
 | 
						|
    def draw_glyph(self, glyphset, glyphname, problems, which, *, x=0, y=0, scale=None):
 | 
						|
        if type(problems) not in (list, tuple):
 | 
						|
            problems = [problems]
 | 
						|
 | 
						|
        midway = any(problem["type"] == "midway" for problem in 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):
 | 
						|
            problem_type = "mixed"
 | 
						|
        glyph = glyphset[glyphname]
 | 
						|
 | 
						|
        recording = RecordingPen()
 | 
						|
        glyph.draw(recording)
 | 
						|
        decomposedRecording = DecomposingRecordingPen(glyphset)
 | 
						|
        glyph.draw(decomposedRecording)
 | 
						|
 | 
						|
        boundsPen = ControlBoundsPen(glyphset)
 | 
						|
        decomposedRecording.replay(boundsPen)
 | 
						|
        bounds = boundsPen.bounds
 | 
						|
        if bounds is None:
 | 
						|
            bounds = (0, 0, 0, 0)
 | 
						|
 | 
						|
        glyph_width = bounds[2] - bounds[0]
 | 
						|
        glyph_height = bounds[3] - bounds[1]
 | 
						|
 | 
						|
        if glyph_width:
 | 
						|
            if scale is None:
 | 
						|
                scale = self.panel_width / glyph_width
 | 
						|
            else:
 | 
						|
                scale = min(scale, self.panel_height / glyph_height)
 | 
						|
        if glyph_height:
 | 
						|
            if scale is None:
 | 
						|
                scale = self.panel_height / glyph_height
 | 
						|
            else:
 | 
						|
                scale = min(scale, self.panel_height / glyph_height)
 | 
						|
        if scale is None:
 | 
						|
            scale = 1
 | 
						|
 | 
						|
        cr = cairo.Context(self.surface)
 | 
						|
        cr.translate(x, y)
 | 
						|
        # Center
 | 
						|
        cr.translate(
 | 
						|
            (self.panel_width - glyph_width * scale) / 2,
 | 
						|
            (self.panel_height - glyph_height * scale) / 2,
 | 
						|
        )
 | 
						|
        cr.scale(scale, -scale)
 | 
						|
        cr.translate(-bounds[0], -bounds[3])
 | 
						|
 | 
						|
        if self.border_color:
 | 
						|
            cr.set_source_rgb(*self.border_color)
 | 
						|
            cr.rectangle(bounds[0], bounds[1], glyph_width, glyph_height)
 | 
						|
            cr.set_line_width(self.border_width / scale)
 | 
						|
            cr.stroke()
 | 
						|
 | 
						|
        if self.fill_color or self.stroke_color:
 | 
						|
            pen = CairoPen(glyphset, cr)
 | 
						|
            decomposedRecording.replay(pen)
 | 
						|
 | 
						|
            if self.fill_color and problem_type != InterpolatableProblem.OPEN_PATH:
 | 
						|
                cr.set_source_rgb(*self.fill_color)
 | 
						|
                cr.fill_preserve()
 | 
						|
 | 
						|
            if self.stroke_color:
 | 
						|
                cr.set_source_rgb(*self.stroke_color)
 | 
						|
                cr.set_line_width(self.stroke_width / scale)
 | 
						|
                cr.stroke_preserve()
 | 
						|
 | 
						|
            cr.new_path()
 | 
						|
 | 
						|
        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 (
 | 
						|
                    InterpolatableProblem.UNDERWEIGHT,
 | 
						|
                    InterpolatableProblem.OVERWEIGHT,
 | 
						|
                ):
 | 
						|
                    contour = perContourPen.value[problem["contour"]]
 | 
						|
                    contour.replay(CairoPen(glyphset, cr))
 | 
						|
                    cr.set_source_rgba(*self.weight_issue_contour_color)
 | 
						|
                    cr.fill()
 | 
						|
 | 
						|
        if any(
 | 
						|
            t in problem_types
 | 
						|
            for t in {
 | 
						|
                InterpolatableProblem.NOTHING,
 | 
						|
                InterpolatableProblem.NODE_COUNT,
 | 
						|
                InterpolatableProblem.NODE_INCOMPATIBILITY,
 | 
						|
            }
 | 
						|
        ):
 | 
						|
            cr.set_line_cap(cairo.LINE_CAP_ROUND)
 | 
						|
 | 
						|
            # Oncurve nodes
 | 
						|
            for segment, args in decomposedRecording.value:
 | 
						|
                if not args:
 | 
						|
                    continue
 | 
						|
                x, y = args[-1]
 | 
						|
                cr.move_to(x, y)
 | 
						|
                cr.line_to(x, y)
 | 
						|
            cr.set_source_rgba(*self.oncurve_node_color)
 | 
						|
            cr.set_line_width(self.oncurve_node_diameter / scale)
 | 
						|
            cr.stroke()
 | 
						|
 | 
						|
            # Offcurve nodes
 | 
						|
            for segment, args in decomposedRecording.value:
 | 
						|
                if not args:
 | 
						|
                    continue
 | 
						|
                for x, y in args[:-1]:
 | 
						|
                    cr.move_to(x, y)
 | 
						|
                    cr.line_to(x, y)
 | 
						|
            cr.set_source_rgba(*self.offcurve_node_color)
 | 
						|
            cr.set_line_width(self.offcurve_node_diameter / scale)
 | 
						|
            cr.stroke()
 | 
						|
 | 
						|
            # Handles
 | 
						|
            for segment, args in decomposedRecording.value:
 | 
						|
                if not args:
 | 
						|
                    pass
 | 
						|
                elif segment in ("moveTo", "lineTo"):
 | 
						|
                    cr.move_to(*args[0])
 | 
						|
                elif segment == "qCurveTo":
 | 
						|
                    for x, y in args:
 | 
						|
                        cr.line_to(x, y)
 | 
						|
                    cr.new_sub_path()
 | 
						|
                    cr.move_to(*args[-1])
 | 
						|
                elif segment == "curveTo":
 | 
						|
                    cr.line_to(*args[0])
 | 
						|
                    cr.new_sub_path()
 | 
						|
                    cr.move_to(*args[1])
 | 
						|
                    cr.line_to(*args[2])
 | 
						|
                    cr.new_sub_path()
 | 
						|
                    cr.move_to(*args[-1])
 | 
						|
                else:
 | 
						|
                    continue
 | 
						|
 | 
						|
            cr.set_source_rgba(*self.handle_color)
 | 
						|
            cr.set_line_width(self.handle_width / scale)
 | 
						|
            cr.stroke()
 | 
						|
 | 
						|
        matching = None
 | 
						|
        for problem in problems:
 | 
						|
            if problem["type"] == InterpolatableProblem.CONTOUR_ORDER:
 | 
						|
                matching = problem["value_2"]
 | 
						|
                colors = cycle(self.contour_colors)
 | 
						|
                perContourPen = PerContourOrComponentPen(
 | 
						|
                    RecordingPen, glyphset=glyphset
 | 
						|
                )
 | 
						|
                recording.replay(perContourPen)
 | 
						|
                for i, contour in enumerate(perContourPen.value):
 | 
						|
                    if matching[i] == i:
 | 
						|
                        continue
 | 
						|
                    color = next(colors)
 | 
						|
                    contour.replay(CairoPen(glyphset, cr))
 | 
						|
                    cr.set_source_rgba(*color, self.contour_alpha)
 | 
						|
                    cr.fill()
 | 
						|
 | 
						|
        for problem in problems:
 | 
						|
            if problem["type"] in (
 | 
						|
                InterpolatableProblem.NOTHING,
 | 
						|
                InterpolatableProblem.WRONG_START_POINT,
 | 
						|
            ):
 | 
						|
                idx = problem.get("contour")
 | 
						|
 | 
						|
                # Draw suggested point
 | 
						|
                if idx is not None and which == 1 and "value_2" in problem:
 | 
						|
                    perContourPen = PerContourOrComponentPen(
 | 
						|
                        RecordingPen, glyphset=glyphset
 | 
						|
                    )
 | 
						|
                    decomposedRecording.replay(perContourPen)
 | 
						|
                    points = SimpleRecordingPointPen()
 | 
						|
                    converter = SegmentToPointPen(points, False)
 | 
						|
                    perContourPen.value[
 | 
						|
                        idx if matching is None else matching[idx]
 | 
						|
                    ].replay(converter)
 | 
						|
                    targetPoint = points.value[problem["value_2"]][0]
 | 
						|
                    cr.save()
 | 
						|
                    cr.translate(*targetPoint)
 | 
						|
                    cr.scale(1 / scale, 1 / scale)
 | 
						|
                    self.draw_dot(
 | 
						|
                        cr,
 | 
						|
                        diameter=self.corrected_start_point_size,
 | 
						|
                        color=self.corrected_start_point_color,
 | 
						|
                    )
 | 
						|
                    cr.restore()
 | 
						|
 | 
						|
                # Draw start-point arrow
 | 
						|
                if which == 0 or not problem.get("reversed"):
 | 
						|
                    color = self.start_point_color
 | 
						|
                else:
 | 
						|
                    color = self.wrong_start_point_color
 | 
						|
                first_pt = None
 | 
						|
                i = 0
 | 
						|
                cr.save()
 | 
						|
                for segment, args in decomposedRecording.value:
 | 
						|
                    if segment == "moveTo":
 | 
						|
                        first_pt = args[0]
 | 
						|
                        continue
 | 
						|
                    if first_pt is None:
 | 
						|
                        continue
 | 
						|
                    if segment == "closePath":
 | 
						|
                        second_pt = first_pt
 | 
						|
                    else:
 | 
						|
                        second_pt = args[0]
 | 
						|
 | 
						|
                    if idx is None or i == idx:
 | 
						|
                        cr.save()
 | 
						|
                        first_pt = complex(*first_pt)
 | 
						|
                        second_pt = complex(*second_pt)
 | 
						|
                        length = abs(second_pt - first_pt)
 | 
						|
                        cr.translate(first_pt.real, first_pt.imag)
 | 
						|
                        if length:
 | 
						|
                            # Draw arrowhead
 | 
						|
                            cr.rotate(
 | 
						|
                                math.atan2(
 | 
						|
                                    second_pt.imag - first_pt.imag,
 | 
						|
                                    second_pt.real - first_pt.real,
 | 
						|
                                )
 | 
						|
                            )
 | 
						|
                            cr.scale(1 / scale, 1 / scale)
 | 
						|
                            self.draw_arrow(cr, color=color)
 | 
						|
                        else:
 | 
						|
                            # Draw circle
 | 
						|
                            cr.scale(1 / scale, 1 / scale)
 | 
						|
                            self.draw_dot(
 | 
						|
                                cr,
 | 
						|
                                diameter=self.corrected_start_point_size,
 | 
						|
                                color=color,
 | 
						|
                            )
 | 
						|
                        cr.restore()
 | 
						|
 | 
						|
                        if idx is not None:
 | 
						|
                            break
 | 
						|
 | 
						|
                    first_pt = None
 | 
						|
                    i += 1
 | 
						|
 | 
						|
                cr.restore()
 | 
						|
 | 
						|
            if problem["type"] == InterpolatableProblem.KINK:
 | 
						|
                idx = problem.get("contour")
 | 
						|
                perContourPen = PerContourOrComponentPen(
 | 
						|
                    RecordingPen, glyphset=glyphset
 | 
						|
                )
 | 
						|
                decomposedRecording.replay(perContourPen)
 | 
						|
                points = SimpleRecordingPointPen()
 | 
						|
                converter = SegmentToPointPen(points, False)
 | 
						|
                perContourPen.value[idx if matching is None else matching[idx]].replay(
 | 
						|
                    converter
 | 
						|
                )
 | 
						|
 | 
						|
                targetPoint = points.value[problem["value"]][0]
 | 
						|
                cr.save()
 | 
						|
                cr.translate(*targetPoint)
 | 
						|
                cr.scale(1 / scale, 1 / scale)
 | 
						|
                if midway:
 | 
						|
                    self.draw_circle(
 | 
						|
                        cr,
 | 
						|
                        diameter=self.kink_circle_size,
 | 
						|
                        stroke_width=self.kink_circle_stroke_width,
 | 
						|
                        color=self.kink_circle_color,
 | 
						|
                    )
 | 
						|
                else:
 | 
						|
                    self.draw_dot(
 | 
						|
                        cr,
 | 
						|
                        diameter=self.kink_point_size,
 | 
						|
                        color=self.kink_point_color,
 | 
						|
                    )
 | 
						|
                cr.restore()
 | 
						|
 | 
						|
        return scale
 | 
						|
 | 
						|
    def draw_dot(self, cr, *, x=0, y=0, color=(0, 0, 0), diameter=10):
 | 
						|
        cr.save()
 | 
						|
        cr.set_line_width(diameter)
 | 
						|
        cr.set_line_cap(cairo.LINE_CAP_ROUND)
 | 
						|
        cr.move_to(x, y)
 | 
						|
        cr.line_to(x, y)
 | 
						|
        if len(color) == 3:
 | 
						|
            color = color + (1,)
 | 
						|
        cr.set_source_rgba(*color)
 | 
						|
        cr.stroke()
 | 
						|
        cr.restore()
 | 
						|
 | 
						|
    def draw_circle(
 | 
						|
        self, cr, *, x=0, y=0, color=(0, 0, 0), diameter=10, stroke_width=1
 | 
						|
    ):
 | 
						|
        cr.save()
 | 
						|
        cr.set_line_width(stroke_width)
 | 
						|
        cr.set_line_cap(cairo.LINE_CAP_SQUARE)
 | 
						|
        cr.arc(x, y, diameter / 2, 0, 2 * math.pi)
 | 
						|
        if len(color) == 3:
 | 
						|
            color = color + (1,)
 | 
						|
        cr.set_source_rgba(*color)
 | 
						|
        cr.stroke()
 | 
						|
        cr.restore()
 | 
						|
 | 
						|
    def draw_arrow(self, cr, *, x=0, y=0, color=(0, 0, 0)):
 | 
						|
        cr.save()
 | 
						|
        if len(color) == 3:
 | 
						|
            color = color + (1,)
 | 
						|
        cr.set_source_rgba(*color)
 | 
						|
        cr.translate(self.start_arrow_length + x, y)
 | 
						|
        cr.move_to(0, 0)
 | 
						|
        cr.line_to(
 | 
						|
            -self.start_arrow_length,
 | 
						|
            -self.start_arrow_length * 0.4,
 | 
						|
        )
 | 
						|
        cr.line_to(
 | 
						|
            -self.start_arrow_length,
 | 
						|
            self.start_arrow_length * 0.4,
 | 
						|
        )
 | 
						|
        cr.close_path()
 | 
						|
        cr.fill()
 | 
						|
        cr.restore()
 | 
						|
 | 
						|
    def draw_text(self, text, *, x=0, y=0, color=(0, 0, 0), width=None, height=None):
 | 
						|
        if width is None:
 | 
						|
            width = self.width
 | 
						|
        if height is None:
 | 
						|
            height = self.height
 | 
						|
 | 
						|
        text = text.splitlines()
 | 
						|
        cr = cairo.Context(self.surface)
 | 
						|
        cr.set_source_rgb(*color)
 | 
						|
        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_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_font_size
 | 
						|
        if not text_width:
 | 
						|
            return
 | 
						|
        cr.translate(x, y)
 | 
						|
        scale = min(width / text_width, height / text_height)
 | 
						|
        # center
 | 
						|
        cr.translate(
 | 
						|
            (width - text_width * scale) / 2, (height - text_height * scale) / 2
 | 
						|
        )
 | 
						|
        cr.scale(scale, scale)
 | 
						|
 | 
						|
        cr.translate(0, font_ascent)
 | 
						|
        for line in text:
 | 
						|
            cr.move_to(0, 0)
 | 
						|
            cr.show_text(line)
 | 
						|
            cr.translate(0, font_font_size)
 | 
						|
 | 
						|
    def draw_cupcake(self):
 | 
						|
        self.draw_label(
 | 
						|
            self.no_issues_label,
 | 
						|
            x=self.pad,
 | 
						|
            y=self.pad,
 | 
						|
            color=self.no_issues_label_color,
 | 
						|
            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.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,
 | 
						|
            width=self.panel_width,
 | 
						|
            height=self.panel_height,
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
class InterpolatablePostscriptLike(InterpolatablePlot):
 | 
						|
    def __exit__(self, type, value, traceback):
 | 
						|
        self.surface.finish()
 | 
						|
 | 
						|
    def show_page(self):
 | 
						|
        super().show_page()
 | 
						|
        self.surface.show_page()
 | 
						|
 | 
						|
 | 
						|
class InterpolatablePS(InterpolatablePostscriptLike):
 | 
						|
    def __enter__(self):
 | 
						|
        self.surface = cairo.PSSurface(self.out, self.width, self.height)
 | 
						|
        return self
 | 
						|
 | 
						|
 | 
						|
class InterpolatablePDF(InterpolatablePostscriptLike):
 | 
						|
    def __enter__(self):
 | 
						|
        self.surface = cairo.PDFSurface(self.out, self.width, self.height)
 | 
						|
        self.surface.set_metadata(
 | 
						|
            cairo.PDF_METADATA_CREATOR, "fonttools varLib.interpolatable"
 | 
						|
        )
 | 
						|
        self.surface.set_metadata(cairo.PDF_METADATA_CREATE_DATE, "")
 | 
						|
        return self
 | 
						|
 | 
						|
 | 
						|
class InterpolatableSVG(InterpolatablePlot):
 | 
						|
    def __enter__(self):
 | 
						|
        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 show_page(self):
 | 
						|
        super().show_page()
 | 
						|
        self.surface.finish()
 | 
						|
        self.out.append(self.sink.getvalue())
 | 
						|
        self.sink = BytesIO()
 | 
						|
        self.surface = cairo.SVGSurface(self.sink, self.width, self.height)
 |