123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- 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
- 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."""
- return l[-k:] + l[:-k]
- class PerContourPen(BasePen):
- def __init__(self, Pen, glyphset=None):
- BasePen.__init__(self, glyphset)
- self._glyphset = glyphset
- self._Pen = Pen
- self._pen = None
- self.value = []
- def _moveTo(self, p0):
- self._newItem()
- self._pen.moveTo(p0)
- def _lineTo(self, p1):
- self._pen.lineTo(p1)
- def _qCurveToOne(self, p1, p2):
- self._pen.qCurveTo(p1, p2)
- def _curveToOne(self, p1, p2, p3):
- self._pen.curveTo(p1, p2, p3)
- def _closePath(self):
- self._pen.closePath()
- self._pen = None
- def _endPath(self):
- self._pen.endPath()
- self._pen = None
- def _newItem(self):
- self._pen = pen = self._Pen()
- self.value.append(pen)
- class PerContourOrComponentPen(PerContourPen):
- def addComponent(self, glyphName, transformation):
- self._newItem()
- self.value[-1].addComponent(glyphName, transformation)
- class SimpleRecordingPointPen(AbstractPointPen):
- def __init__(self):
- self.value = []
- def beginPath(self, identifier=None, **kwargs):
- pass
- def endPath(self) -> None:
- pass
- def addPoint(self, pt, segmentType=None):
- self.value.append((pt, False if segmentType is None else True))
- def vdiff_hypot2(v0, v1):
- s = 0
- for x0, x1 in zip(v0, v1):
- d = x1 - x0
- s += d * d
- return s
- def vdiff_hypot2_complex(v0, v1):
- s = 0
- for x0, x1 in zip(v0, v1):
- d = x1 - x0
- s += d.real * d.real + d.imag * d.imag
- # This does the same but seems to be slower:
- # s += (d * d.conjugate()).real
- return s
- def matching_cost(G, matching):
- return sum(G[i][j] for i, j in enumerate(matching))
- def min_cost_perfect_bipartite_matching_scipy(G):
- n = len(G)
- rows, cols = linear_sum_assignment(G)
- assert (rows == list(range(n))).all()
- # Convert numpy array and integer to Python types,
- # to ensure that this is JSON-serializable.
- cols = list(int(e) for e in cols)
- return list(cols), matching_cost(G, cols)
- def min_cost_perfect_bipartite_matching_munkres(G):
- n = len(G)
- cols = [None] * n
- for row, col in Munkres().compute(G):
- cols[row] = col
- return cols, matching_cost(G, cols)
- def min_cost_perfect_bipartite_matching_bruteforce(G):
- n = len(G)
- if n > 6:
- raise Exception("Install Python module 'munkres' or 'scipy >= 0.17.0'")
- # Otherwise just brute-force
- permutations = itertools.permutations(range(n))
- best = list(next(permutations))
- best_cost = matching_cost(G, best)
- for p in permutations:
- cost = matching_cost(G, p)
- if cost < best_cost:
- best, best_cost = list(p), cost
- return best, best_cost
- try:
- from scipy.optimize import linear_sum_assignment
- min_cost_perfect_bipartite_matching = min_cost_perfect_bipartite_matching_scipy
- except ImportError:
- try:
- from munkres import Munkres
- min_cost_perfect_bipartite_matching = (
- min_cost_perfect_bipartite_matching_munkres
- )
- except ImportError:
- min_cost_perfect_bipartite_matching = (
- min_cost_perfect_bipartite_matching_bruteforce
- )
- def contour_vector_from_stats(stats):
- # Don't change the order of items here.
- # It's okay to add to the end, but otherwise, other
- # code depends on it. Search for "covariance".
- size = sqrt(abs(stats.area))
- return (
- copysign((size), stats.area),
- stats.meanX,
- stats.meanY,
- stats.stddevX * 2,
- stats.stddevY * 2,
- stats.correlation * size,
- )
- def matching_for_vectors(m0, m1):
- n = len(m0)
- identity_matching = list(range(n))
- costs = [[vdiff_hypot2(v0, v1) for v1 in m1] for v0 in m0]
- (
- matching,
- matching_cost,
- ) = min_cost_perfect_bipartite_matching(costs)
- identity_cost = sum(costs[i][i] for i in range(n))
- return matching, matching_cost, identity_cost
- def points_characteristic_bits(points):
- bits = 0
- for pt, b in reversed(points):
- bits = (bits << 1) | b
- return bits
- _NUM_ITEMS_PER_POINTS_COMPLEX_VECTOR = 4
- def points_complex_vector(points):
- vector = []
- if not points:
- return vector
- points = [complex(*pt) for pt, _ in points]
- n = len(points)
- assert _NUM_ITEMS_PER_POINTS_COMPLEX_VECTOR == 4
- points.extend(points[: _NUM_ITEMS_PER_POINTS_COMPLEX_VECTOR - 1])
- while len(points) < _NUM_ITEMS_PER_POINTS_COMPLEX_VECTOR:
- points.extend(points[: _NUM_ITEMS_PER_POINTS_COMPLEX_VECTOR - 1])
- for i in range(n):
- # The weights are magic numbers.
- # The point itself
- p0 = points[i]
- vector.append(p0)
- # The vector to the next point
- p1 = points[i + 1]
- d0 = p1 - p0
- vector.append(d0 * 3)
- # The turn vector
- p2 = points[i + 2]
- d1 = p2 - p1
- vector.append(d1 - d0)
- # The angle to the next point, as a cross product;
- # Square root of, to match dimentionality of distance.
- cross = d0.real * d1.imag - d0.imag * d1.real
- cross = copysign(sqrt(abs(cross)), cross)
- vector.append(cross * 4)
- return vector
- def add_isomorphisms(points, isomorphisms, reverse):
- reference_bits = points_characteristic_bits(points)
- n = len(points)
- # if points[0][0] == points[-1][0]:
- # abort
- if reverse:
- points = points[::-1]
- bits = points_characteristic_bits(points)
- else:
- bits = reference_bits
- vector = points_complex_vector(points)
- assert len(vector) % n == 0
- mult = len(vector) // n
- mask = (1 << n) - 1
- for i in range(n):
- b = ((bits << (n - i)) & mask) | (bits >> i)
- if b == reference_bits:
- isomorphisms.append(
- (rot_list(vector, -i * mult), n - 1 - i if reverse else i, reverse)
- )
- def find_parents_and_order(glyphsets, locations, *, discrete_axes=set()):
- parents = [None] + list(range(len(glyphsets) - 1))
- order = list(range(len(glyphsets)))
- if locations:
- # Order base master first
- bases = [
- i
- for i, l in enumerate(locations)
- if all(v == 0 for k, v in l.items() if k not in discrete_axes)
- ]
- if bases:
- logging.info("Found %s base masters: %s", len(bases), bases)
- else:
- logging.warning("No base master location found")
- # Form a minimum spanning tree of the locations
- try:
- from scipy.sparse.csgraph import minimum_spanning_tree
- graph = [[0] * len(locations) for _ in range(len(locations))]
- axes = set()
- for l in locations:
- axes.update(l.keys())
- axes = sorted(axes)
- vectors = [tuple(l.get(k, 0) for k in axes) for l in locations]
- for i, j in itertools.combinations(range(len(locations)), 2):
- i_discrete_location = {
- k: v for k, v in zip(axes, vectors[i]) if k in discrete_axes
- }
- j_discrete_location = {
- k: v for k, v in zip(axes, vectors[j]) if k in discrete_axes
- }
- if i_discrete_location != j_discrete_location:
- continue
- graph[i][j] = vdiff_hypot2(vectors[i], vectors[j])
- tree = minimum_spanning_tree(graph, overwrite=True)
- rows, cols = tree.nonzero()
- graph = defaultdict(set)
- for row, col in zip(rows, cols):
- graph[row].add(col)
- graph[col].add(row)
- # Traverse graph from the base and assign parents
- parents = [None] * len(locations)
- order = []
- visited = set()
- queue = deque(bases)
- while queue:
- i = queue.popleft()
- visited.add(i)
- order.append(i)
- for j in sorted(graph[i]):
- if j not in visited:
- parents[j] = i
- queue.append(j)
- assert len(order) == len(
- parents
- ), "Not all masters are reachable; report an issue"
- except ImportError:
- pass
- log.info("Parents: %s", parents)
- log.info("Order: %s", order)
- return parents, order
- def transform_from_stats(stats, inverse=False):
- # https://cookierobotics.com/007/
- a = stats.varianceX
- b = stats.covariance
- c = stats.varianceY
- delta = (((a - c) * 0.5) ** 2 + b * b) ** 0.5
- lambda1 = (a + c) * 0.5 + delta # Major eigenvalue
- lambda2 = (a + c) * 0.5 - delta # Minor eigenvalue
- theta = atan2(lambda1 - a, b) if b != 0 else (pi * 0.5 if a < c else 0)
- trans = Transform()
- if lambda2 < 0:
- # XXX This is a hack.
- # The problem is that the covariance matrix is singular.
- # This happens when the contour is a line, or a circle.
- # In that case, the covariance matrix is not a good
- # representation of the contour.
- # We should probably detect this earlier and avoid
- # computing the covariance matrix in the first place.
- # But for now, we just avoid the division by zero.
- lambda2 = 0
- if inverse:
- trans = trans.translate(-stats.meanX, -stats.meanY)
- trans = trans.rotate(-theta)
- trans = trans.scale(1 / sqrt(lambda1), 1 / sqrt(lambda2))
- else:
- trans = trans.scale(sqrt(lambda1), sqrt(lambda2))
- trans = trans.rotate(theta)
- trans = trans.translate(stats.meanX, stats.meanY)
- return trans
|