You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			3339 lines
		
	
	
		
			126 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			3339 lines
		
	
	
		
			126 KiB
		
	
	
	
		
			Python
		
	
"""
 | 
						|
    designSpaceDocument
 | 
						|
 | 
						|
    - Read and write designspace files
 | 
						|
"""
 | 
						|
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import collections
 | 
						|
import copy
 | 
						|
import itertools
 | 
						|
import math
 | 
						|
import os
 | 
						|
import posixpath
 | 
						|
from io import BytesIO, StringIO
 | 
						|
from textwrap import indent
 | 
						|
from typing import Any, Dict, List, MutableMapping, Optional, Tuple, Union, cast
 | 
						|
 | 
						|
from fontTools.misc import etree as ET
 | 
						|
from fontTools.misc import plistlib
 | 
						|
from fontTools.misc.loggingTools import LogMixin
 | 
						|
from fontTools.misc.textTools import tobytes, tostr
 | 
						|
 | 
						|
 | 
						|
__all__ = [
 | 
						|
    "AxisDescriptor",
 | 
						|
    "AxisLabelDescriptor",
 | 
						|
    "AxisMappingDescriptor",
 | 
						|
    "BaseDocReader",
 | 
						|
    "BaseDocWriter",
 | 
						|
    "DesignSpaceDocument",
 | 
						|
    "DesignSpaceDocumentError",
 | 
						|
    "DiscreteAxisDescriptor",
 | 
						|
    "InstanceDescriptor",
 | 
						|
    "LocationLabelDescriptor",
 | 
						|
    "RangeAxisSubsetDescriptor",
 | 
						|
    "RuleDescriptor",
 | 
						|
    "SourceDescriptor",
 | 
						|
    "ValueAxisSubsetDescriptor",
 | 
						|
    "VariableFontDescriptor",
 | 
						|
]
 | 
						|
 | 
						|
# ElementTree allows to find namespace-prefixed elements, but not attributes
 | 
						|
# so we have to do it ourselves for 'xml:lang'
 | 
						|
XML_NS = "{http://www.w3.org/XML/1998/namespace}"
 | 
						|
XML_LANG = XML_NS + "lang"
 | 
						|
 | 
						|
 | 
						|
def posix(path):
 | 
						|
    """Normalize paths using forward slash to work also on Windows."""
 | 
						|
    new_path = posixpath.join(*path.split(os.path.sep))
 | 
						|
    if path.startswith("/"):
 | 
						|
        # The above transformation loses absolute paths
 | 
						|
        new_path = "/" + new_path
 | 
						|
    elif path.startswith(r"\\"):
 | 
						|
        # The above transformation loses leading slashes of UNC path mounts
 | 
						|
        new_path = "//" + new_path
 | 
						|
    return new_path
 | 
						|
 | 
						|
 | 
						|
def posixpath_property(private_name):
 | 
						|
    """Generate a propery that holds a path always using forward slashes."""
 | 
						|
 | 
						|
    def getter(self):
 | 
						|
        # Normal getter
 | 
						|
        return getattr(self, private_name)
 | 
						|
 | 
						|
    def setter(self, value):
 | 
						|
        # The setter rewrites paths using forward slashes
 | 
						|
        if value is not None:
 | 
						|
            value = posix(value)
 | 
						|
        setattr(self, private_name, value)
 | 
						|
 | 
						|
    return property(getter, setter)
 | 
						|
 | 
						|
 | 
						|
class DesignSpaceDocumentError(Exception):
 | 
						|
    def __init__(self, msg, obj=None):
 | 
						|
        self.msg = msg
 | 
						|
        self.obj = obj
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return str(self.msg) + (": %r" % self.obj if self.obj is not None else "")
 | 
						|
 | 
						|
 | 
						|
class AsDictMixin(object):
 | 
						|
    def asdict(self):
 | 
						|
        d = {}
 | 
						|
        for attr, value in self.__dict__.items():
 | 
						|
            if attr.startswith("_"):
 | 
						|
                continue
 | 
						|
            if hasattr(value, "asdict"):
 | 
						|
                value = value.asdict()
 | 
						|
            elif isinstance(value, list):
 | 
						|
                value = [v.asdict() if hasattr(v, "asdict") else v for v in value]
 | 
						|
            d[attr] = value
 | 
						|
        return d
 | 
						|
 | 
						|
 | 
						|
class SimpleDescriptor(AsDictMixin):
 | 
						|
    """Containers for a bunch of attributes"""
 | 
						|
 | 
						|
    # XXX this is ugly. The 'print' is inappropriate here, and instead of
 | 
						|
    # assert, it should simply return True/False
 | 
						|
    def compare(self, other):
 | 
						|
        # test if this object contains the same data as the other
 | 
						|
        for attr in self._attrs:
 | 
						|
            try:
 | 
						|
                assert getattr(self, attr) == getattr(other, attr)
 | 
						|
            except AssertionError:
 | 
						|
                print(
 | 
						|
                    "failed attribute",
 | 
						|
                    attr,
 | 
						|
                    getattr(self, attr),
 | 
						|
                    "!=",
 | 
						|
                    getattr(other, attr),
 | 
						|
                )
 | 
						|
 | 
						|
    def __repr__(self):
 | 
						|
        attrs = [f"{a}={repr(getattr(self, a))}," for a in self._attrs]
 | 
						|
        attrs = indent("\n".join(attrs), "    ")
 | 
						|
        return f"{self.__class__.__name__}(\n{attrs}\n)"
 | 
						|
 | 
						|
 | 
						|
class SourceDescriptor(SimpleDescriptor):
 | 
						|
    """Simple container for data related to the source
 | 
						|
 | 
						|
    .. code:: python
 | 
						|
 | 
						|
        doc = DesignSpaceDocument()
 | 
						|
        s1 = SourceDescriptor()
 | 
						|
        s1.path = masterPath1
 | 
						|
        s1.name = "master.ufo1"
 | 
						|
        s1.font = defcon.Font("master.ufo1")
 | 
						|
        s1.location = dict(weight=0)
 | 
						|
        s1.familyName = "MasterFamilyName"
 | 
						|
        s1.styleName = "MasterStyleNameOne"
 | 
						|
        s1.localisedFamilyName = dict(fr="Caractère")
 | 
						|
        s1.mutedGlyphNames.append("A")
 | 
						|
        s1.mutedGlyphNames.append("Z")
 | 
						|
        doc.addSource(s1)
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    flavor = "source"
 | 
						|
    _attrs = [
 | 
						|
        "filename",
 | 
						|
        "path",
 | 
						|
        "name",
 | 
						|
        "layerName",
 | 
						|
        "location",
 | 
						|
        "copyLib",
 | 
						|
        "copyGroups",
 | 
						|
        "copyFeatures",
 | 
						|
        "muteKerning",
 | 
						|
        "muteInfo",
 | 
						|
        "mutedGlyphNames",
 | 
						|
        "familyName",
 | 
						|
        "styleName",
 | 
						|
        "localisedFamilyName",
 | 
						|
    ]
 | 
						|
 | 
						|
    filename = posixpath_property("_filename")
 | 
						|
    path = posixpath_property("_path")
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        *,
 | 
						|
        filename=None,
 | 
						|
        path=None,
 | 
						|
        font=None,
 | 
						|
        name=None,
 | 
						|
        location=None,
 | 
						|
        designLocation=None,
 | 
						|
        layerName=None,
 | 
						|
        familyName=None,
 | 
						|
        styleName=None,
 | 
						|
        localisedFamilyName=None,
 | 
						|
        copyLib=False,
 | 
						|
        copyInfo=False,
 | 
						|
        copyGroups=False,
 | 
						|
        copyFeatures=False,
 | 
						|
        muteKerning=False,
 | 
						|
        muteInfo=False,
 | 
						|
        mutedGlyphNames=None,
 | 
						|
    ):
 | 
						|
        self.filename = filename
 | 
						|
        """string. A relative path to the source file, **as it is in the document**.
 | 
						|
 | 
						|
        MutatorMath + VarLib.
 | 
						|
        """
 | 
						|
        self.path = path
 | 
						|
        """The absolute path, calculated from filename."""
 | 
						|
 | 
						|
        self.font = font
 | 
						|
        """Any Python object. Optional. Points to a representation of this
 | 
						|
        source font that is loaded in memory, as a Python object (e.g. a
 | 
						|
        ``defcon.Font`` or a ``fontTools.ttFont.TTFont``).
 | 
						|
 | 
						|
        The default document reader will not fill-in this attribute, and the
 | 
						|
        default writer will not use this attribute. It is up to the user of
 | 
						|
        ``designspaceLib`` to either load the resource identified by
 | 
						|
        ``filename`` and store it in this field, or write the contents of
 | 
						|
        this field to the disk and make ```filename`` point to that.
 | 
						|
        """
 | 
						|
 | 
						|
        self.name = name
 | 
						|
        """string. Optional. Unique identifier name for this source.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
        """
 | 
						|
 | 
						|
        self.designLocation = (
 | 
						|
            designLocation if designLocation is not None else location or {}
 | 
						|
        )
 | 
						|
        """dict. Axis values for this source, in design space coordinates.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
 | 
						|
        This may be only part of the full design location.
 | 
						|
        See :meth:`getFullDesignLocation()`
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
 | 
						|
        self.layerName = layerName
 | 
						|
        """string. The name of the layer in the source to look for
 | 
						|
        outline data. Default ``None`` which means ``foreground``.
 | 
						|
        """
 | 
						|
        self.familyName = familyName
 | 
						|
        """string. Family name of this source. Though this data
 | 
						|
        can be extracted from the font, it can be efficient to have it right
 | 
						|
        here.
 | 
						|
 | 
						|
        varLib.
 | 
						|
        """
 | 
						|
        self.styleName = styleName
 | 
						|
        """string. Style name of this source. Though this data
 | 
						|
        can be extracted from the font, it can be efficient to have it right
 | 
						|
        here.
 | 
						|
 | 
						|
        varLib.
 | 
						|
        """
 | 
						|
        self.localisedFamilyName = localisedFamilyName or {}
 | 
						|
        """dict. A dictionary of localised family name strings, keyed by
 | 
						|
        language code.
 | 
						|
 | 
						|
        If present, will be used to build localized names for all instances.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
 | 
						|
        self.copyLib = copyLib
 | 
						|
        """bool. Indicates if the contents of the font.lib need to
 | 
						|
        be copied to the instances.
 | 
						|
 | 
						|
        MutatorMath.
 | 
						|
 | 
						|
        .. deprecated:: 5.0
 | 
						|
        """
 | 
						|
        self.copyInfo = copyInfo
 | 
						|
        """bool. Indicates if the non-interpolating font.info needs
 | 
						|
        to be copied to the instances.
 | 
						|
 | 
						|
        MutatorMath.
 | 
						|
 | 
						|
        .. deprecated:: 5.0
 | 
						|
        """
 | 
						|
        self.copyGroups = copyGroups
 | 
						|
        """bool. Indicates if the groups need to be copied to the
 | 
						|
        instances.
 | 
						|
 | 
						|
        MutatorMath.
 | 
						|
 | 
						|
        .. deprecated:: 5.0
 | 
						|
        """
 | 
						|
        self.copyFeatures = copyFeatures
 | 
						|
        """bool. Indicates if the feature text needs to be
 | 
						|
        copied to the instances.
 | 
						|
 | 
						|
        MutatorMath.
 | 
						|
 | 
						|
        .. deprecated:: 5.0
 | 
						|
        """
 | 
						|
        self.muteKerning = muteKerning
 | 
						|
        """bool. Indicates if the kerning data from this source
 | 
						|
        needs to be muted (i.e. not be part of the calculations).
 | 
						|
 | 
						|
        MutatorMath only.
 | 
						|
        """
 | 
						|
        self.muteInfo = muteInfo
 | 
						|
        """bool. Indicated if the interpolating font.info data for
 | 
						|
        this source needs to be muted.
 | 
						|
 | 
						|
        MutatorMath only.
 | 
						|
        """
 | 
						|
        self.mutedGlyphNames = mutedGlyphNames or []
 | 
						|
        """list. Glyphnames that need to be muted in the
 | 
						|
        instances.
 | 
						|
 | 
						|
        MutatorMath only.
 | 
						|
        """
 | 
						|
 | 
						|
    @property
 | 
						|
    def location(self):
 | 
						|
        """dict. Axis values for this source, in design space coordinates.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
 | 
						|
        .. deprecated:: 5.0
 | 
						|
           Use the more explicit alias for this property :attr:`designLocation`.
 | 
						|
        """
 | 
						|
        return self.designLocation
 | 
						|
 | 
						|
    @location.setter
 | 
						|
    def location(self, location: Optional[SimpleLocationDict]):
 | 
						|
        self.designLocation = location or {}
 | 
						|
 | 
						|
    def setFamilyName(self, familyName, languageCode="en"):
 | 
						|
        """Setter for :attr:`localisedFamilyName`
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        self.localisedFamilyName[languageCode] = tostr(familyName)
 | 
						|
 | 
						|
    def getFamilyName(self, languageCode="en"):
 | 
						|
        """Getter for :attr:`localisedFamilyName`
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        return self.localisedFamilyName.get(languageCode)
 | 
						|
 | 
						|
    def getFullDesignLocation(self, doc: "DesignSpaceDocument") -> SimpleLocationDict:
 | 
						|
        """Get the complete design location of this source, from its
 | 
						|
        :attr:`designLocation` and the document's axis defaults.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        result: SimpleLocationDict = {}
 | 
						|
        for axis in doc.axes:
 | 
						|
            if axis.name in self.designLocation:
 | 
						|
                result[axis.name] = self.designLocation[axis.name]
 | 
						|
            else:
 | 
						|
                result[axis.name] = axis.map_forward(axis.default)
 | 
						|
        return result
 | 
						|
 | 
						|
 | 
						|
class RuleDescriptor(SimpleDescriptor):
 | 
						|
    """Represents the rule descriptor element: a set of glyph substitutions to
 | 
						|
    trigger conditionally in some parts of the designspace.
 | 
						|
 | 
						|
    .. code:: python
 | 
						|
 | 
						|
        r1 = RuleDescriptor()
 | 
						|
        r1.name = "unique.rule.name"
 | 
						|
        r1.conditionSets.append([dict(name="weight", minimum=-10, maximum=10), dict(...)])
 | 
						|
        r1.conditionSets.append([dict(...), dict(...)])
 | 
						|
        r1.subs.append(("a", "a.alt"))
 | 
						|
 | 
						|
    .. code:: xml
 | 
						|
 | 
						|
        <!-- optional: list of substitution rules -->
 | 
						|
        <rules>
 | 
						|
            <rule name="vertical.bars">
 | 
						|
                <conditionset>
 | 
						|
                    <condition minimum="250.000000" maximum="750.000000" name="weight"/>
 | 
						|
                    <condition minimum="100" name="width"/>
 | 
						|
                    <condition minimum="10" maximum="40" name="optical"/>
 | 
						|
                </conditionset>
 | 
						|
                <sub name="cent" with="cent.alt"/>
 | 
						|
                <sub name="dollar" with="dollar.alt"/>
 | 
						|
            </rule>
 | 
						|
        </rules>
 | 
						|
    """
 | 
						|
 | 
						|
    _attrs = ["name", "conditionSets", "subs"]  # what do we need here
 | 
						|
 | 
						|
    def __init__(self, *, name=None, conditionSets=None, subs=None):
 | 
						|
        self.name = name
 | 
						|
        """string. Unique name for this rule. Can be used to reference this rule data."""
 | 
						|
        # list of lists of dict(name='aaaa', minimum=0, maximum=1000)
 | 
						|
        self.conditionSets = conditionSets or []
 | 
						|
        """a list of conditionsets.
 | 
						|
 | 
						|
        -  Each conditionset is a list of conditions.
 | 
						|
        -  Each condition is a dict with ``name``, ``minimum`` and ``maximum`` keys.
 | 
						|
        """
 | 
						|
        # list of substitutions stored as tuples of glyphnames ("a", "a.alt")
 | 
						|
        self.subs = subs or []
 | 
						|
        """list of substitutions.
 | 
						|
 | 
						|
        -  Each substitution is stored as tuples of glyphnames, e.g. ("a", "a.alt").
 | 
						|
        -  Note: By default, rules are applied first, before other text
 | 
						|
           shaping/OpenType layout, as they are part of the
 | 
						|
           `Required Variation Alternates OpenType feature <https://docs.microsoft.com/en-us/typography/opentype/spec/features_pt#-tag-rvrn>`_.
 | 
						|
           See ref:`rules-element` § Attributes.
 | 
						|
        """
 | 
						|
 | 
						|
 | 
						|
def evaluateRule(rule, location):
 | 
						|
    """Return True if any of the rule's conditionsets matches the given location."""
 | 
						|
    return any(evaluateConditions(c, location) for c in rule.conditionSets)
 | 
						|
 | 
						|
 | 
						|
def evaluateConditions(conditions, location):
 | 
						|
    """Return True if all the conditions matches the given location.
 | 
						|
 | 
						|
    - If a condition has no minimum, check for < maximum.
 | 
						|
    - If a condition has no maximum, check for > minimum.
 | 
						|
    """
 | 
						|
    for cd in conditions:
 | 
						|
        value = location[cd["name"]]
 | 
						|
        if cd.get("minimum") is None:
 | 
						|
            if value > cd["maximum"]:
 | 
						|
                return False
 | 
						|
        elif cd.get("maximum") is None:
 | 
						|
            if cd["minimum"] > value:
 | 
						|
                return False
 | 
						|
        elif not cd["minimum"] <= value <= cd["maximum"]:
 | 
						|
            return False
 | 
						|
    return True
 | 
						|
 | 
						|
 | 
						|
def processRules(rules, location, glyphNames):
 | 
						|
    """Apply these rules at this location to these glyphnames.
 | 
						|
 | 
						|
    Return a new list of glyphNames with substitutions applied.
 | 
						|
 | 
						|
    - rule order matters
 | 
						|
    """
 | 
						|
    newNames = []
 | 
						|
    for rule in rules:
 | 
						|
        if evaluateRule(rule, location):
 | 
						|
            for name in glyphNames:
 | 
						|
                swap = False
 | 
						|
                for a, b in rule.subs:
 | 
						|
                    if name == a:
 | 
						|
                        swap = True
 | 
						|
                        break
 | 
						|
                if swap:
 | 
						|
                    newNames.append(b)
 | 
						|
                else:
 | 
						|
                    newNames.append(name)
 | 
						|
            glyphNames = newNames
 | 
						|
            newNames = []
 | 
						|
    return glyphNames
 | 
						|
 | 
						|
 | 
						|
AnisotropicLocationDict = Dict[str, Union[float, Tuple[float, float]]]
 | 
						|
SimpleLocationDict = Dict[str, float]
 | 
						|
 | 
						|
 | 
						|
