1import io 2import fontTools.ttLib.tables.otBase 3from fontTools.misc.testTools import getXML, stripVariableItemsFromTTX 4from fontTools.misc.textTools import tobytes, tostr 5from fontTools import subset 6from fontTools.fontBuilder import FontBuilder 7from fontTools.pens.ttGlyphPen import TTGlyphPen 8from fontTools.ttLib import TTFont, newTable 9from fontTools.ttLib.tables import otTables as ot 10from fontTools.misc.loggingTools import CapturingLogHandler 11from fontTools.subset.svg import etree 12import difflib 13import logging 14import os 15import shutil 16import sys 17import tempfile 18import unittest 19import pathlib 20import pytest 21 22 23class SubsetTest: 24 @classmethod 25 def setup_class(cls): 26 cls.tempdir = None 27 cls.num_tempfiles = 0 28 29 @classmethod 30 def teardown_class(cls): 31 if cls.tempdir: 32 shutil.rmtree(cls.tempdir, ignore_errors=True) 33 34 @staticmethod 35 def getpath(*testfile): 36 path, _ = os.path.split(__file__) 37 return os.path.join(path, "data", *testfile) 38 39 @classmethod 40 def temp_path(cls, suffix): 41 if not cls.tempdir: 42 cls.tempdir = tempfile.mkdtemp() 43 cls.num_tempfiles += 1 44 return os.path.join(cls.tempdir, "tmp%d%s" % (cls.num_tempfiles, suffix)) 45 46 @staticmethod 47 def read_ttx(path): 48 with open(path, "r", encoding="utf-8") as f: 49 ttx = f.read() 50 # don't care whether TTF or OTF, thus strip sfntVersion as well 51 return stripVariableItemsFromTTX(ttx, sfntVersion=True).splitlines(True) 52 53 def expect_ttx(self, font, expected_ttx, tables=None): 54 path = self.temp_path(suffix=".ttx") 55 font.saveXML(path, tables=tables) 56 actual = self.read_ttx(path) 57 expected = self.read_ttx(expected_ttx) 58 if actual != expected: 59 for line in difflib.unified_diff( 60 expected, actual, fromfile=expected_ttx, tofile=path 61 ): 62 sys.stdout.write(line) 63 pytest.fail("TTX output is different from expected") 64 65 def compile_font(self, path, suffix): 66 savepath = self.temp_path(suffix=suffix) 67 font = TTFont(recalcBBoxes=False, recalcTimestamp=False) 68 font.importXML(path) 69 font.save(savepath, reorderTables=None) 70 return savepath 71 72 # ----- 73 # Tests 74 # ----- 75 76 def test_layout_scripts(self): 77 fontpath = self.compile_font(self.getpath("layout_scripts.ttx"), ".otf") 78 subsetpath = self.temp_path(".otf") 79 subset.main( 80 [ 81 fontpath, 82 "--glyphs=*", 83 "--layout-features=*", 84 "--layout-scripts=latn,arab.URD,arab.dflt", 85 "--output-file=%s" % subsetpath, 86 ] 87 ) 88 subsetfont = TTFont(subsetpath) 89 self.expect_ttx( 90 subsetfont, self.getpath("expect_layout_scripts.ttx"), ["GPOS", "GSUB"] 91 ) 92 93 def test_no_notdef_outline_otf(self): 94 fontpath = self.compile_font(self.getpath("TestOTF-Regular.ttx"), ".otf") 95 subsetpath = self.temp_path(".otf") 96 subset.main( 97 [ 98 fontpath, 99 "--no-notdef-outline", 100 "--gids=0", 101 "--output-file=%s" % subsetpath, 102 ] 103 ) 104 subsetfont = TTFont(subsetpath) 105 self.expect_ttx( 106 subsetfont, self.getpath("expect_no_notdef_outline_otf.ttx"), ["CFF "] 107 ) 108 109 def test_no_notdef_outline_cid(self): 110 fontpath = self.compile_font(self.getpath("TestCID-Regular.ttx"), ".otf") 111 subsetpath = self.temp_path(".otf") 112 subset.main( 113 [ 114 fontpath, 115 "--no-notdef-outline", 116 "--gids=0", 117 "--output-file=%s" % subsetpath, 118 ] 119 ) 120 subsetfont = TTFont(subsetpath) 121 self.expect_ttx( 122 subsetfont, self.getpath("expect_no_notdef_outline_cid.ttx"), ["CFF "] 123 ) 124 125 def test_no_notdef_outline_ttf(self): 126 fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf") 127 subsetpath = self.temp_path(".ttf") 128 subset.main( 129 [ 130 fontpath, 131 "--no-notdef-outline", 132 "--gids=0", 133 "--output-file=%s" % subsetpath, 134 ] 135 ) 136 subsetfont = TTFont(subsetpath) 137 self.expect_ttx( 138 subsetfont, 139 self.getpath("expect_no_notdef_outline_ttf.ttx"), 140 ["glyf", "hmtx"], 141 ) 142 143 def test_subset_ankr(self): 144 fontpath = self.compile_font(self.getpath("TestANKR.ttx"), ".ttf") 145 subsetpath = self.temp_path(".ttf") 146 subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath]) 147 subsetfont = TTFont(subsetpath) 148 self.expect_ttx(subsetfont, self.getpath("expect_ankr.ttx"), ["ankr"]) 149 150 def test_subset_ankr_remove(self): 151 fontpath = self.compile_font(self.getpath("TestANKR.ttx"), ".ttf") 152 subsetpath = self.temp_path(".ttf") 153 subset.main([fontpath, "--glyphs=two", "--output-file=%s" % subsetpath]) 154 assert "ankr" not in TTFont(subsetpath) 155 156 def test_subset_bsln_format_0(self): 157 fontpath = self.compile_font(self.getpath("TestBSLN-0.ttx"), ".ttf") 158 subsetpath = self.temp_path(".ttf") 159 subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath]) 160 subsetfont = TTFont(subsetpath) 161 self.expect_ttx(subsetfont, self.getpath("expect_bsln_0.ttx"), ["bsln"]) 162 163 def test_subset_bsln_format_0_from_format_1(self): 164 # TestBSLN-1 defines the ideographic baseline to be the font's default, 165 # and specifies that glyphs {.notdef, zero, one, two} use the roman 166 # baseline instead of the default ideographic baseline. As we request 167 # a subsetted font with {zero, one} and the implicit .notdef, all 168 # glyphs in the resulting font use the Roman baseline. In this case, 169 # we expect a format 0 'bsln' table because it is the most compact. 170 fontpath = self.compile_font(self.getpath("TestBSLN-1.ttx"), ".ttf") 171 subsetpath = self.temp_path(".ttf") 172 subset.main( 173 [fontpath, "--unicodes=U+0030-0031", "--output-file=%s" % subsetpath] 174 ) 175 subsetfont = TTFont(subsetpath) 176 self.expect_ttx(subsetfont, self.getpath("expect_bsln_0.ttx"), ["bsln"]) 177 178 def test_subset_bsln_format_1(self): 179 # TestBSLN-1 defines the ideographic baseline to be the font's default, 180 # and specifies that glyphs {.notdef, zero, one, two} use the roman 181 # baseline instead of the default ideographic baseline. We request 182 # a subset where the majority of glyphs use the roman baseline, 183 # but one single glyph (uni2EA2) is ideographic. In the resulting 184 # subsetted font, we expect a format 1 'bsln' table whose default 185 # is Roman, but with an override that uses the ideographic baseline 186 # for uni2EA2. 187 fontpath = self.compile_font(self.getpath("TestBSLN-1.ttx"), ".ttf") 188 subsetpath = self.temp_path(".ttf") 189 subset.main( 190 [fontpath, "--unicodes=U+0030-0031,U+2EA2", "--output-file=%s" % subsetpath] 191 ) 192 subsetfont = TTFont(subsetpath) 193 self.expect_ttx(subsetfont, self.getpath("expect_bsln_1.ttx"), ["bsln"]) 194 195 def test_subset_bsln_format_2(self): 196 # The 'bsln' table in TestBSLN-2 refers to control points in glyph 'P' 197 # for defining its baselines. Therefore, the subsetted font should 198 # include this glyph even though it is not requested explicitly. 199 fontpath = self.compile_font(self.getpath("TestBSLN-2.ttx"), ".ttf") 200 subsetpath = self.temp_path(".ttf") 201 subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath]) 202 subsetfont = TTFont(subsetpath) 203 self.expect_ttx(subsetfont, self.getpath("expect_bsln_2.ttx"), ["bsln"]) 204 205 def test_subset_bsln_format_2_from_format_3(self): 206 # TestBSLN-3 defines the ideographic baseline to be the font's default, 207 # and specifies that glyphs {.notdef, zero, one, two, P} use the roman 208 # baseline instead of the default ideographic baseline. As we request 209 # a subsetted font with zero and the implicit .notdef and P for 210 # baseline measurement, all glyphs in the resulting font use the Roman 211 # baseline. In this case, we expect a format 2 'bsln' table because it 212 # is the most compact encoding. 213 fontpath = self.compile_font(self.getpath("TestBSLN-3.ttx"), ".ttf") 214 subsetpath = self.temp_path(".ttf") 215 subset.main([fontpath, "--unicodes=U+0030", "--output-file=%s" % subsetpath]) 216 subsetfont = TTFont(subsetpath) 217 self.expect_ttx(subsetfont, self.getpath("expect_bsln_2.ttx"), ["bsln"]) 218 219 def test_subset_bsln_format_3(self): 220 # TestBSLN-3 defines the ideographic baseline to be the font's default, 221 # and specifies that glyphs {.notdef, zero, one, two} use the roman 222 # baseline instead of the default ideographic baseline. We request 223 # a subset where the majority of glyphs use the roman baseline, 224 # but one single glyph (uni2EA2) is ideographic. In the resulting 225 # subsetted font, we expect a format 1 'bsln' table whose default 226 # is Roman, but with an override that uses the ideographic baseline 227 # for uni2EA2. 228 fontpath = self.compile_font(self.getpath("TestBSLN-3.ttx"), ".ttf") 229 subsetpath = self.temp_path(".ttf") 230 subset.main( 231 [fontpath, "--unicodes=U+0030-0031,U+2EA2", "--output-file=%s" % subsetpath] 232 ) 233 subsetfont = TTFont(subsetpath) 234 self.expect_ttx(subsetfont, self.getpath("expect_bsln_3.ttx"), ["bsln"]) 235 236 def test_subset_clr(self): 237 fontpath = self.compile_font(self.getpath("TestCLR-Regular.ttx"), ".ttf") 238 subsetpath = self.temp_path(".ttf") 239 subset.main([fontpath, "--glyphs=smileface", "--output-file=%s" % subsetpath]) 240 subsetfont = TTFont(subsetpath) 241 self.expect_ttx( 242 subsetfont, 243 self.getpath("expect_keep_colr.ttx"), 244 ["GlyphOrder", "hmtx", "glyf", "COLR", "CPAL"], 245 ) 246 247 def test_subset_gvar(self): 248 fontpath = self.compile_font(self.getpath("TestGVAR.ttx"), ".ttf") 249 subsetpath = self.temp_path(".ttf") 250 subset.main( 251 [fontpath, "--unicodes=U+002B,U+2212", "--output-file=%s" % subsetpath] 252 ) 253 subsetfont = TTFont(subsetpath) 254 self.expect_ttx( 255 subsetfont, 256 self.getpath("expect_keep_gvar.ttx"), 257 ["GlyphOrder", "avar", "fvar", "gvar", "name"], 258 ) 259 260 def test_subset_gvar_notdef_outline(self): 261 fontpath = self.compile_font(self.getpath("TestGVAR.ttx"), ".ttf") 262 subsetpath = self.temp_path(".ttf") 263 subset.main( 264 [ 265 fontpath, 266 "--unicodes=U+0030", 267 "--notdef_outline", 268 "--output-file=%s" % subsetpath, 269 ] 270 ) 271 subsetfont = TTFont(subsetpath) 272 self.expect_ttx( 273 subsetfont, 274 self.getpath("expect_keep_gvar_notdef_outline.ttx"), 275 ["GlyphOrder", "avar", "fvar", "gvar", "name"], 276 ) 277 278 def test_subset_lcar_remove(self): 279 fontpath = self.compile_font(self.getpath("TestLCAR-0.ttx"), ".ttf") 280 subsetpath = self.temp_path(".ttf") 281 subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath]) 282 subsetfont = TTFont(subsetpath) 283 assert "lcar" not in subsetfont 284 285 def test_subset_lcar_format_0(self): 286 fontpath = self.compile_font(self.getpath("TestLCAR-0.ttx"), ".ttf") 287 subsetpath = self.temp_path(".ttf") 288 subset.main([fontpath, "--unicodes=U+FB01", "--output-file=%s" % subsetpath]) 289 subsetfont = TTFont(subsetpath) 290 self.expect_ttx(subsetfont, self.getpath("expect_lcar_0.ttx"), ["lcar"]) 291 292 def test_subset_lcar_format_1(self): 293 fontpath = self.compile_font(self.getpath("TestLCAR-1.ttx"), ".ttf") 294 subsetpath = self.temp_path(".ttf") 295 subset.main([fontpath, "--unicodes=U+FB01", "--output-file=%s" % subsetpath]) 296 subsetfont = TTFont(subsetpath) 297 self.expect_ttx(subsetfont, self.getpath("expect_lcar_1.ttx"), ["lcar"]) 298 299 def test_subset_math(self): 300 fontpath = self.compile_font(self.getpath("TestMATH-Regular.ttx"), ".ttf") 301 subsetpath = self.temp_path(".ttf") 302 subset.main( 303 [ 304 fontpath, 305 "--unicodes=U+0041,U+0028,U+0302,U+1D400,U+1D435", 306 "--output-file=%s" % subsetpath, 307 ] 308 ) 309 subsetfont = TTFont(subsetpath) 310 self.expect_ttx( 311 subsetfont, 312 self.getpath("expect_keep_math.ttx"), 313 ["GlyphOrder", "CFF ", "MATH", "hmtx"], 314 ) 315 316 def test_subset_math_partial(self): 317 fontpath = self.compile_font(self.getpath("test_math_partial.ttx"), ".ttf") 318 subsetpath = self.temp_path(".ttf") 319 subset.main([fontpath, "--text=A", "--output-file=%s" % subsetpath]) 320 subsetfont = TTFont(subsetpath) 321 self.expect_ttx(subsetfont, self.getpath("expect_math_partial.ttx"), ["MATH"]) 322 323 def test_subset_opbd_remove(self): 324 # In the test font, only the glyphs 'A' and 'zero' have an entry in 325 # the Optical Bounds table. When subsetting, we do not request any 326 # of those glyphs. Therefore, the produced subsetted font should 327 # not contain an 'opbd' table. 328 fontpath = self.compile_font(self.getpath("TestOPBD-0.ttx"), ".ttf") 329 subsetpath = self.temp_path(".ttf") 330 subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath]) 331 subsetfont = TTFont(subsetpath) 332 assert "opbd" not in subsetfont 333 334 def test_subset_opbd_format_0(self): 335 fontpath = self.compile_font(self.getpath("TestOPBD-0.ttx"), ".ttf") 336 subsetpath = self.temp_path(".ttf") 337 subset.main([fontpath, "--glyphs=A", "--output-file=%s" % subsetpath]) 338 subsetfont = TTFont(subsetpath) 339 self.expect_ttx(subsetfont, self.getpath("expect_opbd_0.ttx"), ["opbd"]) 340 341 def test_subset_opbd_format_1(self): 342 fontpath = self.compile_font(self.getpath("TestOPBD-1.ttx"), ".ttf") 343 subsetpath = self.temp_path(".ttf") 344 subset.main([fontpath, "--glyphs=A", "--output-file=%s" % subsetpath]) 345 subsetfont = TTFont(subsetpath) 346 self.expect_ttx(subsetfont, self.getpath("expect_opbd_1.ttx"), ["opbd"]) 347 348 def test_subset_prop_remove_default_zero(self): 349 # If all glyphs have an AAT glyph property with value 0, 350 # the "prop" table should be removed from the subsetted font. 351 fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf") 352 subsetpath = self.temp_path(".ttf") 353 subset.main([fontpath, "--unicodes=U+0041", "--output-file=%s" % subsetpath]) 354 subsetfont = TTFont(subsetpath) 355 assert "prop" not in subsetfont 356 357 def test_subset_prop_0(self): 358 # If all glyphs share the same AAT glyph properties, the "prop" table 359 # in the subsetted font should use format 0. 360 # 361 # Unless the shared value is zero, in which case the subsetted font 362 # should have no "prop" table at all. But that case has already been 363 # tested above in test_subset_prop_remove_default_zero(). 364 fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf") 365 subsetpath = self.temp_path(".ttf") 366 subset.main( 367 [ 368 fontpath, 369 "--unicodes=U+0030-0032", 370 "--no-notdef-glyph", 371 "--output-file=%s" % subsetpath, 372 ] 373 ) 374 subsetfont = TTFont(subsetpath) 375 self.expect_ttx(subsetfont, self.getpath("expect_prop_0.ttx"), ["prop"]) 376 377 def test_subset_prop_1(self): 378 # If not all glyphs share the same AAT glyph properties, the subsetted 379 # font should contain a "prop" table in format 1. To save space, the 380 # DefaultProperties should be set to the most frequent value. 381 fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf") 382 subsetpath = self.temp_path(".ttf") 383 subset.main( 384 [ 385 fontpath, 386 "--unicodes=U+0030-0032", 387 "--notdef-outline", 388 "--output-file=%s" % subsetpath, 389 ] 390 ) 391 subsetfont = TTFont(subsetpath) 392 self.expect_ttx(subsetfont, self.getpath("expect_prop_1.ttx"), ["prop"]) 393 394 def test_options(self): 395 # https://github.com/fonttools/fonttools/issues/413 396 opt1 = subset.Options() 397 assert "Xyz-" not in opt1.layout_features 398 opt2 = subset.Options() 399 opt2.layout_features.append("Xyz-") 400 assert "Xyz-" in opt2.layout_features 401 assert "Xyz-" not in opt1.layout_features 402 403 def test_google_color(self): 404 fontpath = self.compile_font(self.getpath("google_color.ttx"), ".ttf") 405 subsetpath = self.temp_path(".ttf") 406 subset.main([fontpath, "--gids=0,1", "--output-file=%s" % subsetpath]) 407 subsetfont = TTFont(subsetpath) 408 assert "CBDT" in subsetfont 409 assert "CBLC" in subsetfont 410 assert "x" in subsetfont["CBDT"].strikeData[0] 411 assert "y" not in subsetfont["CBDT"].strikeData[0] 412 413 def test_google_color_all(self): 414 fontpath = self.compile_font(self.getpath("google_color.ttx"), ".ttf") 415 subsetpath = self.temp_path(".ttf") 416 subset.main([fontpath, "--unicodes=*", "--output-file=%s" % subsetpath]) 417 subsetfont = TTFont(subsetpath) 418 assert "x" in subsetfont["CBDT"].strikeData[0] 419 assert "y" in subsetfont["CBDT"].strikeData[0] 420 421 def test_sbix(self): 422 fontpath = self.compile_font(self.getpath("sbix.ttx"), ".ttf") 423 subsetpath = self.temp_path(".ttf") 424 subset.main([fontpath, "--gids=0,1", "--output-file=%s" % subsetpath]) 425 subsetfont = TTFont(subsetpath) 426 self.expect_ttx(subsetfont, self.getpath("expect_sbix.ttx"), ["sbix"]) 427 428 def test_varComposite(self): 429 fontpath = self.getpath("..", "..", "ttLib", "data", "varc-ac00-ac01.ttf") 430 origfont = TTFont(fontpath) 431 assert len(origfont.getGlyphOrder()) == 6 432 subsetpath = self.temp_path(".ttf") 433 subset.main([fontpath, "--unicodes=ac00", "--output-file=%s" % subsetpath]) 434 subsetfont = TTFont(subsetpath) 435 assert len(subsetfont.getGlyphOrder()) == 4 436 subset.main([fontpath, "--unicodes=ac01", "--output-file=%s" % subsetpath]) 437 subsetfont = TTFont(subsetpath) 438 assert len(subsetfont.getGlyphOrder()) == 5 439 440 def test_timing_publishes_parts(self): 441 fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf") 442 443 options = subset.Options() 444 options.timing = True 445 subsetter = subset.Subsetter(options) 446 subsetter.populate(text="ABC") 447 font = TTFont(fontpath) 448 with CapturingLogHandler("fontTools.subset.timer", logging.DEBUG) as captor: 449 subsetter.subset(font) 450 logs = captor.records 451 452 assert len(logs) > 5 453 assert len(logs) == len( 454 [l for l in logs if "msg" in l.args and "time" in l.args] 455 ) 456 # Look for a few things we know should happen 457 assert filter(lambda l: l.args["msg"] == "load 'cmap'", logs) 458 assert filter(lambda l: l.args["msg"] == "subset 'cmap'", logs) 459 assert filter(lambda l: l.args["msg"] == "subset 'glyf'", logs) 460 461 def test_passthrough_tables(self): 462 fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf") 463 font = TTFont(fontpath) 464 unknown_tag = "ZZZZ" 465 unknown_table = newTable(unknown_tag) 466 unknown_table.data = b"\0" * 10 467 font[unknown_tag] = unknown_table 468 font.save(fontpath) 469 470 subsetpath = self.temp_path(".ttf") 471 subset.main([fontpath, "--output-file=%s" % subsetpath]) 472 subsetfont = TTFont(subsetpath) 473 474 # tables we can't subset are dropped by default 475 assert unknown_tag not in subsetfont 476 477 subsetpath = self.temp_path(".ttf") 478 subset.main([fontpath, "--passthrough-tables", "--output-file=%s" % subsetpath]) 479 subsetfont = TTFont(subsetpath) 480 481 # unknown tables are kept if --passthrough-tables option is passed 482 assert unknown_tag in subsetfont 483 484 def test_non_BMP_text_arg_input(self): 485 fontpath = self.compile_font( 486 self.getpath("TestTTF-Regular_non_BMP_char.ttx"), ".ttf" 487 ) 488 subsetpath = self.temp_path(".ttf") 489 text = tostr("A\U0001F6D2", encoding="utf-8") 490 491 subset.main([fontpath, "--text=%s" % text, "--output-file=%s" % subsetpath]) 492 subsetfont = TTFont(subsetpath) 493 494 assert subsetfont["maxp"].numGlyphs == 3 495 assert subsetfont.getGlyphOrder() == [".notdef", "A", "u1F6D2"] 496 497 def test_non_BMP_text_file_input(self): 498 fontpath = self.compile_font( 499 self.getpath("TestTTF-Regular_non_BMP_char.ttx"), ".ttf" 500 ) 501 subsetpath = self.temp_path(".ttf") 502 text = tobytes("A\U0001F6D2", encoding="utf-8") 503 with tempfile.NamedTemporaryFile(delete=False) as tmp: 504 tmp.write(text) 505 506 try: 507 subset.main( 508 [fontpath, "--text-file=%s" % tmp.name, "--output-file=%s" % subsetpath] 509 ) 510 subsetfont = TTFont(subsetpath) 511 finally: 512 os.remove(tmp.name) 513 514 assert subsetfont["maxp"].numGlyphs == 3 515 assert subsetfont.getGlyphOrder() == [".notdef", "A", "u1F6D2"] 516 517 def test_no_hinting_CFF(self): 518 ttxpath = self.getpath("Lobster.subset.ttx") 519 fontpath = self.compile_font(ttxpath, ".otf") 520 subsetpath = self.temp_path(".otf") 521 subset.main( 522 [ 523 fontpath, 524 "--no-hinting", 525 "--notdef-outline", 526 "--output-file=%s" % subsetpath, 527 "*", 528 ] 529 ) 530 subsetfont = TTFont(subsetpath) 531 self.expect_ttx(subsetfont, self.getpath("expect_no_hinting_CFF.ttx"), ["CFF "]) 532 533 def test_desubroutinize_CFF(self): 534 ttxpath = self.getpath("Lobster.subset.ttx") 535 fontpath = self.compile_font(ttxpath, ".otf") 536 subsetpath = self.temp_path(".otf") 537 subset.main( 538 [ 539 fontpath, 540 "--desubroutinize", 541 "--notdef-outline", 542 "--output-file=%s" % subsetpath, 543 "*", 544 ] 545 ) 546 subsetfont = TTFont(subsetpath) 547 self.expect_ttx( 548 subsetfont, self.getpath("expect_desubroutinize_CFF.ttx"), ["CFF "] 549 ) 550 551 def test_desubroutinize_hinted_subrs_CFF(self): 552 ttxpath = self.getpath("test_hinted_subrs_CFF.ttx") 553 fontpath = self.compile_font(ttxpath, ".otf") 554 subsetpath = self.temp_path(".otf") 555 subset.main( 556 [ 557 fontpath, 558 "--desubroutinize", 559 "--notdef-outline", 560 "--output-file=%s" % subsetpath, 561 "*", 562 ] 563 ) 564 subsetfont = TTFont(subsetpath) 565 self.expect_ttx( 566 subsetfont, self.getpath("test_hinted_subrs_CFF.desub.ttx"), ["CFF "] 567 ) 568 569 def test_desubroutinize_cntrmask_CFF(self): 570 ttxpath = self.getpath("test_cntrmask_CFF.ttx") 571 fontpath = self.compile_font(ttxpath, ".otf") 572 subsetpath = self.temp_path(".otf") 573 subset.main( 574 [ 575 fontpath, 576 "--desubroutinize", 577 "--notdef-outline", 578 "--output-file=%s" % subsetpath, 579 "*", 580 ] 581 ) 582 subsetfont = TTFont(subsetpath) 583 self.expect_ttx( 584 subsetfont, self.getpath("test_cntrmask_CFF.desub.ttx"), ["CFF "] 585 ) 586 587 def test_no_hinting_desubroutinize_CFF(self): 588 ttxpath = self.getpath("test_hinted_subrs_CFF.ttx") 589 fontpath = self.compile_font(ttxpath, ".otf") 590 subsetpath = self.temp_path(".otf") 591 subset.main( 592 [ 593 fontpath, 594 "--no-hinting", 595 "--desubroutinize", 596 "--notdef-outline", 597 "--output-file=%s" % subsetpath, 598 "*", 599 ] 600 ) 601 subsetfont = TTFont(subsetpath) 602 self.expect_ttx( 603 subsetfont, 604 self.getpath("expect_no_hinting_desubroutinize_CFF.ttx"), 605 ["CFF "], 606 ) 607 608 def test_no_hinting_TTF(self): 609 fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf") 610 subsetpath = self.temp_path(".ttf") 611 subset.main( 612 [ 613 fontpath, 614 "--no-hinting", 615 "--notdef-outline", 616 "--output-file=%s" % subsetpath, 617 "*", 618 ] 619 ) 620 subsetfont = TTFont(subsetpath) 621 self.expect_ttx( 622 subsetfont, self.getpath("expect_no_hinting_TTF.ttx"), ["glyf", "maxp"] 623 ) 624 for tag in subset.Options().hinting_tables: 625 assert tag not in subsetfont 626 627 def test_notdef_width_cid(self): 628 # https://github.com/fonttools/fonttools/pull/845 629 fontpath = self.compile_font(self.getpath("NotdefWidthCID-Regular.ttx"), ".otf") 630 subsetpath = self.temp_path(".otf") 631 subset.main( 632 [ 633 fontpath, 634 "--no-notdef-outline", 635 "--gids=0,1", 636 "--output-file=%s" % subsetpath, 637 ] 638 ) 639 subsetfont = TTFont(subsetpath) 640 self.expect_ttx( 641 subsetfont, self.getpath("expect_notdef_width_cid.ttx"), ["CFF "] 642 ) 643 644 def test_recalc_bounds_ttf(self): 645 ttxpath = self.getpath("TestTTF-Regular.ttx") 646 font = TTFont() 647 font.importXML(ttxpath) 648 head = font["head"] 649 bounds = [head.xMin, head.yMin, head.xMax, head.yMax] 650 651 fontpath = self.compile_font(ttxpath, ".ttf") 652 subsetpath = self.temp_path(".ttf") 653 654 # by default, the subsetter does not recalculate the bounding box 655 subset.main([fontpath, "--output-file=%s" % subsetpath, "*"]) 656 head = TTFont(subsetpath)["head"] 657 assert bounds == [head.xMin, head.yMin, head.xMax, head.yMax] 658 659 subset.main([fontpath, "--recalc-bounds", "--output-file=%s" % subsetpath, "*"]) 660 head = TTFont(subsetpath)["head"] 661 bounds = [132, 304, 365, 567] 662 assert bounds == [head.xMin, head.yMin, head.xMax, head.yMax] 663 664 def test_recalc_bounds_otf(self): 665 ttxpath = self.getpath("TestOTF-Regular.ttx") 666 font = TTFont() 667 font.importXML(ttxpath) 668 head = font["head"] 669 bounds = [head.xMin, head.yMin, head.xMax, head.yMax] 670 671 fontpath = self.compile_font(ttxpath, ".otf") 672 subsetpath = self.temp_path(".otf") 673 674 # by default, the subsetter does not recalculate the bounding box 675 subset.main([fontpath, "--output-file=%s" % subsetpath, "*"]) 676 head = TTFont(subsetpath)["head"] 677 assert bounds == [head.xMin, head.yMin, head.xMax, head.yMax] 678 679 subset.main([fontpath, "--recalc-bounds", "--output-file=%s" % subsetpath, "*"]) 680 head = TTFont(subsetpath)["head"] 681 bounds = [132, 304, 365, 567] 682 assert bounds == [head.xMin, head.yMin, head.xMax, head.yMax] 683 684 def test_recalc_timestamp_ttf(self): 685 ttxpath = self.getpath("TestTTF-Regular.ttx") 686 font = TTFont() 687 font.importXML(ttxpath) 688 modified = font["head"].modified 689 fontpath = self.compile_font(ttxpath, ".ttf") 690 subsetpath = self.temp_path(".ttf") 691 692 # by default, the subsetter does not recalculate the modified timestamp 693 subset.main([fontpath, "--output-file=%s" % subsetpath, "*"]) 694 assert modified == TTFont(subsetpath)["head"].modified 695 696 subset.main( 697 [fontpath, "--recalc-timestamp", "--output-file=%s" % subsetpath, "*"] 698 ) 699 assert modified < TTFont(subsetpath)["head"].modified 700 701 def test_recalc_timestamp_otf(self): 702 ttxpath = self.getpath("TestOTF-Regular.ttx") 703 font = TTFont() 704 font.importXML(ttxpath) 705 modified = font["head"].modified 706 fontpath = self.compile_font(ttxpath, ".otf") 707 subsetpath = self.temp_path(".otf") 708 709 # by default, the subsetter does not recalculate the modified timestamp 710 subset.main([fontpath, "--output-file=%s" % subsetpath, "*"]) 711 assert modified == TTFont(subsetpath)["head"].modified 712 713 subset.main( 714 [fontpath, "--recalc-timestamp", "--output-file=%s" % subsetpath, "*"] 715 ) 716 assert modified < TTFont(subsetpath)["head"].modified 717 718 def test_recalc_max_context(self): 719 ttxpath = self.getpath("Lobster.subset.ttx") 720 font = TTFont() 721 font.importXML(ttxpath) 722 max_context = font["OS/2"].usMaxContext 723 fontpath = self.compile_font(ttxpath, ".otf") 724 subsetpath = self.temp_path(".otf") 725 726 # by default, the subsetter does not recalculate the usMaxContext 727 subset.main( 728 [fontpath, "--drop-tables+=GSUB,GPOS", "--output-file=%s" % subsetpath] 729 ) 730 assert max_context == TTFont(subsetpath)["OS/2"].usMaxContext 731 732 subset.main( 733 [ 734 fontpath, 735 "--recalc-max-context", 736 "--drop-tables+=GSUB,GPOS", 737 "--output-file=%s" % subsetpath, 738 ] 739 ) 740 assert 0 == TTFont(subsetpath)["OS/2"].usMaxContext 741 742 def test_retain_gids_ttf(self): 743 fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf") 744 font = TTFont(fontpath) 745 746 assert font["hmtx"]["A"] == (500, 132) 747 assert font["hmtx"]["B"] == (400, 132) 748 749 assert font["glyf"]["A"].numberOfContours > 0 750 assert font["glyf"]["B"].numberOfContours > 0 751 752 subsetpath = self.temp_path(".ttf") 753 subset.main( 754 [ 755 fontpath, 756 "--retain-gids", 757 "--output-file=%s" % subsetpath, 758 "--glyph-names", 759 "B", 760 ] 761 ) 762 subsetfont = TTFont(subsetpath) 763 764 assert subsetfont.getGlyphOrder() == font.getGlyphOrder()[0:3] 765 766 hmtx = subsetfont["hmtx"] 767 assert hmtx["A"] == (0, 0) 768 assert hmtx["B"] == (400, 132) 769 770 glyf = subsetfont["glyf"] 771 assert glyf["A"].numberOfContours == 0 772 assert glyf["B"].numberOfContours > 0 773 774 def test_retain_gids_cff(self): 775 fontpath = self.compile_font(self.getpath("TestOTF-Regular.ttx"), ".otf") 776 font = TTFont(fontpath) 777 778 assert font["hmtx"]["A"] == (500, 132) 779 assert font["hmtx"]["B"] == (400, 132) 780 assert font["hmtx"]["C"] == (500, 0) 781 782 font["CFF "].cff[0].decompileAllCharStrings() 783 cs = font["CFF "].cff[0].CharStrings 784 assert len(cs["A"].program) > 0 785 assert len(cs["B"].program) > 0 786 assert len(cs["C"].program) > 0 787 788 subsetpath = self.temp_path(".otf") 789 subset.main( 790 [ 791 fontpath, 792 "--retain-gids", 793 "--output-file=%s" % subsetpath, 794 "--glyph-names", 795 "B", 796 ] 797 ) 798 subsetfont = TTFont(subsetpath) 799 800 assert subsetfont.getGlyphOrder() == font.getGlyphOrder()[0:3] 801 802 hmtx = subsetfont["hmtx"] 803 assert hmtx["A"] == (0, 0) 804 assert hmtx["B"] == (400, 132) 805 806 subsetfont["CFF "].cff[0].decompileAllCharStrings() 807 cs = subsetfont["CFF "].cff[0].CharStrings 808 809 assert cs["A"].program == ["endchar"] 810 assert len(cs["B"].program) > 0 811 812 def test_retain_gids_cff2(self): 813 ttx_path = self.getpath( 814 "../../varLib/data/master_ttx_varfont_otf/TestCFF2VF.ttx" 815 ) 816 fontpath = self.compile_font(ttx_path, ".otf") 817 font = TTFont(fontpath) 818 819 assert font["hmtx"]["A"] == (600, 31) 820 assert font["hmtx"]["T"] == (600, 41) 821 822 font["CFF2"].cff[0].decompileAllCharStrings() 823 cs = font["CFF2"].cff[0].CharStrings 824 assert len(cs["A"].program) > 0 825 assert len(cs["T"].program) > 0 826 827 subsetpath = self.temp_path(".otf") 828 subset.main( 829 [ 830 fontpath, 831 "--retain-gids", 832 "--output-file=%s" % subsetpath, 833 "T", 834 ] 835 ) 836 subsetfont = TTFont(subsetpath) 837 838 assert len(subsetfont.getGlyphOrder()) == len(font.getGlyphOrder()[0:3]) 839 840 hmtx = subsetfont["hmtx"] 841 assert hmtx["glyph00001"] == (0, 0) 842 assert hmtx["T"] == (600, 41) 843 844 subsetfont["CFF2"].cff[0].decompileAllCharStrings() 845 cs = subsetfont["CFF2"].cff[0].CharStrings 846 assert cs["glyph00001"].program == [] 847 assert len(cs["T"].program) > 0 848 849 def test_HVAR_VVAR(self): 850 fontpath = self.compile_font(self.getpath("TestHVVAR.ttx"), ".ttf") 851 subsetpath = self.temp_path(".ttf") 852 subset.main([fontpath, "--text=BD", "--output-file=%s" % subsetpath]) 853 subsetfont = TTFont(subsetpath) 854 self.expect_ttx( 855 subsetfont, 856 self.getpath("expect_HVVAR.ttx"), 857 ["GlyphOrder", "HVAR", "VVAR", "avar", "fvar"], 858 ) 859 860 def test_HVAR_VVAR_retain_gids(self): 861 fontpath = self.compile_font(self.getpath("TestHVVAR.ttx"), ".ttf") 862 subsetpath = self.temp_path(".ttf") 863 subset.main( 864 [fontpath, "--text=BD", "--retain-gids", "--output-file=%s" % subsetpath] 865 ) 866 subsetfont = TTFont(subsetpath) 867 self.expect_ttx( 868 subsetfont, 869 self.getpath("expect_HVVAR_retain_gids.ttx"), 870 ["GlyphOrder", "HVAR", "VVAR", "avar", "fvar"], 871 ) 872 873 def test_subset_flavor_woff(self): 874 fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf") 875 woff_path = self.temp_path(".woff") 876 877 subset.main( 878 [ 879 fontpath, 880 "*", 881 "--flavor=woff", 882 "--output-file=%s" % woff_path, 883 ] 884 ) 885 woff = TTFont(woff_path) 886 887 assert woff.flavor == "woff" 888 889 def test_subset_flavor_woff2(self): 890 # skip if brotli is not importable, required for woff2 891 pytest.importorskip("brotli") 892 893 fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf") 894 woff2_path = self.temp_path(".woff2") 895 896 subset.main( 897 [ 898 fontpath, 899 "*", 900 "--flavor=woff2", 901 "--output-file=%s" % woff2_path, 902 ] 903 ) 904 woff2 = TTFont(woff2_path) 905 906 assert woff2.flavor == "woff2" 907 908 def test_subset_flavor_none(self): 909 fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf") 910 ttf_path = self.temp_path(".ttf") 911 912 subset.main( 913 [ 914 fontpath, 915 "*", 916 "--output-file=%s" % ttf_path, 917 ] 918 ) 919 ttf = TTFont(ttf_path) 920 921 assert ttf.flavor is None 922 923 def test_subset_context_subst_format_3(self): 924 # https://github.com/fonttools/fonttools/issues/1879 925 # Test font contains 'calt' feature with Format 3 ContextSubst lookup subtables 926 ttx = self.getpath("TestContextSubstFormat3.ttx") 927 fontpath = self.compile_font(ttx, ".ttf") 928 subsetpath = self.temp_path(".ttf") 929 subset.main([fontpath, "--unicodes=*", "--output-file=%s" % subsetpath]) 930 subsetfont = TTFont(subsetpath) 931 # check all glyphs are kept via GSUB closure, no changes expected 932 self.expect_ttx(subsetfont, ttx) 933 934 def test_cmap_prune_format12(self): 935 fontpath = self.compile_font(self.getpath("CmapSubsetTest.ttx"), ".ttf") 936 subsetpath = self.temp_path(".ttf") 937 subset.main([fontpath, "--glyphs=a", "--output-file=%s" % subsetpath]) 938 subsetfont = TTFont(subsetpath) 939 self.expect_ttx(subsetfont, self.getpath("CmapSubsetTest.subset.ttx"), ["cmap"]) 940 941 @pytest.mark.parametrize("text, n", [("!", 1), ("#", 2)]) 942 def test_GPOS_PairPos_Format2_useClass0(self, text, n): 943 # Check two things related to class 0 ('every other glyph'): 944 # 1) that it's reused for ClassDef1 when it becomes empty as the subset glyphset 945 # is intersected with the table's Coverage 946 # 2) that it is never reused for ClassDef2 even when it happens to become empty 947 # because of the subset glyphset. In this case, we don't keep a PairPosClass2 948 # subtable if only ClassDef2's class0 survived subsetting. 949 # The test font (from Harfbuzz test suite) is constructed to trigger these two 950 # situations depending on the input subset --text. 951 # https://github.com/fonttools/fonttools/pull/2221 952 fontpath = self.compile_font( 953 self.getpath("GPOS_PairPos_Format2_PR_2221.ttx"), ".ttf" 954 ) 955 subsetpath = self.temp_path(".ttf") 956 957 expected_ttx = self.getpath( 958 f"GPOS_PairPos_Format2_ClassDef{n}_useClass0.subset.ttx" 959 ) 960 subset.main( 961 [ 962 fontpath, 963 f"--text='{text}'", 964 "--layout-features+=test", 965 "--output-file=%s" % subsetpath, 966 ] 967 ) 968 subsetfont = TTFont(subsetpath) 969 self.expect_ttx(subsetfont, expected_ttx, ["GPOS"]) 970 971 def test_GPOS_SinglePos_prune_post_subset_no_value(self): 972 fontpath = self.compile_font( 973 self.getpath("GPOS_SinglePos_no_value_issue_2312.ttx"), ".ttf" 974 ) 975 subsetpath = self.temp_path(".ttf") 976 subset.main([fontpath, "*", "--glyph-names", "--output-file=%s" % subsetpath]) 977 subsetfont = TTFont(subsetpath) 978 self.expect_ttx( 979 subsetfont, 980 self.getpath("GPOS_SinglePos_no_value_issue_2312.subset.ttx"), 981 ["GlyphOrder", "GPOS"], 982 ) 983 984 @pytest.mark.parametrize( 985 "installed, enabled, ok", 986 [ 987 pytest.param(True, None, True, id="installed-auto-ok"), 988 pytest.param(True, None, False, id="installed-auto-fail"), 989 pytest.param(True, True, True, id="installed-enabled-ok"), 990 pytest.param(True, True, False, id="installed-enabled-fail"), 991 pytest.param(True, False, True, id="installed-disabled"), 992 pytest.param(False, True, True, id="not_installed-enabled"), 993 pytest.param(False, False, True, id="not_installed-disabled"), 994 ], 995 ) 996 def test_harfbuzz_repacker(self, caplog, monkeypatch, installed, enabled, ok): 997 # Use a mock to test the pure-python serializer is used when uharfbuzz 998 # returns an error or is not installed 999 have_uharfbuzz = fontTools.ttLib.tables.otBase.have_uharfbuzz 1000 if installed: 1001 if not have_uharfbuzz: 1002 pytest.skip("uharfbuzz is not installed") 1003 if not ok: 1004 # pretend hb.repack/repack_with_tag return an error 1005 import uharfbuzz as hb 1006 1007 def mock_repack(data, obj_list): 1008 raise hb.RepackerError("mocking") 1009 1010 monkeypatch.setattr(hb, "repack", mock_repack) 1011 1012 if hasattr(hb, "repack_with_tag"): # uharfbuzz >= 0.30.0 1013 1014 def mock_repack_with_tag(tag, data, obj_list): 1015 raise hb.RepackerError("mocking") 1016 1017 monkeypatch.setattr(hb, "repack_with_tag", mock_repack_with_tag) 1018 else: 1019 if have_uharfbuzz: 1020 # pretend uharfbuzz is not installed 1021 monkeypatch.setattr( 1022 fontTools.ttLib.tables.otBase, "have_uharfbuzz", False 1023 ) 1024 1025 fontpath = self.compile_font(self.getpath("harfbuzz_repacker.ttx"), ".otf") 1026 subsetpath = self.temp_path(".otf") 1027 args = [ 1028 fontpath, 1029 "--unicodes=0x53a9", 1030 "--layout-features=*", 1031 f"--output-file={subsetpath}", 1032 ] 1033 if enabled is True: 1034 args.append("--harfbuzz-repacker") 1035 elif enabled is False: 1036 args.append("--no-harfbuzz-repacker") 1037 # elif enabled is None: ... is the default 1038 1039 if enabled is True and not installed: 1040 # raise if enabled but not installed 1041 with pytest.raises(ImportError, match="uharfbuzz"): 1042 subset.main(args) 1043 return 1044 1045 with caplog.at_level(logging.DEBUG, "fontTools.ttLib.tables.otBase"): 1046 subset.main(args) 1047 1048 subsetfont = TTFont(subsetpath) 1049 # both hb.repack and pure-python serializer compile to the same ttx 1050 self.expect_ttx( 1051 subsetfont, self.getpath("expect_harfbuzz_repacker.ttx"), ["GSUB"] 1052 ) 1053 1054 if enabled or enabled is None: 1055 if installed: 1056 assert "serializing 'GSUB' with hb.repack" in caplog.text 1057 1058 if enabled is None and not installed: 1059 assert ( 1060 "uharfbuzz not found, compiling 'GSUB' with pure-python serializer" 1061 ) in caplog.text 1062 1063 if enabled is False: 1064 assert ( 1065 "hb.repack disabled, compiling 'GSUB' with pure-python serializer" 1066 ) in caplog.text 1067 1068 # test we emit a log.error if hb.repack fails (and we don't if successful) 1069 assert ( 1070 ( 1071 "hb.repack failed to serialize 'GSUB', attempting fonttools resolutions " 1072 "; the error message was: RepackerError: mocking" 1073 ) 1074 in caplog.text 1075 ) ^ ok 1076 1077 def test_retain_east_asian_spacing_features(self): 1078 # This test font contains halt and vhal features, check that 1079 # they are retained by default after subsetting. 1080 ttx_path = self.getpath("NotoSansCJKjp-Regular.subset.ttx") 1081 ttx = pathlib.Path(ttx_path).read_text() 1082 assert 'FeatureTag value="halt"' in ttx 1083 assert 'FeatureTag value="vhal"' in ttx 1084 1085 fontpath = self.compile_font(ttx_path, ".otf") 1086 subsetpath = self.temp_path(".otf") 1087 subset.main( 1088 [ 1089 fontpath, 1090 "--unicodes=*", 1091 "--output-file=%s" % subsetpath, 1092 ] 1093 ) 1094 # subset output is the same as the input 1095 self.expect_ttx(TTFont(subsetpath), ttx_path) 1096 1097 1098@pytest.fixture 1099def featureVarsTestFont(): 1100 fb = FontBuilder(unitsPerEm=100) 1101 fb.setupGlyphOrder([".notdef", "f", "f_f", "dollar", "dollar.rvrn"]) 1102 fb.setupCharacterMap({ord("f"): "f", ord("$"): "dollar"}) 1103 fb.setupNameTable({"familyName": "TestFeatureVars", "styleName": "Regular"}) 1104 fb.setupPost() 1105 fb.setupFvar(axes=[("wght", 100, 400, 900, "Weight")], instances=[]) 1106 fb.addOpenTypeFeatures( 1107 """\ 1108 feature dlig { 1109 sub f f by f_f; 1110 } dlig; 1111 """ 1112 ) 1113 fb.addFeatureVariations( 1114 [([{"wght": (0.20886, 1.0)}], {"dollar": "dollar.rvrn"})], featureTag="rvrn" 1115 ) 1116 buf = io.BytesIO() 1117 fb.save(buf) 1118 buf.seek(0) 1119 1120 return TTFont(buf) 1121 1122 1123def test_subset_feature_variations_keep_all(featureVarsTestFont): 1124 font = featureVarsTestFont 1125 1126 options = subset.Options() 1127 subsetter = subset.Subsetter(options) 1128 subsetter.populate(unicodes=[ord("f"), ord("$")]) 1129 subsetter.subset(font) 1130 1131 featureTags = {r.FeatureTag for r in font["GSUB"].table.FeatureList.FeatureRecord} 1132 # 'dlig' is discretionary so it is dropped by default 1133 assert "dlig" not in featureTags 1134 assert "f_f" not in font.getGlyphOrder() 1135 # 'rvrn' is required so it is kept by default 1136 assert "rvrn" in featureTags 1137 assert "dollar.rvrn" in font.getGlyphOrder() 1138 1139 1140def test_subset_feature_variations_drop_all(featureVarsTestFont): 1141 font = featureVarsTestFont 1142 1143 options = subset.Options() 1144 options.layout_features.remove("rvrn") # drop 'rvrn' 1145 subsetter = subset.Subsetter(options) 1146 subsetter.populate(unicodes=[ord("f"), ord("$")]) 1147 subsetter.subset(font) 1148 1149 featureTags = {r.FeatureTag for r in font["GSUB"].table.FeatureList.FeatureRecord} 1150 glyphs = set(font.getGlyphOrder()) 1151 1152 assert "rvrn" not in featureTags 1153 assert glyphs == {".notdef", "f", "dollar"} 1154 # all FeatureVariationRecords were dropped 1155 assert font["GSUB"].table.FeatureVariations is None 1156 assert font["GSUB"].table.Version == 0x00010000 1157 1158 1159# TODO test_subset_feature_variations_drop_from_end_empty_records 1160# https://github.com/fonttools/fonttools/issues/1881#issuecomment-619415044 1161 1162 1163@pytest.fixture 1164def singlepos2_font(): 1165 fb = FontBuilder(unitsPerEm=1000) 1166 fb.setupGlyphOrder([".notdef", "a", "b", "c"]) 1167 fb.setupCharacterMap({ord("a"): "a", ord("b"): "b", ord("c"): "c"}) 1168 fb.setupNameTable({"familyName": "TestSingePosFormat", "styleName": "Regular"}) 1169 fb.setupPost() 1170 fb.addOpenTypeFeatures( 1171 """ 1172 feature kern { 1173 pos a -50; 1174 pos b -40; 1175 pos c -50; 1176 } kern; 1177 """ 1178 ) 1179 1180 buf = io.BytesIO() 1181 fb.save(buf) 1182 buf.seek(0) 1183 1184 return TTFont(buf) 1185 1186 1187def test_subset_single_pos_format(singlepos2_font): 1188 font = singlepos2_font 1189 # The input font has a SinglePos Format 2 subtable where each glyph has 1190 # different ValueRecords 1191 assert getXML(font["GPOS"].table.LookupList.Lookup[0].toXML, font) == [ 1192 "<Lookup>", 1193 ' <LookupType value="1"/>', 1194 ' <LookupFlag value="0"/>', 1195 " <!-- SubTableCount=1 -->", 1196 ' <SinglePos index="0" Format="2">', 1197 " <Coverage>", 1198 ' <Glyph value="a"/>', 1199 ' <Glyph value="b"/>', 1200 ' <Glyph value="c"/>', 1201 " </Coverage>", 1202 ' <ValueFormat value="4"/>', 1203 " <!-- ValueCount=3 -->", 1204 ' <Value index="0" XAdvance="-50"/>', 1205 ' <Value index="1" XAdvance="-40"/>', 1206 ' <Value index="2" XAdvance="-50"/>', 1207 " </SinglePos>", 1208 "</Lookup>", 1209 ] 1210 1211 options = subset.Options() 1212 subsetter = subset.Subsetter(options) 1213 subsetter.populate(unicodes=[ord("a"), ord("c")]) 1214 subsetter.subset(font) 1215 1216 # All the subsetted glyphs from the original SinglePos Format2 subtable 1217 # now have the same ValueRecord, so we use a more compact Format 1 subtable. 1218 assert getXML(font["GPOS"].table.LookupList.Lookup[0].toXML, font) == [ 1219 "<Lookup>", 1220 ' <LookupType value="1"/>', 1221 ' <LookupFlag value="0"/>', 1222 " <!-- SubTableCount=1 -->", 1223 ' <SinglePos index="0" Format="1">', 1224 " <Coverage>", 1225 ' <Glyph value="a"/>', 1226 ' <Glyph value="c"/>', 1227 " </Coverage>", 1228 ' <ValueFormat value="4"/>', 1229 ' <Value XAdvance="-50"/>', 1230 " </SinglePos>", 1231 "</Lookup>", 1232 ] 1233 1234 1235def test_subset_single_pos_format2_all_None(singlepos2_font): 1236 # https://github.com/fonttools/fonttools/issues/2602 1237 font = singlepos2_font 1238 gpos = font["GPOS"].table 1239 subtable = gpos.LookupList.Lookup[0].SubTable[0] 1240 assert subtable.Format == 2 1241 # Hack a SinglePosFormat2 with ValueFormat = 0; our own buildSinglePos 1242 # never makes these as a SinglePosFormat1 is more compact, but they can 1243 # be found in the wild. 1244 subtable.Value = [None] * subtable.ValueCount 1245 subtable.ValueFormat = 0 1246 1247 assert getXML(subtable.toXML, font) == [ 1248 '<SinglePos Format="2">', 1249 " <Coverage>", 1250 ' <Glyph value="a"/>', 1251 ' <Glyph value="b"/>', 1252 ' <Glyph value="c"/>', 1253 " </Coverage>", 1254 ' <ValueFormat value="0"/>', 1255 " <!-- ValueCount=3 -->", 1256 "</SinglePos>", 1257 ] 1258 1259 options = subset.Options() 1260 subsetter = subset.Subsetter(options) 1261 subsetter.populate(unicodes=[ord("a"), ord("c")]) 1262 subsetter.subset(font) 1263 1264 # Check it was downgraded to Format1 after subsetting 1265 assert getXML(font["GPOS"].table.LookupList.Lookup[0].SubTable[0].toXML, font) == [ 1266 '<SinglePos Format="1">', 1267 " <Coverage>", 1268 ' <Glyph value="a"/>', 1269 ' <Glyph value="c"/>', 1270 " </Coverage>", 1271 ' <ValueFormat value="0"/>', 1272 "</SinglePos>", 1273 ] 1274 1275 1276@pytest.fixture 1277def ttf_path(tmp_path): 1278 # $(dirname $0)/../ttLib/data 1279 ttLib_data = pathlib.Path(__file__).parent.parent / "ttLib" / "data" 1280 font = TTFont() 1281 font.importXML(ttLib_data / "TestTTF-Regular.ttx") 1282 font_path = tmp_path / "TestTTF-Regular.ttf" 1283 font.save(font_path) 1284 return font_path 1285 1286 1287def test_subset_empty_glyf(tmp_path, ttf_path): 1288 subset_path = tmp_path / (ttf_path.name + ".subset") 1289 # only keep empty .notdef and space glyph, resulting in an empty glyf table 1290 subset.main( 1291 [ 1292 str(ttf_path), 1293 "--no-notdef-outline", 1294 "--glyph-names", 1295 f"--output-file={subset_path}", 1296 "--glyphs=.notdef space", 1297 ] 1298 ) 1299 subset_font = TTFont(subset_path) 1300 1301 assert subset_font.getGlyphOrder() == [".notdef", "space"] 1302 assert subset_font.reader["glyf"] == b"\x00" 1303 1304 glyf = subset_font["glyf"] 1305 assert all(glyf[g].numberOfContours == 0 for g in subset_font.getGlyphOrder()) 1306 1307 loca = subset_font["loca"] 1308 assert all(loc == 0 for loc in loca) 1309 1310 1311@pytest.fixture 1312def colrv1_path(tmp_path): 1313 base_glyph_names = ["uni%04X" % i for i in range(0xE000, 0xE000 + 10)] 1314 layer_glyph_names = ["glyph%05d" % i for i in range(10, 20)] 1315 glyph_order = [".notdef"] + base_glyph_names + layer_glyph_names 1316 1317 pen = TTGlyphPen(glyphSet=None) 1318 pen.moveTo((0, 0)) 1319 pen.lineTo((0, 500)) 1320 pen.lineTo((500, 500)) 1321 pen.lineTo((500, 0)) 1322 pen.closePath() 1323 glyph = pen.glyph() 1324 glyphs = {g: glyph for g in glyph_order} 1325 1326 fb = FontBuilder(unitsPerEm=1024, isTTF=True) 1327 fb.setupGlyphOrder(glyph_order) 1328 fb.setupCharacterMap({int(name[3:], 16): name for name in base_glyph_names}) 1329 fb.setupGlyf(glyphs) 1330 fb.setupHorizontalMetrics({g: (500, 0) for g in glyph_order}) 1331 fb.setupHorizontalHeader() 1332 fb.setupOS2() 1333 fb.setupPost() 1334 fb.setupNameTable({"familyName": "TestCOLRv1", "styleName": "Regular"}) 1335 1336 fb.setupCOLR( 1337 { 1338 "uniE000": ( 1339 ot.PaintFormat.PaintColrLayers, 1340 [ 1341 { 1342 "Format": ot.PaintFormat.PaintGlyph, 1343 "Paint": (ot.PaintFormat.PaintSolid, 0), 1344 "Glyph": "glyph00010", 1345 }, 1346 { 1347 "Format": ot.PaintFormat.PaintGlyph, 1348 "Paint": (ot.PaintFormat.PaintSolid, 2, 0.3), 1349 "Glyph": "glyph00011", 1350 }, 1351 ], 1352 ), 1353 "uniE001": ( 1354 ot.PaintFormat.PaintColrLayers, 1355 [ 1356 { 1357 "Format": ot.PaintFormat.PaintTransform, 1358 "Paint": { 1359 "Format": ot.PaintFormat.PaintGlyph, 1360 "Paint": { 1361 "Format": ot.PaintFormat.PaintRadialGradient, 1362 "x0": 250, 1363 "y0": 250, 1364 "r0": 250, 1365 "x1": 200, 1366 "y1": 200, 1367 "r1": 0, 1368 "ColorLine": { 1369 "ColorStop": [(0.0, 1), (1.0, 2)], 1370 "Extend": "repeat", 1371 }, 1372 }, 1373 "Glyph": "glyph00012", 1374 }, 1375 "Transform": (0.7071, 0.7071, -0.7071, 0.7071, 0, 0), 1376 }, 1377 { 1378 "Format": ot.PaintFormat.PaintGlyph, 1379 "Paint": (ot.PaintFormat.PaintSolid, 1, 0.5), 1380 "Glyph": "glyph00013", 1381 }, 1382 ], 1383 ), 1384 "uniE002": ( 1385 ot.PaintFormat.PaintColrLayers, 1386 [ 1387 { 1388 "Format": ot.PaintFormat.PaintGlyph, 1389 "Paint": { 1390 "Format": ot.PaintFormat.PaintLinearGradient, 1391 "x0": 0, 1392 "y0": 0, 1393 "x1": 500, 1394 "y1": 500, 1395 "x2": -500, 1396 "y2": 500, 1397 "ColorLine": {"ColorStop": [(0.0, 1), (1.0, 2)]}, 1398 }, 1399 "Glyph": "glyph00014", 1400 }, 1401 { 1402 "Format": ot.PaintFormat.PaintTransform, 1403 "Paint": { 1404 "Format": ot.PaintFormat.PaintGlyph, 1405 "Paint": (ot.PaintFormat.PaintSolid, 1), 1406 "Glyph": "glyph00015", 1407 }, 1408 "Transform": (1, 0, 0, 1, 400, 400), 1409 }, 1410 ], 1411 ), 1412 "uniE003": { 1413 "Format": ot.PaintFormat.PaintRotateAroundCenter, 1414 "Paint": { 1415 "Format": ot.PaintFormat.PaintColrGlyph, 1416 "Glyph": "uniE001", 1417 }, 1418 "angle": 45, 1419 "centerX": 250, 1420 "centerY": 250, 1421 }, 1422 "uniE004": [ 1423 ("glyph00016", 1), 1424 ("glyph00017", 0xFFFF), # special palette index for foreground text 1425 ("glyph00018", 2), 1426 ], 1427 }, 1428 clipBoxes={ 1429 "uniE000": (0, 0, 200, 300), 1430 "uniE001": (0, 0, 500, 500), 1431 "uniE002": (-50, -50, 400, 400), 1432 "uniE003": (-50, -50, 400, 400), 1433 }, 1434 ) 1435 fb.setupCPAL( 1436 [ 1437 [ 1438 (1.0, 0.0, 0.0, 1.0), # red 1439 (0.0, 1.0, 0.0, 1.0), # green 1440 (0.0, 0.0, 1.0, 1.0), # blue 1441 ], 1442 ], 1443 ) 1444 1445 output_path = tmp_path / "TestCOLRv1.ttf" 1446 fb.save(output_path) 1447 1448 return output_path 1449 1450 1451@pytest.fixture 1452def colrv1_cpalv1_path(colrv1_path): 1453 # upgrade CPAL from v0 to v1 by adding labels 1454 font = TTFont(colrv1_path) 1455 fb = FontBuilder(font=font) 1456 fb.setupCPAL( 1457 [ 1458 [ 1459 (1.0, 0.0, 0.0, 1.0), # red 1460 (0.0, 1.0, 0.0, 1.0), # green 1461 (0.0, 0.0, 1.0, 1.0), # blue 1462 ], 1463 ], 1464 paletteLabels=["test palette"], 1465 paletteEntryLabels=["first color", "second color", "third color"], 1466 ) 1467 1468 output_path = colrv1_path.parent / "TestCOLRv1CPALv1.ttf" 1469 fb.save(output_path) 1470 1471 return output_path 1472 1473 1474@pytest.fixture 1475def colrv1_cpalv1_share_nameID_path(colrv1_path): 1476 font = TTFont(colrv1_path) 1477 fb = FontBuilder(font=font) 1478 fb.setupCPAL( 1479 [ 1480 [ 1481 (1.0, 0.0, 0.0, 1.0), # red 1482 (0.0, 1.0, 0.0, 1.0), # green 1483 (0.0, 0.0, 1.0, 1.0), # blue 1484 ], 1485 ], 1486 paletteLabels=["test palette"], 1487 paletteEntryLabels=["first color", "second color", "third color"], 1488 ) 1489 1490 # Set the name ID of the first color to use nameID 1 = familyName = "TestCOLRv1" 1491 fb.font["CPAL"].paletteEntryLabels[0] = 1 1492 1493 output_path = colrv1_path.parent / "TestCOLRv1CPALv1.ttf" 1494 fb.save(output_path) 1495 1496 return output_path 1497 1498 1499def test_subset_COLRv1_and_CPAL(colrv1_path): 1500 subset_path = colrv1_path.parent / (colrv1_path.name + ".subset") 1501 1502 subset.main( 1503 [ 1504 str(colrv1_path), 1505 "--glyph-names", 1506 f"--output-file={subset_path}", 1507 "--unicodes=E002,E003,E004", 1508 ] 1509 ) 1510 subset_font = TTFont(subset_path) 1511 1512 glyph_set = set(subset_font.getGlyphOrder()) 1513 1514 # uniE000 and its children are excluded from subset 1515 assert "uniE000" not in glyph_set 1516 assert "glyph00010" not in glyph_set 1517 assert "glyph00011" not in glyph_set 1518 1519 # uniE001 and children are pulled in indirectly as PaintColrGlyph by uniE003 1520 assert "uniE001" in glyph_set 1521 assert "glyph00012" in glyph_set 1522 assert "glyph00013" in glyph_set 1523 1524 assert "uniE002" in glyph_set 1525 assert "glyph00014" in glyph_set 1526 assert "glyph00015" in glyph_set 1527 1528 assert "uniE003" in glyph_set 1529 1530 assert "uniE004" in glyph_set 1531 assert "glyph00016" in glyph_set 1532 assert "glyph00017" in glyph_set 1533 assert "glyph00018" in glyph_set 1534 1535 assert "COLR" in subset_font 1536 colr = subset_font["COLR"].table 1537 assert colr.Version == 1 1538 assert len(colr.BaseGlyphRecordArray.BaseGlyphRecord) == 1 1539 assert len(colr.BaseGlyphList.BaseGlyphPaintRecord) == 3 # was 4 1540 1541 base = colr.BaseGlyphList.BaseGlyphPaintRecord[0] 1542 assert base.BaseGlyph == "uniE001" 1543 layers = colr.LayerList.Paint[ 1544 base.Paint.FirstLayerIndex : base.Paint.FirstLayerIndex + base.Paint.NumLayers 1545 ] 1546 assert len(layers) == 2 1547 # check v1 palette indices were remapped 1548 assert layers[0].Paint.Paint.ColorLine.ColorStop[0].PaletteIndex == 0 1549 assert layers[0].Paint.Paint.ColorLine.ColorStop[1].PaletteIndex == 1 1550 assert layers[1].Paint.PaletteIndex == 0 1551 1552 baseRecV0 = colr.BaseGlyphRecordArray.BaseGlyphRecord[0] 1553 assert baseRecV0.BaseGlyph == "uniE004" 1554 layersV0 = colr.LayerRecordArray.LayerRecord 1555 assert len(layersV0) == 3 1556 # check v0 palette indices were remapped (except for 0xFFFF) 1557 assert layersV0[0].PaletteIndex == 0 1558 assert layersV0[1].PaletteIndex == 0xFFFF 1559 assert layersV0[2].PaletteIndex == 1 1560 1561 clipBoxes = colr.ClipList.clips 1562 assert {"uniE001", "uniE002", "uniE003"} == set(clipBoxes) 1563 assert clipBoxes["uniE002"] == clipBoxes["uniE003"] 1564 1565 assert "CPAL" in subset_font 1566 cpal = subset_font["CPAL"] 1567 assert [ 1568 tuple(v / 255 for v in (c.red, c.green, c.blue, c.alpha)) 1569 for c in cpal.palettes[0] 1570 ] == [ 1571 # the first color 'red' was pruned 1572 (0.0, 1.0, 0.0, 1.0), # green 1573 (0.0, 0.0, 1.0, 1.0), # blue 1574 ] 1575 1576 1577def test_subset_COLRv1_and_CPALv1(colrv1_cpalv1_path): 1578 subset_path = colrv1_cpalv1_path.parent / (colrv1_cpalv1_path.name + ".subset") 1579 1580 subset.main( 1581 [ 1582 str(colrv1_cpalv1_path), 1583 "--glyph-names", 1584 f"--output-file={subset_path}", 1585 "--unicodes=E002,E003,E004", 1586 ] 1587 ) 1588 subset_font = TTFont(subset_path) 1589 1590 assert "CPAL" in subset_font 1591 cpal = subset_font["CPAL"] 1592 name_table = subset_font["name"] 1593 assert [ 1594 name_table.getDebugName(name_id) for name_id in cpal.paletteEntryLabels 1595 ] == [ 1596 # "first color", # The first color was pruned 1597 "second color", 1598 "third color", 1599 ] 1600 # check that the "first color" name is dropped from name table 1601 font = TTFont(colrv1_cpalv1_path) 1602 1603 first_color_nameID = None 1604 for n in font["name"].names: 1605 if n.toUnicode() == "first color": 1606 first_color_nameID = n.nameID 1607 break 1608 assert first_color_nameID is not None 1609 assert all(n.nameID != first_color_nameID for n in name_table.names) 1610 1611 1612def test_subset_COLRv1_and_CPALv1_keep_nameID(colrv1_cpalv1_path): 1613 subset_path = colrv1_cpalv1_path.parent / (colrv1_cpalv1_path.name + ".subset") 1614 1615 # figure out the name ID of first color so we can keep it 1616 font = TTFont(colrv1_cpalv1_path) 1617 1618 first_color_nameID = None 1619 for n in font["name"].names: 1620 if n.toUnicode() == "first color": 1621 first_color_nameID = n.nameID 1622 break 1623 assert first_color_nameID is not None 1624 1625 subset.main( 1626 [ 1627 str(colrv1_cpalv1_path), 1628 "--glyph-names", 1629 f"--output-file={subset_path}", 1630 "--unicodes=E002,E003,E004", 1631 f"--name-IDs={first_color_nameID}", 1632 ] 1633 ) 1634 subset_font = TTFont(subset_path) 1635 1636 assert "CPAL" in subset_font 1637 cpal = subset_font["CPAL"] 1638 name_table = subset_font["name"] 1639 assert [ 1640 name_table.getDebugName(name_id) for name_id in cpal.paletteEntryLabels 1641 ] == [ 1642 # "first color", # The first color was pruned 1643 "second color", 1644 "third color", 1645 ] 1646 1647 # Check that the name ID is kept 1648 assert any(n.nameID == first_color_nameID for n in name_table.names) 1649 1650 1651def test_subset_COLRv1_and_CPALv1_share_nameID(colrv1_cpalv1_share_nameID_path): 1652 subset_path = colrv1_cpalv1_share_nameID_path.parent / ( 1653 colrv1_cpalv1_share_nameID_path.name + ".subset" 1654 ) 1655 1656 subset.main( 1657 [ 1658 str(colrv1_cpalv1_share_nameID_path), 1659 "--glyph-names", 1660 f"--output-file={subset_path}", 1661 "--unicodes=E002,E003,E004", 1662 ] 1663 ) 1664 subset_font = TTFont(subset_path) 1665 1666 assert "CPAL" in subset_font 1667 cpal = subset_font["CPAL"] 1668 name_table = subset_font["name"] 1669 assert [ 1670 name_table.getDebugName(name_id) for name_id in cpal.paletteEntryLabels 1671 ] == [ 1672 # "first color", # The first color was pruned 1673 "second color", 1674 "third color", 1675 ] 1676 1677 # Check that the name ID 1 is kept 1678 assert any(n.nameID == 1 for n in name_table.names) 1679 1680 1681def test_subset_COLRv1_and_CPAL_drop_empty(colrv1_path): 1682 subset_path = colrv1_path.parent / (colrv1_path.name + ".subset") 1683 1684 subset.main( 1685 [ 1686 str(colrv1_path), 1687 "--glyph-names", 1688 f"--output-file={subset_path}", 1689 "--glyphs=glyph00010", 1690 ] 1691 ) 1692 subset_font = TTFont(subset_path) 1693 1694 glyph_set = set(subset_font.getGlyphOrder()) 1695 1696 assert "glyph00010" in glyph_set 1697 assert "uniE000" not in glyph_set 1698 1699 assert "COLR" not in subset_font 1700 assert "CPAL" not in subset_font 1701 1702 1703def test_subset_COLRv1_downgrade_version(colrv1_path): 1704 subset_path = colrv1_path.parent / (colrv1_path.name + ".subset") 1705 1706 subset.main( 1707 [ 1708 str(colrv1_path), 1709 "--glyph-names", 1710 f"--output-file={subset_path}", 1711 "--unicodes=E004", 1712 ] 1713 ) 1714 subset_font = TTFont(subset_path) 1715 1716 assert set(subset_font.getGlyphOrder()) == { 1717 ".notdef", 1718 "uniE004", 1719 "glyph00016", 1720 "glyph00017", 1721 "glyph00018", 1722 } 1723 1724 assert "COLR" in subset_font 1725 assert subset_font["COLR"].version == 0 1726 1727 1728def test_subset_COLRv1_drop_all_v0_glyphs(colrv1_path): 1729 subset_path = colrv1_path.parent / (colrv1_path.name + ".subset") 1730 1731 subset.main( 1732 [ 1733 str(colrv1_path), 1734 "--glyph-names", 1735 f"--output-file={subset_path}", 1736 "--unicodes=E003", 1737 ] 1738 ) 1739 subset_font = TTFont(subset_path) 1740 1741 assert set(subset_font.getGlyphOrder()) == { 1742 ".notdef", 1743 "uniE001", 1744 "uniE003", 1745 "glyph00012", 1746 "glyph00013", 1747 } 1748 1749 assert "COLR" in subset_font 1750 colr = subset_font["COLR"] 1751 assert colr.version == 1 1752 assert colr.table.BaseGlyphRecordCount == 0 1753 assert colr.table.BaseGlyphRecordArray is None 1754 assert colr.table.LayerRecordArray is None 1755 assert colr.table.LayerRecordCount is 0 1756 1757 1758def test_subset_COLRv1_no_ClipList(colrv1_path): 1759 font = TTFont(colrv1_path) 1760 font["COLR"].table.ClipList = None # empty ClipList 1761 font.save(colrv1_path) 1762 1763 subset_path = colrv1_path.parent / (colrv1_path.name + ".subset") 1764 subset.main( 1765 [ 1766 str(colrv1_path), 1767 f"--output-file={subset_path}", 1768 "--unicodes=*", 1769 ] 1770 ) 1771 subset_font = TTFont(subset_path) 1772 assert subset_font["COLR"].table.ClipList is None 1773 1774 1775def test_subset_keep_size_drop_empty_stylistic_set(): 1776 fb = FontBuilder(unitsPerEm=1000, isTTF=True) 1777 glyph_order = [".notdef", "a", "b", "b.ss01"] 1778 fb.setupGlyphOrder(glyph_order) 1779 fb.setupGlyf({g: TTGlyphPen(None).glyph() for g in glyph_order}) 1780 fb.setupCharacterMap({ord("a"): "a", ord("b"): "b"}) 1781 fb.setupHorizontalMetrics({g: (500, 0) for g in glyph_order}) 1782 fb.setupHorizontalHeader() 1783 fb.setupOS2() 1784 fb.setupPost() 1785 fb.setupNameTable({"familyName": "TestKeepSizeFeature", "styleName": "Regular"}) 1786 fb.addOpenTypeFeatures( 1787 """ 1788 feature size { 1789 parameters 10.0 0; 1790 } size; 1791 feature ss01 { 1792 featureNames { 1793 name "Alternate b"; 1794 }; 1795 sub b by b.ss01; 1796 } ss01; 1797 """ 1798 ) 1799 1800 buf = io.BytesIO() 1801 fb.save(buf) 1802 buf.seek(0) 1803 1804 font = TTFont(buf) 1805 1806 gpos_features = font["GPOS"].table.FeatureList.FeatureRecord 1807 assert gpos_features[0].FeatureTag == "size" 1808 assert isinstance(gpos_features[0].Feature.FeatureParams, ot.FeatureParamsSize) 1809 assert gpos_features[0].Feature.LookupCount == 0 1810 gsub_features = font["GSUB"].table.FeatureList.FeatureRecord 1811 assert gsub_features[0].FeatureTag == "ss01" 1812 assert isinstance( 1813 gsub_features[0].Feature.FeatureParams, ot.FeatureParamsStylisticSet 1814 ) 1815 1816 options = subset.Options(layout_features=["*"]) 1817 subsetter = subset.Subsetter(options) 1818 subsetter.populate(unicodes=[ord("a")]) 1819 subsetter.subset(font) 1820 1821 # empty size feature was kept 1822 gpos_features = font["GPOS"].table.FeatureList.FeatureRecord 1823 assert gpos_features[0].FeatureTag == "size" 1824 assert isinstance(gpos_features[0].Feature.FeatureParams, ot.FeatureParamsSize) 1825 assert gpos_features[0].Feature.LookupCount == 0 1826 # empty ss01 feature was dropped 1827 assert font["GSUB"].table.FeatureList.FeatureCount == 0 1828 1829 1830@pytest.mark.skipif(etree is not None, reason="lxml is installed") 1831def test_subset_svg_missing_lxml(ttf_path): 1832 # add dummy SVG table and confirm we raise ImportError upon trying to subset it 1833 font = TTFont(ttf_path) 1834 font["SVG "] = newTable("SVG ") 1835 font["SVG "].docList = [('<svg><g id="glyph1"/></svg>', 1, 1)] 1836 font.save(ttf_path) 1837 1838 with pytest.raises(ImportError): 1839 subset.main([str(ttf_path), "--gids=0,1"]) 1840 1841 1842def test_subset_COLR_glyph_closure(tmp_path): 1843 # https://github.com/fonttools/fonttools/issues/2461 1844 font = TTFont() 1845 ttx = pathlib.Path(__file__).parent / "data" / "BungeeColor-Regular.ttx" 1846 font.importXML(ttx) 1847 1848 color_layers = font["COLR"].ColorLayers 1849 assert ".notdef" in color_layers 1850 assert "Agrave" in color_layers 1851 assert "grave" in color_layers 1852 1853 font_path = tmp_path / "BungeeColor-Regular.ttf" 1854 subset_path = font_path.with_suffix(".subset.ttf)") 1855 font.save(font_path) 1856 1857 subset.main( 1858 [ 1859 str(font_path), 1860 "--glyph-names", 1861 f"--output-file={subset_path}", 1862 "--glyphs=Agrave", 1863 ] 1864 ) 1865 subset_font = TTFont(subset_path) 1866 1867 glyph_order = subset_font.getGlyphOrder() 1868 1869 assert glyph_order == [ 1870 ".notdef", # '.notdef' is always included automatically 1871 "A", 1872 "grave", 1873 "Agrave", 1874 ".notdef.alt001", 1875 ".notdef.alt002", 1876 "A.alt002", 1877 "Agrave.alt001", 1878 "Agrave.alt002", 1879 "grave.alt002", 1880 ] 1881 1882 color_layers = subset_font["COLR"].ColorLayers 1883 assert ".notdef" in color_layers 1884 assert "Agrave" in color_layers 1885 # Agrave 'glyf' uses grave. It should be retained in 'glyf' but NOT in 1886 # COLR when we subset down to Agrave. 1887 assert "grave" not in color_layers 1888 1889 1890def test_subset_recalc_xAvgCharWidth(ttf_path): 1891 # Note that the font in in the *ttLib*/data/TestTTF-Regular.ttx file, 1892 # not this subset/data folder. 1893 font = TTFont(ttf_path) 1894 xAvgCharWidth_before = font["OS/2"].xAvgCharWidth 1895 1896 subset_path = ttf_path.with_suffix(".subset.ttf") 1897 subset.main( 1898 [ 1899 str(ttf_path), 1900 f"--output-file={subset_path}", 1901 # Keep only the ellipsis, which is very wide, that ought to bump up the average 1902 "--glyphs=ellipsis", 1903 "--recalc-average-width", 1904 "--no-prune-unicode-ranges", 1905 ] 1906 ) 1907 subset_font = TTFont(subset_path) 1908 xAvgCharWidth_after = subset_font["OS/2"].xAvgCharWidth 1909 1910 # Check that the value gets updated 1911 assert xAvgCharWidth_after != xAvgCharWidth_before 1912 1913 # Check that the value gets updated to the actual new value 1914 subset_font["OS/2"].recalcAvgCharWidth(subset_font) 1915 assert xAvgCharWidth_after == subset_font["OS/2"].xAvgCharWidth 1916 1917 1918if __name__ == "__main__": 1919 sys.exit(unittest.main()) 1920 1921 1922def test_subset_prune_gdef_markglyphsetsdef(): 1923 # GDEF_MarkGlyphSetsDef 1924 fb = FontBuilder(unitsPerEm=1000, isTTF=True) 1925 glyph_order = [ 1926 ".notdef", 1927 "A", 1928 "Aacute", 1929 "Acircumflex", 1930 "Adieresis", 1931 "a", 1932 "aacute", 1933 "acircumflex", 1934 "adieresis", 1935 "dieresiscomb", 1936 "acutecomb", 1937 "circumflexcomb", 1938 ] 1939 fb.setupGlyphOrder(glyph_order) 1940 fb.setupGlyf({g: TTGlyphPen(None).glyph() for g in glyph_order}) 1941 fb.setupHorizontalMetrics({g: (500, 0) for g in glyph_order}) 1942 fb.setupHorizontalHeader() 1943 fb.setupPost() 1944 fb.setupNameTable( 1945 {"familyName": "TestGDEFMarkGlyphSetsDef", "styleName": "Regular"} 1946 ) 1947 fb.addOpenTypeFeatures( 1948 """ 1949 feature ccmp { 1950 lookup ccmp_1 { 1951 lookupflag UseMarkFilteringSet [acutecomb]; 1952 sub a acutecomb by aacute; 1953 sub A acutecomb by Aacute; 1954 } ccmp_1; 1955 lookup ccmp_2 { 1956 lookupflag UseMarkFilteringSet [circumflexcomb]; 1957 sub a circumflexcomb by acircumflex; 1958 sub A circumflexcomb by Acircumflex; 1959 } ccmp_2; 1960 lookup ccmp_3 { 1961 lookupflag UseMarkFilteringSet [dieresiscomb]; 1962 sub a dieresiscomb by adieresis; 1963 sub A dieresiscomb by Adieresis; 1964 sub A acutecomb by Aacute; 1965 } ccmp_3; 1966 } ccmp; 1967 """ 1968 ) 1969 1970 buf = io.BytesIO() 1971 fb.save(buf) 1972 buf.seek(0) 1973 1974 font = TTFont(buf) 1975 1976 features = font["GSUB"].table.FeatureList.FeatureRecord 1977 assert features[0].FeatureTag == "ccmp" 1978 lookups = font["GSUB"].table.LookupList.Lookup 1979 assert lookups[0].LookupFlag == 16 1980 assert lookups[0].MarkFilteringSet == 0 1981 assert lookups[1].LookupFlag == 16 1982 assert lookups[1].MarkFilteringSet == 1 1983 assert lookups[2].LookupFlag == 16 1984 assert lookups[2].MarkFilteringSet == 2 1985 marksets = font["GDEF"].table.MarkGlyphSetsDef.Coverage 1986 assert marksets[0].glyphs == ["acutecomb"] 1987 assert marksets[1].glyphs == ["circumflexcomb"] 1988 assert marksets[2].glyphs == ["dieresiscomb"] 1989 1990 options = subset.Options(layout_features=["*"]) 1991 subsetter = subset.Subsetter(options) 1992 subsetter.populate(glyphs=["A", "a", "acutecomb", "dieresiscomb"]) 1993 subsetter.subset(font) 1994 1995 features = font["GSUB"].table.FeatureList.FeatureRecord 1996 assert features[0].FeatureTag == "ccmp" 1997 lookups = font["GSUB"].table.LookupList.Lookup 1998 assert lookups[0].LookupFlag == 16 1999 assert lookups[0].MarkFilteringSet == 0 2000 assert lookups[1].LookupFlag == 16 2001 assert lookups[1].MarkFilteringSet == 1 2002 marksets = font["GDEF"].table.MarkGlyphSetsDef.Coverage 2003 assert marksets[0].glyphs == ["acutecomb"] 2004 assert marksets[1].glyphs == ["dieresiscomb"] 2005 2006 buf = io.BytesIO() 2007 fb.save(buf) 2008 buf.seek(0) 2009 2010 font = TTFont(buf) 2011 2012 options = subset.Options(layout_features=["*"], layout_closure=False) 2013 subsetter = subset.Subsetter(options) 2014 subsetter.populate(glyphs=["A", "acutecomb", "Aacute"]) 2015 subsetter.subset(font) 2016 2017 features = font["GSUB"].table.FeatureList.FeatureRecord 2018 assert features[0].FeatureTag == "ccmp" 2019 lookups = font["GSUB"].table.LookupList.Lookup 2020 assert lookups[0].LookupFlag == 16 2021 assert lookups[0].MarkFilteringSet == 0 2022 assert lookups[1].LookupFlag == 0 2023 assert lookups[1].MarkFilteringSet == None 2024 marksets = font["GDEF"].table.MarkGlyphSetsDef.Coverage 2025 assert marksets[0].glyphs == ["acutecomb"] 2026