From 6002ee15a340fded0e62381ef16858b834f6082d Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 28 Jul 2023 17:25:07 +0100 Subject: [PATCH 1/2] add convertCubics option for compileVariableTTF compileTTF already had a way to completely skip cu2qu, but the interpolatable/variable compile paths did not. Add it --- Lib/ufo2ft/__init__.py | 2 ++ Lib/ufo2ft/preProcessor.py | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Lib/ufo2ft/__init__.py b/Lib/ufo2ft/__init__.py index 7ac1e5434..47d3ad840 100644 --- a/Lib/ufo2ft/__init__.py +++ b/Lib/ufo2ft/__init__.py @@ -280,6 +280,7 @@ def compileTTF(ufo, **kwargs): **dict( preProcessorClass=TTFInterpolatablePreProcessor, outlineCompilerClass=OutlineTTFCompiler, + convertCubics=True, cubicConversionError=None, reverseDirection=True, flattenComponents=False, @@ -564,6 +565,7 @@ def compileFeatures( **dict( preProcessorClass=TTFInterpolatablePreProcessor, outlineCompilerClass=OutlineTTFCompiler, + convertCubics=True, cubicConversionError=None, reverseDirection=True, flattenComponents=False, diff --git a/Lib/ufo2ft/preProcessor.py b/Lib/ufo2ft/preProcessor.py index 46c2d6ced..532e77bdf 100644 --- a/Lib/ufo2ft/preProcessor.py +++ b/Lib/ufo2ft/preProcessor.py @@ -262,6 +262,7 @@ def __init__( ufos, inplace=False, flattenComponents=False, + convertCubics=True, conversionError=None, reverseDirection=True, rememberCurveType=True, @@ -289,6 +290,7 @@ def __init__( ) for ufo, layerName in zip(ufos, layerNames) ] + self.convertCubics = convertCubics self._conversionErrors = [ (conversionError or DEFAULT_MAX_ERR) * getAttrWithFallback(ufo.info, "unitsPerEm") @@ -334,14 +336,15 @@ def process(self): for func in funcs: func(ufo, glyphSet) - fonts_to_quadratic( - self.glyphSets, - max_err=self._conversionErrors, - reverse_direction=self._reverseDirection, - dump_stats=True, - remember_curve_type=self._rememberCurveType and self.inplace, - all_quadratic=self.allQuadratic, - ) + if self.convertCubics: + fonts_to_quadratic( + self.glyphSets, + max_err=self._conversionErrors, + reverse_direction=self._reverseDirection, + dump_stats=True, + remember_curve_type=self._rememberCurveType and self.inplace, + all_quadratic=self.allQuadratic, + ) # TrueType fonts cannot mix contours and components, so pick out all glyphs # that have contours (`bool(len(g)) == True`) and decompose their From 376b042baad49c86f6af593428fec288d897e8ff Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 28 Jul 2023 17:25:50 +0100 Subject: [PATCH 2/2] outlineCompiler: check that no cubics sneak in when glyphDataFormat=0 to prevent one inadvertently produce an invalid TTF --- Lib/ufo2ft/outlineCompiler.py | 13 +++++++- tests/outlineCompiler_test.py | 62 +++++++++++++++++------------------ 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/Lib/ufo2ft/outlineCompiler.py b/Lib/ufo2ft/outlineCompiler.py index 28771a0bc..02e583e09 100644 --- a/Lib/ufo2ft/outlineCompiler.py +++ b/Lib/ufo2ft/outlineCompiler.py @@ -22,7 +22,7 @@ from fontTools.pens.ttGlyphPen import TTGlyphPointPen from fontTools.ttLib import TTFont, newTable from fontTools.ttLib.standardGlyphOrder import standardGlyphOrder -from fontTools.ttLib.tables._g_l_y_f import Glyph +from fontTools.ttLib.tables._g_l_y_f import Glyph, flagCubic from fontTools.ttLib.tables._h_e_a_d import mac_epoch_diff from fontTools.ttLib.tables.O_S_2f_2 import Panose @@ -1462,6 +1462,7 @@ def compileGlyphs(self): allGlyphs = self.allGlyphs ttGlyphs = {} round = otRound if self.roundCoordinates else noRound + glyphDataFormat = self.glyphDataFormat for name in self.glyphOrder: glyph = allGlyphs[name] pen = TTGlyphPointPen(allGlyphs) @@ -1475,6 +1476,16 @@ def compileGlyphs(self): dropImpliedOnCurves=self.dropImpliedOnCurves, round=round, ) + if ( + glyphDataFormat == 0 + and ttGlyph.numberOfContours > 0 + and any(f & flagCubic for f in ttGlyph.flags) + ): + raise ValueError( + f"{name!r} has cubic Bezier curves, but glyphDataFormat=0; " + "either convert to quadratic (convertCubics=True) or use " + "allQuadratic=False so that glyphDataFormat=1." + ) ttGlyphs[name] = ttGlyph return ttGlyphs diff --git a/tests/outlineCompiler_test.py b/tests/outlineCompiler_test.py index 86a02f89b..7d0365299 100644 --- a/tests/outlineCompiler_test.py +++ b/tests/outlineCompiler_test.py @@ -70,22 +70,22 @@ def emptyufo(FontClass): class OutlineTTFCompilerTest: - def test_compile_with_gasp(self, testufo): - compiler = OutlineTTFCompiler(testufo) + def test_compile_with_gasp(self, quadufo): + compiler = OutlineTTFCompiler(quadufo) compiler.compile() assert "gasp" in compiler.otf assert compiler.otf["gasp"].gaspRange == {7: 10, 65535: 15} - def test_compile_without_gasp(self, testufo): - testufo.info.openTypeGaspRangeRecords = None - compiler = OutlineTTFCompiler(testufo) + def test_compile_without_gasp(self, quadufo): + quadufo.info.openTypeGaspRangeRecords = None + compiler = OutlineTTFCompiler(quadufo) compiler.compile() assert "gasp" not in compiler.otf - def test_compile_empty_gasp(self, testufo): + def test_compile_empty_gasp(self, quadufo): # ignore empty gasp - testufo.info.openTypeGaspRangeRecords = [] - compiler = OutlineTTFCompiler(testufo) + quadufo.info.openTypeGaspRangeRecords = [] + compiler = OutlineTTFCompiler(quadufo) compiler.compile() assert "gasp" not in compiler.otf @@ -150,10 +150,10 @@ def test_no_contour_glyphs(self, testufo): assert compiler.otf["hhea"].minRightSideBearing == 0 assert compiler.otf["hhea"].xMaxExtent == 0 - def test_os2_no_widths(self, testufo): - for glyph in testufo: + def test_os2_no_widths(self, quadufo): + for glyph in quadufo: glyph.width = 0 - compiler = OutlineTTFCompiler(testufo) + compiler = OutlineTTFCompiler(quadufo) compiler.compile() assert compiler.otf["OS/2"].xAvgCharWidth == 0 @@ -220,8 +220,8 @@ def test_contour_starts_with_offcurve_point(self, emptyufo): assert endPts == [4] assert list(flags) == [0, 0, 0, 0, 1] - def test_setupTable_meta(self, testufo): - testufo.lib["public.openTypeMeta"] = { + def test_setupTable_meta(self, quadufo): + quadufo.lib["public.openTypeMeta"] = { "appl": b"BEEF", "bild": b"AAAA", "dlng": ["en-Latn", "nl-Latn"], @@ -231,7 +231,7 @@ def test_setupTable_meta(self, testufo): "PRIU": "Some private unicode string…", } - compiler = OutlineTTFCompiler(testufo) + compiler = OutlineTTFCompiler(quadufo) ttFont = compiler.compile() meta = ttFont["meta"] @@ -243,13 +243,13 @@ def test_setupTable_meta(self, testufo): assert meta.data["PRIA"] == b"Some private ascii string" assert meta.data["PRIU"] == "Some private unicode string…".encode("utf-8") - def test_setupTable_name(self, testufo): - compiler = OutlineTTFCompiler(testufo) + def test_setupTable_name(self, quadufo): + compiler = OutlineTTFCompiler(quadufo) compiler.compile() actual = compiler.otf["name"].getName(1, 3, 1, 1033).string assert actual == "Some Font Regular (Style Map Family Name)" - testufo.info.openTypeNameRecords.append( + quadufo.info.openTypeNameRecords.append( { "nameID": 1, "platformID": 3, @@ -258,20 +258,20 @@ def test_setupTable_name(self, testufo): "string": "Custom Name for Windows", } ) - compiler = OutlineTTFCompiler(testufo) + compiler = OutlineTTFCompiler(quadufo) compiler.compile() actual = compiler.otf["name"].getName(1, 3, 1, 1033).string assert actual == "Custom Name for Windows" - def test_post_underline_without_public_key(self, testufo): - compiler = OutlineTTFCompiler(testufo) + def test_post_underline_without_public_key(self, quadufo): + compiler = OutlineTTFCompiler(quadufo) compiler.compile() actual = compiler.otf["post"].underlinePosition assert actual == -200 - def test_post_underline_with_public_key(self, testufo): - testufo.lib[OPENTYPE_POST_UNDERLINE_POSITION_KEY] = -485 - compiler = OutlineTTFCompiler(testufo) + def test_post_underline_with_public_key(self, quadufo): + quadufo.lib[OPENTYPE_POST_UNDERLINE_POSITION_KEY] = -485 + compiler = OutlineTTFCompiler(quadufo) compiler.compile() actual = compiler.otf["post"].underlinePosition assert actual == -485 @@ -687,7 +687,7 @@ def test_underline_ps_rounding(self, testufo): class GlyphOrderTest: - def test_compile_original_glyph_order(self, testufo): + def test_compile_original_glyph_order(self, quadufo): DEFAULT_ORDER = [ ".notdef", "space", @@ -704,11 +704,11 @@ def test_compile_original_glyph_order(self, testufo): "k", "l", ] - compiler = OutlineTTFCompiler(testufo) + compiler = OutlineTTFCompiler(quadufo) compiler.compile() assert compiler.otf.getGlyphOrder() == DEFAULT_ORDER - def test_compile_tweaked_glyph_order(self, testufo): + def test_compile_tweaked_glyph_order(self, quadufo): NEW_ORDER = [ ".notdef", "space", @@ -725,12 +725,12 @@ def test_compile_tweaked_glyph_order(self, testufo): "k", "l", ] - testufo.lib["public.glyphOrder"] = NEW_ORDER - compiler = OutlineTTFCompiler(testufo) + quadufo.lib["public.glyphOrder"] = NEW_ORDER + compiler = OutlineTTFCompiler(quadufo) compiler.compile() assert compiler.otf.getGlyphOrder() == NEW_ORDER - def test_compile_strange_glyph_order(self, testufo): + def test_compile_strange_glyph_order(self, quadufo): """Move space and .notdef to end of glyph ids ufo2ft always puts .notdef first. """ @@ -751,8 +751,8 @@ def test_compile_strange_glyph_order(self, testufo): "k", "l", ] - testufo.lib["public.glyphOrder"] = NEW_ORDER - compiler = OutlineTTFCompiler(testufo) + quadufo.lib["public.glyphOrder"] = NEW_ORDER + compiler = OutlineTTFCompiler(quadufo) compiler.compile() assert compiler.otf.getGlyphOrder() == EXPECTED_ORDER