import re import shutil from pathlib import Path import pytest from fontTools.designspaceLib import ( AxisDescriptor, AxisLabelDescriptor, DesignSpaceDocument, DiscreteAxisDescriptor, InstanceDescriptor, LocationLabelDescriptor, RangeAxisSubsetDescriptor, SourceDescriptor, ValueAxisSubsetDescriptor, VariableFontDescriptor, posix, ) from .fixtures import datadir def assert_descriptors_equal(actual, expected): assert len(actual) == len(expected) for a, e in zip(actual, expected): assert a.asdict() == e.asdict() def test_read_v5_document_simple(datadir): doc = DesignSpaceDocument.fromfile(datadir / "test_v5.designspace") assert_descriptors_equal( doc.axes, [ AxisDescriptor( tag="wght", name="Weight", minimum=200, maximum=1000, default=200, labelNames={"en": "Wéíght", "fa-IR": "قطر"}, map=[ (200, 0), (300, 100), (400, 368), (600, 600), (700, 824), (900, 1000), ], axisOrdering=None, axisLabels=[ AxisLabelDescriptor( name="Extra Light", userMinimum=200, userValue=200, userMaximum=250, labelNames={"de": "Extraleicht", "fr": "Extra léger"}, ), AxisLabelDescriptor( name="Light", userMinimum=250, userValue=300, userMaximum=350 ), AxisLabelDescriptor( name="Regular", userMinimum=350, userValue=400, userMaximum=450, elidable=True, ), AxisLabelDescriptor( name="Semi Bold", userMinimum=450, userValue=600, userMaximum=650, ), AxisLabelDescriptor( name="Bold", userMinimum=650, userValue=700, userMaximum=850 ), AxisLabelDescriptor( name="Black", userMinimum=850, userValue=900, userMaximum=900 ), AxisLabelDescriptor( name="Regular", userValue=400, linkedUserValue=700, elidable=True, ), AxisLabelDescriptor( name="Bold", userValue=700, linkedUserValue=400 ), ], ), AxisDescriptor( tag="wdth", name="Width", minimum=50, maximum=150, default=100, hidden=True, labelNames={"fr": "Chasse"}, map=[(50, 10), (100, 20), (125, 66), (150, 990)], axisOrdering=1, axisLabels=[ AxisLabelDescriptor(name="Condensed", userValue=50), AxisLabelDescriptor( name="Normal", elidable=True, olderSibling=True, userValue=100 ), AxisLabelDescriptor(name="Wide", userValue=125), AxisLabelDescriptor( name="Extra Wide", userValue=150, userMinimum=150 ), ], ), DiscreteAxisDescriptor( tag="ital", name="Italic", values=[0, 1], default=0, axisOrdering=None, axisLabels=[ AxisLabelDescriptor( name="Roman", userValue=0, elidable=True, linkedUserValue=1 ), AxisLabelDescriptor(name="Italic", userValue=1), ], ), ], ) assert_descriptors_equal( doc.locationLabels, [ LocationLabelDescriptor( name="Some Style", labelNames={"fr": "Un Style"}, userLocation={"Weight": 300, "Width": 50, "Italic": 0}, ), LocationLabelDescriptor( name="Other", userLocation={"Weight": 700, "Width": 100, "Italic": 1} ), ], ) assert_descriptors_equal( doc.sources, [ SourceDescriptor( filename="masters/masterTest1.ufo", path=posix(str((datadir / "masters/masterTest1.ufo").resolve())), name="master.ufo1", layerName=None, location={"Italic": 0.0, "Weight": 0.0, "Width": 20.0}, copyLib=True, copyInfo=True, copyGroups=False, copyFeatures=True, muteKerning=False, muteInfo=False, mutedGlyphNames=["A", "Z"], familyName="MasterFamilyName", styleName="MasterStyleNameOne", localisedFamilyName={"fr": "Montserrat", "ja": "モンセラート"}, ), SourceDescriptor( filename="masters/masterTest2.ufo", path=posix(str((datadir / "masters/masterTest2.ufo").resolve())), name="master.ufo2", layerName=None, location={"Italic": 0.0, "Weight": 1000.0, "Width": 20.0}, copyLib=False, copyInfo=False, copyGroups=False, copyFeatures=False, muteKerning=True, muteInfo=False, mutedGlyphNames=[], familyName="MasterFamilyName", styleName="MasterStyleNameTwo", localisedFamilyName={}, ), SourceDescriptor( filename="masters/masterTest2.ufo", path=posix(str((datadir / "masters/masterTest2.ufo").resolve())), name="master.ufo2", layerName="supports", location={"Italic": 0.0, "Weight": 1000.0, "Width": 20.0}, copyLib=False, copyInfo=False, copyGroups=False, copyFeatures=False, muteKerning=False, muteInfo=False, mutedGlyphNames=[], familyName="MasterFamilyName", styleName="Supports", localisedFamilyName={}, ), SourceDescriptor( filename="masters/masterTest2.ufo", path=posix(str((datadir / "masters/masterTest2.ufo").resolve())), name="master.ufo3", layerName=None, location={"Italic": 1.0, "Weight": 0.0, "Width": 100.0}, copyLib=False, copyGroups=False, copyFeatures=False, muteKerning=False, muteInfo=False, mutedGlyphNames=[], familyName="MasterFamilyName", styleName="FauxItalic", localisedFamilyName={}, ), ], ) assert_descriptors_equal( doc.variableFonts, [ VariableFontDescriptor( name="Test_WghtWdth", filename="Test_WghtWdth_different_from_name.ttf", axisSubsets=[ RangeAxisSubsetDescriptor(name="Weight"), RangeAxisSubsetDescriptor(name="Width"), ], lib={"com.vtt.source": "sources/vtt/Test_WghtWdth.vtt"}, ), VariableFontDescriptor( name="Test_Wght", axisSubsets=[RangeAxisSubsetDescriptor(name="Weight")], lib={"com.vtt.source": "sources/vtt/Test_Wght.vtt"}, ), VariableFontDescriptor( name="TestCd_Wght", axisSubsets=[ RangeAxisSubsetDescriptor(name="Weight"), ValueAxisSubsetDescriptor(name="Width", userValue=0), ], ), VariableFontDescriptor( name="TestWd_Wght", axisSubsets=[ RangeAxisSubsetDescriptor(name="Weight"), ValueAxisSubsetDescriptor(name="Width", userValue=1000), ], ), VariableFontDescriptor( name="TestItalic_Wght", axisSubsets=[ RangeAxisSubsetDescriptor(name="Weight"), ValueAxisSubsetDescriptor(name="Italic", userValue=1), ], ), VariableFontDescriptor( name="TestRB_Wght", axisSubsets=[ RangeAxisSubsetDescriptor( name="Weight", userMinimum=400, userDefault=400, userMaximum=700 ), ValueAxisSubsetDescriptor(name="Italic", userValue=0), ], ), ], ) assert_descriptors_equal( doc.instances, [ InstanceDescriptor( filename="instances/instanceTest1.ufo", path=posix(str((datadir / "instances/instanceTest1.ufo").resolve())), name="instance.ufo1", designLocation={"Weight": 500.0, "Width": 20.0}, familyName="InstanceFamilyName", styleName="InstanceStyleName", postScriptFontName="InstancePostscriptName", styleMapFamilyName="InstanceStyleMapFamilyName", styleMapStyleName="InstanceStyleMapStyleName", localisedFamilyName={"fr": "Montserrat", "ja": "モンセラート"}, localisedStyleName={"fr": "Demigras", "ja": "半ば"}, localisedStyleMapFamilyName={ "de": "Montserrat Halbfett", "ja": "モンセラート SemiBold", }, localisedStyleMapStyleName={"de": "Standard"}, glyphs={"arrow": {"mute": True, "unicodes": [291, 292, 293]}}, lib={ "com.coolDesignspaceApp.binaryData": b"", "com.coolDesignspaceApp.specimenText": "Hamburgerwhatever", }, ), InstanceDescriptor( filename="instances/instanceTest2.ufo", path=posix(str((datadir / "instances/instanceTest2.ufo").resolve())), name="instance.ufo2", designLocation={"Weight": 500.0, "Width": (400.0, 300.0)}, familyName="InstanceFamilyName", styleName="InstanceStyleName", postScriptFontName="InstancePostscriptName", styleMapFamilyName="InstanceStyleMapFamilyName", styleMapStyleName="InstanceStyleMapStyleName", glyphs={ "arrow": { "unicodes": [101, 201, 301], "note": "A note about this glyph", "instanceLocation": {"Weight": 120.0, "Width": 100.0}, "masters": [ { "font": "master.ufo1", "location": {"Weight": 20.0, "Width": 20.0}, "glyphName": "BB", }, { "font": "master.ufo2", "location": {"Weight": 900.0, "Width": 900.0}, "glyphName": "CC", }, ], }, "arrow2": {}, }, ), InstanceDescriptor( locationLabel="Some Style", ), InstanceDescriptor( designLocation={"Weight": 600.0, "Width": (401.0, 420.0)}, ), InstanceDescriptor( designLocation={"Weight": 10.0, "Italic": 0.0}, userLocation={"Width": 100.0}, ), InstanceDescriptor( userLocation={"Weight": 300.0, "Width": 130.0, "Italic": 1.0}, ), ], ) def test_read_v5_document_decovar(datadir): doc = DesignSpaceDocument.fromfile(datadir / "test_v5_decovar.designspace") assert not doc.variableFonts assert_descriptors_equal( doc.axes, [ AxisDescriptor( default=0, maximum=1000, minimum=0, name="Inline", tag="BLDA" ), AxisDescriptor( default=0, maximum=1000, minimum=0, name="Shearded", tag="TRMD" ), AxisDescriptor( default=0, maximum=1000, minimum=0, name="Rounded Slab", tag="TRMC" ), AxisDescriptor( default=0, maximum=1000, minimum=0, name="Stripes", tag="SKLD" ), AxisDescriptor( default=0, maximum=1000, minimum=0, name="Worm Terminal", tag="TRML" ), AxisDescriptor( default=0, maximum=1000, minimum=0, name="Inline Skeleton", tag="SKLA" ), AxisDescriptor( default=0, maximum=1000, minimum=0, name="Open Inline Terminal", tag="TRMF", ), AxisDescriptor( default=0, maximum=1000, minimum=0, name="Inline Terminal", tag="TRMK" ), AxisDescriptor(default=0, maximum=1000, minimum=0, name="Worm", tag="BLDB"), AxisDescriptor( default=0, maximum=1000, minimum=0, name="Weight", tag="WMX2" ), AxisDescriptor( default=0, maximum=1000, minimum=0, name="Flared", tag="TRMB" ), AxisDescriptor( default=0, maximum=1000, minimum=0, name="Rounded", tag="TRMA" ), AxisDescriptor( default=0, maximum=1000, minimum=0, name="Worm Skeleton", tag="SKLB" ), AxisDescriptor(default=0, maximum=1000, minimum=0, name="Slab", tag="TRMG"), AxisDescriptor( default=0, maximum=1000, minimum=0, name="Bifurcated", tag="TRME" ), ], ) assert_descriptors_equal( doc.locationLabels, [ LocationLabelDescriptor(name="Default", elidable=True, userLocation={}), LocationLabelDescriptor( name="Open", userLocation={"Inline": 1000}, labelNames={"de": "Offen"} ), LocationLabelDescriptor(name="Worm", userLocation={"Worm": 1000}), LocationLabelDescriptor( name="Checkered", userLocation={"Inline Skeleton": 1000} ), LocationLabelDescriptor( name="Checkered Reverse", userLocation={"Inline Terminal": 1000} ), LocationLabelDescriptor(name="Striped", userLocation={"Stripes": 500}), LocationLabelDescriptor(name="Rounded", userLocation={"Rounded": 1000}), LocationLabelDescriptor(name="Flared", userLocation={"Flared": 1000}), LocationLabelDescriptor( name="Flared Open", userLocation={"Inline Skeleton": 1000, "Flared": 1000}, ), LocationLabelDescriptor( name="Rounded Slab", userLocation={"Rounded Slab": 1000} ), LocationLabelDescriptor(name="Sheared", userLocation={"Shearded": 1000}), LocationLabelDescriptor( name="Bifurcated", userLocation={"Bifurcated": 1000} ), LocationLabelDescriptor( name="Inline", userLocation={"Inline Skeleton": 500, "Open Inline Terminal": 500}, ), LocationLabelDescriptor(name="Slab", userLocation={"Slab": 1000}), LocationLabelDescriptor(name="Contrast", userLocation={"Weight": 1000}), LocationLabelDescriptor( name="Fancy", userLocation={"Inline Skeleton": 1000, "Flared": 1000, "Weight": 1000}, ), LocationLabelDescriptor( name="Mayhem", userLocation={ "Inline Skeleton": 1000, "Worm Skeleton": 1000, "Rounded": 500, "Flared": 500, "Rounded Slab": 750, "Bifurcated": 500, "Open Inline Terminal": 250, "Slab": 750, "Inline Terminal": 250, "Worm Terminal": 250, "Weight": 750, "Worm": 1000, }, ), ], ) assert [i.locationLabel for i in doc.instances] == [ "Default", "Open", "Worm", "Checkered", "Checkered Reverse", "Striped", "Rounded", "Flared", "Flared Open", "Rounded Slab", "Sheared", "Bifurcated", "Inline", "Slab", "Contrast", "Fancy", "Mayhem", ] def test_read_v5_document_discrete(datadir): doc = DesignSpaceDocument.fromfile(datadir / "test_v5_discrete.designspace") assert not doc.locationLabels assert not doc.variableFonts assert_descriptors_equal( doc.axes, [ DiscreteAxisDescriptor( default=400, values=[400, 700, 900], name="Weight", tag="wght", axisLabels=[ AxisLabelDescriptor( name="Regular", userValue=400, elidable=True, linkedUserValue=700, ), AxisLabelDescriptor(name="Bold", userValue=700), AxisLabelDescriptor(name="Black", userValue=900), ], ), DiscreteAxisDescriptor( default=100, values=[75, 100], name="Width", tag="wdth", axisLabels=[ AxisLabelDescriptor(name="Narrow", userValue=75), AxisLabelDescriptor(name="Normal", userValue=100, elidable=True), ], ), DiscreteAxisDescriptor( default=0, values=[0, 1], name="Italic", tag="ital", axisLabels=[ AxisLabelDescriptor( name="Roman", userValue=0, elidable=True, linkedUserValue=1 ), AxisLabelDescriptor(name="Italic", userValue=1), ], ), ], ) def test_read_v5_document_aktiv(datadir): doc = DesignSpaceDocument.fromfile(datadir / "test_v5_aktiv.designspace") assert not doc.locationLabels assert_descriptors_equal( doc.axes, [ AxisDescriptor( tag="wght", name="Weight", minimum=100, default=400, maximum=900, map=[ (100, 22), (200, 38), (300, 57), (400, 84), (500, 98), (600, 115), (700, 133), (800, 158), (900, 185), ], axisOrdering=1, axisLabels=[ AxisLabelDescriptor(name="Hair", userValue=100), AxisLabelDescriptor(userValue=200, name="Thin"), AxisLabelDescriptor(userValue=300, name="Light"), AxisLabelDescriptor( userValue=400, name="Regular", elidable=True, linkedUserValue=700, ), AxisLabelDescriptor(userValue=500, name="Medium"), AxisLabelDescriptor(userValue=600, name="SemiBold"), AxisLabelDescriptor(userValue=700, name="Bold"), AxisLabelDescriptor(userValue=800, name="XBold"), AxisLabelDescriptor(userValue=900, name="Black"), ], ), AxisDescriptor( tag="wdth", name="Width", minimum=75, default=100, maximum=125, axisOrdering=0, axisLabels=[ AxisLabelDescriptor(name="Cd", userValue=75), AxisLabelDescriptor(name="Normal", elidable=True, userValue=100), AxisLabelDescriptor(name="Ex", userValue=125), ], ), AxisDescriptor( tag="ital", name="Italic", minimum=0, default=0, maximum=1, axisOrdering=2, axisLabels=[ AxisLabelDescriptor( name="Upright", userValue=0, elidable=True, linkedUserValue=1 ), AxisLabelDescriptor(name="Italic", userValue=1), ], ), ], ) assert_descriptors_equal( doc.variableFonts, [ VariableFontDescriptor( name="AktivGroteskVF_WghtWdthItal", axisSubsets=[ RangeAxisSubsetDescriptor(name="Weight"), RangeAxisSubsetDescriptor(name="Width"), RangeAxisSubsetDescriptor(name="Italic"), ], ), VariableFontDescriptor( name="AktivGroteskVF_WghtWdth", axisSubsets=[ RangeAxisSubsetDescriptor(name="Weight"), RangeAxisSubsetDescriptor(name="Width"), ], ), VariableFontDescriptor( name="AktivGroteskVF_Wght", axisSubsets=[ RangeAxisSubsetDescriptor(name="Weight"), ], ), VariableFontDescriptor( name="AktivGroteskVF_Italics_WghtWdth", axisSubsets=[ RangeAxisSubsetDescriptor(name="Weight"), RangeAxisSubsetDescriptor(name="Width"), ValueAxisSubsetDescriptor(name="Italic", userValue=1), ], ), VariableFontDescriptor( name="AktivGroteskVF_Italics_Wght", axisSubsets=[ RangeAxisSubsetDescriptor(name="Weight"), ValueAxisSubsetDescriptor(name="Italic", userValue=1), ], ), ], ) @pytest.fixture def map_doc(): """Generate a document with a few axes to test the mapping functions""" doc = DesignSpaceDocument() doc.addAxis( AxisDescriptor( tag="wght", name="Weight", minimum=100, maximum=900, default=100, map=[(100, 10), (900, 90)], ) ) doc.addAxis( AxisDescriptor( tag="wdth", name="Width", minimum=75, maximum=200, default=100, map=[(75, 7500), (100, 10000), (200, 20000)], ) ) doc.addAxis( AxisDescriptor(tag="CUST", name="Custom", minimum=1, maximum=2, default=1.5) ) doc.addLocationLabel( LocationLabelDescriptor( name="Wonky", userLocation={"Weight": 800, "Custom": 1.2} ) ) return doc def test_doc_location_map_forward(map_doc: DesignSpaceDocument): assert map_doc.map_forward({"Weight": 400, "Width": 150, "Custom": 2}) == { "Weight": 40, "Width": 15000, "Custom": 2, }, "The mappings should be used to compute the design locations" assert map_doc.map_forward({"Weight": 400}) == { "Weight": 40, "Width": 10000, "Custom": 1.5, }, "Missing user locations should be assumed equal to the axis's default" def test_doc_location_map_backward(map_doc: DesignSpaceDocument): assert map_doc.map_backward({"Weight": 40, "Width": 15000, "Custom": 2}) == { "Weight": 400, "Width": 150, "Custom": 2, }, "The mappings should be used to compute the user locations" assert map_doc.map_backward({"Weight": 40}) == { "Weight": 400, "Width": 100, "Custom": 1.5, }, "Missing design locations should be assumed equal to the axis's default" assert map_doc.map_backward( {"Weight": (40, 50), "Width": (15000, 100000), "Custom": (2, 1.5)} ) == { "Weight": 400, "Width": 150, "Custom": 2, }, "Only the xvalue of anisotropic locations is used" def test_instance_location_from_label(map_doc): inst = InstanceDescriptor(locationLabel="Wonky") assert inst.getFullUserLocation(map_doc) == { "Weight": 800, "Width": 100, "Custom": 1.2, }, "an instance with a locationLabel uses the user location from that label, empty values on the label use axis defaults" assert inst.getFullDesignLocation(map_doc) == { "Weight": 80, "Width": 10000, "Custom": 1.2, }, "an instance with a locationLabel computes the design location from that label, empty values on the label use axis defaults" inst = InstanceDescriptor(locationLabel="Wonky", userLocation={"Width": 200}) assert inst.getFullUserLocation(map_doc) == { "Weight": 800, "Width": 100, "Custom": 1.2, }, "an instance with a locationLabel uses the user location from that label, other location values are ignored" assert inst.getFullDesignLocation(map_doc) == { "Weight": 80, "Width": 10000, "Custom": 1.2, }, "an instance with a locationLabel computes the design location from that label, other location values are ignored" def test_instance_location_no_data(map_doc): inst = InstanceDescriptor() assert inst.getFullUserLocation(map_doc) == { "Weight": 100, "Width": 100, "Custom": 1.5, }, "an instance without any location data has the default user location" assert inst.getFullDesignLocation(map_doc) == { "Weight": 10, "Width": 10000, "Custom": 1.5, }, "an instance without any location data has the default design location" def test_instance_location_design_first(map_doc): inst = InstanceDescriptor( designLocation={"Weight": (60, 61), "Width": 11000, "Custom": 1.2}, userLocation={"Weight": 700, "Width": 180, "Custom": 1.4}, ) assert inst.getFullUserLocation(map_doc) == { "Weight": 600, "Width": 110, "Custom": 1.2, }, "when both design and user location data are provided, design wins" assert inst.getFullDesignLocation(map_doc) == { "Weight": (60, 61), "Width": 11000, "Custom": 1.2, }, "when both design and user location data are provided, design wins (incl. anisotropy)" def test_instance_location_mix(map_doc): inst = InstanceDescriptor( designLocation={"Weight": (60, 61)}, userLocation={"Width": 180}, ) assert inst.getFullUserLocation(map_doc) == { "Weight": 600, "Width": 180, "Custom": 1.5, }, "instance location is a mix of design and user locations" assert inst.getFullDesignLocation(map_doc) == { "Weight": (60, 61), "Width": 18000, "Custom": 1.5, }, "instance location is a mix of design and user location" @pytest.mark.parametrize( "filename", [ "test_v4_original.designspace", "test_v5_original.designspace", "test_v5_aktiv.designspace", "test_v5_decovar.designspace", "test_v5_discrete.designspace", "test_v5_sourceserif.designspace", "test_v5.designspace", ], ) def test_roundtrip(tmpdir, datadir, filename): test_file = datadir / filename output_path = tmpdir / filename # Move the file to the tmpdir so that the filenames stay the same # (they're relative to the file's path) shutil.copy(test_file, output_path) doc = DesignSpaceDocument.fromfile(output_path) doc.write(output_path) # The input XML has comments and empty lines for documentation purposes xml = test_file.read_text(encoding="utf-8") xml = re.sub( r"(.|\n)*?", "", xml, ) xml = re.sub(r"", "", xml) xml = re.sub(r"\s*\n+", "\n", xml) assert output_path.read_text(encoding="utf-8") == xml def test_using_v5_features_upgrades_format(tmpdir, datadir): test_file = datadir / "test_v4_original.designspace" output_4_path = tmpdir / "test_v4.designspace" output_5_path = tmpdir / "test_v5.designspace" shutil.copy(test_file, output_4_path) doc = DesignSpaceDocument.fromfile(output_4_path) doc.write(output_4_path) assert 'format="4.1"' in output_4_path.read_text(encoding="utf-8") doc.addVariableFont(VariableFontDescriptor(name="TestVF")) doc.write(output_5_path) assert 'format="5.0"' in output_5_path.read_text(encoding="utf-8") def test_addAxisDescriptor_discrete(): ds = DesignSpaceDocument() axis = ds.addAxisDescriptor( name="Italic", tag="ital", values=[0, 1], default=0, hidden=True, map=[(0, -12), (1, 0)], axisOrdering=3, axisLabels=[ AxisLabelDescriptor( name="Roman", userValue=0, elidable=True, olderSibling=True, linkedUserValue=1, labelNames={"fr": "Romain"}, ) ], ) assert ds.axes[0] is axis assert_descriptors_equal( [axis], [ DiscreteAxisDescriptor( tag="ital", name="Italic", values=[0, 1], default=0, hidden=True, map=[(0, -12), (1, 0)], axisOrdering=3, axisLabels=[ AxisLabelDescriptor( name="Roman", userValue=0, elidable=True, olderSibling=True, linkedUserValue=1, labelNames={"fr": "Romain"}, ) ], ) ], ) def test_addLocationLabelDescriptor(): ds = DesignSpaceDocument() label = ds.addLocationLabelDescriptor( name="Somewhere", userLocation={}, elidable=True, olderSibling=True, labelNames={"fr": "Quelque part"}, ) assert ds.locationLabels[0] is label assert_descriptors_equal( [label], [ LocationLabelDescriptor( name="Somewhere", userLocation={}, elidable=True, olderSibling=True, labelNames={"fr": "Quelque part"}, ) ], ) def test_addVariableFontDescriptor(): ds = DesignSpaceDocument() vf = ds.addVariableFontDescriptor(name="TestVF", filename="TestVF.ttf") assert ds.variableFonts[0] is vf assert_descriptors_equal( [vf], [VariableFontDescriptor(name="TestVF", filename="TestVF.ttf")] )