Merge pull request #1267 from fonttools/designspaceLib-checkDefault
designspaceLib remove checkDefault, checkAxes
This commit is contained in:
commit
65eac8ef92
@ -573,6 +573,7 @@ There are two meanings for the ``lib`` element:
|
|||||||
- Child element of ``designspace`` and ``instance``
|
- Child element of ``designspace`` and ``instance``
|
||||||
- Contains arbitrary data about the whole document or about a specific
|
- Contains arbitrary data about the whole document or about a specific
|
||||||
instance.
|
instance.
|
||||||
|
- Items in the dict need to use **reverse domain name notation** <https://en.wikipedia.org/wiki/Reverse_domain_name_notation>__
|
||||||
|
|
||||||
.. 32-info-element:
|
.. 32-info-element:
|
||||||
|
|
||||||
|
@ -59,6 +59,8 @@ Make a descriptor object and add it to the document.
|
|||||||
- The ``tag`` attribute is the one of the registered `OpenType
|
- The ``tag`` attribute is the one of the registered `OpenType
|
||||||
Variation Axis
|
Variation Axis
|
||||||
Tags <https://www.microsoft.com/typography/otspec/fvar.htm#VAT>`__
|
Tags <https://www.microsoft.com/typography/otspec/fvar.htm#VAT>`__
|
||||||
|
- The default master is expected at the intersection of all
|
||||||
|
default values of all axes.
|
||||||
|
|
||||||
Option: add label names
|
Option: add label names
|
||||||
-----------------------
|
-----------------------
|
||||||
@ -123,6 +125,7 @@ So go ahead and add another master:
|
|||||||
s1.location = dict(weight=1000)
|
s1.location = dict(weight=1000)
|
||||||
doc.addSource(s1)
|
doc.addSource(s1)
|
||||||
|
|
||||||
|
|
||||||
Option: exclude glyphs
|
Option: exclude glyphs
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
@ -6,18 +6,16 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import posixpath
|
import posixpath
|
||||||
import plistlib
|
import plistlib
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import xml.etree.cElementTree as ET
|
import xml.etree.cElementTree as ET
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
# from mutatorMath.objects.location import biasFromLocations, Location
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
designSpaceDocument
|
designSpaceDocument
|
||||||
|
|
||||||
- read and write designspace files
|
- read and write designspace files
|
||||||
- axes must be defined.
|
|
||||||
- warpmap is stored in its axis element
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -179,9 +177,7 @@ class RuleDescriptor(SimpleDescriptor):
|
|||||||
|
|
||||||
|
|
||||||
def evaluateRule(rule, location):
|
def evaluateRule(rule, location):
|
||||||
""" Return True if any of the rule's conditionsets matches the
|
""" Return True if any of the rule's conditionsets matches the given location."""
|
||||||
given location.
|
|
||||||
"""
|
|
||||||
return any(evaluateConditions(c, location) for c in rule.conditionSets)
|
return any(evaluateConditions(c, location) for c in rule.conditionSets)
|
||||||
|
|
||||||
|
|
||||||
@ -364,14 +360,8 @@ class BaseDocWriter(object):
|
|||||||
self.documentVersion = "4.0"
|
self.documentVersion = "4.0"
|
||||||
self.root = ET.Element("designspace")
|
self.root = ET.Element("designspace")
|
||||||
self.root.attrib['format'] = self.documentVersion
|
self.root.attrib['format'] = self.documentVersion
|
||||||
self.axes = []
|
self._axes = [] # for use by the writer only
|
||||||
self.rules = []
|
self._rules = [] # for use by the writer only
|
||||||
|
|
||||||
def newDefaultLocation(self):
|
|
||||||
loc = collections.OrderedDict()
|
|
||||||
for axisDescriptor in self.axes:
|
|
||||||
loc[axisDescriptor.name] = axisDescriptor.default
|
|
||||||
return loc
|
|
||||||
|
|
||||||
def write(self, pretty=True):
|
def write(self, pretty=True):
|
||||||
if self.documentObject.axes:
|
if self.documentObject.axes:
|
||||||
@ -407,13 +397,11 @@ class BaseDocWriter(object):
|
|||||||
locElement = ET.Element("location")
|
locElement = ET.Element("location")
|
||||||
if name is not None:
|
if name is not None:
|
||||||
locElement.attrib['name'] = name
|
locElement.attrib['name'] = name
|
||||||
defaultLoc = self.newDefaultLocation()
|
validatedLocation = self.documentObject.newDefaultLocation()
|
||||||
# Without OrderedDict, output XML would be non-deterministic.
|
for axisName, axisValue in locationObject.items():
|
||||||
# https://github.com/LettError/designSpaceDocument/issues/10
|
if axisName in validatedLocation:
|
||||||
validatedLocation = collections.OrderedDict()
|
# only accept values we know
|
||||||
for axisName, axisValue in defaultLoc.items():
|
validatedLocation[axisName] = axisValue
|
||||||
# update the location dict with missing default axis values
|
|
||||||
validatedLocation[axisName] = locationObject.get(axisName, axisValue)
|
|
||||||
for dimensionName, dimensionValue in validatedLocation.items():
|
for dimensionName, dimensionValue in validatedLocation.items():
|
||||||
dimElement = ET.Element('dimension')
|
dimElement = ET.Element('dimension')
|
||||||
dimElement.attrib['name'] = dimensionName
|
dimElement.attrib['name'] = dimensionName
|
||||||
@ -432,7 +420,7 @@ class BaseDocWriter(object):
|
|||||||
|
|
||||||
def _addRule(self, ruleObject):
|
def _addRule(self, ruleObject):
|
||||||
# if none of the conditions have minimum or maximum values, do not add the rule.
|
# if none of the conditions have minimum or maximum values, do not add the rule.
|
||||||
self.rules.append(ruleObject)
|
self._rules.append(ruleObject)
|
||||||
ruleElement = ET.Element('rule')
|
ruleElement = ET.Element('rule')
|
||||||
if ruleObject.name is not None:
|
if ruleObject.name is not None:
|
||||||
ruleElement.attrib['name'] = ruleObject.name
|
ruleElement.attrib['name'] = ruleObject.name
|
||||||
@ -451,9 +439,6 @@ class BaseDocWriter(object):
|
|||||||
conditionsetElement.append(conditionElement)
|
conditionsetElement.append(conditionElement)
|
||||||
if len(conditionsetElement):
|
if len(conditionsetElement):
|
||||||
ruleElement.append(conditionsetElement)
|
ruleElement.append(conditionsetElement)
|
||||||
# XXX shouldn't we require at least one sub element?
|
|
||||||
# if not ruleObject.subs:
|
|
||||||
# raise DesignSpaceDocument('Invalid empty rule with no "sub" elements')
|
|
||||||
for sub in ruleObject.subs:
|
for sub in ruleObject.subs:
|
||||||
subElement = ET.Element('sub')
|
subElement = ET.Element('sub')
|
||||||
subElement.attrib['name'] = sub[0]
|
subElement.attrib['name'] = sub[0]
|
||||||
@ -463,7 +448,7 @@ class BaseDocWriter(object):
|
|||||||
self.root.findall('.rules')[0].append(ruleElement)
|
self.root.findall('.rules')[0].append(ruleElement)
|
||||||
|
|
||||||
def _addAxis(self, axisObject):
|
def _addAxis(self, axisObject):
|
||||||
self.axes.append(axisObject)
|
self._axes.append(axisObject)
|
||||||
axisElement = ET.Element('axis')
|
axisElement = ET.Element('axis')
|
||||||
axisElement.attrib['tag'] = axisObject.tag
|
axisElement.attrib['tag'] = axisObject.tag
|
||||||
axisElement.attrib['name'] = axisObject.name
|
axisElement.attrib['name'] = axisObject.name
|
||||||
@ -657,12 +642,13 @@ class BaseDocReader(object):
|
|||||||
tree = ET.parse(self.path)
|
tree = ET.parse(self.path)
|
||||||
self.root = tree.getroot()
|
self.root = tree.getroot()
|
||||||
self.documentObject.formatVersion = self.root.attrib.get("format", "3.0")
|
self.documentObject.formatVersion = self.root.attrib.get("format", "3.0")
|
||||||
self.axes = []
|
self._axes = []
|
||||||
self.rules = []
|
self.rules = []
|
||||||
self.sources = []
|
self.sources = []
|
||||||
self.instances = []
|
self.instances = []
|
||||||
self.axisDefaults = {}
|
self.axisDefaults = {}
|
||||||
self._strictAxisNames = True
|
self._strictAxisNames = True
|
||||||
|
self.logger = logging.getLogger("DesignSpaceLog")
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
self.readAxes()
|
self.readAxes()
|
||||||
@ -677,21 +663,32 @@ class BaseDocReader(object):
|
|||||||
paths.append(self.documentObject.sources[name][0].path)
|
paths.append(self.documentObject.sources[name][0].path)
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
def newDefaultLocation(self):
|
|
||||||
loc = {}
|
|
||||||
for axisDescriptor in self.axes:
|
|
||||||
loc[axisDescriptor.name] = axisDescriptor.default
|
|
||||||
return loc
|
|
||||||
|
|
||||||
def readRules(self):
|
def readRules(self):
|
||||||
# read the rules
|
# we also need to read any conditions that are outside of a condition set.
|
||||||
rules = []
|
rules = []
|
||||||
for ruleElement in self.root.findall(".rules/rule"):
|
for ruleElement in self.root.findall(".rules/rule"):
|
||||||
ruleObject = self.ruleDescriptorClass()
|
ruleObject = self.ruleDescriptorClass()
|
||||||
ruleObject.name = ruleElement.attrib.get("name")
|
ruleObject.name = ruleElement.attrib.get("name")
|
||||||
|
# read any stray conditions outside a condition set
|
||||||
|
externalConditions = self._readConditionElements(ruleElement)
|
||||||
|
if externalConditions:
|
||||||
|
ruleObject.conditionSets.append(externalConditions)
|
||||||
|
self.logger.info('Found stray rule conditions outside a conditionset. Wrapped them in a new conditionset.')
|
||||||
|
# read the conditionsets
|
||||||
for conditionSetElement in ruleElement.findall('.conditionset'):
|
for conditionSetElement in ruleElement.findall('.conditionset'):
|
||||||
|
conditionSet = self._readConditionElements(conditionSetElement)
|
||||||
|
if conditionSet is not None:
|
||||||
|
ruleObject.conditionSets.append(conditionSet)
|
||||||
|
for subElement in ruleElement.findall('.sub'):
|
||||||
|
a = subElement.attrib['name']
|
||||||
|
b = subElement.attrib['with']
|
||||||
|
ruleObject.subs.append((a,b))
|
||||||
|
rules.append(ruleObject)
|
||||||
|
self.documentObject.rules = rules
|
||||||
|
|
||||||
|
def _readConditionElements(self, parentElement):
|
||||||
cds = []
|
cds = []
|
||||||
for conditionElement in conditionSetElement.findall('.condition'):
|
for conditionElement in parentElement.findall('.condition'):
|
||||||
cd = {}
|
cd = {}
|
||||||
cdMin = conditionElement.attrib.get("minimum")
|
cdMin = conditionElement.attrib.get("minimum")
|
||||||
if cdMin is not None:
|
if cdMin is not None:
|
||||||
@ -706,7 +703,7 @@ class BaseDocReader(object):
|
|||||||
# will allow these to be None, assume axis.maximum
|
# will allow these to be None, assume axis.maximum
|
||||||
cd['maximum'] = None
|
cd['maximum'] = None
|
||||||
cd['name'] = conditionElement.attrib.get("name")
|
cd['name'] = conditionElement.attrib.get("name")
|
||||||
# test for things
|
# # test for things
|
||||||
if cd.get('minimum') is None and cd.get('maximum') is None:
|
if cd.get('minimum') is None and cd.get('maximum') is None:
|
||||||
if ruleObject.name is not None:
|
if ruleObject.name is not None:
|
||||||
n = ruleObject.name
|
n = ruleObject.name
|
||||||
@ -714,19 +711,12 @@ class BaseDocReader(object):
|
|||||||
n = "%d" % len(rules)
|
n = "%d" % len(rules)
|
||||||
raise DesignSpaceDocumentError("No minimum or maximum defined in rule \"%s\"." % n)
|
raise DesignSpaceDocumentError("No minimum or maximum defined in rule \"%s\"." % n)
|
||||||
cds.append(cd)
|
cds.append(cd)
|
||||||
ruleObject.conditionSets.append(cds)
|
return cds
|
||||||
for subElement in ruleElement.findall('.sub'):
|
|
||||||
a = subElement.attrib['name']
|
|
||||||
b = subElement.attrib['with']
|
|
||||||
ruleObject.subs.append((a,b))
|
|
||||||
rules.append(ruleObject)
|
|
||||||
self.documentObject.rules = rules
|
|
||||||
|
|
||||||
def readAxes(self):
|
def readAxes(self):
|
||||||
# read the axes elements, including the warp map.
|
# read the axes elements, including the warp map.
|
||||||
axes = []
|
axes = []
|
||||||
if len(self.root.findall(".axes/axis"))==0:
|
if len(self.root.findall(".axes/axis"))==0:
|
||||||
self.guessAxes()
|
|
||||||
self._strictAxisNames = False
|
self._strictAxisNames = False
|
||||||
return
|
return
|
||||||
for axisElement in self.root.findall(".axes/axis"):
|
for axisElement in self.root.findall(".axes/axis"):
|
||||||
@ -750,66 +740,7 @@ class BaseDocReader(object):
|
|||||||
axisObject.labelNames[lang] = labelName
|
axisObject.labelNames[lang] = labelName
|
||||||
self.documentObject.axes.append(axisObject)
|
self.documentObject.axes.append(axisObject)
|
||||||
self.axisDefaults[axisObject.name] = axisObject.default
|
self.axisDefaults[axisObject.name] = axisObject.default
|
||||||
|
self.documentObject.defaultLoc = self.axisDefaults
|
||||||
def _locationFromElement(self, locationElement):
|
|
||||||
# mostly duplicated from readLocationElement, Needs Resolve.
|
|
||||||
loc = {}
|
|
||||||
for dimensionElement in locationElement.findall(".dimension"):
|
|
||||||
dimName = dimensionElement.attrib.get("name")
|
|
||||||
xValue = yValue = None
|
|
||||||
try:
|
|
||||||
xValue = dimensionElement.attrib.get('xvalue')
|
|
||||||
xValue = float(xValue)
|
|
||||||
except ValueError:
|
|
||||||
self.logger.info("KeyError in readLocation xValue %3.3f", xValue)
|
|
||||||
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 guessAxes(self):
|
|
||||||
# Called when we have no axes element in the file.
|
|
||||||
# Look at all locations and collect the axis names and values
|
|
||||||
# assumptions:
|
|
||||||
# look for the default value on an axis from a master location
|
|
||||||
# Needs deprecation warning
|
|
||||||
allLocations = []
|
|
||||||
minima = {}
|
|
||||||
maxima = {}
|
|
||||||
for locationElement in self.root.findall(".sources/source/location"):
|
|
||||||
allLocations.append(self._locationFromElement(locationElement))
|
|
||||||
for locationElement in self.root.findall(".instances/instance/location"):
|
|
||||||
allLocations.append(self._locationFromElement(locationElement))
|
|
||||||
for loc in allLocations:
|
|
||||||
for dimName, value in loc.items():
|
|
||||||
if not isinstance(value, tuple):
|
|
||||||
value = [value]
|
|
||||||
for v in value:
|
|
||||||
if dimName not in minima:
|
|
||||||
minima[dimName] = v
|
|
||||||
continue
|
|
||||||
if minima[dimName] > v:
|
|
||||||
minima[dimName] = v
|
|
||||||
if dimName not in maxima:
|
|
||||||
maxima[dimName] = v
|
|
||||||
continue
|
|
||||||
if maxima[dimName] < v:
|
|
||||||
maxima[dimName] = v
|
|
||||||
newAxes = []
|
|
||||||
for axisName in maxima.keys():
|
|
||||||
a = self.axisDescriptorClass()
|
|
||||||
a.default = a.minimum = minima[axisName]
|
|
||||||
a.maximum = maxima[axisName]
|
|
||||||
a.name = axisName
|
|
||||||
a.tag, a.labelNames = tagForAxisName(axisName)
|
|
||||||
self.documentObject.axes.append(a)
|
|
||||||
|
|
||||||
def readSources(self):
|
def readSources(self):
|
||||||
for sourceCount, sourceElement in enumerate(self.root.findall(".sources/source")):
|
for sourceCount, sourceElement in enumerate(self.root.findall(".sources/source")):
|
||||||
@ -870,21 +801,21 @@ class BaseDocReader(object):
|
|||||||
|
|
||||||
def readLocationElement(self, locationElement):
|
def readLocationElement(self, locationElement):
|
||||||
""" Format 0 location reader """
|
""" Format 0 location reader """
|
||||||
|
if not self.documentObject.axes:
|
||||||
|
raise DesignSpaceDocumentError("No axes defined.")
|
||||||
loc = {}
|
loc = {}
|
||||||
for dimensionElement in locationElement.findall(".dimension"):
|
for dimensionElement in locationElement.findall(".dimension"):
|
||||||
dimName = dimensionElement.attrib.get("name")
|
dimName = dimensionElement.attrib.get("name")
|
||||||
if self._strictAxisNames and dimName not in self.axisDefaults:
|
if self._strictAxisNames and dimName not in self.axisDefaults:
|
||||||
# In case the document contains axis definitions,
|
# In case the document contains no axis definitions,
|
||||||
# then we should only read the axes we know about.
|
self.logger.warning("Location with undefined axis: \"%s\".", dimName)
|
||||||
# However, if the document does not contain axes,
|
|
||||||
# then we need to create them after reading.
|
|
||||||
continue
|
continue
|
||||||
xValue = yValue = None
|
xValue = yValue = None
|
||||||
try:
|
try:
|
||||||
xValue = dimensionElement.attrib.get('xvalue')
|
xValue = dimensionElement.attrib.get('xvalue')
|
||||||
xValue = float(xValue)
|
xValue = float(xValue)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.logger.info("KeyError in readLocation xValue %3.3f", xValue)
|
self.logger.warning("KeyError in readLocation xValue %3.3f", xValue)
|
||||||
try:
|
try:
|
||||||
yValue = dimensionElement.attrib.get('yvalue')
|
yValue = dimensionElement.attrib.get('yvalue')
|
||||||
if yValue is not None:
|
if yValue is not None:
|
||||||
@ -963,49 +894,27 @@ class BaseDocReader(object):
|
|||||||
instanceObject.lib = from_plist(libElement[0])
|
instanceObject.lib = from_plist(libElement[0])
|
||||||
|
|
||||||
def readInfoElement(self, infoElement, instanceObject):
|
def readInfoElement(self, infoElement, instanceObject):
|
||||||
""" Read the info element.
|
""" Read the info element."""
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
<info/>
|
|
||||||
|
|
||||||
Let's drop support for a different location for the info. Never needed it.
|
|
||||||
|
|
||||||
"""
|
|
||||||
infoLocation = self.locationFromElement(infoElement)
|
infoLocation = self.locationFromElement(infoElement)
|
||||||
instanceObject.info = True
|
instanceObject.info = True
|
||||||
|
|
||||||
def readKerningElement(self, kerningElement, instanceObject):
|
def readKerningElement(self, kerningElement, instanceObject):
|
||||||
""" Read the kerning element.
|
""" Read the kerning element."""
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Make kerning at the location and with the masters specified at the instance level.
|
|
||||||
<kerning/>
|
|
||||||
|
|
||||||
"""
|
|
||||||
kerningLocation = self.locationFromElement(kerningElement)
|
kerningLocation = self.locationFromElement(kerningElement)
|
||||||
instanceObject.addKerning(kerningLocation)
|
instanceObject.addKerning(kerningLocation)
|
||||||
|
|
||||||
def readGlyphElement(self, glyphElement, instanceObject):
|
def readGlyphElement(self, glyphElement, instanceObject):
|
||||||
"""
|
"""
|
||||||
Read the glyph element.
|
Read the glyph element.
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
<glyph name="b" unicode="0x62"/>
|
<glyph name="b" unicode="0x62"/>
|
||||||
|
|
||||||
<glyph name="b"/>
|
<glyph name="b"/>
|
||||||
|
|
||||||
<glyph name="b">
|
<glyph name="b">
|
||||||
<master location="location-token-bbb" source="master-token-aaa2"/>
|
<master location="location-token-bbb" source="master-token-aaa2"/>
|
||||||
<master glyphname="b.alt1" location="location-token-ccc" source="master-token-aaa3"/>
|
<master glyphname="b.alt1" location="location-token-ccc" source="master-token-aaa3"/>
|
||||||
|
|
||||||
<note>
|
<note>
|
||||||
This is an instance from an anisotropic interpolation.
|
This is an instance from an anisotropic interpolation.
|
||||||
</note>
|
</note>
|
||||||
</glyph>
|
</glyph>
|
||||||
|
|
||||||
"""
|
"""
|
||||||
glyphData = {}
|
glyphData = {}
|
||||||
glyphName = glyphElement.attrib.get('name')
|
glyphName = glyphElement.attrib.get('name')
|
||||||
@ -1057,7 +966,7 @@ class BaseDocReader(object):
|
|||||||
class DesignSpaceDocument(object):
|
class DesignSpaceDocument(object):
|
||||||
""" Read, write data from the designspace file"""
|
""" Read, write data from the designspace file"""
|
||||||
def __init__(self, readerClass=None, writerClass=None):
|
def __init__(self, readerClass=None, writerClass=None):
|
||||||
self.logger = logging.getLogger("DesignSpaceDocumentLog")
|
self.logger = logging.getLogger("DesignSpaceLog")
|
||||||
self.path = None
|
self.path = None
|
||||||
self.filename = None
|
self.filename = None
|
||||||
"""String, optional. When the document is read from the disk, this is
|
"""String, optional. When the document is read from the disk, this is
|
||||||
@ -1094,6 +1003,8 @@ class DesignSpaceDocument(object):
|
|||||||
self.filename = os.path.basename(path)
|
self.filename = os.path.basename(path)
|
||||||
reader = self.readerClass(path, self)
|
reader = self.readerClass(path, self)
|
||||||
reader.read()
|
reader.read()
|
||||||
|
if self.sources:
|
||||||
|
self.findDefault()
|
||||||
|
|
||||||
def write(self, path):
|
def write(self, path):
|
||||||
self.path = path
|
self.path = path
|
||||||
@ -1177,7 +1088,9 @@ class DesignSpaceDocument(object):
|
|||||||
self.rules.append(ruleDescriptor)
|
self.rules.append(ruleDescriptor)
|
||||||
|
|
||||||
def newDefaultLocation(self):
|
def newDefaultLocation(self):
|
||||||
loc = {}
|
# Without OrderedDict, output XML would be non-deterministic.
|
||||||
|
# https://github.com/LettError/designSpaceDocument/issues/10
|
||||||
|
loc = collections.OrderedDict()
|
||||||
for axisDescriptor in self.axes:
|
for axisDescriptor in self.axes:
|
||||||
loc[axisDescriptor.name] = axisDescriptor.default
|
loc[axisDescriptor.name] = axisDescriptor.default
|
||||||
return loc
|
return loc
|
||||||
@ -1222,151 +1135,23 @@ class DesignSpaceDocument(object):
|
|||||||
return axisDescriptor
|
return axisDescriptor
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def check(self):
|
def findDefault(self):
|
||||||
"""
|
# new default finder
|
||||||
After reading we need to make sure we have a valid designspace.
|
# take the sourcedescriptor with the location at all the defaults
|
||||||
This means making repairs if things are missing
|
# if we can't find it, return None, let someone else figure it out
|
||||||
- check if we have axes and deduce them from the masters if they're missing
|
self.default = None
|
||||||
- that can include axes referenced in masters, instances, glyphs.
|
|
||||||
- if no default is assigned, use mutatormath to find out.
|
|
||||||
- record the default in the designspace
|
|
||||||
- report all the changes in a log
|
|
||||||
- save a "repaired" version of the doc
|
|
||||||
"""
|
|
||||||
self.checkAxes()
|
|
||||||
self.checkDefault()
|
|
||||||
|
|
||||||
def checkDefault(self):
|
|
||||||
""" Check the sources for a copyInfo flag."""
|
|
||||||
flaggedDefaultCandidate = None
|
|
||||||
for sourceDescriptor in self.sources:
|
for sourceDescriptor in self.sources:
|
||||||
names = set()
|
if sourceDescriptor.location == self.defaultLoc:
|
||||||
if sourceDescriptor.copyInfo:
|
|
||||||
# we choose you!
|
# we choose you!
|
||||||
flaggedDefaultCandidate = sourceDescriptor
|
self.default = sourceDescriptor
|
||||||
mutatorDefaultCandidate = self.getMutatorDefaultCandidate()
|
return sourceDescriptor
|
||||||
# what are we going to do?
|
|
||||||
if flaggedDefaultCandidate is not None:
|
|
||||||
if mutatorDefaultCandidate is not None:
|
|
||||||
if mutatorDefaultCandidate.name != flaggedDefaultCandidate.name:
|
|
||||||
# warn if we have a conflict
|
|
||||||
self.logger.info("Note: conflicting default masters:\n\tUsing %s as default\n\tMutator found %s" % (flaggedDefaultCandidate.name, mutatorDefaultCandidate.name))
|
|
||||||
self.default = flaggedDefaultCandidate
|
|
||||||
self.defaultLoc = self.default.location
|
|
||||||
else:
|
|
||||||
# we have no flagged default candidate
|
|
||||||
# let's use the one from mutator
|
|
||||||
if flaggedDefaultCandidate is None and mutatorDefaultCandidate is not None:
|
|
||||||
# we didn't have a flag, use the one selected by mutator
|
|
||||||
self.default = mutatorDefaultCandidate
|
|
||||||
self.defaultLoc = self.default.location
|
|
||||||
self.default.copyInfo = True
|
|
||||||
# now that we have a default, let's check if the axes are ok
|
|
||||||
for axisObj in self.axes:
|
|
||||||
if axisObj.name not in self.default.location:
|
|
||||||
# extend the location of the neutral master with missing default value for this axis
|
|
||||||
self.default.location[axisObj.name] = axisObj.default
|
|
||||||
else:
|
|
||||||
if axisObj.default == self.default.location.get(axisObj.name):
|
|
||||||
continue
|
|
||||||
# proposed remedy: change default value in the axisdescriptor to the value of the neutral
|
|
||||||
neutralAxisValue = self.default.location.get(axisObj.name)
|
|
||||||
# make sure this value is between the min and max
|
|
||||||
if axisObj.minimum <= neutralAxisValue <= axisObj.maximum:
|
|
||||||
# yes we can fix this
|
|
||||||
axisObj.default = neutralAxisValue
|
|
||||||
self.logger.info("Note: updating the default value of axis %s to neutral master at %3.3f" % (axisObj.name, neutralAxisValue))
|
|
||||||
# always fit the axis dimensions to the location of the designated neutral
|
|
||||||
elif neutralAxisValue < axisObj.minimum:
|
|
||||||
axisObj.default = neutralAxisValue
|
|
||||||
axisObj.minimum = neutralAxisValue
|
|
||||||
elif neutralAxisValue > axisObj.maximum:
|
|
||||||
axisObj.maximum = neutralAxisValue
|
|
||||||
axisObj.default = neutralAxisValue
|
|
||||||
else:
|
|
||||||
# now we're in trouble, can't solve this, alert.
|
|
||||||
self.logger.info("Warning: mismatched default value for axis %s and neutral master. Master value outside of axis bounds" % (axisObj.name))
|
|
||||||
|
|
||||||
def getMutatorDefaultCandidate(self):
|
|
||||||
# FIXME: original implementation using MutatorMath
|
|
||||||
# masterLocations = [src.location for src in self.sources]
|
|
||||||
# mutatorBias = biasFromLocations(masterLocations, preferOrigin=False)
|
|
||||||
# for src in self.sources:
|
|
||||||
# if src.location == mutatorBias:
|
|
||||||
# return src
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _prepAxesForBender(self):
|
|
||||||
"""
|
|
||||||
Make the axis data we have available in
|
|
||||||
"""
|
|
||||||
benderAxes = {}
|
|
||||||
for axisDescriptor in self.axes:
|
|
||||||
d = {
|
|
||||||
'name': axisDescriptor.name,
|
|
||||||
'tag': axisDescriptor.tag,
|
|
||||||
'minimum': axisDescriptor.minimum,
|
|
||||||
'maximum': axisDescriptor.maximum,
|
|
||||||
'default': axisDescriptor.default,
|
|
||||||
'map': axisDescriptor.map,
|
|
||||||
}
|
|
||||||
benderAxes[axisDescriptor.name] = d
|
|
||||||
return benderAxes
|
|
||||||
|
|
||||||
def checkAxes(self, overwrite=False):
|
|
||||||
"""
|
|
||||||
If we don't have axes in the document, make some, report
|
|
||||||
Should we include the instance locations when determining the axis extrema?
|
|
||||||
"""
|
|
||||||
axisValues = {}
|
|
||||||
# find all the axes
|
|
||||||
locations = []
|
|
||||||
for sourceDescriptor in self.sources:
|
|
||||||
locations.append(sourceDescriptor.location)
|
|
||||||
for instanceDescriptor in self.instances:
|
|
||||||
locations.append(instanceDescriptor.location)
|
|
||||||
for name, glyphData in instanceDescriptor.glyphs.items():
|
|
||||||
loc = glyphData.get("instanceLocation")
|
|
||||||
if loc is not None:
|
|
||||||
locations.append(loc)
|
|
||||||
for m in glyphData.get('masters', []):
|
|
||||||
locations.append(m['location'])
|
|
||||||
for loc in locations:
|
|
||||||
for name, value in loc.items():
|
|
||||||
if not name in axisValues:
|
|
||||||
axisValues[name] = []
|
|
||||||
if type(value)==tuple:
|
|
||||||
for v in value:
|
|
||||||
axisValues[name].append(v)
|
|
||||||
else:
|
|
||||||
axisValues[name].append(value)
|
|
||||||
have = self.getAxisOrder()
|
|
||||||
for name, values in axisValues.items():
|
|
||||||
a = None
|
|
||||||
if name in have:
|
|
||||||
if overwrite:
|
|
||||||
# we have the axis,
|
|
||||||
a = self.getAxis(name)
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
# we need to make this axis
|
|
||||||
a = self.newAxisDescriptor()
|
|
||||||
self.addAxis(a)
|
|
||||||
a.name = name
|
|
||||||
a.minimum = min(values)
|
|
||||||
a.maximum = max(values)
|
|
||||||
a.default = a.minimum
|
|
||||||
a.tag, a.labelNames = tagForAxisName(a.name)
|
|
||||||
self.logger.info("CheckAxes: added a missing axis %s, %3.3f %3.3f", a.name, a.minimum, a.maximum)
|
|
||||||
|
|
||||||
|
|
||||||
def normalizeLocation(self, location):
|
def normalizeLocation(self, location):
|
||||||
# scale this location based on the axes
|
# adapted from fontTools.varlib.models.normalizeLocation because:
|
||||||
# accept only values for the axes that we have definitions for
|
# - this needs to work with axis names, not tags
|
||||||
# only normalise if we're valid?
|
# - this needs to accomodate anisotropic locations
|
||||||
# normalise anisotropic cooordinates to isotropic.
|
# - the axes are stored differently here, it's just math
|
||||||
# copied from fontTools.varlib.models.normalizeLocation
|
|
||||||
new = {}
|
new = {}
|
||||||
for axis in self.axes:
|
for axis in self.axes:
|
||||||
if not axis.name in location:
|
if not axis.name in location:
|
||||||
@ -1391,6 +1176,7 @@ class DesignSpaceDocument(object):
|
|||||||
return new
|
return new
|
||||||
|
|
||||||
def normalize(self):
|
def normalize(self):
|
||||||
|
# Normalise the geometry of this designspace:
|
||||||
# scale all the locations of all masters and instances to the -1 - 0 - 1 value.
|
# 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.
|
# we need the axis data to do the scaling, so we do those last.
|
||||||
# masters
|
# masters
|
||||||
@ -1404,7 +1190,7 @@ class DesignSpaceDocument(object):
|
|||||||
for glyphMaster in glyphData['masters']:
|
for glyphMaster in glyphData['masters']:
|
||||||
glyphMaster['location'] = self.normalizeLocation(glyphMaster['location'])
|
glyphMaster['location'] = self.normalizeLocation(glyphMaster['location'])
|
||||||
item.location = self.normalizeLocation(item.location)
|
item.location = self.normalizeLocation(item.location)
|
||||||
# now the axes
|
# the axes
|
||||||
for axis in self.axes:
|
for axis in self.axes:
|
||||||
# scale the map first
|
# scale the map first
|
||||||
newMap = []
|
newMap = []
|
||||||
@ -1439,20 +1225,3 @@ class DesignSpaceDocument(object):
|
|||||||
newConditionSets.append(newConditions)
|
newConditionSets.append(newConditions)
|
||||||
rule.conditionSets = newConditionSets
|
rule.conditionSets = newConditionSets
|
||||||
|
|
||||||
|
|
||||||
def rulesToFeature(doc, whiteSpace="\t", newLine="\n"):
|
|
||||||
""" Showing how rules could be expressed as FDK feature text.
|
|
||||||
Speculative. Experimental.
|
|
||||||
"""
|
|
||||||
axisNames = {axis.name: axis.tag for axis in doc.axes}
|
|
||||||
axisDims = {axis.tag: (axis.minimum, axis.maximum) for axis in doc.axes}
|
|
||||||
text = []
|
|
||||||
for rule in doc.rules:
|
|
||||||
text.append("rule %s{" % rule.name)
|
|
||||||
for cd in rule.conditions:
|
|
||||||
axisTag = axisNames.get(cd.get('name'), "****")
|
|
||||||
axisMinimum = cd.get('minimum', axisDims.get(axisTag, [0,0])[0])
|
|
||||||
axisMaximum = cd.get('maximum', axisDims.get(axisTag, [0,0])[1])
|
|
||||||
text.append("%s%s %f %f;" % (whiteSpace, axisTag, axisMinimum, axisMaximum))
|
|
||||||
text.append("} %s;" % rule.name)
|
|
||||||
return newLine.join(text)
|
|
||||||
|
@ -5,12 +5,30 @@ from __future__ import (print_function, division, absolute_import,
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
|
import warnings
|
||||||
|
|
||||||
from fontTools.misc.py23 import open
|
from fontTools.misc.py23 import open
|
||||||
from fontTools.designspaceLib import (
|
from fontTools.designspaceLib import (
|
||||||
DesignSpaceDocument, SourceDescriptor, AxisDescriptor, RuleDescriptor,
|
DesignSpaceDocument, SourceDescriptor, AxisDescriptor, RuleDescriptor,
|
||||||
InstanceDescriptor, evaluateRule, processRules, posix, DesignSpaceDocumentError)
|
InstanceDescriptor, evaluateRule, processRules, posix, DesignSpaceDocumentError)
|
||||||
|
|
||||||
|
def _axesAsDict(axes):
|
||||||
|
"""
|
||||||
|
Make the axis data we have available in
|
||||||
|
"""
|
||||||
|
axesDict = {}
|
||||||
|
for axisDescriptor in axes:
|
||||||
|
d = {
|
||||||
|
'name': axisDescriptor.name,
|
||||||
|
'tag': axisDescriptor.tag,
|
||||||
|
'minimum': axisDescriptor.minimum,
|
||||||
|
'maximum': axisDescriptor.maximum,
|
||||||
|
'default': axisDescriptor.default,
|
||||||
|
'map': axisDescriptor.map,
|
||||||
|
}
|
||||||
|
axesDict[axisDescriptor.name] = d
|
||||||
|
return axesDict
|
||||||
|
|
||||||
|
|
||||||
def assert_equals_test_file(path, test_filename):
|
def assert_equals_test_file(path, test_filename):
|
||||||
with open(path) as fp:
|
with open(path) as fp:
|
||||||
@ -31,6 +49,29 @@ def test_fill_document(tmpdir):
|
|||||||
instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo")
|
instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo")
|
||||||
instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo")
|
instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo")
|
||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
|
|
||||||
|
# write some axes
|
||||||
|
a1 = AxisDescriptor()
|
||||||
|
a1.minimum = 0
|
||||||
|
a1.maximum = 1000
|
||||||
|
a1.default = 0
|
||||||
|
a1.name = "weight"
|
||||||
|
a1.tag = "wght"
|
||||||
|
# note: just to test the element language, not an actual label name recommendations.
|
||||||
|
a1.labelNames[u'fa-IR'] = u"قطر"
|
||||||
|
a1.labelNames[u'en'] = u"Wéíght"
|
||||||
|
doc.addAxis(a1)
|
||||||
|
a2 = AxisDescriptor()
|
||||||
|
a2.minimum = 0
|
||||||
|
a2.maximum = 1000
|
||||||
|
a2.default = 20
|
||||||
|
a2.name = "width"
|
||||||
|
a2.tag = "wdth"
|
||||||
|
a2.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)]
|
||||||
|
a2.hidden = True
|
||||||
|
a2.labelNames[u'fr'] = u"Chasse"
|
||||||
|
doc.addAxis(a2)
|
||||||
|
|
||||||
# add master 1
|
# add master 1
|
||||||
s1 = SourceDescriptor()
|
s1 = SourceDescriptor()
|
||||||
s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath))
|
s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath))
|
||||||
@ -107,45 +148,6 @@ def test_fill_document(tmpdir):
|
|||||||
doc.filename = "suggestedFileName.designspace"
|
doc.filename = "suggestedFileName.designspace"
|
||||||
doc.lib['com.coolDesignspaceApp.previewSize'] = 30
|
doc.lib['com.coolDesignspaceApp.previewSize'] = 30
|
||||||
|
|
||||||
# now we have sources and instances, but no axes yet.
|
|
||||||
doc.check()
|
|
||||||
|
|
||||||
# Here, since the axes are not defined in the document, but instead are
|
|
||||||
# infered from the locations of the instances, we cannot guarantee the
|
|
||||||
# order in which they will be created by the `check()` method.
|
|
||||||
assert set(doc.getAxisOrder()) == set(['spooky', 'weight', 'width'])
|
|
||||||
doc.axes = [] # clear the axes
|
|
||||||
|
|
||||||
# write some axes
|
|
||||||
a1 = AxisDescriptor()
|
|
||||||
a1.minimum = 0
|
|
||||||
a1.maximum = 1000
|
|
||||||
a1.default = 0
|
|
||||||
a1.name = "weight"
|
|
||||||
a1.tag = "wght"
|
|
||||||
# note: just to test the element language, not an actual label name recommendations.
|
|
||||||
a1.labelNames[u'fa-IR'] = u"قطر"
|
|
||||||
a1.labelNames[u'en'] = u"Wéíght"
|
|
||||||
doc.addAxis(a1)
|
|
||||||
a2 = AxisDescriptor()
|
|
||||||
a2.minimum = 0
|
|
||||||
a2.maximum = 1000
|
|
||||||
a2.default = 20
|
|
||||||
a2.name = "width"
|
|
||||||
a2.tag = "wdth"
|
|
||||||
a2.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)]
|
|
||||||
a2.hidden = True
|
|
||||||
a2.labelNames[u'fr'] = u"Chasse"
|
|
||||||
doc.addAxis(a2)
|
|
||||||
# add an axis that is not part of any location to see if that works
|
|
||||||
a3 = AxisDescriptor()
|
|
||||||
a3.minimum = 333
|
|
||||||
a3.maximum = 666
|
|
||||||
a3.default = 444
|
|
||||||
a3.name = "spooky"
|
|
||||||
a3.tag = "SPOK"
|
|
||||||
a3.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)]
|
|
||||||
#doc.addAxis(a3) # uncomment this line to test the effects of default axes values
|
|
||||||
# write some rules
|
# write some rules
|
||||||
r1 = RuleDescriptor()
|
r1 = RuleDescriptor()
|
||||||
r1.name = "named.rule.1"
|
r1.name = "named.rule.1"
|
||||||
@ -163,23 +165,11 @@ def test_fill_document(tmpdir):
|
|||||||
new = DesignSpaceDocument()
|
new = DesignSpaceDocument()
|
||||||
new.read(testDocPath)
|
new.read(testDocPath)
|
||||||
|
|
||||||
new.check()
|
|
||||||
assert new.default.location == {'width': 20.0, 'weight': 0.0}
|
assert new.default.location == {'width': 20.0, 'weight': 0.0}
|
||||||
assert new.filename == 'test.designspace'
|
assert new.filename == 'test.designspace'
|
||||||
assert new.lib == doc.lib
|
assert new.lib == doc.lib
|
||||||
assert new.instances[0].lib == doc.instances[0].lib
|
assert new.instances[0].lib == doc.instances[0].lib
|
||||||
|
|
||||||
# >>> for a, b in zip(doc.instances, new.instances):
|
|
||||||
# ... a.compare(b)
|
|
||||||
# >>> for a, b in zip(doc.sources, new.sources):
|
|
||||||
# ... a.compare(b)
|
|
||||||
# >>> for a, b in zip(doc.axes, new.axes):
|
|
||||||
# ... a.compare(b)
|
|
||||||
# >>> [n.mutedGlyphNames for n in new.sources]
|
|
||||||
# [['A', 'Z'], []]
|
|
||||||
# >>> doc.getFonts()
|
|
||||||
# []
|
|
||||||
|
|
||||||
# test roundtrip for the axis attributes and data
|
# test roundtrip for the axis attributes and data
|
||||||
axes = {}
|
axes = {}
|
||||||
for axis in doc.axes:
|
for axis in doc.axes:
|
||||||
@ -197,50 +187,6 @@ def test_fill_document(tmpdir):
|
|||||||
assert a == b
|
assert a == b
|
||||||
|
|
||||||
|
|
||||||
def test_adjustAxisDefaultToNeutral(tmpdir):
|
|
||||||
tmpdir = str(tmpdir)
|
|
||||||
testDocPath = os.path.join(tmpdir, "testAdjustAxisDefaultToNeutral.designspace")
|
|
||||||
masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo")
|
|
||||||
masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo")
|
|
||||||
instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo")
|
|
||||||
instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo")
|
|
||||||
doc = DesignSpaceDocument()
|
|
||||||
# add master 1
|
|
||||||
s1 = SourceDescriptor()
|
|
||||||
s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath))
|
|
||||||
s1.name = "master.ufo1"
|
|
||||||
s1.copyInfo = True
|
|
||||||
s1.copyFeatures = True
|
|
||||||
s1.location = dict(weight=55, width=1000)
|
|
||||||
doc.addSource(s1)
|
|
||||||
# write some axes
|
|
||||||
a1 = AxisDescriptor()
|
|
||||||
a1.minimum = 0
|
|
||||||
a1.maximum = 1000
|
|
||||||
a1.default = 0 # the wrong value
|
|
||||||
a1.name = "weight"
|
|
||||||
a1.tag = "wght"
|
|
||||||
doc.addAxis(a1)
|
|
||||||
a2 = AxisDescriptor()
|
|
||||||
a2.minimum = -10
|
|
||||||
a2.maximum = 10
|
|
||||||
a2.default = 0 # the wrong value
|
|
||||||
a2.name = "width"
|
|
||||||
a2.tag = "wdth"
|
|
||||||
doc.addAxis(a2)
|
|
||||||
# write the document
|
|
||||||
doc.write(testDocPath)
|
|
||||||
assert os.path.exists(testDocPath)
|
|
||||||
# import it again
|
|
||||||
new = DesignSpaceDocument()
|
|
||||||
new.read(testDocPath)
|
|
||||||
new.check()
|
|
||||||
loc = new.default.location
|
|
||||||
for axisObj in new.axes:
|
|
||||||
n = axisObj.name
|
|
||||||
assert axisObj.default == loc.get(n)
|
|
||||||
|
|
||||||
|
|
||||||
def test_unicodes(tmpdir):
|
def test_unicodes(tmpdir):
|
||||||
tmpdir = str(tmpdir)
|
tmpdir = str(tmpdir)
|
||||||
testDocPath = os.path.join(tmpdir, "testUnicodes.designspace")
|
testDocPath = os.path.join(tmpdir, "testUnicodes.designspace")
|
||||||
@ -457,7 +403,6 @@ def test_handleNoAxes(tmpdir):
|
|||||||
doc.addInstance(i1)
|
doc.addInstance(i1)
|
||||||
|
|
||||||
doc.write(testDocPath)
|
doc.write(testDocPath)
|
||||||
__removeAxesFromDesignSpace(testDocPath)
|
|
||||||
verify = DesignSpaceDocument()
|
verify = DesignSpaceDocument()
|
||||||
verify.read(testDocPath)
|
verify.read(testDocPath)
|
||||||
verify.write(testDocPath2)
|
verify.write(testDocPath2)
|
||||||
@ -476,8 +421,16 @@ def test_pathNameResolve(tmpdir):
|
|||||||
instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo")
|
instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo")
|
||||||
instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo")
|
instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo")
|
||||||
|
|
||||||
|
a1 = AxisDescriptor()
|
||||||
|
a1.tag = "TAGA"
|
||||||
|
a1.name = "axisName_a"
|
||||||
|
a1.minimum = 0
|
||||||
|
a1.maximum = 1000
|
||||||
|
a1.default = 0
|
||||||
|
|
||||||
# Case 1: filename and path are both empty. Nothing to calculate, nothing to put in the file.
|
# Case 1: filename and path are both empty. Nothing to calculate, nothing to put in the file.
|
||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
|
doc.addAxis(a1)
|
||||||
s = SourceDescriptor()
|
s = SourceDescriptor()
|
||||||
s.filename = None
|
s.filename = None
|
||||||
s.path = None
|
s.path = None
|
||||||
@ -494,6 +447,7 @@ def test_pathNameResolve(tmpdir):
|
|||||||
|
|
||||||
# Case 2: filename is empty, path points somewhere: calculate a new filename.
|
# Case 2: filename is empty, path points somewhere: calculate a new filename.
|
||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
|
doc.addAxis(a1)
|
||||||
s = SourceDescriptor()
|
s = SourceDescriptor()
|
||||||
s.filename = None
|
s.filename = None
|
||||||
s.path = masterPath1
|
s.path = masterPath1
|
||||||
@ -510,6 +464,7 @@ def test_pathNameResolve(tmpdir):
|
|||||||
|
|
||||||
# Case 3: the filename is set, the path is None.
|
# Case 3: the filename is set, the path is None.
|
||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
|
doc.addAxis(a1)
|
||||||
s = SourceDescriptor()
|
s = SourceDescriptor()
|
||||||
s.filename = "../somewhere/over/the/rainbow.ufo"
|
s.filename = "../somewhere/over/the/rainbow.ufo"
|
||||||
s.path = None
|
s.path = None
|
||||||
@ -528,6 +483,7 @@ def test_pathNameResolve(tmpdir):
|
|||||||
|
|
||||||
# Case 4: the filename points to one file, the path points to another. The path takes precedence.
|
# Case 4: the filename points to one file, the path points to another. The path takes precedence.
|
||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
|
doc.addAxis(a1)
|
||||||
s = SourceDescriptor()
|
s = SourceDescriptor()
|
||||||
s.filename = "../somewhere/over/the/rainbow.ufo"
|
s.filename = "../somewhere/over/the/rainbow.ufo"
|
||||||
s.path = masterPath1
|
s.path = masterPath1
|
||||||
@ -543,6 +499,7 @@ def test_pathNameResolve(tmpdir):
|
|||||||
|
|
||||||
# Case 5: the filename is None, path has a value, update the filename
|
# Case 5: the filename is None, path has a value, update the filename
|
||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
|
doc.addAxis(a1)
|
||||||
s = SourceDescriptor()
|
s = SourceDescriptor()
|
||||||
s.filename = None
|
s.filename = None
|
||||||
s.path = masterPath1
|
s.path = masterPath1
|
||||||
@ -557,6 +514,7 @@ def test_pathNameResolve(tmpdir):
|
|||||||
|
|
||||||
# Case 6: the filename has a value, path has a value, update the filenames with force
|
# Case 6: the filename has a value, path has a value, update the filenames with force
|
||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
|
doc.addAxis(a1)
|
||||||
s = SourceDescriptor()
|
s = SourceDescriptor()
|
||||||
s.filename = "../somewhere/over/the/rainbow.ufo"
|
s.filename = "../somewhere/over/the/rainbow.ufo"
|
||||||
s.path = masterPath1
|
s.path = masterPath1
|
||||||
@ -571,7 +529,8 @@ def test_pathNameResolve(tmpdir):
|
|||||||
assert doc.sources[0].filename == "masters/masterTest1.ufo"
|
assert doc.sources[0].filename == "masters/masterTest1.ufo"
|
||||||
|
|
||||||
|
|
||||||
def test_normalise():
|
def test_normalise1():
|
||||||
|
# normalisation of anisotropic locations, clipping
|
||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
# write some axes
|
# write some axes
|
||||||
a1 = AxisDescriptor()
|
a1 = AxisDescriptor()
|
||||||
@ -581,10 +540,8 @@ def test_normalise():
|
|||||||
a1.name = "axisName_a"
|
a1.name = "axisName_a"
|
||||||
a1.tag = "TAGA"
|
a1.tag = "TAGA"
|
||||||
doc.addAxis(a1)
|
doc.addAxis(a1)
|
||||||
|
|
||||||
assert doc.normalizeLocation(dict(axisName_a=0)) == {'axisName_a': 0.0}
|
assert doc.normalizeLocation(dict(axisName_a=0)) == {'axisName_a': 0.0}
|
||||||
assert doc.normalizeLocation(dict(axisName_a=1000)) == {'axisName_a': 1.0}
|
assert doc.normalizeLocation(dict(axisName_a=1000)) == {'axisName_a': 1.0}
|
||||||
|
|
||||||
# clipping beyond max values:
|
# clipping beyond max values:
|
||||||
assert doc.normalizeLocation(dict(axisName_a=1001)) == {'axisName_a': 1.0}
|
assert doc.normalizeLocation(dict(axisName_a=1001)) == {'axisName_a': 1.0}
|
||||||
assert doc.normalizeLocation(dict(axisName_a=500)) == {'axisName_a': 0.5}
|
assert doc.normalizeLocation(dict(axisName_a=500)) == {'axisName_a': 0.5}
|
||||||
@ -599,6 +556,8 @@ def test_normalise():
|
|||||||
r.sort()
|
r.sort()
|
||||||
assert r == [('axisName_a', -1.0, 0.0, 1.0)]
|
assert r == [('axisName_a', -1.0, 0.0, 1.0)]
|
||||||
|
|
||||||
|
def test_normalise2():
|
||||||
|
# normalisation with minimum > 0
|
||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
# write some axes
|
# write some axes
|
||||||
a2 = AxisDescriptor()
|
a2 = AxisDescriptor()
|
||||||
@ -624,6 +583,8 @@ def test_normalise():
|
|||||||
r.sort()
|
r.sort()
|
||||||
assert r == [('axisName_b', 0.0, 0.0, 1.0)]
|
assert r == [('axisName_b', 0.0, 0.0, 1.0)]
|
||||||
|
|
||||||
|
def test_normalise3():
|
||||||
|
# normalisation of negative values, with default == maximum
|
||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
# write some axes
|
# write some axes
|
||||||
a3 = AxisDescriptor()
|
a3 = AxisDescriptor()
|
||||||
@ -636,7 +597,6 @@ def test_normalise():
|
|||||||
assert doc.normalizeLocation(dict(ccc=1)) == {'ccc': 0.0}
|
assert doc.normalizeLocation(dict(ccc=1)) == {'ccc': 0.0}
|
||||||
assert doc.normalizeLocation(dict(ccc=-1000)) == {'ccc': -1.0}
|
assert doc.normalizeLocation(dict(ccc=-1000)) == {'ccc': -1.0}
|
||||||
assert doc.normalizeLocation(dict(ccc=-1001)) == {'ccc': -1.0}
|
assert doc.normalizeLocation(dict(ccc=-1001)) == {'ccc': -1.0}
|
||||||
|
|
||||||
doc.normalize()
|
doc.normalize()
|
||||||
r = []
|
r = []
|
||||||
for axis in doc.axes:
|
for axis in doc.axes:
|
||||||
@ -644,28 +604,8 @@ def test_normalise():
|
|||||||
r.sort()
|
r.sort()
|
||||||
assert r == [('ccc', -1.0, 0.0, 0.0)]
|
assert r == [('ccc', -1.0, 0.0, 0.0)]
|
||||||
|
|
||||||
|
def test_normalise4():
|
||||||
doc = DesignSpaceDocument()
|
# normalisation with a map
|
||||||
# write some axes
|
|
||||||
a3 = AxisDescriptor()
|
|
||||||
a3.minimum = 2000
|
|
||||||
a3.maximum = 3000
|
|
||||||
a3.default = 2000
|
|
||||||
a3.name = "ccc"
|
|
||||||
doc.addAxis(a3)
|
|
||||||
assert doc.normalizeLocation(dict(ccc=0)) == {'ccc': 0.0}
|
|
||||||
assert doc.normalizeLocation(dict(ccc=1)) == {'ccc': 0.0}
|
|
||||||
assert doc.normalizeLocation(dict(ccc=-1000)) == {'ccc': 0.0}
|
|
||||||
assert doc.normalizeLocation(dict(ccc=-1001)) == {'ccc': 0.0}
|
|
||||||
|
|
||||||
doc.normalize()
|
|
||||||
r = []
|
|
||||||
for axis in doc.axes:
|
|
||||||
r.append((axis.name, axis.minimum, axis.default, axis.maximum))
|
|
||||||
r.sort()
|
|
||||||
assert r == [('ccc', 0.0, 0.0, 1.0)]
|
|
||||||
|
|
||||||
|
|
||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
# write some axes
|
# write some axes
|
||||||
a4 = AxisDescriptor()
|
a4 = AxisDescriptor()
|
||||||
@ -682,28 +622,27 @@ def test_normalise():
|
|||||||
r.sort()
|
r.sort()
|
||||||
assert r == [('ddd', [(0, 0.1), (300, 0.5), (600, 0.5), (1000, 0.9)])]
|
assert r == [('ddd', [(0, 0.1), (300, 0.5), (600, 0.5), (1000, 0.9)])]
|
||||||
|
|
||||||
|
def test_axisMapping():
|
||||||
def test_rules(tmpdir):
|
# note: because designspance lib does not do any actual
|
||||||
tmpdir = str(tmpdir)
|
# processing of the mapping data, we can only check if there data is there.
|
||||||
testDocPath = os.path.join(tmpdir, "testRules.designspace")
|
|
||||||
testDocPath2 = os.path.join(tmpdir, "testRules_roundtrip.designspace")
|
|
||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
# write some axes
|
# write some axes
|
||||||
a1 = AxisDescriptor()
|
a4 = AxisDescriptor()
|
||||||
a1.tag = "TAGA"
|
a4.minimum = 0
|
||||||
a1.name = "axisName_a"
|
a4.maximum = 1000
|
||||||
a1.minimum = 0
|
a4.default = 0
|
||||||
a1.maximum = 1000
|
a4.name = "ddd"
|
||||||
a1.default = 0
|
a4.map = [(0,100), (300, 500), (600, 500), (1000,900)]
|
||||||
doc.addAxis(a1)
|
doc.addAxis(a4)
|
||||||
a2 = AxisDescriptor()
|
doc.normalize()
|
||||||
a2.tag = "TAGB"
|
r = []
|
||||||
a2.name = "axisName_b"
|
for axis in doc.axes:
|
||||||
a2.minimum = 0
|
r.append((axis.name, axis.map))
|
||||||
a2.maximum = 3000
|
r.sort()
|
||||||
a2.default = 0
|
assert r == [('ddd', [(0, 0.1), (300, 0.5), (600, 0.5), (1000, 0.9)])]
|
||||||
doc.addAxis(a2)
|
|
||||||
|
|
||||||
|
def test_rulesConditions(tmpdir):
|
||||||
|
# tests of rules, conditionsets and conditions
|
||||||
r1 = RuleDescriptor()
|
r1 = RuleDescriptor()
|
||||||
r1.name = "named.rule.1"
|
r1.name = "named.rule.1"
|
||||||
r1.conditionSets.append([
|
r1.conditionSets.append([
|
||||||
@ -712,11 +651,6 @@ def test_rules(tmpdir):
|
|||||||
])
|
])
|
||||||
r1.subs.append(("a", "a.alt"))
|
r1.subs.append(("a", "a.alt"))
|
||||||
|
|
||||||
# rule with minium and maximum
|
|
||||||
doc.addRule(r1)
|
|
||||||
assert len(doc.rules) == 1
|
|
||||||
assert len(doc.rules[0].conditionSets) == 1
|
|
||||||
assert len(doc.rules[0].conditionSets[0]) == 2
|
|
||||||
assert evaluateRule(r1, dict(axisName_a = 500, axisName_b = 0)) == True
|
assert evaluateRule(r1, dict(axisName_a = 500, axisName_b = 0)) == True
|
||||||
assert evaluateRule(r1, dict(axisName_a = 0, axisName_b = 0)) == True
|
assert evaluateRule(r1, dict(axisName_a = 0, axisName_b = 0)) == True
|
||||||
assert evaluateRule(r1, dict(axisName_a = 1000, axisName_b = 0)) == True
|
assert evaluateRule(r1, dict(axisName_a = 1000, axisName_b = 0)) == True
|
||||||
@ -761,6 +695,12 @@ def test_rules(tmpdir):
|
|||||||
assert evaluateRule(r4, dict(axisName_a = 0, axisName_b = 0)) == False
|
assert evaluateRule(r4, dict(axisName_a = 0, axisName_b = 0)) == False
|
||||||
assert evaluateRule(r4, dict(axisName_a = 1000, axisName_b = 1000)) == False
|
assert evaluateRule(r4, dict(axisName_a = 1000, axisName_b = 1000)) == False
|
||||||
|
|
||||||
|
def test_rulesDocument(tmpdir):
|
||||||
|
# tests of rules in a document, roundtripping.
|
||||||
|
tmpdir = str(tmpdir)
|
||||||
|
testDocPath = os.path.join(tmpdir, "testRules.designspace")
|
||||||
|
testDocPath2 = os.path.join(tmpdir, "testRules_roundtrip.designspace")
|
||||||
|
doc = DesignSpaceDocument()
|
||||||
a1 = AxisDescriptor()
|
a1 = AxisDescriptor()
|
||||||
a1.minimum = 0
|
a1.minimum = 0
|
||||||
a1.maximum = 1000
|
a1.maximum = 1000
|
||||||
@ -775,69 +715,77 @@ def test_rules(tmpdir):
|
|||||||
b1.tag = "TAGB"
|
b1.tag = "TAGB"
|
||||||
doc.addAxis(a1)
|
doc.addAxis(a1)
|
||||||
doc.addAxis(b1)
|
doc.addAxis(b1)
|
||||||
assert doc._prepAxesForBender() == {'axisName_a': {'map': [], 'name': 'axisName_a', 'default': 0, 'minimum': 0, 'maximum': 1000, 'tag': 'TAGA'}, 'axisName_b': {'map': [], 'name': 'axisName_b', 'default': 2000, 'minimum': 2000, 'maximum': 3000, 'tag': 'TAGB'}}
|
r1 = RuleDescriptor()
|
||||||
|
r1.name = "named.rule.1"
|
||||||
|
r1.conditionSets.append([
|
||||||
|
dict(name='axisName_a', minimum=0, maximum=1000),
|
||||||
|
dict(name='axisName_b', minimum=0, maximum=3000)
|
||||||
|
])
|
||||||
|
r1.subs.append(("a", "a.alt"))
|
||||||
|
# rule with minium and maximum
|
||||||
|
doc.addRule(r1)
|
||||||
|
assert len(doc.rules) == 1
|
||||||
|
assert len(doc.rules[0].conditionSets) == 1
|
||||||
|
assert len(doc.rules[0].conditionSets[0]) == 2
|
||||||
|
assert _axesAsDict(doc.axes) == {'axisName_a': {'map': [], 'name': 'axisName_a', 'default': 0, 'minimum': 0, 'maximum': 1000, 'tag': 'TAGA'}, 'axisName_b': {'map': [], 'name': 'axisName_b', 'default': 2000, 'minimum': 2000, 'maximum': 3000, 'tag': 'TAGB'}}
|
||||||
assert doc.rules[0].conditionSets == [[
|
assert doc.rules[0].conditionSets == [[
|
||||||
{'minimum': 0, 'maximum': 1000, 'name': 'axisName_a'},
|
{'minimum': 0, 'maximum': 1000, 'name': 'axisName_a'},
|
||||||
{'minimum': 0, 'maximum': 3000, 'name': 'axisName_b'}]]
|
{'minimum': 0, 'maximum': 3000, 'name': 'axisName_b'}]]
|
||||||
|
|
||||||
assert doc.rules[0].subs == [('a', 'a.alt')]
|
assert doc.rules[0].subs == [('a', 'a.alt')]
|
||||||
|
|
||||||
doc.normalize()
|
doc.normalize()
|
||||||
assert doc.rules[0].name == 'named.rule.1'
|
assert doc.rules[0].name == 'named.rule.1'
|
||||||
assert doc.rules[0].conditionSets == [[
|
assert doc.rules[0].conditionSets == [[
|
||||||
{'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_a'},
|
{'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_a'},
|
||||||
{'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_b'}]]
|
{'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_b'}]]
|
||||||
|
# still one conditionset
|
||||||
|
assert len(doc.rules[0].conditionSets) == 1
|
||||||
doc.write(testDocPath)
|
doc.write(testDocPath)
|
||||||
new = DesignSpaceDocument()
|
# add a stray conditionset
|
||||||
|
_addUnwrappedCondition(testDocPath)
|
||||||
|
doc2 = DesignSpaceDocument()
|
||||||
|
doc2.read(testDocPath)
|
||||||
|
assert len(doc2.axes) == 2
|
||||||
|
assert len(doc2.rules) == 1
|
||||||
|
assert len(doc2.rules[0].conditionSets) == 2
|
||||||
|
doc2.write(testDocPath2)
|
||||||
|
# verify these results
|
||||||
|
# make sure the stray condition is now neatly wrapped in a conditionset.
|
||||||
|
doc3 = DesignSpaceDocument()
|
||||||
|
doc3.read(testDocPath2)
|
||||||
|
assert len(doc3.rules) == 1
|
||||||
|
assert len(doc3.rules[0].conditionSets) == 2
|
||||||
|
|
||||||
new.read(testDocPath)
|
def _addUnwrappedCondition(path):
|
||||||
assert len(new.axes) == 4
|
|
||||||
assert len(new.rules) == 1
|
|
||||||
new.write(testDocPath2)
|
|
||||||
|
|
||||||
|
|
||||||
def test_incompleteRule(tmpdir):
|
|
||||||
tmpdir = str(tmpdir)
|
|
||||||
testDocPath1 = os.path.join(tmpdir, "testIncompleteRule.designspace")
|
|
||||||
doc = DesignSpaceDocument()
|
|
||||||
r1 = RuleDescriptor()
|
|
||||||
r1.name = "incomplete.rule.1"
|
|
||||||
r1.conditionSets.append([
|
|
||||||
dict(name='axisName_a', minimum=100),
|
|
||||||
dict(name='axisName_b', maximum=200)
|
|
||||||
])
|
|
||||||
r1.subs.append(("c", "c.alt"))
|
|
||||||
doc.addRule(r1)
|
|
||||||
doc.write(testDocPath1)
|
|
||||||
__removeConditionMinimumMaximumDesignSpace(testDocPath1)
|
|
||||||
new = DesignSpaceDocument()
|
|
||||||
with pytest.raises(DesignSpaceDocumentError) as excinfo:
|
|
||||||
new.read(testDocPath1)
|
|
||||||
assert "No minimum or maximum defined in rule" in str(excinfo.value)
|
|
||||||
|
|
||||||
def __removeConditionMinimumMaximumDesignSpace(path):
|
|
||||||
# only for testing, so we can make an invalid designspace file
|
# only for testing, so we can make an invalid designspace file
|
||||||
# without making the designSpaceDocument also support it.
|
# older designspace files may have conditions that are not wrapped in a conditionset
|
||||||
|
# These can be read into a new conditionset.
|
||||||
f = open(path, 'r', encoding='utf-8')
|
f = open(path, 'r', encoding='utf-8')
|
||||||
d = f.read()
|
d = f.read()
|
||||||
|
print(d)
|
||||||
f.close()
|
f.close()
|
||||||
d = d.replace(' minimum="100"', '')
|
d = d.replace('<rule name="named.rule.1">', '<rule name="named.rule.1">\n\t<condition maximum="22" minimum="33" name="axisName_a" />')
|
||||||
f = open(path, 'w', encoding='utf-8')
|
f = open(path, 'w', encoding='utf-8')
|
||||||
f.write(d)
|
f.write(d)
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
def __removeAxesFromDesignSpace(path):
|
def test_documentLib(tmpdir):
|
||||||
# only for testing, so we can make an invalid designspace file
|
# roundtrip test of the document lib with some nested data
|
||||||
# without making the designSpaceDocument also support it.
|
tmpdir = str(tmpdir)
|
||||||
f = open(path, 'r', encoding='utf-8')
|
testDocPath1 = os.path.join(tmpdir, "testDocumentLibTest.designspace")
|
||||||
d = f.read()
|
doc = DesignSpaceDocument()
|
||||||
f.close()
|
a1 = AxisDescriptor()
|
||||||
start = d.find("<axes>")
|
a1.tag = "TAGA"
|
||||||
end = d.find("</axes>")+len("</axes>")
|
a1.name = "axisName_a"
|
||||||
n = d[0:start] + d[end:]
|
a1.minimum = 0
|
||||||
f = open(path, 'w', encoding='utf-8')
|
a1.maximum = 1000
|
||||||
f.write(n)
|
a1.default = 0
|
||||||
f.close()
|
doc.addAxis(a1)
|
||||||
|
dummyData = dict(a=123, b=u"äbc", c=[1,2,3], d={'a':123})
|
||||||
|
dummyKey = "org.fontTools.designspaceLib"
|
||||||
|
doc.lib = {dummyKey: dummyData}
|
||||||
|
doc.write(testDocPath1)
|
||||||
|
new = DesignSpaceDocument()
|
||||||
|
new.read(testDocPath1)
|
||||||
|
assert dummyKey in new.lib
|
||||||
|
assert new.lib[dummyKey] == dummyData
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user