class AxisMappingDescriptor(SimpleDescriptor):
 | 
						|
    """Represents the axis mapping element: mapping an input location
 | 
						|
    to an output location in the designspace.
 | 
						|
 | 
						|
    .. code:: python
 | 
						|
 | 
						|
        m1 = AxisMappingDescriptor()
 | 
						|
        m1.inputLocation = {"weight": 900, "width": 150}
 | 
						|
        m1.outputLocation = {"weight": 870}
 | 
						|
 | 
						|
    .. code:: xml
 | 
						|
 | 
						|
        <mappings>
 | 
						|
            <mapping>
 | 
						|
                <input>
 | 
						|
                    <dimension name="weight" xvalue="900"/>
 | 
						|
                    <dimension name="width" xvalue="150"/>
 | 
						|
                </input>
 | 
						|
                <output>
 | 
						|
                    <dimension name="weight" xvalue="870"/>
 | 
						|
                </output>
 | 
						|
            </mapping>
 | 
						|
        </mappings>
 | 
						|
    """
 | 
						|
 | 
						|
    _attrs = ["inputLocation", "outputLocation"]
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        *,
 | 
						|
        inputLocation=None,
 | 
						|
        outputLocation=None,
 | 
						|
        description=None,
 | 
						|
        groupDescription=None,
 | 
						|
    ):
 | 
						|
        self.inputLocation: SimpleLocationDict = inputLocation or {}
 | 
						|
        """dict. Axis values for the input of the mapping, in design space coordinates.
 | 
						|
 | 
						|
        varLib.
 | 
						|
 | 
						|
        .. versionadded:: 5.1
 | 
						|
        """
 | 
						|
        self.outputLocation: SimpleLocationDict = outputLocation or {}
 | 
						|
        """dict. Axis values for the output of the mapping, in design space coordinates.
 | 
						|
 | 
						|
        varLib.
 | 
						|
 | 
						|
        .. versionadded:: 5.1
 | 
						|
        """
 | 
						|
        self.description = description
 | 
						|
        """string. A description of the mapping.
 | 
						|
 | 
						|
        varLib.
 | 
						|
 | 
						|
        .. versionadded:: 5.2
 | 
						|
        """
 | 
						|
        self.groupDescription = groupDescription
 | 
						|
        """string. A description of the group of mappings.
 | 
						|
 | 
						|
        varLib.
 | 
						|
 | 
						|
        .. versionadded:: 5.2
 | 
						|
        """
 | 
						|
 | 
						|
 | 
						|
class InstanceDescriptor(SimpleDescriptor):
 | 
						|
    """Simple container for data related to the instance
 | 
						|
 | 
						|
 | 
						|
    .. code:: python
 | 
						|
 | 
						|
        i2 = InstanceDescriptor()
 | 
						|
        i2.path = instancePath2
 | 
						|
        i2.familyName = "InstanceFamilyName"
 | 
						|
        i2.styleName = "InstanceStyleName"
 | 
						|
        i2.name = "instance.ufo2"
 | 
						|
        # anisotropic location
 | 
						|
        i2.designLocation = dict(weight=500, width=(400,300))
 | 
						|
        i2.postScriptFontName = "InstancePostscriptName"
 | 
						|
        i2.styleMapFamilyName = "InstanceStyleMapFamilyName"
 | 
						|
        i2.styleMapStyleName = "InstanceStyleMapStyleName"
 | 
						|
        i2.lib['com.coolDesignspaceApp.specimenText'] = 'Hamburgerwhatever'
 | 
						|
        doc.addInstance(i2)
 | 
						|
    """
 | 
						|
 | 
						|
    flavor = "instance"
 | 
						|
    _defaultLanguageCode = "en"
 | 
						|
    _attrs = [
 | 
						|
        "filename",
 | 
						|
        "path",
 | 
						|
        "name",
 | 
						|
        "locationLabel",
 | 
						|
        "designLocation",
 | 
						|
        "userLocation",
 | 
						|
        "familyName",
 | 
						|
        "styleName",
 | 
						|
        "postScriptFontName",
 | 
						|
        "styleMapFamilyName",
 | 
						|
        "styleMapStyleName",
 | 
						|
        "localisedFamilyName",
 | 
						|
        "localisedStyleName",
 | 
						|
        "localisedStyleMapFamilyName",
 | 
						|
        "localisedStyleMapStyleName",
 | 
						|
        "glyphs",
 | 
						|
        "kerning",
 | 
						|
        "info",
 | 
						|
        "lib",
 | 
						|
    ]
 | 
						|
 | 
						|
    filename = posixpath_property("_filename")
 | 
						|
    path = posixpath_property("_path")
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        *,
 | 
						|
        filename=None,
 | 
						|
        path=None,
 | 
						|
        font=None,
 | 
						|
        name=None,
 | 
						|
        location=None,
 | 
						|
        locationLabel=None,
 | 
						|
        designLocation=None,
 | 
						|
        userLocation=None,
 | 
						|
        familyName=None,
 | 
						|
        styleName=None,
 | 
						|
        postScriptFontName=None,
 | 
						|
        styleMapFamilyName=None,
 | 
						|
        styleMapStyleName=None,
 | 
						|
        localisedFamilyName=None,
 | 
						|
        localisedStyleName=None,
 | 
						|
        localisedStyleMapFamilyName=None,
 | 
						|
        localisedStyleMapStyleName=None,
 | 
						|
        glyphs=None,
 | 
						|
        kerning=True,
 | 
						|
        info=True,
 | 
						|
        lib=None,
 | 
						|
    ):
 | 
						|
        self.filename = filename
 | 
						|
        """string. Relative path to the instance file, **as it is
 | 
						|
        in the document**. The file may or may not exist.
 | 
						|
 | 
						|
        MutatorMath + VarLib.
 | 
						|
        """
 | 
						|
        self.path = path
 | 
						|
        """string. Absolute path to the instance file, calculated from
 | 
						|
        the document path and the string in the filename attr. The file may
 | 
						|
        or may not exist.
 | 
						|
 | 
						|
        MutatorMath.
 | 
						|
        """
 | 
						|
        self.font = font
 | 
						|
        """Same as :attr:`SourceDescriptor.font`
 | 
						|
 | 
						|
        .. seealso:: :attr:`SourceDescriptor.font`
 | 
						|
        """
 | 
						|
        self.name = name
 | 
						|
        """string. Unique identifier name of the instance, used to
 | 
						|
        identify it if it needs to be referenced from elsewhere in the
 | 
						|
        document.
 | 
						|
        """
 | 
						|
        self.locationLabel = locationLabel
 | 
						|
        """Name of a :class:`LocationLabelDescriptor`. If
 | 
						|
        provided, the instance should have the same location as the
 | 
						|
        LocationLabel.
 | 
						|
 | 
						|
        .. seealso::
 | 
						|
           :meth:`getFullDesignLocation`
 | 
						|
           :meth:`getFullUserLocation`
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        self.designLocation: AnisotropicLocationDict = (
 | 
						|
            designLocation if designLocation is not None else (location or {})
 | 
						|
        )
 | 
						|
        """dict. Axis values for this instance, in design space coordinates.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
 | 
						|
        .. seealso:: This may be only part of the full location. See:
 | 
						|
           :meth:`getFullDesignLocation`
 | 
						|
           :meth:`getFullUserLocation`
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        self.userLocation: SimpleLocationDict = userLocation or {}
 | 
						|
        """dict. Axis values for this instance, in user space coordinates.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
 | 
						|
        .. seealso:: This may be only part of the full location. See:
 | 
						|
           :meth:`getFullDesignLocation`
 | 
						|
           :meth:`getFullUserLocation`
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        self.familyName = familyName
 | 
						|
        """string. Family name of this instance.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
        """
 | 
						|
        self.styleName = styleName
 | 
						|
        """string. Style name of this instance.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
        """
 | 
						|
        self.postScriptFontName = postScriptFontName
 | 
						|
        """string. Postscript fontname for this instance.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
        """
 | 
						|
        self.styleMapFamilyName = styleMapFamilyName
 | 
						|
        """string. StyleMap familyname for this instance.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
        """
 | 
						|
        self.styleMapStyleName = styleMapStyleName
 | 
						|
        """string. StyleMap stylename for this instance.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
        """
 | 
						|
        self.localisedFamilyName = localisedFamilyName or {}
 | 
						|
        """dict. A dictionary of localised family name
 | 
						|
        strings, keyed by language code.
 | 
						|
        """
 | 
						|
        self.localisedStyleName = localisedStyleName or {}
 | 
						|
        """dict. A dictionary of localised stylename
 | 
						|
        strings, keyed by language code.
 | 
						|
        """
 | 
						|
        self.localisedStyleMapFamilyName = localisedStyleMapFamilyName or {}
 | 
						|
        """A dictionary of localised style map
 | 
						|
        familyname strings, keyed by language code.
 | 
						|
        """
 | 
						|
        self.localisedStyleMapStyleName = localisedStyleMapStyleName or {}
 | 
						|
        """A dictionary of localised style map
 | 
						|
        stylename strings, keyed by language code.
 | 
						|
        """
 | 
						|
        self.glyphs = glyphs or {}
 | 
						|
        """dict for special master definitions for glyphs. If glyphs
 | 
						|
        need special masters (to record the results of executed rules for
 | 
						|
        example).
 | 
						|
 | 
						|
        MutatorMath.
 | 
						|
 | 
						|
        .. deprecated:: 5.0
 | 
						|
            Use rules or sparse sources instead.
 | 
						|
        """
 | 
						|
        self.kerning = kerning
 | 
						|
        """ bool. Indicates if this instance needs its kerning
 | 
						|
        calculated.
 | 
						|
 | 
						|
        MutatorMath.
 | 
						|
 | 
						|
        .. deprecated:: 5.0
 | 
						|
        """
 | 
						|
        self.info = info
 | 
						|
        """bool. Indicated if this instance needs the interpolating
 | 
						|
        font.info calculated.
 | 
						|
 | 
						|
        .. deprecated:: 5.0
 | 
						|
        """
 | 
						|
 | 
						|
        self.lib = lib or {}
 | 
						|
        """Custom data associated with this instance."""
 | 
						|
 | 
						|
    @property
 | 
						|
    def location(self):
 | 
						|
        """dict. Axis values for this instance.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
 | 
						|
        .. deprecated:: 5.0
 | 
						|
           Use the more explicit alias for this property :attr:`designLocation`.
 | 
						|
        """
 | 
						|
        return self.designLocation
 | 
						|
 | 
						|
    @location.setter
 | 
						|
    def location(self, location: Optional[AnisotropicLocationDict]):
 | 
						|
        self.designLocation = location or {}
 | 
						|
 | 
						|
    def setStyleName(self, styleName, languageCode="en"):
 | 
						|
        """These methods give easier access to the localised names."""
 | 
						|
        self.localisedStyleName[languageCode] = tostr(styleName)
 | 
						|
 | 
						|
    def getStyleName(self, languageCode="en"):
 | 
						|
        return self.localisedStyleName.get(languageCode)
 | 
						|
 | 
						|
    def setFamilyName(self, familyName, languageCode="en"):
 | 
						|
        self.localisedFamilyName[languageCode] = tostr(familyName)
 | 
						|
 | 
						|
    def getFamilyName(self, languageCode="en"):
 | 
						|
        return self.localisedFamilyName.get(languageCode)
 | 
						|
 | 
						|
    def setStyleMapStyleName(self, styleMapStyleName, languageCode="en"):
 | 
						|
        self.localisedStyleMapStyleName[languageCode] = tostr(styleMapStyleName)
 | 
						|
 | 
						|
    def getStyleMapStyleName(self, languageCode="en"):
 | 
						|
        return self.localisedStyleMapStyleName.get(languageCode)
 | 
						|
 | 
						|
    def setStyleMapFamilyName(self, styleMapFamilyName, languageCode="en"):
 | 
						|
        self.localisedStyleMapFamilyName[languageCode] = tostr(styleMapFamilyName)
 | 
						|
 | 
						|
    def getStyleMapFamilyName(self, languageCode="en"):
 | 
						|
        return self.localisedStyleMapFamilyName.get(languageCode)
 | 
						|
 | 
						|
    def clearLocation(self, axisName: Optional[str] = None):
 | 
						|
        """Clear all location-related fields. Ensures that
 | 
						|
        :attr:``designLocation`` and :attr:``userLocation`` are dictionaries
 | 
						|
        (possibly empty if clearing everything).
 | 
						|
 | 
						|
        In order to update the location of this instance wholesale, a user
 | 
						|
        should first clear all the fields, then change the field(s) for which
 | 
						|
        they have data.
 | 
						|
 | 
						|
        .. code:: python
 | 
						|
 | 
						|
            instance.clearLocation()
 | 
						|
            instance.designLocation = {'Weight': (34, 36.5), 'Width': 100}
 | 
						|
            instance.userLocation = {'Opsz': 16}
 | 
						|
 | 
						|
        In order to update a single axis location, the user should only clear
 | 
						|
        that axis, then edit the values:
 | 
						|
 | 
						|
        .. code:: python
 | 
						|
 | 
						|
            instance.clearLocation('Weight')
 | 
						|
            instance.designLocation['Weight'] = (34, 36.5)
 | 
						|
 | 
						|
        Args:
 | 
						|
          axisName: if provided, only clear the location for that axis.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        self.locationLabel = None
 | 
						|
        if axisName is None:
 | 
						|
            self.designLocation = {}
 | 
						|
            self.userLocation = {}
 | 
						|
        else:
 | 
						|
            if self.designLocation is None:
 | 
						|
                self.designLocation = {}
 | 
						|
            if axisName in self.designLocation:
 | 
						|
                del self.designLocation[axisName]
 | 
						|
            if self.userLocation is None:
 | 
						|
                self.userLocation = {}
 | 
						|
            if axisName in self.userLocation:
 | 
						|
                del self.userLocation[axisName]
 | 
						|
 | 
						|
    def getLocationLabelDescriptor(
 | 
						|
        self, doc: "DesignSpaceDocument"
 | 
						|
    ) -> Optional[LocationLabelDescriptor]:
 | 
						|
        """Get the :class:`LocationLabelDescriptor` instance that matches
 | 
						|
        this instances's :attr:`locationLabel`.
 | 
						|
 | 
						|
        Raises if the named label can't be found.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        if self.locationLabel is None:
 | 
						|
            return None
 | 
						|
        label = doc.getLocationLabel(self.locationLabel)
 | 
						|
        if label is None:
 | 
						|
            raise DesignSpaceDocumentError(
 | 
						|
                "InstanceDescriptor.getLocationLabelDescriptor(): "
 | 
						|
                f"unknown location label `{self.locationLabel}` in instance `{self.name}`."
 | 
						|
            )
 | 
						|
        return label
 | 
						|
 | 
						|
    def getFullDesignLocation(
 | 
						|
        self, doc: "DesignSpaceDocument"
 | 
						|
    ) -> AnisotropicLocationDict:
 | 
						|
        """Get the complete design location of this instance, by combining data
 | 
						|
        from the various location fields, default axis values and mappings, and
 | 
						|
        top-level location labels.
 | 
						|
 | 
						|
        The source of truth for this instance's location is determined for each
 | 
						|
        axis independently by taking the first not-None field in this list:
 | 
						|
 | 
						|
        - ``locationLabel``: the location along this axis is the same as the
 | 
						|
          matching STAT format 4 label. No anisotropy.
 | 
						|
        - ``designLocation[axisName]``: the explicit design location along this
 | 
						|
          axis, possibly anisotropic.
 | 
						|
        - ``userLocation[axisName]``: the explicit user location along this
 | 
						|
          axis. No anisotropy.
 | 
						|
        - ``axis.default``: default axis value. No anisotropy.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        label = self.getLocationLabelDescriptor(doc)
 | 
						|
        if label is not None:
 | 
						|
            return doc.map_forward(label.userLocation)  # type: ignore
 | 
						|
        result: AnisotropicLocationDict = {}
 | 
						|
        for axis in doc.axes:
 | 
						|
            if axis.name in self.designLocation:
 | 
						|
                result[axis.name] = self.designLocation[axis.name]
 | 
						|
            elif axis.name in self.userLocation:
 | 
						|
                result[axis.name] = axis.map_forward(self.userLocation[axis.name])
 | 
						|
            else:
 | 
						|
                result[axis.name] = axis.map_forward(axis.default)
 | 
						|
        return result
 | 
						|
 | 
						|
    def getFullUserLocation(self, doc: "DesignSpaceDocument") -> SimpleLocationDict:
 | 
						|
        """Get the complete user location for this instance.
 | 
						|
 | 
						|
        .. seealso:: :meth:`getFullDesignLocation`
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        return doc.map_backward(self.getFullDesignLocation(doc))
 | 
						|
 | 
						|
 | 
						|
def tagForAxisName(name):
 | 
						|
    # try to find or make a tag name for this axis name
 | 
						|
    names = {
 | 
						|
        "weight": ("wght", dict(en="Weight")),
 | 
						|
        "width": ("wdth", dict(en="Width")),
 | 
						|
        "optical": ("opsz", dict(en="Optical Size")),
 | 
						|
        "slant": ("slnt", dict(en="Slant")),
 | 
						|
        "italic": ("ital", dict(en="Italic")),
 | 
						|
    }
 | 
						|
    if name.lower() in names:
 | 
						|
        return names[name.lower()]
 | 
						|
    if len(name) < 4:
 | 
						|
        tag = name + "*" * (4 - len(name))
 | 
						|
    else:
 | 
						|
        tag = name[:4]
 | 
						|
    return tag, dict(en=name)
 | 
						|
 | 
						|
 | 
						|
class AbstractAxisDescriptor(SimpleDescriptor):
 | 
						|
    flavor = "axis"
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        *,
 | 
						|
        tag=None,
 | 
						|
        name=None,
 | 
						|
        labelNames=None,
 | 
						|
        hidden=False,
 | 
						|
        map=None,
 | 
						|
        axisOrdering=None,
 | 
						|
        axisLabels=None,
 | 
						|
    ):
 | 
						|
        # opentype tag for this axis
 | 
						|
        self.tag = tag
 | 
						|
        """string. Four letter tag for this axis. Some might be
 | 
						|
        registered at the `OpenType
 | 
						|
        specification <https://www.microsoft.com/typography/otspec/fvar.htm#VAT>`__.
 | 
						|
        Privately-defined axis tags must begin with an uppercase letter and
 | 
						|
        use only uppercase letters or digits.
 | 
						|
        """
 | 
						|
        # name of the axis used in locations
 | 
						|
        self.name = name
 | 
						|
        """string. Name of the axis as it is used in the location dicts.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
        """
 | 
						|
        # names for UI purposes, if this is not a standard axis,
 | 
						|
        self.labelNames = labelNames or {}
 | 
						|
        """dict. When defining a non-registered axis, it will be
 | 
						|
        necessary to define user-facing readable names for the axis. Keyed by
 | 
						|
        xml:lang code. Values are required to be ``unicode`` strings, even if
 | 
						|
        they only contain ASCII characters.
 | 
						|
        """
 | 
						|
        self.hidden = hidden
 | 
						|
        """bool. Whether this axis should be hidden in user interfaces.
 | 
						|
        """
 | 
						|
        self.map = map or []
 | 
						|
        """list of input / output values that can describe a warp of user space
 | 
						|
        to design space coordinates. If no map values are present, it is assumed
 | 
						|
        user space is the same as design space, as in [(minimum, minimum),
 | 
						|
        (maximum, maximum)].
 | 
						|
 | 
						|
        varLib.
 | 
						|
        """
 | 
						|
        self.axisOrdering = axisOrdering
 | 
						|
        """STAT table field ``axisOrdering``.
 | 
						|
 | 
						|
        See: `OTSpec STAT Axis Record <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-records>`_
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        self.axisLabels: List[AxisLabelDescriptor] = axisLabels or []
 | 
						|
        """STAT table entries for Axis Value Tables format 1, 2, 3.
 | 
						|
 | 
						|
        See: `OTSpec STAT Axis Value Tables <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-tables>`_
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
 | 
						|
 | 
						|
class AxisDescriptor(AbstractAxisDescriptor):
 | 
						|
    """Simple container for the axis data.
 | 
						|
 | 
						|
    Add more localisations?
 | 
						|
 | 
						|
    .. code:: python
 | 
						|
 | 
						|
        a1 = AxisDescriptor()
 | 
						|
        a1.minimum = 1
 | 
						|
        a1.maximum = 1000
 | 
						|
        a1.default = 400
 | 
						|
        a1.name = "weight"
 | 
						|
        a1.tag = "wght"
 | 
						|
        a1.labelNames['fa-IR'] = "قطر"
 | 
						|
        a1.labelNames['en'] = "Wéíght"
 | 
						|
        a1.map = [(1.0, 10.0), (400.0, 66.0), (1000.0, 990.0)]
 | 
						|
        a1.axisOrdering = 1
 | 
						|
        a1.axisLabels = [
 | 
						|
            AxisLabelDescriptor(name="Regular", userValue=400, elidable=True)
 | 
						|
        ]
 | 
						|
        doc.addAxis(a1)
 | 
						|
    """
 | 
						|
 | 
						|
    _attrs = [
 | 
						|
        "tag",
 | 
						|
        "name",
 | 
						|
        "maximum",
 | 
						|
        "minimum",
 | 
						|
        "default",
 | 
						|
        "map",
 | 
						|
        "axisOrdering",
 | 
						|
        "axisLabels",
 | 
						|
    ]
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        *,
 | 
						|
        tag=None,
 | 
						|
        name=None,
 | 
						|
        labelNames=None,
 | 
						|
        minimum=None,
 | 
						|
        default=None,
 | 
						|
        maximum=None,
 | 
						|
        hidden=False,
 | 
						|
        map=None,
 | 
						|
        axisOrdering=None,
 | 
						|
        axisLabels=None,
 | 
						|
    ):
 | 
						|
        super().__init__(
 | 
						|
            tag=tag,
 | 
						|
            name=name,
 | 
						|
            labelNames=labelNames,
 | 
						|
            hidden=hidden,
 | 
						|
            map=map,
 | 
						|
            axisOrdering=axisOrdering,
 | 
						|
            axisLabels=axisLabels,
 | 
						|
        )
 | 
						|
        self.minimum = minimum
 | 
						|
        """number. The minimum value for this axis in user space.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
        """
 | 
						|
        self.maximum = maximum
 | 
						|
        """number. The maximum value for this axis in user space.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
        """
 | 
						|
        self.default = default
 | 
						|
        """number. The default value for this axis, i.e. when a new location is
 | 
						|
        created, this is the value this axis will get in user space.
 | 
						|
 | 
						|
        MutatorMath + varLib.
 | 
						|
        """
 | 
						|
 | 
						|
    def serialize(self):
 | 
						|
        # output to a dict, used in testing
 | 
						|
        return dict(
 | 
						|
            tag=self.tag,
 | 
						|
            name=self.name,
 | 
						|
            labelNames=self.labelNames,
 | 
						|
            maximum=self.maximum,
 | 
						|
            minimum=self.minimum,
 | 
						|
            default=self.default,
 | 
						|
            hidden=self.hidden,
 | 
						|
            map=self.map,
 | 
						|
            axisOrdering=self.axisOrdering,
 | 
						|
            axisLabels=self.axisLabels,
 | 
						|
        )
 | 
						|
 | 
						|
    def map_forward(self, v):
 | 
						|
        """Maps value from axis mapping's input (user) to output (design)."""
 | 
						|
        from fontTools.varLib.models import piecewiseLinearMap
 | 
						|
 | 
						|
        if not self.map:
 | 
						|
            return v
 | 
						|
        return piecewiseLinearMap(v, {k: v for k, v in self.map})
 | 
						|
 | 
						|
    def map_backward(self, v):
 | 
						|
        """Maps value from axis mapping's output (design) to input (user)."""
 | 
						|
        from fontTools.varLib.models import piecewiseLinearMap
 | 
						|
 | 
						|
        if isinstance(v, tuple):
 | 
						|
            v = v[0]
 | 
						|
        if not self.map:
 | 
						|
            return v
 | 
						|
        return piecewiseLinearMap(v, {v: k for k, v in self.map})
 | 
						|
 | 
						|
 | 
						|
