1from fontTools.ttLib import TTFont, newTable 2from fontTools.varLib import build, load_designspace 3from fontTools.varLib.errors import VarLibValidationError 4import fontTools.varLib.errors as varLibErrors 5from fontTools.varLib.mutator import instantiateVariableFont 6from fontTools.varLib import main as varLib_main, load_masters 7from fontTools.varLib import set_default_weight_width_slant 8from fontTools.designspaceLib import ( 9 DesignSpaceDocumentError, DesignSpaceDocument, SourceDescriptor, 10) 11from fontTools.feaLib.builder import addOpenTypeFeaturesFromString 12import difflib 13from io import BytesIO 14import os 15import shutil 16import sys 17import tempfile 18import unittest 19import pytest 20 21 22def reload_font(font): 23 """(De)serialize to get final binary layout.""" 24 buf = BytesIO() 25 font.save(buf) 26 # Close the font to release filesystem resources so that on Windows the tearDown 27 # method can successfully remove the temporary directory created during setUp. 28 font.close() 29 buf.seek(0) 30 return TTFont(buf) 31 32 33class BuildTest(unittest.TestCase): 34 def __init__(self, methodName): 35 unittest.TestCase.__init__(self, methodName) 36 # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, 37 # and fires deprecation warnings if a program uses the old name. 38 if not hasattr(self, "assertRaisesRegex"): 39 self.assertRaisesRegex = self.assertRaisesRegexp 40 41 def setUp(self): 42 self.tempdir = None 43 self.num_tempfiles = 0 44 45 def tearDown(self): 46 if self.tempdir: 47 shutil.rmtree(self.tempdir) 48 49 def get_test_input(self, test_file_or_folder, copy=False): 50 parent_dir = os.path.dirname(__file__) 51 path = os.path.join(parent_dir, "data", test_file_or_folder) 52 if copy: 53 copied_path = os.path.join(self.tempdir, test_file_or_folder) 54 shutil.copy2(path, copied_path) 55 return copied_path 56 else: 57 return path 58 59 @staticmethod 60 def get_test_output(test_file_or_folder): 61 path, _ = os.path.split(__file__) 62 return os.path.join(path, "data", "test_results", test_file_or_folder) 63 64 @staticmethod 65 def get_file_list(folder, suffix, prefix=''): 66 all_files = os.listdir(folder) 67 file_list = [] 68 for p in all_files: 69 if p.startswith(prefix) and p.endswith(suffix): 70 file_list.append(os.path.abspath(os.path.join(folder, p))) 71 return file_list 72 73 def temp_path(self, suffix): 74 self.temp_dir() 75 self.num_tempfiles += 1 76 return os.path.join(self.tempdir, 77 "tmp%d%s" % (self.num_tempfiles, suffix)) 78 79 def temp_dir(self): 80 if not self.tempdir: 81 self.tempdir = tempfile.mkdtemp() 82 83 def read_ttx(self, path): 84 lines = [] 85 with open(path, "r", encoding="utf-8") as ttx: 86 for line in ttx.readlines(): 87 # Elide ttFont attributes because ttLibVersion may change, 88 # and use os-native line separators so we can run difflib. 89 if line.startswith("<ttFont "): 90 lines.append("<ttFont>" + os.linesep) 91 else: 92 lines.append(line.rstrip() + os.linesep) 93 return lines 94 95 def expect_ttx(self, font, expected_ttx, tables): 96 path = self.temp_path(suffix=".ttx") 97 font.saveXML(path, tables=tables) 98 actual = self.read_ttx(path) 99 expected = self.read_ttx(expected_ttx) 100 if actual != expected: 101 for line in difflib.unified_diff( 102 expected, actual, fromfile=expected_ttx, tofile=path): 103 sys.stdout.write(line) 104 self.fail("TTX output is different from expected") 105 106 def check_ttx_dump(self, font, expected_ttx, tables, suffix): 107 """Ensure the TTX dump is the same after saving and reloading the font.""" 108 path = self.temp_path(suffix=suffix) 109 font.save(path) 110 self.expect_ttx(TTFont(path), expected_ttx, tables) 111 112 def compile_font(self, path, suffix, temp_dir): 113 ttx_filename = os.path.basename(path) 114 savepath = os.path.join(temp_dir, ttx_filename.replace('.ttx', suffix)) 115 font = TTFont(recalcBBoxes=False, recalcTimestamp=False) 116 font.importXML(path) 117 font.save(savepath, reorderTables=None) 118 return font, savepath 119 120 def _run_varlib_build_test(self, designspace_name, font_name, tables, 121 expected_ttx_name, save_before_dump=False, 122 post_process_master=None): 123 suffix = '.ttf' 124 ds_path = self.get_test_input(designspace_name + '.designspace') 125 ufo_dir = self.get_test_input('master_ufo') 126 ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') 127 128 self.temp_dir() 129 ttx_paths = self.get_file_list(ttx_dir, '.ttx', font_name + '-') 130 for path in ttx_paths: 131 font, savepath = self.compile_font(path, suffix, self.tempdir) 132 if post_process_master is not None: 133 post_process_master(font, savepath) 134 135 finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) 136 varfont, model, _ = build(ds_path, finder) 137 138 if save_before_dump: 139 # some data (e.g. counts printed in TTX inline comments) is only 140 # calculated at compile time, so before we can compare the TTX 141 # dumps we need to save to a temporary stream, and realod the font 142 varfont = reload_font(varfont) 143 144 expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx') 145 self.expect_ttx(varfont, expected_ttx_path, tables) 146 self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix) 147# ----- 148# Tests 149# ----- 150 151 def test_varlib_build_ttf(self): 152 """Designspace file contains <axes> element.""" 153 self._run_varlib_build_test( 154 designspace_name='Build', 155 font_name='TestFamily', 156 tables=['GDEF', 'HVAR', 'MVAR', 'fvar', 'gvar'], 157 expected_ttx_name='Build' 158 ) 159 160 def test_varlib_build_no_axes_ttf(self): 161 """Designspace file does not contain an <axes> element.""" 162 ds_path = self.get_test_input('InterpolateLayout3.designspace') 163 with self.assertRaisesRegex(DesignSpaceDocumentError, "No axes defined"): 164 build(ds_path) 165 166 def test_varlib_avar_single_axis(self): 167 """Designspace file contains a 'weight' axis with <map> elements 168 modifying the normalization mapping. An 'avar' table is generated. 169 """ 170 test_name = 'BuildAvarSingleAxis' 171 self._run_varlib_build_test( 172 designspace_name=test_name, 173 font_name='TestFamily3', 174 tables=['avar'], 175 expected_ttx_name=test_name 176 ) 177 178 def test_varlib_avar_with_identity_maps(self): 179 """Designspace file contains two 'weight' and 'width' axes both with 180 <map> elements. 181 182 The 'width' axis only contains identity mappings, however the resulting 183 avar segment will not be empty but will contain the default axis value 184 maps: {-1.0: -1.0, 0.0: 0.0, 1.0: 1.0}. 185 186 This is to work around an issue with some rasterizers: 187 https://github.com/googlei18n/fontmake/issues/295 188 https://github.com/fonttools/fonttools/issues/1011 189 """ 190 test_name = 'BuildAvarIdentityMaps' 191 self._run_varlib_build_test( 192 designspace_name=test_name, 193 font_name='TestFamily3', 194 tables=['avar'], 195 expected_ttx_name=test_name 196 ) 197 198 def test_varlib_avar_empty_axis(self): 199 """Designspace file contains two 'weight' and 'width' axes, but 200 only one axis ('weight') has some <map> elements. 201 202 Even if no <map> elements are defined for the 'width' axis, the 203 resulting avar segment still contains the default axis value maps: 204 {-1.0: -1.0, 0.0: 0.0, 1.0: 1.0}. 205 206 This is again to work around an issue with some rasterizers: 207 https://github.com/googlei18n/fontmake/issues/295 208 https://github.com/fonttools/fonttools/issues/1011 209 """ 210 test_name = 'BuildAvarEmptyAxis' 211 self._run_varlib_build_test( 212 designspace_name=test_name, 213 font_name='TestFamily3', 214 tables=['avar'], 215 expected_ttx_name=test_name 216 ) 217 218 def test_varlib_build_feature_variations(self): 219 """Designspace file contains <rules> element, used to build 220 GSUB FeatureVariations table. 221 """ 222 self._run_varlib_build_test( 223 designspace_name="FeatureVars", 224 font_name="TestFamily", 225 tables=["fvar", "GSUB"], 226 expected_ttx_name="FeatureVars", 227 save_before_dump=True, 228 ) 229 230 def test_varlib_build_feature_variations_custom_tag(self): 231 """Designspace file contains <rules> element, used to build 232 GSUB FeatureVariations table. 233 """ 234 self._run_varlib_build_test( 235 designspace_name="FeatureVarsCustomTag", 236 font_name="TestFamily", 237 tables=["fvar", "GSUB"], 238 expected_ttx_name="FeatureVarsCustomTag", 239 save_before_dump=True, 240 ) 241 242 def test_varlib_build_feature_variations_whole_range(self): 243 """Designspace file contains <rules> element specifying the entire design 244 space, used to build GSUB FeatureVariations table. 245 """ 246 self._run_varlib_build_test( 247 designspace_name="FeatureVarsWholeRange", 248 font_name="TestFamily", 249 tables=["fvar", "GSUB"], 250 expected_ttx_name="FeatureVarsWholeRange", 251 save_before_dump=True, 252 ) 253 254 def test_varlib_build_feature_variations_whole_range_empty(self): 255 """Designspace file contains <rules> element without a condition, specifying 256 the entire design space, used to build GSUB FeatureVariations table. 257 """ 258 self._run_varlib_build_test( 259 designspace_name="FeatureVarsWholeRangeEmpty", 260 font_name="TestFamily", 261 tables=["fvar", "GSUB"], 262 expected_ttx_name="FeatureVarsWholeRange", 263 save_before_dump=True, 264 ) 265 266 def test_varlib_build_feature_variations_with_existing_rclt(self): 267 """Designspace file contains <rules> element, used to build GSUB 268 FeatureVariations table. <rules> is specified to do its OT processing 269 "last", so a 'rclt' feature will be used or created. This test covers 270 the case when a 'rclt' already exists in the masters. 271 272 We dynamically add a 'rclt' feature to an existing set of test 273 masters, to avoid adding more test data. 274 275 The multiple languages are done to verify whether multiple existing 276 'rclt' features are updated correctly. 277 """ 278 def add_rclt(font, savepath): 279 features = """ 280 languagesystem DFLT dflt; 281 languagesystem latn dflt; 282 languagesystem latn NLD; 283 284 feature rclt { 285 script latn; 286 language NLD; 287 lookup A { 288 sub uni0041 by uni0061; 289 } A; 290 language dflt; 291 lookup B { 292 sub uni0041 by uni0061; 293 } B; 294 } rclt; 295 """ 296 addOpenTypeFeaturesFromString(font, features) 297 font.save(savepath) 298 self._run_varlib_build_test( 299 designspace_name="FeatureVars", 300 font_name="TestFamily", 301 tables=["fvar", "GSUB"], 302 expected_ttx_name="FeatureVars_rclt", 303 save_before_dump=True, 304 post_process_master=add_rclt, 305 ) 306 307 def test_varlib_gvar_explicit_delta(self): 308 """The variable font contains a composite glyph odieresis which does not 309 need a gvar entry, because all its deltas are 0, but it must be added 310 anyway to work around an issue with macOS 10.14. 311 312 https://github.com/fonttools/fonttools/issues/1381 313 """ 314 test_name = 'BuildGvarCompositeExplicitDelta' 315 self._run_varlib_build_test( 316 designspace_name=test_name, 317 font_name='TestFamily4', 318 tables=['gvar'], 319 expected_ttx_name=test_name 320 ) 321 322 def test_varlib_nonmarking_CFF2(self): 323 self.temp_dir() 324 325 ds_path = self.get_test_input('TestNonMarkingCFF2.designspace', copy=True) 326 ttx_dir = self.get_test_input("master_non_marking_cff2") 327 expected_ttx_path = self.get_test_output("TestNonMarkingCFF2.ttx") 328 329 for path in self.get_file_list(ttx_dir, '.ttx', 'TestNonMarkingCFF2_'): 330 self.compile_font(path, ".otf", self.tempdir) 331 332 ds = DesignSpaceDocument.fromfile(ds_path) 333 for source in ds.sources: 334 source.path = os.path.join( 335 self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf") 336 ) 337 ds.updatePaths() 338 339 varfont, _, _ = build(ds) 340 varfont = reload_font(varfont) 341 342 tables = ["CFF2"] 343 self.expect_ttx(varfont, expected_ttx_path, tables) 344 345 def test_varlib_build_CFF2(self): 346 self.temp_dir() 347 348 ds_path = self.get_test_input('TestCFF2.designspace', copy=True) 349 ttx_dir = self.get_test_input("master_cff2") 350 expected_ttx_path = self.get_test_output("BuildTestCFF2.ttx") 351 352 for path in self.get_file_list(ttx_dir, '.ttx', 'TestCFF2_'): 353 self.compile_font(path, ".otf", self.tempdir) 354 355 ds = DesignSpaceDocument.fromfile(ds_path) 356 for source in ds.sources: 357 source.path = os.path.join( 358 self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf") 359 ) 360 ds.updatePaths() 361 362 varfont, _, _ = build(ds) 363 varfont = reload_font(varfont) 364 365 tables = ["fvar", "CFF2"] 366 self.expect_ttx(varfont, expected_ttx_path, tables) 367 368 def test_varlib_build_CFF2_from_CFF2(self): 369 self.temp_dir() 370 371 ds_path = self.get_test_input('TestCFF2Input.designspace', copy=True) 372 ttx_dir = self.get_test_input("master_cff2_input") 373 expected_ttx_path = self.get_test_output("BuildTestCFF2.ttx") 374 375 for path in self.get_file_list(ttx_dir, '.ttx', 'TestCFF2_'): 376 self.compile_font(path, ".otf", self.tempdir) 377 378 ds = DesignSpaceDocument.fromfile(ds_path) 379 for source in ds.sources: 380 source.path = os.path.join( 381 self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf") 382 ) 383 ds.updatePaths() 384 385 varfont, _, _ = build(ds) 386 varfont = reload_font(varfont) 387 388 tables = ["fvar", "CFF2"] 389 self.expect_ttx(varfont, expected_ttx_path, tables) 390 391 def test_varlib_build_sparse_CFF2(self): 392 self.temp_dir() 393 394 ds_path = self.get_test_input('TestSparseCFF2VF.designspace', copy=True) 395 ttx_dir = self.get_test_input("master_sparse_cff2") 396 expected_ttx_path = self.get_test_output("TestSparseCFF2VF.ttx") 397 398 for path in self.get_file_list(ttx_dir, '.ttx', 'MasterSet_Kanji-'): 399 self.compile_font(path, ".otf", self.tempdir) 400 401 ds = DesignSpaceDocument.fromfile(ds_path) 402 for source in ds.sources: 403 source.path = os.path.join( 404 self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf") 405 ) 406 ds.updatePaths() 407 408 varfont, _, _ = build(ds) 409 varfont = reload_font(varfont) 410 411 tables = ["fvar", "CFF2"] 412 self.expect_ttx(varfont, expected_ttx_path, tables) 413 414 def test_varlib_build_vpal(self): 415 self.temp_dir() 416 417 ds_path = self.get_test_input('test_vpal.designspace', copy=True) 418 ttx_dir = self.get_test_input("master_vpal_test") 419 expected_ttx_path = self.get_test_output("test_vpal.ttx") 420 421 for path in self.get_file_list(ttx_dir, '.ttx', 'master_vpal_test_'): 422 self.compile_font(path, ".otf", self.tempdir) 423 424 ds = DesignSpaceDocument.fromfile(ds_path) 425 for source in ds.sources: 426 source.path = os.path.join( 427 self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf") 428 ) 429 ds.updatePaths() 430 431 varfont, _, _ = build(ds) 432 varfont = reload_font(varfont) 433 434 tables = ["GPOS"] 435 self.expect_ttx(varfont, expected_ttx_path, tables) 436 437 def test_varlib_main_ttf(self): 438 """Mostly for testing varLib.main() 439 """ 440 suffix = '.ttf' 441 ds_path = self.get_test_input('Build.designspace') 442 ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') 443 444 self.temp_dir() 445 ttf_dir = os.path.join(self.tempdir, 'master_ttf_interpolatable') 446 os.makedirs(ttf_dir) 447 ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily-') 448 for path in ttx_paths: 449 self.compile_font(path, suffix, ttf_dir) 450 451 ds_copy = os.path.join(self.tempdir, 'BuildMain.designspace') 452 shutil.copy2(ds_path, ds_copy) 453 454 # by default, varLib.main finds master TTFs inside a 455 # 'master_ttf_interpolatable' subfolder in current working dir 456 cwd = os.getcwd() 457 os.chdir(self.tempdir) 458 try: 459 varLib_main([ds_copy]) 460 finally: 461 os.chdir(cwd) 462 463 varfont_path = os.path.splitext(ds_copy)[0] + '-VF' + suffix 464 self.assertTrue(os.path.exists(varfont_path)) 465 466 # try again passing an explicit --master-finder 467 os.remove(varfont_path) 468 finder = "%s/master_ttf_interpolatable/{stem}.ttf" % self.tempdir 469 varLib_main([ds_copy, "--master-finder", finder]) 470 self.assertTrue(os.path.exists(varfont_path)) 471 472 # and also with explicit -o output option 473 os.remove(varfont_path) 474 varfont_path = os.path.splitext(varfont_path)[0] + "-o" + suffix 475 varLib_main([ds_copy, "-o", varfont_path, "--master-finder", finder]) 476 self.assertTrue(os.path.exists(varfont_path)) 477 478 varfont = TTFont(varfont_path) 479 tables = [table_tag for table_tag in varfont.keys() if table_tag != 'head'] 480 expected_ttx_path = self.get_test_output('BuildMain.ttx') 481 self.expect_ttx(varfont, expected_ttx_path, tables) 482 483 def test_varlib_build_from_ds_object_in_memory_ttfonts(self): 484 ds_path = self.get_test_input("Build.designspace") 485 ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf") 486 expected_ttx_path = self.get_test_output("BuildMain.ttx") 487 488 self.temp_dir() 489 for path in self.get_file_list(ttx_dir, '.ttx', 'TestFamily-'): 490 self.compile_font(path, ".ttf", self.tempdir) 491 492 ds = DesignSpaceDocument.fromfile(ds_path) 493 for source in ds.sources: 494 filename = os.path.join( 495 self.tempdir, os.path.basename(source.filename).replace(".ufo", ".ttf") 496 ) 497 source.font = TTFont( 498 filename, recalcBBoxes=False, recalcTimestamp=False, lazy=True 499 ) 500 source.filename = None # Make sure no file path gets into build() 501 502 varfont, _, _ = build(ds) 503 varfont = reload_font(varfont) 504 tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"] 505 self.expect_ttx(varfont, expected_ttx_path, tables) 506 507 def test_varlib_build_from_ttf_paths(self): 508 self.temp_dir() 509 510 ds_path = self.get_test_input("Build.designspace", copy=True) 511 ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf") 512 expected_ttx_path = self.get_test_output("BuildMain.ttx") 513 514 for path in self.get_file_list(ttx_dir, '.ttx', 'TestFamily-'): 515 self.compile_font(path, ".ttf", self.tempdir) 516 517 ds = DesignSpaceDocument.fromfile(ds_path) 518 for source in ds.sources: 519 source.path = os.path.join( 520 self.tempdir, os.path.basename(source.filename).replace(".ufo", ".ttf") 521 ) 522 ds.updatePaths() 523 524 varfont, _, _ = build(ds) 525 varfont = reload_font(varfont) 526 tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"] 527 self.expect_ttx(varfont, expected_ttx_path, tables) 528 529 def test_varlib_build_from_ttx_paths(self): 530 ds_path = self.get_test_input("Build.designspace") 531 ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf") 532 expected_ttx_path = self.get_test_output("BuildMain.ttx") 533 534 ds = DesignSpaceDocument.fromfile(ds_path) 535 for source in ds.sources: 536 source.path = os.path.join( 537 ttx_dir, os.path.basename(source.filename).replace(".ufo", ".ttx") 538 ) 539 ds.updatePaths() 540 541 varfont, _, _ = build(ds) 542 varfont = reload_font(varfont) 543 tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"] 544 self.expect_ttx(varfont, expected_ttx_path, tables) 545 546 def test_varlib_build_sparse_masters(self): 547 ds_path = self.get_test_input("SparseMasters.designspace") 548 expected_ttx_path = self.get_test_output("SparseMasters.ttx") 549 550 varfont, _, _ = build(ds_path) 551 varfont = reload_font(varfont) 552 tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"] 553 self.expect_ttx(varfont, expected_ttx_path, tables) 554 555 def test_varlib_build_lazy_masters(self): 556 # See https://github.com/fonttools/fonttools/issues/1808 557 ds_path = self.get_test_input("SparseMasters.designspace") 558 expected_ttx_path = self.get_test_output("SparseMasters.ttx") 559 560 def _open_font(master_path, master_finder=lambda s: s): 561 font = TTFont() 562 font.importXML(master_path) 563 buf = BytesIO() 564 font.save(buf, reorderTables=False) 565 buf.seek(0) 566 font = TTFont(buf, lazy=True) # reopen in lazy mode, to reproduce #1808 567 return font 568 569 ds = DesignSpaceDocument.fromfile(ds_path) 570 ds.loadSourceFonts(_open_font) 571 varfont, _, _ = build(ds) 572 varfont = reload_font(varfont) 573 tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"] 574 self.expect_ttx(varfont, expected_ttx_path, tables) 575 576 def test_varlib_build_sparse_masters_MVAR(self): 577 import fontTools.varLib.mvar 578 579 ds_path = self.get_test_input("SparseMasters.designspace") 580 ds = DesignSpaceDocument.fromfile(ds_path) 581 load_masters(ds) 582 583 # Trigger MVAR generation so varLib is forced to create deltas with a 584 # sparse master inbetween. 585 font_0_os2 = ds.sources[0].font["OS/2"] 586 font_0_os2.sTypoAscender = 1 587 font_0_os2.sTypoDescender = 1 588 font_0_os2.sTypoLineGap = 1 589 font_0_os2.usWinAscent = 1 590 font_0_os2.usWinDescent = 1 591 font_0_os2.sxHeight = 1 592 font_0_os2.sCapHeight = 1 593 font_0_os2.ySubscriptXSize = 1 594 font_0_os2.ySubscriptYSize = 1 595 font_0_os2.ySubscriptXOffset = 1 596 font_0_os2.ySubscriptYOffset = 1 597 font_0_os2.ySuperscriptXSize = 1 598 font_0_os2.ySuperscriptYSize = 1 599 font_0_os2.ySuperscriptXOffset = 1 600 font_0_os2.ySuperscriptYOffset = 1 601 font_0_os2.yStrikeoutSize = 1 602 font_0_os2.yStrikeoutPosition = 1 603 font_0_vhea = newTable("vhea") 604 font_0_vhea.ascent = 1 605 font_0_vhea.descent = 1 606 font_0_vhea.lineGap = 1 607 font_0_vhea.caretSlopeRise = 1 608 font_0_vhea.caretSlopeRun = 1 609 font_0_vhea.caretOffset = 1 610 ds.sources[0].font["vhea"] = font_0_vhea 611 font_0_hhea = ds.sources[0].font["hhea"] 612 font_0_hhea.caretSlopeRise = 1 613 font_0_hhea.caretSlopeRun = 1 614 font_0_hhea.caretOffset = 1 615 font_0_post = ds.sources[0].font["post"] 616 font_0_post.underlineThickness = 1 617 font_0_post.underlinePosition = 1 618 619 font_2_os2 = ds.sources[2].font["OS/2"] 620 font_2_os2.sTypoAscender = 800 621 font_2_os2.sTypoDescender = 800 622 font_2_os2.sTypoLineGap = 800 623 font_2_os2.usWinAscent = 800 624 font_2_os2.usWinDescent = 800 625 font_2_os2.sxHeight = 800 626 font_2_os2.sCapHeight = 800 627 font_2_os2.ySubscriptXSize = 800 628 font_2_os2.ySubscriptYSize = 800 629 font_2_os2.ySubscriptXOffset = 800 630 font_2_os2.ySubscriptYOffset = 800 631 font_2_os2.ySuperscriptXSize = 800 632 font_2_os2.ySuperscriptYSize = 800 633 font_2_os2.ySuperscriptXOffset = 800 634 font_2_os2.ySuperscriptYOffset = 800 635 font_2_os2.yStrikeoutSize = 800 636 font_2_os2.yStrikeoutPosition = 800 637 font_2_vhea = newTable("vhea") 638 font_2_vhea.ascent = 800 639 font_2_vhea.descent = 800 640 font_2_vhea.lineGap = 800 641 font_2_vhea.caretSlopeRise = 800 642 font_2_vhea.caretSlopeRun = 800 643 font_2_vhea.caretOffset = 800 644 ds.sources[2].font["vhea"] = font_2_vhea 645 font_2_hhea = ds.sources[2].font["hhea"] 646 font_2_hhea.caretSlopeRise = 800 647 font_2_hhea.caretSlopeRun = 800 648 font_2_hhea.caretOffset = 800 649 font_2_post = ds.sources[2].font["post"] 650 font_2_post.underlineThickness = 800 651 font_2_post.underlinePosition = 800 652 653 varfont, _, _ = build(ds) 654 mvar_tags = [vr.ValueTag for vr in varfont["MVAR"].table.ValueRecord] 655 assert all(tag in mvar_tags for tag in fontTools.varLib.mvar.MVAR_ENTRIES) 656 657 def test_varlib_build_VVAR_CFF2(self): 658 self.temp_dir() 659 660 ds_path = self.get_test_input('TestVVAR.designspace', copy=True) 661 ttx_dir = self.get_test_input("master_vvar_cff2") 662 expected_ttx_name = 'TestVVAR' 663 suffix = '.otf' 664 665 for path in self.get_file_list(ttx_dir, '.ttx', 'TestVVAR'): 666 font, savepath = self.compile_font(path, suffix, self.tempdir) 667 668 ds = DesignSpaceDocument.fromfile(ds_path) 669 for source in ds.sources: 670 source.path = os.path.join( 671 self.tempdir, os.path.basename(source.filename).replace(".ufo", suffix) 672 ) 673 ds.updatePaths() 674 675 varfont, _, _ = build(ds) 676 varfont = reload_font(varfont) 677 678 expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx') 679 tables = ["VVAR"] 680 self.expect_ttx(varfont, expected_ttx_path, tables) 681 self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix) 682 683 def test_varlib_build_BASE(self): 684 self.temp_dir() 685 686 ds_path = self.get_test_input('TestBASE.designspace', copy=True) 687 ttx_dir = self.get_test_input("master_base_test") 688 expected_ttx_name = 'TestBASE' 689 suffix = '.otf' 690 691 for path in self.get_file_list(ttx_dir, '.ttx', 'TestBASE'): 692 font, savepath = self.compile_font(path, suffix, self.tempdir) 693 694 ds = DesignSpaceDocument.fromfile(ds_path) 695 for source in ds.sources: 696 source.path = os.path.join( 697 self.tempdir, os.path.basename(source.filename).replace(".ufo", suffix) 698 ) 699 ds.updatePaths() 700 701 varfont, _, _ = build(ds) 702 varfont = reload_font(varfont) 703 704 expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx') 705 tables = ["BASE"] 706 self.expect_ttx(varfont, expected_ttx_path, tables) 707 self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix) 708 709 def test_varlib_build_single_master(self): 710 self._run_varlib_build_test( 711 designspace_name='SingleMaster', 712 font_name='TestFamily', 713 tables=['GDEF', 'HVAR', 'MVAR', 'STAT', 'fvar', 'cvar', 'gvar', 'name'], 714 expected_ttx_name='SingleMaster', 715 save_before_dump=True, 716 ) 717 718 def test_kerning_merging(self): 719 """Test the correct merging of class-based pair kerning. 720 721 Problem description at https://github.com/fonttools/fonttools/pull/1638. 722 Test font and Designspace generated by 723 https://gist.github.com/madig/183d0440c9f7d05f04bd1280b9664bd1. 724 """ 725 ds_path = self.get_test_input("KerningMerging.designspace") 726 ttx_dir = self.get_test_input("master_kerning_merging") 727 728 ds = DesignSpaceDocument.fromfile(ds_path) 729 for source in ds.sources: 730 ttx_dump = TTFont() 731 ttx_dump.importXML( 732 os.path.join( 733 ttx_dir, os.path.basename(source.filename).replace(".ttf", ".ttx") 734 ) 735 ) 736 source.font = reload_font(ttx_dump) 737 738 varfont, _, _ = build(ds) 739 varfont = reload_font(varfont) 740 741 class_kerning_tables = [ 742 t 743 for l in varfont["GPOS"].table.LookupList.Lookup 744 for t in l.SubTable 745 if t.Format == 2 746 ] 747 assert len(class_kerning_tables) == 1 748 class_kerning_table = class_kerning_tables[0] 749 750 # Test that no class kerned against class zero (containing all glyphs not 751 # classed) has a `XAdvDevice` table attached, which in the variable font 752 # context is a "VariationIndex" table and points to kerning deltas in the GDEF 753 # table. Variation deltas of any kerning class against class zero should 754 # probably never exist. 755 for class1_record in class_kerning_table.Class1Record: 756 class2_zero = class1_record.Class2Record[0] 757 assert getattr(class2_zero.Value1, "XAdvDevice", None) is None 758 759 # Assert the variable font's kerning table (without deltas) is equal to the 760 # default font's kerning table. The bug fixed in 761 # https://github.com/fonttools/fonttools/pull/1638 caused rogue kerning 762 # values to be written to the variable font. 763 assert _extract_flat_kerning(varfont, class_kerning_table) == { 764 ("A", ".notdef"): 0, 765 ("A", "A"): 0, 766 ("A", "B"): -20, 767 ("A", "C"): 0, 768 ("A", "D"): -20, 769 ("B", ".notdef"): 0, 770 ("B", "A"): 0, 771 ("B", "B"): 0, 772 ("B", "C"): 0, 773 ("B", "D"): 0, 774 } 775 776 instance_thin = instantiateVariableFont(varfont, {"wght": 100}) 777 instance_thin_kerning_table = ( 778 instance_thin["GPOS"].table.LookupList.Lookup[0].SubTable[0] 779 ) 780 assert _extract_flat_kerning(instance_thin, instance_thin_kerning_table) == { 781 ("A", ".notdef"): 0, 782 ("A", "A"): 0, 783 ("A", "B"): 0, 784 ("A", "C"): 10, 785 ("A", "D"): 0, 786 ("B", ".notdef"): 0, 787 ("B", "A"): 0, 788 ("B", "B"): 0, 789 ("B", "C"): 10, 790 ("B", "D"): 0, 791 } 792 793 instance_black = instantiateVariableFont(varfont, {"wght": 900}) 794 instance_black_kerning_table = ( 795 instance_black["GPOS"].table.LookupList.Lookup[0].SubTable[0] 796 ) 797 assert _extract_flat_kerning(instance_black, instance_black_kerning_table) == { 798 ("A", ".notdef"): 0, 799 ("A", "A"): 0, 800 ("A", "B"): 0, 801 ("A", "C"): 0, 802 ("A", "D"): 40, 803 ("B", ".notdef"): 0, 804 ("B", "A"): 0, 805 ("B", "B"): 0, 806 ("B", "C"): 0, 807 ("B", "D"): 40, 808 } 809 810 def test_designspace_fill_in_location(self): 811 ds_path = self.get_test_input("VarLibLocationTest.designspace") 812 ds = DesignSpaceDocument.fromfile(ds_path) 813 ds_loaded = load_designspace(ds) 814 815 assert ds_loaded.instances[0].location == {"weight": 0, "width": 50} 816 817 def test_varlib_build_incompatible_features(self): 818 with pytest.raises( 819 varLibErrors.ShouldBeConstant, 820 match = """ 821 822Couldn't merge the fonts, because some values were different, but should have 823been the same. This happened while performing the following operation: 824GPOS.table.FeatureList.FeatureCount 825 826The problem is likely to be in Simple Two Axis Bold: 827 828Incompatible features between masters. 829Expected: kern, mark. 830Got: kern. 831"""): 832 833 self._run_varlib_build_test( 834 designspace_name="IncompatibleFeatures", 835 font_name="IncompatibleFeatures", 836 tables=["GPOS"], 837 expected_ttx_name="IncompatibleFeatures", 838 save_before_dump=True, 839 ) 840 841 def test_varlib_build_incompatible_lookup_types(self): 842 with pytest.raises( 843 varLibErrors.MismatchedTypes, 844 match = r"MarkBasePos, instead saw PairPos" 845 ): 846 self._run_varlib_build_test( 847 designspace_name="IncompatibleLookupTypes", 848 font_name="IncompatibleLookupTypes", 849 tables=["GPOS"], 850 expected_ttx_name="IncompatibleLookupTypes", 851 save_before_dump=True, 852 ) 853 854 def test_varlib_build_incompatible_arrays(self): 855 with pytest.raises( 856 varLibErrors.ShouldBeConstant, 857 match = """ 858 859Couldn't merge the fonts, because some values were different, but should have 860been the same. This happened while performing the following operation: 861GPOS.table.ScriptList.ScriptCount 862 863The problem is likely to be in Simple Two Axis Bold: 864Expected to see .ScriptCount==1, instead saw 0""" 865 ): 866 self._run_varlib_build_test( 867 designspace_name="IncompatibleArrays", 868 font_name="IncompatibleArrays", 869 tables=["GPOS"], 870 expected_ttx_name="IncompatibleArrays", 871 save_before_dump=True, 872 ) 873 874def test_load_masters_layerName_without_required_font(): 875 ds = DesignSpaceDocument() 876 s = SourceDescriptor() 877 s.font = None 878 s.layerName = "Medium" 879 ds.addSource(s) 880 881 with pytest.raises( 882 VarLibValidationError, 883 match="specified a layer name but lacks the required TTFont object", 884 ): 885 load_masters(ds) 886 887 888def _extract_flat_kerning(font, pairpos_table): 889 extracted_kerning = {} 890 for glyph_name_1 in pairpos_table.Coverage.glyphs: 891 class_def_1 = pairpos_table.ClassDef1.classDefs.get(glyph_name_1, 0) 892 for glyph_name_2 in font.getGlyphOrder(): 893 class_def_2 = pairpos_table.ClassDef2.classDefs.get(glyph_name_2, 0) 894 kern_value = ( 895 pairpos_table.Class1Record[class_def_1] 896 .Class2Record[class_def_2] 897 .Value1.XAdvance 898 ) 899 extracted_kerning[(glyph_name_1, glyph_name_2)] = kern_value 900 return extracted_kerning 901 902 903@pytest.fixture 904def ttFont(): 905 f = TTFont() 906 f["OS/2"] = newTable("OS/2") 907 f["OS/2"].usWeightClass = 400 908 f["OS/2"].usWidthClass = 100 909 f["post"] = newTable("post") 910 f["post"].italicAngle = 0 911 return f 912 913 914class SetDefaultWeightWidthSlantTest(object): 915 @pytest.mark.parametrize( 916 "location, expected", 917 [ 918 ({"wght": 0}, 1), 919 ({"wght": 1}, 1), 920 ({"wght": 100}, 100), 921 ({"wght": 1000}, 1000), 922 ({"wght": 1001}, 1000), 923 ], 924 ) 925 def test_wght(self, ttFont, location, expected): 926 set_default_weight_width_slant(ttFont, location) 927 928 assert ttFont["OS/2"].usWeightClass == expected 929 930 @pytest.mark.parametrize( 931 "location, expected", 932 [ 933 ({"wdth": 0}, 1), 934 ({"wdth": 56}, 1), 935 ({"wdth": 57}, 2), 936 ({"wdth": 62.5}, 2), 937 ({"wdth": 75}, 3), 938 ({"wdth": 87.5}, 4), 939 ({"wdth": 100}, 5), 940 ({"wdth": 112.5}, 6), 941 ({"wdth": 125}, 7), 942 ({"wdth": 150}, 8), 943 ({"wdth": 200}, 9), 944 ({"wdth": 201}, 9), 945 ({"wdth": 1000}, 9), 946 ], 947 ) 948 def test_wdth(self, ttFont, location, expected): 949 set_default_weight_width_slant(ttFont, location) 950 951 assert ttFont["OS/2"].usWidthClass == expected 952 953 @pytest.mark.parametrize( 954 "location, expected", 955 [ 956 ({"slnt": -91}, -90), 957 ({"slnt": -90}, -90), 958 ({"slnt": 0}, 0), 959 ({"slnt": 11.5}, 11.5), 960 ({"slnt": 90}, 90), 961 ({"slnt": 91}, 90), 962 ], 963 ) 964 def test_slnt(self, ttFont, location, expected): 965 set_default_weight_width_slant(ttFont, location) 966 967 assert ttFont["post"].italicAngle == expected 968 969 def test_all(self, ttFont): 970 set_default_weight_width_slant( 971 ttFont, {"wght": 500, "wdth": 150, "slnt": -12.0} 972 ) 973 974 assert ttFont["OS/2"].usWeightClass == 500 975 assert ttFont["OS/2"].usWidthClass == 8 976 assert ttFont["post"].italicAngle == -12.0 977 978 979if __name__ == "__main__": 980 sys.exit(unittest.main()) 981