Improve support for rules only a minimum or a maximum attribute.

More tests.
This commit is contained in:
Erik 2016-12-18 22:15:54 +01:00
parent 95cc8885d4
commit 3a693b37ec
2 changed files with 104 additions and 12 deletions

View File

@ -106,12 +106,21 @@ class RuleDescriptor(SimpleDescriptor):
self.subs = [] # list of substitutions stored as tuples of glyphnames ("a", "a.alt")
def evaluateRule(rule, location):
""" Test if rule is True at location """
""" Test if rule is True at location.maximum
If a condition has no minimum, check for < maximum.
If a condition has no maximum, check for > minimum.
"""
for cd in rule.conditions:
if not cd['name'] in location:
#print("skipping", cd['name'])
continue
#print(cd['minimum'] <= location[cd['name']] <= cd['maximum'])
if cd.get('minimum') is None:
if not location[cd['name']] <= cd['maximum']:
return False
elif cd.get('maximum') is None:
if not cd['minimum'] <= location[cd['name']]:
return False
else:
if not cd['minimum'] <= location[cd['name']] <= cd['maximum']:
return False
return True
@ -303,16 +312,25 @@ class BaseDocWriter(object):
return "%f" % num
def _addRule(self, ruleObject):
# if none of the conditions have minimum or maximum values, do not add the rule.
self.rules.append(ruleObject)
ruleElement = ET.Element('rule')
ruleElement.attrib['name'] = ruleObject.name
for cond in ruleObject.conditions:
if cond.get('minimum') is None and cond.get('maximum') is None:
# neither is defined, don't add this condition
continue
conditionElement = ET.Element('condition')
conditionElement.attrib['name'] = cond.get('name')
if cond.get('minimum') is not None:
conditionElement.attrib['minimum'] = self.intOrFloat(cond.get('minimum'))
if cond.get('maximum') is not None:
conditionElement.attrib['maximum'] = self.intOrFloat(cond.get('maximum'))
ruleElement.append(conditionElement)
for sub in ruleObject.subs:
# skip empty subs
if sub[0] == '' and sub[1] == '':
continue
subElement = ET.Element('sub')
subElement.attrib['name'] = sub[0]
subElement.attrib['with'] = sub[1]
@ -496,8 +514,18 @@ class BaseDocReader(object):
ruleObject.name = ruleElement.attrib.get("name")
for conditionElement in ruleElement.findall('.condition'):
cd = {}
cd['minimum'] = float(conditionElement.attrib.get("minimum"))
cd['maximum'] = float(conditionElement.attrib.get("maximum"))
cdMin = conditionElement.attrib.get("minimum")
if cdMin is not None:
cd['minimum'] = float(cdMin)
else:
# will allow these to be None, assume axis.minimum
cd['minimum'] = None
cdMax = conditionElement.attrib.get("maximum")
if cdMax is not None:
cd['maximum'] = float(cdMax)
else:
# will allow these to be None, assume axis.maximum
cd['maximum'] = None
cd['name'] = conditionElement.attrib.get("name")
ruleObject.conditions.append(cd)
for subElement in ruleElement.findall('.sub'):
@ -983,8 +1011,14 @@ class DesignSpaceDocument(object):
for rule in self.rules:
newConditions = []
for cond in rule.conditions:
if cond.get('minimum') is not None:
minimum = self.normalizeLocation({cond['name']:cond['minimum']}).get(cond['name'])
else:
minimum = None
if cond.get('maximum') is not None:
maximum = self.normalizeLocation({cond['name']:cond['maximum']}).get(cond['name'])
else:
maximum = None
newConditions.append(dict(name=cond['name'], minimum=minimum, maximum=maximum))
rule.conditions = newConditions
@ -1400,6 +1434,7 @@ if __name__ == "__main__":
>>> r1.conditions.append(dict(name='bbbb', 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].conditions) == 2
@ -1424,6 +1459,48 @@ if __name__ == "__main__":
>>> processRules([r1], dict(aaaa = 2000), ["a", "b", "c"])
['a', 'b', 'c']
>>> # rule with only a maximum
>>> r2 = RuleDescriptor()
>>> r2.name = "named.rule.2"
>>> r2.conditions.append(dict(name='aaaa', maximum=500))
>>> r2.subs.append(("b", "b.alt"))
>>>
>>> evaluateRule(r2, dict(aaaa = 0))
True
>>> evaluateRule(r2, dict(aaaa = -500))
True
>>> evaluateRule(r2, dict(aaaa = 1000))
False
>>> # rule with only a minimum
>>> r3 = RuleDescriptor()
>>> r3.name = "named.rule.3"
>>> r3.conditions.append(dict(name='aaaa', minimum=500))
>>> r3.subs.append(("c", "c.alt"))
>>>
>>> evaluateRule(r3, dict(aaaa = 0))
False
>>> evaluateRule(r3, dict(aaaa = 1000))
True
>>> evaluateRule(r3, dict(bbbb = 1000))
True
>>> # rule with only a minimum, maximum in separate conditions
>>> r4 = RuleDescriptor()
>>> r4.name = "named.rule.4"
>>> r4.conditions.append(dict(name='aaaa', minimum=500))
>>> r4.conditions.append(dict(name='bbbb', maximum=500))
>>> r4.subs.append(("c", "c.alt"))
>>>
>>> evaluateRule(r4, dict()) # is this what we expect though?
True
>>> evaluateRule(r4, dict(aaaa = 1000, bbbb = 0))
True
>>> evaluateRule(r4, dict(aaaa = 0, bbbb = 0))
False
>>> evaluateRule(r4, dict(aaaa = 1000, bbbb = 1000))
False
>>> a1 = AxisDescriptor()
>>> a1.minimum = 0
>>> a1.maximum = 1000

