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