From 6a276d9f6a69a8eac49fa3268cc879c5a4d5c41a Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 2 Jun 2023 13:51:28 +0100 Subject: [PATCH] dropImpliedOnCurvePoints: raise if incompatible, skip empty/composites --- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 40 ++++++++++++++++--- Tests/ttLib/tables/_g_l_y_f_test.py | 54 +++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index c33d89d67..deaff166a 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -1541,6 +1541,8 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]: 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. + Composite glyphs or empty glyphs are skipped, only simple glyphs with 1 or more + contours are considered. The input glyph(s) is/are modified in-place. Args: @@ -1549,16 +1551,40 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]: Returns: The set of point indices that were dropped if any. + Raises: + ValueError if simple glyphs are not in fact interpolatable because they have + different point flags or number of contours. + Reference: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html """ - assert len(interpolatable_glyphs) > 0 - + numContours = None + flags = None drop = None - for glyph in interpolatable_glyphs: + simple_glyphs = [] + for i, glyph in enumerate(interpolatable_glyphs): + if glyph.numberOfContours < 1: + # ignore composite or empty glyphs + continue + + if numContours is None: + numContours = glyph.numberOfContours + elif glyph.numberOfContours != numContours: + raise ValueError( + f"Incompatible number of contours for glyph at master index {i}: " + f"expected {numContours}, found {glyph.numberOfContours}" + ) + + if flags is None: + flags = glyph.flags + elif glyph.flags != flags: + raise ValueError( + f"Incompatible flags for simple glyph at master index {i}: " + f"expected {flags}, found {glyph.flags}" + ) + may_drop = set() start = 0 - flags = glyph.flags coords = glyph.coordinates for last in glyph.endPtsOfContours: for i in range(start, last + 1): @@ -1583,9 +1609,11 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]: else: drop.intersection_update(may_drop) + simple_glyphs.append(glyph) + if drop: # Do the actual dropping - for glyph in interpolatable_glyphs: + for glyph in simple_glyphs: coords = glyph.coordinates glyph.coordinates = GlyphCoordinates( coords[i] for i in range(len(coords)) if i not in drop @@ -1608,7 +1636,7 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]: i += 1 glyph.endPtsOfContours = newEndPts - return drop + return drop if drop is not None else set() class GlyphComponent(object): diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py index 3a918def2..f2f300da9 100644 --- a/Tests/ttLib/tables/_g_l_y_f_test.py +++ b/Tests/ttLib/tables/_g_l_y_f_test.py @@ -860,13 +860,17 @@ def test_dropImpliedOnCurvePoints_all_quad_off_curves(): ], Transform().scale(2.0), ) + # also add an empty glyph (will be ignored); we use this trick for 'sparse' masters + glyph3 = Glyph() + glyph3.numberOfContours = 0 - assert dropImpliedOnCurvePoints(glyph1, glyph2) == {0, 2, 4, 6} + assert dropImpliedOnCurvePoints(glyph1, glyph2, glyph3) == {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] + assert glyph3.numberOfContours == 0 def test_dropImpliedOnCurvePoints_all_cubic_off_curves(): @@ -890,8 +894,10 @@ def test_dropImpliedOnCurvePoints_all_cubic_off_curves(): ], Transform().translate(10.0), ) + glyph3 = Glyph() + glyph3.numberOfContours = 0 - assert dropImpliedOnCurvePoints(glyph1, glyph2) == {0, 3, 6, 9} + assert dropImpliedOnCurvePoints(glyph1, glyph2, glyph3) == {0, 3, 6, 9} assert glyph1.flags == glyph2.flags == array.array("B", [flagCubic] * 8) assert glyph1.coordinates == GlyphCoordinates( @@ -901,6 +907,7 @@ def test_dropImpliedOnCurvePoints_all_cubic_off_curves(): [(11, 1), (11, 1), (11, -1), (11, -1), (9, -1), (9, -1), (9, 1), (9, 1)] ) assert glyph1.endPtsOfContours == glyph2.endPtsOfContours == [7] + assert glyph3.numberOfContours == 0 def test_dropImpliedOnCurvePoints_not_all_impliable(): @@ -936,6 +943,49 @@ def test_dropImpliedOnCurvePoints_not_all_impliable(): assert glyph2.flags == array.array("B", [0, flagOnCurve, 0, 0, 0]) +def test_dropImpliedOnCurvePoints_all_empty_glyphs(): + glyph1 = Glyph() + glyph1.numberOfContours = 0 + glyph2 = Glyph() + glyph2.numberOfContours = 0 + + assert dropImpliedOnCurvePoints(glyph1, glyph2) == set() + + +def test_dropImpliedOnCurvePoints_incompatible_number_of_contours(): + glyph1 = Glyph() + glyph1.numberOfContours = 1 + glyph1.endPtsOfContours = [3] + glyph1.flags = array.array("B", [1, 1, 1, 1]) + glyph1.coordinates = GlyphCoordinates([(0, 0), (1, 1), (2, 2), (3, 3)]) + + glyph2 = Glyph() + glyph2.numberOfContours = 2 + glyph2.endPtsOfContours = [1, 3] + glyph2.flags = array.array("B", [1, 1, 1, 1]) + glyph2.coordinates = GlyphCoordinates([(0, 0), (1, 1), (2, 2), (3, 3)]) + + with pytest.raises(ValueError, match="Incompatible number of contours"): + dropImpliedOnCurvePoints(glyph1, glyph2) + + +def test_dropImpliedOnCurvePoints_incompatible_flags(): + glyph1 = Glyph() + glyph1.numberOfContours = 1 + glyph1.endPtsOfContours = [3] + glyph1.flags = array.array("B", [1, 1, 1, 1]) + glyph1.coordinates = GlyphCoordinates([(0, 0), (1, 1), (2, 2), (3, 3)]) + + glyph2 = Glyph() + glyph2.numberOfContours = 1 + glyph2.endPtsOfContours = [3] + glyph2.flags = array.array("B", [0, 0, 0, 0]) + glyph2.coordinates = GlyphCoordinates([(0, 0), (1, 1), (2, 2), (3, 3)]) + + with pytest.raises(ValueError, match="Incompatible flags"): + dropImpliedOnCurvePoints(glyph1, glyph2) + + if __name__ == "__main__": import sys