View File

@ -45,6 +45,18 @@ Some validation is done when reading.
* If no source has a `copyInfo` flag, mutatorMath will be used to select one. This source gets its `copyInfo` flag set. If you save the document this flag will be set.
* Use `doc.checkDefault()` to set the default font.
# Rules
**The `rule` element is experimental.** Some ideas behind how rules could work in designspaces come from Superpolator. Such rules can maybe be used to describe some of the conditional GSUB functionality of OpenType 1.8. The definition of a rule is not that complicated. A rule has a name, and it has a number of conditions. The rule also contains a list of glyphname pairs: the glyphs that need to be substituted.
### Variable font instances
* 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.
* The infrastructure to get this rule data in a variable font needs to be built.
### UFO instances
* When making instances as UFOs however, we need to swap the glyphs so that the original shape is still available. For instance, if a rule swaps `a` for `a.alt`, but a glyph that references `a` in a component would then show the new `a.alt`.
* But that can lead to unexpected results. So, if there are no rules for `adieresis` (assuming it references `a`) then that glyph **should not change appearance**. That means that when the rule swaps `a` and `a.alt` it also swaps all components that reference these glyphs so they keep their appearance.
* The swap function also needs to take care of swapping the names in kerning data.
# `SourceDescriptor` object
### Attributes
* `path`: string. Path to the source file. MutatorMath + Varlib.
@ -417,11 +429,14 @@ myDoc = DesignSpaceDocument(KeyedDocReader, KeyedDocWriter)
# 5.1.1 `condition` element
* Child element of `rule`
* Between the `minimum` and `maximum` this rule is `true`.
* If `minimum` is not available, assume it is `axis.minimum`.
* If `maximum` is not available, assume it is `axis.maximum`.
* One or the other or both need to be present.
### Attributes
* `name`: string, required. Must match one of the defined `axis` name attributes.
* `minimum`: number, required. The low value.
* `maximum`: number, required. The high value.
* `minimum`: number, required*. The low value.
* `maximum`: number, required*. The high value.
# 5.1.2 `sub` element
* Child element of `rule`.