class DiscreteAxisDescriptor(AbstractAxisDescriptor):
 | 
						|
    """Container for discrete axis data.
 | 
						|
 | 
						|
    Use this for axes that do not interpolate. The main difference from a
 | 
						|
    continuous axis is that a continuous axis has a ``minimum`` and ``maximum``,
 | 
						|
    while a discrete axis has a list of ``values``.
 | 
						|
 | 
						|
    Example: an Italic axis with 2 stops, Roman and Italic, that are not
 | 
						|
    compatible. The axis still allows to bind together the full font family,
 | 
						|
    which is useful for the STAT table, however it can't become a variation
 | 
						|
    axis in a VF.
 | 
						|
 | 
						|
    .. code:: python
 | 
						|
 | 
						|
        a2 = DiscreteAxisDescriptor()
 | 
						|
        a2.values = [0, 1]
 | 
						|
        a2.default = 0
 | 
						|
        a2.name = "Italic"
 | 
						|
        a2.tag = "ITAL"
 | 
						|
        a2.labelNames['fr'] = "Italique"
 | 
						|
        a2.map = [(0, 0), (1, -11)]
 | 
						|
        a2.axisOrdering = 2
 | 
						|
        a2.axisLabels = [
 | 
						|
            AxisLabelDescriptor(name="Roman", userValue=0, elidable=True)
 | 
						|
        ]
 | 
						|
        doc.addAxis(a2)
 | 
						|
 | 
						|
    .. versionadded:: 5.0
 | 
						|
    """
 | 
						|
 | 
						|
    flavor = "axis"
 | 
						|
    _attrs = ("tag", "name", "values", "default", "map", "axisOrdering", "axisLabels")
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        *,
 | 
						|
        tag=None,
 | 
						|
        name=None,
 | 
						|
        labelNames=None,
 | 
						|
        values=None,
 | 
						|
        default=None,
 | 
						|
        hidden=False,
 | 
						|
        map=None,
 | 
						|
        axisOrdering=None,
 | 
						|
        axisLabels=None,
 | 
						|
    ):
 | 
						|
        super().__init__(
 | 
						|
            tag=tag,
 | 
						|
            name=name,
 | 
						|
            labelNames=labelNames,
 | 
						|
            hidden=hidden,
 | 
						|
            map=map,
 | 
						|
            axisOrdering=axisOrdering,
 | 
						|
            axisLabels=axisLabels,
 | 
						|
        )
 | 
						|
        self.default: float = default
 | 
						|
        """The default value for this axis, i.e. when a new location is
 | 
						|
        created, this is the value this axis will get in user space.
 | 
						|
 | 
						|
        However, this default value is less important than in continuous axes:
 | 
						|
 | 
						|
        -  it doesn't define the "neutral" version of outlines from which
 | 
						|
           deltas would apply, as this axis does not interpolate.
 | 
						|
        -  it doesn't provide the reference glyph set for the designspace, as
 | 
						|
           fonts at each value can have different glyph sets.
 | 
						|
        """
 | 
						|
        self.values: List[float] = values or []
 | 
						|
        """List of possible values for this axis. Contrary to continuous axes,
 | 
						|
        only the values in this list can be taken by the axis, nothing in-between.
 | 
						|
        """
 | 
						|
 | 
						|
    def map_forward(self, value):
 | 
						|
        """Maps value from axis mapping's input to output.
 | 
						|
 | 
						|
        Returns value unchanged if no mapping entry is found.
 | 
						|
 | 
						|
        Note: for discrete axes, each value must have its mapping entry, if
 | 
						|
        you intend that value to be mapped.
 | 
						|
        """
 | 
						|
        return next((v for k, v in self.map if k == value), value)
 | 
						|
 | 
						|
    def map_backward(self, value):
 | 
						|
        """Maps value from axis mapping's output to input.
 | 
						|
 | 
						|
        Returns value unchanged if no mapping entry is found.
 | 
						|
 | 
						|
        Note: for discrete axes, each value must have its mapping entry, if
 | 
						|
        you intend that value to be mapped.
 | 
						|
        """
 | 
						|
        if isinstance(value, tuple):
 | 
						|
            value = value[0]
 | 
						|
        return next((k for k, v in self.map if v == value), value)
 | 
						|
 | 
						|
 | 
						|
class AxisLabelDescriptor(SimpleDescriptor):
 | 
						|
    """Container for axis label data.
 | 
						|
 | 
						|
    Analogue of OpenType's STAT data for a single axis (formats 1, 2 and 3).
 | 
						|
    All values are user values.
 | 
						|
    See: `OTSpec STAT Axis value table, format 1, 2, 3 <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-1>`_
 | 
						|
 | 
						|
    The STAT format of the Axis value depends on which field are filled-in,
 | 
						|
    see :meth:`getFormat`
 | 
						|
 | 
						|
    .. versionadded:: 5.0
 | 
						|
    """
 | 
						|
 | 
						|
    flavor = "label"
 | 
						|
    _attrs = (
 | 
						|
        "userMinimum",
 | 
						|
        "userValue",
 | 
						|
        "userMaximum",
 | 
						|
        "name",
 | 
						|
        "elidable",
 | 
						|
        "olderSibling",
 | 
						|
        "linkedUserValue",
 | 
						|
        "labelNames",
 | 
						|
    )
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        *,
 | 
						|
        name,
 | 
						|
        userValue,
 | 
						|
        userMinimum=None,
 | 
						|
        userMaximum=None,
 | 
						|
        elidable=False,
 | 
						|
        olderSibling=False,
 | 
						|
        linkedUserValue=None,
 | 
						|
        labelNames=None,
 | 
						|
    ):
 | 
						|
        self.userMinimum: Optional[float] = userMinimum
 | 
						|
        """STAT field ``rangeMinValue`` (format 2)."""
 | 
						|
        self.userValue: float = userValue
 | 
						|
        """STAT field ``value`` (format 1, 3) or ``nominalValue`` (format 2)."""
 | 
						|
        self.userMaximum: Optional[float] = userMaximum
 | 
						|
        """STAT field ``rangeMaxValue`` (format 2)."""
 | 
						|
        self.name: str = name
 | 
						|
        """Label for this axis location, STAT field ``valueNameID``."""
 | 
						|
        self.elidable: bool = elidable
 | 
						|
        """STAT flag ``ELIDABLE_AXIS_VALUE_NAME``.
 | 
						|
 | 
						|
        See: `OTSpec STAT Flags <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#flags>`_
 | 
						|
        """
 | 
						|
        self.olderSibling: bool = olderSibling
 | 
						|
        """STAT flag ``OLDER_SIBLING_FONT_ATTRIBUTE``.
 | 
						|
 | 
						|
        See: `OTSpec STAT Flags <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#flags>`_
 | 
						|
        """
 | 
						|
        self.linkedUserValue: Optional[float] = linkedUserValue
 | 
						|
        """STAT field ``linkedValue`` (format 3)."""
 | 
						|
        self.labelNames: MutableMapping[str, str] = labelNames or {}
 | 
						|
        """User-facing translations of this location's label. Keyed by
 | 
						|
        ``xml:lang`` code.
 | 
						|
        """
 | 
						|
 | 
						|
    def getFormat(self) -> int:
 | 
						|
        """Determine which format of STAT Axis value to use to encode this label.
 | 
						|
 | 
						|
        ===========  =========  ===========  ===========  ===============
 | 
						|
        STAT Format  userValue  userMinimum  userMaximum  linkedUserValue
 | 
						|
        ===========  =========  ===========  ===========  ===============
 | 
						|
        1            ✅          ❌            ❌            ❌
 | 
						|
        2            ✅          ✅            ✅            ❌
 | 
						|
        3            ✅          ❌            ❌            ✅
 | 
						|
        ===========  =========  ===========  ===========  ===============
 | 
						|
        """
 | 
						|
        if self.linkedUserValue is not None:
 | 
						|
            return 3
 | 
						|
        if self.userMinimum is not None or self.userMaximum is not None:
 | 
						|
            return 2
 | 
						|
        return 1
 | 
						|
 | 
						|
    @property
 | 
						|
    def defaultName(self) -> str:
 | 
						|
        """Return the English name from :attr:`labelNames` or the :attr:`name`."""
 | 
						|
        return self.labelNames.get("en") or self.name
 | 
						|
 | 
						|
 | 
						|
