1import os 2 3from fontTools.misc.loggingTools import CapturingLogHandler 4from fontTools.cu2qu.ufo import ( 5 fonts_to_quadratic, 6 font_to_quadratic, 7 glyphs_to_quadratic, 8 glyph_to_quadratic, 9 logger, 10 CURVE_TYPE_LIB_KEY, 11) 12from fontTools.cu2qu.errors import ( 13 IncompatibleSegmentNumberError, 14 IncompatibleSegmentTypesError, 15 IncompatibleFontsError, 16) 17 18import pytest 19 20 21ufoLib2 = pytest.importorskip("ufoLib2") 22 23DATADIR = os.path.join(os.path.dirname(__file__), "data") 24 25TEST_UFOS = [ 26 os.path.join(DATADIR, "RobotoSubset-Regular.ufo"), 27 os.path.join(DATADIR, "RobotoSubset-Bold.ufo"), 28] 29 30 31@pytest.fixture 32def fonts(): 33 return [ufoLib2.Font.open(ufo) for ufo in TEST_UFOS] 34 35 36class FontsToQuadraticTest(object): 37 def test_modified(self, fonts): 38 modified = fonts_to_quadratic(fonts) 39 assert modified 40 41 def test_stats(self, fonts): 42 stats = {} 43 fonts_to_quadratic(fonts, stats=stats) 44 assert stats == {"1": 1, "2": 79, "3": 130, "4": 2} 45 46 def test_dump_stats(self, fonts): 47 with CapturingLogHandler(logger, "INFO") as captor: 48 fonts_to_quadratic(fonts, dump_stats=True) 49 assert captor.assertRegex("New spline lengths:") 50 51 def test_remember_curve_type_quadratic(self, fonts): 52 fonts_to_quadratic(fonts, remember_curve_type=True) 53 assert fonts[0].lib[CURVE_TYPE_LIB_KEY] == "quadratic" 54 with CapturingLogHandler(logger, "INFO") as captor: 55 fonts_to_quadratic(fonts, remember_curve_type=True) 56 assert captor.assertRegex("already converted") 57 58 def test_remember_curve_type_mixed(self, fonts): 59 fonts_to_quadratic(fonts, remember_curve_type=True, all_quadratic=False) 60 assert fonts[0].lib[CURVE_TYPE_LIB_KEY] == "mixed" 61 with CapturingLogHandler(logger, "INFO") as captor: 62 fonts_to_quadratic(fonts, remember_curve_type=True) 63 assert captor.assertRegex("already converted") 64 65 def test_no_remember_curve_type(self, fonts): 66 assert CURVE_TYPE_LIB_KEY not in fonts[0].lib 67 fonts_to_quadratic(fonts, remember_curve_type=False) 68 assert CURVE_TYPE_LIB_KEY not in fonts[0].lib 69 70 def test_different_glyphsets(self, fonts): 71 del fonts[0]["a"] 72 assert "a" not in fonts[0] 73 assert "a" in fonts[1] 74 assert fonts_to_quadratic(fonts) 75 76 def test_max_err_em_float(self, fonts): 77 stats = {} 78 fonts_to_quadratic(fonts, max_err_em=0.002, stats=stats) 79 assert stats == {"1": 5, "2": 193, "3": 14} 80 81 def test_max_err_em_list(self, fonts): 82 stats = {} 83 fonts_to_quadratic(fonts, max_err_em=[0.002, 0.002], stats=stats) 84 assert stats == {"1": 5, "2": 193, "3": 14} 85 86 def test_max_err_float(self, fonts): 87 stats = {} 88 fonts_to_quadratic(fonts, max_err=4.096, stats=stats) 89 assert stats == {"1": 5, "2": 193, "3": 14} 90 91 def test_max_err_list(self, fonts): 92 stats = {} 93 fonts_to_quadratic(fonts, max_err=[4.096, 4.096], stats=stats) 94 assert stats == {"1": 5, "2": 193, "3": 14} 95 96 def test_both_max_err_and_max_err_em(self, fonts): 97 with pytest.raises(TypeError, match="Only one .* can be specified"): 98 fonts_to_quadratic(fonts, max_err=1.000, max_err_em=0.001) 99 100 def test_single_font(self, fonts): 101 assert font_to_quadratic(fonts[0], max_err_em=0.002, reverse_direction=True) 102 assert font_to_quadratic( 103 fonts[1], max_err_em=0.002, reverse_direction=True, all_quadratic=False 104 ) 105 106 107class GlyphsToQuadraticTest(object): 108 @pytest.mark.parametrize( 109 ["glyph", "expected"], 110 [("A", False), ("a", True)], # contains no curves, it is not modified 111 ids=["lines-only", "has-curves"], 112 ) 113 def test_modified(self, fonts, glyph, expected): 114 glyphs = [f[glyph] for f in fonts] 115 assert glyphs_to_quadratic(glyphs) == expected 116 117 def test_stats(self, fonts): 118 stats = {} 119 glyphs_to_quadratic([f["a"] for f in fonts], stats=stats) 120 assert stats == {"2": 1, "3": 7, "4": 3, "5": 1} 121 122 def test_max_err_float(self, fonts): 123 glyphs = [f["a"] for f in fonts] 124 stats = {} 125 glyphs_to_quadratic(glyphs, max_err=4.096, stats=stats) 126 assert stats == {"2": 11, "3": 1} 127 128 def test_max_err_list(self, fonts): 129 glyphs = [f["a"] for f in fonts] 130 stats = {} 131 glyphs_to_quadratic(glyphs, max_err=[4.096, 4.096], stats=stats) 132 assert stats == {"2": 11, "3": 1} 133 134 def test_reverse_direction(self, fonts): 135 glyphs = [f["A"] for f in fonts] 136 assert glyphs_to_quadratic(glyphs, reverse_direction=True) 137 138 def test_single_glyph(self, fonts): 139 assert glyph_to_quadratic(fonts[0]["a"], max_err=4.096, reverse_direction=True) 140 141 @pytest.mark.parametrize( 142 ["outlines", "exception", "message"], 143 [ 144 [ 145 [ 146 [ 147 ("moveTo", ((0, 0),)), 148 ("curveTo", ((1, 1), (2, 2), (3, 3))), 149 ("curveTo", ((4, 4), (5, 5), (6, 6))), 150 ("closePath", ()), 151 ], 152 [ 153 ("moveTo", ((7, 7),)), 154 ("curveTo", ((8, 8), (9, 9), (10, 10))), 155 ("closePath", ()), 156 ], 157 ], 158 IncompatibleSegmentNumberError, 159 "have different number of segments", 160 ], 161 [ 162 [ 163 [ 164 ("moveTo", ((0, 0),)), 165 ("curveTo", ((1, 1), (2, 2), (3, 3))), 166 ("closePath", ()), 167 ], 168 [ 169 ("moveTo", ((4, 4),)), 170 ("lineTo", ((5, 5),)), 171 ("closePath", ()), 172 ], 173 ], 174 IncompatibleSegmentTypesError, 175 "have incompatible segment types", 176 ], 177 ], 178 ids=[ 179 "unequal-length", 180 "different-segment-types", 181 ], 182 ) 183 def test_incompatible_glyphs(self, outlines, exception, message): 184 glyphs = [] 185 for i, outline in enumerate(outlines): 186 glyph = ufoLib2.objects.Glyph("glyph%d" % i) 187 pen = glyph.getPen() 188 for operator, args in outline: 189 getattr(pen, operator)(*args) 190 glyphs.append(glyph) 191 with pytest.raises(exception) as excinfo: 192 glyphs_to_quadratic(glyphs) 193 assert excinfo.match(message) 194 195 def test_incompatible_fonts(self): 196 font1 = ufoLib2.Font() 197 font1.info.unitsPerEm = 1000 198 glyph1 = font1.newGlyph("a") 199 pen1 = glyph1.getPen() 200 for operator, args in [ 201 ("moveTo", ((0, 0),)), 202 ("lineTo", ((1, 1),)), 203 ("endPath", ()), 204 ]: 205 getattr(pen1, operator)(*args) 206 207 font2 = ufoLib2.Font() 208 font2.info.unitsPerEm = 1000 209 glyph2 = font2.newGlyph("a") 210 pen2 = glyph2.getPen() 211 for operator, args in [ 212 ("moveTo", ((0, 0),)), 213 ("curveTo", ((1, 1), (2, 2), (3, 3))), 214 ("endPath", ()), 215 ]: 216 getattr(pen2, operator)(*args) 217 218 with pytest.raises(IncompatibleFontsError) as excinfo: 219 fonts_to_quadratic([font1, font2]) 220 assert excinfo.match("fonts contains incompatible glyphs: 'a'") 221 222 assert hasattr(excinfo.value, "glyph_errors") 223 error = excinfo.value.glyph_errors["a"] 224 assert isinstance(error, IncompatibleSegmentTypesError) 225 assert error.segments == {1: ["line", "curve"]} 226 227 def test_already_quadratic(self): 228 glyph = ufoLib2.objects.Glyph() 229 pen = glyph.getPen() 230 pen.moveTo((0, 0)) 231 pen.qCurveTo((1, 1), (2, 2)) 232 pen.closePath() 233 assert not glyph_to_quadratic(glyph) 234 235 def test_open_paths(self): 236 glyph = ufoLib2.objects.Glyph() 237 pen = glyph.getPen() 238 pen.moveTo((0, 0)) 239 pen.lineTo((1, 1)) 240 pen.curveTo((2, 2), (3, 3), (4, 4)) 241 pen.endPath() 242 assert glyph_to_quadratic(glyph) 243 # open contour is still open 244 assert glyph[-1][0].segmentType == "move" 245 246 def test_ignore_components(self): 247 glyph = ufoLib2.objects.Glyph() 248 pen = glyph.getPen() 249 pen.addComponent("a", (1, 0, 0, 1, 0, 0)) 250 pen.moveTo((0, 0)) 251 pen.curveTo((1, 1), (2, 2), (3, 3)) 252 pen.closePath() 253 assert glyph_to_quadratic(glyph) 254 assert len(glyph.components) == 1 255 256 def test_overlapping_start_end_points(self): 257 # https://github.com/googlefonts/fontmake/issues/572 258 glyph1 = ufoLib2.objects.Glyph() 259 pen = glyph1.getPointPen() 260 pen.beginPath() 261 pen.addPoint((0, 651), segmentType="line") 262 pen.addPoint((0, 101), segmentType="line") 263 pen.addPoint((0, 101), segmentType="line") 264 pen.addPoint((0, 651), segmentType="line") 265 pen.endPath() 266 267 glyph2 = ufoLib2.objects.Glyph() 268 pen = glyph2.getPointPen() 269 pen.beginPath() 270 pen.addPoint((1, 651), segmentType="line") 271 pen.addPoint((2, 101), segmentType="line") 272 pen.addPoint((3, 101), segmentType="line") 273 pen.addPoint((4, 651), segmentType="line") 274 pen.endPath() 275 276 glyphs = [glyph1, glyph2] 277 278 assert glyphs_to_quadratic(glyphs, reverse_direction=True) 279 280 assert [[(p.x, p.y) for p in glyph[0]] for glyph in glyphs] == [ 281 [ 282 (0, 651), 283 (0, 651), 284 (0, 101), 285 (0, 101), 286 ], 287 [(1, 651), (4, 651), (3, 101), (2, 101)], 288 ] 289