From 6a276d9f6a69a8eac49fa3268cc879c5a4d5c41a Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 2 Jun 2023 13:51:28 +0100 Subject: [PATCH 1/4] 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 From b6bb9dfef048b3223b2c1ca0a7363dd5b63cc132 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 2 Jun 2023 13:53:00 +0100 Subject: [PATCH 2/4] ttGlyphPen: only call dropImpliedOnCurvePoints for simple glyphs after fully formed --- Lib/fontTools/pens/ttGlyphPen.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/pens/ttGlyphPen.py b/Lib/fontTools/pens/ttGlyphPen.py index 9db320dc3..1042f21d6 100644 --- a/Lib/fontTools/pens/ttGlyphPen.py +++ b/Lib/fontTools/pens/ttGlyphPen.py @@ -144,10 +144,7 @@ class _TTGlyphBasePen: glyph.coordinates = GlyphCoordinates(self.points) glyph.endPtsOfContours = self.endPts glyph.flags = array("B", self.types) - glyph.coordinates.toInt() - if dropImpliedOnCurves: - dropImpliedOnCurvePoints(glyph) self.init() @@ -160,6 +157,8 @@ class _TTGlyphBasePen: glyph.numberOfContours = len(glyph.endPtsOfContours) glyph.program = ttProgram.Program() glyph.program.fromBytecode(b"") + if dropImpliedOnCurves: + dropImpliedOnCurvePoints(glyph) return glyph From 0690703b86c2e78c5a9a52291702df31a6e22ba2 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 2 Jun 2023 15:29:19 +0100 Subject: [PATCH 3/4] varLib: add --drop-implied-oncurves option For the test, I used the Tests/varLib/data/Build.designspace as starting point, modified the 'a' glyph so that 1 on-curve point (the first one) becomes impliable for all the masters. --- Lib/fontTools/varLib/__init__.py | 63 ++- Tests/varLib/data/DropOnCurves.designspace | 20 + .../TestFamily-Master1.ttx | 312 +++++++++++ .../TestFamily-Master2.ttx | 313 +++++++++++ .../varLib/data/test_results/DropOnCurves.ttx | 498 ++++++++++++++++++ Tests/varLib/varLib_test.py | 25 + 6 files changed, 1229 insertions(+), 2 deletions(-) create mode 100644 Tests/varLib/data/DropOnCurves.designspace create mode 100644 Tests/varLib/data/master_ttx_drop_oncurves/TestFamily-Master1.ttx create mode 100644 Tests/varLib/data/master_ttx_drop_oncurves/TestFamily-Master2.ttx create mode 100644 Tests/varLib/data/test_results/DropOnCurves.ttx diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py index 86fa8d704..2574a8ee9 100644 --- a/Lib/fontTools/varLib/__init__.py +++ b/Lib/fontTools/varLib/__init__.py @@ -24,7 +24,7 @@ from fontTools.misc.roundTools import noRound, otRound from fontTools.misc.textTools import Tag, tostr from fontTools.ttLib import TTFont, newTable from fontTools.ttLib.tables._f_v_a_r import Axis, NamedInstance -from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates +from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates, dropImpliedOnCurvePoints from fontTools.ttLib.tables.ttProgram import Program from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.ttLib.tables import otTables as ot @@ -40,7 +40,7 @@ from fontTools.varLib.stat import buildVFStatTable from fontTools.colorLib.builder import buildColrV1 from fontTools.colorLib.unbuilder import unbuildColrV1 from functools import partial -from collections import OrderedDict, namedtuple +from collections import OrderedDict, defaultdict, namedtuple import os.path import logging from copy import deepcopy @@ -965,6 +965,46 @@ def set_default_weight_width_slant(font, location): font["post"].italicAngle = italicAngle +def drop_implied_oncurve_points(*masters: TTFont) -> int: + """Drop impliable on-curve points from all the simple glyphs in masters. + + In TrueType glyf outlines, on-curve points can be implied when they are located + exactly at the midpoint of the line connecting two consecutive off-curve points. + + The input masters' glyf tables are assumed to contain same-named glyphs that are + interpolatable. Oncurve points are only dropped if they can be implied for all + the masters. The fonts are modified in-place. + + Args: + masters: The TTFont(s) to modify + + Returns: + The total number of points that were dropped if any. + + Reference: + https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html + """ + + count = 0 + glyph_masters = defaultdict(list) + # multiple DS source may point to the same TTFont object and we want to + # avoid processing the same glyph twice as they are modified in-place + for font in {id(m): m for m in masters}.values(): + glyf = font["glyf"] + for glyphName in glyf.keys(): + glyph_masters[glyphName].append(glyf[glyphName]) + count = 0 + for glyphName, glyphs in glyph_masters.items(): + try: + dropped = dropImpliedOnCurvePoints(*glyphs) + except ValueError as e: + # we don't fail for incompatible glyphs in _add_gvar so we shouldn't here + log.warning("Failed to drop implied oncurves for %r: %s", glyphName, e) + else: + count += len(dropped) + return count + + def build_many( designspace: DesignSpaceDocument, master_finder=lambda s: s, @@ -972,6 +1012,7 @@ def build_many( optimize=True, skip_vf=lambda vf_name: False, colr_layer_reuse=True, + drop_implied_oncurves=False, ): """ Build variable fonts from a designspace file, version 5 which can define @@ -1015,6 +1056,7 @@ def build_many( exclude=exclude, optimize=optimize, colr_layer_reuse=colr_layer_reuse, + drop_implied_oncurves=drop_implied_oncurves, )[0] if doBuildStatFromDSv5: buildVFStatTable(vf, designspace, name) @@ -1028,6 +1070,7 @@ def build( exclude=[], optimize=True, colr_layer_reuse=True, + drop_implied_oncurves=False, ): """ Build variation font from a designspace file. @@ -1055,6 +1098,13 @@ def build( except AttributeError: master_ttfs.append(None) # in-memory fonts have no path + if drop_implied_oncurves and "glyf" in master_fonts[ds.base_idx]: + drop_count = drop_implied_oncurve_points(*master_fonts) + log.info( + "Dropped %s on-curve points from simple glyphs in the 'glyf' table", + drop_count, + ) + # Copy the base master to work from it vf = deepcopy(master_fonts[ds.base_idx]) @@ -1228,6 +1278,14 @@ def main(args=None): action="store_false", help="do not rebuild variable COLR table to optimize COLR layer reuse", ) + parser.add_argument( + "--drop-implied-oncurves", + action="store_true", + help=( + "drop on-curve points that can be implied when exactly in the middle of " + "two off-curve points (only applies to TrueType fonts)" + ), + ) parser.add_argument( "--master-finder", default="master_ttf_interpolatable/{stem}.ttf", @@ -1312,6 +1370,7 @@ def main(args=None): exclude=options.exclude, optimize=options.optimize, colr_layer_reuse=options.colr_layer_reuse, + drop_implied_oncurves=options.drop_implied_oncurves, ) for vf_name, vf in vfs.items(): diff --git a/Tests/varLib/data/DropOnCurves.designspace b/Tests/varLib/data/DropOnCurves.designspace new file mode 100644 index 000000000..a4769aa2f --- /dev/null +++ b/Tests/varLib/data/DropOnCurves.designspace @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_drop_oncurves/TestFamily-Master1.ttx b/Tests/varLib/data/master_ttx_drop_oncurves/TestFamily-Master1.ttx new file mode 100644 index 000000000..14e64a750 --- /dev/null +++ b/Tests/varLib/data/master_ttx_drop_oncurves/TestFamily-Master1.ttx @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test Family + + + Regular + + + Version 1.001;ADBO;Test Family Regular + + + Test Family + + + Version 1.001 + + + TestFamily-Master1 + + + Frank Grießhammer + + + Master 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_drop_oncurves/TestFamily-Master2.ttx b/Tests/varLib/data/master_ttx_drop_oncurves/TestFamily-Master2.ttx new file mode 100644 index 000000000..1559071a5 --- /dev/null +++ b/Tests/varLib/data/master_ttx_drop_oncurves/TestFamily-Master2.ttx @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test Family + + + Regular + + + Version 1.001;ADBO;Test Family Regular + + + Test Family + + + Version 1.001 + + + TestFamily-Master2 + + + Frank Grießhammer + + + Master 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/DropOnCurves.ttx b/Tests/varLib/data/test_results/DropOnCurves.ttx new file mode 100644 index 000000000..4bfd36ad0 --- /dev/null +++ b/Tests/varLib/data/test_results/DropOnCurves.ttx @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Weight + + + Test Family + + + Regular + + + Version 1.001;ADBO;Test Family Regular + + + Test Family + + + Version 1.001 + + + TestFamily-Master1 + + + Frank Grießhammer + + + Master 1 + + + Weight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wght + 0x0 + 400.0 + 400.0 + 1000.0 + 256 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/varLib_test.py b/Tests/varLib/varLib_test.py index a8f7a4574..5d5b9bbdc 100644 --- a/Tests/varLib/varLib_test.py +++ b/Tests/varLib/varLib_test.py @@ -538,6 +538,31 @@ class BuildTest(unittest.TestCase): self.assertTrue(os.path.isdir(outdir)) self.assertTrue(os.path.exists(os.path.join(outdir, "BuildMain-VF.ttf"))) + def test_varLib_main_drop_implied_oncurves(self): + self.temp_dir() + outdir = os.path.join(self.tempdir, "drop_implied_oncurves_test") + self.assertFalse(os.path.exists(outdir)) + + ttf_dir = os.path.join(outdir, "master_ttf_interpolatable") + os.makedirs(ttf_dir) + ttx_dir = self.get_test_input("master_ttx_drop_oncurves") + ttx_paths = self.get_file_list(ttx_dir, ".ttx", "TestFamily-") + for path in ttx_paths: + self.compile_font(path, ".ttf", ttf_dir) + + ds_copy = os.path.join(outdir, "DropOnCurves.designspace") + ds_path = self.get_test_input("DropOnCurves.designspace") + shutil.copy2(ds_path, ds_copy) + + finder = "%s/master_ttf_interpolatable/{stem}.ttf" % outdir + varLib_main([ds_copy, "--master-finder", finder, "--drop-implied-oncurves"]) + + vf_path = os.path.join(outdir, "DropOnCurves-VF.ttf") + varfont = TTFont(vf_path) + tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"] + expected_ttx_path = self.get_test_output("DropOnCurves.ttx") + self.expect_ttx(varfont, expected_ttx_path, tables) + def test_varLib_build_many_no_overwrite_STAT(self): # Ensure that varLib.build_many doesn't overwrite a pre-existing STAT table, # e.g. one built by feaLib from features.fea; the VF simply should inherit the From 5b93100616199945e23cdcd664242b68ad9bf0f8 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Mon, 5 Jun 2023 12:11:17 +0100 Subject: [PATCH 4/4] also check endPtsOfContours in dropImpliedOnCurvePoints as per review https://github.com/fonttools/fonttools/pull/3147#discussion_r1214708207 Also, don't do same work multiple times when dropping points from flags/endPtsOfContours arrays since these are supposed to be the same for all interpolatable glyphs --- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 72 ++++++++++++++------------ Tests/ttLib/tables/_g_l_y_f_test.py | 19 ++++++- 2 files changed, 56 insertions(+), 35 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index deaff166a..2324dd22a 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 types import SimpleNamespace from typing import Set log = logging.getLogger(__name__) @@ -1558,8 +1559,9 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]: Reference: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html """ - numContours = None - flags = None + staticAttributes = SimpleNamespace( + numberOfContours=None, flags=None, endPtsOfContours=None + ) drop = None simple_glyphs = [] for i, glyph in enumerate(interpolatable_glyphs): @@ -1567,26 +1569,23 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]: # 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}" - ) + for attr in staticAttributes.__dict__: + expected = getattr(staticAttributes, attr) + found = getattr(glyph, attr) + if expected is None: + setattr(staticAttributes, attr, found) + elif expected != found: + raise ValueError( + f"Incompatible {attr} for glyph at master index {i}: " + f"expected {expected}, found {found}" + ) may_drop = set() start = 0 coords = glyph.coordinates - for last in glyph.endPtsOfContours: + flags = staticAttributes.flags + endPtsOfContours = staticAttributes.endPtsOfContours + for last in endPtsOfContours: for i in range(start, last + 1): if not (flags[i] & flagOnCurve): continue @@ -1613,27 +1612,32 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]: if drop: # Do the actual dropping + flags = staticAttributes.flags + assert flags is not None + newFlags = array.array( + "B", (flags[i] for i in range(len(flags)) if i not in drop) + ) + + endPts = staticAttributes.endPtsOfContours + assert endPts is not None + 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 + for glyph in simple_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.flags = newFlags glyph.endPtsOfContours = newEndPts return drop if drop is not None else set() diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py index f2f300da9..0e9b1b409 100644 --- a/Tests/ttLib/tables/_g_l_y_f_test.py +++ b/Tests/ttLib/tables/_g_l_y_f_test.py @@ -965,7 +965,7 @@ def test_dropImpliedOnCurvePoints_incompatible_number_of_contours(): 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"): + with pytest.raises(ValueError, match="Incompatible numberOfContours"): dropImpliedOnCurvePoints(glyph1, glyph2) @@ -986,6 +986,23 @@ def test_dropImpliedOnCurvePoints_incompatible_flags(): dropImpliedOnCurvePoints(glyph1, glyph2) +def test_dropImpliedOnCurvePoints_incompatible_endPtsOfContours(): + glyph1 = Glyph() + glyph1.numberOfContours = 2 + glyph1.endPtsOfContours = [2, 6] + glyph1.flags = array.array("B", [1, 1, 1, 1, 1, 1, 1]) + glyph1.coordinates = GlyphCoordinates([(i, i) for i in range(7)]) + + glyph2 = Glyph() + glyph2.numberOfContours = 2 + glyph2.endPtsOfContours = [3, 6] + glyph2.flags = array.array("B", [1, 1, 1, 1, 1, 1, 1]) + glyph2.coordinates = GlyphCoordinates([(i, i) for i in range(7)]) + + with pytest.raises(ValueError, match="Incompatible endPtsOfContours"): + dropImpliedOnCurvePoints(glyph1, glyph2) + + if __name__ == "__main__": import sys