1# coding=utf-8 2 3import os 4import sys 5import pytest 6import warnings 7 8from fontTools.misc import plistlib 9from fontTools.designspaceLib import ( 10 DesignSpaceDocument, SourceDescriptor, AxisDescriptor, RuleDescriptor, 11 InstanceDescriptor, evaluateRule, processRules, posix, DesignSpaceDocumentError) 12from fontTools import ttLib 13 14def _axesAsDict(axes): 15 """ 16 Make the axis data we have available in 17 """ 18 axesDict = {} 19 for axisDescriptor in axes: 20 d = { 21 'name': axisDescriptor.name, 22 'tag': axisDescriptor.tag, 23 'minimum': axisDescriptor.minimum, 24 'maximum': axisDescriptor.maximum, 25 'default': axisDescriptor.default, 26 'map': axisDescriptor.map, 27 } 28 axesDict[axisDescriptor.name] = d 29 return axesDict 30 31 32def assert_equals_test_file(path, test_filename): 33 with open(path) as fp: 34 actual = fp.read() 35 36 test_path = os.path.join(os.path.dirname(__file__), test_filename) 37 with open(test_path) as fp: 38 expected = fp.read() 39 40 assert actual == expected 41 42 43def test_fill_document(tmpdir): 44 tmpdir = str(tmpdir) 45 testDocPath = os.path.join(tmpdir, "test.designspace") 46 masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") 47 masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") 48 instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") 49 instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") 50 doc = DesignSpaceDocument() 51 doc.rulesProcessingLast = True 52 53 # write some axes 54 a1 = AxisDescriptor() 55 a1.minimum = 0 56 a1.maximum = 1000 57 a1.default = 0 58 a1.name = "weight" 59 a1.tag = "wght" 60 # note: just to test the element language, not an actual label name recommendations. 61 a1.labelNames[u'fa-IR'] = u"قطر" 62 a1.labelNames[u'en'] = u"Wéíght" 63 doc.addAxis(a1) 64 a2 = AxisDescriptor() 65 a2.minimum = 0 66 a2.maximum = 1000 67 a2.default = 15 68 a2.name = "width" 69 a2.tag = "wdth" 70 a2.map = [(0.0, 10.0), (15.0, 20.0), (401.0, 66.0), (1000.0, 990.0)] 71 a2.hidden = True 72 a2.labelNames[u'fr'] = u"Chasse" 73 doc.addAxis(a2) 74 75 # add master 1 76 s1 = SourceDescriptor() 77 s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) 78 assert s1.font is None 79 s1.name = "master.ufo1" 80 s1.copyLib = True 81 s1.copyInfo = True 82 s1.copyFeatures = True 83 s1.location = dict(weight=0) 84 s1.familyName = "MasterFamilyName" 85 s1.styleName = "MasterStyleNameOne" 86 s1.mutedGlyphNames.append("A") 87 s1.mutedGlyphNames.append("Z") 88 doc.addSource(s1) 89 # add master 2 90 s2 = SourceDescriptor() 91 s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) 92 s2.name = "master.ufo2" 93 s2.copyLib = False 94 s2.copyInfo = False 95 s2.copyFeatures = False 96 s2.muteKerning = True 97 s2.location = dict(weight=1000) 98 s2.familyName = "MasterFamilyName" 99 s2.styleName = "MasterStyleNameTwo" 100 doc.addSource(s2) 101 # add master 3 from a different layer 102 s3 = SourceDescriptor() 103 s3.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) 104 s3.name = "master.ufo2" 105 s3.copyLib = False 106 s3.copyInfo = False 107 s3.copyFeatures = False 108 s3.muteKerning = False 109 s3.layerName = "supports" 110 s3.location = dict(weight=1000) 111 s3.familyName = "MasterFamilyName" 112 s3.styleName = "Supports" 113 doc.addSource(s3) 114 # add instance 1 115 i1 = InstanceDescriptor() 116 i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) 117 i1.familyName = "InstanceFamilyName" 118 i1.styleName = "InstanceStyleName" 119 i1.name = "instance.ufo1" 120 i1.location = dict(weight=500, spooky=666) # this adds a dimension that is not defined. 121 i1.postScriptFontName = "InstancePostscriptName" 122 i1.styleMapFamilyName = "InstanceStyleMapFamilyName" 123 i1.styleMapStyleName = "InstanceStyleMapStyleName" 124 glyphData = dict(name="arrow", mute=True, unicodes=[0x123, 0x124, 0x125]) 125 i1.glyphs['arrow'] = glyphData 126 i1.lib['com.coolDesignspaceApp.binaryData'] = plistlib.Data(b'<binary gunk>') 127 i1.lib['com.coolDesignspaceApp.specimenText'] = "Hamburgerwhatever" 128 doc.addInstance(i1) 129 # add instance 2 130 i2 = InstanceDescriptor() 131 i2.filename = os.path.relpath(instancePath2, os.path.dirname(testDocPath)) 132 i2.familyName = "InstanceFamilyName" 133 i2.styleName = "InstanceStyleName" 134 i2.name = "instance.ufo2" 135 # anisotropic location 136 i2.location = dict(weight=500, width=(400,300)) 137 i2.postScriptFontName = "InstancePostscriptName" 138 i2.styleMapFamilyName = "InstanceStyleMapFamilyName" 139 i2.styleMapStyleName = "InstanceStyleMapStyleName" 140 glyphMasters = [dict(font="master.ufo1", glyphName="BB", location=dict(width=20,weight=20)), dict(font="master.ufo2", glyphName="CC", location=dict(width=900,weight=900))] 141 glyphData = dict(name="arrow", unicodes=[101, 201, 301]) 142 glyphData['masters'] = glyphMasters 143 glyphData['note'] = "A note about this glyph" 144 glyphData['instanceLocation'] = dict(width=100, weight=120) 145 i2.glyphs['arrow'] = glyphData 146 i2.glyphs['arrow2'] = dict(mute=False) 147 doc.addInstance(i2) 148 149 doc.filename = "suggestedFileName.designspace" 150 doc.lib['com.coolDesignspaceApp.previewSize'] = 30 151 152 # write some rules 153 r1 = RuleDescriptor() 154 r1.name = "named.rule.1" 155 r1.conditionSets.append([ 156 dict(name='axisName_a', minimum=0, maximum=1), 157 dict(name='axisName_b', minimum=2, maximum=3) 158 ]) 159 r1.subs.append(("a", "a.alt")) 160 doc.addRule(r1) 161 # write the document 162 doc.write(testDocPath) 163 assert os.path.exists(testDocPath) 164 assert_equals_test_file(testDocPath, 'data/test.designspace') 165 # import it again 166 new = DesignSpaceDocument() 167 new.read(testDocPath) 168 169 assert new.default.location == {'width': 20.0, 'weight': 0.0} 170 assert new.filename == 'test.designspace' 171 assert new.lib == doc.lib 172 assert new.instances[0].lib == doc.instances[0].lib 173 174 # test roundtrip for the axis attributes and data 175 axes = {} 176 for axis in doc.axes: 177 if axis.tag not in axes: 178 axes[axis.tag] = [] 179 axes[axis.tag].append(axis.serialize()) 180 for axis in new.axes: 181 if axis.tag[0] == "_": 182 continue 183 if axis.tag not in axes: 184 axes[axis.tag] = [] 185 axes[axis.tag].append(axis.serialize()) 186 for v in axes.values(): 187 a, b = v 188 assert a == b 189 190 191def test_unicodes(tmpdir): 192 tmpdir = str(tmpdir) 193 testDocPath = os.path.join(tmpdir, "testUnicodes.designspace") 194 testDocPath2 = os.path.join(tmpdir, "testUnicodes_roundtrip.designspace") 195 masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") 196 masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") 197 instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") 198 instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") 199 doc = DesignSpaceDocument() 200 # add master 1 201 s1 = SourceDescriptor() 202 s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) 203 s1.name = "master.ufo1" 204 s1.copyInfo = True 205 s1.location = dict(weight=0) 206 doc.addSource(s1) 207 # add master 2 208 s2 = SourceDescriptor() 209 s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) 210 s2.name = "master.ufo2" 211 s2.location = dict(weight=1000) 212 doc.addSource(s2) 213 # add instance 1 214 i1 = InstanceDescriptor() 215 i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) 216 i1.name = "instance.ufo1" 217 i1.location = dict(weight=500) 218 glyphData = dict(name="arrow", mute=True, unicodes=[100, 200, 300]) 219 i1.glyphs['arrow'] = glyphData 220 doc.addInstance(i1) 221 # now we have sources and instances, but no axes yet. 222 doc.axes = [] # clear the axes 223 # write some axes 224 a1 = AxisDescriptor() 225 a1.minimum = 0 226 a1.maximum = 1000 227 a1.default = 0 228 a1.name = "weight" 229 a1.tag = "wght" 230 doc.addAxis(a1) 231 # write the document 232 doc.write(testDocPath) 233 assert os.path.exists(testDocPath) 234 # import it again 235 new = DesignSpaceDocument() 236 new.read(testDocPath) 237 new.write(testDocPath2) 238 # compare the file contents 239 with open(testDocPath, 'r', encoding='utf-8') as f1: 240 t1 = f1.read() 241 with open(testDocPath2, 'r', encoding='utf-8') as f2: 242 t2 = f2.read() 243 assert t1 == t2 244 # check the unicode values read from the document 245 assert new.instances[0].glyphs['arrow']['unicodes'] == [100,200,300] 246 247 248def test_localisedNames(tmpdir): 249 tmpdir = str(tmpdir) 250 testDocPath = os.path.join(tmpdir, "testLocalisedNames.designspace") 251 testDocPath2 = os.path.join(tmpdir, "testLocalisedNames_roundtrip.designspace") 252 masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") 253 masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") 254 instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") 255 instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") 256 doc = DesignSpaceDocument() 257 # add master 1 258 s1 = SourceDescriptor() 259 s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) 260 s1.name = "master.ufo1" 261 s1.copyInfo = True 262 s1.location = dict(weight=0) 263 doc.addSource(s1) 264 # add master 2 265 s2 = SourceDescriptor() 266 s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) 267 s2.name = "master.ufo2" 268 s2.location = dict(weight=1000) 269 doc.addSource(s2) 270 # add instance 1 271 i1 = InstanceDescriptor() 272 i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) 273 i1.familyName = "Montserrat" 274 i1.styleName = "SemiBold" 275 i1.styleMapFamilyName = "Montserrat SemiBold" 276 i1.styleMapStyleName = "Regular" 277 i1.setFamilyName("Montserrat", "fr") 278 i1.setFamilyName(u"モンセラート", "ja") 279 i1.setStyleName("Demigras", "fr") 280 i1.setStyleName(u"半ば", "ja") 281 i1.setStyleMapStyleName(u"Standard", "de") 282 i1.setStyleMapFamilyName("Montserrat Halbfett", "de") 283 i1.setStyleMapFamilyName(u"モンセラート SemiBold", "ja") 284 i1.name = "instance.ufo1" 285 i1.location = dict(weight=500, spooky=666) # this adds a dimension that is not defined. 286 i1.postScriptFontName = "InstancePostscriptName" 287 glyphData = dict(name="arrow", mute=True, unicodes=[0x123]) 288 i1.glyphs['arrow'] = glyphData 289 doc.addInstance(i1) 290 # now we have sources and instances, but no axes yet. 291 doc.axes = [] # clear the axes 292 # write some axes 293 a1 = AxisDescriptor() 294 a1.minimum = 0 295 a1.maximum = 1000 296 a1.default = 0 297 a1.name = "weight" 298 a1.tag = "wght" 299 # note: just to test the element language, not an actual label name recommendations. 300 a1.labelNames[u'fa-IR'] = u"قطر" 301 a1.labelNames[u'en'] = u"Wéíght" 302 doc.addAxis(a1) 303 a2 = AxisDescriptor() 304 a2.minimum = 0 305 a2.maximum = 1000 306 a2.default = 0 307 a2.name = "width" 308 a2.tag = "wdth" 309 a2.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] 310 a2.labelNames[u'fr'] = u"Poids" 311 doc.addAxis(a2) 312 # add an axis that is not part of any location to see if that works 313 a3 = AxisDescriptor() 314 a3.minimum = 333 315 a3.maximum = 666 316 a3.default = 444 317 a3.name = "spooky" 318 a3.tag = "spok" 319 a3.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] 320 #doc.addAxis(a3) # uncomment this line to test the effects of default axes values 321 # write some rules 322 r1 = RuleDescriptor() 323 r1.name = "named.rule.1" 324 r1.conditionSets.append([ 325 dict(name='weight', minimum=200, maximum=500), 326 dict(name='width', minimum=0, maximum=150) 327 ]) 328 r1.subs.append(("a", "a.alt")) 329 doc.addRule(r1) 330 # write the document 331 doc.write(testDocPath) 332 assert os.path.exists(testDocPath) 333 # import it again 334 new = DesignSpaceDocument() 335 new.read(testDocPath) 336 new.write(testDocPath2) 337 with open(testDocPath, 'r', encoding='utf-8') as f1: 338 t1 = f1.read() 339 with open(testDocPath2, 'r', encoding='utf-8') as f2: 340 t2 = f2.read() 341 assert t1 == t2 342 343 344def test_handleNoAxes(tmpdir): 345 tmpdir = str(tmpdir) 346 # test what happens if the designspacedocument has no axes element. 347 testDocPath = os.path.join(tmpdir, "testNoAxes_source.designspace") 348 testDocPath2 = os.path.join(tmpdir, "testNoAxes_recontructed.designspace") 349 masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") 350 masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") 351 instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") 352 instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") 353 354 # Case 1: No axes element in the document, but there are sources and instances 355 doc = DesignSpaceDocument() 356 357 for name, value in [('One', 1),('Two', 2),('Three', 3)]: 358 a = AxisDescriptor() 359 a.minimum = 0 360 a.maximum = 1000 361 a.default = 0 362 a.name = "axisName%s" % (name) 363 a.tag = "ax_%d" % (value) 364 doc.addAxis(a) 365 366 # add master 1 367 s1 = SourceDescriptor() 368 s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) 369 s1.name = "master.ufo1" 370 s1.copyLib = True 371 s1.copyInfo = True 372 s1.copyFeatures = True 373 s1.location = dict(axisNameOne=-1000, axisNameTwo=0, axisNameThree=1000) 374 s1.familyName = "MasterFamilyName" 375 s1.styleName = "MasterStyleNameOne" 376 doc.addSource(s1) 377 378 # add master 2 379 s2 = SourceDescriptor() 380 s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) 381 s2.name = "master.ufo1" 382 s2.copyLib = False 383 s2.copyInfo = False 384 s2.copyFeatures = False 385 s2.location = dict(axisNameOne=1000, axisNameTwo=1000, axisNameThree=0) 386 s2.familyName = "MasterFamilyName" 387 s2.styleName = "MasterStyleNameTwo" 388 doc.addSource(s2) 389 390 # add instance 1 391 i1 = InstanceDescriptor() 392 i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) 393 i1.familyName = "InstanceFamilyName" 394 i1.styleName = "InstanceStyleName" 395 i1.name = "instance.ufo1" 396 i1.location = dict(axisNameOne=(-1000,500), axisNameTwo=100) 397 i1.postScriptFontName = "InstancePostscriptName" 398 i1.styleMapFamilyName = "InstanceStyleMapFamilyName" 399 i1.styleMapStyleName = "InstanceStyleMapStyleName" 400 doc.addInstance(i1) 401 402 doc.write(testDocPath) 403 verify = DesignSpaceDocument() 404 verify.read(testDocPath) 405 verify.write(testDocPath2) 406 407def test_pathNameResolve(tmpdir): 408 tmpdir = str(tmpdir) 409 # test how descriptor.path and descriptor.filename are resolved 410 testDocPath1 = os.path.join(tmpdir, "testPathName_case1.designspace") 411 testDocPath2 = os.path.join(tmpdir, "testPathName_case2.designspace") 412 testDocPath3 = os.path.join(tmpdir, "testPathName_case3.designspace") 413 testDocPath4 = os.path.join(tmpdir, "testPathName_case4.designspace") 414 testDocPath5 = os.path.join(tmpdir, "testPathName_case5.designspace") 415 testDocPath6 = os.path.join(tmpdir, "testPathName_case6.designspace") 416 masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") 417 masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") 418 instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") 419 instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") 420 421 a1 = AxisDescriptor() 422 a1.tag = "TAGA" 423 a1.name = "axisName_a" 424 a1.minimum = 0 425 a1.maximum = 1000 426 a1.default = 0 427 428 # Case 1: filename and path are both empty. Nothing to calculate, nothing to put in the file. 429 doc = DesignSpaceDocument() 430 doc.addAxis(a1) 431 s = SourceDescriptor() 432 s.filename = None 433 s.path = None 434 s.copyInfo = True 435 s.location = dict(weight=0) 436 s.familyName = "MasterFamilyName" 437 s.styleName = "MasterStyleNameOne" 438 doc.addSource(s) 439 doc.write(testDocPath1) 440 verify = DesignSpaceDocument() 441 verify.read(testDocPath1) 442 assert verify.sources[0].filename == None 443 assert verify.sources[0].path == None 444 445 # Case 2: filename is empty, path points somewhere: calculate a new filename. 446 doc = DesignSpaceDocument() 447 doc.addAxis(a1) 448 s = SourceDescriptor() 449 s.filename = None 450 s.path = masterPath1 451 s.copyInfo = True 452 s.location = dict(weight=0) 453 s.familyName = "MasterFamilyName" 454 s.styleName = "MasterStyleNameOne" 455 doc.addSource(s) 456 doc.write(testDocPath2) 457 verify = DesignSpaceDocument() 458 verify.read(testDocPath2) 459 assert verify.sources[0].filename == "masters/masterTest1.ufo" 460 assert verify.sources[0].path == posix(masterPath1) 461 462 # Case 3: the filename is set, the path is None. 463 doc = DesignSpaceDocument() 464 doc.addAxis(a1) 465 s = SourceDescriptor() 466 s.filename = "../somewhere/over/the/rainbow.ufo" 467 s.path = None 468 s.copyInfo = True 469 s.location = dict(weight=0) 470 s.familyName = "MasterFamilyName" 471 s.styleName = "MasterStyleNameOne" 472 doc.addSource(s) 473 doc.write(testDocPath3) 474 verify = DesignSpaceDocument() 475 verify.read(testDocPath3) 476 assert verify.sources[0].filename == "../somewhere/over/the/rainbow.ufo" 477 # make the absolute path for filename so we can see if it matches the path 478 p = os.path.abspath(os.path.join(os.path.dirname(testDocPath3), verify.sources[0].filename)) 479 assert verify.sources[0].path == posix(p) 480 481 # Case 4: the filename points to one file, the path points to another. The path takes precedence. 482 doc = DesignSpaceDocument() 483 doc.addAxis(a1) 484 s = SourceDescriptor() 485 s.filename = "../somewhere/over/the/rainbow.ufo" 486 s.path = masterPath1 487 s.copyInfo = True 488 s.location = dict(weight=0) 489 s.familyName = "MasterFamilyName" 490 s.styleName = "MasterStyleNameOne" 491 doc.addSource(s) 492 doc.write(testDocPath4) 493 verify = DesignSpaceDocument() 494 verify.read(testDocPath4) 495 assert verify.sources[0].filename == "masters/masterTest1.ufo" 496 497 # Case 5: the filename is None, path has a value, update the filename 498 doc = DesignSpaceDocument() 499 doc.addAxis(a1) 500 s = SourceDescriptor() 501 s.filename = None 502 s.path = masterPath1 503 s.copyInfo = True 504 s.location = dict(weight=0) 505 s.familyName = "MasterFamilyName" 506 s.styleName = "MasterStyleNameOne" 507 doc.addSource(s) 508 doc.write(testDocPath5) # so that the document has a path 509 doc.updateFilenameFromPath() 510 assert doc.sources[0].filename == "masters/masterTest1.ufo" 511 512 # Case 6: the filename has a value, path has a value, update the filenames with force 513 doc = DesignSpaceDocument() 514 doc.addAxis(a1) 515 s = SourceDescriptor() 516 s.filename = "../somewhere/over/the/rainbow.ufo" 517 s.path = masterPath1 518 s.copyInfo = True 519 s.location = dict(weight=0) 520 s.familyName = "MasterFamilyName" 521 s.styleName = "MasterStyleNameOne" 522 doc.write(testDocPath5) # so that the document has a path 523 doc.addSource(s) 524 assert doc.sources[0].filename == "../somewhere/over/the/rainbow.ufo" 525 doc.updateFilenameFromPath(force=True) 526 assert doc.sources[0].filename == "masters/masterTest1.ufo" 527 528 529def test_normalise1(): 530 # normalisation of anisotropic locations, clipping 531 doc = DesignSpaceDocument() 532 # write some axes 533 a1 = AxisDescriptor() 534 a1.minimum = -1000 535 a1.maximum = 1000 536 a1.default = 0 537 a1.name = "axisName_a" 538 a1.tag = "TAGA" 539 doc.addAxis(a1) 540 assert doc.normalizeLocation(dict(axisName_a=0)) == {'axisName_a': 0.0} 541 assert doc.normalizeLocation(dict(axisName_a=1000)) == {'axisName_a': 1.0} 542 # clipping beyond max values: 543 assert doc.normalizeLocation(dict(axisName_a=1001)) == {'axisName_a': 1.0} 544 assert doc.normalizeLocation(dict(axisName_a=500)) == {'axisName_a': 0.5} 545 assert doc.normalizeLocation(dict(axisName_a=-1000)) == {'axisName_a': -1.0} 546 assert doc.normalizeLocation(dict(axisName_a=-1001)) == {'axisName_a': -1.0} 547 # anisotropic coordinates normalise to isotropic 548 assert doc.normalizeLocation(dict(axisName_a=(1000, -1000))) == {'axisName_a': 1.0} 549 doc.normalize() 550 r = [] 551 for axis in doc.axes: 552 r.append((axis.name, axis.minimum, axis.default, axis.maximum)) 553 r.sort() 554 assert r == [('axisName_a', -1.0, 0.0, 1.0)] 555 556def test_normalise2(): 557 # normalisation with minimum > 0 558 doc = DesignSpaceDocument() 559 # write some axes 560 a2 = AxisDescriptor() 561 a2.minimum = 100 562 a2.maximum = 1000 563 a2.default = 100 564 a2.name = "axisName_b" 565 doc.addAxis(a2) 566 assert doc.normalizeLocation(dict(axisName_b=0)) == {'axisName_b': 0.0} 567 assert doc.normalizeLocation(dict(axisName_b=1000)) == {'axisName_b': 1.0} 568 # clipping beyond max values: 569 assert doc.normalizeLocation(dict(axisName_b=1001)) == {'axisName_b': 1.0} 570 assert doc.normalizeLocation(dict(axisName_b=500)) == {'axisName_b': 0.4444444444444444} 571 assert doc.normalizeLocation(dict(axisName_b=-1000)) == {'axisName_b': 0.0} 572 assert doc.normalizeLocation(dict(axisName_b=-1001)) == {'axisName_b': 0.0} 573 # anisotropic coordinates normalise to isotropic 574 assert doc.normalizeLocation(dict(axisName_b=(1000,-1000))) == {'axisName_b': 1.0} 575 assert doc.normalizeLocation(dict(axisName_b=1001)) == {'axisName_b': 1.0} 576 doc.normalize() 577 r = [] 578 for axis in doc.axes: 579 r.append((axis.name, axis.minimum, axis.default, axis.maximum)) 580 r.sort() 581 assert r == [('axisName_b', 0.0, 0.0, 1.0)] 582 583def test_normalise3(): 584 # normalisation of negative values, with default == maximum 585 doc = DesignSpaceDocument() 586 # write some axes 587 a3 = AxisDescriptor() 588 a3.minimum = -1000 589 a3.maximum = 0 590 a3.default = 0 591 a3.name = "ccc" 592 doc.addAxis(a3) 593 assert doc.normalizeLocation(dict(ccc=0)) == {'ccc': 0.0} 594 assert doc.normalizeLocation(dict(ccc=1)) == {'ccc': 0.0} 595 assert doc.normalizeLocation(dict(ccc=-1000)) == {'ccc': -1.0} 596 assert doc.normalizeLocation(dict(ccc=-1001)) == {'ccc': -1.0} 597 doc.normalize() 598 r = [] 599 for axis in doc.axes: 600 r.append((axis.name, axis.minimum, axis.default, axis.maximum)) 601 r.sort() 602 assert r == [('ccc', -1.0, 0.0, 0.0)] 603 604def test_normalise4(): 605 # normalisation with a map 606 doc = DesignSpaceDocument() 607 # write some axes 608 a4 = AxisDescriptor() 609 a4.minimum = 0 610 a4.maximum = 1000 611 a4.default = 0 612 a4.name = "ddd" 613 a4.map = [(0,100), (300, 500), (600, 500), (1000,900)] 614 doc.addAxis(a4) 615 doc.normalize() 616 r = [] 617 for axis in doc.axes: 618 r.append((axis.name, axis.map)) 619 r.sort() 620 assert r == [('ddd', [(0, 0.0), (300, 0.5), (600, 0.5), (1000, 1.0)])] 621 622def test_axisMapping(): 623 # note: because designspance lib does not do any actual 624 # processing of the mapping data, we can only check if there data is there. 625 doc = DesignSpaceDocument() 626 # write some axes 627 a4 = AxisDescriptor() 628 a4.minimum = 0 629 a4.maximum = 1000 630 a4.default = 0 631 a4.name = "ddd" 632 a4.map = [(0,100), (300, 500), (600, 500), (1000,900)] 633 doc.addAxis(a4) 634 doc.normalize() 635 r = [] 636 for axis in doc.axes: 637 r.append((axis.name, axis.map)) 638 r.sort() 639 assert r == [('ddd', [(0, 0.0), (300, 0.5), (600, 0.5), (1000, 1.0)])] 640 641def test_rulesConditions(tmpdir): 642 # tests of rules, conditionsets and conditions 643 r1 = RuleDescriptor() 644 r1.name = "named.rule.1" 645 r1.conditionSets.append([ 646 dict(name='axisName_a', minimum=0, maximum=1000), 647 dict(name='axisName_b', minimum=0, maximum=3000) 648 ]) 649 r1.subs.append(("a", "a.alt")) 650 651 assert evaluateRule(r1, dict(axisName_a = 500, axisName_b = 0)) == True 652 assert evaluateRule(r1, dict(axisName_a = 0, axisName_b = 0)) == True 653 assert evaluateRule(r1, dict(axisName_a = 1000, axisName_b = 0)) == True 654 assert evaluateRule(r1, dict(axisName_a = 1000, axisName_b = -100)) == False 655 assert evaluateRule(r1, dict(axisName_a = 1000.0001, axisName_b = 0)) == False 656 assert evaluateRule(r1, dict(axisName_a = -0.0001, axisName_b = 0)) == False 657 assert evaluateRule(r1, dict(axisName_a = -100, axisName_b = 0)) == False 658 assert processRules([r1], dict(axisName_a = 500, axisName_b = 0), ["a", "b", "c"]) == ['a.alt', 'b', 'c'] 659 assert processRules([r1], dict(axisName_a = 500, axisName_b = 0), ["a.alt", "b", "c"]) == ['a.alt', 'b', 'c'] 660 assert processRules([r1], dict(axisName_a = 2000, axisName_b = 0), ["a", "b", "c"]) == ['a', 'b', 'c'] 661 662 # rule with only a maximum 663 r2 = RuleDescriptor() 664 r2.name = "named.rule.2" 665 r2.conditionSets.append([dict(name='axisName_a', maximum=500)]) 666 r2.subs.append(("b", "b.alt")) 667 668 assert evaluateRule(r2, dict(axisName_a = 0)) == True 669 assert evaluateRule(r2, dict(axisName_a = -500)) == True 670 assert evaluateRule(r2, dict(axisName_a = 1000)) == False 671 672 # rule with only a minimum 673 r3 = RuleDescriptor() 674 r3.name = "named.rule.3" 675 r3.conditionSets.append([dict(name='axisName_a', minimum=500)]) 676 r3.subs.append(("c", "c.alt")) 677 678 assert evaluateRule(r3, dict(axisName_a = 0)) == False 679 assert evaluateRule(r3, dict(axisName_a = 1000)) == True 680 assert evaluateRule(r3, dict(axisName_a = 1000)) == True 681 682 # rule with only a minimum, maximum in separate conditions 683 r4 = RuleDescriptor() 684 r4.name = "named.rule.4" 685 r4.conditionSets.append([ 686 dict(name='axisName_a', minimum=500), 687 dict(name='axisName_b', maximum=500) 688 ]) 689 r4.subs.append(("c", "c.alt")) 690 691 assert evaluateRule(r4, dict(axisName_a = 1000, axisName_b = 0)) == True 692 assert evaluateRule(r4, dict(axisName_a = 0, axisName_b = 0)) == False 693 assert evaluateRule(r4, dict(axisName_a = 1000, axisName_b = 1000)) == False 694 695def test_rulesDocument(tmpdir): 696 # tests of rules in a document, roundtripping. 697 tmpdir = str(tmpdir) 698 testDocPath = os.path.join(tmpdir, "testRules.designspace") 699 testDocPath2 = os.path.join(tmpdir, "testRules_roundtrip.designspace") 700 doc = DesignSpaceDocument() 701 doc.rulesProcessingLast = True 702 a1 = AxisDescriptor() 703 a1.minimum = 0 704 a1.maximum = 1000 705 a1.default = 0 706 a1.name = "axisName_a" 707 a1.tag = "TAGA" 708 b1 = AxisDescriptor() 709 b1.minimum = 2000 710 b1.maximum = 3000 711 b1.default = 2000 712 b1.name = "axisName_b" 713 b1.tag = "TAGB" 714 doc.addAxis(a1) 715 doc.addAxis(b1) 716 r1 = RuleDescriptor() 717 r1.name = "named.rule.1" 718 r1.conditionSets.append([ 719 dict(name='axisName_a', minimum=0, maximum=1000), 720 dict(name='axisName_b', minimum=0, maximum=3000) 721 ]) 722 r1.subs.append(("a", "a.alt")) 723 # rule with minium and maximum 724 doc.addRule(r1) 725 assert len(doc.rules) == 1 726 assert len(doc.rules[0].conditionSets) == 1 727 assert len(doc.rules[0].conditionSets[0]) == 2 728 assert _axesAsDict(doc.axes) == {'axisName_a': {'map': [], 'name': 'axisName_a', 'default': 0, 'minimum': 0, 'maximum': 1000, 'tag': 'TAGA'}, 'axisName_b': {'map': [], 'name': 'axisName_b', 'default': 2000, 'minimum': 2000, 'maximum': 3000, 'tag': 'TAGB'}} 729 assert doc.rules[0].conditionSets == [[ 730 {'minimum': 0, 'maximum': 1000, 'name': 'axisName_a'}, 731 {'minimum': 0, 'maximum': 3000, 'name': 'axisName_b'}]] 732 assert doc.rules[0].subs == [('a', 'a.alt')] 733 doc.normalize() 734 assert doc.rules[0].name == 'named.rule.1' 735 assert doc.rules[0].conditionSets == [[ 736 {'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_a'}, 737 {'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_b'}]] 738 # still one conditionset 739 assert len(doc.rules[0].conditionSets) == 1 740 doc.write(testDocPath) 741 # add a stray conditionset 742 _addUnwrappedCondition(testDocPath) 743 doc2 = DesignSpaceDocument() 744 doc2.read(testDocPath) 745 assert doc2.rulesProcessingLast 746 assert len(doc2.axes) == 2 747 assert len(doc2.rules) == 1 748 assert len(doc2.rules[0].conditionSets) == 2 749 doc2.write(testDocPath2) 750 # verify these results 751 # make sure the stray condition is now neatly wrapped in a conditionset. 752 doc3 = DesignSpaceDocument() 753 doc3.read(testDocPath2) 754 assert len(doc3.rules) == 1 755 assert len(doc3.rules[0].conditionSets) == 2 756 757def _addUnwrappedCondition(path): 758 # only for testing, so we can make an invalid designspace file 759 # older designspace files may have conditions that are not wrapped in a conditionset 760 # These can be read into a new conditionset. 761 with open(path, 'r', encoding='utf-8') as f: 762 d = f.read() 763 print(d) 764 d = d.replace('<rule name="named.rule.1">', '<rule name="named.rule.1">\n\t<condition maximum="22" minimum="33" name="axisName_a" />') 765 with open(path, 'w', encoding='utf-8') as f: 766 f.write(d) 767 768def test_documentLib(tmpdir): 769 # roundtrip test of the document lib with some nested data 770 tmpdir = str(tmpdir) 771 testDocPath1 = os.path.join(tmpdir, "testDocumentLibTest.designspace") 772 doc = DesignSpaceDocument() 773 a1 = AxisDescriptor() 774 a1.tag = "TAGA" 775 a1.name = "axisName_a" 776 a1.minimum = 0 777 a1.maximum = 1000 778 a1.default = 0 779 doc.addAxis(a1) 780 dummyData = dict(a=123, b=u"äbc", c=[1,2,3], d={'a':123}) 781 dummyKey = "org.fontTools.designspaceLib" 782 doc.lib = {dummyKey: dummyData} 783 doc.write(testDocPath1) 784 new = DesignSpaceDocument() 785 new.read(testDocPath1) 786 assert dummyKey in new.lib 787 assert new.lib[dummyKey] == dummyData 788 789 790def test_updatePaths(tmpdir): 791 doc = DesignSpaceDocument() 792 doc.path = str(tmpdir / "foo" / "bar" / "MyDesignspace.designspace") 793 794 s1 = SourceDescriptor() 795 doc.addSource(s1) 796 797 doc.updatePaths() 798 799 # expect no changes 800 assert s1.path is None 801 assert s1.filename is None 802 803 name1 = "../masters/Source1.ufo" 804 path1 = posix(str(tmpdir / "foo" / "masters" / "Source1.ufo")) 805 806 s1.path = path1 807 s1.filename = None 808 809 doc.updatePaths() 810 811 assert s1.path == path1 812 assert s1.filename == name1 # empty filename updated 813 814 name2 = "../masters/Source2.ufo" 815 s1.filename = name2 816 817 doc.updatePaths() 818 819 # conflicting filename discarded, path always gets precedence 820 assert s1.path == path1 821 assert s1.filename == "../masters/Source1.ufo" 822 823 s1.path = None 824 s1.filename = name2 825 826 doc.updatePaths() 827 828 # expect no changes 829 assert s1.path is None 830 assert s1.filename == name2 831 832 833@pytest.mark.skipif(sys.version_info[:2] < (3, 6), reason="pathlib is only tested on 3.6 and up") 834def test_read_with_path_object(): 835 import pathlib 836 source = (pathlib.Path(__file__) / "../data/test.designspace").resolve() 837 assert source.exists() 838 doc = DesignSpaceDocument() 839 doc.read(source) 840 841 842@pytest.mark.skipif(sys.version_info[:2] < (3, 6), reason="pathlib is only tested on 3.6 and up") 843def test_with_with_path_object(tmpdir): 844 import pathlib 845 tmpdir = str(tmpdir) 846 dest = pathlib.Path(tmpdir) / "test.designspace" 847 doc = DesignSpaceDocument() 848 doc.write(dest) 849 assert dest.exists() 850 851 852def test_findDefault_axis_mapping(): 853 designspace_string = """\ 854<?xml version='1.0' encoding='UTF-8'?> 855<designspace format="4.0"> 856 <axes> 857 <axis tag="wght" name="Weight" minimum="100" maximum="800" default="400"> 858 <map input="100" output="20"/> 859 <map input="300" output="40"/> 860 <map input="400" output="80"/> 861 <map input="700" output="126"/> 862 <map input="800" output="170"/> 863 </axis> 864 <axis tag="ital" name="Italic" minimum="0" maximum="1" default="1"/> 865 </axes> 866 <sources> 867 <source filename="Font-Light.ufo"> 868 <location> 869 <dimension name="Weight" xvalue="20"/> 870 <dimension name="Italic" xvalue="0"/> 871 </location> 872 </source> 873 <source filename="Font-Regular.ufo"> 874 <location> 875 <dimension name="Weight" xvalue="80"/> 876 <dimension name="Italic" xvalue="0"/> 877 </location> 878 </source> 879 <source filename="Font-Bold.ufo"> 880 <location> 881 <dimension name="Weight" xvalue="170"/> 882 <dimension name="Italic" xvalue="0"/> 883 </location> 884 </source> 885 <source filename="Font-LightItalic.ufo"> 886 <location> 887 <dimension name="Weight" xvalue="20"/> 888 <dimension name="Italic" xvalue="1"/> 889 </location> 890 </source> 891 <source filename="Font-Italic.ufo"> 892 <location> 893 <dimension name="Weight" xvalue="80"/> 894 <dimension name="Italic" xvalue="1"/> 895 </location> 896 </source> 897 <source filename="Font-BoldItalic.ufo"> 898 <location> 899 <dimension name="Weight" xvalue="170"/> 900 <dimension name="Italic" xvalue="1"/> 901 </location> 902 </source> 903 </sources> 904</designspace> 905 """ 906 designspace = DesignSpaceDocument.fromstring(designspace_string) 907 assert designspace.findDefault().filename == "Font-Italic.ufo" 908 909 designspace.axes[1].default = 0 910 911 assert designspace.findDefault().filename == "Font-Regular.ufo" 912 913 914def test_loadSourceFonts(): 915 916 def opener(path): 917 font = ttLib.TTFont() 918 font.importXML(path) 919 return font 920 921 # this designspace file contains .TTX source paths 922 path = os.path.join( 923 os.path.dirname(os.path.dirname(__file__)), 924 "varLib", 925 "data", 926 "SparseMasters.designspace" 927 ) 928 designspace = DesignSpaceDocument.fromfile(path) 929 930 # force two source descriptors to have the same path 931 designspace.sources[1].path = designspace.sources[0].path 932 933 fonts = designspace.loadSourceFonts(opener) 934 935 assert len(fonts) == 3 936 assert all(isinstance(font, ttLib.TTFont) for font in fonts) 937 assert fonts[0] is fonts[1] # same path, identical font object 938 939 fonts2 = designspace.loadSourceFonts(opener) 940 941 for font1, font2 in zip(fonts, fonts2): 942 assert font1 is font2 943 944 945def test_loadSourceFonts_no_required_path(): 946 designspace = DesignSpaceDocument() 947 designspace.sources.append(SourceDescriptor()) 948 949 with pytest.raises(DesignSpaceDocumentError, match="no 'path' attribute"): 950 designspace.loadSourceFonts(lambda p: p) 951 952 953def test_addAxisDescriptor(): 954 ds = DesignSpaceDocument() 955 956 axis = ds.addAxisDescriptor( 957 name="Weight", tag="wght", minimum=100, default=400, maximum=900 958 ) 959 960 assert ds.axes[0] is axis 961 assert isinstance(axis, AxisDescriptor) 962 assert axis.name == "Weight" 963 assert axis.tag == "wght" 964 assert axis.minimum == 100 965 assert axis.default == 400 966 assert axis.maximum == 900 967 968 969def test_addSourceDescriptor(): 970 ds = DesignSpaceDocument() 971 972 source = ds.addSourceDescriptor(name="TestSource", location={"Weight": 400}) 973 974 assert ds.sources[0] is source 975 assert isinstance(source, SourceDescriptor) 976 assert source.name == "TestSource" 977 assert source.location == {"Weight": 400} 978 979 980def test_addInstanceDescriptor(): 981 ds = DesignSpaceDocument() 982 983 instance = ds.addInstanceDescriptor( 984 name="TestInstance", 985 location={"Weight": 400}, 986 styleName="Regular", 987 styleMapStyleName="regular", 988 ) 989 990 assert ds.instances[0] is instance 991 assert isinstance(instance, InstanceDescriptor) 992 assert instance.name == "TestInstance" 993 assert instance.location == {"Weight": 400} 994 assert instance.styleName == "Regular" 995 assert instance.styleMapStyleName == "regular" 996 997 998def test_addRuleDescriptor(tmp_path): 999 ds = DesignSpaceDocument() 1000 1001 rule = ds.addRuleDescriptor( 1002 name="TestRule", 1003 conditionSets=[ 1004 [ 1005 dict(name="Weight", minimum=100, maximum=200), 1006 dict(name="Weight", minimum=700, maximum=900), 1007 ] 1008 ], 1009 subs=[("a", "a.alt")], 1010 ) 1011 1012 assert ds.rules[0] is rule 1013 assert isinstance(rule, RuleDescriptor) 1014 assert rule.name == "TestRule" 1015 assert rule.conditionSets == [ 1016 [ 1017 dict(name="Weight", minimum=100, maximum=200), 1018 dict(name="Weight", minimum=700, maximum=900), 1019 ] 1020 ] 1021 assert rule.subs == [("a", "a.alt")] 1022 1023 # Test it doesn't crash. 1024 ds.write(tmp_path / "test.designspace") 1025