aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/fonttools/fontTools/varLib
diff options
context:
space:
mode:
authorAlexSm <alex@ydb.tech>2024-01-09 18:56:40 +0100
committerGitHub <noreply@github.com>2024-01-09 18:56:40 +0100
commite95f266d2a3e48e62015220588a4fd73d5d5a5cb (patch)
treea8a784b6931fe52ad5f511cfef85af14e5f63991 /contrib/python/fonttools/fontTools/varLib
parent50a65e3b48a82d5b51f272664da389f2e0b0c99a (diff)
downloadydb-e95f266d2a3e48e62015220588a4fd73d5d5a5cb.tar.gz
Library import 6 (#888)
Diffstat (limited to 'contrib/python/fonttools/fontTools/varLib')
-rw-r--r--contrib/python/fonttools/fontTools/varLib/instancer/__init__.py2
-rw-r--r--contrib/python/fonttools/fontTools/varLib/instancer/solver.py4
-rw-r--r--contrib/python/fonttools/fontTools/varLib/interpolatable.py337
-rw-r--r--contrib/python/fonttools/fontTools/varLib/interpolatableHelpers.py95
-rw-r--r--contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py413
-rw-r--r--contrib/python/fonttools/fontTools/varLib/interpolatableTestContourOrder.py10
-rw-r--r--contrib/python/fonttools/fontTools/varLib/interpolatableTestStartingPoint.py16
-rw-r--r--contrib/python/fonttools/fontTools/varLib/merger.py24
-rw-r--r--contrib/python/fonttools/fontTools/varLib/models.py58
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)