From a73abc6b878c206e524cd0001b3bdf431af36354 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 23 May 2023 18:20:58 +0100 Subject: [PATCH 1/5] make drop_implied_oncurves work with multiple interpolatable glyphs --- Lib/fontTools/pens/ttGlyphPen.py | 81 ++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/Lib/fontTools/pens/ttGlyphPen.py b/Lib/fontTools/pens/ttGlyphPen.py index 16505d168..c1590e651 100644 --- a/Lib/fontTools/pens/ttGlyphPen.py +++ b/Lib/fontTools/pens/ttGlyphPen.py @@ -17,48 +17,57 @@ import math __all__ = ["TTGlyphPen", "TTGlyphPointPen"] -def drop_implied_oncurves(glyph): - drop = set() - start = 0 - flags = glyph.flags - coords = glyph.coordinates - for last in glyph.endPtsOfContours: - for i in range(start, last + 1): - if not (flags[i] & flagOnCurve): - continue - prv = i - 1 if i > start else last - nxt = i + 1 if i < last else start - if (flags[prv] & flagOnCurve) or flags[prv] != flags[nxt]: - continue - p0 = coords[prv] - p1 = coords[i] - p2 = coords[nxt] - if not math.isclose(p1[0] - p0[0], p2[0] - p1[0]) or not math.isclose( - p1[1] - p0[1], p2[1] - p1[1] - ): - continue +def drop_implied_oncurves(*interpolatable_glyphs): + drop = None + for glyph in interpolatable_glyphs: + may_drop = set() + start = 0 + flags = glyph.flags + coords = glyph.coordinates + for last in glyph.endPtsOfContours: + for i in range(start, last + 1): + if not (flags[i] & flagOnCurve): + continue + prv = i - 1 if i > start else last + nxt = i + 1 if i < last else start + if (flags[prv] & flagOnCurve) or flags[prv] != flags[nxt]: + continue + p0 = coords[prv] + p1 = coords[i] + p2 = coords[nxt] + if not math.isclose(p1[0] - p0[0], p2[0] - p1[0]) or not math.isclose( + p1[1] - p0[1], p2[1] - p1[1] + ): + continue + + may_drop.add(i) + # we only want to drop if ALL interpolatable glyphs have the same implied oncurves + if drop is None: + drop = may_drop + else: + drop.intersection_update(may_drop) - drop.add(i) if drop: # Do the actual dropping - glyph.coordinates = GlyphCoordinates( - coords[i] for i in range(len(coords)) if i not in drop - ) - glyph.flags = array("B", (flags[i] for i in range(len(flags)) if i not in drop)) + for glyph in interpolatable_glyphs: + glyph.coordinates = GlyphCoordinates( + coords[i] for i in range(len(coords)) if i not in drop + ) + glyph.flags = array("B", (flags[i] for i in range(len(flags)) if i not in drop)) - endPts = glyph.endPtsOfContours - newEndPts = [] - i = 0 - delta = 0 - for d in sorted(drop): - while d > endPts[i]: + endPts = glyph.endPtsOfContours + newEndPts = [] + i = 0 + delta = 0 + for d in sorted(drop): + while d > endPts[i]: + newEndPts.append(endPts[i] - delta) + i += 1 + delta += 1 + while i < len(endPts): newEndPts.append(endPts[i] - delta) i += 1 - delta += 1 - while i < len(endPts): - newEndPts.append(endPts[i] - delta) - i += 1 - glyph.endPtsOfContours = newEndPts + glyph.endPtsOfContours = newEndPts class _TTGlyphBasePen: From b7f4e9b83e1794b99c8fa8faf59a04ca1d8007f8 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 25 May 2023 13:42:29 +0100 Subject: [PATCH 2/5] move dropImpliedOnCurvePoints function to glyf table module so it can be used by client code on multiple glyf Glyph objects regardles of TTGlyphPen --- Lib/fontTools/pens/ttGlyphPen.py | 56 +------------------------ Lib/fontTools/ttLib/tables/_g_l_y_f.py | 57 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 54 deletions(-) diff --git a/Lib/fontTools/pens/ttGlyphPen.py b/Lib/fontTools/pens/ttGlyphPen.py index c1590e651..9db320dc3 100644 --- a/Lib/fontTools/pens/ttGlyphPen.py +++ b/Lib/fontTools/pens/ttGlyphPen.py @@ -11,65 +11,13 @@ from fontTools.ttLib.tables._g_l_y_f import flagOnCurve, flagCubic from fontTools.ttLib.tables._g_l_y_f import Glyph from fontTools.ttLib.tables._g_l_y_f import GlyphComponent from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates +from fontTools.ttLib.tables._g_l_y_f import dropImpliedOnCurvePoints import math __all__ = ["TTGlyphPen", "TTGlyphPointPen"] -def drop_implied_oncurves(*interpolatable_glyphs): - drop = None - for glyph in interpolatable_glyphs: - may_drop = set() - start = 0 - flags = glyph.flags - coords = glyph.coordinates - for last in glyph.endPtsOfContours: - for i in range(start, last + 1): - if not (flags[i] & flagOnCurve): - continue - prv = i - 1 if i > start else last - nxt = i + 1 if i < last else start - if (flags[prv] & flagOnCurve) or flags[prv] != flags[nxt]: - continue - p0 = coords[prv] - p1 = coords[i] - p2 = coords[nxt] - if not math.isclose(p1[0] - p0[0], p2[0] - p1[0]) or not math.isclose( - p1[1] - p0[1], p2[1] - p1[1] - ): - continue - - may_drop.add(i) - # we only want to drop if ALL interpolatable glyphs have the same implied oncurves - if drop is None: - drop = may_drop - else: - drop.intersection_update(may_drop) - - if drop: - # Do the actual dropping - for glyph in interpolatable_glyphs: - glyph.coordinates = GlyphCoordinates( - coords[i] for i in range(len(coords)) if i not in drop - ) - glyph.flags = array("B", (flags[i] for i in range(len(flags)) if i not in drop)) - - endPts = glyph.endPtsOfContours - newEndPts = [] - i = 0 - delta = 0 - for d in sorted(drop): - while d > endPts[i]: - newEndPts.append(endPts[i] - delta) - i += 1 - delta += 1 - while i < len(endPts): - newEndPts.append(endPts[i] - delta) - i += 1 - glyph.endPtsOfContours = newEndPts - - class _TTGlyphBasePen: def __init__( self, @@ -199,7 +147,7 @@ class _TTGlyphBasePen: glyph.coordinates.toInt() if dropImpliedOnCurves: - drop_implied_oncurves(glyph) + dropImpliedOnCurvePoints(glyph) self.init() diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index f71fd3e2d..48c08df3a 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -22,6 +22,7 @@ import sys import struct import array import logging +import math import os from fontTools.misc import xmlWriter from fontTools.misc.filenames import userNameToFileName @@ -1530,6 +1531,62 @@ class Glyph(object): return result if result is NotImplemented else not result +def dropImpliedOnCurvePoints(*interpolatable_glyphs): + drop = None + for glyph in interpolatable_glyphs: + may_drop = set() + start = 0 + flags = glyph.flags + coords = glyph.coordinates + for last in glyph.endPtsOfContours: + for i in range(start, last + 1): + if not (flags[i] & flagOnCurve): + continue + prv = i - 1 if i > start else last + nxt = i + 1 if i < last else start + if (flags[prv] & flagOnCurve) or flags[prv] != flags[nxt]: + continue + p0 = coords[prv] + p1 = coords[i] + p2 = coords[nxt] + if not math.isclose(p1[0] - p0[0], p2[0] - p1[0]) or not math.isclose( + p1[1] - p0[1], p2[1] - p1[1] + ): + continue + + may_drop.add(i) + # we only want to drop if ALL interpolatable glyphs have the same implied oncurves + if drop is None: + drop = may_drop + else: + drop.intersection_update(may_drop) + + if drop: + # Do the actual dropping + for glyph in interpolatable_glyphs: + coords = glyph.coordinates + glyph.coordinates = GlyphCoordinates( + coords[i] for i in range(len(coords)) if i not in drop + ) + glyph.flags = array.array( + "B", (flags[i] for i in range(len(flags)) if i not in drop) + ) + + endPts = glyph.endPtsOfContours + newEndPts = [] + i = 0 + delta = 0 + for d in sorted(drop): + while d > endPts[i]: + newEndPts.append(endPts[i] - delta) + i += 1 + delta += 1 + while i < len(endPts): + newEndPts.append(endPts[i] - delta) + i += 1 + glyph.endPtsOfContours = newEndPts + + class GlyphComponent(object): """Represents a component within a composite glyph. From e19871981d8262ba3ce84d88cad239a4c42c1e3f Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 25 May 2023 13:56:19 +0100 Subject: [PATCH 3/5] add docstring to dropImpliedOnCurvePoints function --- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index 48c08df3a..142168811 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -1531,7 +1531,20 @@ class Glyph(object): return result if result is NotImplemented else not result -def dropImpliedOnCurvePoints(*interpolatable_glyphs): +def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> None: + """Drop impliable on-curve points from the (simple) glyph or glyphs. + + In TrueType glyf outlines, on-curve points can be implied when they are located at + the midpoint of the line connecting two consecutive off-curve points. + + If more than one glyphs are passed, these are assumed to be interpolatable masters + of the same glyph impliable, and thus only the on-curve points that are impliable + for all of them will actually be implied. + The input glyph(s) is/are modified in-place. + + Reference: + https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html + """ drop = None for glyph in interpolatable_glyphs: may_drop = set() From 3b62811b63772bdd3b380cfcdcd93c52324211d4 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 1 Jun 2023 18:21:59 +0100 Subject: [PATCH 4/5] have dropImpliedOnCurvePoints return the set of point indices useful for testing --- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index 142168811..c33d89d67 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -28,6 +28,7 @@ from fontTools.misc import xmlWriter from fontTools.misc.filenames import userNameToFileName from fontTools.misc.loggingTools import deprecateFunction from enum import IntFlag +from typing import Set log = logging.getLogger(__name__) @@ -1531,7 +1532,7 @@ class Glyph(object): return result if result is NotImplemented else not result -def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> None: +def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]: """Drop impliable on-curve points from the (simple) glyph or glyphs. In TrueType glyf outlines, on-curve points can be implied when they are located at @@ -1542,9 +1543,17 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> None: for all of them will actually be implied. The input glyph(s) is/are modified in-place. + Args: + interpolatable_glyphs: The glyph or glyphs to modify in-place. + + Returns: + The set of point indices that were dropped if any. + Reference: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html """ + assert len(interpolatable_glyphs) > 0 + drop = None for glyph in interpolatable_glyphs: may_drop = set() @@ -1599,6 +1608,8 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> None: i += 1 glyph.endPtsOfContours = newEndPts + return drop + class GlyphComponent(object): """Represents a component within a composite glyph. From a039e1dda8833243aa5a9adbd91a8df80d5f08d3 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 1 Jun 2023 19:19:53 +0100 Subject: [PATCH 5/5] _g_l_y_f_test: add tests for dropImpliedOnCurvePoints --- Tests/ttLib/tables/_g_l_y_f_test.py | 123 ++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py index 4f38d0cb1..3a918def2 100644 --- a/Tests/ttLib/tables/_g_l_y_f_test.py +++ b/Tests/ttLib/tables/_g_l_y_f_test.py @@ -1,5 +1,6 @@ from fontTools.misc.fixedTools import otRound from fontTools.misc.testTools import getXML, parseXML +from fontTools.misc.transform import Transform from fontTools.pens.ttGlyphPen import TTGlyphPen from fontTools.pens.recordingPen import RecordingPen, RecordingPointPen from fontTools.pens.pointPen import PointToSegmentPen @@ -8,6 +9,7 @@ from fontTools.ttLib.tables._g_l_y_f import ( Glyph, GlyphCoordinates, GlyphComponent, + dropImpliedOnCurvePoints, flagOnCurve, flagCubic, ARGS_ARE_XY_VALUES, @@ -20,6 +22,7 @@ from fontTools.ttLib.tables._g_l_y_f import ( from fontTools.ttLib.tables import ttProgram import sys import array +from copy import deepcopy from io import StringIO, BytesIO import itertools import pytest @@ -813,6 +816,126 @@ class GlyphCubicTest: ] +def build_interpolatable_glyphs(contours, *transforms): + # given a list of lists of (point, flag) tuples (one per contour), build a Glyph + # then make len(transforms) copies transformed accordingly, and return a + # list of such interpolatable glyphs. + glyph1 = Glyph() + glyph1.numberOfContours = len(contours) + glyph1.coordinates = GlyphCoordinates( + [pt for contour in contours for pt, _flag in contour] + ) + glyph1.flags = array.array( + "B", [flag for contour in contours for _pt, flag in contour] + ) + glyph1.endPtsOfContours = [ + sum(len(contour) for contour in contours[: i + 1]) - 1 + for i in range(len(contours)) + ] + result = [glyph1] + for t in transforms: + glyph = deepcopy(glyph1) + glyph.coordinates.transform((t[0:2], t[2:4])) + glyph.coordinates.translate(t[4:6]) + result.append(glyph) + return result + + +def test_dropImpliedOnCurvePoints_all_quad_off_curves(): + # Two interpolatable glyphs with same structure, the coordinates of one are 2x the + # other; all the on-curve points are impliable in each one, thus are dropped from + # both, leaving contours with off-curve points only. + glyph1, glyph2 = build_interpolatable_glyphs( + [ + [ + ((0, 1), flagOnCurve), + ((1, 1), 0), + ((1, 0), flagOnCurve), + ((1, -1), 0), + ((0, -1), flagOnCurve), + ((-1, -1), 0), + ((-1, 0), flagOnCurve), + ((-1, 1), 0), + ] + ], + Transform().scale(2.0), + ) + + assert dropImpliedOnCurvePoints(glyph1, glyph2) == {0, 2, 4, 6} + + assert glyph1.flags == glyph2.flags == array.array("B", [0, 0, 0, 0]) + assert glyph1.coordinates == GlyphCoordinates([(1, 1), (1, -1), (-1, -1), (-1, 1)]) + assert glyph2.coordinates == GlyphCoordinates([(2, 2), (2, -2), (-2, -2), (-2, 2)]) + assert glyph1.endPtsOfContours == glyph2.endPtsOfContours == [3] + + +def test_dropImpliedOnCurvePoints_all_cubic_off_curves(): + # same as above this time using cubic curves + glyph1, glyph2 = build_interpolatable_glyphs( + [ + [ + ((0, 1), flagOnCurve), + ((1, 1), flagCubic), + ((1, 1), flagCubic), + ((1, 0), flagOnCurve), + ((1, -1), flagCubic), + ((1, -1), flagCubic), + ((0, -1), flagOnCurve), + ((-1, -1), flagCubic), + ((-1, -1), flagCubic), + ((-1, 0), flagOnCurve), + ((-1, 1), flagCubic), + ((-1, 1), flagCubic), + ] + ], + Transform().translate(10.0), + ) + + assert dropImpliedOnCurvePoints(glyph1, glyph2) == {0, 3, 6, 9} + + assert glyph1.flags == glyph2.flags == array.array("B", [flagCubic] * 8) + assert glyph1.coordinates == GlyphCoordinates( + [(1, 1), (1, 1), (1, -1), (1, -1), (-1, -1), (-1, -1), (-1, 1), (-1, 1)] + ) + assert glyph2.coordinates == GlyphCoordinates( + [(11, 1), (11, 1), (11, -1), (11, -1), (9, -1), (9, -1), (9, 1), (9, 1)] + ) + assert glyph1.endPtsOfContours == glyph2.endPtsOfContours == [7] + + +def test_dropImpliedOnCurvePoints_not_all_impliable(): + # same input as in in test_dropImpliedOnCurvePoints_all_quad_off_curves but we + # perturbate one of the glyphs such that the 2nd on-curve is no longer half-way + # between the neighboring off-curves. + glyph1, glyph2, glyph3 = build_interpolatable_glyphs( + [ + [ + ((0, 1), flagOnCurve), + ((1, 1), 0), + ((1, 0), flagOnCurve), + ((1, -1), 0), + ((0, -1), flagOnCurve), + ((-1, -1), 0), + ((-1, 0), flagOnCurve), + ((-1, 1), 0), + ] + ], + Transform().translate(10.0), + Transform().translate(10.0).scale(2.0), + ) + p2 = glyph2.coordinates[2] + glyph2.coordinates[2] = (p2[0] + 2.0, p2[1] - 2.0) + + assert dropImpliedOnCurvePoints(glyph1, glyph2, glyph3) == { + 0, + # 2, this is NOT implied because it's no longer impliable for all glyphs + 4, + 6, + } + + assert glyph2.flags == array.array("B", [0, flagOnCurve, 0, 0, 0]) + + if __name__ == "__main__": import sys