# coding=utf-8 import os from pathlib import Path import re import shutil import pytest from fontTools import ttLib from fontTools.designspaceLib import ( AxisDescriptor, AxisMappingDescriptor, AxisLabelDescriptor, DesignSpaceDocument, DesignSpaceDocumentError, DiscreteAxisDescriptor, InstanceDescriptor, RuleDescriptor, SourceDescriptor, evaluateRule, posix, processRules, ) from fontTools.designspaceLib.types import Range from fontTools.misc import plistlib from .fixtures import datadir def _axesAsDict(axes): """ Make the axis data we have available in """ axesDict = {} for axisDescriptor in axes: d = { "name": axisDescriptor.name, "tag": axisDescriptor.tag, "minimum": axisDescriptor.minimum, "maximum": axisDescriptor.maximum, "default": axisDescriptor.default, "map": axisDescriptor.map, } axesDict[axisDescriptor.name] = d return axesDict def assert_equals_test_file(path, test_filename): with open(path, encoding="utf-8") as fp: actual = fp.read() test_path = os.path.join(os.path.dirname(__file__), test_filename) with open(test_path, encoding="utf-8") as fp: expected = fp.read() expected = re.sub(r"", "", expected) expected = re.sub(r"\s*\n+", "\n", expected) assert actual == expected def test_fill_document(tmpdir): tmpdir = str(tmpdir) testDocPath = os.path.join(tmpdir, "test_v4.designspace") testDocPath5 = os.path.join(tmpdir, "test_v5.designspace") masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") doc = DesignSpaceDocument() doc.rulesProcessingLast = True # write some axes a1 = AxisDescriptor() a1.minimum = 0 a1.maximum = 1000 a1.default = 0 a1.name = "weight" a1.tag = "wght" # note: just to test the element language, not an actual label name recommendations. a1.labelNames["fa-IR"] = "قطر" a1.labelNames["en"] = "Wéíght" doc.addAxis(a1) a2 = AxisDescriptor() a2.minimum = 0 a2.maximum = 1000 a2.default = 15 a2.name = "width" a2.tag = "wdth" a2.map = [(0.0, 10.0), (15.0, 20.0), (401.0, 66.0), (1000.0, 990.0)] a2.hidden = True a2.labelNames["fr"] = "Chasse" doc.addAxis(a2) # add master 1 s1 = SourceDescriptor() s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) assert s1.font is None s1.name = "master.ufo1" s1.copyLib = True s1.copyInfo = True s1.copyFeatures = True s1.location = dict(weight=0) s1.familyName = "MasterFamilyName" s1.styleName = "MasterStyleNameOne" s1.mutedGlyphNames.append("A") s1.mutedGlyphNames.append("Z") doc.addSource(s1) # add master 2 s2 = SourceDescriptor() s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) s2.name = "master.ufo2" s2.copyLib = False s2.copyInfo = False s2.copyFeatures = False s2.muteKerning = True s2.location = dict(weight=1000) s2.familyName = "MasterFamilyName" s2.styleName = "MasterStyleNameTwo" doc.addSource(s2) # add master 3 from a different layer s3 = SourceDescriptor() s3.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) s3.name = "master.ufo2" s3.copyLib = False s3.copyInfo = False s3.copyFeatures = False s3.muteKerning = False s3.layerName = "supports" s3.location = dict(weight=1000) s3.familyName = "MasterFamilyName" s3.styleName = "Supports" doc.addSource(s3) # add instance 1 i1 = InstanceDescriptor() i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) i1.familyName = "InstanceFamilyName" i1.styleName = "InstanceStyleName" i1.name = "instance.ufo1" i1.location = dict( weight=500, spooky=666 ) # this adds a dimension that is not defined. i1.postScriptFontName = "InstancePostscriptName" i1.styleMapFamilyName = "InstanceStyleMapFamilyName" i1.styleMapStyleName = "InstanceStyleMapStyleName" i1.localisedStyleName = dict(fr="Demigras", ja="半ば") i1.localisedFamilyName = dict(fr="Montserrat", ja="モンセラート") i1.localisedStyleMapStyleName = dict(de="Standard") i1.localisedStyleMapFamilyName = dict( de="Montserrat Halbfett", ja="モンセラート SemiBold" ) glyphData = dict(name="arrow", mute=True, unicodes=[0x123, 0x124, 0x125]) i1.glyphs["arrow"] = glyphData i1.lib["com.coolDesignspaceApp.binaryData"] = plistlib.Data(b"") i1.lib["com.coolDesignspaceApp.specimenText"] = "Hamburgerwhatever" doc.addInstance(i1) # add instance 2 i2 = InstanceDescriptor() i2.filename = os.path.relpath(instancePath2, os.path.dirname(testDocPath)) i2.familyName = "InstanceFamilyName" i2.styleName = "InstanceStyleName" i2.name = "instance.ufo2" # anisotropic location i2.location = dict(weight=500, width=(400, 300)) i2.postScriptFontName = "InstancePostscriptName" i2.styleMapFamilyName = "InstanceStyleMapFamilyName" i2.styleMapStyleName = "InstanceStyleMapStyleName" 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)), ] glyphData = dict(name="arrow", unicodes=[101, 201, 301]) glyphData["masters"] = glyphMasters glyphData["note"] = "A note about this glyph" glyphData["instanceLocation"] = dict(width=100, weight=120) i2.glyphs["arrow"] = glyphData i2.glyphs["arrow2"] = dict(mute=False) doc.addInstance(i2) doc.filename = "suggestedFileName.designspace" doc.lib["com.coolDesignspaceApp.previewSize"] = 30 # write some rules r1 = RuleDescriptor() r1.name = "named.rule.1" r1.conditionSets.append( [ dict(name="axisName_a", minimum=0, maximum=1), dict(name="axisName_b", minimum=2, maximum=3), ] ) r1.subs.append(("a", "a.alt")) doc.addRule(r1) # write the document; without an explicit format it will be 5.0 by default doc.write(testDocPath5) assert os.path.exists(testDocPath5) assert_equals_test_file(testDocPath5, "data/test_v5_original.designspace") # write again with an explicit format = 4.1 doc.formatVersion = "4.1" doc.write(testDocPath) assert os.path.exists(testDocPath) assert_equals_test_file(testDocPath, "data/test_v4_original.designspace") # import it again new = DesignSpaceDocument() new.read(testDocPath) assert new.default.location == {"width": 20.0, "weight": 0.0} assert new.filename == "test_v4.designspace" assert new.lib == doc.lib assert new.instances[0].lib == doc.instances[0].lib # test roundtrip for the axis attributes and data axes = {} for axis in doc.axes: if axis.tag not in axes: axes[axis.tag] = [] axes[axis.tag].append(axis.serialize()) for axis in new.axes: if axis.tag[0] == "_": continue if axis.tag not in axes: axes[axis.tag] = [] axes[axis.tag].append(axis.serialize()) for v in axes.values(): a, b = v assert a == b def test_unicodes(tmpdir): tmpdir = str(tmpdir) testDocPath = os.path.join(tmpdir, "testUnicodes.designspace") testDocPath2 = os.path.join(tmpdir, "testUnicodes_roundtrip.designspace") masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") doc = DesignSpaceDocument() doc.formatVersion = "4.1" # This test about instance glyphs is deprecated in v5 # add master 1 s1 = SourceDescriptor() s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) s1.name = "master.ufo1" s1.copyInfo = True s1.location = dict(weight=0) doc.addSource(s1) # add master 2 s2 = SourceDescriptor() s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) s2.name = "master.ufo2" s2.location = dict(weight=1000) doc.addSource(s2) # add instance 1 i1 = InstanceDescriptor() i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) i1.name = "instance.ufo1" i1.location = dict(weight=500) glyphData = dict(name="arrow", mute=True, unicodes=[100, 200, 300]) i1.glyphs["arrow"] = glyphData doc.addInstance(i1) # now we have sources and instances, but no axes yet. doc.axes = [] # clear the axes # write some axes a1 = AxisDescriptor() a1.minimum = 0 a1.maximum = 1000 a1.default = 0 a1.name = "weight" a1.tag = "wght" doc.addAxis(a1) # write the document doc.write(testDocPath) assert os.path.exists(testDocPath) # import it again new = DesignSpaceDocument() new.read(testDocPath) new.write(testDocPath2) # compare the file contents with open(testDocPath, "r", encoding="utf-8") as f1: t1 = f1.read() with open(testDocPath2, "r", encoding="utf-8") as f2: t2 = f2.read() assert t1 == t2 # check the unicode values read from the document assert new.instances[0].glyphs["arrow"]["unicodes"] == [100, 200, 300] def test_localisedNames(tmpdir): tmpdir = str(tmpdir) testDocPath = os.path.join(tmpdir, "testLocalisedNames.designspace") testDocPath2 = os.path.join(tmpdir, "testLocalisedNames_roundtrip.designspace") masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") doc = DesignSpaceDocument() # add master 1 s1 = SourceDescriptor() s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) s1.name = "master.ufo1" s1.copyInfo = True s1.location = dict(weight=0) doc.addSource(s1) # add master 2 s2 = SourceDescriptor() s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) s2.name = "master.ufo2" s2.location = dict(weight=1000) doc.addSource(s2) # add instance 1 i1 = InstanceDescriptor() i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) i1.familyName = "Montserrat" i1.styleName = "SemiBold" i1.styleMapFamilyName = "Montserrat SemiBold" i1.styleMapStyleName = "Regular" i1.setFamilyName("Montserrat", "fr") i1.setFamilyName("モンセラート", "ja") i1.setStyleName("Demigras", "fr") i1.setStyleName("半ば", "ja") i1.setStyleMapStyleName("Standard", "de") i1.setStyleMapFamilyName("Montserrat Halbfett", "de") i1.setStyleMapFamilyName("モンセラート SemiBold", "ja") i1.name = "instance.ufo1" i1.location = dict( weight=500, spooky=666 ) # this adds a dimension that is not defined. i1.postScriptFontName = "InstancePostscriptName" glyphData = dict(name="arrow", mute=True, unicodes=[0x123]) i1.glyphs["arrow"] = glyphData doc.addInstance(i1) # now we have sources and instances, but no axes yet. doc.axes = [] # clear the axes # write some axes a1 = AxisDescriptor() a1.minimum = 0 a1.maximum = 1000 a1.default = 0 a1.name = "weight" a1.tag = "wght" # note: just to test the element language, not an actual label name recommendations. a1.labelNames["fa-IR"] = "قطر" a1.labelNames["en"] = "Wéíght" doc.addAxis(a1) a2 = AxisDescriptor() a2.minimum = 0 a2.maximum = 1000 a2.default = 0 a2.name = "width" a2.tag = "wdth" a2.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] a2.labelNames["fr"] = "Poids" doc.addAxis(a2) # add an axis that is not part of any location to see if that works a3 = AxisDescriptor() a3.minimum = 333 a3.maximum = 666 a3.default = 444 a3.name = "spooky" a3.tag = "spok" a3.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] # doc.addAxis(a3) # uncomment this line to test the effects of default axes values # write some rules r1 = RuleDescriptor() r1.name = "named.rule.1" r1.conditionSets.append( [ dict(name="weight", minimum=200, maximum=500), dict(name="width", minimum=0, maximum=150), ] ) r1.subs.append(("a", "a.alt")) doc.addRule(r1) # write the document doc.write(testDocPath) assert os.path.exists(testDocPath) # import it again new = DesignSpaceDocument() new.read(testDocPath) new.write(testDocPath2) with open(testDocPath, "r", encoding="utf-8") as f1: t1 = f1.read() with open(testDocPath2, "r", encoding="utf-8") as f2: t2 = f2.read() assert t1 == t2 def test_handleNoAxes(tmpdir): tmpdir = str(tmpdir) # test what happens if the designspacedocument has no axes element. testDocPath = os.path.join(tmpdir, "testNoAxes_source.designspace") testDocPath2 = os.path.join(tmpdir, "testNoAxes_recontructed.designspace") masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") # Case 1: No axes element in the document, but there are sources and instances doc = DesignSpaceDocument() for name, value in [("One", 1), ("Two", 2), ("Three", 3)]: a = AxisDescriptor() a.minimum = 0 a.maximum = 1000 a.default = 0 a.name = "axisName%s" % (name) a.tag = "ax_%d" % (value) doc.addAxis(a) # add master 1 s1 = SourceDescriptor() s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) s1.name = "master.ufo1" s1.copyLib = True s1.copyInfo = True s1.copyFeatures = True s1.location = dict(axisNameOne=-1000, axisNameTwo=0, axisNameThree=1000) s1.familyName = "MasterFamilyName" s1.styleName = "MasterStyleNameOne" doc.addSource(s1) # add master 2 s2 = SourceDescriptor() s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) s2.name = "master.ufo1" s2.copyLib = False s2.copyInfo = False s2.copyFeatures = False s2.location = dict(axisNameOne=1000, axisNameTwo=1000, axisNameThree=0) s2.familyName = "MasterFamilyName" s2.styleName = "MasterStyleNameTwo" doc.addSource(s2) # add instance 1 i1 = InstanceDescriptor() i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) i1.familyName = "InstanceFamilyName" i1.styleName = "InstanceStyleName" i1.name = "instance.ufo1" i1.location = dict(axisNameOne=(-1000, 500), axisNameTwo=100) i1.postScriptFontName = "InstancePostscriptName" i1.styleMapFamilyName = "InstanceStyleMapFamilyName" i1.styleMapStyleName = "InstanceStyleMapStyleName" doc.addInstance(i1) doc.write(testDocPath) verify = DesignSpaceDocument() verify.read(testDocPath) verify.write(testDocPath2) def test_pathNameResolve(tmpdir): tmpdir = str(tmpdir) # test how descriptor.path and descriptor.filename are resolved testDocPath1 = os.path.join(tmpdir, "testPathName_case1.designspace") testDocPath2 = os.path.join(tmpdir, "testPathName_case2.designspace") testDocPath3 = os.path.join(tmpdir, "testPathName_case3.designspace") testDocPath4 = os.path.join(tmpdir, "testPathName_case4.designspace") testDocPath5 = os.path.join(tmpdir, "testPathName_case5.designspace") testDocPath6 = os.path.join(tmpdir, "testPathName_case6.designspace") masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") a1 = AxisDescriptor() a1.tag = "TAGA" a1.name = "axisName_a" a1.minimum = 0 a1.maximum = 1000 a1.default = 0 # Case 1: filename and path are both empty. Nothing to calculate, nothing to put in the file. doc = DesignSpaceDocument() doc.addAxis(a1) s = SourceDescriptor() s.filename = None s.path = None s.copyInfo = True s.location = dict(weight=0) s.familyName = "MasterFamilyName" s.styleName = "MasterStyleNameOne" doc.addSource(s) doc.write(testDocPath1) verify = DesignSpaceDocument() verify.read(testDocPath1) assert verify.sources[0].filename == None assert verify.sources[0].path == None # Case 2: filename is empty, path points somewhere: calculate a new filename. doc = DesignSpaceDocument() doc.addAxis(a1) s = SourceDescriptor() s.filename = None s.path = masterPath1 s.copyInfo = True s.location = dict(weight=0) s.familyName = "MasterFamilyName" s.styleName = "MasterStyleNameOne" doc.addSource(s) doc.write(testDocPath2) verify = DesignSpaceDocument() verify.read(testDocPath2) assert verify.sources[0].filename == "masters/masterTest1.ufo" assert verify.sources[0].path == posix(masterPath1) # Case 3: the filename is set, the path is None. doc = DesignSpaceDocument() doc.addAxis(a1) s = SourceDescriptor() s.filename = "../somewhere/over/the/rainbow.ufo" s.path = None s.copyInfo = True s.location = dict(weight=0) s.familyName = "MasterFamilyName" s.styleName = "MasterStyleNameOne" doc.addSource(s) doc.write(testDocPath3) verify = DesignSpaceDocument() verify.read(testDocPath3) assert verify.sources[0].filename == "../somewhere/over/the/rainbow.ufo" # make the absolute path for filename so we can see if it matches the path p = os.path.abspath( os.path.join(os.path.dirname(testDocPath3), verify.sources[0].filename) ) assert verify.sources[0].path == posix(p) # Case 4: the filename points to one file, the path points to another. The path takes precedence. doc = DesignSpaceDocument() doc.addAxis(a1) s = SourceDescriptor() s.filename = "../somewhere/over/the/rainbow.ufo" s.path = masterPath1 s.copyInfo = True s.location = dict(weight=0) s.familyName = "MasterFamilyName" s.styleName = "MasterStyleNameOne" doc.addSource(s) doc.write(testDocPath4) verify = DesignSpaceDocument() verify.read(testDocPath4) assert verify.sources[0].filename == "masters/masterTest1.ufo" # Case 5: the filename is None, path has a value, update the filename doc = DesignSpaceDocument() doc.addAxis(a1) s = SourceDescriptor() s.filename = None s.path = masterPath1 s.copyInfo = True s.location = dict(weight=0) s.familyName = "MasterFamilyName" s.styleName = "MasterStyleNameOne" doc.addSource(s) doc.write(testDocPath5) # so that the document has a path doc.updateFilenameFromPath() assert doc.sources[0].filename == "masters/masterTest1.ufo" # Case 6: the filename has a value, path has a value, update the filenames with force doc = DesignSpaceDocument() doc.addAxis(a1) s = SourceDescriptor() s.filename = "../somewhere/over/the/rainbow.ufo" s.path = masterPath1 s.copyInfo = True s.location = dict(weight=0) s.familyName = "MasterFamilyName" s.styleName = "MasterStyleNameOne" doc.write(testDocPath5) # so that the document has a path doc.addSource(s) assert doc.sources[0].filename == "../somewhere/over/the/rainbow.ufo" doc.updateFilenameFromPath(force=True) assert doc.sources[0].filename == "masters/masterTest1.ufo" def test_normalise1(): # normalisation of anisotropic locations, clipping doc = DesignSpaceDocument() # write some axes a1 = AxisDescriptor() a1.minimum = -1000 a1.maximum = 1000 a1.default = 0 a1.name = "axisName_a" a1.tag = "TAGA" doc.addAxis(a1) assert doc.normalizeLocation(dict(axisName_a=0)) == {"axisName_a": 0.0} assert doc.normalizeLocation(dict(axisName_a=1000)) == {"axisName_a": 1.0} # clipping beyond max values: assert doc.normalizeLocation(dict(axisName_a=1001)) == {"axisName_a": 1.0} assert doc.normalizeLocation(dict(axisName_a=500)) == {"axisName_a": 0.5} assert doc.normalizeLocation(dict(axisName_a=-1000)) == {"axisName_a": -1.0} assert doc.normalizeLocation(dict(axisName_a=-1001)) == {"axisName_a": -1.0} # anisotropic coordinates normalise to isotropic assert doc.normalizeLocation(dict(axisName_a=(1000, -1000))) == {"axisName_a": 1.0} doc.normalize() r = [] for axis in doc.axes: r.append((axis.name, axis.minimum, axis.default, axis.maximum)) r.sort() assert r == [("axisName_a", -1.0, 0.0, 1.0)] def test_normalise2(): # normalisation with minimum > 0 doc = DesignSpaceDocument() # write some axes a2 = AxisDescriptor() a2.minimum = 100 a2.maximum = 1000 a2.default = 100 a2.name = "axisName_b" doc.addAxis(a2) assert doc.normalizeLocation(dict(axisName_b=0)) == {"axisName_b": 0.0} assert doc.normalizeLocation(dict(axisName_b=1000)) == {"axisName_b": 1.0} # clipping beyond max values: assert doc.normalizeLocation(dict(axisName_b=1001)) == {"axisName_b": 1.0} assert doc.normalizeLocation(dict(axisName_b=500)) == { "axisName_b": 0.4444444444444444 } assert doc.normalizeLocation(dict(axisName_b=-1000)) == {"axisName_b": 0.0} assert doc.normalizeLocation(dict(axisName_b=-1001)) == {"axisName_b": 0.0} # anisotropic coordinates normalise to isotropic assert doc.normalizeLocation(dict(axisName_b=(1000, -1000))) == {"axisName_b": 1.0} assert doc.normalizeLocation(dict(axisName_b=1001)) == {"axisName_b": 1.0} doc.normalize() r = [] for axis in doc.axes: r.append((axis.name, axis.minimum, axis.default, axis.maximum)) r.sort() assert r == [("axisName_b", 0.0, 0.0, 1.0)] def test_normalise3(): # normalisation of negative values, with default == maximum doc = DesignSpaceDocument() # write some axes a3 = AxisDescriptor() a3.minimum = -1000 a3.maximum = 0 a3.default = 0 a3.name = "ccc" doc.addAxis(a3) assert doc.normalizeLocation(dict(ccc=0)) == {"ccc": 0.0} assert doc.normalizeLocation(dict(ccc=1)) == {"ccc": 0.0} assert doc.normalizeLocation(dict(ccc=-1000)) == {"ccc": -1.0} assert doc.normalizeLocation(dict(ccc=-1001)) == {"ccc": -1.0} doc.normalize() r = [] for axis in doc.axes: r.append((axis.name, axis.minimum, axis.default, axis.maximum)) r.sort() assert r == [("ccc", -1.0, 0.0, 0.0)] def test_normalise4(): # normalisation with a map doc = DesignSpaceDocument() # write some axes a4 = AxisDescriptor() a4.minimum = 0 a4.maximum = 1000 a4.default = 0 a4.name = "ddd" a4.map = [(0, 100), (300, 500), (600, 500), (1000, 900)] doc.addAxis(a4) doc.normalize() r = [] for axis in doc.axes: r.append((axis.name, axis.map)) r.sort() assert r == [("ddd", [(0, 0.0), (300, 0.5), (600, 0.5), (1000, 1.0)])] def test_axisMapping(): # note: because designspance lib does not do any actual # processing of the mapping data, we can only check if there data is there. doc = DesignSpaceDocument() # write some axes a4 = AxisDescriptor() a4.minimum = 0 a4.maximum = 1000 a4.default = 0 a4.name = "ddd" a4.map = [(0, 100), (300, 500), (600, 500), (1000, 900)] doc.addAxis(a4) doc.normalize() r = [] for axis in doc.axes: r.append((axis.name, axis.map)) r.sort() assert r == [("ddd", [(0, 0.0), (300, 0.5), (600, 0.5), (1000, 1.0)])] def test_axisMappingsRoundtrip(tmpdir): # tests of axisMappings in a document, roundtripping. tmpdir = str(tmpdir) srcDocPath = (Path(__file__) / "../data/test_avar2.designspace").resolve() testDocPath = os.path.join(tmpdir, "test_avar2.designspace") shutil.copy(srcDocPath, testDocPath) testDocPath2 = os.path.join(tmpdir, "test_avar2_roundtrip.designspace") doc = DesignSpaceDocument() doc.read(testDocPath) assert doc.axisMappings assert len(doc.axisMappings) == 1 assert doc.axisMappings[0].inputLocation == {"Justify": -100.0, "Width": 100.0} # This is a bit of a hack, but it's the only way to make sure # that the save works on Windows if the tempdir and the data # dir are on different drives. for descriptor in doc.sources + doc.instances: descriptor.path = None doc.write(testDocPath2) # verify these results doc2 = DesignSpaceDocument() doc2.read(testDocPath2) assert [mapping.inputLocation for mapping in doc.axisMappings] == [ mapping.inputLocation for mapping in doc2.axisMappings ] assert [mapping.outputLocation for mapping in doc.axisMappings] == [ mapping.outputLocation for mapping in doc2.axisMappings ] assert [mapping.description for mapping in doc.axisMappings] == [ mapping.description for mapping in doc2.axisMappings ] assert [mapping.groupDescription for mapping in doc.axisMappings] == [ mapping.groupDescription for mapping in doc2.axisMappings ] def test_rulesConditions(tmpdir): # tests of rules, conditionsets and conditions r1 = RuleDescriptor() r1.name = "named.rule.1" r1.conditionSets.append( [ dict(name="axisName_a", minimum=0, maximum=1000), dict(name="axisName_b", minimum=0, maximum=3000), ] ) r1.subs.append(("a", "a.alt")) assert evaluateRule(r1, dict(axisName_a=500, axisName_b=0)) == True assert evaluateRule(r1, dict(axisName_a=0, axisName_b=0)) == True assert evaluateRule(r1, dict(axisName_a=1000, axisName_b=0)) == True assert evaluateRule(r1, dict(axisName_a=1000, axisName_b=-100)) == False assert evaluateRule(r1, dict(axisName_a=1000.0001, axisName_b=0)) == False assert evaluateRule(r1, dict(axisName_a=-0.0001, axisName_b=0)) == False assert evaluateRule(r1, dict(axisName_a=-100, axisName_b=0)) == False assert processRules([r1], dict(axisName_a=500, axisName_b=0), ["a", "b", "c"]) == [ "a.alt", "b", "c", ] assert processRules( [r1], dict(axisName_a=500, axisName_b=0), ["a.alt", "b", "c"] ) == ["a.alt", "b", "c"] assert processRules([r1], dict(axisName_a=2000, axisName_b=0), ["a", "b", "c"]) == [ "a", "b", "c", ] # rule with only a maximum r2 = RuleDescriptor() r2.name = "named.rule.2" r2.conditionSets.append([dict(name="axisName_a", maximum=500)]) r2.subs.append(("b", "b.alt")) assert evaluateRule(r2, dict(axisName_a=0)) == True assert evaluateRule(r2, dict(axisName_a=-500)) == True assert evaluateRule(r2, dict(axisName_a=1000)) == False # rule with only a minimum r3 = RuleDescriptor() r3.name = "named.rule.3" r3.conditionSets.append([dict(name="axisName_a", minimum=500)]) r3.subs.append(("c", "c.alt")) assert evaluateRule(r3, dict(axisName_a=0)) == False assert evaluateRule(r3, dict(axisName_a=1000)) == True assert evaluateRule(r3, dict(axisName_a=1000)) == True # rule with only a minimum, maximum in separate conditions r4 = RuleDescriptor() r4.name = "named.rule.4" r4.conditionSets.append( [dict(name="axisName_a", minimum=500), dict(name="axisName_b", maximum=500)] ) r4.subs.append(("c", "c.alt")) assert evaluateRule(r4, dict(axisName_a=1000, axisName_b=0)) == True assert evaluateRule(r4, dict(axisName_a=0, axisName_b=0)) == False assert evaluateRule(r4, dict(axisName_a=1000, axisName_b=1000)) == False def test_rulesDocument(tmpdir): # tests of rules in a document, roundtripping. tmpdir = str(tmpdir) testDocPath = os.path.join(tmpdir, "testRules.designspace") testDocPath2 = os.path.join(tmpdir, "testRules_roundtrip.designspace") doc = DesignSpaceDocument() doc.rulesProcessingLast = True a1 = AxisDescriptor() a1.minimum = 0 a1.maximum = 1000 a1.default = 0 a1.name = "axisName_a" a1.tag = "TAGA" b1 = AxisDescriptor() b1.minimum = 2000 b1.maximum = 3000 b1.default = 2000 b1.name = "axisName_b" b1.tag = "TAGB" doc.addAxis(a1) doc.addAxis(b1) r1 = RuleDescriptor() r1.name = "named.rule.1" r1.conditionSets.append( [ dict(name="axisName_a", minimum=0, maximum=1000), dict(name="axisName_b", minimum=0, maximum=3000), ] ) r1.subs.append(("a", "a.alt")) # rule with minium and maximum doc.addRule(r1) assert len(doc.rules) == 1 assert len(doc.rules[0].conditionSets) == 1 assert len(doc.rules[0].conditionSets[0]) == 2 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", }, } assert doc.rules[0].conditionSets == [ [ {"minimum": 0, "maximum": 1000, "name": "axisName_a"}, {"minimum": 0, "maximum": 3000, "name": "axisName_b"}, ] ] assert doc.rules[0].subs == [("a", "a.alt")] doc.normalize() assert doc.rules[0].name == "named.rule.1" assert doc.rules[0].conditionSets == [ [ {"minimum": 0.0, "maximum": 1.0, "name": "axisName_a"}, {"minimum": 0.0, "maximum": 1.0, "name": "axisName_b"}, ] ] # still one conditionset assert len(doc.rules[0].conditionSets) == 1 doc.write(testDocPath) # add a stray conditionset _addUnwrappedCondition(testDocPath) doc2 = DesignSpaceDocument() doc2.read(testDocPath) assert doc2.rulesProcessingLast assert len(doc2.axes) == 2 assert len(doc2.rules) == 1 assert len(doc2.rules[0].conditionSets) == 2 doc2.write(testDocPath2) # verify these results # make sure the stray condition is now neatly wrapped in a conditionset. doc3 = DesignSpaceDocument() doc3.read(testDocPath2) assert len(doc3.rules) == 1 assert len(doc3.rules[0].conditionSets) == 2 def _addUnwrappedCondition(path): # only for testing, so we can make an invalid designspace file # older designspace files may have conditions that are not wrapped in a conditionset # These can be read into a new conditionset. with open(path, "r", encoding="utf-8") as f: d = f.read() print(d) d = d.replace( '', '\n\t', ) with open(path, "w", encoding="utf-8") as f: f.write(d) def test_documentLib(tmpdir): # roundtrip test of the document lib with some nested data tmpdir = str(tmpdir) testDocPath1 = os.path.join(tmpdir, "testDocumentLibTest.designspace") doc = DesignSpaceDocument() a1 = AxisDescriptor() a1.tag = "TAGA" a1.name = "axisName_a" a1.minimum = 0 a1.maximum = 1000 a1.default = 0 doc.addAxis(a1) dummyData = dict(a=123, b="äbc", c=[1, 2, 3], d={"a": 123}) dummyKey = "org.fontTools.designspaceLib" doc.lib = {dummyKey: dummyData} doc.write(testDocPath1) new = DesignSpaceDocument() new.read(testDocPath1) assert dummyKey in new.lib assert new.lib[dummyKey] == dummyData def test_updatePaths(tmpdir): doc = DesignSpaceDocument() doc.path = str(tmpdir / "foo" / "bar" / "MyDesignspace.designspace") s1 = SourceDescriptor() doc.addSource(s1) doc.updatePaths() # expect no changes assert s1.path is None assert s1.filename is None name1 = "../masters/Source1.ufo" path1 = posix(str(tmpdir / "foo" / "masters" / "Source1.ufo")) s1.path = path1 s1.filename = None doc.updatePaths() assert s1.path == path1 assert s1.filename == name1 # empty filename updated name2 = "../masters/Source2.ufo" s1.filename = name2 doc.updatePaths() # conflicting filename discarded, path always gets precedence assert s1.path == path1 assert s1.filename == "../masters/Source1.ufo" s1.path = None s1.filename = name2 doc.updatePaths() # expect no changes assert s1.path is None assert s1.filename == name2 def test_read_with_path_object(): source = (Path(__file__) / "../data/test_v4_original.designspace").resolve() assert source.exists() doc = DesignSpaceDocument() doc.read(source) def test_with_with_path_object(tmpdir): tmpdir = str(tmpdir) dest = Path(tmpdir) / "test_v4_original.designspace" doc = DesignSpaceDocument() doc.write(dest) assert dest.exists() def test_findDefault_axis_mapping(): designspace_string = """\ """ designspace = DesignSpaceDocument.fromstring(designspace_string) assert designspace.findDefault().filename == "Font-Italic.ufo" designspace.axes[1].default = 0 assert designspace.findDefault().filename == "Font-Regular.ufo" def test_loadSourceFonts(): def opener(path): font = ttLib.TTFont() font.importXML(path) return font # this designspace file contains .TTX source paths path = os.path.join( os.path.dirname(os.path.dirname(__file__)), "varLib", "data", "SparseMasters.designspace", ) designspace = DesignSpaceDocument.fromfile(path) # force two source descriptors to have the same path designspace.sources[1].path = designspace.sources[0].path fonts = designspace.loadSourceFonts(opener) assert len(fonts) == 3 assert all(isinstance(font, ttLib.TTFont) for font in fonts) assert fonts[0] is fonts[1] # same path, identical font object fonts2 = designspace.loadSourceFonts(opener) for font1, font2 in zip(fonts, fonts2): assert font1 is font2 def test_loadSourceFonts_no_required_path(): designspace = DesignSpaceDocument() designspace.sources.append(SourceDescriptor()) with pytest.raises(DesignSpaceDocumentError, match="no 'path' attribute"): designspace.loadSourceFonts(lambda p: p) def test_addAxisDescriptor(): ds = DesignSpaceDocument() axis = ds.addAxisDescriptor( name="Weight", tag="wght", minimum=100, default=400, maximum=900 ) assert ds.axes[0] is axis assert isinstance(axis, AxisDescriptor) assert axis.name == "Weight" assert axis.tag == "wght" assert axis.minimum == 100 assert axis.default == 400 assert axis.maximum == 900 def test_addAxisDescriptor(): ds = DesignSpaceDocument() mapping = ds.addAxisMappingDescriptor( inputLocation={"weight": 900, "width": 150}, outputLocation={"weight": 870} ) assert ds.axisMappings[0] is mapping assert isinstance(mapping, AxisMappingDescriptor) assert mapping.inputLocation == {"weight": 900, "width": 150} assert mapping.outputLocation == {"weight": 870} def test_addSourceDescriptor(): ds = DesignSpaceDocument() source = ds.addSourceDescriptor(name="TestSource", location={"Weight": 400}) assert ds.sources[0] is source assert isinstance(source, SourceDescriptor) assert source.name == "TestSource" assert source.location == {"Weight": 400} def test_addInstanceDescriptor(): ds = DesignSpaceDocument() instance = ds.addInstanceDescriptor( name="TestInstance", location={"Weight": 400}, styleName="Regular", styleMapStyleName="regular", ) assert ds.instances[0] is instance assert isinstance(instance, InstanceDescriptor) assert instance.name == "TestInstance" assert instance.location == {"Weight": 400} assert instance.styleName == "Regular" assert instance.styleMapStyleName == "regular" def test_addRuleDescriptor(tmp_path): ds = DesignSpaceDocument() rule = ds.addRuleDescriptor( name="TestRule", conditionSets=[ [ dict(name="Weight", minimum=100, maximum=200), dict(name="Weight", minimum=700, maximum=900), ] ], subs=[("a", "a.alt")], ) assert ds.rules[0] is rule assert isinstance(rule, RuleDescriptor) assert rule.name == "TestRule" assert rule.conditionSets == [ [ dict(name="Weight", minimum=100, maximum=200), dict(name="Weight", minimum=700, maximum=900), ] ] assert rule.subs == [("a", "a.alt")] # Test it doesn't crash. ds.write(tmp_path / "test.designspace") def test_deepcopyExceptFonts(): ds = DesignSpaceDocument() ds.addSourceDescriptor(font=object()) ds.addSourceDescriptor(font=object()) ds_copy = ds.deepcopyExceptFonts() assert ds.tostring() == ds_copy.tostring() assert ds.sources[0].font is ds_copy.sources[0].font assert ds.sources[1].font is ds_copy.sources[1].font def test_Range_post_init(): # test min and max are sorted and default is clamped to either min/max r = Range(minimum=2, maximum=-1, default=-2) assert r.minimum == -1 assert r.maximum == 2 assert r.default == -1 def test_get_axes(datadir: Path) -> None: ds = DesignSpaceDocument.fromfile(datadir / "test_v5.designspace") assert ds.getAxis("Width") is ds.getAxisByTag("wdth") assert ds.getAxis("Italic") is ds.getAxisByTag("ital")