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:
Cosimo Lupo 2018-05-14 10:29:48 +01:00 committed by Erik van Blokland
parent 4da8f43eeb
commit ceb41ec484
4 changed files with 234 additions and 184 deletions

View File

@ -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>

View File

@ -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"):

View File

@ -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>

View File

@ -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.