xref: /aosp_15_r20/external/fonttools/Tests/designspaceLib/designspace_v5_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1import re
2import shutil
3from pathlib import Path
4
5import pytest
6from fontTools.designspaceLib import (
7    AxisDescriptor,
8    AxisLabelDescriptor,
9    DesignSpaceDocument,
10    DiscreteAxisDescriptor,
11    InstanceDescriptor,
12    LocationLabelDescriptor,
13    RangeAxisSubsetDescriptor,
14    SourceDescriptor,
15    ValueAxisSubsetDescriptor,
16    VariableFontDescriptor,
17    posix,
18)
19
20from .fixtures import datadir
21
22
23def assert_descriptors_equal(actual, expected):
24    assert len(actual) == len(expected)
25    for a, e in zip(actual, expected):
26        assert a.asdict() == e.asdict()
27
28
29def test_read_v5_document_simple(datadir):
30    doc = DesignSpaceDocument.fromfile(datadir / "test_v5.designspace")
31
32    assert_descriptors_equal(
33        doc.axes,
34        [
35            AxisDescriptor(
36                tag="wght",
37                name="Weight",
38                minimum=200,
39                maximum=1000,
40                default=200,
41                labelNames={"en": "Wéíght", "fa-IR": "قطر"},
42                map=[
43                    (200, 0),
44                    (300, 100),
45                    (400, 368),
46                    (600, 600),
47                    (700, 824),
48                    (900, 1000),
49                ],
50                axisOrdering=None,
51                axisLabels=[
52                    AxisLabelDescriptor(
53                        name="Extra Light",
54                        userMinimum=200,
55                        userValue=200,
56                        userMaximum=250,
57                        labelNames={"de": "Extraleicht", "fr": "Extra léger"},
58                    ),
59                    AxisLabelDescriptor(
60                        name="Light", userMinimum=250, userValue=300, userMaximum=350
61                    ),
62                    AxisLabelDescriptor(
63                        name="Regular",
64                        userMinimum=350,
65                        userValue=400,
66                        userMaximum=450,
67                        elidable=True,
68                    ),
69                    AxisLabelDescriptor(
70                        name="Semi Bold",
71                        userMinimum=450,
72                        userValue=600,
73                        userMaximum=650,
74                    ),
75                    AxisLabelDescriptor(
76                        name="Bold", userMinimum=650, userValue=700, userMaximum=850
77                    ),
78                    AxisLabelDescriptor(
79                        name="Black", userMinimum=850, userValue=900, userMaximum=900
80                    ),
81                    AxisLabelDescriptor(
82                        name="Regular",
83                        userValue=400,
84                        linkedUserValue=700,
85                        elidable=True,
86                    ),
87                    AxisLabelDescriptor(
88                        name="Bold", userValue=700, linkedUserValue=400
89                    ),
90                ],
91            ),
92            AxisDescriptor(
93                tag="wdth",
94                name="Width",
95                minimum=50,
96                maximum=150,
97                default=100,
98                hidden=True,
99                labelNames={"fr": "Chasse"},
100                map=[(50, 10), (100, 20), (125, 66), (150, 990)],
101                axisOrdering=1,
102                axisLabels=[
103                    AxisLabelDescriptor(name="Condensed", userValue=50),
104                    AxisLabelDescriptor(
105                        name="Normal", elidable=True, olderSibling=True, userValue=100
106                    ),
107                    AxisLabelDescriptor(name="Wide", userValue=125),
108                    AxisLabelDescriptor(
109                        name="Extra Wide", userValue=150, userMinimum=150
110                    ),
111                ],
112            ),
113            DiscreteAxisDescriptor(
114                tag="ital",
115                name="Italic",
116                values=[0, 1],
117                default=0,
118                axisOrdering=None,
119                axisLabels=[
120                    AxisLabelDescriptor(
121                        name="Roman", userValue=0, elidable=True, linkedUserValue=1
122                    ),
123                    AxisLabelDescriptor(name="Italic", userValue=1),
124                ],
125            ),
126        ],
127    )
128
129    assert_descriptors_equal(
130        doc.locationLabels,
131        [
132            LocationLabelDescriptor(
133                name="Some Style",
134                labelNames={"fr": "Un Style"},
135                userLocation={"Weight": 300, "Width": 50, "Italic": 0},
136            ),
137            LocationLabelDescriptor(
138                name="Other", userLocation={"Weight": 700, "Width": 100, "Italic": 1}
139            ),
140        ],
141    )
142
143    assert_descriptors_equal(
144        doc.sources,
145        [
146            SourceDescriptor(
147                filename="masters/masterTest1.ufo",
148                path=posix(str((datadir / "masters/masterTest1.ufo").resolve())),
149                name="master.ufo1",
150                layerName=None,
151                location={"Italic": 0.0, "Weight": 0.0, "Width": 20.0},
152                copyLib=True,
153                copyInfo=True,
154                copyGroups=False,
155                copyFeatures=True,
156                muteKerning=False,
157                muteInfo=False,
158                mutedGlyphNames=["A", "Z"],
159                familyName="MasterFamilyName",
160                styleName="MasterStyleNameOne",
161                localisedFamilyName={"fr": "Montserrat", "ja": "モンセラート"},
162            ),
163            SourceDescriptor(
164                filename="masters/masterTest2.ufo",
165                path=posix(str((datadir / "masters/masterTest2.ufo").resolve())),
166                name="master.ufo2",
167                layerName=None,
168                location={"Italic": 0.0, "Weight": 1000.0, "Width": 20.0},
169                copyLib=False,
170                copyInfo=False,
171                copyGroups=False,
172                copyFeatures=False,
173                muteKerning=True,
174                muteInfo=False,
175                mutedGlyphNames=[],
176                familyName="MasterFamilyName",
177                styleName="MasterStyleNameTwo",
178                localisedFamilyName={},
179            ),
180            SourceDescriptor(
181                filename="masters/masterTest2.ufo",
182                path=posix(str((datadir / "masters/masterTest2.ufo").resolve())),
183                name="master.ufo2",
184                layerName="supports",
185                location={"Italic": 0.0, "Weight": 1000.0, "Width": 20.0},
186                copyLib=False,
187                copyInfo=False,
188                copyGroups=False,
189                copyFeatures=False,
190                muteKerning=False,
191                muteInfo=False,
192                mutedGlyphNames=[],
193                familyName="MasterFamilyName",
194                styleName="Supports",
195                localisedFamilyName={},
196            ),
197            SourceDescriptor(
198                filename="masters/masterTest2.ufo",
199                path=posix(str((datadir / "masters/masterTest2.ufo").resolve())),
200                name="master.ufo3",
201                layerName=None,
202                location={"Italic": 1.0, "Weight": 0.0, "Width": 100.0},
203                copyLib=False,
204                copyGroups=False,
205                copyFeatures=False,
206                muteKerning=False,
207                muteInfo=False,
208                mutedGlyphNames=[],
209                familyName="MasterFamilyName",
210                styleName="FauxItalic",
211                localisedFamilyName={},
212            ),
213        ],
214    )
215
216    assert_descriptors_equal(
217        doc.variableFonts,
218        [
219            VariableFontDescriptor(
220                name="Test_WghtWdth",
221                filename="Test_WghtWdth_different_from_name.ttf",
222                axisSubsets=[
223                    RangeAxisSubsetDescriptor(name="Weight"),
224                    RangeAxisSubsetDescriptor(name="Width"),
225                ],
226                lib={"com.vtt.source": "sources/vtt/Test_WghtWdth.vtt"},
227            ),
228            VariableFontDescriptor(
229                name="Test_Wght",
230                axisSubsets=[RangeAxisSubsetDescriptor(name="Weight")],
231                lib={"com.vtt.source": "sources/vtt/Test_Wght.vtt"},
232            ),
233            VariableFontDescriptor(
234                name="TestCd_Wght",
235                axisSubsets=[
236                    RangeAxisSubsetDescriptor(name="Weight"),
237                    ValueAxisSubsetDescriptor(name="Width", userValue=0),
238                ],
239            ),
240            VariableFontDescriptor(
241                name="TestWd_Wght",
242                axisSubsets=[
243                    RangeAxisSubsetDescriptor(name="Weight"),
244                    ValueAxisSubsetDescriptor(name="Width", userValue=1000),
245                ],
246            ),
247            VariableFontDescriptor(
248                name="TestItalic_Wght",
249                axisSubsets=[
250                    RangeAxisSubsetDescriptor(name="Weight"),
251                    ValueAxisSubsetDescriptor(name="Italic", userValue=1),
252                ],
253            ),
254            VariableFontDescriptor(
255                name="TestRB_Wght",
256                axisSubsets=[
257                    RangeAxisSubsetDescriptor(
258                        name="Weight", userMinimum=400, userDefault=400, userMaximum=700
259                    ),
260                    ValueAxisSubsetDescriptor(name="Italic", userValue=0),
261                ],
262            ),
263        ],
264    )
265
266    assert_descriptors_equal(
267        doc.instances,
268        [
269            InstanceDescriptor(
270                filename="instances/instanceTest1.ufo",
271                path=posix(str((datadir / "instances/instanceTest1.ufo").resolve())),
272                name="instance.ufo1",
273                designLocation={"Weight": 500.0, "Width": 20.0},
274                familyName="InstanceFamilyName",
275                styleName="InstanceStyleName",
276                postScriptFontName="InstancePostscriptName",
277                styleMapFamilyName="InstanceStyleMapFamilyName",
278                styleMapStyleName="InstanceStyleMapStyleName",
279                localisedFamilyName={"fr": "Montserrat", "ja": "モンセラート"},
280                localisedStyleName={"fr": "Demigras", "ja": "半ば"},
281                localisedStyleMapFamilyName={
282                    "de": "Montserrat Halbfett",
283                    "ja": "モンセラート SemiBold",
284                },
285                localisedStyleMapStyleName={"de": "Standard"},
286                glyphs={"arrow": {"mute": True, "unicodes": [291, 292, 293]}},
287                lib={
288                    "com.coolDesignspaceApp.binaryData": b"<binary gunk>",
289                    "com.coolDesignspaceApp.specimenText": "Hamburgerwhatever",
290                },
291            ),
292            InstanceDescriptor(
293                filename="instances/instanceTest2.ufo",
294                path=posix(str((datadir / "instances/instanceTest2.ufo").resolve())),
295                name="instance.ufo2",
296                designLocation={"Weight": 500.0, "Width": (400.0, 300.0)},
297                familyName="InstanceFamilyName",
298                styleName="InstanceStyleName",
299                postScriptFontName="InstancePostscriptName",
300                styleMapFamilyName="InstanceStyleMapFamilyName",
301                styleMapStyleName="InstanceStyleMapStyleName",
302                glyphs={
303                    "arrow": {
304                        "unicodes": [101, 201, 301],
305                        "note": "A note about this glyph",
306                        "instanceLocation": {"Weight": 120.0, "Width": 100.0},
307                        "masters": [
308                            {
309                                "font": "master.ufo1",
310                                "location": {"Weight": 20.0, "Width": 20.0},
311                                "glyphName": "BB",
312                            },
313                            {
314                                "font": "master.ufo2",
315                                "location": {"Weight": 900.0, "Width": 900.0},
316                                "glyphName": "CC",
317                            },
318                        ],
319                    },
320                    "arrow2": {},
321                },
322            ),
323            InstanceDescriptor(
324                locationLabel="Some Style",
325            ),
326            InstanceDescriptor(
327                designLocation={"Weight": 600.0, "Width": (401.0, 420.0)},
328            ),
329            InstanceDescriptor(
330                designLocation={"Weight": 10.0, "Italic": 0.0},
331                userLocation={"Width": 100.0},
332            ),
333            InstanceDescriptor(
334                userLocation={"Weight": 300.0, "Width": 130.0, "Italic": 1.0},
335            ),
336        ],
337    )
338
339
340def test_read_v5_document_decovar(datadir):
341    doc = DesignSpaceDocument.fromfile(datadir / "test_v5_decovar.designspace")
342
343    assert not doc.variableFonts
344
345    assert_descriptors_equal(
346        doc.axes,
347        [
348            AxisDescriptor(
349                default=0, maximum=1000, minimum=0, name="Inline", tag="BLDA"
350            ),
351            AxisDescriptor(
352                default=0, maximum=1000, minimum=0, name="Shearded", tag="TRMD"
353            ),
354            AxisDescriptor(
355                default=0, maximum=1000, minimum=0, name="Rounded Slab", tag="TRMC"
356            ),
357            AxisDescriptor(
358                default=0, maximum=1000, minimum=0, name="Stripes", tag="SKLD"
359            ),
360            AxisDescriptor(
361                default=0, maximum=1000, minimum=0, name="Worm Terminal", tag="TRML"
362            ),
363            AxisDescriptor(
364                default=0, maximum=1000, minimum=0, name="Inline Skeleton", tag="SKLA"
365            ),
366            AxisDescriptor(
367                default=0,
368                maximum=1000,
369                minimum=0,
370                name="Open Inline Terminal",
371                tag="TRMF",
372            ),
373            AxisDescriptor(
374                default=0, maximum=1000, minimum=0, name="Inline Terminal", tag="TRMK"
375            ),
376            AxisDescriptor(default=0, maximum=1000, minimum=0, name="Worm", tag="BLDB"),
377            AxisDescriptor(
378                default=0, maximum=1000, minimum=0, name="Weight", tag="WMX2"
379            ),
380            AxisDescriptor(
381                default=0, maximum=1000, minimum=0, name="Flared", tag="TRMB"
382            ),
383            AxisDescriptor(
384                default=0, maximum=1000, minimum=0, name="Rounded", tag="TRMA"
385            ),
386            AxisDescriptor(
387                default=0, maximum=1000, minimum=0, name="Worm Skeleton", tag="SKLB"
388            ),
389            AxisDescriptor(default=0, maximum=1000, minimum=0, name="Slab", tag="TRMG"),
390            AxisDescriptor(
391                default=0, maximum=1000, minimum=0, name="Bifurcated", tag="TRME"
392            ),
393        ],
394    )
395
396    assert_descriptors_equal(
397        doc.locationLabels,
398        [
399            LocationLabelDescriptor(name="Default", elidable=True, userLocation={}),
400            LocationLabelDescriptor(
401                name="Open", userLocation={"Inline": 1000}, labelNames={"de": "Offen"}
402            ),
403            LocationLabelDescriptor(name="Worm", userLocation={"Worm": 1000}),
404            LocationLabelDescriptor(
405                name="Checkered", userLocation={"Inline Skeleton": 1000}
406            ),
407            LocationLabelDescriptor(
408                name="Checkered Reverse", userLocation={"Inline Terminal": 1000}
409            ),
410            LocationLabelDescriptor(name="Striped", userLocation={"Stripes": 500}),
411            LocationLabelDescriptor(name="Rounded", userLocation={"Rounded": 1000}),
412            LocationLabelDescriptor(name="Flared", userLocation={"Flared": 1000}),
413            LocationLabelDescriptor(
414                name="Flared Open",
415                userLocation={"Inline Skeleton": 1000, "Flared": 1000},
416            ),
417            LocationLabelDescriptor(
418                name="Rounded Slab", userLocation={"Rounded Slab": 1000}
419            ),
420            LocationLabelDescriptor(name="Sheared", userLocation={"Shearded": 1000}),
421            LocationLabelDescriptor(
422                name="Bifurcated", userLocation={"Bifurcated": 1000}
423            ),
424            LocationLabelDescriptor(
425                name="Inline",
426                userLocation={"Inline Skeleton": 500, "Open Inline Terminal": 500},
427            ),
428            LocationLabelDescriptor(name="Slab", userLocation={"Slab": 1000}),
429            LocationLabelDescriptor(name="Contrast", userLocation={"Weight": 1000}),
430            LocationLabelDescriptor(
431                name="Fancy",
432                userLocation={"Inline Skeleton": 1000, "Flared": 1000, "Weight": 1000},
433            ),
434            LocationLabelDescriptor(
435                name="Mayhem",
436                userLocation={
437                    "Inline Skeleton": 1000,
438                    "Worm Skeleton": 1000,
439                    "Rounded": 500,
440                    "Flared": 500,
441                    "Rounded Slab": 750,
442                    "Bifurcated": 500,
443                    "Open Inline Terminal": 250,
444                    "Slab": 750,
445                    "Inline Terminal": 250,
446                    "Worm Terminal": 250,
447                    "Weight": 750,
448                    "Worm": 1000,
449                },
450            ),
451        ],
452    )
453
454    assert [i.locationLabel for i in doc.instances] == [
455        "Default",
456        "Open",
457        "Worm",
458        "Checkered",
459        "Checkered Reverse",
460        "Striped",
461        "Rounded",
462        "Flared",
463        "Flared Open",
464        "Rounded Slab",
465        "Sheared",
466        "Bifurcated",
467        "Inline",
468        "Slab",
469        "Contrast",
470        "Fancy",
471        "Mayhem",
472    ]
473
474
475def test_read_v5_document_discrete(datadir):
476    doc = DesignSpaceDocument.fromfile(datadir / "test_v5_discrete.designspace")
477
478    assert not doc.locationLabels
479    assert not doc.variableFonts
480
481    assert_descriptors_equal(
482        doc.axes,
483        [
484            DiscreteAxisDescriptor(
485                default=400,
486                values=[400, 700, 900],
487                name="Weight",
488                tag="wght",
489                axisLabels=[
490                    AxisLabelDescriptor(
491                        name="Regular",
492                        userValue=400,
493                        elidable=True,
494                        linkedUserValue=700,
495                    ),
496                    AxisLabelDescriptor(name="Bold", userValue=700),
497                    AxisLabelDescriptor(name="Black", userValue=900),
498                ],
499            ),
500            DiscreteAxisDescriptor(
501                default=100,
502                values=[75, 100],
503                name="Width",
504                tag="wdth",
505                axisLabels=[
506                    AxisLabelDescriptor(name="Narrow", userValue=75),
507                    AxisLabelDescriptor(name="Normal", userValue=100, elidable=True),
508                ],
509            ),
510            DiscreteAxisDescriptor(
511                default=0,
512                values=[0, 1],
513                name="Italic",
514                tag="ital",
515                axisLabels=[
516                    AxisLabelDescriptor(
517                        name="Roman", userValue=0, elidable=True, linkedUserValue=1
518                    ),
519                    AxisLabelDescriptor(name="Italic", userValue=1),
520                ],
521            ),
522        ],
523    )
524
525
526def test_read_v5_document_aktiv(datadir):
527    doc = DesignSpaceDocument.fromfile(datadir / "test_v5_aktiv.designspace")
528
529    assert not doc.locationLabels
530
531    assert_descriptors_equal(
532        doc.axes,
533        [
534            AxisDescriptor(
535                tag="wght",
536                name="Weight",
537                minimum=100,
538                default=400,
539                maximum=900,
540                map=[
541                    (100, 22),
542                    (200, 38),
543                    (300, 57),
544                    (400, 84),
545                    (500, 98),
546                    (600, 115),
547                    (700, 133),
548                    (800, 158),
549                    (900, 185),
550                ],
551                axisOrdering=1,
552                axisLabels=[
553                    AxisLabelDescriptor(name="Hair", userValue=100),
554                    AxisLabelDescriptor(userValue=200, name="Thin"),
555                    AxisLabelDescriptor(userValue=300, name="Light"),
556                    AxisLabelDescriptor(
557                        userValue=400,
558                        name="Regular",
559                        elidable=True,
560                        linkedUserValue=700,
561                    ),
562                    AxisLabelDescriptor(userValue=500, name="Medium"),
563                    AxisLabelDescriptor(userValue=600, name="SemiBold"),
564                    AxisLabelDescriptor(userValue=700, name="Bold"),
565                    AxisLabelDescriptor(userValue=800, name="XBold"),
566                    AxisLabelDescriptor(userValue=900, name="Black"),
567                ],
568            ),
569            AxisDescriptor(
570                tag="wdth",
571                name="Width",
572                minimum=75,
573                default=100,
574                maximum=125,
575                axisOrdering=0,
576                axisLabels=[
577                    AxisLabelDescriptor(name="Cd", userValue=75),
578                    AxisLabelDescriptor(name="Normal", elidable=True, userValue=100),
579                    AxisLabelDescriptor(name="Ex", userValue=125),
580                ],
581            ),
582            AxisDescriptor(
583                tag="ital",
584                name="Italic",
585                minimum=0,
586                default=0,
587                maximum=1,
588                axisOrdering=2,
589                axisLabels=[
590                    AxisLabelDescriptor(
591                        name="Upright", userValue=0, elidable=True, linkedUserValue=1
592                    ),
593                    AxisLabelDescriptor(name="Italic", userValue=1),
594                ],
595            ),
596        ],
597    )
598
599    assert_descriptors_equal(
600        doc.variableFonts,
601        [
602            VariableFontDescriptor(
603                name="AktivGroteskVF_WghtWdthItal",
604                axisSubsets=[
605                    RangeAxisSubsetDescriptor(name="Weight"),
606                    RangeAxisSubsetDescriptor(name="Width"),
607                    RangeAxisSubsetDescriptor(name="Italic"),
608                ],
609            ),
610            VariableFontDescriptor(
611                name="AktivGroteskVF_WghtWdth",
612                axisSubsets=[
613                    RangeAxisSubsetDescriptor(name="Weight"),
614                    RangeAxisSubsetDescriptor(name="Width"),
615                ],
616            ),
617            VariableFontDescriptor(
618                name="AktivGroteskVF_Wght",
619                axisSubsets=[
620                    RangeAxisSubsetDescriptor(name="Weight"),
621                ],
622            ),
623            VariableFontDescriptor(
624                name="AktivGroteskVF_Italics_WghtWdth",
625                axisSubsets=[
626                    RangeAxisSubsetDescriptor(name="Weight"),
627                    RangeAxisSubsetDescriptor(name="Width"),
628                    ValueAxisSubsetDescriptor(name="Italic", userValue=1),
629                ],
630            ),
631            VariableFontDescriptor(
632                name="AktivGroteskVF_Italics_Wght",
633                axisSubsets=[
634                    RangeAxisSubsetDescriptor(name="Weight"),
635                    ValueAxisSubsetDescriptor(name="Italic", userValue=1),
636                ],
637            ),
638        ],
639    )
640
641
642@pytest.fixture
643def map_doc():
644    """Generate a document with a few axes to test the mapping functions"""
645    doc = DesignSpaceDocument()
646    doc.addAxis(
647        AxisDescriptor(
648            tag="wght",
649            name="Weight",
650            minimum=100,
651            maximum=900,
652            default=100,
653            map=[(100, 10), (900, 90)],
654        )
655    )
656    doc.addAxis(
657        AxisDescriptor(
658            tag="wdth",
659            name="Width",
660            minimum=75,
661            maximum=200,
662            default=100,
663            map=[(75, 7500), (100, 10000), (200, 20000)],
664        )
665    )
666    doc.addAxis(
667        AxisDescriptor(tag="CUST", name="Custom", minimum=1, maximum=2, default=1.5)
668    )
669    doc.addLocationLabel(
670        LocationLabelDescriptor(
671            name="Wonky", userLocation={"Weight": 800, "Custom": 1.2}
672        )
673    )
674    return doc
675
676
677def test_doc_location_map_forward(map_doc: DesignSpaceDocument):
678    assert map_doc.map_forward({"Weight": 400, "Width": 150, "Custom": 2}) == {
679        "Weight": 40,
680        "Width": 15000,
681        "Custom": 2,
682    }, "The mappings should be used to compute the design locations"
683    assert map_doc.map_forward({"Weight": 400}) == {
684        "Weight": 40,
685        "Width": 10000,
686        "Custom": 1.5,
687    }, "Missing user locations should be assumed equal to the axis's default"
688
689
690def test_doc_location_map_backward(map_doc: DesignSpaceDocument):
691    assert map_doc.map_backward({"Weight": 40, "Width": 15000, "Custom": 2}) == {
692        "Weight": 400,
693        "Width": 150,
694        "Custom": 2,
695    }, "The mappings should be used to compute the user locations"
696    assert map_doc.map_backward({"Weight": 40}) == {
697        "Weight": 400,
698        "Width": 100,
699        "Custom": 1.5,
700    }, "Missing design locations should be assumed equal to the axis's default"
701    assert map_doc.map_backward(
702        {"Weight": (40, 50), "Width": (15000, 100000), "Custom": (2, 1.5)}
703    ) == {
704        "Weight": 400,
705        "Width": 150,
706        "Custom": 2,
707    }, "Only the xvalue of anisotropic locations is used"
708
709
710def test_instance_location_from_label(map_doc):
711    inst = InstanceDescriptor(locationLabel="Wonky")
712    assert inst.getFullUserLocation(map_doc) == {
713        "Weight": 800,
714        "Width": 100,
715        "Custom": 1.2,
716    }, "an instance with a locationLabel uses the user location from that label, empty values on the label use axis defaults"
717    assert inst.getFullDesignLocation(map_doc) == {
718        "Weight": 80,
719        "Width": 10000,
720        "Custom": 1.2,
721    }, "an instance with a locationLabel computes the design location from that label, empty values on the label use axis defaults"
722
723    inst = InstanceDescriptor(locationLabel="Wonky", userLocation={"Width": 200})
724    assert inst.getFullUserLocation(map_doc) == {
725        "Weight": 800,
726        "Width": 100,
727        "Custom": 1.2,
728    }, "an instance with a locationLabel uses the user location from that label, other location values are ignored"
729    assert inst.getFullDesignLocation(map_doc) == {
730        "Weight": 80,
731        "Width": 10000,
732        "Custom": 1.2,
733    }, "an instance with a locationLabel computes the design location from that label, other location values are ignored"
734
735
736def test_instance_location_no_data(map_doc):
737    inst = InstanceDescriptor()
738    assert inst.getFullUserLocation(map_doc) == {
739        "Weight": 100,
740        "Width": 100,
741        "Custom": 1.5,
742    }, "an instance without any location data has the default user location"
743    assert inst.getFullDesignLocation(map_doc) == {
744        "Weight": 10,
745        "Width": 10000,
746        "Custom": 1.5,
747    }, "an instance without any location data has the default design location"
748
749
750def test_instance_location_design_first(map_doc):
751    inst = InstanceDescriptor(
752        designLocation={"Weight": (60, 61), "Width": 11000, "Custom": 1.2},
753        userLocation={"Weight": 700, "Width": 180, "Custom": 1.4},
754    )
755    assert inst.getFullUserLocation(map_doc) == {
756        "Weight": 600,
757        "Width": 110,
758        "Custom": 1.2,
759    }, "when both design and user location data are provided, design wins"
760    assert inst.getFullDesignLocation(map_doc) == {
761        "Weight": (60, 61),
762        "Width": 11000,
763        "Custom": 1.2,
764    }, "when both design and user location data are provided, design wins (incl. anisotropy)"
765
766
767def test_instance_location_mix(map_doc):
768    inst = InstanceDescriptor(
769        designLocation={"Weight": (60, 61)},
770        userLocation={"Width": 180},
771    )
772    assert inst.getFullUserLocation(map_doc) == {
773        "Weight": 600,
774        "Width": 180,
775        "Custom": 1.5,
776    }, "instance location is a mix of design and user locations"
777    assert inst.getFullDesignLocation(map_doc) == {
778        "Weight": (60, 61),
779        "Width": 18000,
780        "Custom": 1.5,
781    }, "instance location is a mix of design and user location"
782
783
784@pytest.mark.parametrize(
785    "filename",
786    [
787        "test_v4_original.designspace",
788        "test_v5_original.designspace",
789        "test_v5_aktiv.designspace",
790        "test_v5_decovar.designspace",
791        "test_v5_discrete.designspace",
792        "test_v5_sourceserif.designspace",
793        "test_v5.designspace",
794    ],
795)
796def test_roundtrip(tmpdir, datadir, filename):
797    test_file = datadir / filename
798    output_path = tmpdir / filename
799    # Move the file to the tmpdir so that the filenames stay the same
800    # (they're relative to the file's path)
801    shutil.copy(test_file, output_path)
802    doc = DesignSpaceDocument.fromfile(output_path)
803    doc.write(output_path)
804    # The input XML has comments and empty lines for documentation purposes
805    xml = test_file.read_text(encoding="utf-8")
806    xml = re.sub(
807        r"<!-- ROUNDTRIP_TEST_REMOVE_ME_BEGIN -->(.|\n)*?<!-- ROUNDTRIP_TEST_REMOVE_ME_END -->",
808        "",
809        xml,
810    )
811    xml = re.sub(r"<!--(.|\n)*?-->", "", xml)
812    xml = re.sub(r"\s*\n+", "\n", xml)
813    assert output_path.read_text(encoding="utf-8") == xml
814
815
816def test_using_v5_features_upgrades_format(tmpdir, datadir):
817    test_file = datadir / "test_v4_original.designspace"
818    output_4_path = tmpdir / "test_v4.designspace"
819    output_5_path = tmpdir / "test_v5.designspace"
820    shutil.copy(test_file, output_4_path)
821    doc = DesignSpaceDocument.fromfile(output_4_path)
822    doc.write(output_4_path)
823    assert 'format="4.1"' in output_4_path.read_text(encoding="utf-8")
824    doc.addVariableFont(VariableFontDescriptor(name="TestVF"))
825    doc.write(output_5_path)
826    assert 'format="5.0"' in output_5_path.read_text(encoding="utf-8")
827
828
829def test_addAxisDescriptor_discrete():
830    ds = DesignSpaceDocument()
831
832    axis = ds.addAxisDescriptor(
833        name="Italic",
834        tag="ital",
835        values=[0, 1],
836        default=0,
837        hidden=True,
838        map=[(0, -12), (1, 0)],
839        axisOrdering=3,
840        axisLabels=[
841            AxisLabelDescriptor(
842                name="Roman",
843                userValue=0,
844                elidable=True,
845                olderSibling=True,
846                linkedUserValue=1,
847                labelNames={"fr": "Romain"},
848            )
849        ],
850    )
851
852    assert ds.axes[0] is axis
853    assert_descriptors_equal(
854        [axis],
855        [
856            DiscreteAxisDescriptor(
857                tag="ital",
858                name="Italic",
859                values=[0, 1],
860                default=0,
861                hidden=True,
862                map=[(0, -12), (1, 0)],
863                axisOrdering=3,
864                axisLabels=[
865                    AxisLabelDescriptor(
866                        name="Roman",
867                        userValue=0,
868                        elidable=True,
869                        olderSibling=True,
870                        linkedUserValue=1,
871                        labelNames={"fr": "Romain"},
872                    )
873                ],
874            )
875        ],
876    )
877
878
879def test_addLocationLabelDescriptor():
880    ds = DesignSpaceDocument()
881
882    label = ds.addLocationLabelDescriptor(
883        name="Somewhere",
884        userLocation={},
885        elidable=True,
886        olderSibling=True,
887        labelNames={"fr": "Quelque part"},
888    )
889
890    assert ds.locationLabels[0] is label
891    assert_descriptors_equal(
892        [label],
893        [
894            LocationLabelDescriptor(
895                name="Somewhere",
896                userLocation={},
897                elidable=True,
898                olderSibling=True,
899                labelNames={"fr": "Quelque part"},
900            )
901        ],
902    )
903
904
905def test_addVariableFontDescriptor():
906    ds = DesignSpaceDocument()
907
908    vf = ds.addVariableFontDescriptor(name="TestVF", filename="TestVF.ttf")
909
910    assert ds.variableFonts[0] is vf
911    assert_descriptors_equal(
912        [vf], [VariableFontDescriptor(name="TestVF", filename="TestVF.ttf")]
913    )
914