2016-11-15 13:27:39 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
2018-06-07 11:43:14 +01:00
|
|
|
from fontTools.misc.loggingTools import LogMixin
|
2021-08-20 00:45:43 +02:00
|
|
|
from fontTools.misc.textTools import tobytes, tostr
|
2017-04-11 18:16:28 +02:00
|
|
|
import collections
|
2021-03-29 11:45:58 +02:00
|
|
|
from io import BytesIO, StringIO
|
2016-11-15 13:27:39 +01:00
|
|
|
import os
|
2017-10-03 17:31:14 +01:00
|
|
|
import posixpath
|
2018-10-18 20:03:54 +01:00
|
|
|
from fontTools.misc import etree as ET
|
|
|
|
from fontTools.misc import plistlib
|
2016-11-15 13:27:39 +01:00
|
|
|
|
|
|
|
"""
|
|
|
|
designSpaceDocument
|
|
|
|
|
|
|
|
- read and write designspace files
|
|
|
|
"""
|
|
|
|
|
2018-02-12 12:25:12 +00:00
|
|
|
__all__ = [
|
|
|
|
'DesignSpaceDocumentError', 'DesignSpaceDocument', 'SourceDescriptor',
|
|
|
|
'InstanceDescriptor', 'AxisDescriptor', 'RuleDescriptor', 'BaseDocReader',
|
|
|
|
'BaseDocWriter'
|
|
|
|
]
|
|
|
|
|
2018-06-07 12:44:49 +01:00
|
|
|
# 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"
|
|
|
|
|
2018-02-12 12:25:12 +00:00
|
|
|
|
2017-11-29 17:08:23 +00:00
|
|
|
def posix(path):
|
|
|
|
"""Normalize paths using forward slash to work also on Windows."""
|
2017-11-30 12:06:35 +00:00
|
|
|
new_path = posixpath.join(*path.split(os.path.sep))
|
|
|
|
if path.startswith('/'):
|
|
|
|
# The above transformation loses absolute paths
|
|
|
|
new_path = '/' + new_path
|
2021-05-09 18:59:34 +02:00
|
|
|
elif path.startswith(r'\\'):
|
|
|
|
# The above transformation loses leading slashes of UNC path mounts
|
|
|
|
new_path = '//' + new_path
|
2017-11-30 12:06:35 +00:00
|
|
|
return new_path
|
2017-11-29 17:08:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
def posixpath_property(private_name):
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
class DesignSpaceDocumentError(Exception):
|
|
|
|
def __init__(self, msg, obj=None):
|
|
|
|
self.msg = msg
|
|
|
|
self.obj = obj
|
2016-11-15 20:15:04 +00:00
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
def __str__(self):
|
2018-06-11 16:35:02 +01:00
|
|
|
return str(self.msg) + (
|
|
|
|
": %r" % self.obj if self.obj is not None else "")
|
2016-11-15 13:27:39 +01:00
|
|
|
|
2016-11-15 20:15:04 +00:00
|
|
|
|
2018-09-11 18:09:22 +02:00
|
|
|
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):
|
2016-11-15 13:27:39 +01:00
|
|
|
""" Containers for a bunch of attributes"""
|
2018-06-07 12:44:49 +01:00
|
|
|
|
|
|
|
# XXX this is ugly. The 'print' is inappropriate here, and instead of
|
|
|
|
# assert, it should simply return True/False
|
2016-11-15 13:27:39 +01:00
|
|
|
def compare(self, other):
|
|
|
|
# test if this object contains the same data as the other
|
|
|
|
for attr in self._attrs:
|
|
|
|
try:
|
2016-11-15 20:15:04 +00:00
|
|
|
assert(getattr(self, attr) == getattr(other, attr))
|
2016-11-15 13:27:39 +01:00
|
|
|
except AssertionError:
|
2016-11-15 20:16:48 +00:00
|
|
|
print("failed attribute", attr, getattr(self, attr), "!=", getattr(other, attr))
|
2016-11-15 13:27:39 +01:00
|
|
|
|
|
|
|
|
|
|
|
class SourceDescriptor(SimpleDescriptor):
|
|
|
|
"""Simple container for data related to the source"""
|
2016-11-15 20:15:04 +00:00
|
|
|
flavor = "source"
|
2018-04-27 13:24:11 +02:00
|
|
|
_attrs = ['filename', 'path', 'name', 'layerName',
|
2016-11-15 20:15:04 +00:00
|
|
|
'location', 'copyLib',
|
|
|
|
'copyGroups', 'copyFeatures',
|
|
|
|
'muteKerning', 'muteInfo',
|
|
|
|
'mutedGlyphNames',
|
|
|
|
'familyName', 'styleName']
|
|
|
|
|
2020-03-20 15:18:46 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
*,
|
|
|
|
filename=None,
|
|
|
|
path=None,
|
|
|
|
font=None,
|
|
|
|
name=None,
|
|
|
|
location=None,
|
|
|
|
layerName=None,
|
|
|
|
familyName=None,
|
|
|
|
styleName=None,
|
|
|
|
copyLib=False,
|
|
|
|
copyInfo=False,
|
|
|
|
copyGroups=False,
|
|
|
|
copyFeatures=False,
|
|
|
|
muteKerning=False,
|
|
|
|
muteInfo=False,
|
|
|
|
mutedGlyphNames=None,
|
|
|
|
):
|
|
|
|
self.filename = filename
|
2018-02-14 11:15:54 +00:00
|
|
|
"""The original path as found in the document."""
|
|
|
|
|
2020-03-20 15:18:46 +00:00
|
|
|
self.path = path
|
2018-02-14 11:15:54 +00:00
|
|
|
"""The absolute path, calculated from filename."""
|
|
|
|
|
2020-03-20 15:18:46 +00:00
|
|
|
self.font = font
|
2018-02-14 11:15:54 +00:00
|
|
|
"""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.
|
|
|
|
"""
|
|
|
|
|
2020-03-20 15:18:46 +00:00
|
|
|
self.name = name
|
|
|
|
self.location = location
|
|
|
|
self.layerName = layerName
|
|
|
|
self.familyName = familyName
|
|
|
|
self.styleName = styleName
|
|
|
|
|
|
|
|
self.copyLib = copyLib
|
|
|
|
self.copyInfo = copyInfo
|
|
|
|
self.copyGroups = copyGroups
|
|
|
|
self.copyFeatures = copyFeatures
|
|
|
|
self.muteKerning = muteKerning
|
|
|
|
self.muteInfo = muteInfo
|
|
|
|
self.mutedGlyphNames = mutedGlyphNames or []
|
2016-11-15 13:27:39 +01:00
|
|
|
|
2017-11-29 17:08:23 +00:00
|
|
|
path = posixpath_property("_path")
|
|
|
|
filename = posixpath_property("_filename")
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
|
2016-12-11 08:18:49 -05:00
|
|
|
class RuleDescriptor(SimpleDescriptor):
|
|
|
|
"""<!-- optional: list of substitution rules -->
|
|
|
|
<rules>
|
2018-05-14 10:29:48 +01:00
|
|
|
<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"/>
|
2016-12-11 08:18:49 -05:00
|
|
|
</rule>
|
|
|
|
</rules>
|
|
|
|
"""
|
2018-05-14 10:29:48 +01:00
|
|
|
_attrs = ['name', 'conditionSets', 'subs'] # what do we need here
|
2018-06-07 12:44:49 +01:00
|
|
|
|
2020-03-20 15:18:46 +00:00
|
|
|
def __init__(self, *, name=None, conditionSets=None, subs=None):
|
|
|
|
self.name = name
|
|
|
|
# list of lists of dict(name='aaaa', minimum=0, maximum=1000)
|
|
|
|
self.conditionSets = conditionSets or []
|
|
|
|
# list of substitutions stored as tuples of glyphnames ("a", "a.alt")
|
|
|
|
self.subs = subs or []
|
2018-05-14 10:29:48 +01:00
|
|
|
|
2016-12-11 08:18:49 -05:00
|
|
|
|
2016-12-13 08:53:49 +01:00
|
|
|
def evaluateRule(rule, location):
|
2018-05-25 15:22:00 +02:00
|
|
|
""" Return True if any of the rule's conditionsets matches the given location."""
|
2018-05-14 10:29:48 +01:00
|
|
|
return any(evaluateConditions(c, location) for c in rule.conditionSets)
|
|
|
|
|
|
|
|
|
|
|
|
def evaluateConditions(conditions, location):
|
|
|
|
""" Return True if all the conditions matches the given location.
|
2018-05-25 15:22:00 +02:00
|
|
|
If a condition has no minimum, check for < maximum.
|
|
|
|
If a condition has no maximum, check for > minimum.
|
2018-05-14 10:29:48 +01:00
|
|
|
"""
|
|
|
|
for cd in conditions:
|
|
|
|
value = location[cd['name']]
|
2016-12-18 22:15:54 +01:00
|
|
|
if cd.get('minimum') is None:
|
2018-05-14 10:29:48 +01:00
|
|
|
if value > cd['maximum']:
|
2016-12-18 22:15:54 +01:00
|
|
|
return False
|
|
|
|
elif cd.get('maximum') is None:
|
2018-05-14 10:29:48 +01:00
|
|
|
if cd['minimum'] > value:
|
2016-12-18 22:15:54 +01:00
|
|
|
return False
|
2018-05-14 10:29:48 +01:00
|
|
|
elif not cd['minimum'] <= value <= cd['maximum']:
|
|
|
|
return False
|
2016-12-13 08:53:49 +01:00
|
|
|
return True
|
|
|
|
|
2018-05-14 10:29:48 +01:00
|
|
|
|
2016-12-13 17:56:21 +01:00
|
|
|
def processRules(rules, location, glyphNames):
|
2019-11-07 19:30:51 +01:00
|
|
|
""" Apply these rules at this location to these glyphnames
|
2016-12-13 08:53:49 +01:00
|
|
|
- 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
|
|
|
|
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
class InstanceDescriptor(SimpleDescriptor):
|
|
|
|
"""Simple container for data related to the instance"""
|
2016-11-15 20:15:04 +00:00
|
|
|
flavor = "instance"
|
2017-04-23 15:39:17 +02:00
|
|
|
_defaultLanguageCode = "en"
|
2018-06-07 12:44:49 +01:00
|
|
|
_attrs = ['path',
|
|
|
|
'name',
|
|
|
|
'location',
|
|
|
|
'familyName',
|
|
|
|
'styleName',
|
|
|
|
'postScriptFontName',
|
|
|
|
'styleMapFamilyName',
|
|
|
|
'styleMapStyleName',
|
|
|
|
'kerning',
|
|
|
|
'info',
|
|
|
|
'lib']
|
2016-11-15 20:15:04 +00:00
|
|
|
|
2020-03-20 15:18:46 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
*,
|
|
|
|
filename=None,
|
|
|
|
path=None,
|
|
|
|
font=None,
|
|
|
|
name=None,
|
|
|
|
location=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,
|
|
|
|
):
|
|
|
|
# the original path as found in the document
|
|
|
|
self.filename = filename
|
|
|
|
# the absolute path, calculated from filename
|
|
|
|
self.path = path
|
|
|
|
# Same as in SourceDescriptor.
|
|
|
|
self.font = font
|
|
|
|
self.name = name
|
|
|
|
self.location = location
|
|
|
|
self.familyName = familyName
|
|
|
|
self.styleName = styleName
|
|
|
|
self.postScriptFontName = postScriptFontName
|
|
|
|
self.styleMapFamilyName = styleMapFamilyName
|
|
|
|
self.styleMapStyleName = styleMapStyleName
|
|
|
|
self.localisedFamilyName = localisedFamilyName or {}
|
|
|
|
self.localisedStyleName = localisedStyleName or {}
|
|
|
|
self.localisedStyleMapFamilyName = localisedStyleMapFamilyName or {}
|
|
|
|
self.localisedStyleMapStyleName = localisedStyleMapStyleName or {}
|
|
|
|
self.glyphs = glyphs or {}
|
|
|
|
self.kerning = kerning
|
|
|
|
self.info = info
|
|
|
|
|
|
|
|
self.lib = lib or {}
|
2018-02-14 11:15:54 +00:00
|
|
|
"""Custom data associated with this instance."""
|
2016-11-15 13:27:39 +01:00
|
|
|
|
2017-11-29 17:08:23 +00:00
|
|
|
path = posixpath_property("_path")
|
|
|
|
filename = posixpath_property("_filename")
|
|
|
|
|
2017-04-23 16:16:22 +02:00
|
|
|
def setStyleName(self, styleName, languageCode="en"):
|
2021-03-29 11:45:58 +02:00
|
|
|
self.localisedStyleName[languageCode] = tostr(styleName)
|
2018-06-07 12:44:49 +01:00
|
|
|
|
2017-04-23 16:16:22 +02:00
|
|
|
def getStyleName(self, languageCode="en"):
|
|
|
|
return self.localisedStyleName.get(languageCode)
|
2017-04-23 15:39:17 +02:00
|
|
|
|
2017-04-23 16:16:22 +02:00
|
|
|
def setFamilyName(self, familyName, languageCode="en"):
|
2021-03-29 11:45:58 +02:00
|
|
|
self.localisedFamilyName[languageCode] = tostr(familyName)
|
2018-06-07 12:44:49 +01:00
|
|
|
|
2017-04-23 16:16:22 +02:00
|
|
|
def getFamilyName(self, languageCode="en"):
|
|
|
|
return self.localisedFamilyName.get(languageCode)
|
2017-04-23 15:39:17 +02:00
|
|
|
|
2017-04-23 16:16:22 +02:00
|
|
|
def setStyleMapStyleName(self, styleMapStyleName, languageCode="en"):
|
2021-03-29 11:45:58 +02:00
|
|
|
self.localisedStyleMapStyleName[languageCode] = tostr(styleMapStyleName)
|
2018-06-07 12:44:49 +01:00
|
|
|
|
2017-04-23 16:16:22 +02:00
|
|
|
def getStyleMapStyleName(self, languageCode="en"):
|
|
|
|
return self.localisedStyleMapStyleName.get(languageCode)
|
2017-04-23 15:39:17 +02:00
|
|
|
|
2017-04-23 16:16:22 +02:00
|
|
|
def setStyleMapFamilyName(self, styleMapFamilyName, languageCode="en"):
|
2021-03-29 11:45:58 +02:00
|
|
|
self.localisedStyleMapFamilyName[languageCode] = tostr(styleMapFamilyName)
|
2018-06-07 12:44:49 +01:00
|
|
|
|
2017-04-23 16:16:22 +02:00
|
|
|
def getStyleMapFamilyName(self, languageCode="en"):
|
|
|
|
return self.localisedStyleMapFamilyName.get(languageCode)
|
2016-11-15 13:27:39 +01:00
|
|
|
|
2018-06-07 12:44:49 +01:00
|
|
|
|
2016-12-02 12:22:07 +01:00
|
|
|
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:
|
2018-06-07 12:44:49 +01:00
|
|
|
tag = name + "*" * (4 - len(name))
|
2016-12-02 12:22:07 +01:00
|
|
|
else:
|
|
|
|
tag = name[:4]
|
2018-06-07 12:44:49 +01:00
|
|
|
return tag, dict(en=name)
|
2016-12-02 12:22:07 +01:00
|
|
|
|
2016-12-13 08:53:49 +01:00
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
class AxisDescriptor(SimpleDescriptor):
|
2017-04-23 15:39:17 +02:00
|
|
|
""" Simple container for the axis data
|
|
|
|
Add more localisations?
|
|
|
|
"""
|
2016-11-15 20:15:04 +00:00
|
|
|
flavor = "axis"
|
2016-11-15 13:27:39 +01:00
|
|
|
_attrs = ['tag', 'name', 'maximum', 'minimum', 'default', 'map']
|
2016-11-15 20:15:04 +00:00
|
|
|
|
2020-03-20 15:18:46 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
*,
|
|
|
|
tag=None,
|
|
|
|
name=None,
|
|
|
|
labelNames=None,
|
|
|
|
minimum=None,
|
|
|
|
default=None,
|
|
|
|
maximum=None,
|
|
|
|
hidden=False,
|
|
|
|
map=None,
|
|
|
|
):
|
|
|
|
# opentype tag for this axis
|
|
|
|
self.tag = tag
|
|
|
|
# name of the axis used in locations
|
|
|
|
self.name = name
|
|
|
|
# names for UI purposes, if this is not a standard axis,
|
|
|
|
self.labelNames = labelNames or {}
|
|
|
|
self.minimum = minimum
|
|
|
|
self.maximum = maximum
|
|
|
|
self.default = default
|
|
|
|
self.hidden = hidden
|
|
|
|
self.map = map or []
|
2016-11-15 13:27:39 +01:00
|
|
|
|
2016-11-20 10:05:55 +01:00
|
|
|
def serialize(self):
|
2016-11-20 10:14:25 +01:00
|
|
|
# output to a dict, used in testing
|
2018-06-07 12:44:49 +01:00
|
|
|
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,
|
|
|
|
)
|
2016-11-20 10:05:55 +01:00
|
|
|
|
2018-09-11 18:10:51 +02:00
|
|
|
def map_forward(self, v):
|
|
|
|
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):
|
|
|
|
from fontTools.varLib.models import piecewiseLinearMap
|
|
|
|
|
|
|
|
if not self.map:
|
|
|
|
return v
|
|
|
|
return piecewiseLinearMap(v, {v: k for k, v in self.map})
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
|
|
|
|
class BaseDocWriter(object):
|
|
|
|
_whiteSpace = " "
|
2016-12-11 08:18:49 -05:00
|
|
|
ruleDescriptorClass = RuleDescriptor
|
2016-11-15 13:27:39 +01:00
|
|
|
axisDescriptorClass = AxisDescriptor
|
|
|
|
sourceDescriptorClass = SourceDescriptor
|
|
|
|
instanceDescriptorClass = InstanceDescriptor
|
2016-11-15 20:15:04 +00:00
|
|
|
|
2016-12-02 12:22:07 +01:00
|
|
|
@classmethod
|
|
|
|
def getAxisDecriptor(cls):
|
|
|
|
return cls.axisDescriptorClass()
|
|
|
|
|
2016-12-09 08:29:39 -08:00
|
|
|
@classmethod
|
|
|
|
def getSourceDescriptor(cls):
|
|
|
|
return cls.sourceDescriptorClass()
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def getInstanceDescriptor(cls):
|
|
|
|
return cls.instanceDescriptorClass()
|
|
|
|
|
2016-12-11 08:18:49 -05:00
|
|
|
@classmethod
|
|
|
|
def getRuleDescriptor(cls):
|
|
|
|
return cls.ruleDescriptorClass()
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
def __init__(self, documentPath, documentObject):
|
|
|
|
self.path = documentPath
|
|
|
|
self.documentObject = documentObject
|
2019-10-18 13:20:28 +02:00
|
|
|
self.documentVersion = "4.1"
|
2016-11-15 13:27:39 +01:00
|
|
|
self.root = ET.Element("designspace")
|
2018-05-14 10:29:48 +01:00
|
|
|
self.root.attrib['format'] = self.documentVersion
|
2018-05-24 22:47:31 +02:00
|
|
|
self._axes = [] # for use by the writer only
|
|
|
|
self._rules = [] # for use by the writer only
|
2016-11-15 13:27:39 +01:00
|
|
|
|
2018-10-18 20:03:54 +01:00
|
|
|
def write(self, pretty=True, encoding="UTF-8", xml_declaration=True):
|
2016-12-11 08:18:49 -05:00
|
|
|
if self.documentObject.axes:
|
|
|
|
self.root.append(ET.Element("axes"))
|
2016-11-15 13:27:39 +01:00
|
|
|
for axisObject in self.documentObject.axes:
|
|
|
|
self._addAxis(axisObject)
|
2016-12-11 08:18:49 -05:00
|
|
|
|
|
|
|
if self.documentObject.rules:
|
2019-10-18 13:20:28 +02:00
|
|
|
if getattr(self.documentObject, "rulesProcessingLast", False):
|
|
|
|
attributes = {"processing": "last"}
|
|
|
|
else:
|
|
|
|
attributes = {}
|
|
|
|
self.root.append(ET.Element("rules", attributes))
|
2016-12-11 08:18:49 -05:00
|
|
|
for ruleObject in self.documentObject.rules:
|
|
|
|
self._addRule(ruleObject)
|
|
|
|
|
|
|
|
if self.documentObject.sources:
|
|
|
|
self.root.append(ET.Element("sources"))
|
2016-11-15 13:27:39 +01:00
|
|
|
for sourceObject in self.documentObject.sources:
|
|
|
|
self._addSource(sourceObject)
|
2016-12-11 08:18:49 -05:00
|
|
|
|
|
|
|
if self.documentObject.instances:
|
|
|
|
self.root.append(ET.Element("instances"))
|
2016-11-15 13:27:39 +01:00
|
|
|
for instanceObject in self.documentObject.instances:
|
|
|
|
self._addInstance(instanceObject)
|
2018-02-12 12:25:12 +00:00
|
|
|
|
|
|
|
if self.documentObject.lib:
|
|
|
|
self._addLib(self.documentObject.lib)
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
tree = ET.ElementTree(self.root)
|
2018-09-11 15:12:43 +02:00
|
|
|
tree.write(
|
|
|
|
self.path,
|
|
|
|
encoding=encoding,
|
|
|
|
method='xml',
|
|
|
|
xml_declaration=xml_declaration,
|
2018-10-18 20:03:54 +01:00
|
|
|
pretty_print=pretty,
|
2018-09-11 15:12:43 +02:00
|
|
|
)
|
2016-11-15 13:27:39 +01:00
|
|
|
|
|
|
|
def _makeLocationElement(self, locationObject, name=None):
|
|
|
|
""" Convert Location dict to a locationElement."""
|
|
|
|
locElement = ET.Element("location")
|
|
|
|
if name is not None:
|
2016-11-15 20:15:04 +00:00
|
|
|
locElement.attrib['name'] = name
|
2018-05-25 11:43:58 +02:00
|
|
|
validatedLocation = self.documentObject.newDefaultLocation()
|
|
|
|
for axisName, axisValue in locationObject.items():
|
|
|
|
if axisName in validatedLocation:
|
|
|
|
# only accept values we know
|
|
|
|
validatedLocation[axisName] = axisValue
|
2016-11-15 13:27:39 +01:00
|
|
|
for dimensionName, dimensionValue in validatedLocation.items():
|
2016-11-15 20:15:04 +00:00
|
|
|
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)
|
2016-11-15 13:27:39 +01:00
|
|
|
return locElement, validatedLocation
|
2016-11-15 20:15:04 +00:00
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
def intOrFloat(self, num):
|
|
|
|
if int(num) == num:
|
2016-11-15 20:15:04 +00:00
|
|
|
return "%d" % num
|
|
|
|
return "%f" % num
|
2016-11-15 13:27:39 +01:00
|
|
|
|
2016-12-11 08:18:49 -05:00
|
|
|
def _addRule(self, ruleObject):
|
2016-12-18 22:15:54 +01:00
|
|
|
# if none of the conditions have minimum or maximum values, do not add the rule.
|
2018-05-24 22:47:31 +02:00
|
|
|
self._rules.append(ruleObject)
|
2018-06-07 12:44:49 +01:00
|
|
|
ruleElement = ET.Element('rule')
|
2018-05-14 10:29:48 +01:00
|
|
|
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)
|
2016-12-11 08:18:49 -05:00
|
|
|
for sub in ruleObject.subs:
|
|
|
|
subElement = ET.Element('sub')
|
|
|
|
subElement.attrib['name'] = sub[0]
|
|
|
|
subElement.attrib['with'] = sub[1]
|
|
|
|
ruleElement.append(subElement)
|
2018-05-14 10:29:48 +01:00
|
|
|
if len(ruleElement):
|
|
|
|
self.root.findall('.rules')[0].append(ruleElement)
|
2016-12-11 08:18:49 -05:00
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
def _addAxis(self, axisObject):
|
2018-05-24 22:47:31 +02:00
|
|
|
self._axes.append(axisObject)
|
2016-11-15 13:27:39 +01:00
|
|
|
axisElement = ET.Element('axis')
|
|
|
|
axisElement.attrib['tag'] = axisObject.tag
|
|
|
|
axisElement.attrib['name'] = axisObject.name
|
2017-05-05 16:10:26 +01:00
|
|
|
axisElement.attrib['minimum'] = self.intOrFloat(axisObject.minimum)
|
|
|
|
axisElement.attrib['maximum'] = self.intOrFloat(axisObject.maximum)
|
|
|
|
axisElement.attrib['default'] = self.intOrFloat(axisObject.default)
|
2017-10-27 10:23:04 +02:00
|
|
|
if axisObject.hidden:
|
|
|
|
axisElement.attrib['hidden'] = "1"
|
2018-01-23 17:03:10 +00:00
|
|
|
for languageCode, labelName in sorted(axisObject.labelNames.items()):
|
2016-11-20 10:05:55 +01:00
|
|
|
languageElement = ET.Element('labelname')
|
2018-10-18 20:03:54 +01:00
|
|
|
languageElement.attrib[XML_LANG] = languageCode
|
2016-11-15 13:27:39 +01:00
|
|
|
languageElement.text = labelName
|
|
|
|
axisElement.append(languageElement)
|
|
|
|
if axisObject.map:
|
|
|
|
for inputValue, outputValue in axisObject.map:
|
|
|
|
mapElement = ET.Element('map')
|
2017-05-05 16:10:26 +01:00
|
|
|
mapElement.attrib['input'] = self.intOrFloat(inputValue)
|
|
|
|
mapElement.attrib['output'] = self.intOrFloat(outputValue)
|
2016-11-15 13:27:39 +01:00
|
|
|
axisElement.append(mapElement)
|
|
|
|
self.root.findall('.axes')[0].append(axisElement)
|
|
|
|
|
|
|
|
def _addInstance(self, instanceObject):
|
|
|
|
instanceElement = ET.Element('instance')
|
|
|
|
if instanceObject.name is not None:
|
|
|
|
instanceElement.attrib['name'] = instanceObject.name
|
|
|
|
if instanceObject.familyName is not None:
|
|
|
|
instanceElement.attrib['familyname'] = instanceObject.familyName
|
|
|
|
if instanceObject.styleName is not None:
|
|
|
|
instanceElement.attrib['stylename'] = instanceObject.styleName
|
2017-04-23 15:39:17 +02:00
|
|
|
# add localisations
|
|
|
|
if instanceObject.localisedStyleName:
|
2017-11-29 17:08:23 +00:00
|
|
|
languageCodes = list(instanceObject.localisedStyleName.keys())
|
2017-04-23 15:39:17 +02:00
|
|
|
languageCodes.sort()
|
|
|
|
for code in languageCodes:
|
2018-06-07 12:44:49 +01:00
|
|
|
if code == "en":
|
|
|
|
continue # already stored in the element attribute
|
2017-04-23 15:39:17 +02:00
|
|
|
localisedStyleNameElement = ET.Element('stylename')
|
2018-10-18 20:03:54 +01:00
|
|
|
localisedStyleNameElement.attrib[XML_LANG] = code
|
2017-04-23 15:39:17 +02:00
|
|
|
localisedStyleNameElement.text = instanceObject.getStyleName(code)
|
2017-04-23 16:16:22 +02:00
|
|
|
instanceElement.append(localisedStyleNameElement)
|
|
|
|
if instanceObject.localisedFamilyName:
|
2017-11-29 17:08:23 +00:00
|
|
|
languageCodes = list(instanceObject.localisedFamilyName.keys())
|
2017-04-23 16:16:22 +02:00
|
|
|
languageCodes.sort()
|
|
|
|
for code in languageCodes:
|
2018-06-07 12:44:49 +01:00
|
|
|
if code == "en":
|
|
|
|
continue # already stored in the element attribute
|
2017-04-23 16:16:22 +02:00
|
|
|
localisedFamilyNameElement = ET.Element('familyname')
|
2018-10-18 20:03:54 +01:00
|
|
|
localisedFamilyNameElement.attrib[XML_LANG] = code
|
2017-04-23 16:16:22 +02:00
|
|
|
localisedFamilyNameElement.text = instanceObject.getFamilyName(code)
|
|
|
|
instanceElement.append(localisedFamilyNameElement)
|
|
|
|
if instanceObject.localisedStyleMapStyleName:
|
2017-11-29 17:08:23 +00:00
|
|
|
languageCodes = list(instanceObject.localisedStyleMapStyleName.keys())
|
2017-04-23 16:16:22 +02:00
|
|
|
languageCodes.sort()
|
|
|
|
for code in languageCodes:
|
2018-06-07 12:44:49 +01:00
|
|
|
if code == "en":
|
|
|
|
continue
|
2017-04-23 16:16:22 +02:00
|
|
|
localisedStyleMapStyleNameElement = ET.Element('stylemapstylename')
|
2018-10-18 20:03:54 +01:00
|
|
|
localisedStyleMapStyleNameElement.attrib[XML_LANG] = code
|
2017-04-23 16:16:22 +02:00
|
|
|
localisedStyleMapStyleNameElement.text = instanceObject.getStyleMapStyleName(code)
|
|
|
|
instanceElement.append(localisedStyleMapStyleNameElement)
|
|
|
|
if instanceObject.localisedStyleMapFamilyName:
|
2017-11-29 17:08:23 +00:00
|
|
|
languageCodes = list(instanceObject.localisedStyleMapFamilyName.keys())
|
2017-04-23 16:16:22 +02:00
|
|
|
languageCodes.sort()
|
|
|
|
for code in languageCodes:
|
2018-06-07 12:44:49 +01:00
|
|
|
if code == "en":
|
|
|
|
continue
|
2017-04-23 16:16:22 +02:00
|
|
|
localisedStyleMapFamilyNameElement = ET.Element('stylemapfamilyname')
|
2018-10-18 20:03:54 +01:00
|
|
|
localisedStyleMapFamilyNameElement.attrib[XML_LANG] = code
|
2017-04-23 16:16:22 +02:00
|
|
|
localisedStyleMapFamilyNameElement.text = instanceObject.getStyleMapFamilyName(code)
|
|
|
|
instanceElement.append(localisedStyleMapFamilyNameElement)
|
2017-04-23 15:39:17 +02:00
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
if instanceObject.location is not None:
|
|
|
|
locationElement, instanceObject.location = self._makeLocationElement(instanceObject.location)
|
|
|
|
instanceElement.append(locationElement)
|
2017-02-04 10:30:03 +01:00
|
|
|
if instanceObject.filename is not None:
|
|
|
|
instanceElement.attrib['filename'] = instanceObject.filename
|
2016-11-15 13:27:39 +01:00
|
|
|
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 instanceObject.glyphs:
|
|
|
|
if instanceElement.findall('.glyphs') == []:
|
|
|
|
glyphsElement = ET.Element('glyphs')
|
|
|
|
instanceElement.append(glyphsElement)
|
|
|
|
glyphsElement = instanceElement.findall('.glyphs')[0]
|
2018-01-23 17:03:10 +00:00
|
|
|
for glyphName, data in sorted(instanceObject.glyphs.items()):
|
2016-11-15 13:27:39 +01:00
|
|
|
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)
|
2018-02-12 12:25:12 +00:00
|
|
|
if instanceObject.lib:
|
|
|
|
libElement = ET.Element('lib')
|
2018-10-18 20:03:54 +01:00
|
|
|
libElement.append(plistlib.totree(instanceObject.lib, indent_level=4))
|
2018-02-12 12:25:12 +00:00
|
|
|
instanceElement.append(libElement)
|
2016-11-15 13:27:39 +01:00
|
|
|
self.root.findall('.instances')[0].append(instanceElement)
|
|
|
|
|
|
|
|
def _addSource(self, sourceObject):
|
|
|
|
sourceElement = ET.Element("source")
|
2017-02-04 10:30:03 +01:00
|
|
|
if sourceObject.filename is not None:
|
|
|
|
sourceElement.attrib['filename'] = sourceObject.filename
|
2016-11-15 13:27:39 +01:00
|
|
|
if sourceObject.name is not None:
|
2018-06-07 12:44:49 +01:00
|
|
|
if sourceObject.name.find("temp_master") != 0:
|
2017-03-28 23:00:13 +02:00
|
|
|
# do not save temporary source names
|
|
|
|
sourceElement.attrib['name'] = sourceObject.name
|
2016-11-15 13:27:39 +01:00
|
|
|
if sourceObject.familyName is not None:
|
|
|
|
sourceElement.attrib['familyname'] = sourceObject.familyName
|
|
|
|
if sourceObject.styleName is not None:
|
|
|
|
sourceElement.attrib['stylename'] = sourceObject.styleName
|
2018-04-27 13:24:11 +02:00
|
|
|
if sourceObject.layerName is not None:
|
|
|
|
sourceElement.attrib['layer'] = sourceObject.layerName
|
2016-11-15 13:27:39 +01:00
|
|
|
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)
|
|
|
|
locationElement, sourceObject.location = self._makeLocationElement(sourceObject.location)
|
|
|
|
sourceElement.append(locationElement)
|
|
|
|
self.root.findall('.sources')[0].append(sourceElement)
|
|
|
|
|
2018-02-12 12:25:12 +00:00
|
|
|
def _addLib(self, dict):
|
|
|
|
libElement = ET.Element('lib')
|
2018-10-18 20:03:54 +01:00
|
|
|
libElement.append(plistlib.totree(dict, indent_level=2))
|
2018-02-12 12:25:12 +00:00
|
|
|
self.root.append(libElement)
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
def _writeGlyphElement(self, instanceElement, instanceObject, glyphName, data):
|
|
|
|
glyphElement = ET.Element('glyph')
|
|
|
|
if data.get('mute'):
|
|
|
|
glyphElement.attrib['mute'] = "1"
|
2017-08-25 13:17:25 +02:00
|
|
|
if data.get('unicodes') is not None:
|
|
|
|
glyphElement.attrib['unicode'] = " ".join([hex(u) for u in data.get('unicodes')])
|
2016-11-15 13:27:39 +01:00
|
|
|
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
|
|
|
|
|
2016-11-15 20:15:04 +00:00
|
|
|
|
2018-06-07 11:43:14 +01:00
|
|
|
class BaseDocReader(LogMixin):
|
2016-12-11 08:18:49 -05:00
|
|
|
ruleDescriptorClass = RuleDescriptor
|
2016-11-15 13:27:39 +01:00
|
|
|
axisDescriptorClass = AxisDescriptor
|
|
|
|
sourceDescriptorClass = SourceDescriptor
|
|
|
|
instanceDescriptorClass = InstanceDescriptor
|
2016-11-15 20:15:04 +00:00
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
def __init__(self, documentPath, documentObject):
|
|
|
|
self.path = documentPath
|
|
|
|
self.documentObject = documentObject
|
|
|
|
tree = ET.parse(self.path)
|
|
|
|
self.root = tree.getroot()
|
2018-05-14 10:29:48 +01:00
|
|
|
self.documentObject.formatVersion = self.root.attrib.get("format", "3.0")
|
2018-05-24 22:47:31 +02:00
|
|
|
self._axes = []
|
2016-12-11 08:18:49 -05:00
|
|
|
self.rules = []
|
2016-11-15 13:27:39 +01:00
|
|
|
self.sources = []
|
|
|
|
self.instances = []
|
|
|
|
self.axisDefaults = {}
|
2016-12-02 16:53:39 +01:00
|
|
|
self._strictAxisNames = True
|
2016-11-15 13:27:39 +01:00
|
|
|
|
2018-09-11 15:12:43 +02:00
|
|
|
@classmethod
|
|
|
|
def fromstring(cls, string, documentObject):
|
|
|
|
f = BytesIO(tobytes(string, encoding="utf-8"))
|
|
|
|
self = cls(f, documentObject)
|
|
|
|
self.path = None
|
|
|
|
return self
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
def read(self):
|
|
|
|
self.readAxes()
|
2016-12-11 08:18:49 -05:00
|
|
|
self.readRules()
|
2016-11-15 13:27:39 +01:00
|
|
|
self.readSources()
|
|
|
|
self.readInstances()
|
2018-02-12 12:25:12 +00:00
|
|
|
self.readLib()
|
2016-11-15 13:27:39 +01:00
|
|
|
|
2016-12-11 08:18:49 -05:00
|
|
|
def readRules(self):
|
2018-05-27 19:18:13 +02:00
|
|
|
# we also need to read any conditions that are outside of a condition set.
|
2016-12-11 08:18:49 -05:00
|
|
|
rules = []
|
2019-10-18 13:20:28 +02:00
|
|
|
rulesElement = self.root.find(".rules")
|
|
|
|
if rulesElement is not None:
|
|
|
|
processingValue = rulesElement.attrib.get("processing", "first")
|
|
|
|
if processingValue not in {"first", "last"}:
|
2019-10-18 13:24:32 +02:00
|
|
|
raise DesignSpaceDocumentError(
|
|
|
|
"<rules> processing attribute value is not valid: %r, "
|
|
|
|
"expected 'first' or 'last'" % processingValue)
|
2019-10-18 13:20:28 +02:00
|
|
|
self.documentObject.rulesProcessingLast = processingValue == "last"
|
2016-12-11 08:18:49 -05:00
|
|
|
for ruleElement in self.root.findall(".rules/rule"):
|
|
|
|
ruleObject = self.ruleDescriptorClass()
|
2018-06-07 11:43:14 +01:00
|
|
|
ruleName = ruleObject.name = ruleElement.attrib.get("name")
|
2018-05-27 19:18:13 +02:00
|
|
|
# read any stray conditions outside a condition set
|
2018-06-07 11:43:14 +01:00
|
|
|
externalConditions = self._readConditionElements(
|
|
|
|
ruleElement,
|
|
|
|
ruleName,
|
|
|
|
)
|
2018-05-27 19:18:13 +02:00
|
|
|
if externalConditions:
|
|
|
|
ruleObject.conditionSets.append(externalConditions)
|
2018-06-07 11:43:14 +01:00
|
|
|
self.log.info(
|
|
|
|
"Found stray rule conditions outside a conditionset. "
|
|
|
|
"Wrapped them in a new conditionset."
|
2018-06-07 12:44:49 +01:00
|
|
|
)
|
2018-05-27 19:18:13 +02:00
|
|
|
# read the conditionsets
|
2018-05-14 10:29:48 +01:00
|
|
|
for conditionSetElement in ruleElement.findall('.conditionset'):
|
2018-06-07 11:43:14 +01:00
|
|
|
conditionSet = self._readConditionElements(
|
|
|
|
conditionSetElement,
|
|
|
|
ruleName,
|
|
|
|
)
|
2018-05-27 19:18:13 +02:00
|
|
|
if conditionSet is not None:
|
|
|
|
ruleObject.conditionSets.append(conditionSet)
|
2016-12-11 08:18:49 -05:00
|
|
|
for subElement in ruleElement.findall('.sub'):
|
|
|
|
a = subElement.attrib['name']
|
|
|
|
b = subElement.attrib['with']
|
2018-06-07 12:44:49 +01:00
|
|
|
ruleObject.subs.append((a, b))
|
2016-12-11 08:18:49 -05:00
|
|
|
rules.append(ruleObject)
|
|
|
|
self.documentObject.rules = rules
|
|
|
|
|
2018-06-07 11:43:14 +01:00
|
|
|
def _readConditionElements(self, parentElement, ruleName=None):
|
2018-05-27 19:18:13 +02:00
|
|
|
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
|
2018-05-30 22:26:59 +02:00
|
|
|
if cd.get('minimum') is None and cd.get('maximum') is None:
|
2018-06-07 11:43:14 +01:00
|
|
|
raise DesignSpaceDocumentError(
|
2018-06-11 16:35:02 +01:00
|
|
|
"condition missing required minimum or maximum in rule" +
|
|
|
|
(" '%s'" % ruleName if ruleName is not None else ""))
|
2018-05-27 19:18:13 +02:00
|
|
|
cds.append(cd)
|
|
|
|
return cds
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
def readAxes(self):
|
2016-11-15 20:15:04 +00:00
|
|
|
# read the axes elements, including the warp map.
|
2018-09-11 18:12:25 +02:00
|
|
|
axisElements = self.root.findall(".axes/axis")
|
|
|
|
if not axisElements:
|
2017-02-23 11:06:22 +01:00
|
|
|
return
|
2018-09-11 18:12:25 +02:00
|
|
|
for axisElement in axisElements:
|
2016-11-15 13:27:39 +01:00
|
|
|
axisObject = self.axisDescriptorClass()
|
|
|
|
axisObject.name = axisElement.attrib.get("name")
|
|
|
|
axisObject.minimum = float(axisElement.attrib.get("minimum"))
|
|
|
|
axisObject.maximum = float(axisElement.attrib.get("maximum"))
|
2017-10-27 10:23:04 +02:00
|
|
|
if axisElement.attrib.get('hidden', False):
|
|
|
|
axisObject.hidden = True
|
2018-05-14 10:29:48 +01:00
|
|
|
axisObject.default = float(axisElement.attrib.get("default"))
|
2016-11-15 13:27:39 +01:00
|
|
|
axisObject.tag = axisElement.attrib.get("tag")
|
|
|
|
for mapElement in axisElement.findall('map'):
|
|
|
|
a = float(mapElement.attrib['input'])
|
|
|
|
b = float(mapElement.attrib['output'])
|
2018-06-07 12:44:49 +01:00
|
|
|
axisObject.map.append((a, b))
|
2016-11-20 10:05:55 +01:00
|
|
|
for labelNameElement in axisElement.findall('labelname'):
|
2018-10-18 20:03:54 +01:00
|
|
|
# Note: elementtree reads the "xml:lang" attribute name as
|
2016-11-20 10:05:55 +01:00
|
|
|
# '{http://www.w3.org/XML/1998/namespace}lang'
|
|
|
|
for key, lang in labelNameElement.items():
|
2018-06-07 12:44:49 +01:00
|
|
|
if key == XML_LANG:
|
2021-03-29 11:45:58 +02:00
|
|
|
axisObject.labelNames[lang] = tostr(labelNameElement.text)
|
2016-11-15 13:27:39 +01:00
|
|
|
self.documentObject.axes.append(axisObject)
|
|
|
|
self.axisDefaults[axisObject.name] = axisObject.default
|
|
|
|
|
|
|
|
def readSources(self):
|
2017-03-29 00:08:25 +02:00
|
|
|
for sourceCount, sourceElement in enumerate(self.root.findall(".sources/source")):
|
2016-11-15 13:27:39 +01:00
|
|
|
filename = sourceElement.attrib.get('filename')
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
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
|
2016-11-15 13:27:39 +01:00
|
|
|
sourceName = sourceElement.attrib.get('name')
|
2017-03-28 23:00:13 +02:00
|
|
|
if sourceName is None:
|
|
|
|
# add a temporary source name
|
2018-05-14 10:29:48 +01:00
|
|
|
sourceName = "temp_master.%d" % (sourceCount)
|
2016-11-15 13:27:39 +01:00
|
|
|
sourceObject = self.sourceDescriptorClass()
|
2017-02-04 10:30:03 +01:00
|
|
|
sourceObject.path = sourcePath # absolute path to the ufo source
|
|
|
|
sourceObject.filename = filename # path as it is stored in the document
|
2016-11-15 13:27:39 +01:00
|
|
|
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
|
|
|
|
sourceObject.location = self.locationFromElement(sourceElement)
|
2018-04-27 13:24:11 +02:00
|
|
|
layerName = sourceElement.attrib.get('layer')
|
|
|
|
if layerName is not None:
|
|
|
|
sourceObject.layerName = layerName
|
2016-11-15 13:27:39 +01:00
|
|
|
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):
|
|
|
|
elementLocation = None
|
|
|
|
for locationElement in element.findall('.location'):
|
|
|
|
elementLocation = self.readLocationElement(locationElement)
|
|
|
|
break
|
|
|
|
return elementLocation
|
|
|
|
|
|
|
|
def readLocationElement(self, locationElement):
|
|
|
|
""" Format 0 location reader """
|
2018-09-11 18:12:25 +02:00
|
|
|
if self._strictAxisNames and not self.documentObject.axes:
|
2018-06-11 16:35:02 +01:00
|
|
|
raise DesignSpaceDocumentError("No axes defined")
|
2016-11-15 13:27:39 +01:00
|
|
|
loc = {}
|
|
|
|
for dimensionElement in locationElement.findall(".dimension"):
|
|
|
|
dimName = dimensionElement.attrib.get("name")
|
2016-12-02 16:53:39 +01:00
|
|
|
if self._strictAxisNames and dimName not in self.axisDefaults:
|
2018-05-23 13:19:23 +02:00
|
|
|
# In case the document contains no axis definitions,
|
2018-06-07 11:43:14 +01:00
|
|
|
self.log.warning("Location with undefined axis: \"%s\".", dimName)
|
2016-11-15 13:27:39 +01:00
|
|
|
continue
|
|
|
|
xValue = yValue = None
|
|
|
|
try:
|
|
|
|
xValue = dimensionElement.attrib.get('xvalue')
|
|
|
|
xValue = float(xValue)
|
|
|
|
except ValueError:
|
2018-06-07 11:43:14 +01:00
|
|
|
self.log.warning("KeyError in readLocation xValue %3.3f", xValue)
|
2016-11-15 13:27:39 +01:00
|
|
|
try:
|
|
|
|
yValue = dimensionElement.attrib.get('yvalue')
|
|
|
|
if yValue is not None:
|
|
|
|
yValue = float(yValue)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
if yValue is not None:
|
|
|
|
loc[dimName] = (xValue, yValue)
|
|
|
|
else:
|
|
|
|
loc[dimName] = xValue
|
|
|
|
return loc
|
|
|
|
|
|
|
|
def readInstances(self, makeGlyphs=True, makeKerning=True, makeInfo=True):
|
|
|
|
instanceElements = self.root.findall('.instances/instance')
|
2018-06-07 12:44:49 +01:00
|
|
|
for instanceElement in instanceElements:
|
2016-11-15 13:27:39 +01:00
|
|
|
self._readSingleInstanceElement(instanceElement, makeGlyphs=makeGlyphs, makeKerning=makeKerning, makeInfo=makeInfo)
|
|
|
|
|
|
|
|
def _readSingleInstanceElement(self, instanceElement, makeGlyphs=True, makeKerning=True, makeInfo=True):
|
|
|
|
filename = instanceElement.attrib.get('filename')
|
2018-09-11 15:12:43 +02:00
|
|
|
if filename is not None and self.documentObject.path is not None:
|
2016-11-15 13:27:39 +01:00
|
|
|
instancePath = os.path.join(os.path.dirname(self.documentObject.path), filename)
|
|
|
|
else:
|
|
|
|
instancePath = None
|
|
|
|
instanceObject = self.instanceDescriptorClass()
|
2017-02-04 10:30:03 +01:00
|
|
|
instanceObject.path = instancePath # absolute path to the instance
|
|
|
|
instanceObject.filename = filename # path as it is stored in the document
|
2016-11-15 13:27:39 +01:00
|
|
|
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
|
2017-04-23 23:27:41 +02:00
|
|
|
# read localised names
|
|
|
|
for styleNameElement in instanceElement.findall('stylename'):
|
|
|
|
for key, lang in styleNameElement.items():
|
2018-06-07 12:44:49 +01:00
|
|
|
if key == XML_LANG:
|
|
|
|
styleName = styleNameElement.text
|
|
|
|
instanceObject.setStyleName(styleName, lang)
|
2017-04-23 23:27:41 +02:00
|
|
|
for familyNameElement in instanceElement.findall('familyname'):
|
|
|
|
for key, lang in familyNameElement.items():
|
2018-06-07 12:44:49 +01:00
|
|
|
if key == XML_LANG:
|
|
|
|
familyName = familyNameElement.text
|
|
|
|
instanceObject.setFamilyName(familyName, lang)
|
2017-04-23 23:27:41 +02:00
|
|
|
for styleMapStyleNameElement in instanceElement.findall('stylemapstylename'):
|
|
|
|
for key, lang in styleMapStyleNameElement.items():
|
2018-06-07 12:44:49 +01:00
|
|
|
if key == XML_LANG:
|
|
|
|
styleMapStyleName = styleMapStyleNameElement.text
|
|
|
|
instanceObject.setStyleMapStyleName(styleMapStyleName, lang)
|
2017-04-23 23:27:41 +02:00
|
|
|
for styleMapFamilyNameElement in instanceElement.findall('stylemapfamilyname'):
|
|
|
|
for key, lang in styleMapFamilyNameElement.items():
|
2018-06-07 12:44:49 +01:00
|
|
|
if key == XML_LANG:
|
|
|
|
styleMapFamilyName = styleMapFamilyNameElement.text
|
|
|
|
instanceObject.setStyleMapFamilyName(styleMapFamilyName, lang)
|
2016-11-15 13:27:39 +01:00
|
|
|
instanceLocation = self.locationFromElement(instanceElement)
|
|
|
|
if instanceLocation is not None:
|
|
|
|
instanceObject.location = instanceLocation
|
|
|
|
for glyphElement in instanceElement.findall('.glyphs/glyph'):
|
|
|
|
self.readGlyphElement(glyphElement, instanceObject)
|
|
|
|
for infoElement in instanceElement.findall("info"):
|
|
|
|
self.readInfoElement(infoElement, instanceObject)
|
2018-02-12 12:25:12 +00:00
|
|
|
for libElement in instanceElement.findall('lib'):
|
|
|
|
self.readLibElement(libElement, instanceObject)
|
2016-11-15 13:27:39 +01:00
|
|
|
self.documentObject.instances.append(instanceObject)
|
|
|
|
|
2018-02-12 12:25:12 +00:00
|
|
|
def readLibElement(self, libElement, instanceObject):
|
|
|
|
"""Read the lib element for the given instance."""
|
2018-10-18 20:03:54 +01:00
|
|
|
instanceObject.lib = plistlib.fromtree(libElement[0])
|
2018-02-12 12:25:12 +00:00
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
def readInfoElement(self, infoElement, instanceObject):
|
2018-05-25 15:22:00 +02:00
|
|
|
""" Read the info element."""
|
2016-11-15 13:27:39 +01:00
|
|
|
instanceObject.info = True
|
|
|
|
|
|
|
|
def readKerningElement(self, kerningElement, instanceObject):
|
2018-05-25 15:22:00 +02:00
|
|
|
""" Read the kerning element."""
|
2016-11-15 13:27:39 +01:00
|
|
|
kerningLocation = self.locationFromElement(kerningElement)
|
|
|
|
instanceObject.addKerning(kerningLocation)
|
|
|
|
|
|
|
|
def readGlyphElement(self, glyphElement, instanceObject):
|
|
|
|
"""
|
|
|
|
Read the glyph element.
|
|
|
|
<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:
|
2018-06-11 16:35:02 +01:00
|
|
|
raise DesignSpaceDocumentError("Glyph object without name attribute")
|
2016-11-15 13:27:39 +01:00
|
|
|
mute = glyphElement.attrib.get("mute")
|
|
|
|
if mute == "1":
|
|
|
|
glyphData['mute'] = True
|
2017-08-25 13:17:25 +02:00
|
|
|
# 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)
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
for noteElement in glyphElement.findall('.note'):
|
|
|
|
glyphData['note'] = noteElement.text
|
|
|
|
break
|
|
|
|
instanceLocation = self.locationFromElement(glyphElement)
|
|
|
|
if instanceLocation is not None:
|
|
|
|
glyphData['instanceLocation'] = instanceLocation
|
|
|
|
glyphSources = None
|
|
|
|
for masterElement in glyphElement.findall('.masters/master'):
|
|
|
|
fontSourceName = masterElement.attrib.get('source')
|
|
|
|
sourceLocation = self.locationFromElement(masterElement)
|
|
|
|
masterGlyphName = masterElement.attrib.get('glyphname')
|
|
|
|
if masterGlyphName is None:
|
|
|
|
# if we don't read a glyphname, use the one we have
|
|
|
|
masterGlyphName = glyphName
|
2016-11-15 20:15:04 +00:00
|
|
|
d = dict(font=fontSourceName,
|
|
|
|
location=sourceLocation,
|
|
|
|
glyphName=masterGlyphName)
|
2016-11-15 13:27:39 +01:00
|
|
|
if glyphSources is None:
|
|
|
|
glyphSources = []
|
|
|
|
glyphSources.append(d)
|
|
|
|
if glyphSources is not None:
|
|
|
|
glyphData['masters'] = glyphSources
|
|
|
|
instanceObject.glyphs[glyphName] = glyphData
|
|
|
|
|
2018-02-12 12:25:12 +00:00
|
|
|
def readLib(self):
|
|
|
|
"""Read the lib element for the whole document."""
|
|
|
|
for libElement in self.root.findall(".lib"):
|
2018-10-18 20:03:54 +01:00
|
|
|
self.documentObject.lib = plistlib.fromtree(libElement[0])
|
2018-02-12 12:25:12 +00:00
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
|
2018-09-11 18:09:22 +02:00
|
|
|
class DesignSpaceDocument(LogMixin, AsDictMixin):
|
2016-11-15 13:27:39 +01:00
|
|
|
""" Read, write data from the designspace file"""
|
2017-11-30 12:21:09 +00:00
|
|
|
def __init__(self, readerClass=None, writerClass=None):
|
2016-11-15 13:27:39 +01:00
|
|
|
self.path = None
|
2018-02-12 12:25:12 +00:00
|
|
|
self.filename = None
|
2018-02-14 11:15:54 +00:00
|
|
|
"""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.
|
|
|
|
"""
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
self.formatVersion = None
|
|
|
|
self.sources = []
|
|
|
|
self.instances = []
|
|
|
|
self.axes = []
|
2016-12-11 08:18:49 -05:00
|
|
|
self.rules = []
|
2019-10-18 13:20:28 +02:00
|
|
|
self.rulesProcessingLast = False
|
2016-12-02 12:22:07 +01:00
|
|
|
self.default = None # name of the default master
|
2018-02-14 11:15:54 +00:00
|
|
|
|
2018-02-12 12:25:12 +00:00
|
|
|
self.lib = {}
|
2018-02-14 11:15:54 +00:00
|
|
|
"""Custom data associated with the whole document."""
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
#
|
|
|
|
if readerClass is not None:
|
|
|
|
self.readerClass = readerClass
|
|
|
|
else:
|
|
|
|
self.readerClass = BaseDocReader
|
|
|
|
if writerClass is not None:
|
|
|
|
self.writerClass = writerClass
|
|
|
|
else:
|
|
|
|
self.writerClass = BaseDocWriter
|
|
|
|
|
2018-09-11 15:12:43 +02:00
|
|
|
@classmethod
|
|
|
|
def fromfile(cls, path, readerClass=None, writerClass=None):
|
|
|
|
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):
|
2021-03-29 11:45:58 +02:00
|
|
|
if encoding is str or (
|
2018-09-11 15:12:43 +02:00
|
|
|
encoding is not None and encoding.lower() == "unicode"
|
|
|
|
):
|
2021-03-29 11:45:58 +02:00
|
|
|
f = StringIO()
|
2018-09-11 15:12:43 +02:00
|
|
|
xml_declaration = False
|
|
|
|
elif encoding is None or encoding == "utf-8":
|
|
|
|
f = BytesIO()
|
2018-10-18 20:03:54 +01:00
|
|
|
encoding = "UTF-8"
|
2018-09-11 15:12:43 +02:00
|
|
|
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()
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
def read(self, path):
|
2018-12-30 12:07:05 +01:00
|
|
|
if hasattr(path, "__fspath__"): # support os.PathLike objects
|
|
|
|
path = path.__fspath__()
|
2016-11-15 13:27:39 +01:00
|
|
|
self.path = path
|
2018-02-14 12:45:32 +00:00
|
|
|
self.filename = os.path.basename(path)
|
2016-11-15 13:27:39 +01:00
|
|
|
reader = self.readerClass(path, self)
|
|
|
|
reader.read()
|
2018-05-24 22:47:31 +02:00
|
|
|
if self.sources:
|
|
|
|
self.findDefault()
|
2016-11-15 13:27:39 +01:00
|
|
|
|
|
|
|
def write(self, path):
|
2018-12-30 12:07:05 +01:00
|
|
|
if hasattr(path, "__fspath__"): # support os.PathLike objects
|
|
|
|
path = path.__fspath__()
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
self.path = path
|
2018-02-14 12:45:32 +00:00
|
|
|
self.filename = os.path.basename(path)
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
self.updatePaths()
|
2016-11-15 13:27:39 +01:00
|
|
|
writer = self.writerClass(path, self)
|
|
|
|
writer.write()
|
|
|
|
|
2017-10-03 17:31:14 +01:00
|
|
|
def _posixRelativePath(self, otherPath):
|
|
|
|
relative = os.path.relpath(otherPath, os.path.dirname(self.path))
|
2017-11-29 17:08:23 +00:00
|
|
|
return posix(relative)
|
2017-10-03 17:31:14 +01:00
|
|
|
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
def updatePaths(self):
|
2017-11-29 17:08:23 +00:00
|
|
|
"""
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
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.
|
|
|
|
|
2017-11-29 17:08:23 +00:00
|
|
|
case 1.
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
descriptor.filename == None
|
|
|
|
descriptor.path == None
|
|
|
|
|
2017-02-06 14:17:56 +01:00
|
|
|
-- action:
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
write as is, descriptors will not have a filename attr.
|
|
|
|
useless, but no reason to interfere.
|
|
|
|
|
|
|
|
|
2017-11-29 17:08:23 +00:00
|
|
|
case 2.
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
descriptor.filename == "../something"
|
|
|
|
descriptor.path == None
|
|
|
|
|
2017-02-06 14:17:56 +01:00
|
|
|
-- action:
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
write as is. The filename attr should not be touched.
|
|
|
|
|
|
|
|
|
2017-11-29 17:08:23 +00:00
|
|
|
case 3.
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
descriptor.filename == None
|
|
|
|
descriptor.path == "~/absolute/path/there"
|
|
|
|
|
2017-02-06 14:17:56 +01:00
|
|
|
-- action:
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
calculate the relative path for filename.
|
|
|
|
We're not overwriting some other value for filename, it should be fine
|
|
|
|
|
|
|
|
|
2017-11-29 17:08:23 +00:00
|
|
|
case 4.
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
descriptor.filename == '../somewhere'
|
|
|
|
descriptor.path == "~/absolute/path/there"
|
|
|
|
|
2017-02-06 14:17:56 +01:00
|
|
|
-- action:
|
2017-11-29 17:08:23 +00:00
|
|
|
there is a conflict between the given filename, and the path.
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
So we know where the file is relative to the document.
|
2017-02-06 14:40:20 +01:00
|
|
|
Can't guess why they're different, we just choose for path to be correct and update filename.
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
|
|
|
|
|
|
|
|
"""
|
2019-01-03 14:21:09 +00:00
|
|
|
assert self.path is not None
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
for descriptor in self.sources + self.instances:
|
2019-01-03 14:21:09 +00:00
|
|
|
if descriptor.path is not None:
|
|
|
|
# case 3 and 4: filename gets updated and relativized
|
2017-10-03 17:31:14 +01:00
|
|
|
descriptor.filename = self._posixRelativePath(descriptor.path)
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
def addSource(self, sourceDescriptor):
|
|
|
|
self.sources.append(sourceDescriptor)
|
|
|
|
|
2020-03-20 15:58:21 +00:00
|
|
|
def addSourceDescriptor(self, **kwargs):
|
|
|
|
source = self.writerClass.sourceDescriptorClass(**kwargs)
|
|
|
|
self.addSource(source)
|
|
|
|
return source
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
def addInstance(self, instanceDescriptor):
|
|
|
|
self.instances.append(instanceDescriptor)
|
|
|
|
|
2020-03-20 15:58:21 +00:00
|
|
|
def addInstanceDescriptor(self, **kwargs):
|
|
|
|
instance = self.writerClass.instanceDescriptorClass(**kwargs)
|
|
|
|
self.addInstance(instance)
|
|
|
|
return instance
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
def addAxis(self, axisDescriptor):
|
|
|
|
self.axes.append(axisDescriptor)
|
|
|
|
|
2020-03-20 15:58:21 +00:00
|
|
|
def addAxisDescriptor(self, **kwargs):
|
|
|
|
axis = self.writerClass.axisDescriptorClass(**kwargs)
|
|
|
|
self.addAxis(axis)
|
|
|
|
return axis
|
|
|
|
|
2016-12-11 08:18:49 -05:00
|
|
|
def addRule(self, ruleDescriptor):
|
|
|
|
self.rules.append(ruleDescriptor)
|
|
|
|
|
2020-03-20 15:58:21 +00:00
|
|
|
def addRuleDescriptor(self, **kwargs):
|
|
|
|
rule = self.writerClass.ruleDescriptorClass(**kwargs)
|
|
|
|
self.addRule(rule)
|
|
|
|
return rule
|
|
|
|
|
2016-11-15 13:27:39 +01:00
|
|
|
def newDefaultLocation(self):
|
2019-03-10 22:20:00 +00:00
|
|
|
"""Return default location in design space."""
|
2018-05-25 11:43:58 +02:00
|
|
|
# Without OrderedDict, output XML would be non-deterministic.
|
|
|
|
# https://github.com/LettError/designSpaceDocument/issues/10
|
|
|
|
loc = collections.OrderedDict()
|
|
|
|
for axisDescriptor in self.axes:
|
2019-03-10 22:20:00 +00:00
|
|
|
loc[axisDescriptor.name] = axisDescriptor.map_forward(
|
|
|
|
axisDescriptor.default
|
|
|
|
)
|
2016-11-15 13:27:39 +01:00
|
|
|
return loc
|
|
|
|
|
2017-02-06 22:33:03 +01:00
|
|
|
def updateFilenameFromPath(self, masters=True, instances=True, force=False):
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
# set a descriptor filename attr from the path and this document path
|
2017-02-06 22:33:03 +01:00
|
|
|
# if the filename attribute is not None: skip it.
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
if masters:
|
|
|
|
for descriptor in self.sources:
|
2017-02-06 22:33:03 +01:00
|
|
|
if descriptor.filename is not None and not force:
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
continue
|
2017-02-06 22:33:03 +01:00
|
|
|
if self.path is not None:
|
2017-10-03 17:31:14 +01:00
|
|
|
descriptor.filename = self._posixRelativePath(descriptor.path)
|
2017-02-06 14:17:56 +01:00
|
|
|
if instances:
|
2018-02-12 12:25:12 +00:00
|
|
|
for descriptor in self.instances:
|
2017-02-06 22:33:03 +01:00
|
|
|
if descriptor.filename is not None and not force:
|
2017-02-06 14:17:56 +01:00
|
|
|
continue
|
2017-02-06 22:33:03 +01:00
|
|
|
if self.path is not None:
|
2017-10-03 17:31:14 +01:00
|
|
|
descriptor.filename = self._posixRelativePath(descriptor.path)
|
New "filename" attribute for source and instance descriptor objects that contains the relative path to the ufo.
So we have:
descriptor.filename: the path to the UFO, relative to the documentpath
descriptor.path: the resolved, absolute path to the UFO
This means we have to be aware of a couple of situations, described in updatePaths() and testPatgNameResolve().
Case 1: both filename and path attributes are None. Action: write the descriptor as is, without filename attr.
Case 2: filename attribute points somewhere, but the path attribute is None. So we can't actually verify if the UFO really exists, but we don't have to. Action: write the filename attribute as is. We could calculate a new path though.
Case 3: filename attribute is None, path attribute has a path. So there is no legacy value for filename that we need to look out for. Action: we can calculate a new relative path and store that in filename.
Case 4: filename and path attributes are not None, but they're in conflict, pointing to different places/ So the absolute path of the UFO and the absolute path of the document produce a different relative path than is stored in filename. One of them must be wrong.
When a new filename is set, make sure to set the path attribute to None and vice versa.
2017-02-04 18:17:20 +01:00
|
|
|
|
2016-12-02 12:22:07 +01:00
|
|
|
def newAxisDescriptor(self):
|
|
|
|
# Ask the writer class to make us a new axisDescriptor
|
|
|
|
return self.writerClass.getAxisDecriptor()
|
|
|
|
|
2016-12-09 08:29:39 -08:00
|
|
|
def newSourceDescriptor(self):
|
|
|
|
# Ask the writer class to make us a new sourceDescriptor
|
2016-12-17 11:46:30 +01:00
|
|
|
return self.writerClass.getSourceDescriptor()
|
|
|
|
|
|
|
|
def newInstanceDescriptor(self):
|
|
|
|
# Ask the writer class to make us a new instanceDescriptor
|
|
|
|
return self.writerClass.getInstanceDescriptor()
|
2016-12-09 08:29:39 -08:00
|
|
|
|
2016-11-26 14:45:56 +01:00
|
|
|
def getAxisOrder(self):
|
|
|
|
names = []
|
|
|
|
for axisDescriptor in self.axes:
|
|
|
|
names.append(axisDescriptor.name)
|
|
|
|
return names
|
|
|
|
|
2016-12-02 20:39:31 +01:00
|
|
|
def getAxis(self, name):
|
|
|
|
for axisDescriptor in self.axes:
|
|
|
|
if axisDescriptor.name == name:
|
|
|
|
return axisDescriptor
|
|
|
|
return None
|
|
|
|
|
2018-05-23 13:19:23 +02:00
|
|
|
def findDefault(self):
|
2019-03-10 22:12:06 +00:00
|
|
|
"""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.
|
|
|
|
"""
|
2018-05-23 13:19:23 +02:00
|
|
|
self.default = None
|
2019-03-10 22:12:06 +00:00
|
|
|
|
|
|
|
# Convert the default location from user space to design space before comparing
|
|
|
|
# it against the SourceDescriptor locations (always in design space).
|
2019-04-08 16:58:55 +01:00
|
|
|
default_location_design = self.newDefaultLocation()
|
2019-03-10 22:12:06 +00:00
|
|
|
|
2018-05-23 13:19:23 +02:00
|
|
|
for sourceDescriptor in self.sources:
|
2019-03-10 22:12:06 +00:00
|
|
|
if sourceDescriptor.location == default_location_design:
|
2018-05-23 13:19:23 +02:00
|
|
|
self.default = sourceDescriptor
|
|
|
|
return sourceDescriptor
|
2019-03-10 22:12:06 +00:00
|
|
|
|
2017-11-29 17:08:23 +00:00
|
|
|
return None
|
2016-12-02 12:22:07 +01:00
|
|
|
|
2016-11-28 17:12:46 +01:00
|
|
|
def normalizeLocation(self, location):
|
2019-02-27 14:09:29 -08:00
|
|
|
from fontTools.varLib.models import normalizeValue
|
|
|
|
|
2016-11-28 17:12:46 +01:00
|
|
|
new = {}
|
|
|
|
for axis in self.axes:
|
2018-06-07 12:44:49 +01:00
|
|
|
if axis.name not in location:
|
2016-11-28 17:12:46 +01:00
|
|
|
# skipping this dimension it seems
|
|
|
|
continue
|
2019-02-27 14:09:29 -08:00
|
|
|
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)
|
2016-11-28 17:12:46 +01:00
|
|
|
return new
|
|
|
|
|
|
|
|
def normalize(self):
|
2018-05-30 22:21:05 +02:00
|
|
|
# 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.
|
2016-11-28 22:29:14 +01:00
|
|
|
# masters
|
2016-11-28 17:12:46 +01:00
|
|
|
for item in self.sources:
|
|
|
|
item.location = self.normalizeLocation(item.location)
|
2016-11-28 22:29:14 +01:00
|
|
|
# instances
|
2016-11-28 17:12:46 +01:00
|
|
|
for item in self.instances:
|
2016-11-28 22:29:14 +01:00
|
|
|
# glyph masters for this instance
|
2018-06-07 12:44:49 +01:00
|
|
|
for _, glyphData in item.glyphs.items():
|
2016-11-28 22:29:14 +01:00
|
|
|
glyphData['instanceLocation'] = self.normalizeLocation(glyphData['instanceLocation'])
|
|
|
|
for glyphMaster in glyphData['masters']:
|
|
|
|
glyphMaster['location'] = self.normalizeLocation(glyphMaster['location'])
|
2016-11-28 17:12:46 +01:00
|
|
|
item.location = self.normalizeLocation(item.location)
|
2018-05-30 22:21:05 +02:00
|
|
|
# the axes
|
2016-11-28 17:12:46 +01:00
|
|
|
for axis in self.axes:
|
2016-11-28 17:18:36 +01:00
|
|
|
# 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
|
2016-11-28 22:29:14 +01:00
|
|
|
# finally the axis values
|
2018-06-07 12:44:49 +01:00
|
|
|
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)
|
2016-12-02 12:22:07 +01:00
|
|
|
# and set them in the axis.minimum
|
2016-11-28 17:12:46 +01:00
|
|
|
axis.minimum = minimum
|
|
|
|
axis.maximum = maximum
|
|
|
|
axis.default = default
|
2016-12-11 08:18:49 -05:00
|
|
|
# now the rules
|
|
|
|
for rule in self.rules:
|
2018-05-14 10:29:48 +01:00
|
|
|
newConditionSets = []
|
|
|
|
for conditions in rule.conditionSets:
|
|
|
|
newConditions = []
|
|
|
|
for cond in conditions:
|
|
|
|
if cond.get('minimum') is not None:
|
2018-06-07 12:44:49 +01:00
|
|
|
minimum = self.normalizeLocation({cond['name']: cond['minimum']}).get(cond['name'])
|
2018-05-14 10:29:48 +01:00
|
|
|
else:
|
|
|
|
minimum = None
|
|
|
|
if cond.get('maximum') is not None:
|
2018-06-07 12:44:49 +01:00
|
|
|
maximum = self.normalizeLocation({cond['name']: cond['maximum']}).get(cond['name'])
|
2018-05-14 10:29:48 +01:00
|
|
|
else:
|
|
|
|
maximum = None
|
|
|
|
newConditions.append(dict(name=cond['name'], minimum=minimum, maximum=maximum))
|
|
|
|
newConditionSets.append(newConditions)
|
|
|
|
rule.conditionSets = newConditionSets
|
2019-05-10 16:32:11 +01:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
2019-05-10 18:43:42 +01:00
|
|
|
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)
|
|
|
|
|
2019-05-10 16:32:11 +01:00
|
|
|
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
|