WIP: implement conditionSets in designspaceLib.RuleDescriptor (#1255)
* [designspaceLib] WIP: add conditionSets to RuleDescriptor... the BaseDocReader still needs to be updated to be able to read both legacy condition-without-conditionset and new-style conditionset elements. * test_rules: replace rule.conditions with conditionSets * Tests: update test.designspace to use conditionset; replace 'with' with 'byname' in sub element I don't like too much that now 'byname' precedes alphabetically 'name'. We need to see if ElementTree allows to sort attribute arbitrarily * Change versionnumber to 4.0 - would that be enough, or should there be seperate major / minor fields? If we bump the version up to numeral 4, I'd like the check for the with / byname attributes. We can read the old `byname` and only write the new `with`. Remove support for `initial` in axis attributes. * WIP - formatversion, with attribute, * WIP anything that is not version 4.0 is version 3.0 added some local paths to the test so I can see what it's generating. I can take this out later. * WIP read the conditionsets properly. This makes the tests fail no longer. * WIP disambiguate axis tags from axis names. Test axes are now called "axisName_x", tags are called "TAGX" * WIP forgot to push * WIP Raise `DesignSpaceDocumentError` when no maximum or minimum are given in a condition. Add test for such an incomplete rule. Remove path cruft. * WIP improved test of raised error. * WIP because no bot. * WIP more spaces around percents. * WIP - don't skip empty subs, just load whatever is there. * WIP do not permit empty axes in a location when evaluating a rule. Fix some tests. * WIP missed a str(tempdir), some rule tests. * WIP proposal for changes to the designspace spec to the `rule` and `sub` elements. * WIP edits. * WIP. Typo.
This commit is contained in:
parent
4da8f43eeb
commit
ceb41ec484
@ -84,17 +84,12 @@ Rules
|
|||||||
*****
|
*****
|
||||||
|
|
||||||
Rules describe designspace areas in which one glyph should be replaced by another.
|
Rules describe designspace areas in which one glyph should be replaced by another.
|
||||||
A rule has a name and a number of conditionsets. The rule also contains a list of glyphname
|
A rule has a name and a number of conditionsets. The rule also contains a list of
|
||||||
pairs: the glyphs that need to be substituted. For a rule to be triggered
|
glyphname pairs: the glyphs that need to be substituted. For a rule to be triggered
|
||||||
only one of the conditionsets needs to be true, ``OR``. Within a conditionset all
|
**only one** of the conditionsets needs to be true, ``OR``. Within a conditionset
|
||||||
conditions need to be true, ``AND``.
|
**all** conditions need to be true, ``AND``.
|
||||||
|
|
||||||
Variable fonts
|
The ``sub`` element contains a pair of glyphnames. The ``name`` attribute is the glyph that should be visible when the rule evaluates to **False**. The ``with`` attribute is the glyph that should be visible when the rule evaluates to **True**.
|
||||||
=======================
|
|
||||||
|
|
||||||
- In an variable font the substitution happens at run time: there are
|
|
||||||
no changes in the font, only in the sequence of glyphnames that is
|
|
||||||
rendered.
|
|
||||||
|
|
||||||
UFO instances
|
UFO instances
|
||||||
=============
|
=============
|
||||||
@ -547,7 +542,7 @@ Attributes
|
|||||||
to the root path of this document. The path can be at the same level
|
to the root path of this document. The path can be at the same level
|
||||||
as the document or lower.
|
as the document or lower.
|
||||||
- ``layer``: optional, string. The name of the layer in the source file.
|
- ``layer``: optional, string. The name of the layer in the source file.
|
||||||
If no layer attribute is given assume it is the foreground layer.
|
If no layer attribute is given assume the foreground layer should be used.
|
||||||
|
|
||||||
.. 31-lib-element:
|
.. 31-lib-element:
|
||||||
|
|
||||||
@ -856,8 +851,11 @@ Example
|
|||||||
- Defines a named rule.
|
- Defines a named rule.
|
||||||
- Each ``rule`` element contains one or more ``conditionset`` elements.
|
- Each ``rule`` element contains one or more ``conditionset`` elements.
|
||||||
- Only one ``conditionset`` needs to be true to trigger the rule.
|
- Only one ``conditionset`` needs to be true to trigger the rule.
|
||||||
- All conditions must be true to make the ``conditionset`` true.
|
- All conditions in a ``conditionset`` must be true to make the ``conditionset`` true.
|
||||||
- For backwards compatibility a ``rule`` can contain ``condition`` elements outside of a conditionset. These are then understood to be part of a single, implied, ``conditionset``.
|
- For backwards compatibility a ``rule`` can contain ``condition`` elements outside of a conditionset. These are then understood to be part of a single, implied, ``conditionset``. Note: these conditions should be written wrapped in a conditionset.
|
||||||
|
- A rule element needs to contain one or more ``sub`` elements in order to be compiled to a variable font.
|
||||||
|
- Rules without sub elements should be ignored when compiling a font.
|
||||||
|
- For authoring tools it might be necessary to save designspace files without ``sub`` elements just because the work is incomplete.
|
||||||
|
|
||||||
.. attributes-11:
|
.. attributes-11:
|
||||||
|
|
||||||
@ -865,7 +863,8 @@ Attributes
|
|||||||
----------
|
----------
|
||||||
|
|
||||||
- ``name``: optional, string. A unique name that can be used to
|
- ``name``: optional, string. A unique name that can be used to
|
||||||
identify this rule if it needs to be referenced elsewhere.
|
identify this rule if it needs to be referenced elsewhere. The name
|
||||||
|
is not important for compiling variable fonts.
|
||||||
|
|
||||||
5.1.1 conditionset element
|
5.1.1 conditionset element
|
||||||
=======================
|
=======================
|
||||||
@ -879,7 +878,7 @@ Attributes
|
|||||||
=======================
|
=======================
|
||||||
|
|
||||||
- Child element of ``conditionset``
|
- Child element of ``conditionset``
|
||||||
- Between the ``minimum`` and ``maximum`` this rule is ``true``.
|
- Between the ``minimum`` and ``maximum`` this rule is ``True``.
|
||||||
- If ``minimum`` is not available, assume it is ``axis.minimum``.
|
- If ``minimum`` is not available, assume it is ``axis.minimum``.
|
||||||
- If ``maximum`` is not available, assume it is ``axis.maximum``.
|
- If ``maximum`` is not available, assume it is ``axis.maximum``.
|
||||||
- The condition must contain at least a minimum or maximum or both.
|
- The condition must contain at least a minimum or maximum or both.
|
||||||
@ -900,9 +899,7 @@ Attributes
|
|||||||
=================
|
=================
|
||||||
|
|
||||||
- Child element of ``rule``.
|
- Child element of ``rule``.
|
||||||
- Defines which glyphs to replace when the rule is true.
|
- Defines which glyph to replace when the rule evaluates to **True**.
|
||||||
- This element is optional. It may be useful for editors to know which
|
|
||||||
glyphs can be used to preview the axis.
|
|
||||||
|
|
||||||
.. attributes-13:
|
.. attributes-13:
|
||||||
|
|
||||||
@ -911,7 +908,7 @@ Attributes
|
|||||||
|
|
||||||
- ``name``: string, required. The name of the glyph this rule looks
|
- ``name``: string, required. The name of the glyph this rule looks
|
||||||
for.
|
for.
|
||||||
- ``byname``: string, required. The name of the glyph it is replaced
|
- ``with``: string, required. The name of the glyph it is replaced
|
||||||
with.
|
with.
|
||||||
|
|
||||||
.. example-7:
|
.. example-7:
|
||||||
@ -928,7 +925,7 @@ contained in a conditionset.
|
|||||||
<rule name="named.rule.1">
|
<rule name="named.rule.1">
|
||||||
<condition minimum="250" maximum="750" name="weight" />
|
<condition minimum="250" maximum="750" name="weight" />
|
||||||
<condition minimum="50" maximum="100" name="width" />
|
<condition minimum="50" maximum="100" name="width" />
|
||||||
<sub name="dollar" byname="dollar.alt"/>
|
<sub name="dollar" with="dollar.alt"/>
|
||||||
</rule>
|
</rule>
|
||||||
</rules>
|
</rules>
|
||||||
|
|
||||||
@ -946,7 +943,7 @@ Example with ``conditionsets``. All conditions in a conditionset must be true.
|
|||||||
<condition ... />
|
<condition ... />
|
||||||
<condition ... />
|
<condition ... />
|
||||||
</conditionset>
|
</conditionset>
|
||||||
<sub name="dollar" byname="dollar.alt"/>
|
<sub name="dollar" with="dollar.alt"/>
|
||||||
</rule>
|
</rule>
|
||||||
</rules>
|
</rules>
|
||||||
|
|
||||||
|
@ -160,45 +160,49 @@ class SourceDescriptor(SimpleDescriptor):
|
|||||||
class RuleDescriptor(SimpleDescriptor):
|
class RuleDescriptor(SimpleDescriptor):
|
||||||
"""<!-- optional: list of substitution rules -->
|
"""<!-- optional: list of substitution rules -->
|
||||||
<rules>
|
<rules>
|
||||||
<rule name="vertical.bars" enabled="true">
|
<rule name="vertical.bars">
|
||||||
<sub name="cent" byname="cent.alt"/>
|
<conditionset>
|
||||||
<sub name="dollar" byname="dollar.alt"/>
|
<condition minimum="250.000000" maximum="750.000000" name="weight"/>
|
||||||
<condition tag="wght" minimum ="250.000000" maximum ="750.000000"/>
|
<condition minimum="100" name="width"/>
|
||||||
<condition tag="wdth" minimum ="100"/>
|
<condition minimum="10" maximum="40" name="optical"/>
|
||||||
<condition tag="opsz" minimum="10" maximum="40"/>
|
</conditionset>
|
||||||
|
<sub name="cent" with="cent.alt"/>
|
||||||
|
<sub name="dollar" with="dollar.alt"/>
|
||||||
</rule>
|
</rule>
|
||||||
</rules>
|
</rules>
|
||||||
|
|
||||||
Discussion:
|
|
||||||
use axis names rather than tags - then we can evaluate the rule without having to look up the axes.
|
|
||||||
remove the subs from the rule.
|
|
||||||
remove 'enabled' attr form rule
|
|
||||||
"""
|
"""
|
||||||
_attrs = ['name', 'conditions', 'subs'] # what do we need here
|
_attrs = ['name', 'conditionSets', 'subs'] # what do we need here
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.name = None
|
self.name = None
|
||||||
self.conditions = [] # list of dict(tag='aaaa', minimum=0, maximum=1000)
|
self.conditionSets = [] # list of list of dict(name='aaaa', minimum=0, maximum=1000)
|
||||||
self.subs = [] # list of substitutions stored as tuples of glyphnames ("a", "a.alt")
|
self.subs = [] # list of substitutions stored as tuples of glyphnames ("a", "a.alt")
|
||||||
|
|
||||||
|
|
||||||
def evaluateRule(rule, location):
|
def evaluateRule(rule, location):
|
||||||
""" Test if rule is True at location.maximum
|
""" Return True if any of the rule's conditionsets matches the
|
||||||
|
given location.
|
||||||
|
"""
|
||||||
|
return any(evaluateConditions(c, location) for c in rule.conditionSets)
|
||||||
|
|
||||||
|
|
||||||
|
def evaluateConditions(conditions, location):
|
||||||
|
""" Return True if all the conditions matches the given location.
|
||||||
If a condition has no minimum, check for < maximum.
|
If a condition has no minimum, check for < maximum.
|
||||||
If a condition has no maximum, check for > minimum.
|
If a condition has no maximum, check for > minimum.
|
||||||
"""
|
"""
|
||||||
for cd in rule.conditions:
|
for cd in conditions:
|
||||||
if not cd['name'] in location:
|
value = location[cd['name']]
|
||||||
continue
|
|
||||||
if cd.get('minimum') is None:
|
if cd.get('minimum') is None:
|
||||||
if not location[cd['name']] <= cd['maximum']:
|
if value > cd['maximum']:
|
||||||
return False
|
return False
|
||||||
elif cd.get('maximum') is None:
|
elif cd.get('maximum') is None:
|
||||||
if not cd['minimum'] <= location[cd['name']]:
|
if cd['minimum'] > value:
|
||||||
return False
|
return False
|
||||||
else:
|
elif not cd['minimum'] <= value <= cd['maximum']:
|
||||||
if not cd['minimum'] <= location[cd['name']] <= cd['maximum']:
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def processRules(rules, location, glyphNames):
|
def processRules(rules, location, glyphNames):
|
||||||
""" Apply these rules at this location to these glyphnames.minimum
|
""" Apply these rules at this location to these glyphnames.minimum
|
||||||
- rule order matters
|
- rule order matters
|
||||||
@ -357,13 +361,9 @@ class BaseDocWriter(object):
|
|||||||
def __init__(self, documentPath, documentObject):
|
def __init__(self, documentPath, documentObject):
|
||||||
self.path = documentPath
|
self.path = documentPath
|
||||||
self.documentObject = documentObject
|
self.documentObject = documentObject
|
||||||
self.toolVersion = 3
|
self.documentVersion = "4.0"
|
||||||
self.root = ET.Element("designspace")
|
self.root = ET.Element("designspace")
|
||||||
self.root.attrib['format'] = "%d" % self.toolVersion
|
self.root.attrib['format'] = self.documentVersion
|
||||||
#self.root.append(ET.Element("axes"))
|
|
||||||
#self.root.append(ET.Element("rules"))
|
|
||||||
#self.root.append(ET.Element("sources"))
|
|
||||||
#self.root.append(ET.Element("instances"))
|
|
||||||
self.axes = []
|
self.axes = []
|
||||||
self.rules = []
|
self.rules = []
|
||||||
|
|
||||||
@ -434,8 +434,11 @@ class BaseDocWriter(object):
|
|||||||
# 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:
|
||||||
ruleElement.attrib['name'] = ruleObject.name
|
ruleElement.attrib['name'] = ruleObject.name
|
||||||
for cond in ruleObject.conditions:
|
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:
|
if cond.get('minimum') is None and cond.get('maximum') is None:
|
||||||
# neither is defined, don't add this condition
|
# neither is defined, don't add this condition
|
||||||
continue
|
continue
|
||||||
@ -445,15 +448,18 @@ class BaseDocWriter(object):
|
|||||||
conditionElement.attrib['minimum'] = self.intOrFloat(cond.get('minimum'))
|
conditionElement.attrib['minimum'] = self.intOrFloat(cond.get('minimum'))
|
||||||
if cond.get('maximum') is not None:
|
if cond.get('maximum') is not None:
|
||||||
conditionElement.attrib['maximum'] = self.intOrFloat(cond.get('maximum'))
|
conditionElement.attrib['maximum'] = self.intOrFloat(cond.get('maximum'))
|
||||||
ruleElement.append(conditionElement)
|
conditionsetElement.append(conditionElement)
|
||||||
|
if len(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:
|
||||||
# skip empty subs
|
|
||||||
if sub[0] == '' and sub[1] == '':
|
|
||||||
continue
|
|
||||||
subElement = ET.Element('sub')
|
subElement = ET.Element('sub')
|
||||||
subElement.attrib['name'] = sub[0]
|
subElement.attrib['name'] = sub[0]
|
||||||
subElement.attrib['with'] = sub[1]
|
subElement.attrib['with'] = sub[1]
|
||||||
ruleElement.append(subElement)
|
ruleElement.append(subElement)
|
||||||
|
if len(ruleElement):
|
||||||
self.root.findall('.rules')[0].append(ruleElement)
|
self.root.findall('.rules')[0].append(ruleElement)
|
||||||
|
|
||||||
def _addAxis(self, axisObject):
|
def _addAxis(self, axisObject):
|
||||||
@ -648,10 +654,9 @@ class BaseDocReader(object):
|
|||||||
def __init__(self, documentPath, documentObject):
|
def __init__(self, documentPath, documentObject):
|
||||||
self.path = documentPath
|
self.path = documentPath
|
||||||
self.documentObject = documentObject
|
self.documentObject = documentObject
|
||||||
self.documentObject.formatVersion = 0
|
|
||||||
tree = ET.parse(self.path)
|
tree = ET.parse(self.path)
|
||||||
self.root = tree.getroot()
|
self.root = tree.getroot()
|
||||||
self.documentObject.formatVersion = int(self.root.attrib.get("format", 0))
|
self.documentObject.formatVersion = self.root.attrib.get("format", "3.0")
|
||||||
self.axes = []
|
self.axes = []
|
||||||
self.rules = []
|
self.rules = []
|
||||||
self.sources = []
|
self.sources = []
|
||||||
@ -684,7 +689,9 @@ class BaseDocReader(object):
|
|||||||
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")
|
||||||
for conditionElement in ruleElement.findall('.condition'):
|
for conditionSetElement in ruleElement.findall('.conditionset'):
|
||||||
|
cds = []
|
||||||
|
for conditionElement in conditionSetElement.findall('.condition'):
|
||||||
cd = {}
|
cd = {}
|
||||||
cdMin = conditionElement.attrib.get("minimum")
|
cdMin = conditionElement.attrib.get("minimum")
|
||||||
if cdMin is not None:
|
if cdMin is not None:
|
||||||
@ -699,7 +706,15 @@ 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")
|
||||||
ruleObject.conditions.append(cd)
|
# test for things
|
||||||
|
if cd.get('minimum') is None and cd.get('maximum') is None:
|
||||||
|
if ruleObject.name is not None:
|
||||||
|
n = ruleObject.name
|
||||||
|
else:
|
||||||
|
n = "%d" % len(rules)
|
||||||
|
raise DesignSpaceDocumentError("No minimum or maximum defined in rule \"%s\"." % n)
|
||||||
|
cds.append(cd)
|
||||||
|
ruleObject.conditionSets.append(cds)
|
||||||
for subElement in ruleElement.findall('.sub'):
|
for subElement in ruleElement.findall('.sub'):
|
||||||
a = subElement.attrib['name']
|
a = subElement.attrib['name']
|
||||||
b = subElement.attrib['with']
|
b = subElement.attrib['with']
|
||||||
@ -721,14 +736,6 @@ class BaseDocReader(object):
|
|||||||
axisObject.maximum = float(axisElement.attrib.get("maximum"))
|
axisObject.maximum = float(axisElement.attrib.get("maximum"))
|
||||||
if axisElement.attrib.get('hidden', False):
|
if axisElement.attrib.get('hidden', False):
|
||||||
axisObject.hidden = True
|
axisObject.hidden = True
|
||||||
# we need to check if there is an attribute named "initial"
|
|
||||||
if axisElement.attrib.get("default") is None:
|
|
||||||
if axisElement.attrib.get("initial") is not None:
|
|
||||||
# stop doing this,
|
|
||||||
axisObject.default = float(axisElement.attrib.get("initial"))
|
|
||||||
else:
|
|
||||||
axisObject.default = axisObject.minimum
|
|
||||||
else:
|
|
||||||
axisObject.default = float(axisElement.attrib.get("default"))
|
axisObject.default = float(axisElement.attrib.get("default"))
|
||||||
axisObject.tag = axisElement.attrib.get("tag")
|
axisObject.tag = axisElement.attrib.get("tag")
|
||||||
for mapElement in axisElement.findall('map'):
|
for mapElement in axisElement.findall('map'):
|
||||||
@ -1416,8 +1423,10 @@ class DesignSpaceDocument(object):
|
|||||||
axis.default = default
|
axis.default = default
|
||||||
# now the rules
|
# now the rules
|
||||||
for rule in self.rules:
|
for rule in self.rules:
|
||||||
|
newConditionSets = []
|
||||||
|
for conditions in rule.conditionSets:
|
||||||
newConditions = []
|
newConditions = []
|
||||||
for cond in rule.conditions:
|
for cond in conditions:
|
||||||
if cond.get('minimum') is not None:
|
if cond.get('minimum') is not None:
|
||||||
minimum = self.normalizeLocation({cond['name']:cond['minimum']}).get(cond['name'])
|
minimum = self.normalizeLocation({cond['name']:cond['minimum']}).get(cond['name'])
|
||||||
else:
|
else:
|
||||||
@ -1427,7 +1436,8 @@ class DesignSpaceDocument(object):
|
|||||||
else:
|
else:
|
||||||
maximum = None
|
maximum = None
|
||||||
newConditions.append(dict(name=cond['name'], minimum=minimum, maximum=maximum))
|
newConditions.append(dict(name=cond['name'], minimum=minimum, maximum=maximum))
|
||||||
rule.conditions = newConditions
|
newConditionSets.append(newConditions)
|
||||||
|
rule.conditionSets = newConditionSets
|
||||||
|
|
||||||
|
|
||||||
def rulesToFeature(doc, whiteSpace="\t", newLine="\n"):
|
def rulesToFeature(doc, whiteSpace="\t", newLine="\n"):
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<designspace format="3">
|
<designspace format="4.0">
|
||||||
<axes>
|
<axes>
|
||||||
<axis default="0" maximum="1000" minimum="0" name="weight" tag="wght">
|
<axis default="0" maximum="1000" minimum="0" name="weight" tag="wght">
|
||||||
<labelname xml:lang="en">Wéíght</labelname>
|
<labelname xml:lang="en">Wéíght</labelname>
|
||||||
@ -14,8 +14,10 @@
|
|||||||
</axes>
|
</axes>
|
||||||
<rules>
|
<rules>
|
||||||
<rule name="named.rule.1">
|
<rule name="named.rule.1">
|
||||||
<condition maximum="1" minimum="0" name="aaaa" />
|
<conditionset>
|
||||||
<condition maximum="3" minimum="2" name="bbbb" />
|
<condition maximum="1" minimum="0" name="axisName_a" />
|
||||||
|
<condition maximum="3" minimum="2" name="axisName_b" />
|
||||||
|
</conditionset>
|
||||||
<sub name="a" with="a.alt" />
|
<sub name="a" with="a.alt" />
|
||||||
</rule>
|
</rule>
|
||||||
</rules>
|
</rules>
|
||||||
|
@ -9,7 +9,7 @@ import pytest
|
|||||||
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)
|
InstanceDescriptor, evaluateRule, processRules, posix, DesignSpaceDocumentError)
|
||||||
|
|
||||||
|
|
||||||
def assert_equals_test_file(path, test_filename):
|
def assert_equals_test_file(path, test_filename):
|
||||||
@ -149,8 +149,10 @@ def test_fill_document(tmpdir):
|
|||||||
# write some rules
|
# write some rules
|
||||||
r1 = RuleDescriptor()
|
r1 = RuleDescriptor()
|
||||||
r1.name = "named.rule.1"
|
r1.name = "named.rule.1"
|
||||||
r1.conditions.append(dict(name='aaaa', minimum=0, maximum=1))
|
r1.conditionSets.append([
|
||||||
r1.conditions.append(dict(name='bbbb', minimum=2, maximum=3))
|
dict(name='axisName_a', minimum=0, maximum=1),
|
||||||
|
dict(name='axisName_b', minimum=2, maximum=3)
|
||||||
|
])
|
||||||
r1.subs.append(("a", "a.alt"))
|
r1.subs.append(("a", "a.alt"))
|
||||||
doc.addRule(r1)
|
doc.addRule(r1)
|
||||||
# write the document
|
# write the document
|
||||||
@ -374,8 +376,10 @@ def test_localisedNames(tmpdir):
|
|||||||
# write some rules
|
# write some rules
|
||||||
r1 = RuleDescriptor()
|
r1 = RuleDescriptor()
|
||||||
r1.name = "named.rule.1"
|
r1.name = "named.rule.1"
|
||||||
r1.conditions.append(dict(name='aaaa', minimum=0, maximum=1))
|
r1.conditionSets.append([
|
||||||
r1.conditions.append(dict(name='bbbb', minimum=2, maximum=3))
|
dict(name='weight', minimum=200, maximum=500),
|
||||||
|
dict(name='width', minimum=0, maximum=150)
|
||||||
|
])
|
||||||
r1.subs.append(("a", "a.alt"))
|
r1.subs.append(("a", "a.alt"))
|
||||||
doc.addRule(r1)
|
doc.addRule(r1)
|
||||||
# write the document
|
# write the document
|
||||||
@ -458,7 +462,6 @@ def test_handleNoAxes(tmpdir):
|
|||||||
verify.read(testDocPath)
|
verify.read(testDocPath)
|
||||||
verify.write(testDocPath2)
|
verify.write(testDocPath2)
|
||||||
|
|
||||||
|
|
||||||
def test_pathNameResolve(tmpdir):
|
def test_pathNameResolve(tmpdir):
|
||||||
tmpdir = str(tmpdir)
|
tmpdir = str(tmpdir)
|
||||||
# test how descriptor.path and descriptor.filename are resolved
|
# test how descriptor.path and descriptor.filename are resolved
|
||||||
@ -575,26 +578,26 @@ def test_normalise():
|
|||||||
a1.minimum = -1000
|
a1.minimum = -1000
|
||||||
a1.maximum = 1000
|
a1.maximum = 1000
|
||||||
a1.default = 0
|
a1.default = 0
|
||||||
a1.name = "aaa"
|
a1.name = "axisName_a"
|
||||||
a1.tag = "aaaa"
|
a1.tag = "TAGA"
|
||||||
doc.addAxis(a1)
|
doc.addAxis(a1)
|
||||||
|
|
||||||
assert doc.normalizeLocation(dict(aaa=0)) == {'aaa': 0.0}
|
assert doc.normalizeLocation(dict(axisName_a=0)) == {'axisName_a': 0.0}
|
||||||
assert doc.normalizeLocation(dict(aaa=1000)) == {'aaa': 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(aaa=1001)) == {'aaa': 1.0}
|
assert doc.normalizeLocation(dict(axisName_a=1001)) == {'axisName_a': 1.0}
|
||||||
assert doc.normalizeLocation(dict(aaa=500)) == {'aaa': 0.5}
|
assert doc.normalizeLocation(dict(axisName_a=500)) == {'axisName_a': 0.5}
|
||||||
assert doc.normalizeLocation(dict(aaa=-1000)) == {'aaa': -1.0}
|
assert doc.normalizeLocation(dict(axisName_a=-1000)) == {'axisName_a': -1.0}
|
||||||
assert doc.normalizeLocation(dict(aaa=-1001)) == {'aaa': -1.0}
|
assert doc.normalizeLocation(dict(axisName_a=-1001)) == {'axisName_a': -1.0}
|
||||||
# anisotropic coordinates normalise to isotropic
|
# anisotropic coordinates normalise to isotropic
|
||||||
assert doc.normalizeLocation(dict(aaa=(1000, -1000))) == {'aaa': 1.0}
|
assert doc.normalizeLocation(dict(axisName_a=(1000, -1000))) == {'axisName_a': 1.0}
|
||||||
doc.normalize()
|
doc.normalize()
|
||||||
r = []
|
r = []
|
||||||
for axis in doc.axes:
|
for axis in doc.axes:
|
||||||
r.append((axis.name, axis.minimum, axis.default, axis.maximum))
|
r.append((axis.name, axis.minimum, axis.default, axis.maximum))
|
||||||
r.sort()
|
r.sort()
|
||||||
assert r == [('aaa', -1.0, 0.0, 1.0)]
|
assert r == [('axisName_a', -1.0, 0.0, 1.0)]
|
||||||
|
|
||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
# write some axes
|
# write some axes
|
||||||
@ -602,24 +605,24 @@ def test_normalise():
|
|||||||
a2.minimum = 100
|
a2.minimum = 100
|
||||||
a2.maximum = 1000
|
a2.maximum = 1000
|
||||||
a2.default = 100
|
a2.default = 100
|
||||||
a2.name = "bbb"
|
a2.name = "axisName_b"
|
||||||
doc.addAxis(a2)
|
doc.addAxis(a2)
|
||||||
assert doc.normalizeLocation(dict(bbb=0)) == {'bbb': 0.0}
|
assert doc.normalizeLocation(dict(axisName_b=0)) == {'axisName_b': 0.0}
|
||||||
assert doc.normalizeLocation(dict(bbb=1000)) == {'bbb': 1.0}
|
assert doc.normalizeLocation(dict(axisName_b=1000)) == {'axisName_b': 1.0}
|
||||||
# clipping beyond max values:
|
# clipping beyond max values:
|
||||||
assert doc.normalizeLocation(dict(bbb=1001)) == {'bbb': 1.0}
|
assert doc.normalizeLocation(dict(axisName_b=1001)) == {'axisName_b': 1.0}
|
||||||
assert doc.normalizeLocation(dict(bbb=500)) == {'bbb': 0.4444444444444444}
|
assert doc.normalizeLocation(dict(axisName_b=500)) == {'axisName_b': 0.4444444444444444}
|
||||||
assert doc.normalizeLocation(dict(bbb=-1000)) == {'bbb': 0.0}
|
assert doc.normalizeLocation(dict(axisName_b=-1000)) == {'axisName_b': 0.0}
|
||||||
assert doc.normalizeLocation(dict(bbb=-1001)) == {'bbb': 0.0}
|
assert doc.normalizeLocation(dict(axisName_b=-1001)) == {'axisName_b': 0.0}
|
||||||
# anisotropic coordinates normalise to isotropic
|
# anisotropic coordinates normalise to isotropic
|
||||||
assert doc.normalizeLocation(dict(bbb=(1000,-1000))) == {'bbb': 1.0}
|
assert doc.normalizeLocation(dict(axisName_b=(1000,-1000))) == {'axisName_b': 1.0}
|
||||||
assert doc.normalizeLocation(dict(bbb=1001)) == {'bbb': 1.0}
|
assert doc.normalizeLocation(dict(axisName_b=1001)) == {'axisName_b': 1.0}
|
||||||
doc.normalize()
|
doc.normalize()
|
||||||
r = []
|
r = []
|
||||||
for axis in doc.axes:
|
for axis in doc.axes:
|
||||||
r.append((axis.name, axis.minimum, axis.default, axis.maximum))
|
r.append((axis.name, axis.minimum, axis.default, axis.maximum))
|
||||||
r.sort()
|
r.sort()
|
||||||
assert r == [('bbb', 0.0, 0.0, 1.0)]
|
assert r == [('axisName_b', 0.0, 0.0, 1.0)]
|
||||||
|
|
||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
# write some axes
|
# write some axes
|
||||||
@ -687,15 +690,15 @@ def test_rules(tmpdir):
|
|||||||
doc = DesignSpaceDocument()
|
doc = DesignSpaceDocument()
|
||||||
# write some axes
|
# write some axes
|
||||||
a1 = AxisDescriptor()
|
a1 = AxisDescriptor()
|
||||||
a1.tag = "taga"
|
a1.tag = "TAGA"
|
||||||
a1.name = "aaaa"
|
a1.name = "axisName_a"
|
||||||
a1.minimum = 0
|
a1.minimum = 0
|
||||||
a1.maximum = 1000
|
a1.maximum = 1000
|
||||||
a1.default = 0
|
a1.default = 0
|
||||||
doc.addAxis(a1)
|
doc.addAxis(a1)
|
||||||
a2 = AxisDescriptor()
|
a2 = AxisDescriptor()
|
||||||
a2.tag = "tagb"
|
a2.tag = "TAGB"
|
||||||
a2.name = "bbbb"
|
a2.name = "axisName_b"
|
||||||
a2.minimum = 0
|
a2.minimum = 0
|
||||||
a2.maximum = 3000
|
a2.maximum = 3000
|
||||||
a2.default = 0
|
a2.default = 0
|
||||||
@ -703,80 +706,88 @@ def test_rules(tmpdir):
|
|||||||
|
|
||||||
r1 = RuleDescriptor()
|
r1 = RuleDescriptor()
|
||||||
r1.name = "named.rule.1"
|
r1.name = "named.rule.1"
|
||||||
r1.conditions.append(dict(name='aaaa', minimum=0, maximum=1000))
|
r1.conditionSets.append([
|
||||||
r1.conditions.append(dict(name='bbbb', minimum=0, maximum=3000))
|
dict(name='axisName_a', minimum=0, maximum=1000),
|
||||||
|
dict(name='axisName_b', minimum=0, maximum=3000)
|
||||||
|
])
|
||||||
r1.subs.append(("a", "a.alt"))
|
r1.subs.append(("a", "a.alt"))
|
||||||
|
|
||||||
# rule with minium and maximum
|
# rule with minium and maximum
|
||||||
doc.addRule(r1)
|
doc.addRule(r1)
|
||||||
assert len(doc.rules) == 1
|
assert len(doc.rules) == 1
|
||||||
assert len(doc.rules[0].conditions) == 2
|
assert len(doc.rules[0].conditionSets) == 1
|
||||||
assert evaluateRule(r1, dict(aaaa = 500, bbbb = 0)) == True
|
assert len(doc.rules[0].conditionSets[0]) == 2
|
||||||
assert evaluateRule(r1, dict(aaaa = 0, bbbb = 0)) == True
|
assert evaluateRule(r1, dict(axisName_a = 500, axisName_b = 0)) == True
|
||||||
assert evaluateRule(r1, dict(aaaa = 1000, bbbb = 0)) == True
|
assert evaluateRule(r1, dict(axisName_a = 0, axisName_b = 0)) == True
|
||||||
assert evaluateRule(r1, dict(aaaa = 1000, bbbb = -100)) == False
|
assert evaluateRule(r1, dict(axisName_a = 1000, axisName_b = 0)) == True
|
||||||
assert evaluateRule(r1, dict(aaaa = 1000.0001, bbbb = 0)) == False
|
assert evaluateRule(r1, dict(axisName_a = 1000, axisName_b = -100)) == False
|
||||||
assert evaluateRule(r1, dict(aaaa = -0.0001, bbbb = 0)) == False
|
assert evaluateRule(r1, dict(axisName_a = 1000.0001, axisName_b = 0)) == False
|
||||||
assert evaluateRule(r1, dict(aaaa = -100, bbbb = 0)) == False
|
assert evaluateRule(r1, dict(axisName_a = -0.0001, axisName_b = 0)) == False
|
||||||
assert processRules([r1], dict(aaaa = 500), ["a", "b", "c"]) == ['a.alt', 'b', 'c']
|
assert evaluateRule(r1, dict(axisName_a = -100, axisName_b = 0)) == False
|
||||||
assert processRules([r1], dict(aaaa = 500), ["a.alt", "b", "c"]) == ['a.alt', 'b', 'c']
|
assert processRules([r1], dict(axisName_a = 500, axisName_b = 0), ["a", "b", "c"]) == ['a.alt', 'b', 'c']
|
||||||
assert processRules([r1], dict(aaaa = 2000), ["a", "b", "c"]) == ['a', 'b', 'c']
|
assert processRules([r1], dict(axisName_a = 500, axisName_b = 0), ["a.alt", "b", "c"]) == ['a.alt', 'b', 'c']
|
||||||
|
assert processRules([r1], dict(axisName_a = 2000, axisName_b = 0), ["a", "b", "c"]) == ['a', 'b', 'c']
|
||||||
|
|
||||||
# rule with only a maximum
|
# rule with only a maximum
|
||||||
r2 = RuleDescriptor()
|
r2 = RuleDescriptor()
|
||||||
r2.name = "named.rule.2"
|
r2.name = "named.rule.2"
|
||||||
r2.conditions.append(dict(name='aaaa', maximum=500))
|
r2.conditionSets.append([dict(name='axisName_a', maximum=500)])
|
||||||
r2.subs.append(("b", "b.alt"))
|
r2.subs.append(("b", "b.alt"))
|
||||||
|
|
||||||
assert evaluateRule(r2, dict(aaaa = 0)) == True
|
assert evaluateRule(r2, dict(axisName_a = 0)) == True
|
||||||
assert evaluateRule(r2, dict(aaaa = -500)) == True
|
assert evaluateRule(r2, dict(axisName_a = -500)) == True
|
||||||
assert evaluateRule(r2, dict(aaaa = 1000)) == False
|
assert evaluateRule(r2, dict(axisName_a = 1000)) == False
|
||||||
|
|
||||||
# rule with only a minimum
|
# rule with only a minimum
|
||||||
r3 = RuleDescriptor()
|
r3 = RuleDescriptor()
|
||||||
r3.name = "named.rule.3"
|
r3.name = "named.rule.3"
|
||||||
r3.conditions.append(dict(name='aaaa', minimum=500))
|
r3.conditionSets.append([dict(name='axisName_a', minimum=500)])
|
||||||
r3.subs.append(("c", "c.alt"))
|
r3.subs.append(("c", "c.alt"))
|
||||||
|
|
||||||
assert evaluateRule(r3, dict(aaaa = 0)) == False
|
assert evaluateRule(r3, dict(axisName_a = 0)) == False
|
||||||
assert evaluateRule(r3, dict(aaaa = 1000)) == True
|
assert evaluateRule(r3, dict(axisName_a = 1000)) == True
|
||||||
assert evaluateRule(r3, dict(bbbb = 1000)) == True
|
assert evaluateRule(r3, dict(axisName_a = 1000)) == True
|
||||||
|
|
||||||
# rule with only a minimum, maximum in separate conditions
|
# rule with only a minimum, maximum in separate conditions
|
||||||
r4 = RuleDescriptor()
|
r4 = RuleDescriptor()
|
||||||
r4.name = "named.rule.4"
|
r4.name = "named.rule.4"
|
||||||
r4.conditions.append(dict(name='aaaa', minimum=500))
|
r4.conditionSets.append([
|
||||||
r4.conditions.append(dict(name='bbbb', maximum=500))
|
dict(name='axisName_a', minimum=500),
|
||||||
|
dict(name='axisName_b', maximum=500)
|
||||||
|
])
|
||||||
r4.subs.append(("c", "c.alt"))
|
r4.subs.append(("c", "c.alt"))
|
||||||
|
|
||||||
assert evaluateRule(r4, dict()) == True # is this what we expect though?
|
assert evaluateRule(r4, dict(axisName_a = 1000, axisName_b = 0)) == True
|
||||||
assert evaluateRule(r4, dict(aaaa = 1000, bbbb = 0)) == True
|
assert evaluateRule(r4, dict(axisName_a = 0, axisName_b = 0)) == False
|
||||||
assert evaluateRule(r4, dict(aaaa = 0, bbbb = 0)) == False
|
assert evaluateRule(r4, dict(axisName_a = 1000, axisName_b = 1000)) == False
|
||||||
assert evaluateRule(r4, dict(aaaa = 1000, bbbb = 1000)) == False
|
|
||||||
|
|
||||||
a1 = AxisDescriptor()
|
a1 = AxisDescriptor()
|
||||||
a1.minimum = 0
|
a1.minimum = 0
|
||||||
a1.maximum = 1000
|
a1.maximum = 1000
|
||||||
a1.default = 0
|
a1.default = 0
|
||||||
a1.name = "aaaa"
|
a1.name = "axisName_a"
|
||||||
a1.tag = "aaaa"
|
a1.tag = "TAGA"
|
||||||
b1 = AxisDescriptor()
|
b1 = AxisDescriptor()
|
||||||
b1.minimum = 2000
|
b1.minimum = 2000
|
||||||
b1.maximum = 3000
|
b1.maximum = 3000
|
||||||
b1.default = 2000
|
b1.default = 2000
|
||||||
b1.name = "bbbb"
|
b1.name = "axisName_b"
|
||||||
b1.tag = "bbbb"
|
b1.tag = "TAGB"
|
||||||
doc.addAxis(a1)
|
doc.addAxis(a1)
|
||||||
doc.addAxis(b1)
|
doc.addAxis(b1)
|
||||||
assert doc._prepAxesForBender() == {'aaaa': {'map': [], 'name': 'aaaa', 'default': 0, 'minimum': 0, 'maximum': 1000, 'tag': 'aaaa'}, 'bbbb': {'map': [], 'name': 'bbbb', 'default': 2000, 'minimum': 2000, 'maximum': 3000, 'tag': 'bbbb'}}
|
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'}}
|
||||||
|
|
||||||
assert doc.rules[0].conditions == [{'minimum': 0, 'maximum': 1000, 'name': 'aaaa'}, {'minimum': 0, 'maximum': 3000, 'name': 'bbbb'}]
|
assert doc.rules[0].conditionSets == [[
|
||||||
|
{'minimum': 0, 'maximum': 1000, 'name': 'axisName_a'},
|
||||||
|
{'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].conditions == [{'minimum': 0.0, 'maximum': 1.0, 'name': 'aaaa'}, {'minimum': 0.0, 'maximum': 1.0, 'name': 'bbbb'}]
|
assert doc.rules[0].conditionSets == [[
|
||||||
|
{'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_a'},
|
||||||
|
{'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_b'}]]
|
||||||
|
|
||||||
doc.write(testDocPath)
|
doc.write(testDocPath)
|
||||||
new = DesignSpaceDocument()
|
new = DesignSpaceDocument()
|
||||||
@ -787,6 +798,36 @@ def test_rules(tmpdir):
|
|||||||
new.write(testDocPath2)
|
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
|
||||||
|
# without making the designSpaceDocument also support it.
|
||||||
|
f = open(path, 'r', encoding='utf-8')
|
||||||
|
d = f.read()
|
||||||
|
f.close()
|
||||||
|
d = d.replace(' minimum="100"', '')
|
||||||
|
f = open(path, 'w', encoding='utf-8')
|
||||||
|
f.write(d)
|
||||||
|
f.close()
|
||||||
|
|
||||||
def __removeAxesFromDesignSpace(path):
|
def __removeAxesFromDesignSpace(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.
|
# without making the designSpaceDocument also support it.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user