class LocationLabelDescriptor(SimpleDescriptor):
 | 
						|
    """Container for location label data.
 | 
						|
 | 
						|
    Analogue of OpenType's STAT data for a free-floating location (format 4).
 | 
						|
    All values are user values.
 | 
						|
 | 
						|
    See: `OTSpec STAT Axis value table, format 4 <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4>`_
 | 
						|
 | 
						|
    .. versionadded:: 5.0
 | 
						|
    """
 | 
						|
 | 
						|
    flavor = "label"
 | 
						|
    _attrs = ("name", "elidable", "olderSibling", "userLocation", "labelNames")
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        *,
 | 
						|
        name,
 | 
						|
        userLocation,
 | 
						|
        elidable=False,
 | 
						|
        olderSibling=False,
 | 
						|
        labelNames=None,
 | 
						|
    ):
 | 
						|
        self.name: str = name
 | 
						|
        """Label for this named location, STAT field ``valueNameID``."""
 | 
						|
        self.userLocation: SimpleLocationDict = userLocation or {}
 | 
						|
        """Location in user coordinates along each axis.
 | 
						|
 | 
						|
        If an axis is not mentioned, it is assumed to be at its default location.
 | 
						|
 | 
						|
        .. seealso:: This may be only part of the full location. See:
 | 
						|
           :meth:`getFullUserLocation`
 | 
						|
        """
 | 
						|
        self.elidable: bool = elidable
 | 
						|
        """STAT flag ``ELIDABLE_AXIS_VALUE_NAME``.
 | 
						|
 | 
						|
        See: `OTSpec STAT Flags <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#flags>`_
 | 
						|
        """
 | 
						|
        self.olderSibling: bool = olderSibling
 | 
						|
        """STAT flag ``OLDER_SIBLING_FONT_ATTRIBUTE``.
 | 
						|
 | 
						|
        See: `OTSpec STAT Flags <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#flags>`_
 | 
						|
        """
 | 
						|
        self.labelNames: Dict[str, str] = labelNames or {}
 | 
						|
        """User-facing translations of this location's label. Keyed by
 | 
						|
        xml:lang code.
 | 
						|
        """
 | 
						|
 | 
						|
    @property
 | 
						|
    def defaultName(self) -> str:
 | 
						|
        """Return the English name from :attr:`labelNames` or the :attr:`name`."""
 | 
						|
        return self.labelNames.get("en") or self.name
 | 
						|
 | 
						|
    def getFullUserLocation(self, doc: "DesignSpaceDocument") -> SimpleLocationDict:
 | 
						|
        """Get the complete user location of this label, by combining data
 | 
						|
        from the explicit user location and default axis values.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        return {
 | 
						|
            axis.name: self.userLocation.get(axis.name, axis.default)
 | 
						|
            for axis in doc.axes
 | 
						|
        }
 | 
						|
 | 
						|
 | 
						|
class VariableFontDescriptor(SimpleDescriptor):
 | 
						|
    """Container for variable fonts, sub-spaces of the Designspace.
 | 
						|
 | 
						|
    Use-cases:
 | 
						|
 | 
						|
    - From a single DesignSpace with discrete axes, define 1 variable font
 | 
						|
      per value on the discrete axes. Before version 5, you would have needed
 | 
						|
      1 DesignSpace per such variable font, and a lot of data duplication.
 | 
						|
    - From a big variable font with many axes, define subsets of that variable
 | 
						|
      font that only include some axes and freeze other axes at a given location.
 | 
						|
 | 
						|
    .. versionadded:: 5.0
 | 
						|
    """
 | 
						|
 | 
						|
    flavor = "variable-font"
 | 
						|
    _attrs = ("filename", "axisSubsets", "lib")
 | 
						|
 | 
						|
    filename = posixpath_property("_filename")
 | 
						|
 | 
						|
    def __init__(self, *, name, filename=None, axisSubsets=None, lib=None):
 | 
						|
        self.name: str = name
 | 
						|
        """string, required. Name of this variable to identify it during the
 | 
						|
        build process and from other parts of the document, and also as a
 | 
						|
        filename in case the filename property is empty.
 | 
						|
 | 
						|
        VarLib.
 | 
						|
        """
 | 
						|
        self.filename: str = filename
 | 
						|
        """string, optional. Relative path to the variable font file, **as it is
 | 
						|
        in the document**. The file may or may not exist.
 | 
						|
 | 
						|
        If not specified, the :attr:`name` will be used as a basename for the file.
 | 
						|
        """
 | 
						|
        self.axisSubsets: List[
 | 
						|
            Union[RangeAxisSubsetDescriptor, ValueAxisSubsetDescriptor]
 | 
						|
        ] = (axisSubsets or [])
 | 
						|
        """Axis subsets to include in this variable font.
 | 
						|
 | 
						|
        If an axis is not mentioned, assume that we only want the default
 | 
						|
        location of that axis (same as a :class:`ValueAxisSubsetDescriptor`).
 | 
						|
        """
 | 
						|
        self.lib: MutableMapping[str, Any] = lib or {}
 | 
						|
        """Custom data associated with this variable font."""
 | 
						|
 | 
						|
 | 
						|
class RangeAxisSubsetDescriptor(SimpleDescriptor):
 | 
						|
    """Subset of a continuous axis to include in a variable font.
 | 
						|
 | 
						|
    .. versionadded:: 5.0
 | 
						|
    """
 | 
						|
 | 
						|
    flavor = "axis-subset"
 | 
						|
    _attrs = ("name", "userMinimum", "userDefault", "userMaximum")
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self, *, name, userMinimum=-math.inf, userDefault=None, userMaximum=math.inf
 | 
						|
    ):
 | 
						|
        self.name: str = name
 | 
						|
        """Name of the :class:`AxisDescriptor` to subset."""
 | 
						|
        self.userMinimum: float = userMinimum
 | 
						|
        """New minimum value of the axis in the target variable font.
 | 
						|
        If not specified, assume the same minimum value as the full axis.
 | 
						|
        (default = ``-math.inf``)
 | 
						|
        """
 | 
						|
        self.userDefault: Optional[float] = userDefault
 | 
						|
        """New default value of the axis in the target variable font.
 | 
						|
        If not specified, assume the same default value as the full axis.
 | 
						|
        (default = ``None``)
 | 
						|
        """
 | 
						|
        self.userMaximum: float = userMaximum
 | 
						|
        """New maximum value of the axis in the target variable font.
 | 
						|
        If not specified, assume the same maximum value as the full axis.
 | 
						|
        (default = ``math.inf``)
 | 
						|
        """
 | 
						|
 | 
						|
 | 
						|
class ValueAxisSubsetDescriptor(SimpleDescriptor):
 | 
						|
    """Single value of a discrete or continuous axis to use in a variable font.
 | 
						|
 | 
						|
    .. versionadded:: 5.0
 | 
						|
    """
 | 
						|
 | 
						|
    flavor = "axis-subset"
 | 
						|
    _attrs = ("name", "userValue")
 | 
						|
 | 
						|
    def __init__(self, *, name, userValue):
 | 
						|
        self.name: str = name
 | 
						|
        """Name of the :class:`AxisDescriptor` or :class:`DiscreteAxisDescriptor`
 | 
						|
        to "snapshot" or "freeze".
 | 
						|
        """
 | 
						|
        self.userValue: float = userValue
 | 
						|
        """Value in user coordinates at which to freeze the given axis."""
 | 
						|
 | 
						|
 | 
						|
class BaseDocWriter(object):
 | 
						|
    _whiteSpace = "    "
 | 
						|
    axisDescriptorClass = AxisDescriptor
 | 
						|
    discreteAxisDescriptorClass = DiscreteAxisDescriptor
 | 
						|
    axisLabelDescriptorClass = AxisLabelDescriptor
 | 
						|
    axisMappingDescriptorClass = AxisMappingDescriptor
 | 
						|
    locationLabelDescriptorClass = LocationLabelDescriptor
 | 
						|
    ruleDescriptorClass = RuleDescriptor
 | 
						|
    sourceDescriptorClass = SourceDescriptor
 | 
						|
    variableFontDescriptorClass = VariableFontDescriptor
 | 
						|
    valueAxisSubsetDescriptorClass = ValueAxisSubsetDescriptor
 | 
						|
    rangeAxisSubsetDescriptorClass = RangeAxisSubsetDescriptor
 | 
						|
    instanceDescriptorClass = InstanceDescriptor
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def getAxisDecriptor(cls):
 | 
						|
        return cls.axisDescriptorClass()
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def getAxisMappingDescriptor(cls):
 | 
						|
        return cls.axisMappingDescriptorClass()
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def getSourceDescriptor(cls):
 | 
						|
        return cls.sourceDescriptorClass()
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def getInstanceDescriptor(cls):
 | 
						|
        return cls.instanceDescriptorClass()
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def getRuleDescriptor(cls):
 | 
						|
        return cls.ruleDescriptorClass()
 | 
						|
 | 
						|
    def __init__(self, documentPath, documentObject: DesignSpaceDocument):
 | 
						|
        self.path = documentPath
 | 
						|
        self.documentObject = documentObject
 | 
						|
        self.effectiveFormatTuple = self._getEffectiveFormatTuple()
 | 
						|
        self.root = ET.Element("designspace")
 | 
						|
 | 
						|
    def write(self, pretty=True, encoding="UTF-8", xml_declaration=True):
 | 
						|
        self.root.attrib["format"] = ".".join(str(i) for i in self.effectiveFormatTuple)
 | 
						|
 | 
						|
        if (
 | 
						|
            self.documentObject.axes
 | 
						|
            or self.documentObject.axisMappings
 | 
						|
            or self.documentObject.elidedFallbackName is not None
 | 
						|
        ):
 | 
						|
            axesElement = ET.Element("axes")
 | 
						|
            if self.documentObject.elidedFallbackName is not None:
 | 
						|
                axesElement.attrib["elidedfallbackname"] = (
 | 
						|
                    self.documentObject.elidedFallbackName
 | 
						|
                )
 | 
						|
            self.root.append(axesElement)
 | 
						|
        for axisObject in self.documentObject.axes:
 | 
						|
            self._addAxis(axisObject)
 | 
						|
 | 
						|
        if self.documentObject.axisMappings:
 | 
						|
            mappingsElement = None
 | 
						|
            lastGroup = object()
 | 
						|
            for mappingObject in self.documentObject.axisMappings:
 | 
						|
                if getattr(mappingObject, "groupDescription", None) != lastGroup:
 | 
						|
                    if mappingsElement is not None:
 | 
						|
                        self.root.findall(".axes")[0].append(mappingsElement)
 | 
						|
                    lastGroup = getattr(mappingObject, "groupDescription", None)
 | 
						|
                    mappingsElement = ET.Element("mappings")
 | 
						|
                    if lastGroup is not None:
 | 
						|
                        mappingsElement.attrib["description"] = lastGroup
 | 
						|
                self._addAxisMapping(mappingsElement, mappingObject)
 | 
						|
            if mappingsElement is not None:
 | 
						|
                self.root.findall(".axes")[0].append(mappingsElement)
 | 
						|
 | 
						|
        if self.documentObject.locationLabels:
 | 
						|
            labelsElement = ET.Element("labels")
 | 
						|
            for labelObject in self.documentObject.locationLabels:
 | 
						|
                self._addLocationLabel(labelsElement, labelObject)
 | 
						|
            self.root.append(labelsElement)
 | 
						|
 | 
						|
        if self.documentObject.rules:
 | 
						|
            if getattr(self.documentObject, "rulesProcessingLast", False):
 | 
						|
                attributes = {"processing": "last"}
 | 
						|
            else:
 | 
						|
                attributes = {}
 | 
						|
            self.root.append(ET.Element("rules", attributes))
 | 
						|
        for ruleObject in self.documentObject.rules:
 | 
						|
            self._addRule(ruleObject)
 | 
						|
 | 
						|
        if self.documentObject.sources:
 | 
						|
            self.root.append(ET.Element("sources"))
 | 
						|
        for sourceObject in self.documentObject.sources:
 | 
						|
            self._addSource(sourceObject)
 | 
						|
 | 
						|
        if self.documentObject.variableFonts:
 | 
						|
            variableFontsElement = ET.Element("variable-fonts")
 | 
						|
            for variableFont in self.documentObject.variableFonts:
 | 
						|
                self._addVariableFont(variableFontsElement, variableFont)
 | 
						|
            self.root.append(variableFontsElement)
 | 
						|
 | 
						|
        if self.documentObject.instances:
 | 
						|
            self.root.append(ET.Element("instances"))
 | 
						|
        for instanceObject in self.documentObject.instances:
 | 
						|
            self._addInstance(instanceObject)
 | 
						|
 | 
						|
        if self.documentObject.lib:
 | 
						|
            self._addLib(self.root, self.documentObject.lib, 2)
 | 
						|
 | 
						|
        tree = ET.ElementTree(self.root)
 | 
						|
        tree.write(
 | 
						|
            self.path,
 | 
						|
            encoding=encoding,
 | 
						|
            method="xml",
 | 
						|
            xml_declaration=xml_declaration,
 | 
						|
            pretty_print=pretty,
 | 
						|
        )
 | 
						|
 | 
						|
    def _getEffectiveFormatTuple(self):
 | 
						|
        """Try to use the version specified in the document, or a sufficiently
 | 
						|
        recent version to be able to encode what the document contains.
 | 
						|
        """
 | 
						|
        minVersion = self.documentObject.formatTuple
 | 
						|
        if (
 | 
						|
            any(
 | 
						|
                hasattr(axis, "values")
 | 
						|
                or axis.axisOrdering is not None
 | 
						|
                or axis.axisLabels
 | 
						|
                for axis in self.documentObject.axes
 | 
						|
            )
 | 
						|
            or self.documentObject.locationLabels
 | 
						|
            or any(source.localisedFamilyName for source in self.documentObject.sources)
 | 
						|
            or self.documentObject.variableFonts
 | 
						|
            or any(
 | 
						|
                instance.locationLabel or instance.userLocation
 | 
						|
                for instance in self.documentObject.instances
 | 
						|
            )
 | 
						|
        ):
 | 
						|
            if minVersion < (5, 0):
 | 
						|
                minVersion = (5, 0)
 | 
						|
        if self.documentObject.axisMappings:
 | 
						|
            if minVersion < (5, 1):
 | 
						|
                minVersion = (5, 1)
 | 
						|
        return minVersion
 | 
						|
 | 
						|
    def _makeLocationElement(self, locationObject, name=None):
 | 
						|
        """Convert Location dict to a locationElement."""
 | 
						|
        locElement = ET.Element("location")
 | 
						|
        if name is not None:
 | 
						|
            locElement.attrib["name"] = name
 | 
						|
        validatedLocation = self.documentObject.newDefaultLocation()
 | 
						|
        for axisName, axisValue in locationObject.items():
 | 
						|
            if axisName in validatedLocation:
 | 
						|
                # only accept values we know
 | 
						|
                validatedLocation[axisName] = axisValue
 | 
						|
        for dimensionName, dimensionValue in validatedLocation.items():
 | 
						|
            dimElement = ET.Element("dimension")
 | 
						|
            dimElement.attrib["name"] = dimensionName
 | 
						|
            if type(dimensionValue) == tuple:
 | 
						|
                dimElement.attrib["xvalue"] = self.intOrFloat(dimensionValue[0])
 | 
						|
                dimElement.attrib["yvalue"] = self.intOrFloat(dimensionValue[1])
 | 
						|
            else:
 | 
						|
                dimElement.attrib["xvalue"] = self.intOrFloat(dimensionValue)
 | 
						|
            locElement.append(dimElement)
 | 
						|
        return locElement, validatedLocation
 | 
						|
 | 
						|
    def intOrFloat(self, num):
 | 
						|
        if int(num) == num:
 | 
						|
            return "%d" % num
 | 
						|
        return ("%f" % num).rstrip("0").rstrip(".")
 | 
						|
 | 
						|
    def _addRule(self, ruleObject):
 | 
						|
        # if none of the conditions have minimum or maximum values, do not add the rule.
 | 
						|
        ruleElement = ET.Element("rule")
 | 
						|
        if ruleObject.name is not None:
 | 
						|
            ruleElement.attrib["name"] = ruleObject.name
 | 
						|
        for conditions in ruleObject.conditionSets:
 | 
						|
            conditionsetElement = ET.Element("conditionset")
 | 
						|
            for cond in conditions:
 | 
						|
                if cond.get("minimum") is None and cond.get("maximum") is None:
 | 
						|
                    # neither is defined, don't add this condition
 | 
						|
                    continue
 | 
						|
                conditionElement = ET.Element("condition")
 | 
						|
                conditionElement.attrib["name"] = cond.get("name")
 | 
						|
                if cond.get("minimum") is not None:
 | 
						|
                    conditionElement.attrib["minimum"] = self.intOrFloat(
 | 
						|
                        cond.get("minimum")
 | 
						|
                    )
 | 
						|
                if cond.get("maximum") is not None:
 | 
						|
                    conditionElement.attrib["maximum"] = self.intOrFloat(
 | 
						|
                        cond.get("maximum")
 | 
						|
                    )
 | 
						|
                conditionsetElement.append(conditionElement)
 | 
						|
            if len(conditionsetElement):
 | 
						|
                ruleElement.append(conditionsetElement)
 | 
						|
        for sub in ruleObject.subs:
 | 
						|
            subElement = ET.Element("sub")
 | 
						|
            subElement.attrib["name"] = sub[0]
 | 
						|
            subElement.attrib["with"] = sub[1]
 | 
						|
            ruleElement.append(subElement)
 | 
						|
        if len(ruleElement):
 | 
						|
            self.root.findall(".rules")[0].append(ruleElement)
 | 
						|
 | 
						|
    def _addAxis(self, axisObject):
 | 
						|
        axisElement = ET.Element("axis")
 | 
						|
        axisElement.attrib["tag"] = axisObject.tag
 | 
						|
        axisElement.attrib["name"] = axisObject.name
 | 
						|
        self._addLabelNames(axisElement, axisObject.labelNames)
 | 
						|
        if axisObject.map:
 | 
						|
            for inputValue, outputValue in axisObject.map:
 | 
						|
                mapElement = ET.Element("map")
 | 
						|
                mapElement.attrib["input"] = self.intOrFloat(inputValue)
 | 
						|
                mapElement.attrib["output"] = self.intOrFloat(outputValue)
 | 
						|
                axisElement.append(mapElement)
 | 
						|
        if axisObject.axisOrdering is not None or axisObject.axisLabels:
 | 
						|
            labelsElement = ET.Element("labels")
 | 
						|
            if axisObject.axisOrdering is not None:
 | 
						|
                labelsElement.attrib["ordering"] = str(axisObject.axisOrdering)
 | 
						|
            for label in axisObject.axisLabels:
 | 
						|
                self._addAxisLabel(labelsElement, label)
 | 
						|
            axisElement.append(labelsElement)
 | 
						|
        if hasattr(axisObject, "minimum"):
 | 
						|
            axisElement.attrib["minimum"] = self.intOrFloat(axisObject.minimum)
 | 
						|
            axisElement.attrib["maximum"] = self.intOrFloat(axisObject.maximum)
 | 
						|
        elif hasattr(axisObject, "values"):
 | 
						|
            axisElement.attrib["values"] = " ".join(
 | 
						|
                self.intOrFloat(v) for v in axisObject.values
 | 
						|
            )
 | 
						|
        axisElement.attrib["default"] = self.intOrFloat(axisObject.default)
 | 
						|
        if axisObject.hidden:
 | 
						|
            axisElement.attrib["hidden"] = "1"
 | 
						|
        self.root.findall(".axes")[0].append(axisElement)
 | 
						|
 | 
						|
    def _addAxisMapping(self, mappingsElement, mappingObject):
 | 
						|
        mappingElement = ET.Element("mapping")
 | 
						|
        if getattr(mappingObject, "description", None) is not None:
 | 
						|
            mappingElement.attrib["description"] = mappingObject.description
 | 
						|
        for what in ("inputLocation", "outputLocation"):
 | 
						|
            whatObject = getattr(mappingObject, what, None)
 | 
						|
            if whatObject is None:
 | 
						|
                continue
 | 
						|
            whatElement = ET.Element(what[:-8])
 | 
						|
            mappingElement.append(whatElement)
 | 
						|
 | 
						|
            for name, value in whatObject.items():
 | 
						|
                dimensionElement = ET.Element("dimension")
 | 
						|
                dimensionElement.attrib["name"] = name
 | 
						|
                dimensionElement.attrib["xvalue"] = self.intOrFloat(value)
 | 
						|
                whatElement.append(dimensionElement)
 | 
						|
 | 
						|
        mappingsElement.append(mappingElement)
 | 
						|
 | 
						|
    def _addAxisLabel(
 | 
						|
        self, axisElement: ET.Element, label: AxisLabelDescriptor
 | 
						|
    ) -> None:
 | 
						|
        labelElement = ET.Element("label")
 | 
						|
        labelElement.attrib["uservalue"] = self.intOrFloat(label.userValue)
 | 
						|
        if label.userMinimum is not None:
 | 
						|
            labelElement.attrib["userminimum"] = self.intOrFloat(label.userMinimum)
 | 
						|
        if label.userMaximum is not None:
 | 
						|
            labelElement.attrib["usermaximum"] = self.intOrFloat(label.userMaximum)
 | 
						|
        labelElement.attrib["name"] = label.name
 | 
						|
        if label.elidable:
 | 
						|
            labelElement.attrib["elidable"] = "true"
 | 
						|
        if label.olderSibling:
 | 
						|
            labelElement.attrib["oldersibling"] = "true"
 | 
						|
        if label.linkedUserValue is not None:
 | 
						|
            labelElement.attrib["linkeduservalue"] = self.intOrFloat(
 | 
						|
                label.linkedUserValue
 | 
						|
            )
 | 
						|
        self._addLabelNames(labelElement, label.labelNames)
 | 
						|
        axisElement.append(labelElement)
 | 
						|
 | 
						|
    def _addLabelNames(self, parentElement, labelNames):
 | 
						|
        for languageCode, labelName in sorted(labelNames.items()):
 | 
						|
            languageElement = ET.Element("labelname")
 | 
						|
            languageElement.attrib[XML_LANG] = languageCode
 | 
						|
            languageElement.text = labelName
 | 
						|
            parentElement.append(languageElement)
 | 
						|
 | 
						|
    def _addLocationLabel(
 | 
						|
        self, parentElement: ET.Element, label: LocationLabelDescriptor
 | 
						|
    ) -> None:
 | 
						|
        labelElement = ET.Element("label")
 | 
						|
        labelElement.attrib["name"] = label.name
 | 
						|
        if label.elidable:
 | 
						|
            labelElement.attrib["elidable"] = "true"
 | 
						|
        if label.olderSibling:
 | 
						|
            labelElement.attrib["oldersibling"] = "true"
 | 
						|
        self._addLabelNames(labelElement, label.labelNames)
 | 
						|
        self._addLocationElement(labelElement, userLocation=label.userLocation)
 | 
						|
        parentElement.append(labelElement)
 | 
						|
 | 
						|
    def _addLocationElement(
 | 
						|
        self,
 | 
						|
        parentElement,
 | 
						|
        *,
 | 
						|
        designLocation: AnisotropicLocationDict = None,
 | 
						|
        userLocation: SimpleLocationDict = None,
 | 
						|
    ):
 | 
						|
        locElement = ET.Element("location")
 | 
						|
        for axis in self.documentObject.axes:
 | 
						|
            if designLocation is not None and axis.name in designLocation:
 | 
						|
                dimElement = ET.Element("dimension")
 | 
						|
                dimElement.attrib["name"] = axis.name
 | 
						|
                value = designLocation[axis.name]
 | 
						|
                if isinstance(value, tuple):
 | 
						|
                    dimElement.attrib["xvalue"] = self.intOrFloat(value[0])
 | 
						|
                    dimElement.attrib["yvalue"] = self.intOrFloat(value[1])
 | 
						|
                else:
 | 
						|
                    dimElement.attrib["xvalue"] = self.intOrFloat(value)
 | 
						|
                locElement.append(dimElement)
 | 
						|
            elif userLocation is not None and axis.name in userLocation:
 | 
						|
                dimElement = ET.Element("dimension")
 | 
						|
                dimElement.attrib["name"] = axis.name
 | 
						|
                value = userLocation[axis.name]
 | 
						|
                dimElement.attrib["uservalue"] = self.intOrFloat(value)
 | 
						|
                locElement.append(dimElement)
 | 
						|
        if len(locElement) > 0:
 | 
						|
            parentElement.append(locElement)
 | 
						|
 | 
						|
    def _addInstance(self, instanceObject):
 | 
						|
        instanceElement = ET.Element("instance")
 | 
						|
        if instanceObject.name is not None:
 | 
						|
            instanceElement.attrib["name"] = instanceObject.name
 | 
						|
        if instanceObject.locationLabel is not None:
 | 
						|
            instanceElement.attrib["location"] = instanceObject.locationLabel
 | 
						|
        if instanceObject.familyName is not None:
 | 
						|
            instanceElement.attrib["familyname"] = instanceObject.familyName
 | 
						|
        if instanceObject.styleName is not None:
 | 
						|
            instanceElement.attrib["stylename"] = instanceObject.styleName
 | 
						|
        # add localisations
 | 
						|
        if instanceObject.localisedStyleName:
 | 
						|
            languageCodes = list(instanceObject.localisedStyleName.keys())
 | 
						|
            languageCodes.sort()
 | 
						|
            for code in languageCodes:
 | 
						|
                if code == "en":
 | 
						|
                    continue  # already stored in the element attribute
 | 
						|
                localisedStyleNameElement = ET.Element("stylename")
 | 
						|
                localisedStyleNameElement.attrib[XML_LANG] = code
 | 
						|
                localisedStyleNameElement.text = instanceObject.getStyleName(code)
 | 
						|
                instanceElement.append(localisedStyleNameElement)
 | 
						|
        if instanceObject.localisedFamilyName:
 | 
						|
            languageCodes = list(instanceObject.localisedFamilyName.keys())
 | 
						|
            languageCodes.sort()
 | 
						|
            for code in languageCodes:
 | 
						|
                if code == "en":
 | 
						|
                    continue  # already stored in the element attribute
 | 
						|
                localisedFamilyNameElement = ET.Element("familyname")
 | 
						|
                localisedFamilyNameElement.attrib[XML_LANG] = code
 | 
						|
                localisedFamilyNameElement.text = instanceObject.getFamilyName(code)
 | 
						|
                instanceElement.append(localisedFamilyNameElement)
 | 
						|
        if instanceObject.localisedStyleMapStyleName:
 | 
						|
            languageCodes = list(instanceObject.localisedStyleMapStyleName.keys())
 | 
						|
            languageCodes.sort()
 | 
						|
            for code in languageCodes:
 | 
						|
                if code == "en":
 | 
						|
                    continue
 | 
						|
                localisedStyleMapStyleNameElement = ET.Element("stylemapstylename")
 | 
						|
                localisedStyleMapStyleNameElement.attrib[XML_LANG] = code
 | 
						|
                localisedStyleMapStyleNameElement.text = (
 | 
						|
                    instanceObject.getStyleMapStyleName(code)
 | 
						|
                )
 | 
						|
                instanceElement.append(localisedStyleMapStyleNameElement)
 | 
						|
        if instanceObject.localisedStyleMapFamilyName:
 | 
						|
            languageCodes = list(instanceObject.localisedStyleMapFamilyName.keys())
 | 
						|
            languageCodes.sort()
 | 
						|
            for code in languageCodes:
 | 
						|
                if code == "en":
 | 
						|
                    continue
 | 
						|
                localisedStyleMapFamilyNameElement = ET.Element("stylemapfamilyname")
 | 
						|
                localisedStyleMapFamilyNameElement.attrib[XML_LANG] = code
 | 
						|
                localisedStyleMapFamilyNameElement.text = (
 | 
						|
                    instanceObject.getStyleMapFamilyName(code)
 | 
						|
                )
 | 
						|
                instanceElement.append(localisedStyleMapFamilyNameElement)
 | 
						|
 | 
						|
        if self.effectiveFormatTuple >= (5, 0):
 | 
						|
            if instanceObject.locationLabel is None:
 | 
						|
                self._addLocationElement(
 | 
						|
                    instanceElement,
 | 
						|
                    designLocation=instanceObject.designLocation,
 | 
						|
                    userLocation=instanceObject.userLocation,
 | 
						|
                )
 | 
						|
        else:
 | 
						|
            # Pre-version 5.0 code was validating and filling in the location
 | 
						|
            # dict while writing it out, as preserved below.
 | 
						|
            if instanceObject.location is not None:
 | 
						|
                locationElement, instanceObject.location = self._makeLocationElement(
 | 
						|
                    instanceObject.location
 | 
						|
                )
 | 
						|
                instanceElement.append(locationElement)
 | 
						|
        if instanceObject.filename is not None:
 | 
						|
            instanceElement.attrib["filename"] = instanceObject.filename
 | 
						|
        if instanceObject.postScriptFontName is not None:
 | 
						|
            instanceElement.attrib["postscriptfontname"] = (
 | 
						|
                instanceObject.postScriptFontName
 | 
						|
            )
 | 
						|
        if instanceObject.styleMapFamilyName is not None:
 | 
						|
            instanceElement.attrib["stylemapfamilyname"] = (
 | 
						|
                instanceObject.styleMapFamilyName
 | 
						|
            )
 | 
						|
        if instanceObject.styleMapStyleName is not None:
 | 
						|
            instanceElement.attrib["stylemapstylename"] = (
 | 
						|
                instanceObject.styleMapStyleName
 | 
						|
            )
 | 
						|
        if self.effectiveFormatTuple < (5, 0):
 | 
						|
            # Deprecated members as of version 5.0
 | 
						|
            if instanceObject.glyphs:
 | 
						|
                if instanceElement.findall(".glyphs") == []:
 | 
						|
                    glyphsElement = ET.Element("glyphs")
 | 
						|
                    instanceElement.append(glyphsElement)
 | 
						|
                glyphsElement = instanceElement.findall(".glyphs")[0]
 | 
						|
                for glyphName, data in sorted(instanceObject.glyphs.items()):
 | 
						|
                    glyphElement = self._writeGlyphElement(
 | 
						|
                        instanceElement, instanceObject, glyphName, data
 | 
						|
                    )
 | 
						|
                    glyphsElement.append(glyphElement)
 | 
						|
            if instanceObject.kerning:
 | 
						|
                kerningElement = ET.Element("kerning")
 | 
						|
                instanceElement.append(kerningElement)
 | 
						|
            if instanceObject.info:
 | 
						|
                infoElement = ET.Element("info")
 | 
						|
                instanceElement.append(infoElement)
 | 
						|
        self._addLib(instanceElement, instanceObject.lib, 4)
 | 
						|
        self.root.findall(".instances")[0].append(instanceElement)
 | 
						|
 | 
						|
    def _addSource(self, sourceObject):
 | 
						|
        sourceElement = ET.Element("source")
 | 
						|
        if sourceObject.filename is not None:
 | 
						|
            sourceElement.attrib["filename"] = sourceObject.filename
 | 
						|
        if sourceObject.name is not None:
 | 
						|
            if sourceObject.name.find("temp_master") != 0:
 | 
						|
                # do not save temporary source names
 | 
						|
                sourceElement.attrib["name"] = sourceObject.name
 | 
						|
        if sourceObject.familyName is not None:
 | 
						|
            sourceElement.attrib["familyname"] = sourceObject.familyName
 | 
						|
        if sourceObject.styleName is not None:
 | 
						|
            sourceElement.attrib["stylename"] = sourceObject.styleName
 | 
						|
        if sourceObject.layerName is not None:
 | 
						|
            sourceElement.attrib["layer"] = sourceObject.layerName
 | 
						|
        if sourceObject.localisedFamilyName:
 | 
						|
            languageCodes = list(sourceObject.localisedFamilyName.keys())
 | 
						|
            languageCodes.sort()
 | 
						|
            for code in languageCodes:
 | 
						|
                if code == "en":
 | 
						|
                    continue  # already stored in the element attribute
 | 
						|
                localisedFamilyNameElement = ET.Element("familyname")
 | 
						|
                localisedFamilyNameElement.attrib[XML_LANG] = code
 | 
						|
                localisedFamilyNameElement.text = sourceObject.getFamilyName(code)
 | 
						|
                sourceElement.append(localisedFamilyNameElement)
 | 
						|
        if sourceObject.copyLib:
 | 
						|
            libElement = ET.Element("lib")
 | 
						|
            libElement.attrib["copy"] = "1"
 | 
						|
            sourceElement.append(libElement)
 | 
						|
        if sourceObject.copyGroups:
 | 
						|
            groupsElement = ET.Element("groups")
 | 
						|
            groupsElement.attrib["copy"] = "1"
 | 
						|
            sourceElement.append(groupsElement)
 | 
						|
        if sourceObject.copyFeatures:
 | 
						|
            featuresElement = ET.Element("features")
 | 
						|
            featuresElement.attrib["copy"] = "1"
 | 
						|
            sourceElement.append(featuresElement)
 | 
						|
        if sourceObject.copyInfo or sourceObject.muteInfo:
 | 
						|
            infoElement = ET.Element("info")
 | 
						|
            if sourceObject.copyInfo:
 | 
						|
                infoElement.attrib["copy"] = "1"
 | 
						|
            if sourceObject.muteInfo:
 | 
						|
                infoElement.attrib["mute"] = "1"
 | 
						|
            sourceElement.append(infoElement)
 | 
						|
        if sourceObject.muteKerning:
 | 
						|
            kerningElement = ET.Element("kerning")
 | 
						|
            kerningElement.attrib["mute"] = "1"
 | 
						|
            sourceElement.append(kerningElement)
 | 
						|
        if sourceObject.mutedGlyphNames:
 | 
						|
            for name in sourceObject.mutedGlyphNames:
 | 
						|
                glyphElement = ET.Element("glyph")
 | 
						|
                glyphElement.attrib["name"] = name
 | 
						|
                glyphElement.attrib["mute"] = "1"
 | 
						|
                sourceElement.append(glyphElement)
 | 
						|
        if self.effectiveFormatTuple >= (5, 0):
 | 
						|
            self._addLocationElement(
 | 
						|
                sourceElement, designLocation=sourceObject.location
 | 
						|
            )
 | 
						|
        else:
 | 
						|
            # Pre-version 5.0 code was validating and filling in the location
 | 
						|
            # dict while writing it out, as preserved below.
 | 
						|
            locationElement, sourceObject.location = self._makeLocationElement(
 | 
						|
                sourceObject.location
 | 
						|
            )
 | 
						|
            sourceElement.append(locationElement)
 | 
						|
        self.root.findall(".sources")[0].append(sourceElement)
 | 
						|
 | 
						|
    def _addVariableFont(
 | 
						|
        self, parentElement: ET.Element, vf: VariableFontDescriptor
 | 
						|
    ) -> None:
 | 
						|
        vfElement = ET.Element("variable-font")
 | 
						|
        vfElement.attrib["name"] = vf.name
 | 
						|
        if vf.filename is not None:
 | 
						|
            vfElement.attrib["filename"] = vf.filename
 | 
						|
        if vf.axisSubsets:
 | 
						|
            subsetsElement = ET.Element("axis-subsets")
 | 
						|
            for subset in vf.axisSubsets:
 | 
						|
                subsetElement = ET.Element("axis-subset")
 | 
						|
                subsetElement.attrib["name"] = subset.name
 | 
						|
                # Mypy doesn't support narrowing union types via hasattr()
 | 
						|
                # https://mypy.readthedocs.io/en/stable/type_narrowing.html
 | 
						|
                # TODO(Python 3.10): use TypeGuard
 | 
						|
                if hasattr(subset, "userMinimum"):
 | 
						|
                    subset = cast(RangeAxisSubsetDescriptor, subset)
 | 
						|
                    if subset.userMinimum != -math.inf:
 | 
						|
                        subsetElement.attrib["userminimum"] = self.intOrFloat(
 | 
						|
                            subset.userMinimum
 | 
						|
                        )
 | 
						|
                    if subset.userMaximum != math.inf:
 | 
						|
                        subsetElement.attrib["usermaximum"] = self.intOrFloat(
 | 
						|
                            subset.userMaximum
 | 
						|
                        )
 | 
						|
                    if subset.userDefault is not None:
 | 
						|
                        subsetElement.attrib["userdefault"] = self.intOrFloat(
 | 
						|
                            subset.userDefault
 | 
						|
                        )
 | 
						|
                elif hasattr(subset, "userValue"):
 | 
						|
                    subset = cast(ValueAxisSubsetDescriptor, subset)
 | 
						|
                    subsetElement.attrib["uservalue"] = self.intOrFloat(
 | 
						|
                        subset.userValue
 | 
						|
                    )
 | 
						|
                subsetsElement.append(subsetElement)
 | 
						|
            vfElement.append(subsetsElement)
 | 
						|
        self._addLib(vfElement, vf.lib, 4)
 | 
						|
        parentElement.append(vfElement)
 | 
						|
 | 
						|
    def _addLib(self, parentElement: ET.Element, data: Any, indent_level: int) -> None:
 | 
						|
        if not data:
 | 
						|
            return
 | 
						|
        libElement = ET.Element("lib")
 | 
						|
        libElement.append(plistlib.totree(data, indent_level=indent_level))
 | 
						|
        parentElement.append(libElement)
 | 
						|
 | 
						|
    def _writeGlyphElement(self, instanceElement, instanceObject, glyphName, data):
 | 
						|
        glyphElement = ET.Element("glyph")
 | 
						|
        if data.get("mute"):
 | 
						|
            glyphElement.attrib["mute"] = "1"
 | 
						|
        if data.get("unicodes") is not None:
 | 
						|
            glyphElement.attrib["unicode"] = " ".join(
 | 
						|
                [hex(u) for u in data.get("unicodes")]
 | 
						|
            )
 | 
						|
        if data.get("instanceLocation") is not None:
 | 
						|
            locationElement, data["instanceLocation"] = self._makeLocationElement(
 | 
						|
                data.get("instanceLocation")
 | 
						|
            )
 | 
						|
            glyphElement.append(locationElement)
 | 
						|
        if glyphName is not None:
 | 
						|
            glyphElement.attrib["name"] = glyphName
 | 
						|
        if data.get("note") is not None:
 | 
						|
            noteElement = ET.Element("note")
 | 
						|
            noteElement.text = data.get("note")
 | 
						|
            glyphElement.append(noteElement)
 | 
						|
        if data.get("masters") is not None:
 | 
						|
            mastersElement = ET.Element("masters")
 | 
						|
            for m in data.get("masters"):
 | 
						|
                masterElement = ET.Element("master")
 | 
						|
                if m.get("glyphName") is not None:
 | 
						|
                    masterElement.attrib["glyphname"] = m.get("glyphName")
 | 
						|
                if m.get("font") is not None:
 | 
						|
                    masterElement.attrib["source"] = m.get("font")
 | 
						|
                if m.get("location") is not None:
 | 
						|
                    locationElement, m["location"] = self._makeLocationElement(
 | 
						|
                        m.get("location")
 | 
						|
                    )
 | 
						|
                    masterElement.append(locationElement)
 | 
						|
                mastersElement.append(masterElement)
 | 
						|
            glyphElement.append(mastersElement)
 | 
						|
        return glyphElement
 | 
						|
 | 
						|
 | 
						|
class BaseDocReader(LogMixin):
 | 
						|
    axisDescriptorClass = AxisDescriptor
 | 
						|
    discreteAxisDescriptorClass = DiscreteAxisDescriptor
 | 
						|
    axisLabelDescriptorClass = AxisLabelDescriptor
 | 
						|
    axisMappingDescriptorClass = AxisMappingDescriptor
 | 
						|
    locationLabelDescriptorClass = LocationLabelDescriptor
 | 
						|
    ruleDescriptorClass = RuleDescriptor
 | 
						|
    sourceDescriptorClass = SourceDescriptor
 | 
						|
    variableFontsDescriptorClass = VariableFontDescriptor
 | 
						|
    valueAxisSubsetDescriptorClass = ValueAxisSubsetDescriptor
 | 
						|
    rangeAxisSubsetDescriptorClass = RangeAxisSubsetDescriptor
 | 
						|
    instanceDescriptorClass = InstanceDescriptor
 | 
						|
 | 
						|
    def __init__(self, documentPath, documentObject):
 | 
						|
        self.path = documentPath
 | 
						|
        self.documentObject = documentObject
 | 
						|
        tree = ET.parse(self.path)
 | 
						|
        self.root = tree.getroot()
 | 
						|
        self.documentObject.formatVersion = self.root.attrib.get("format", "3.0")
 | 
						|
        self._axes = []
 | 
						|
        self.rules = []
 | 
						|
        self.sources = []
 | 
						|
        self.instances = []
 | 
						|
        self.axisDefaults = {}
 | 
						|
        self._strictAxisNames = True
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def fromstring(cls, string, documentObject):
 | 
						|
        f = BytesIO(tobytes(string, encoding="utf-8"))
 | 
						|
        self = cls(f, documentObject)
 | 
						|
        self.path = None
 | 
						|
        return self
 | 
						|
 | 
						|
    def read(self):
 | 
						|
        self.readAxes()
 | 
						|
        self.readLabels()
 | 
						|
        self.readRules()
 | 
						|
        self.readVariableFonts()
 | 
						|
        self.readSources()
 | 
						|
        self.readInstances()
 | 
						|
        self.readLib()
 | 
						|
 | 
						|
    def readRules(self):
 | 
						|
        # we also need to read any conditions that are outside of a condition set.
 | 
						|
        rules = []
 | 
						|
        rulesElement = self.root.find(".rules")
 | 
						|
        if rulesElement is not None:
 | 
						|
            processingValue = rulesElement.attrib.get("processing", "first")
 | 
						|
            if processingValue not in {"first", "last"}:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    "<rules> processing attribute value is not valid: %r, "
 | 
						|
                    "expected 'first' or 'last'" % processingValue
 | 
						|
                )
 | 
						|
            self.documentObject.rulesProcessingLast = processingValue == "last"
 | 
						|
        for ruleElement in self.root.findall(".rules/rule"):
 | 
						|
            ruleObject = self.ruleDescriptorClass()
 | 
						|
            ruleName = ruleObject.name = ruleElement.attrib.get("name")
 | 
						|
            # read any stray conditions outside a condition set
 | 
						|
            externalConditions = self._readConditionElements(
 | 
						|
                ruleElement,
 | 
						|
                ruleName,
 | 
						|
            )
 | 
						|
            if externalConditions:
 | 
						|
                ruleObject.conditionSets.append(externalConditions)
 | 
						|
                self.log.info(
 | 
						|
                    "Found stray rule conditions outside a conditionset. "
 | 
						|
                    "Wrapped them in a new conditionset."
 | 
						|
                )
 | 
						|
            # read the conditionsets
 | 
						|
            for conditionSetElement in ruleElement.findall(".conditionset"):
 | 
						|
                conditionSet = self._readConditionElements(
 | 
						|
                    conditionSetElement,
 | 
						|
                    ruleName,
 | 
						|
                )
 | 
						|
                if conditionSet is not None:
 | 
						|
                    ruleObject.conditionSets.append(conditionSet)
 | 
						|
            for subElement in ruleElement.findall(".sub"):
 | 
						|
                a = subElement.attrib["name"]
 | 
						|
                b = subElement.attrib["with"]
 | 
						|
                ruleObject.subs.append((a, b))
 | 
						|
            rules.append(ruleObject)
 | 
						|
        self.documentObject.rules = rules
 | 
						|
 | 
						|
    def _readConditionElements(self, parentElement, ruleName=None):
 | 
						|
        cds = []
 | 
						|
        for conditionElement in parentElement.findall(".condition"):
 | 
						|
            cd = {}
 | 
						|
            cdMin = conditionElement.attrib.get("minimum")
 | 
						|
            if cdMin is not None:
 | 
						|
                cd["minimum"] = float(cdMin)
 | 
						|
            else:
 | 
						|
                # will allow these to be None, assume axis.minimum
 | 
						|
                cd["minimum"] = None
 | 
						|
            cdMax = conditionElement.attrib.get("maximum")
 | 
						|
            if cdMax is not None:
 | 
						|
                cd["maximum"] = float(cdMax)
 | 
						|
            else:
 | 
						|
                # will allow these to be None, assume axis.maximum
 | 
						|
                cd["maximum"] = None
 | 
						|
            cd["name"] = conditionElement.attrib.get("name")
 | 
						|
            # # test for things
 | 
						|
            if cd.get("minimum") is None and cd.get("maximum") is None:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    "condition missing required minimum or maximum in rule"
 | 
						|
                    + (" '%s'" % ruleName if ruleName is not None else "")
 | 
						|
                )
 | 
						|
            cds.append(cd)
 | 
						|
        return cds
 | 
						|
 | 
						|
    def readAxes(self):
 | 
						|
        # read the axes elements, including the warp map.
 | 
						|
        axesElement = self.root.find(".axes")
 | 
						|
        if axesElement is not None and "elidedfallbackname" in axesElement.attrib:
 | 
						|
            self.documentObject.elidedFallbackName = axesElement.attrib[
 | 
						|
                "elidedfallbackname"
 | 
						|
            ]
 | 
						|
        axisElements = self.root.findall(".axes/axis")
 | 
						|
        if not axisElements:
 | 
						|
            return
 | 
						|
        for axisElement in axisElements:
 | 
						|
            if (
 | 
						|
                self.documentObject.formatTuple >= (5, 0)
 | 
						|
                and "values" in axisElement.attrib
 | 
						|
            ):
 | 
						|
                axisObject = self.discreteAxisDescriptorClass()
 | 
						|
                axisObject.values = [
 | 
						|
                    float(s) for s in axisElement.attrib["values"].split(" ")
 | 
						|
                ]
 | 
						|
            else:
 | 
						|
                axisObject = self.axisDescriptorClass()
 | 
						|
                axisObject.minimum = float(axisElement.attrib.get("minimum"))
 | 
						|
                axisObject.maximum = float(axisElement.attrib.get("maximum"))
 | 
						|
            axisObject.default = float(axisElement.attrib.get("default"))
 | 
						|
            axisObject.name = axisElement.attrib.get("name")
 | 
						|
            if axisElement.attrib.get("hidden", False):
 | 
						|
                axisObject.hidden = True
 | 
						|
            axisObject.tag = axisElement.attrib.get("tag")
 | 
						|
            for mapElement in axisElement.findall("map"):
 | 
						|
                a = float(mapElement.attrib["input"])
 | 
						|
                b = float(mapElement.attrib["output"])
 | 
						|
                axisObject.map.append((a, b))
 | 
						|
            for labelNameElement in axisElement.findall("labelname"):
 | 
						|
                # Note: elementtree reads the "xml:lang" attribute name as
 | 
						|
                # '{http://www.w3.org/XML/1998/namespace}lang'
 | 
						|
                for key, lang in labelNameElement.items():
 | 
						|
                    if key == XML_LANG:
 | 
						|
                        axisObject.labelNames[lang] = tostr(labelNameElement.text)
 | 
						|
            labelElement = axisElement.find(".labels")
 | 
						|
            if labelElement is not None:
 | 
						|
                if "ordering" in labelElement.attrib:
 | 
						|
                    axisObject.axisOrdering = int(labelElement.attrib["ordering"])
 | 
						|
                for label in labelElement.findall(".label"):
 | 
						|
                    axisObject.axisLabels.append(self.readAxisLabel(label))
 | 
						|
            self.documentObject.axes.append(axisObject)
 | 
						|
            self.axisDefaults[axisObject.name] = axisObject.default
 | 
						|
 | 
						|
        self.documentObject.axisMappings = []
 | 
						|
        for mappingsElement in self.root.findall(".axes/mappings"):
 | 
						|
            groupDescription = mappingsElement.attrib.get("description")
 | 
						|
            for mappingElement in mappingsElement.findall("mapping"):
 | 
						|
                description = mappingElement.attrib.get("description")
 | 
						|
                inputElement = mappingElement.find("input")
 | 
						|
                outputElement = mappingElement.find("output")
 | 
						|
                inputLoc = {}
 | 
						|
                outputLoc = {}
 | 
						|
                for dimElement in inputElement.findall(".dimension"):
 | 
						|
                    name = dimElement.attrib["name"]
 | 
						|
                    value = float(dimElement.attrib["xvalue"])
 | 
						|
                    inputLoc[name] = value
 | 
						|
                for dimElement in outputElement.findall(".dimension"):
 | 
						|
                    name = dimElement.attrib["name"]
 | 
						|
                    value = float(dimElement.attrib["xvalue"])
 | 
						|
                    outputLoc[name] = value
 | 
						|
                axisMappingObject = self.axisMappingDescriptorClass(
 | 
						|
                    inputLocation=inputLoc,
 | 
						|
                    outputLocation=outputLoc,
 | 
						|
                    description=description,
 | 
						|
                    groupDescription=groupDescription,
 | 
						|
                )
 | 
						|
                self.documentObject.axisMappings.append(axisMappingObject)
 | 
						|
 | 
						|
    def readAxisLabel(self, element: ET.Element):
 | 
						|
        xml_attrs = {
 | 
						|
            "userminimum",
 | 
						|
            "uservalue",
 | 
						|
            "usermaximum",
 | 
						|
            "name",
 | 
						|
            "elidable",
 | 
						|
            "oldersibling",
 | 
						|
            "linkeduservalue",
 | 
						|
        }
 | 
						|
        unknown_attrs = set(element.attrib) - xml_attrs
 | 
						|
        if unknown_attrs:
 | 
						|
            raise DesignSpaceDocumentError(
 | 
						|
                f"label element contains unknown attributes: {', '.join(unknown_attrs)}"
 | 
						|
            )
 | 
						|
 | 
						|
        name = element.get("name")
 | 
						|
        if name is None:
 | 
						|
            raise DesignSpaceDocumentError("label element must have a name attribute.")
 | 
						|
        valueStr = element.get("uservalue")
 | 
						|
        if valueStr is None:
 | 
						|
            raise DesignSpaceDocumentError(
 | 
						|
                "label element must have a uservalue attribute."
 | 
						|
            )
 | 
						|
        value = float(valueStr)
 | 
						|
        minimumStr = element.get("userminimum")
 | 
						|
        minimum = float(minimumStr) if minimumStr is not None else None
 | 
						|
        maximumStr = element.get("usermaximum")
 | 
						|
        maximum = float(maximumStr) if maximumStr is not None else None
 | 
						|
        linkedValueStr = element.get("linkeduservalue")
 | 
						|
        linkedValue = float(linkedValueStr) if linkedValueStr is not None else None
 | 
						|
        elidable = True if element.get("elidable") == "true" else False
 | 
						|
        olderSibling = True if element.get("oldersibling") == "true" else False
 | 
						|
        labelNames = {
 | 
						|
            lang: label_name.text or ""
 | 
						|
            for label_name in element.findall("labelname")
 | 
						|
            for attr, lang in label_name.items()
 | 
						|
            if attr == XML_LANG
 | 
						|
            # Note: elementtree reads the "xml:lang" attribute name as
 | 
						|
            # '{http://www.w3.org/XML/1998/namespace}lang'
 | 
						|
        }
 | 
						|
        return self.axisLabelDescriptorClass(
 | 
						|
            name=name,
 | 
						|
            userValue=value,
 | 
						|
            userMinimum=minimum,
 | 
						|
            userMaximum=maximum,
 | 
						|
            elidable=elidable,
 | 
						|
            olderSibling=olderSibling,
 | 
						|
            linkedUserValue=linkedValue,
 | 
						|
            labelNames=labelNames,
 | 
						|
        )
 | 
						|
 | 
						|
    def readLabels(self):
 | 
						|
        if self.documentObject.formatTuple < (5, 0):
 | 
						|
            return
 | 
						|
 | 
						|
        xml_attrs = {"name", "elidable", "oldersibling"}
 | 
						|
        for labelElement in self.root.findall(".labels/label"):
 | 
						|
            unknown_attrs = set(labelElement.attrib) - xml_attrs
 | 
						|
            if unknown_attrs:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    f"Label element contains unknown attributes: {', '.join(unknown_attrs)}"
 | 
						|
                )
 | 
						|
 | 
						|
            name = labelElement.get("name")
 | 
						|
            if name is None:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    "label element must have a name attribute."
 | 
						|
                )
 | 
						|
            designLocation, userLocation = self.locationFromElement(labelElement)
 | 
						|
            if designLocation:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    f'<label> element "{name}" must only have user locations (using uservalue="").'
 | 
						|
                )
 | 
						|
            elidable = True if labelElement.get("elidable") == "true" else False
 | 
						|
            olderSibling = True if labelElement.get("oldersibling") == "true" else False
 | 
						|
            labelNames = {
 | 
						|
                lang: label_name.text or ""
 | 
						|
                for label_name in labelElement.findall("labelname")
 | 
						|
                for attr, lang in label_name.items()
 | 
						|
                if attr == XML_LANG
 | 
						|
                # Note: elementtree reads the "xml:lang" attribute name as
 | 
						|
                # '{http://www.w3.org/XML/1998/namespace}lang'
 | 
						|
            }
 | 
						|
            locationLabel = self.locationLabelDescriptorClass(
 | 
						|
                name=name,
 | 
						|
                userLocation=userLocation,
 | 
						|
                elidable=elidable,
 | 
						|
                olderSibling=olderSibling,
 | 
						|
                labelNames=labelNames,
 | 
						|
            )
 | 
						|
            self.documentObject.locationLabels.append(locationLabel)
 | 
						|
 | 
						|
    def readVariableFonts(self):
 | 
						|
        if self.documentObject.formatTuple < (5, 0):
 | 
						|
            return
 | 
						|
 | 
						|
        xml_attrs = {"name", "filename"}
 | 
						|
        for variableFontElement in self.root.findall(".variable-fonts/variable-font"):
 | 
						|
            unknown_attrs = set(variableFontElement.attrib) - xml_attrs
 | 
						|
            if unknown_attrs:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    f"variable-font element contains unknown attributes: {', '.join(unknown_attrs)}"
 | 
						|
                )
 | 
						|
 | 
						|
            name = variableFontElement.get("name")
 | 
						|
            if name is None:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    "variable-font element must have a name attribute."
 | 
						|
                )
 | 
						|
 | 
						|
            filename = variableFontElement.get("filename")
 | 
						|
 | 
						|
            axisSubsetsElement = variableFontElement.find(".axis-subsets")
 | 
						|
            if axisSubsetsElement is None:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    "variable-font element must contain an axis-subsets element."
 | 
						|
                )
 | 
						|
            axisSubsets = []
 | 
						|
            for axisSubset in axisSubsetsElement.iterfind(".axis-subset"):
 | 
						|
                axisSubsets.append(self.readAxisSubset(axisSubset))
 | 
						|
 | 
						|
            lib = None
 | 
						|
            libElement = variableFontElement.find(".lib")
 | 
						|
            if libElement is not None:
 | 
						|
                lib = plistlib.fromtree(libElement[0])
 | 
						|
 | 
						|
            variableFont = self.variableFontsDescriptorClass(
 | 
						|
                name=name,
 | 
						|
                filename=filename,
 | 
						|
                axisSubsets=axisSubsets,
 | 
						|
                lib=lib,
 | 
						|
            )
 | 
						|
            self.documentObject.variableFonts.append(variableFont)
 | 
						|
 | 
						|
    def readAxisSubset(self, element: ET.Element):
 | 
						|
        if "uservalue" in element.attrib:
 | 
						|
            xml_attrs = {"name", "uservalue"}
 | 
						|
            unknown_attrs = set(element.attrib) - xml_attrs
 | 
						|
            if unknown_attrs:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    f"axis-subset element contains unknown attributes: {', '.join(unknown_attrs)}"
 | 
						|
                )
 | 
						|
 | 
						|
            name = element.get("name")
 | 
						|
            if name is None:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    "axis-subset element must have a name attribute."
 | 
						|
                )
 | 
						|
            userValueStr = element.get("uservalue")
 | 
						|
            if userValueStr is None:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    "The axis-subset element for a discrete subset must have a uservalue attribute."
 | 
						|
                )
 | 
						|
            userValue = float(userValueStr)
 | 
						|
 | 
						|
            return self.valueAxisSubsetDescriptorClass(name=name, userValue=userValue)
 | 
						|
        else:
 | 
						|
            xml_attrs = {"name", "userminimum", "userdefault", "usermaximum"}
 | 
						|
            unknown_attrs = set(element.attrib) - xml_attrs
 | 
						|
            if unknown_attrs:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    f"axis-subset element contains unknown attributes: {', '.join(unknown_attrs)}"
 | 
						|
                )
 | 
						|
 | 
						|
            name = element.get("name")
 | 
						|
            if name is None:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    "axis-subset element must have a name attribute."
 | 
						|
                )
 | 
						|
 | 
						|
            userMinimum = element.get("userminimum")
 | 
						|
            userDefault = element.get("userdefault")
 | 
						|
            userMaximum = element.get("usermaximum")
 | 
						|
            if (
 | 
						|
                userMinimum is not None
 | 
						|
                and userDefault is not None
 | 
						|
                and userMaximum is not None
 | 
						|
            ):
 | 
						|
                return self.rangeAxisSubsetDescriptorClass(
 | 
						|
                    name=name,
 | 
						|
                    userMinimum=float(userMinimum),
 | 
						|
                    userDefault=float(userDefault),
 | 
						|
                    userMaximum=float(userMaximum),
 | 
						|
                )
 | 
						|
            if all(v is None for v in (userMinimum, userDefault, userMaximum)):
 | 
						|
                return self.rangeAxisSubsetDescriptorClass(name=name)
 | 
						|
 | 
						|
            raise DesignSpaceDocumentError(
 | 
						|
                "axis-subset element must have min/max/default values or none at all."
 | 
						|
            )
 | 
						|
 | 
						|
    def readSources(self):
 | 
						|
        for sourceCount, sourceElement in enumerate(
 | 
						|
            self.root.findall(".sources/source")
 | 
						|
        ):
 | 
						|
            filename = sourceElement.attrib.get("filename")
 | 
						|
            if filename is not None and self.path is not None:
 | 
						|
                sourcePath = os.path.abspath(
 | 
						|
                    os.path.join(os.path.dirname(self.path), filename)
 | 
						|
                )
 | 
						|
            else:
 | 
						|
                sourcePath = None
 | 
						|
            sourceName = sourceElement.attrib.get("name")
 | 
						|
            if sourceName is None:
 | 
						|
                # add a temporary source name
 | 
						|
                sourceName = "temp_master.%d" % (sourceCount)
 | 
						|
            sourceObject = self.sourceDescriptorClass()
 | 
						|
            sourceObject.path = sourcePath  # absolute path to the ufo source
 | 
						|
            sourceObject.filename = filename  # path as it is stored in the document
 | 
						|
            sourceObject.name = sourceName
 | 
						|
            familyName = sourceElement.attrib.get("familyname")
 | 
						|
            if familyName is not None:
 | 
						|
                sourceObject.familyName = familyName
 | 
						|
            styleName = sourceElement.attrib.get("stylename")
 | 
						|
            if styleName is not None:
 | 
						|
                sourceObject.styleName = styleName
 | 
						|
            for familyNameElement in sourceElement.findall("familyname"):
 | 
						|
                for key, lang in familyNameElement.items():
 | 
						|
                    if key == XML_LANG:
 | 
						|
                        familyName = familyNameElement.text
 | 
						|
                        sourceObject.setFamilyName(familyName, lang)
 | 
						|
            designLocation, userLocation = self.locationFromElement(sourceElement)
 | 
						|
            if userLocation:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    f'<source> element "{sourceName}" must only have design locations (using xvalue="").'
 | 
						|
                )
 | 
						|
            sourceObject.location = designLocation
 | 
						|
            layerName = sourceElement.attrib.get("layer")
 | 
						|
            if layerName is not None:
 | 
						|
                sourceObject.layerName = layerName
 | 
						|
            for libElement in sourceElement.findall(".lib"):
 | 
						|
                if libElement.attrib.get("copy") == "1":
 | 
						|
                    sourceObject.copyLib = True
 | 
						|
            for groupsElement in sourceElement.findall(".groups"):
 | 
						|
                if groupsElement.attrib.get("copy") == "1":
 | 
						|
                    sourceObject.copyGroups = True
 | 
						|
            for infoElement in sourceElement.findall(".info"):
 | 
						|
                if infoElement.attrib.get("copy") == "1":
 | 
						|
                    sourceObject.copyInfo = True
 | 
						|
                if infoElement.attrib.get("mute") == "1":
 | 
						|
                    sourceObject.muteInfo = True
 | 
						|
            for featuresElement in sourceElement.findall(".features"):
 | 
						|
                if featuresElement.attrib.get("copy") == "1":
 | 
						|
                    sourceObject.copyFeatures = True
 | 
						|
            for glyphElement in sourceElement.findall(".glyph"):
 | 
						|
                glyphName = glyphElement.attrib.get("name")
 | 
						|
                if glyphName is None:
 | 
						|
                    continue
 | 
						|
                if glyphElement.attrib.get("mute") == "1":
 | 
						|
                    sourceObject.mutedGlyphNames.append(glyphName)
 | 
						|
            for kerningElement in sourceElement.findall(".kerning"):
 | 
						|
                if kerningElement.attrib.get("mute") == "1":
 | 
						|
                    sourceObject.muteKerning = True
 | 
						|
            self.documentObject.sources.append(sourceObject)
 | 
						|
 | 
						|
    def locationFromElement(self, element):
 | 
						|
        """Read a nested ``<location>`` element inside the given ``element``.
 | 
						|
 | 
						|
        .. versionchanged:: 5.0
 | 
						|
           Return a tuple of (designLocation, userLocation)
 | 
						|
        """
 | 
						|
        elementLocation = (None, None)
 | 
						|
        for locationElement in element.findall(".location"):
 | 
						|
            elementLocation = self.readLocationElement(locationElement)
 | 
						|
            break
 | 
						|
        return elementLocation
 | 
						|
 | 
						|
    def readLocationElement(self, locationElement):
 | 
						|
        """Read a ``<location>`` element.
 | 
						|
 | 
						|
        .. versionchanged:: 5.0
 | 
						|
           Return a tuple of (designLocation, userLocation)
 | 
						|
        """
 | 
						|
        if self._strictAxisNames and not self.documentObject.axes:
 | 
						|
            raise DesignSpaceDocumentError("No axes defined")
 | 
						|
        userLoc = {}
 | 
						|
        designLoc = {}
 | 
						|
        for dimensionElement in locationElement.findall(".dimension"):
 | 
						|
            dimName = dimensionElement.attrib.get("name")
 | 
						|
            if self._strictAxisNames and dimName not in self.axisDefaults:
 | 
						|
                # In case the document contains no axis definitions,
 | 
						|
                self.log.warning('Location with undefined axis: "%s".', dimName)
 | 
						|
                continue
 | 
						|
            userValue = xValue = yValue = None
 | 
						|
            try:
 | 
						|
                userValue = dimensionElement.attrib.get("uservalue")
 | 
						|
                if userValue is not None:
 | 
						|
                    userValue = float(userValue)
 | 
						|
            except ValueError:
 | 
						|
                self.log.warning(
 | 
						|
                    "ValueError in readLocation userValue %3.3f", userValue
 | 
						|
                )
 | 
						|
            try:
 | 
						|
                xValue = dimensionElement.attrib.get("xvalue")
 | 
						|
                if xValue is not None:
 | 
						|
                    xValue = float(xValue)
 | 
						|
            except ValueError:
 | 
						|
                self.log.warning("ValueError in readLocation xValue %3.3f", xValue)
 | 
						|
            try:
 | 
						|
                yValue = dimensionElement.attrib.get("yvalue")
 | 
						|
                if yValue is not None:
 | 
						|
                    yValue = float(yValue)
 | 
						|
            except ValueError:
 | 
						|
                self.log.warning("ValueError in readLocation yValue %3.3f", yValue)
 | 
						|
            if userValue is None == xValue is None:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    f'Exactly one of uservalue="" or xvalue="" must be provided for location dimension "{dimName}"'
 | 
						|
                )
 | 
						|
            if yValue is not None:
 | 
						|
                if xValue is None:
 | 
						|
                    raise DesignSpaceDocumentError(
 | 
						|
                        f'Missing xvalue="" for the location dimension "{dimName}"" with yvalue="{yValue}"'
 | 
						|
                    )
 | 
						|
                designLoc[dimName] = (xValue, yValue)
 | 
						|
            elif xValue is not None:
 | 
						|
                designLoc[dimName] = xValue
 | 
						|
            else:
 | 
						|
                userLoc[dimName] = userValue
 | 
						|
        return designLoc, userLoc
 | 
						|
 | 
						|
    def readInstances(self, makeGlyphs=True, makeKerning=True, makeInfo=True):
 | 
						|
        instanceElements = self.root.findall(".instances/instance")
 | 
						|
        for instanceElement in instanceElements:
 | 
						|
            self._readSingleInstanceElement(
 | 
						|
                instanceElement,
 | 
						|
                makeGlyphs=makeGlyphs,
 | 
						|
                makeKerning=makeKerning,
 | 
						|
                makeInfo=makeInfo,
 | 
						|
            )
 | 
						|
 | 
						|
    def _readSingleInstanceElement(
 | 
						|
        self, instanceElement, makeGlyphs=True, makeKerning=True, makeInfo=True
 | 
						|
    ):
 | 
						|
        filename = instanceElement.attrib.get("filename")
 | 
						|
        if filename is not None and self.documentObject.path is not None:
 | 
						|
            instancePath = os.path.join(
 | 
						|
                os.path.dirname(self.documentObject.path), filename
 | 
						|
            )
 | 
						|
        else:
 | 
						|
            instancePath = None
 | 
						|
        instanceObject = self.instanceDescriptorClass()
 | 
						|
        instanceObject.path = instancePath  # absolute path to the instance
 | 
						|
        instanceObject.filename = filename  # path as it is stored in the document
 | 
						|
        name = instanceElement.attrib.get("name")
 | 
						|
        if name is not None:
 | 
						|
            instanceObject.name = name
 | 
						|
        familyname = instanceElement.attrib.get("familyname")
 | 
						|
        if familyname is not None:
 | 
						|
            instanceObject.familyName = familyname
 | 
						|
        stylename = instanceElement.attrib.get("stylename")
 | 
						|
        if stylename is not None:
 | 
						|
            instanceObject.styleName = stylename
 | 
						|
        postScriptFontName = instanceElement.attrib.get("postscriptfontname")
 | 
						|
        if postScriptFontName is not None:
 | 
						|
            instanceObject.postScriptFontName = postScriptFontName
 | 
						|
        styleMapFamilyName = instanceElement.attrib.get("stylemapfamilyname")
 | 
						|
        if styleMapFamilyName is not None:
 | 
						|
            instanceObject.styleMapFamilyName = styleMapFamilyName
 | 
						|
        styleMapStyleName = instanceElement.attrib.get("stylemapstylename")
 | 
						|
        if styleMapStyleName is not None:
 | 
						|
            instanceObject.styleMapStyleName = styleMapStyleName
 | 
						|
        # read localised names
 | 
						|
        for styleNameElement in instanceElement.findall("stylename"):
 | 
						|
            for key, lang in styleNameElement.items():
 | 
						|
                if key == XML_LANG:
 | 
						|
                    styleName = styleNameElement.text
 | 
						|
                    instanceObject.setStyleName(styleName, lang)
 | 
						|
        for familyNameElement in instanceElement.findall("familyname"):
 | 
						|
            for key, lang in familyNameElement.items():
 | 
						|
                if key == XML_LANG:
 | 
						|
                    familyName = familyNameElement.text
 | 
						|
                    instanceObject.setFamilyName(familyName, lang)
 | 
						|
        for styleMapStyleNameElement in instanceElement.findall("stylemapstylename"):
 | 
						|
            for key, lang in styleMapStyleNameElement.items():
 | 
						|
                if key == XML_LANG:
 | 
						|
                    styleMapStyleName = styleMapStyleNameElement.text
 | 
						|
                    instanceObject.setStyleMapStyleName(styleMapStyleName, lang)
 | 
						|
        for styleMapFamilyNameElement in instanceElement.findall("stylemapfamilyname"):
 | 
						|
            for key, lang in styleMapFamilyNameElement.items():
 | 
						|
                if key == XML_LANG:
 | 
						|
                    styleMapFamilyName = styleMapFamilyNameElement.text
 | 
						|
                    instanceObject.setStyleMapFamilyName(styleMapFamilyName, lang)
 | 
						|
        designLocation, userLocation = self.locationFromElement(instanceElement)
 | 
						|
        locationLabel = instanceElement.attrib.get("location")
 | 
						|
        if (designLocation or userLocation) and locationLabel is not None:
 | 
						|
            raise DesignSpaceDocumentError(
 | 
						|
                'instance element must have at most one of the location="..." attribute or the nested location element'
 | 
						|
            )
 | 
						|
        instanceObject.locationLabel = locationLabel
 | 
						|
        instanceObject.userLocation = userLocation or {}
 | 
						|
        instanceObject.designLocation = designLocation or {}
 | 
						|
        for glyphElement in instanceElement.findall(".glyphs/glyph"):
 | 
						|
            self.readGlyphElement(glyphElement, instanceObject)
 | 
						|
        for infoElement in instanceElement.findall("info"):
 | 
						|
            self.readInfoElement(infoElement, instanceObject)
 | 
						|
        for libElement in instanceElement.findall("lib"):
 | 
						|
            self.readLibElement(libElement, instanceObject)
 | 
						|
        self.documentObject.instances.append(instanceObject)
 | 
						|
 | 
						|
    def readLibElement(self, libElement, instanceObject):
 | 
						|
        """Read the lib element for the given instance."""
 | 
						|
        instanceObject.lib = plistlib.fromtree(libElement[0])
 | 
						|
 | 
						|
    def readInfoElement(self, infoElement, instanceObject):
 | 
						|
        """Read the info element."""
 | 
						|
        instanceObject.info = True
 | 
						|
 | 
						|
    def readGlyphElement(self, glyphElement, instanceObject):
 | 
						|
        """
 | 
						|
        Read the glyph element, which could look like either one of these:
 | 
						|
 | 
						|
        .. code-block:: xml
 | 
						|
 | 
						|
            <glyph name="b" unicode="0x62"/>
 | 
						|
 | 
						|
            <glyph name="b"/>
 | 
						|
 | 
						|
            <glyph name="b">
 | 
						|
                <master location="location-token-bbb" source="master-token-aaa2"/>
 | 
						|
                <master glyphname="b.alt1" location="location-token-ccc" source="master-token-aaa3"/>
 | 
						|
                <note>
 | 
						|
                    This is an instance from an anisotropic interpolation.
 | 
						|
                </note>
 | 
						|
            </glyph>
 | 
						|
        """
 | 
						|
        glyphData = {}
 | 
						|
        glyphName = glyphElement.attrib.get("name")
 | 
						|
        if glyphName is None:
 | 
						|
            raise DesignSpaceDocumentError("Glyph object without name attribute")
 | 
						|
        mute = glyphElement.attrib.get("mute")
 | 
						|
        if mute == "1":
 | 
						|
            glyphData["mute"] = True
 | 
						|
        # unicode
 | 
						|
        unicodes = glyphElement.attrib.get("unicode")
 | 
						|
        if unicodes is not None:
 | 
						|
            try:
 | 
						|
                unicodes = [int(u, 16) for u in unicodes.split(" ")]
 | 
						|
                glyphData["unicodes"] = unicodes
 | 
						|
            except ValueError:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    "unicode values %s are not integers" % unicodes
 | 
						|
                )
 | 
						|
 | 
						|
        for noteElement in glyphElement.findall(".note"):
 | 
						|
            glyphData["note"] = noteElement.text
 | 
						|
            break
 | 
						|
        designLocation, userLocation = self.locationFromElement(glyphElement)
 | 
						|
        if userLocation:
 | 
						|
            raise DesignSpaceDocumentError(
 | 
						|
                f'<glyph> element "{glyphName}" must only have design locations (using xvalue="").'
 | 
						|
            )
 | 
						|
        if designLocation is not None:
 | 
						|
            glyphData["instanceLocation"] = designLocation
 | 
						|
        glyphSources = None
 | 
						|
        for masterElement in glyphElement.findall(".masters/master"):
 | 
						|
            fontSourceName = masterElement.attrib.get("source")
 | 
						|
            designLocation, userLocation = self.locationFromElement(masterElement)
 | 
						|
            if userLocation:
 | 
						|
                raise DesignSpaceDocumentError(
 | 
						|
                    f'<master> element "{fontSourceName}" must only have design locations (using xvalue="").'
 | 
						|
                )
 | 
						|
            masterGlyphName = masterElement.attrib.get("glyphname")
 | 
						|
            if masterGlyphName is None:
 | 
						|
                # if we don't read a glyphname, use the one we have
 | 
						|
                masterGlyphName = glyphName
 | 
						|
            d = dict(
 | 
						|
                font=fontSourceName, location=designLocation, glyphName=masterGlyphName
 | 
						|
            )
 | 
						|
            if glyphSources is None:
 | 
						|
                glyphSources = []
 | 
						|
            glyphSources.append(d)
 | 
						|
        if glyphSources is not None:
 | 
						|
            glyphData["masters"] = glyphSources
 | 
						|
        instanceObject.glyphs[glyphName] = glyphData
 | 
						|
 | 
						|
    def readLib(self):
 | 
						|
        """Read the lib element for the whole document."""
 | 
						|
        for libElement in self.root.findall(".lib"):
 | 
						|
            self.documentObject.lib = plistlib.fromtree(libElement[0])
 | 
						|
 | 
						|
 | 
						|
class DesignSpaceDocument(LogMixin, AsDictMixin):
 | 
						|
    """The DesignSpaceDocument object can read and write ``.designspace`` data.
 | 
						|
    It imports the axes, sources, variable fonts and instances to very basic
 | 
						|
    **descriptor** objects that store the data in attributes. Data is added to
 | 
						|
    the document by creating such descriptor objects, filling them with data
 | 
						|
    and then adding them to the document. This makes it easy to integrate this
 | 
						|
    object in different contexts.
 | 
						|
 | 
						|
    The **DesignSpaceDocument** object can be subclassed to work with
 | 
						|
    different objects, as long as they have the same attributes. Reader and
 | 
						|
    Writer objects can be subclassed as well.
 | 
						|
 | 
						|
    **Note:** Python attribute names are usually camelCased, the
 | 
						|
    corresponding `XML <document-xml-structure>`_ attributes are usually
 | 
						|
    all lowercase.
 | 
						|
 | 
						|
    .. code:: python
 | 
						|
 | 
						|
        from fontTools.designspaceLib import DesignSpaceDocument
 | 
						|
        doc = DesignSpaceDocument.fromfile("some/path/to/my.designspace")
 | 
						|
        doc.formatVersion
 | 
						|
        doc.elidedFallbackName
 | 
						|
        doc.axes
 | 
						|
        doc.axisMappings
 | 
						|
        doc.locationLabels
 | 
						|
        doc.rules
 | 
						|
        doc.rulesProcessingLast
 | 
						|
        doc.sources
 | 
						|
        doc.variableFonts
 | 
						|
        doc.instances
 | 
						|
        doc.lib
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, readerClass=None, writerClass=None):
 | 
						|
        self.path = None
 | 
						|
        """String, optional. When the document is read from the disk, this is
 | 
						|
        the full path that was given to :meth:`read` or :meth:`fromfile`.
 | 
						|
        """
 | 
						|
        self.filename = None
 | 
						|
        """String, optional. When the document is read from the disk, this is
 | 
						|
        its original file name, i.e. the last part of its path.
 | 
						|
 | 
						|
        When the document is produced by a Python script and still only exists
 | 
						|
        in memory, the producing script can write here an indication of a
 | 
						|
        possible "good" filename, in case one wants to save the file somewhere.
 | 
						|
        """
 | 
						|
 | 
						|
        self.formatVersion: Optional[str] = None
 | 
						|
        """Format version for this document, as a string. E.g. "4.0" """
 | 
						|
 | 
						|
        self.elidedFallbackName: Optional[str] = None
 | 
						|
        """STAT Style Attributes Header field ``elidedFallbackNameID``.
 | 
						|
 | 
						|
        See: `OTSpec STAT Style Attributes Header <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#style-attributes-header>`_
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
 | 
						|
        self.axes: List[Union[AxisDescriptor, DiscreteAxisDescriptor]] = []
 | 
						|
        """List of this document's axes."""
 | 
						|
 | 
						|
        self.axisMappings: List[AxisMappingDescriptor] = []
 | 
						|
        """List of this document's axis mappings."""
 | 
						|
 | 
						|
        self.locationLabels: List[LocationLabelDescriptor] = []
 | 
						|
        """List of this document's STAT format 4 labels.
 | 
						|
 | 
						|
        .. versionadded:: 5.0"""
 | 
						|
        self.rules: List[RuleDescriptor] = []
 | 
						|
        """List of this document's rules."""
 | 
						|
        self.rulesProcessingLast: bool = False
 | 
						|
        """This flag indicates whether the substitution rules should be applied
 | 
						|
        before or after other glyph substitution features.
 | 
						|
 | 
						|
        - False: before
 | 
						|
        - True: after.
 | 
						|
 | 
						|
        Default is False. For new projects, you probably want True. See
 | 
						|
        the following issues for more information:
 | 
						|
        `fontTools#1371 <https://github.com/fonttools/fonttools/issues/1371#issuecomment-590214572>`__
 | 
						|
        `fontTools#2050 <https://github.com/fonttools/fonttools/issues/2050#issuecomment-678691020>`__
 | 
						|
 | 
						|
        If you want to use a different feature altogether, e.g. ``calt``,
 | 
						|
        use the lib key ``com.github.fonttools.varLib.featureVarsFeatureTag``
 | 
						|
 | 
						|
        .. code:: xml
 | 
						|
 | 
						|
            <lib>
 | 
						|
                <dict>
 | 
						|
                    <key>com.github.fonttools.varLib.featureVarsFeatureTag</key>
 | 
						|
                    <string>calt</string>
 | 
						|
                </dict>
 | 
						|
            </lib>
 | 
						|
        """
 | 
						|
        self.sources: List[SourceDescriptor] = []
 | 
						|
        """List of this document's sources."""
 | 
						|
        self.variableFonts: List[VariableFontDescriptor] = []
 | 
						|
        """List of this document's variable fonts.
 | 
						|
 | 
						|
        .. versionadded:: 5.0"""
 | 
						|
        self.instances: List[InstanceDescriptor] = []
 | 
						|
        """List of this document's instances."""
 | 
						|
        self.lib: Dict = {}
 | 
						|
        """User defined, custom data associated with the whole document.
 | 
						|
 | 
						|
        Use reverse-DNS notation to identify your own data.
 | 
						|
        Respect the data stored by others.
 | 
						|
        """
 | 
						|
 | 
						|
        self.default: Optional[str] = None
 | 
						|
        """Name of the default master.
 | 
						|
 | 
						|
        This attribute is updated by the :meth:`findDefault`
 | 
						|
        """
 | 
						|
 | 
						|
        if readerClass is not None:
 | 
						|
            self.readerClass = readerClass
 | 
						|
        else:
 | 
						|
            self.readerClass = BaseDocReader
 | 
						|
        if writerClass is not None:
 | 
						|
            self.writerClass = writerClass
 | 
						|
        else:
 | 
						|
            self.writerClass = BaseDocWriter
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def fromfile(cls, path, readerClass=None, writerClass=None):
 | 
						|
        """Read a designspace file from ``path`` and return a new instance of
 | 
						|
        :class:.
 | 
						|
        """
 | 
						|
        self = cls(readerClass=readerClass, writerClass=writerClass)
 | 
						|
        self.read(path)
 | 
						|
        return self
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def fromstring(cls, string, readerClass=None, writerClass=None):
 | 
						|
        self = cls(readerClass=readerClass, writerClass=writerClass)
 | 
						|
        reader = self.readerClass.fromstring(string, self)
 | 
						|
        reader.read()
 | 
						|
        if self.sources:
 | 
						|
            self.findDefault()
 | 
						|
        return self
 | 
						|
 | 
						|
    def tostring(self, encoding=None):
 | 
						|
        """Returns the designspace as a string. Default encoding ``utf-8``."""
 | 
						|
        if encoding is str or (encoding is not None and encoding.lower() == "unicode"):
 | 
						|
            f = StringIO()
 | 
						|
            xml_declaration = False
 | 
						|
        elif encoding is None or encoding == "utf-8":
 | 
						|
            f = BytesIO()
 | 
						|
            encoding = "UTF-8"
 | 
						|
            xml_declaration = True
 | 
						|
        else:
 | 
						|
            raise ValueError("unsupported encoding: '%s'" % encoding)
 | 
						|
        writer = self.writerClass(f, self)
 | 
						|
        writer.write(encoding=encoding, xml_declaration=xml_declaration)
 | 
						|
        return f.getvalue()
 | 
						|
 | 
						|
    def read(self, path):
 | 
						|
        """Read a designspace file from ``path`` and populates the fields of
 | 
						|
        ``self`` with the data.
 | 
						|
        """
 | 
						|
        if hasattr(path, "__fspath__"):  # support os.PathLike objects
 | 
						|
            path = path.__fspath__()
 | 
						|
        self.path = path
 | 
						|
        self.filename = os.path.basename(path)
 | 
						|
        reader = self.readerClass(path, self)
 | 
						|
        reader.read()
 | 
						|
        if self.sources:
 | 
						|
            self.findDefault()
 | 
						|
 | 
						|
    def write(self, path):
 | 
						|
        """Write this designspace to ``path``."""
 | 
						|
        if hasattr(path, "__fspath__"):  # support os.PathLike objects
 | 
						|
            path = path.__fspath__()
 | 
						|
        self.path = path
 | 
						|
        self.filename = os.path.basename(path)
 | 
						|
        self.updatePaths()
 | 
						|
        writer = self.writerClass(path, self)
 | 
						|
        writer.write()
 | 
						|
 | 
						|
    def _posixRelativePath(self, otherPath):
 | 
						|
        relative = os.path.relpath(otherPath, os.path.dirname(self.path))
 | 
						|
        return posix(relative)
 | 
						|
 | 
						|
    def updatePaths(self):
 | 
						|
        """
 | 
						|
        Right before we save we need to identify and respond to the following situations:
 | 
						|
        In each descriptor, we have to do the right thing for the filename attribute.
 | 
						|
 | 
						|
        ::
 | 
						|
 | 
						|
            case 1.
 | 
						|
            descriptor.filename == None
 | 
						|
            descriptor.path == None
 | 
						|
 | 
						|
            -- action:
 | 
						|
            write as is, descriptors will not have a filename attr.
 | 
						|
            useless, but no reason to interfere.
 | 
						|
 | 
						|
 | 
						|
            case 2.
 | 
						|
            descriptor.filename == "../something"
 | 
						|
            descriptor.path == None
 | 
						|
 | 
						|
            -- action:
 | 
						|
            write as is. The filename attr should not be touched.
 | 
						|
 | 
						|
 | 
						|
            case 3.
 | 
						|
            descriptor.filename == None
 | 
						|
            descriptor.path == "~/absolute/path/there"
 | 
						|
 | 
						|
            -- action:
 | 
						|
            calculate the relative path for filename.
 | 
						|
            We're not overwriting some other value for filename, it should be fine
 | 
						|
 | 
						|
 | 
						|
            case 4.
 | 
						|
            descriptor.filename == '../somewhere'
 | 
						|
            descriptor.path == "~/absolute/path/there"
 | 
						|
 | 
						|
            -- action:
 | 
						|
            there is a conflict between the given filename, and the path.
 | 
						|
            So we know where the file is relative to the document.
 | 
						|
            Can't guess why they're different, we just choose for path to be correct and update filename.
 | 
						|
        """
 | 
						|
        assert self.path is not None
 | 
						|
        for descriptor in self.sources + self.instances:
 | 
						|
            if descriptor.path is not None:
 | 
						|
                # case 3 and 4: filename gets updated and relativized
 | 
						|
                descriptor.filename = self._posixRelativePath(descriptor.path)
 | 
						|
 | 
						|
    def addSource(self, sourceDescriptor: SourceDescriptor):
 | 
						|
        """Add the given ``sourceDescriptor`` to ``doc.sources``."""
 | 
						|
        self.sources.append(sourceDescriptor)
 | 
						|
 | 
						|
    def addSourceDescriptor(self, **kwargs):
 | 
						|
        """Instantiate a new :class:`SourceDescriptor` using the given
 | 
						|
        ``kwargs`` and add it to ``doc.sources``.
 | 
						|
        """
 | 
						|
        source = self.writerClass.sourceDescriptorClass(**kwargs)
 | 
						|
        self.addSource(source)
 | 
						|
        return source
 | 
						|
 | 
						|
    def addInstance(self, instanceDescriptor: InstanceDescriptor):
 | 
						|
        """Add the given ``instanceDescriptor`` to :attr:`instances`."""
 | 
						|
        self.instances.append(instanceDescriptor)
 | 
						|
 | 
						|
    def addInstanceDescriptor(self, **kwargs):
 | 
						|
        """Instantiate a new :class:`InstanceDescriptor` using the given
 | 
						|
        ``kwargs`` and add it to :attr:`instances`.
 | 
						|
        """
 | 
						|
        instance = self.writerClass.instanceDescriptorClass(**kwargs)
 | 
						|
        self.addInstance(instance)
 | 
						|
        return instance
 | 
						|
 | 
						|
    def addAxis(self, axisDescriptor: Union[AxisDescriptor, DiscreteAxisDescriptor]):
 | 
						|
        """Add the given ``axisDescriptor`` to :attr:`axes`."""
 | 
						|
        self.axes.append(axisDescriptor)
 | 
						|
 | 
						|
    def addAxisDescriptor(self, **kwargs):
 | 
						|
        """Instantiate a new :class:`AxisDescriptor` using the given
 | 
						|
        ``kwargs`` and add it to :attr:`axes`.
 | 
						|
 | 
						|
        The axis will be and instance of :class:`DiscreteAxisDescriptor` if
 | 
						|
        the ``kwargs`` provide a ``value``, or a :class:`AxisDescriptor` otherwise.
 | 
						|
        """
 | 
						|
        if "values" in kwargs:
 | 
						|
            axis = self.writerClass.discreteAxisDescriptorClass(**kwargs)
 | 
						|
        else:
 | 
						|
            axis = self.writerClass.axisDescriptorClass(**kwargs)
 | 
						|
        self.addAxis(axis)
 | 
						|
        return axis
 | 
						|
 | 
						|
    def addAxisMapping(self, axisMappingDescriptor: AxisMappingDescriptor):
 | 
						|
        """Add the given ``axisMappingDescriptor`` to :attr:`axisMappings`."""
 | 
						|
        self.axisMappings.append(axisMappingDescriptor)
 | 
						|
 | 
						|
    def addAxisMappingDescriptor(self, **kwargs):
 | 
						|
        """Instantiate a new :class:`AxisMappingDescriptor` using the given
 | 
						|
        ``kwargs`` and add it to :attr:`rules`.
 | 
						|
        """
 | 
						|
        axisMapping = self.writerClass.axisMappingDescriptorClass(**kwargs)
 | 
						|
        self.addAxisMapping(axisMapping)
 | 
						|
        return axisMapping
 | 
						|
 | 
						|
    def addRule(self, ruleDescriptor: RuleDescriptor):
 | 
						|
        """Add the given ``ruleDescriptor`` to :attr:`rules`."""
 | 
						|
        self.rules.append(ruleDescriptor)
 | 
						|
 | 
						|
    def addRuleDescriptor(self, **kwargs):
 | 
						|
        """Instantiate a new :class:`RuleDescriptor` using the given
 | 
						|
        ``kwargs`` and add it to :attr:`rules`.
 | 
						|
        """
 | 
						|
        rule = self.writerClass.ruleDescriptorClass(**kwargs)
 | 
						|
        self.addRule(rule)
 | 
						|
        return rule
 | 
						|
 | 
						|
    def addVariableFont(self, variableFontDescriptor: VariableFontDescriptor):
 | 
						|
        """Add the given ``variableFontDescriptor`` to :attr:`variableFonts`.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        self.variableFonts.append(variableFontDescriptor)
 | 
						|
 | 
						|
    def addVariableFontDescriptor(self, **kwargs):
 | 
						|
        """Instantiate a new :class:`VariableFontDescriptor` using the given
 | 
						|
        ``kwargs`` and add it to :attr:`variableFonts`.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        variableFont = self.writerClass.variableFontDescriptorClass(**kwargs)
 | 
						|
        self.addVariableFont(variableFont)
 | 
						|
        return variableFont
 | 
						|
 | 
						|
    def addLocationLabel(self, locationLabelDescriptor: LocationLabelDescriptor):
 | 
						|
        """Add the given ``locationLabelDescriptor`` to :attr:`locationLabels`.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        self.locationLabels.append(locationLabelDescriptor)
 | 
						|
 | 
						|
    def addLocationLabelDescriptor(self, **kwargs):
 | 
						|
        """Instantiate a new :class:`LocationLabelDescriptor` using the given
 | 
						|
        ``kwargs`` and add it to :attr:`locationLabels`.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        locationLabel = self.writerClass.locationLabelDescriptorClass(**kwargs)
 | 
						|
        self.addLocationLabel(locationLabel)
 | 
						|
        return locationLabel
 | 
						|
 | 
						|
    def newDefaultLocation(self):
 | 
						|
        """Return a dict with the default location in design space coordinates."""
 | 
						|
        # Without OrderedDict, output XML would be non-deterministic.
 | 
						|
        # https://github.com/LettError/designSpaceDocument/issues/10
 | 
						|
        loc = collections.OrderedDict()
 | 
						|
        for axisDescriptor in self.axes:
 | 
						|
            loc[axisDescriptor.name] = axisDescriptor.map_forward(
 | 
						|
                axisDescriptor.default
 | 
						|
            )
 | 
						|
        return loc
 | 
						|
 | 
						|
    def labelForUserLocation(
 | 
						|
        self, userLocation: SimpleLocationDict
 | 
						|
    ) -> Optional[LocationLabelDescriptor]:
 | 
						|
        """Return the :class:`LocationLabel` that matches the given
 | 
						|
        ``userLocation``, or ``None`` if no such label exists.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        return next(
 | 
						|
            (
 | 
						|
                label
 | 
						|
                for label in self.locationLabels
 | 
						|
                if label.userLocation == userLocation
 | 
						|
            ),
 | 
						|
            None,
 | 
						|
        )
 | 
						|
 | 
						|
    def updateFilenameFromPath(self, masters=True, instances=True, force=False):
 | 
						|
        """Set a descriptor filename attr from the path and this document path.
 | 
						|
 | 
						|
        If the filename attribute is not None: skip it.
 | 
						|
        """
 | 
						|
        if masters:
 | 
						|
            for descriptor in self.sources:
 | 
						|
                if descriptor.filename is not None and not force:
 | 
						|
                    continue
 | 
						|
                if self.path is not None:
 | 
						|
                    descriptor.filename = self._posixRelativePath(descriptor.path)
 | 
						|
        if instances:
 | 
						|
            for descriptor in self.instances:
 | 
						|
                if descriptor.filename is not None and not force:
 | 
						|
                    continue
 | 
						|
                if self.path is not None:
 | 
						|
                    descriptor.filename = self._posixRelativePath(descriptor.path)
 | 
						|
 | 
						|
    def newAxisDescriptor(self):
 | 
						|
        """Ask the writer class to make us a new axisDescriptor."""
 | 
						|
        return self.writerClass.getAxisDecriptor()
 | 
						|
 | 
						|
    def newSourceDescriptor(self):
 | 
						|
        """Ask the writer class to make us a new sourceDescriptor."""
 | 
						|
        return self.writerClass.getSourceDescriptor()
 | 
						|
 | 
						|
    def newInstanceDescriptor(self):
 | 
						|
        """Ask the writer class to make us a new instanceDescriptor."""
 | 
						|
        return self.writerClass.getInstanceDescriptor()
 | 
						|
 | 
						|
    def getAxisOrder(self):
 | 
						|
        """Return a list of axis names, in the same order as defined in the document."""
 | 
						|
        names = []
 | 
						|
        for axisDescriptor in self.axes:
 | 
						|
            names.append(axisDescriptor.name)
 | 
						|
        return names
 | 
						|
 | 
						|
    def getAxis(self, name: str) -> AxisDescriptor | DiscreteAxisDescriptor | None:
 | 
						|
        """Return the axis with the given ``name``, or ``None`` if no such axis exists."""
 | 
						|
        return next((axis for axis in self.axes if axis.name == name), None)
 | 
						|
 | 
						|
    def getAxisByTag(self, tag: str) -> AxisDescriptor | DiscreteAxisDescriptor | None:
 | 
						|
        """Return the axis with the given ``tag``, or ``None`` if no such axis exists."""
 | 
						|
        return next((axis for axis in self.axes if axis.tag == tag), None)
 | 
						|
 | 
						|
    def getLocationLabel(self, name: str) -> Optional[LocationLabelDescriptor]:
 | 
						|
        """Return the top-level location label with the given ``name``, or
 | 
						|
        ``None`` if no such label exists.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        for label in self.locationLabels:
 | 
						|
            if label.name == name:
 | 
						|
                return label
 | 
						|
        return None
 | 
						|
 | 
						|
    def map_forward(self, userLocation: SimpleLocationDict) -> SimpleLocationDict:
 | 
						|
        """Map a user location to a design location.
 | 
						|
 | 
						|
        Assume that missing coordinates are at the default location for that axis.
 | 
						|
 | 
						|
        Note: the output won't be anisotropic, only the xvalue is set.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        return {
 | 
						|
            axis.name: axis.map_forward(userLocation.get(axis.name, axis.default))
 | 
						|
            for axis in self.axes
 | 
						|
        }
 | 
						|
 | 
						|
    def map_backward(
 | 
						|
        self, designLocation: AnisotropicLocationDict
 | 
						|
    ) -> SimpleLocationDict:
 | 
						|
        """Map a design location to a user location.
 | 
						|
 | 
						|
        Assume that missing coordinates are at the default location for that axis.
 | 
						|
 | 
						|
        When the input has anisotropic locations, only the xvalue is used.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        return {
 | 
						|
            axis.name: (
 | 
						|
                axis.map_backward(designLocation[axis.name])
 | 
						|
                if axis.name in designLocation
 | 
						|
                else axis.default
 | 
						|
            )
 | 
						|
            for axis in self.axes
 | 
						|
        }
 | 
						|
 | 
						|
    def findDefault(self):
 | 
						|
        """Set and return SourceDescriptor at the default location or None.
 | 
						|
 | 
						|
        The default location is the set of all `default` values in user space
 | 
						|
        of all axes.
 | 
						|
 | 
						|
        This function updates the document's :attr:`default` value.
 | 
						|
 | 
						|
        .. versionchanged:: 5.0
 | 
						|
           Allow the default source to not specify some of the axis values, and
 | 
						|
           they are assumed to be the default.
 | 
						|
           See :meth:`SourceDescriptor.getFullDesignLocation()`
 | 
						|
        """
 | 
						|
        self.default = None
 | 
						|
 | 
						|
        # Convert the default location from user space to design space before comparing
 | 
						|
        # it against the SourceDescriptor locations (always in design space).
 | 
						|
        defaultDesignLocation = self.newDefaultLocation()
 | 
						|
 | 
						|
        for sourceDescriptor in self.sources:
 | 
						|
            if sourceDescriptor.getFullDesignLocation(self) == defaultDesignLocation:
 | 
						|
                self.default = sourceDescriptor
 | 
						|
                return sourceDescriptor
 | 
						|
 | 
						|
        return None
 | 
						|
 | 
						|
    def normalizeLocation(self, location):
 | 
						|
        """Return a dict with normalized axis values."""
 | 
						|
        from fontTools.varLib.models import normalizeValue
 | 
						|
 | 
						|
        new = {}
 | 
						|
        for axis in self.axes:
 | 
						|
            if axis.name not in location:
 | 
						|
                # skipping this dimension it seems
 | 
						|
                continue
 | 
						|
            value = location[axis.name]
 | 
						|
            # 'anisotropic' location, take first coord only
 | 
						|
            if isinstance(value, tuple):
 | 
						|
                value = value[0]
 | 
						|
            triple = [
 | 
						|
                axis.map_forward(v) for v in (axis.minimum, axis.default, axis.maximum)
 | 
						|
            ]
 | 
						|
            new[axis.name] = normalizeValue(value, triple)
 | 
						|
        return new
 | 
						|
 | 
						|
    def normalize(self):
 | 
						|
        """
 | 
						|
        Normalise the geometry of this designspace:
 | 
						|
 | 
						|
        - scale all the locations of all masters and instances to the -1 - 0 - 1 value.
 | 
						|
        - we need the axis data to do the scaling, so we do those last.
 | 
						|
        """
 | 
						|
        # masters
 | 
						|
        for item in self.sources:
 | 
						|
            item.location = self.normalizeLocation(item.location)
 | 
						|
        # instances
 | 
						|
        for item in self.instances:
 | 
						|
            # glyph masters for this instance
 | 
						|
            for _, glyphData in item.glyphs.items():
 | 
						|
                glyphData["instanceLocation"] = self.normalizeLocation(
 | 
						|
                    glyphData["instanceLocation"]
 | 
						|
                )
 | 
						|
                for glyphMaster in glyphData["masters"]:
 | 
						|
                    glyphMaster["location"] = self.normalizeLocation(
 | 
						|
                        glyphMaster["location"]
 | 
						|
                    )
 | 
						|
            item.location = self.normalizeLocation(item.location)
 | 
						|
        # the axes
 | 
						|
        for axis in self.axes:
 | 
						|
            # scale the map first
 | 
						|
            newMap = []
 | 
						|
            for inputValue, outputValue in axis.map:
 | 
						|
                newOutputValue = self.normalizeLocation({axis.name: outputValue}).get(
 | 
						|
                    axis.name
 | 
						|
                )
 | 
						|
                newMap.append((inputValue, newOutputValue))
 | 
						|
            if newMap:
 | 
						|
                axis.map = newMap
 | 
						|
            # finally the axis values
 | 
						|
            minimum = self.normalizeLocation({axis.name: axis.minimum}).get(axis.name)
 | 
						|
            maximum = self.normalizeLocation({axis.name: axis.maximum}).get(axis.name)
 | 
						|
            default = self.normalizeLocation({axis.name: axis.default}).get(axis.name)
 | 
						|
            # and set them in the axis.minimum
 | 
						|
            axis.minimum = minimum
 | 
						|
            axis.maximum = maximum
 | 
						|
            axis.default = default
 | 
						|
        # now the rules
 | 
						|
        for rule in self.rules:
 | 
						|
            newConditionSets = []
 | 
						|
            for conditions in rule.conditionSets:
 | 
						|
                newConditions = []
 | 
						|
                for cond in conditions:
 | 
						|
                    if cond.get("minimum") is not None:
 | 
						|
                        minimum = self.normalizeLocation(
 | 
						|
                            {cond["name"]: cond["minimum"]}
 | 
						|
                        ).get(cond["name"])
 | 
						|
                    else:
 | 
						|
                        minimum = None
 | 
						|
                    if cond.get("maximum") is not None:
 | 
						|
                        maximum = self.normalizeLocation(
 | 
						|
                            {cond["name"]: cond["maximum"]}
 | 
						|
                        ).get(cond["name"])
 | 
						|
                    else:
 | 
						|
                        maximum = None
 | 
						|
                    newConditions.append(
 | 
						|
                        dict(name=cond["name"], minimum=minimum, maximum=maximum)
 | 
						|
                    )
 | 
						|
                newConditionSets.append(newConditions)
 | 
						|
            rule.conditionSets = newConditionSets
 | 
						|
 | 
						|
    def loadSourceFonts(self, opener, **kwargs):
 | 
						|
        """Ensure SourceDescriptor.font attributes are loaded, and return list of fonts.
 | 
						|
 | 
						|
        Takes a callable which initializes a new font object (e.g. TTFont, or
 | 
						|
        defcon.Font, etc.) from the SourceDescriptor.path, and sets the
 | 
						|
        SourceDescriptor.font attribute.
 | 
						|
        If the font attribute is already not None, it is not loaded again.
 | 
						|
        Fonts with the same path are only loaded once and shared among SourceDescriptors.
 | 
						|
 | 
						|
        For example, to load UFO sources using defcon:
 | 
						|
 | 
						|
            designspace = DesignSpaceDocument.fromfile("path/to/my.designspace")
 | 
						|
            designspace.loadSourceFonts(defcon.Font)
 | 
						|
 | 
						|
        Or to load masters as FontTools binary fonts, including extra options:
 | 
						|
 | 
						|
            designspace.loadSourceFonts(ttLib.TTFont, recalcBBoxes=False)
 | 
						|
 | 
						|
        Args:
 | 
						|
            opener (Callable): takes one required positional argument, the source.path,
 | 
						|
                and an optional list of keyword arguments, and returns a new font object
 | 
						|
                loaded from the path.
 | 
						|
            **kwargs: extra options passed on to the opener function.
 | 
						|
 | 
						|
        Returns:
 | 
						|
            List of font objects in the order they appear in the sources list.
 | 
						|
        """
 | 
						|
        # we load fonts with the same source.path only once
 | 
						|
        loaded = {}
 | 
						|
        fonts = []
 | 
						|
        for source in self.sources:
 | 
						|
            if source.font is not None:  # font already loaded
 | 
						|
                fonts.append(source.font)
 | 
						|
                continue
 | 
						|
            if source.path in loaded:
 | 
						|
                source.font = loaded[source.path]
 | 
						|
            else:
 | 
						|
                if source.path is None:
 | 
						|
                    raise DesignSpaceDocumentError(
 | 
						|
                        "Designspace source '%s' has no 'path' attribute"
 | 
						|
                        % (source.name or "<Unknown>")
 | 
						|
                    )
 | 
						|
                source.font = opener(source.path, **kwargs)
 | 
						|
                loaded[source.path] = source.font
 | 
						|
            fonts.append(source.font)
 | 
						|
        return fonts
 | 
						|
 | 
						|
    @property
 | 
						|
    def formatTuple(self):
 | 
						|
        """Return the formatVersion as a tuple of (major, minor).
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        if self.formatVersion is None:
 | 
						|
            return (5, 0)
 | 
						|
        numbers = (int(i) for i in self.formatVersion.split("."))
 | 
						|
        major = next(numbers)
 | 
						|
        minor = next(numbers, 0)
 | 
						|
        return (major, minor)
 | 
						|
 | 
						|
    def getVariableFonts(self) -> List[VariableFontDescriptor]:
 | 
						|
        """Return all variable fonts defined in this document, or implicit
 | 
						|
        variable fonts that can be built from the document's continuous axes.
 | 
						|
 | 
						|
        In the case of Designspace documents before version 5, the whole
 | 
						|
        document was implicitly describing a variable font that covers the
 | 
						|
        whole space.
 | 
						|
 | 
						|
        In version 5 and above documents, there can be as many variable fonts
 | 
						|
        as there are locations on discrete axes.
 | 
						|
 | 
						|
        .. seealso:: :func:`splitInterpolable`
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        if self.variableFonts:
 | 
						|
            return self.variableFonts
 | 
						|
 | 
						|
        variableFonts = []
 | 
						|
        discreteAxes = []
 | 
						|
        rangeAxisSubsets: List[
 | 
						|
            Union[RangeAxisSubsetDescriptor, ValueAxisSubsetDescriptor]
 | 
						|
        ] = []
 | 
						|
        for axis in self.axes:
 | 
						|
            if hasattr(axis, "values"):
 | 
						|
                # Mypy doesn't support narrowing union types via hasattr()
 | 
						|
                # TODO(Python 3.10): use TypeGuard
 | 
						|
                # https://mypy.readthedocs.io/en/stable/type_narrowing.html
 | 
						|
                axis = cast(DiscreteAxisDescriptor, axis)
 | 
						|
                discreteAxes.append(axis)  # type: ignore
 | 
						|
            else:
 | 
						|
                rangeAxisSubsets.append(RangeAxisSubsetDescriptor(name=axis.name))
 | 
						|
        valueCombinations = itertools.product(*[axis.values for axis in discreteAxes])
 | 
						|
        for values in valueCombinations:
 | 
						|
            basename = None
 | 
						|
            if self.filename is not None:
 | 
						|
                basename = os.path.splitext(self.filename)[0] + "-VF"
 | 
						|
            if self.path is not None:
 | 
						|
                basename = os.path.splitext(os.path.basename(self.path))[0] + "-VF"
 | 
						|
            if basename is None:
 | 
						|
                basename = "VF"
 | 
						|
            axisNames = "".join(
 | 
						|
                [f"-{axis.tag}{value}" for axis, value in zip(discreteAxes, values)]
 | 
						|
            )
 | 
						|
            variableFonts.append(
 | 
						|
                VariableFontDescriptor(
 | 
						|
                    name=f"{basename}{axisNames}",
 | 
						|
                    axisSubsets=rangeAxisSubsets
 | 
						|
                    + [
 | 
						|
                        ValueAxisSubsetDescriptor(name=axis.name, userValue=value)
 | 
						|
                        for axis, value in zip(discreteAxes, values)
 | 
						|
                    ],
 | 
						|
                )
 | 
						|
            )
 | 
						|
        return variableFonts
 | 
						|
 | 
						|
    def deepcopyExceptFonts(self):
 | 
						|
        """Allow deep-copying a DesignSpace document without deep-copying
 | 
						|
        attached UFO fonts or TTFont objects. The :attr:`font` attribute
 | 
						|
        is shared by reference between the original and the copy.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
        """
 | 
						|
        fonts = [source.font for source in self.sources]
 | 
						|
        try:
 | 
						|
            for source in self.sources:
 | 
						|
                source.font = None
 | 
						|
            res = copy.deepcopy(self)
 | 
						|
            for source, font in zip(res.sources, fonts):
 | 
						|
                source.font = font
 | 
						|
            return res
 | 
						|
        finally:
 | 
						|
            for source, font in zip(self.sources, fonts):
 | 
						|
                source.font = font
 | 
						|
 | 
						|
 | 
						|
def main(args=None):
 | 
						|
    """Roundtrip .designspace file through the DesignSpaceDocument class"""
 | 
						|
 | 
						|
    if args is None:
 | 
						|
        import sys
 | 
						|
 | 
						|
        args = sys.argv[1:]
 | 
						|
 | 
						|
    from argparse import ArgumentParser
 | 
						|
 | 
						|
    parser = ArgumentParser(prog="designspaceLib", description=main.__doc__)
 | 
						|
    parser.add_argument("input")
 | 
						|
    parser.add_argument("output")
 | 
						|
 | 
						|
    options = parser.parse_args(args)
 | 
						|
 | 
						|
    ds = DesignSpaceDocument.fromfile(options.input)
 | 
						|
    ds.write(options.output)
 |