From c7be064ef6f55e5ce1021550f80e08343ac55269 Mon Sep 17 00:00:00 2001 From: David Jones Date: Mon, 3 Jun 2024 17:01:20 +0100 Subject: [PATCH 01/13] Test absent hex attribute on unicode element (currently fails) --- Tests/ufoLib/GLIF1_test.py | 16 ++++++++++++++++ Tests/ufoLib/GLIF2_test.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/Tests/ufoLib/GLIF1_test.py b/Tests/ufoLib/GLIF1_test.py index c4991ca3e..64af3f6a2 100644 --- a/Tests/ufoLib/GLIF1_test.py +++ b/Tests/ufoLib/GLIF1_test.py @@ -300,6 +300,22 @@ class TestGLIF1(unittest.TestCase): self.assertRaises(GlifLibError, self.pyToGLIF, py) self.assertRaises(GlifLibError, self.glifToPy, glif) + def testUnicodes_hex_present(self): + """Test that a present element must have a + 'hex' attribute; by testing that an invalid + element raises an appropriate error. + """ + + # illegal + glif = """ + + + + + + """ + self.assertRaises(GlifLibError, self.glifToPy, glif) + def testNote(self): glif = """ diff --git a/Tests/ufoLib/GLIF2_test.py b/Tests/ufoLib/GLIF2_test.py index d8c96d653..f4ee4271f 100644 --- a/Tests/ufoLib/GLIF2_test.py +++ b/Tests/ufoLib/GLIF2_test.py @@ -300,6 +300,22 @@ class TestGLIF2(unittest.TestCase): self.assertRaises(GlifLibError, self.pyToGLIF, py) self.assertRaises(GlifLibError, self.glifToPy, glif) + def testUnicodes_hex_present(self): + """Test that a present element must have a + 'hex' attribute; by testing that an invalid + element raises an appropriate error. + """ + + # illegal + glif = """ + + + + + + """ + self.assertRaises(GlifLibError, self.glifToPy, glif) + def testNote(self): glif = """ From fb30c9822b3827486ea86a3f394103f3c50d42e9 Mon Sep 17 00:00:00 2001 From: David Jones Date: Mon, 3 Jun 2024 17:02:34 +0100 Subject: [PATCH 02/13] Verify that unicode elements have hex attribute --- Lib/fontTools/ufoLib/glifLib.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lib/fontTools/ufoLib/glifLib.py b/Lib/fontTools/ufoLib/glifLib.py index 62e87db0d..24ab284e4 100755 --- a/Lib/fontTools/ufoLib/glifLib.py +++ b/Lib/fontTools/ufoLib/glifLib.py @@ -1191,6 +1191,10 @@ def _readGlyphFromTreeFormat1( haveSeenAdvance = True _readAdvance(glyphObject, element) elif element.tag == "unicode": + if element.get("hex") is None: + raise GlifLibError( + "A unicode element is missing its required hex attribute." + ) try: v = element.get("hex") v = int(v, 16) @@ -1254,6 +1258,10 @@ def _readGlyphFromTreeFormat2( haveSeenAdvance = True _readAdvance(glyphObject, element) elif element.tag == "unicode": + if element.get("hex") is None: + raise GlifLibError( + "A unicode element is missing its required hex attribute." + ) try: v = element.get("hex") v = int(v, 16) From 7d39064a3674e413ed24577c7ddabb71ad47af6b Mon Sep 17 00:00:00 2001 From: David Jones Date: Mon, 3 Jun 2024 17:09:54 +0100 Subject: [PATCH 03/13] Fix oops: GLIF2 test should have format=2 --- Tests/ufoLib/GLIF2_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ufoLib/GLIF2_test.py b/Tests/ufoLib/GLIF2_test.py index f4ee4271f..a193ab5c5 100644 --- a/Tests/ufoLib/GLIF2_test.py +++ b/Tests/ufoLib/GLIF2_test.py @@ -308,7 +308,7 @@ class TestGLIF2(unittest.TestCase): # illegal glif = """ - + From aa2d9196c0fa9dd42c4b430d244cad6e1e4b8a32 Mon Sep 17 00:00:00 2001 From: David Jones Date: Thu, 18 Jul 2024 19:29:41 +0100 Subject: [PATCH 04/13] Lift .get("hex") out of try:; and avoid re-evaluating it --- Lib/fontTools/ufoLib/glifLib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/ufoLib/glifLib.py b/Lib/fontTools/ufoLib/glifLib.py index 24ab284e4..8f5cba064 100755 --- a/Lib/fontTools/ufoLib/glifLib.py +++ b/Lib/fontTools/ufoLib/glifLib.py @@ -1191,12 +1191,12 @@ def _readGlyphFromTreeFormat1( haveSeenAdvance = True _readAdvance(glyphObject, element) elif element.tag == "unicode": - if element.get("hex") is None: + v = element.get("hex") + if v is None: raise GlifLibError( "A unicode element is missing its required hex attribute." ) try: - v = element.get("hex") v = int(v, 16) if v not in unicodes: unicodes.append(v) @@ -1258,12 +1258,12 @@ def _readGlyphFromTreeFormat2( haveSeenAdvance = True _readAdvance(glyphObject, element) elif element.tag == "unicode": - if element.get("hex") is None: + v = element.get("hex") + if v is None: raise GlifLibError( "A unicode element is missing its required hex attribute." ) try: - v = element.get("hex") v = int(v, 16) if v not in unicodes: unicodes.append(v) From e8146a6d0725d398cfa110cba683946ee762f8e2 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 9 Oct 2024 11:04:25 -0600 Subject: [PATCH 05/13] [glyf] Add optimizeSize option Set to True by default. Can be turned to False on the table, or at Glyph() compile time. Also fixes Glyph's draw() to expand the glyph first. Otherwise it was failing. --- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 77 ++++++++++++++++++++++++-- Tests/ttLib/tables/_g_l_y_f_test.py | 31 +++++++++++ 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index fa11cf8f4..bc7d4bf1e 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -713,7 +713,9 @@ class Glyph(object): else: self.decompileCoordinates(data) - def compile(self, glyfTable, recalcBBoxes=True, *, boundsDone=None): + def compile( + self, glyfTable, recalcBBoxes=True, *, boundsDone=None, optimizeSize=None + ): if hasattr(self, "data"): if recalcBBoxes: # must unpack glyph in order to recalculate bounding box @@ -730,7 +732,9 @@ class Glyph(object): if self.isComposite(): data = data + self.compileComponents(glyfTable) else: - data = data + self.compileCoordinates() + if optimizeSize is None: + optimizeSize = getattr(glyfTable, "optimizeSize", True) + data = data + self.compileCoordinates(optimizeSize=optimizeSize) return data def toXML(self, writer, ttFont): @@ -976,7 +980,7 @@ class Glyph(object): data = data + struct.pack(">h", len(instructions)) + instructions return data - def compileCoordinates(self): + def compileCoordinates(self, *, optimizeSize=True): assert len(self.coordinates) == len(self.flags) data = [] endPtsOfContours = array.array("H", self.endPtsOfContours) @@ -991,9 +995,12 @@ class Glyph(object): deltas.toInt() deltas.absoluteToRelative() - # TODO(behdad): Add a configuration option for this? - deltas = self.compileDeltasGreedy(self.flags, deltas) - # deltas = self.compileDeltasOptimal(self.flags, deltas) + if optimizeSize: + # TODO(behdad): Add a configuration option for this? + deltas = self.compileDeltasGreedy(self.flags, deltas) + # deltas = self.compileDeltasOptimal(self.flags, deltas) + else: + deltas = self.compileDeltasForSpeed(self.flags, deltas) data.extend(deltas) return b"".join(data) @@ -1110,6 +1117,63 @@ class Glyph(object): return (compressedFlags, compressedXs, compressedYs) + def compileDeltasForSpeed(self, flags, deltas): + # uses widest representation needed, for all deltas. + compressedFlags = bytearray() + compressedXs = bytearray() + compressedYs = bytearray() + + # Compute the necessary width for each axis + xs = [d[0] for d in deltas] + ys = [d[1] for d in deltas] + minX, minY, maxX, maxY = min(xs), min(ys), max(xs), max(ys) + xZero = minX == 0 and maxX == 0 + yZero = minY == 0 and maxY == 0 + xShort = -255 <= minX <= maxX <= 255 + yShort = -255 <= minY <= maxY <= 255 + + lastflag = None + repeat = 0 + for flag, (x, y) in zip(flags, deltas): + # Oh, the horrors of TrueType + # do x + if xZero: + flag = flag | flagXsame + elif xShort: + flag = flag | flagXShort + if x > 0: + flag = flag | flagXsame + else: + x = -x + compressedXs.append(x) + else: + compressedXs.extend(struct.pack(">h", x)) + # do y + if yZero: + flag = flag | flagYsame + elif yShort: + flag = flag | flagYShort + if y > 0: + flag = flag | flagYsame + else: + y = -y + compressedYs.append(y) + else: + compressedYs.extend(struct.pack(">h", y)) + # handle repeating flags + if flag == lastflag and repeat != 255: + repeat = repeat + 1 + if repeat == 1: + compressedFlags.append(flag) + else: + compressedFlags[-2] = flag | flagRepeat + compressedFlags[-1] = repeat + else: + repeat = 0 + compressedFlags.append(flag) + lastflag = flag + return (compressedFlags, compressedXs, compressedYs) + def recalcBounds(self, glyfTable, *, boundsDone=None): """Recalculates the bounds of the glyph. @@ -1404,6 +1468,7 @@ class Glyph(object): pen.addComponent(glyphName, transform) return + self.expand(glyfTable) coordinates, endPts, flags = self.getCoordinates(glyfTable) if offset: coordinates = coordinates.copy() diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py index 9a3fd2eaf..10e053c4d 100644 --- a/Tests/ttLib/tables/_g_l_y_f_test.py +++ b/Tests/ttLib/tables/_g_l_y_f_test.py @@ -707,6 +707,37 @@ class GlyphComponentTest: '' ] + def test_compile_for_speed(self): + glyph = Glyph() + glyph.numberOfContours = 1 + glyph.coordinates = GlyphCoordinates( + [(0, 0), (1, 0), (1, 0), (1, 1), (1, 1), (0, 1), (0, 1)] + ) + glyph.flags = array.array("B", [flagOnCurve] + [flagCubic] * 6) + glyph.endPtsOfContours = [6] + glyph.program = ttProgram.Program() + + glyph.expand(None) + sizeBytes = glyph.compile(None, optimizeSize=True) + glyph.expand(None) + speedBytes = glyph.compile(None, optimizeSize=False) + + assert len(sizeBytes) < len(speedBytes) + + for data in sizeBytes, speedBytes: + glyph = Glyph(data) + + pen = RecordingPen() + glyph.draw(pen, None) + + assert pen.value == [ + ("moveTo", ((0, 0),)), + ("curveTo", ((1, 0), (1, 0), (1.0, 0.5))), + ("curveTo", ((1, 1), (1, 1), (0.5, 1.0))), + ("curveTo", ((0, 1), (0, 1), (0, 0))), + ("closePath", ()), + ] + def test_fromXML_reference_points(self): comp = GlyphComponent() for name, attrs, content in parseXML( From f7ecc6fe65cb7fac1142548c8e4f64363b002114 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sat, 12 Oct 2024 01:14:48 +0300 Subject: [PATCH 06/13] [removeOverlaps] Add test for handling CFF.Private.nominalWidthX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test currently fails because we don’t take CFF.Private.nominalWidthX into account when creating new CharString after overlap removal. --- Tests/ttLib/data/IBMPlexSans-Bold.subset.otf | Bin 0 -> 884 bytes Tests/ttLib/removeOverlaps_test.py | 24 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 Tests/ttLib/data/IBMPlexSans-Bold.subset.otf diff --git a/Tests/ttLib/data/IBMPlexSans-Bold.subset.otf b/Tests/ttLib/data/IBMPlexSans-Bold.subset.otf new file mode 100644 index 0000000000000000000000000000000000000000..23a0301fc1bcd77e2658531c7eadb2f187b18b70 GIT binary patch literal 884 zcmZWnT}TvB6h3!;+$~2pUG2d&y`e!r=3g2?5u{{9h_WjbB-ZTCY`Zz`EVGMg5B@xe zQjwU(Km(jW+z;+TrQA{}jmYeV zZ`r9RWj9MeCPX?rb!*{3ed}qRsunK8sH5RwswReM(o!Soctka*rbpw6csgz-s2Qc{ zm`*R5mX@lcQPVV%mWiO6&}cjnQIl%e(78&tEYqrQH5lW=hD}# zmCdm_|KRWT?Fg+RxyUNqMHH;Ez-Gkbe>fWrPxl4fPPf?I=8n7L3@z)d_76UPIz4;0 z+W*k?Fkg97Xqk!C8y+UUTmJgw@#MsVs?Fn`iFkBM3lxvqjy%Km+N{;SXBW6J+nJy7 zKm{07ZDULu(_06B`f6}cssF)zyR0**?*@Csxc zoGZc^1W)BH=>v@%_7b7cE6M_<#`y;6?@)g2gpwc$ib}SY)JcT+k>@poP$axSLvt1J ks(5r6?wl>dJQS^jFZv{y&F1&aqkMEuje_hFpY8vjKai373jhEB literal 0 HcmV?d00001 diff --git a/Tests/ttLib/removeOverlaps_test.py b/Tests/ttLib/removeOverlaps_test.py index 1320c9be5..33493f68f 100644 --- a/Tests/ttLib/removeOverlaps_test.py +++ b/Tests/ttLib/removeOverlaps_test.py @@ -1,9 +1,13 @@ import logging import pytest +from pathlib import Path pathops = pytest.importorskip("pathops") -from fontTools.ttLib.removeOverlaps import _simplify, _round_path +from fontTools.ttLib import TTFont +from fontTools.ttLib.removeOverlaps import removeOverlaps, _simplify, _round_path + +DATA_DIR = Path(__file__).parent / "data" def test_pathops_simplify_bug_workaround(caplog): @@ -49,3 +53,21 @@ def test_pathops_simplify_bug_workaround(caplog): expected.close() assert expected == _round_path(result, round=lambda v: round(v, 3)) + + +def test_CFF_CharString_width_nominalWidthX(): + font_path = DATA_DIR / "IBMPlexSans-Bold.subset.otf" + font = TTFont(str(font_path)) + + assert font["hmtx"]["OE"][0] == 998 + + # calcBounds() has the side effect of setting the width attribute + font["CFF "].cff[0].CharStrings["OE"].calcBounds({}) + assert font["CFF "].cff[0].CharStrings["OE"].width == font["hmtx"]["OE"][0] + + removeOverlaps(font) + + assert font["hmtx"]["OE"][0] == 998 + + font["CFF "].cff[0].CharStrings["OE"].calcBounds({}) + assert font["CFF "].cff[0].CharStrings["OE"].width == font["hmtx"]["OE"][0] From 40b525c1e3cc20b4b64004b8e3224a67adc2adf1 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sat, 12 Oct 2024 01:22:17 +0300 Subject: [PATCH 07/13] [removeOverlaps] Fix CFF CharString width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The width argument of `T2CharStringPen()` is inserted directly into the CharString program, so it must be relative to Private.nominalWidthX, but CharString.width is a calculated absolute value. Some implementations, notably Adobe’s, will use the width from the CFF CharString instead of the one from hmtx table. Fixes https://github.com/fonttools/fonttools/issues/3658 --- Lib/fontTools/ttLib/removeOverlaps.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/removeOverlaps.py b/Lib/fontTools/ttLib/removeOverlaps.py index 312b56b29..4ab8a1a64 100644 --- a/Lib/fontTools/ttLib/removeOverlaps.py +++ b/Lib/fontTools/ttLib/removeOverlaps.py @@ -87,7 +87,8 @@ def ttfGlyphFromSkPath(path: pathops.Path) -> _g_l_y_f.Glyph: def _charString_from_SkPath( path: pathops.Path, charString: T2CharString ) -> T2CharString: - t2Pen = T2CharStringPen(width=charString.width, glyphSet=None) + width = charString.width - charString.private.nominalWidthX + t2Pen = T2CharStringPen(width=width, glyphSet=None) path.draw(t2Pen) return t2Pen.getCharString(charString.private, charString.globalSubrs) From 101ff1508c8acfe647c56f26df7a118b968f21cf Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sat, 12 Oct 2024 01:39:50 +0300 Subject: [PATCH 08/13] [removeOverlaps] Pass None to T2CharStringPen if widths equals defaultWidthX --- Lib/fontTools/ttLib/removeOverlaps.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/removeOverlaps.py b/Lib/fontTools/ttLib/removeOverlaps.py index 4ab8a1a64..6dadf4aa5 100644 --- a/Lib/fontTools/ttLib/removeOverlaps.py +++ b/Lib/fontTools/ttLib/removeOverlaps.py @@ -87,7 +87,10 @@ def ttfGlyphFromSkPath(path: pathops.Path) -> _g_l_y_f.Glyph: def _charString_from_SkPath( path: pathops.Path, charString: T2CharString ) -> T2CharString: - width = charString.width - charString.private.nominalWidthX + if charString.width == charString.private.defaultWidthX: + width = None + else: + width = charString.width - charString.private.nominalWidthX t2Pen = T2CharStringPen(width=width, glyphSet=None) path.draw(t2Pen) return t2Pen.getCharString(charString.private, charString.globalSubrs) From c79cb346e6d1b3f78ce092ed15f800d5050ebafd Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 14 Oct 2024 17:49:04 +0200 Subject: [PATCH 09/13] Update sphinx from 8.0.2 to 8.1.3 --- Doc/docs-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/docs-requirements.txt b/Doc/docs-requirements.txt index 3ecc993b2..a86731c63 100644 --- a/Doc/docs-requirements.txt +++ b/Doc/docs-requirements.txt @@ -1,4 +1,4 @@ -sphinx==8.0.2 +sphinx==8.1.3 sphinx_rtd_theme==3.0.0 reportlab==4.2.5 freetype-py==2.5.1 From 1eef3f25961331a89c399a080e7e833500f8cce0 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 14 Oct 2024 17:49:05 +0200 Subject: [PATCH 10/13] Update sphinx_rtd_theme from 3.0.0 to 3.0.1 --- Doc/docs-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/docs-requirements.txt b/Doc/docs-requirements.txt index a86731c63..30adbe51c 100644 --- a/Doc/docs-requirements.txt +++ b/Doc/docs-requirements.txt @@ -1,4 +1,4 @@ sphinx==8.1.3 -sphinx_rtd_theme==3.0.0 +sphinx_rtd_theme==3.0.1 reportlab==4.2.5 freetype-py==2.5.1 From 5143420bf998e3d3f2db7c991331f976dc8fdc14 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 14 Oct 2024 17:49:05 +0200 Subject: [PATCH 11/13] Update black from 24.8.0 to 24.10.0 --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 13bebe81f..036591192 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -6,4 +6,4 @@ mypy>=0.782 readme_renderer[md]>=43.0 # Pin black as each version could change formatting, breaking CI randomly. -black==24.8.0 +black==24.10.0 From 246cc85c29ebd04b23e5aaf242f740e76cbdb6d3 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 14 Oct 2024 17:49:06 +0200 Subject: [PATCH 12/13] Update ufo2ft from 3.3.0 to 3.3.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 861a91aa7..9431aade0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ fs==2.4.16 skia-pathops==0.8.0.post1; platform_python_implementation != "PyPy" # this is only required to run Tests/cu2qu/{ufo,cli}_test.py ufoLib2==0.16.0 -ufo2ft==3.3.0 +ufo2ft==3.3.1 pyobjc==10.3.1; sys_platform == "darwin" freetype-py==2.5.1 uharfbuzz==0.41.0 From ce3b747699fea2ee7806f7bba34431bf305868c0 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 14 Oct 2024 17:49:06 +0200 Subject: [PATCH 13/13] Update glyphslib from 6.9.0 to 6.9.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9431aade0..6497646f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,6 @@ ufo2ft==3.3.1 pyobjc==10.3.1; sys_platform == "darwin" freetype-py==2.5.1 uharfbuzz==0.41.0 -glyphsLib==6.9.0 # this is only required to run Tests/varLib/interpolatable_test.py +glyphsLib==6.9.2 # this is only required to run Tests/varLib/interpolatable_test.py lxml==5.3.0 sympy==1.13.3