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