Merge pull request #1110 from belluzj/merge-design-space-document
Merge designSpaceDocument
This commit is contained in:
commit
29deb7e6fb
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,3 +23,4 @@ htmlcov/
|
||||
|
||||
# OSX Finder
|
||||
.DS_Store
|
||||
|
||||
|
17
Doc/source/designspaceLib/index.rst
Normal file
17
Doc/source/designspaceLib/index.rst
Normal file
@ -0,0 +1,17 @@
|
||||
##############
|
||||
designspaceLib
|
||||
##############
|
||||
|
||||
MutatorMath started out with its own reader and writer for designspaces.
|
||||
Since then the use of designspace has broadened and it would be useful
|
||||
to have a reader and writer that are independent of a specific system.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
readme
|
||||
scripting
|
||||
|
||||
.. automodule:: fontTools.designspaceLib
|
||||
:members:
|
||||
:undoc-members:
|
953
Doc/source/designspaceLib/readme.rst
Normal file
953
Doc/source/designspaceLib/readme.rst
Normal file
@ -0,0 +1,953 @@
|
||||
DesignSpaceDocument
|
||||
===================
|
||||
|
||||
An object to read, write and edit interpolation systems for typefaces.
|
||||
|
||||
- the format was originally written for MutatorMath.
|
||||
- the format is now also used in fontTools.varlib.
|
||||
- Define sources, axes and instances.
|
||||
- Not all values might be required by all applications.
|
||||
|
||||
A couple of differences between things that use designspaces:
|
||||
|
||||
- Varlib does not support anisotropic interpolations.
|
||||
- MutatorMath and Superpolator will extrapolate over the boundaries of
|
||||
the axes. Varlib can not.
|
||||
- Varlib requires much less data to define an instance than
|
||||
MutatorMath.
|
||||
- The goals of Varlib and MutatorMath are different, so not all
|
||||
attributes are always needed.
|
||||
- Need to expand the description of FDK use of designspace files.
|
||||
|
||||
The DesignSpaceDocument object can read and write ``.designspace`` data.
|
||||
It imports the axes, sources and instances to very basic **descriptor**
|
||||
objects that store the data in attributes. Data is added to the document
|
||||
by creating such descriptor objects, filling them with data and then
|
||||
adding them to the document. This makes it easy to integrate this object
|
||||
in different contexts.
|
||||
|
||||
The **DesignSpaceDocument** object can be subclassed to work with
|
||||
different objects, as long as they have the same attributes.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from designSpaceDocument import DesignSpaceDocument
|
||||
doc = DesignSpaceDocument()
|
||||
doc.read("some/path/to/my.designspace")
|
||||
doc.axes
|
||||
doc.sources
|
||||
doc.instances
|
||||
|
||||
Validation
|
||||
==========
|
||||
|
||||
Some validation is done when reading.
|
||||
|
||||
Axes
|
||||
~~~~
|
||||
|
||||
- If the ``axes`` element is available in the document then all
|
||||
locations will check their dimensions against the defined axes. If a
|
||||
location uses an axis that is not defined it will be ignored.
|
||||
- If there are no ``axes`` in the document, locations will accept all
|
||||
axis names, so that we can..
|
||||
- Use ``doc.checkAxes()`` to reconstruct axes definitions based on the
|
||||
``source.location`` values. If you save the document the axes will be
|
||||
there.
|
||||
|
||||
Default font
|
||||
~~~~~~~~~~~~
|
||||
|
||||
- The source with the ``copyInfo`` flag indicates this is the default
|
||||
font.
|
||||
- In mutatorMath the default font is selected automatically. A warning
|
||||
is printed if the mutatorMath default selection differs from the one
|
||||
set by ``copyInfo``. But the ``copyInfo`` source will be used.
|
||||
- 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.
|
||||
|
||||
Localisation
|
||||
============
|
||||
|
||||
Some of the descriptors support localised names. The names are stored in
|
||||
dictionaries using the language code as key. That means that there are
|
||||
now two places to store names: the old attribute and the new localised
|
||||
dictionary, ``obj.stylename`` and ``obj.localisedStyleName['en']``.
|
||||
|
||||
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
|
||||
~~~~~~~~~~
|
||||
|
||||
- ``filename``: string. A relative path to the source file, **as it is
|
||||
in the document**. MutatorMath + Varlib.
|
||||
- ``path``: string. Absolute path to the source file, calculated from
|
||||
the document path and the string in the filename attr. MutatorMath +
|
||||
Varlib.
|
||||
- ``name``: string. Optional. Unique identifier name for this source,
|
||||
if there is one or more ``instance.glyph`` elements in the document.
|
||||
MutatorMath.
|
||||
- ``location``: dict. Axis values for this source. MutatorMath + Varlib
|
||||
- ``copyLib``: bool. Indicates if the contents of the font.lib need to
|
||||
be copied to the instances. MutatorMath.
|
||||
- ``copyInfo`` bool. Indicates if the non-interpolating font.info needs
|
||||
to be copied to the instances. Also indicates this source is expected
|
||||
to be the default font. MutatorMath + Varlib
|
||||
- ``copyGroups`` bool. Indicates if the groups need to be copied to the
|
||||
instances. MutatorMath.
|
||||
- ``copyFeatures`` bool. Indicates if the feature text needs to be
|
||||
copied to the instances. MutatorMath.
|
||||
- ``muteKerning``: bool. Indicates if the kerning data from this source
|
||||
needs to be muted (i.e. not be part of the calculations).
|
||||
MutatorMath.
|
||||
- ``muteInfo``: bool. Indicated if the interpolating font.info data for
|
||||
this source needs to be muted. MutatorMath.
|
||||
- ``mutedGlyphNames``: list. Glyphnames that need to be muted in the
|
||||
instances. MutatorMath.
|
||||
- ``familyName``: string. Family name of this source. Though this data
|
||||
can be extracted from the font, it can be efficient to have it right
|
||||
here. Varlib.
|
||||
- ``styleName``: string. Style name of this source. Though this data
|
||||
can be extracted from the font, it can be efficient to have it right
|
||||
here. Varlib.
|
||||
|
||||
.. code:: python
|
||||
|
||||
doc = DesignSpaceDocument()
|
||||
s1 = SourceDescriptor()
|
||||
s1.path = masterPath1
|
||||
s1.name = "master.ufo1"
|
||||
s1.copyLib = True
|
||||
s1.copyInfo = True
|
||||
s1.copyFeatures = True
|
||||
s1.location = dict(weight=0)
|
||||
s1.familyName = "MasterFamilyName"
|
||||
s1.styleName = "MasterStyleNameOne"
|
||||
s1.mutedGlyphNames.append("A")
|
||||
s1.mutedGlyphNames.append("Z")
|
||||
doc.addSource(s1)
|
||||
|
||||
InstanceDescriptor object
|
||||
-------------------------
|
||||
|
||||
.. attributes-1:
|
||||
|
||||
Attributes
|
||||
~~~~~~~~~~
|
||||
|
||||
- ``filename``: string. Relative path to the instance file, **as it is
|
||||
in the document**. The file may or may not exist. MutatorMath.
|
||||
- ``path``: string. Absolute path to the source file, calculated from
|
||||
the document path and the string in the filename attr. The file may
|
||||
or may not exist. MutatorMath.
|
||||
- ``name``: string. Unique identifier name of the instance, used to
|
||||
identify it if it needs to be referenced from elsewhere in the
|
||||
document.
|
||||
- ``location``: dict. Axis values for this source. MutatorMath +
|
||||
Varlib.
|
||||
- ``familyName``: string. Family name of this instance. MutatorMath +
|
||||
Varlib.
|
||||
- ``localisedFamilyName``: dict. A dictionary of localised family name
|
||||
strings, keyed by language code.
|
||||
- ``styleName``: string. Style name of this source. MutatorMath +
|
||||
Varlib.
|
||||
- ``localisedStyleName``: dict. A dictionary of localised stylename
|
||||
strings, keyed by language code.
|
||||
- ``postScriptFontName``: string. Postscript fontname for this
|
||||
instance. MutatorMath.
|
||||
- ``styleMapFamilyName``: string. StyleMap familyname for this
|
||||
instance. MutatorMath.
|
||||
- ``localisedStyleMapFamilyName``: A dictionary of localised style map
|
||||
familyname strings, keyed by language code.
|
||||
- ``localisedStyleMapStyleName``: A dictionary of localised style map
|
||||
stylename strings, keyed by language code.
|
||||
- ``styleMapStyleName``: string. StyleMap stylename for this instance.
|
||||
MutatorMath.
|
||||
- ``glyphs``: dict for special master definitions for glyphs. If glyphs
|
||||
need special masters (to record the results of executed rules for
|
||||
example). MutatorMath.
|
||||
- ``mutedGlyphNames``: list of glyphnames that should be suppressed in
|
||||
the generation of this instance.
|
||||
- ``kerning``: bool. Indicates if this instance needs its kerning
|
||||
calculated. MutatorMath.
|
||||
- ``info``: bool. Indicated if this instance needs the interpolating
|
||||
font.info calculated.
|
||||
|
||||
Methods
|
||||
~~~~~~~
|
||||
|
||||
These methods give easier access to the localised names.
|
||||
|
||||
- ``setStyleName(styleName, languageCode="en")``
|
||||
- ``getStyleName(languageCode="en")``
|
||||
- ``setFamilyName(familyName, languageCode="en")``
|
||||
- ``getFamilyName(self, languageCode="en")``
|
||||
- ``setStyleMapStyleName(styleMapStyleName, languageCode="en")``
|
||||
- ``getStyleMapStyleName(languageCode="en")``
|
||||
- ``setStyleMapFamilyName(styleMapFamilyName, languageCode="en")``
|
||||
- ``getStyleMapFamilyName(languageCode="en")``
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
.. code:: python
|
||||
|
||||
i2 = InstanceDescriptor()
|
||||
i2.path = instancePath2
|
||||
i2.familyName = "InstanceFamilyName"
|
||||
i2.styleName = "InstanceStyleName"
|
||||
i2.name = "instance.ufo2"
|
||||
# anisotropic location
|
||||
i2.location = dict(weight=500, width=(400,300))
|
||||
i2.postScriptFontName = "InstancePostscriptName"
|
||||
i2.styleMapFamilyName = "InstanceStyleMapFamilyName"
|
||||
i2.styleMapStyleName = "InstanceStyleMapStyleName"
|
||||
glyphMasters = [dict(font="master.ufo1", glyphName="BB", location=dict(width=20,weight=20)), dict(font="master.ufo2", glyphName="CC", location=dict(width=900,weight=900))]
|
||||
glyphData = dict(name="arrow", unicodeValue=1234)
|
||||
glyphData['masters'] = glyphMasters
|
||||
glyphData['note'] = "A note about this glyph"
|
||||
glyphData['instanceLocation'] = dict(width=100, weight=120)
|
||||
i2.glyphs['arrow'] = glyphData
|
||||
i2.glyphs['arrow2'] = dict(mute=False)
|
||||
doc.addInstance(i2)
|
||||
|
||||
AxisDescriptor object
|
||||
---------------------
|
||||
|
||||
- ``tag``: string. Four letter tag for this axis. Some might be
|
||||
registered at the `OpenType
|
||||
specification <https://www.microsoft.com/typography/otspec/fvar.htm#VAT>`__.
|
||||
Privately-defined axis tags must begin with an uppercase letter and
|
||||
use only uppercase letters or digits.
|
||||
- ``name``: string. Name of the axis as it is used in the location
|
||||
dicts. MutatorMath + Varlib.
|
||||
- ``labelNames``: dict. When defining a non-registered axis, it will be
|
||||
necessary to define user-facing readable names for the axis. Keyed by
|
||||
xml:lang code. Varlib.
|
||||
- ``minimum``: number. The minimum value for this axis. MutatorMath +
|
||||
Varlib.
|
||||
- ``maximum``: number. The maximum value for this axis. MutatorMath +
|
||||
Varlib.
|
||||
- ``default``: number. The default value for this axis, i.e. when a new
|
||||
location is created, this is the value this axis will get.
|
||||
MutatorMath + Varlib.
|
||||
- ``map``: list of input / output values that can describe a warp of
|
||||
user space to designspace coordinates. If no map values are present,
|
||||
it is assumed it is [(minimum, minimum), (maximum, maximum)]. Varlib.
|
||||
|
||||
.. code:: python
|
||||
|
||||
a1 = AxisDescriptor()
|
||||
a1.minimum = 1
|
||||
a1.maximum = 1000
|
||||
a1.default = 400
|
||||
a1.name = "weight"
|
||||
a1.tag = "wght"
|
||||
a1.labelNames[u'fa-IR'] = u"قطر"
|
||||
a1.labelNames[u'en'] = u"Wéíght"
|
||||
a1.map = [(1.0, 10.0), (400.0, 66.0), (1000.0, 990.0)]
|
||||
|
||||
RuleDescriptor object
|
||||
---------------------
|
||||
|
||||
- ``name``: string. Unique name for this rule. Will be used to
|
||||
reference this rule data.
|
||||
- ``conditions``: list of dicts with condition data.
|
||||
- Each condition specifies the axis name it is active on and the values
|
||||
between which the condition is true.
|
||||
|
||||
.. code:: python
|
||||
|
||||
r1 = RuleDescriptor()
|
||||
r1.name = "unique.rule.name"
|
||||
r1.conditions.append(dict(name="weight", minimum=-10, maximum=10))
|
||||
r1.conditions.append(dict(name="width", minimum=-10, maximum=10))
|
||||
|
||||
Subclassing descriptors
|
||||
=======================
|
||||
|
||||
The DesignSpaceDocument can take subclassed Reader and Writer objects.
|
||||
This allows you to work with your own descriptors. You could subclass
|
||||
the descriptors. But as long as they have the basic attributes the
|
||||
descriptor does not need to be a subclass.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class MyDocReader(BaseDocReader):
|
||||
ruleDescriptorClass = MyRuleDescriptor
|
||||
axisDescriptorClass = MyAxisDescriptor
|
||||
sourceDescriptorClass = MySourceDescriptor
|
||||
instanceDescriptorClass = MyInstanceDescriptor
|
||||
|
||||
class MyDocWriter(BaseDocWriter):
|
||||
ruleDescriptorClass = MyRuleDescriptor
|
||||
axisDescriptorClass = MyAxisDescriptor
|
||||
sourceDescriptorClass = MySourceDescriptor
|
||||
instanceDescriptorClass = MyInstanceDescriptor
|
||||
|
||||
myDoc = DesignSpaceDocument(KeyedDocReader, KeyedDocWriter)
|
||||
|
||||
Document xml structure
|
||||
======================
|
||||
|
||||
- The ``axes`` element contains one or more ``axis`` elements.
|
||||
- The ``sources`` element contains one or more ``source`` elements.
|
||||
- The ``instances`` element contains one or more ``instance`` elements.
|
||||
|
||||
.. code:: xml
|
||||
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<designspace format="3">
|
||||
<axes>
|
||||
<!-- define axes here -->
|
||||
<axis../>
|
||||
</axes>
|
||||
<sources>
|
||||
<!-- define masters here -->
|
||||
<source../>
|
||||
</sources>
|
||||
<instances>
|
||||
<!-- define instances here -->
|
||||
<instance../>
|
||||
</instances>
|
||||
</designspace>
|
||||
|
||||
.. 1-axis-element:
|
||||
|
||||
1. axis element
|
||||
===============
|
||||
|
||||
- Define a single axis
|
||||
- Child element of ``axes``
|
||||
|
||||
.. attributes-2:
|
||||
|
||||
Attributes
|
||||
~~~~~~~~~~
|
||||
|
||||
- ``name``: required, string. Name of the axis that is used in the
|
||||
location elements.
|
||||
- ``tag``: required, string, 4 letters. Some axis tags are registered
|
||||
in the OpenType Specification.
|
||||
- ``minimum``: required, number. The minimum value for this axis.
|
||||
- ``maximum``: required, number. The maximum value for this axis.
|
||||
- ``default``: required, number. The default value for this axis.
|
||||
- ``hidden``: optional, 0 or 1. Records whether this axis needs to be
|
||||
hidden in interfaces.
|
||||
|
||||
.. code:: xml
|
||||
|
||||
<axis name="weight" tag="wght" minimum="1" maximum="1000" default="400">
|
||||
|
||||
.. 11-labelname-element:
|
||||
|
||||
1.1 labelname element
|
||||
=====================
|
||||
|
||||
- Defines a human readable name for UI use.
|
||||
- Optional for non-registered axis names.
|
||||
- Can be localised with ``xml:lang``
|
||||
- Child element of ``axis``
|
||||
|
||||
.. attributes-3:
|
||||
|
||||
Attributes
|
||||
~~~~~~~~~~
|
||||
|
||||
- ``xml:lang``: required, string. `XML language
|
||||
definition <https://www.w3.org/International/questions/qa-when-xmllang.en>`__
|
||||
|
||||
Value
|
||||
~~~~~
|
||||
|
||||
- The natural language name of this axis.
|
||||
|
||||
.. example-1:
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
.. code:: xml
|
||||
|
||||
<labelname xml:lang="fa-IR">قطر</labelname>
|
||||
<labelname xml:lang="en">Wéíght</labelname>
|
||||
|
||||
.. 12-map-element:
|
||||
|
||||
1.2 map element
|
||||
===============
|
||||
|
||||
- Defines a single node in a series of input value / output value
|
||||
pairs.
|
||||
- Together these values transform the designspace.
|
||||
- Child of ``axis`` element.
|
||||
|
||||
.. example-2:
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
.. code:: xml
|
||||
|
||||
<map input="1.0" output="10.0" />
|
||||
<map input="400.0" output="66.0" />
|
||||
<map input="1000.0" output="990.0" />
|
||||
|
||||
Example of all axis elements together:
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code:: xml
|
||||
|
||||
<axes>
|
||||
<axis default="1" maximum="1000" minimum="0" name="weight" tag="wght">
|
||||
<labelname xml:lang="fa-IR">قطر</labelname>
|
||||
<labelname xml:lang="en">Wéíght</labelname>
|
||||
</axis>
|
||||
<axis default="100" maximum="200" minimum="50" name="width" tag="wdth">
|
||||
<map input="50.0" output="10.0" />
|
||||
<map input="100.0" output="66.0" />
|
||||
<map input="200.0" output="990.0" />
|
||||
</axis>
|
||||
</axes>
|
||||
|
||||
.. 2-location-element:
|
||||
|
||||
2. location element
|
||||
===================
|
||||
|
||||
- Defines a coordinate in the design space.
|
||||
- Dictionary of axisname: axisvalue
|
||||
- Used in ``source``, ``instance`` and ``glyph`` elements.
|
||||
|
||||
.. 21-dimension-element:
|
||||
|
||||
2.1 dimension element
|
||||
=====================
|
||||
|
||||
- Child element of ``location``
|
||||
|
||||
.. attributes-4:
|
||||
|
||||
Attributes
|
||||
~~~~~~~~~~
|
||||
|
||||
- ``name``: required, string. Name of the axis.
|
||||
- ``xvalue``: required, number. The value on this axis.
|
||||
- ``yvalue``: optional, number. Separate value for anisotropic
|
||||
interpolations.
|
||||
|
||||
.. example-3:
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
.. code:: xml
|
||||
|
||||
<location>
|
||||
<dimension name="width" xvalue="0.000000" />
|
||||
<dimension name="weight" xvalue="0.000000" yvalue="0.003" />
|
||||
</location>
|
||||
|
||||
.. 3-source-element:
|
||||
|
||||
3. source element
|
||||
=================
|
||||
|
||||
- Defines a single font that contributes to the designspace.
|
||||
- Child element of ``sources``
|
||||
|
||||
.. attributes-5:
|
||||
|
||||
Attributes
|
||||
~~~~~~~~~~
|
||||
|
||||
- ``familyname``: optional, string. The family name of the source font.
|
||||
While this could be extracted from the font data itself, it can be
|
||||
more efficient to add it here.
|
||||
- ``stylename``: optional, string. The style name of the source font.
|
||||
- ``name``: required, string. A unique name that can be used to
|
||||
identify this font if it needs to be referenced elsewhere.
|
||||
- ``filename``: required, string. A path to the source file, relative
|
||||
to the root path of this document. The path can be at the same level
|
||||
as the document or lower.
|
||||
|
||||
.. 31-lib-element:
|
||||
|
||||
3.1 lib element
|
||||
===============
|
||||
|
||||
- ``<lib copy="1" />``
|
||||
- Child element of ``source``
|
||||
- Defines if the instances can inherit the data in the lib of this
|
||||
source.
|
||||
- MutatorMath only
|
||||
|
||||
.. 32-info-element:
|
||||
|
||||
3.2 info element
|
||||
================
|
||||
|
||||
- ``<info copy="1" />``
|
||||
- Child element of ``source``
|
||||
- Defines if the instances can inherit the non-interpolating font info
|
||||
from this source.
|
||||
- MutatorMath + Varlib
|
||||
- NOTE: **This presence of this element indicates this source is to be
|
||||
the default font.**
|
||||
|
||||
.. 33-features-element:
|
||||
|
||||
3.3 features element
|
||||
====================
|
||||
|
||||
- ``<features copy="1" />``
|
||||
- Defines if the instances can inherit opentype feature text from this
|
||||
source.
|
||||
- Child element of ``source``
|
||||
- MutatorMath only
|
||||
|
||||
.. 34-glyph-element:
|
||||
|
||||
3.4 glyph element
|
||||
=================
|
||||
|
||||
- Can appear in ``source`` as well as in ``instance`` elements.
|
||||
- In a ``source`` element this states if a glyph is to be excluded from
|
||||
the calculation.
|
||||
- MutatorMath only
|
||||
|
||||
.. attributes-6:
|
||||
|
||||
Attributes
|
||||
~~~~~~~~~~
|
||||
|
||||
- ``mute``: optional attribute, number 1 or 0. Indicate if this glyph
|
||||
should be ignored as a master.
|
||||
- ``<glyph mute="1" name="A"/>``
|
||||
- MutatorMath only
|
||||
|
||||
.. 35-kerning-element:
|
||||
|
||||
3.5 kerning element
|
||||
===================
|
||||
|
||||
- ``<kerning mute="1" />``
|
||||
- Can appear in ``source`` as well as in ``instance`` elements.
|
||||
|
||||
.. attributes-7:
|
||||
|
||||
Attributes
|
||||
~~~~~~~~~~
|
||||
|
||||
- ``mute``: required attribute, number 1 or 0. Indicate if the kerning
|
||||
data from this source is to be excluded from the calculation.
|
||||
- If the kerning element is not present, assume ``mute=0``, yes,
|
||||
include the kerning of this source in the calculation.
|
||||
- MutatorMath only
|
||||
|
||||
.. example-4:
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
.. code:: xml
|
||||
|
||||
<source familyname="MasterFamilyName" filename="masters/masterTest1.ufo" name="master.ufo1" stylename="MasterStyleNameOne">
|
||||
<lib copy="1" />
|
||||
<features copy="1" />
|
||||
<info copy="1" />
|
||||
<glyph mute="1" name="A" />
|
||||
<glyph mute="1" name="Z" />
|
||||
<location>
|
||||
<dimension name="width" xvalue="0.000000" />
|
||||
<dimension name="weight" xvalue="0.000000" />
|
||||
</location>
|
||||
</source>
|
||||
|
||||
.. 4-instance-element:
|
||||
|
||||
4. instance element
|
||||
===================
|
||||
|
||||
- Defines a single font that can be calculated with the designspace.
|
||||
- Child element of ``instances``
|
||||
- For use in Varlib the instance element really only needs the names
|
||||
and the location. The ``glyphs`` element is not required.
|
||||
- MutatorMath uses the ``glyphs`` element to describe how certain
|
||||
glyphs need different masters, mainly to describe the effects of
|
||||
conditional rules in Superpolator.
|
||||
|
||||
.. attributes-8:
|
||||
|
||||
Attributes
|
||||
~~~~~~~~~~
|
||||
|
||||
- ``familyname``: required, string. The family name of the instance
|
||||
font. Corresponds with ``font.info.familyName``
|
||||
- ``stylename``: required, string. The style name of the instance font.
|
||||
Corresponds with ``font.info.styleName``
|
||||
- ``name``: required, string. A unique name that can be used to
|
||||
identify this font if it needs to be referenced elsewhere.
|
||||
- ``filename``: string. Required for MutatorMath. A path to the
|
||||
instance file, relative to the root path of this document. The path
|
||||
can be at the same level as the document or lower.
|
||||
- ``postscriptfontname``: string. Optional for MutatorMath. Corresponds
|
||||
with ``font.info.postscriptFontName``
|
||||
- ``stylemapfamilyname``: string. Optional for MutatorMath. Corresponds
|
||||
with ``styleMapFamilyName``
|
||||
- ``stylemapstylename``: string. Optional for MutatorMath. Corresponds
|
||||
with ``styleMapStyleName``
|
||||
|
||||
Example for varlib
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code:: xml
|
||||
|
||||
<instance familyname="InstanceFamilyName" filename="instances/instanceTest2.ufo" name="instance.ufo2" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName" stylename="InstanceStyleName">
|
||||
<location>
|
||||
<dimension name="width" xvalue="400" yvalue="300" />
|
||||
<dimension name="weight" xvalue="66" />
|
||||
</location>
|
||||
<kerning />
|
||||
<info />
|
||||
</instance>
|
||||
|
||||
.. 41-glyphs-element:
|
||||
|
||||
4.1 glyphs element
|
||||
==================
|
||||
|
||||
- Container for ``glyph`` elements.
|
||||
- Optional
|
||||
- MutatorMath only.
|
||||
|
||||
.. 42-glyph-element:
|
||||
|
||||
4.2 glyph element
|
||||
=================
|
||||
|
||||
- Child element of ``glyphs``
|
||||
- May contain a ``location`` element.
|
||||
|
||||
.. attributes-9:
|
||||
|
||||
Attributes
|
||||
~~~~~~~~~~
|
||||
|
||||
- ``name``: string. The name of the glyph.
|
||||
- ``unicode``: string. Unicode values for this glyph, in hexadecimal.
|
||||
Multiple values should be separated with a space.
|
||||
- ``mute``: optional attribute, number 1 or 0. Indicate if this glyph
|
||||
should be supressed in the output.
|
||||
|
||||
.. 421-note-element:
|
||||
|
||||
4.2.1 note element
|
||||
==================
|
||||
|
||||
- String. The value corresponds to glyph.note in UFO.
|
||||
|
||||
.. 422-masters-element:
|
||||
|
||||
4.2.2 masters element
|
||||
=====================
|
||||
|
||||
- Container for ``master`` elements
|
||||
- These ``master`` elements define an alternative set of glyph masters
|
||||
for this glyph.
|
||||
|
||||
.. 4221-master-element:
|
||||
|
||||
4.2.2.1 master element
|
||||
======================
|
||||
|
||||
- Defines a single alternative master for this glyph.
|
||||
|
||||
#4.3 Localised names for intances Localised names for instances can be
|
||||
included with these simple elements with an xml:lang attribute: `XML
|
||||
language
|
||||
definition <https://www.w3.org/International/questions/qa-when-xmllang.en>`__
|
||||
|
||||
- stylename
|
||||
- familyname
|
||||
- stylemapstylename
|
||||
- stylemapfamilyname
|
||||
|
||||
.. example-5:
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
.. code:: xml
|
||||
|
||||
<stylename xml:lang="fr">Demigras</stylename>
|
||||
<stylename xml:lang="ja">半ば</stylename>
|
||||
<familyname xml:lang="fr">Montserrat</familyname>
|
||||
<familyname xml:lang="ja">モンセラート</familyname>
|
||||
<stylemapstylename xml:lang="de">Standard</stylemapstylename>
|
||||
<stylemapfamilyname xml:lang="de">Montserrat Halbfett</stylemapfamilyname>
|
||||
<stylemapfamilyname xml:lang="ja">モンセラート SemiBold</stylemapfamilyname>
|
||||
|
||||
.. attributes-10:
|
||||
|
||||
Attributes
|
||||
~~~~~~~~~~
|
||||
|
||||
- ``glyphname``: the name of the alternate master glyph.
|
||||
- ``source``: the identifier name of the source this master glyph needs
|
||||
to be loaded from
|
||||
|
||||
.. example-6:
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
.. code:: xml
|
||||
|
||||
<instance familyname="InstanceFamilyName" filename="instances/instanceTest2.ufo" name="instance.ufo2" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName" stylename="InstanceStyleName">
|
||||
<location>
|
||||
<dimension name="width" xvalue="400" yvalue="300" />
|
||||
<dimension name="weight" xvalue="66" />
|
||||
</location>
|
||||
<glyphs>
|
||||
<glyph name="arrow2" />
|
||||
<glyph name="arrow" unicode="0x4d2 0x4d3">
|
||||
<location>
|
||||
<dimension name="width" xvalue="100" />
|
||||
<dimension name="weight" xvalue="120" />
|
||||
</location>
|
||||
<note>A note about this glyph</note>
|
||||
<masters>
|
||||
<master glyphname="BB" source="master.ufo1">
|
||||
<location>
|
||||
<dimension name="width" xvalue="20" />
|
||||
<dimension name="weight" xvalue="20" />
|
||||
</location>
|
||||
</master>
|
||||
</masters>
|
||||
</glyph>
|
||||
</glyphs>
|
||||
<kerning />
|
||||
<info />
|
||||
</instance>
|
||||
|
||||
.. 50-rules-element:
|
||||
|
||||
5.0 rules element
|
||||
=================
|
||||
|
||||
- Container for ``rule`` elements
|
||||
|
||||
.. 51-rule-element:
|
||||
|
||||
5.1 rule element
|
||||
================
|
||||
|
||||
- Defines a named rule with a set of conditions.
|
||||
- The conditional substitutions specifed in the OpenType specification
|
||||
can be much more elaborate than what it recorded in this element.
|
||||
- So while authoring tools are welcome to use the ``sub`` element,
|
||||
they're intended as preview / example / test substitutions for the
|
||||
rule.
|
||||
|
||||
.. attributes-11:
|
||||
|
||||
Attributes
|
||||
~~~~~~~~~~
|
||||
|
||||
- ``name``: required, string. A unique name that can be used to
|
||||
identify this rule if it needs to be referenced elsewhere.
|
||||
|
||||
.. 511-condition-element:
|
||||
|
||||
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-12:
|
||||
|
||||
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.
|
||||
|
||||
.. 512-sub-element:
|
||||
|
||||
5.1.2 sub element
|
||||
=================
|
||||
|
||||
- Child element of ``rule``.
|
||||
- Defines which glyphs to replace when the rule is 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
|
||||
~~~~~~~~~~
|
||||
|
||||
- ``name``: string, required. The name of the glyph this rule looks
|
||||
for.
|
||||
- ``byname``: string, required. The name of the glyph it is replaced
|
||||
with.
|
||||
|
||||
.. example-7:
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
.. code:: xml
|
||||
|
||||
<rules>
|
||||
<rule name="named.rule.1">
|
||||
<condition minimum="250" maximum="750" name="weight" />
|
||||
<condition minimum="50" maximum="100" name="width" />
|
||||
<sub name="dollar" byname="dollar.alt"/>
|
||||
</rule>
|
||||
</rules>
|
||||
|
||||
.. 6-notes:
|
||||
|
||||
6 Notes
|
||||
=======
|
||||
|
||||
Paths and filenames
|
||||
-------------------
|
||||
|
||||
A designspace file needs to store many references to UFO files.
|
||||
|
||||
- designspace files can be part of versioning systems and appear on
|
||||
different computers. This means it is not possible to store absolute
|
||||
paths.
|
||||
- So, all paths are relative to the designspace document path.
|
||||
- Using relative paths allows designspace files and UFO files to be
|
||||
**near** each other, and that they can be **found** without enforcing
|
||||
one particular structure.
|
||||
- The **filename** attribute in the ``SourceDescriptor`` and
|
||||
``InstanceDescriptor`` classes stores the preferred relative path.
|
||||
- The **path** attribute in these objects stores the absolute path. It
|
||||
is calculated from the document path and the relative path in the
|
||||
filename attribute when the object is created.
|
||||
- Only the **filename** attribute is written to file.
|
||||
- Both **filename** and **path** must use forward slashes (``/``) as
|
||||
path separators, even on Windows.
|
||||
|
||||
Right before we save we need to identify and respond to the following
|
||||
situations:
|
||||
|
||||
In each descriptor, we have to do the right thing for the filename
|
||||
attribute. Before writing to file, the ``documentObject.updatePaths()``
|
||||
method prepares the paths as follows:
|
||||
|
||||
**Case 1**
|
||||
|
||||
::
|
||||
|
||||
descriptor.filename == None
|
||||
descriptor.path == None
|
||||
|
||||
**Action**
|
||||
|
||||
- write as is, descriptors will not have a filename attr. Useless, but
|
||||
no reason to interfere.
|
||||
|
||||
**Case 2**
|
||||
|
||||
::
|
||||
|
||||
descriptor.filename == "../something"
|
||||
descriptor.path == None
|
||||
|
||||
**Action**
|
||||
|
||||
- write as is. The filename attr should not be touched.
|
||||
|
||||
**Case 3**
|
||||
|
||||
::
|
||||
|
||||
descriptor.filename == None
|
||||
descriptor.path == "~/absolute/path/there"
|
||||
|
||||
**Action**
|
||||
|
||||
- calculate the relative path for filename. We're not overwriting some
|
||||
other value for filename, it should be fine.
|
||||
|
||||
**Case 4**
|
||||
|
||||
::
|
||||
|
||||
descriptor.filename == '../somewhere'
|
||||
descriptor.path == "~/absolute/path/there"
|
||||
|
||||
**Action**
|
||||
|
||||
- There is a conflict between the given filename, and the path. The
|
||||
difference could have happened for any number of reasons. Assuming
|
||||
the values were not in conflict when the object was created, either
|
||||
could have changed. We can't guess.
|
||||
- Assume the path attribute is more up to date. Calculate a new value
|
||||
for filename based on the path and the document path.
|
||||
|
||||
Recommendation for editors
|
||||
--------------------------
|
||||
|
||||
- If you want to explicitly set the **filename** attribute, leave the
|
||||
path attribute empty.
|
||||
- If you want to explicitly set the **path** attribute, leave the
|
||||
filename attribute empty. It will be recalculated.
|
||||
- Use ``documentObject.updateFilenameFromPath()`` to explicitly set the
|
||||
**filename** attributes for all instance and source descriptors.
|
||||
|
||||
.. 7-this-document:
|
||||
|
||||
7 This document
|
||||
===============
|
||||
|
||||
- The package is rather new and changes are to be expected.
|
246
Doc/source/designspaceLib/scripting.rst
Normal file
246
Doc/source/designspaceLib/scripting.rst
Normal file
@ -0,0 +1,246 @@
|
||||
Scripting a designspace
|
||||
=======================
|
||||
|
||||
It can be useful to build a designspace with a script rather than
|
||||
construct one with an interface like
|
||||
`Superpolator <http://superpolator.com>`__ or
|
||||
`DesignSpaceEditor <https://github.com/LettError/designSpaceRoboFontExtension>`__.
|
||||
The
|
||||
`designSpaceDocument <https://github.com/LettError/designSpaceDocument>`__
|
||||
offers a some tools for building designspaces in Python. This document
|
||||
shows an example.
|
||||
|
||||
So, suppose you installed the
|
||||
`designSpaceDocument <https://github.com/LettError/designSpaceDocument>`__
|
||||
package through your favorite ``git`` client.
|
||||
|
||||
The ``DesignSpaceDocument`` object represents the document, whether it
|
||||
already exists or not. Make a new one:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from designSpaceDocument import DesignSpaceDocument, AxisDescriptor, SourceDescriptor, InstanceDescriptor
|
||||
doc = DesignSpaceDocument()
|
||||
|
||||
We want to create definitions for axes, sources and instances. That
|
||||
means there are a lot of attributes to set. The **DesignSpaceDocument
|
||||
object** uses objects to describe the axes, sources and instances. These
|
||||
are relatively simple objects, think of these as collections of
|
||||
attributes.
|
||||
|
||||
- `Attributes of the Source
|
||||
descriptor <https://github.com/LettError/designSpaceDocument#source-descriptor-object-attributes>`__
|
||||
- `Attributes of the Instance
|
||||
descriptor <https://github.com/LettError/designSpaceDocument#instance-descriptor-object>`__
|
||||
- `Attributes of the Axis
|
||||
descriptor <https://github.com/LettError/designSpaceDocument#axis-descriptor-object>`__
|
||||
- Read about `subclassing
|
||||
descriptors <https://github.com/LettError/designSpaceDocument#subclassing-descriptors>`__
|
||||
|
||||
Make an axis object
|
||||
-------------------
|
||||
|
||||
Make a descriptor object and add it to the document.
|
||||
|
||||
.. code:: python
|
||||
|
||||
a1 = AxisDescriptor()
|
||||
a1.maximum = 1000
|
||||
a1.minimum = 0
|
||||
a1.default = 0
|
||||
a1.name = "weight"
|
||||
a1.tag = "wght"
|
||||
doc.addAxis(a1)
|
||||
|
||||
- You can add as many axes as you need. OpenType has a maximum of
|
||||
around 64K. DesignSpaceEditor has a maximum of 5.
|
||||
- The ``name`` attribute is the name you'll be using as the axis name
|
||||
in the locations.
|
||||
- The ``tag`` attribute is the one of the registered `OpenType
|
||||
Variation Axis
|
||||
Tags <https://www.microsoft.com/typography/otspec/fvar.htm#VAT>`__
|
||||
|
||||
Option: add label names
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The **labelnames** attribute is intended to store localisable, human
|
||||
readable names for this axis if this is not an axis that is registered
|
||||
by OpenType. Think "The label next to the slider". The attribute is a
|
||||
dictionary. The key is the `xml language
|
||||
tag <https://www.w3.org/International/articles/language-tags/>`__, the
|
||||
value is a utf-8 string with the name. Whether or not this attribute is
|
||||
used depends on the font building tool, the operating system and the
|
||||
authoring software. This, at least, is the place to record it.
|
||||
|
||||
.. code:: python
|
||||
|
||||
a1.labelNames['fa-IR'] = u"قطر"
|
||||
a1.labelNames['en'] = u"Wéíght"
|
||||
|
||||
Option: add a map
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The **map** attribute is a list of (input, output) mapping values
|
||||
intended for `axis variations table of
|
||||
OpenType <https://www.microsoft.com/typography/otspec/avar.htm>`__.
|
||||
|
||||
.. code:: python
|
||||
|
||||
a1.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)]
|
||||
|
||||
Make a source object
|
||||
--------------------
|
||||
|
||||
A **source** is an object that points to a UFO file. It provides the
|
||||
outline geometry, kerning and font.info that we want to work with.
|
||||
|
||||
.. code:: python
|
||||
|
||||
s0 = SourceDescriptor()
|
||||
s0.path = "my/path/to/thin.ufo"
|
||||
s0.name = "master.thin"
|
||||
s0.location = dict(weight=0)
|
||||
doc.addSource(s0)
|
||||
|
||||
- You'll need to have at least 2 sources in your document, so go ahead
|
||||
and add another one.
|
||||
- The **location** attribute is a dictionary with the designspace
|
||||
location for this master.
|
||||
- The axis names in the location have to match one of the ``axis.name``
|
||||
values you defined before.
|
||||
- The **path** attribute is the absolute path to an existing UFO.
|
||||
- The **name** attribute is a unique name for this source used to keep
|
||||
track it.
|
||||
|
||||
So go ahead and add another master:
|
||||
|
||||
.. code:: python
|
||||
|
||||
s1 = SourceDescriptor()
|
||||
s1.path = "my/path/to/bold.ufo"
|
||||
s1.name = "master.bold"
|
||||
s1.location = dict(weight=1000)
|
||||
doc.addSource(s1)
|
||||
|
||||
Option: exclude glyphs
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default all glyphs in a source will be processed. If you want to
|
||||
exclude certain glyphs, add their names to the ``mutedGlyphNames`` list.
|
||||
|
||||
.. code:: python
|
||||
|
||||
s1.mutedGlyphNames = ["A.test", "A.old"]
|
||||
|
||||
Make an instance object
|
||||
-----------------------
|
||||
|
||||
An **instance** is description of a UFO that you want to generate with
|
||||
the designspace. For an instance you can define more things. If you want
|
||||
to generate UFO instances with MutatorMath then you can define different
|
||||
names and set flags for if you want to generate kerning and font info
|
||||
and so on. You can also set a path where to generate the instance.
|
||||
|
||||
.. code:: python
|
||||
|
||||
i0 = InstanceDescriptor()
|
||||
i0.familyName = "MyVariableFontPrototype"
|
||||
i0.styleName = "Medium"
|
||||
i0.path = os.path.join(root, "instances","MyVariableFontPrototype-Medium.ufo")
|
||||
i0.location = dict(weight=500)
|
||||
i0.kerning = True
|
||||
i0.info = True
|
||||
doc.addInstance(i0)
|
||||
|
||||
- The ``path`` attribute needs to be the absolute (real or intended)
|
||||
path for the instance. When the document is saved this path will
|
||||
written as relative to the path of the document.
|
||||
- instance paths should be on the same level as the document, or in a
|
||||
level below.
|
||||
- Instances for MutatorMath will generate to UFO.
|
||||
- Instances for variable fonts become **named instances**.
|
||||
|
||||
Option: add more names
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want you can add a PostScript font name, a stylemap familyName
|
||||
and a stylemap styleName.
|
||||
|
||||
.. code:: python
|
||||
|
||||
i0.postScriptFontName = "MyVariableFontPrototype-Medium"
|
||||
i0.styleMapFamilyName = "MyVarProtoMedium"
|
||||
i0.styleMapStyleName = "regular"
|
||||
|
||||
Option: add glyph specific masters
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This bit is not supported by OpenType variable fonts, but it is needed
|
||||
for some designspaces intended for generating instances with
|
||||
MutatorMath. The code becomes a bit verbose, so you're invited to wrap
|
||||
this into something clever.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# we're making a dict with all sorts of
|
||||
#(optional) settings for a glyph.
|
||||
#In this example: the dollar.
|
||||
glyphData = dict(name="dollar", unicodeValue=0x24)
|
||||
|
||||
# you can specify a different location for a glyph
|
||||
glyphData['instanceLocation'] = dict(weight=500)
|
||||
|
||||
# You can specify different masters
|
||||
# for this specific glyph.
|
||||
# You can also give those masters new
|
||||
# locations. It's a miniature designspace.
|
||||
# Remember the "name" attribute we assigned to the sources?
|
||||
glyphData['masters'] = [
|
||||
dict(font="master.thin",
|
||||
glyphName="dollar.nostroke",
|
||||
location=dict(weight=0)),
|
||||
dict(font="master.bold",
|
||||
glyphName="dollar.nostroke",
|
||||
location=dict(weight=1000)),
|
||||
]
|
||||
|
||||
# With all of that set up, store it in the instance.
|
||||
i4.glyphs['dollar'] = glyphData
|
||||
|
||||
Saving
|
||||
======
|
||||
|
||||
.. code:: python
|
||||
|
||||
path = "myprototype.designspace"
|
||||
doc.write(path)
|
||||
|
||||
Reading old designspaces
|
||||
========================
|
||||
|
||||
Old designspace files might not contain ``axes`` definitions. This is
|
||||
how you reconstruct the axes from the extremes of the source locations
|
||||
|
||||
.. code:: python
|
||||
|
||||
doc.checkAxes()
|
||||
|
||||
This is how you check the default font.
|
||||
|
||||
.. code:: python
|
||||
|
||||
doc.checkDefault()
|
||||
|
||||
Generating?
|
||||
===========
|
||||
|
||||
You can generate the UFO's with MutatorMath:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from mutatorMath.ufo import build
|
||||
build("whatevs/myprototype.designspace")
|
||||
|
||||
- Assuming the outline data in the masters is compatible.
|
||||
|
||||
Or you can use the file in making a **variable font** with varlib.
|
@ -7,6 +7,7 @@ fontTools Docs
|
||||
afmLib
|
||||
agl
|
||||
cffLib
|
||||
designspaceLib/index
|
||||
inspect
|
||||
encodings
|
||||
feaLib
|
||||
|
1351
Lib/fontTools/designspaceLib/__init__.py
Normal file
1351
Lib/fontTools/designspaceLib/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
865
Tests/designspaceLib/designspace_test.py
Normal file
865
Tests/designspaceLib/designspace_test.py
Normal file
@ -0,0 +1,865 @@
|
||||
# coding=utf-8
|
||||
|
||||
from __future__ import (print_function, division, absolute_import,
|
||||
unicode_literals)
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from fontTools.misc.py23 import open
|
||||
from fontTools.designspaceLib import (
|
||||
DesignSpaceDocument, SourceDescriptor, AxisDescriptor, RuleDescriptor,
|
||||
InstanceDescriptor, evaluateRule, processRules, posix)
|
||||
|
||||
|
||||
def test_fill_document(tmpdir):
|
||||
tmpdir = str(tmpdir)
|
||||
testDocPath = os.path.join(tmpdir, "test.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.copyLib = True
|
||||
s1.copyInfo = True
|
||||
s1.copyFeatures = True
|
||||
s1.location = dict(weight=0)
|
||||
s1.familyName = "MasterFamilyName"
|
||||
s1.styleName = "MasterStyleNameOne"
|
||||
s1.mutedGlyphNames.append("A")
|
||||
s1.mutedGlyphNames.append("Z")
|
||||
doc.addSource(s1)
|
||||
# add master 2
|
||||
s2 = SourceDescriptor()
|
||||
s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath))
|
||||
s2.name = "master.ufo2"
|
||||
s2.copyLib = False
|
||||
s2.copyInfo = False
|
||||
s2.copyFeatures = False
|
||||
s2.muteKerning = True
|
||||
s2.location = dict(weight=1000)
|
||||
s2.familyName = "MasterFamilyName"
|
||||
s2.styleName = "MasterStyleNameTwo"
|
||||
doc.addSource(s2)
|
||||
# add instance 1
|
||||
i1 = InstanceDescriptor()
|
||||
i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath))
|
||||
i1.familyName = "InstanceFamilyName"
|
||||
i1.styleName = "InstanceStyleName"
|
||||
i1.name = "instance.ufo1"
|
||||
i1.location = dict(weight=500, spooky=666) # this adds a dimension that is not defined.
|
||||
i1.postScriptFontName = "InstancePostscriptName"
|
||||
i1.styleMapFamilyName = "InstanceStyleMapFamilyName"
|
||||
i1.styleMapStyleName = "InstanceStyleMapStyleName"
|
||||
glyphData = dict(name="arrow", mute=True, unicodes=[0x123, 0x124, 0x125])
|
||||
i1.glyphs['arrow'] = glyphData
|
||||
doc.addInstance(i1)
|
||||
# add instance 2
|
||||
i2 = InstanceDescriptor()
|
||||
i2.filename = os.path.relpath(instancePath2, os.path.dirname(testDocPath))
|
||||
i2.familyName = "InstanceFamilyName"
|
||||
i2.styleName = "InstanceStyleName"
|
||||
i2.name = "instance.ufo2"
|
||||
# anisotropic location
|
||||
i2.location = dict(weight=500, width=(400,300))
|
||||
i2.postScriptFontName = "InstancePostscriptName"
|
||||
i2.styleMapFamilyName = "InstanceStyleMapFamilyName"
|
||||
i2.styleMapStyleName = "InstanceStyleMapStyleName"
|
||||
glyphMasters = [dict(font="master.ufo1", glyphName="BB", location=dict(width=20,weight=20)), dict(font="master.ufo2", glyphName="CC", location=dict(width=900,weight=900))]
|
||||
glyphData = dict(name="arrow", unicodes=[101, 201, 301])
|
||||
glyphData['masters'] = glyphMasters
|
||||
glyphData['note'] = "A note about this glyph"
|
||||
glyphData['instanceLocation'] = dict(width=100, weight=120)
|
||||
i2.glyphs['arrow'] = glyphData
|
||||
i2.glyphs['arrow2'] = dict(mute=False)
|
||||
doc.addInstance(i2)
|
||||
|
||||
# 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"Poids"
|
||||
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
|
||||
r1 = RuleDescriptor()
|
||||
r1.name = "named.rule.1"
|
||||
r1.conditions.append(dict(name='aaaa', minimum=0, maximum=1))
|
||||
r1.conditions.append(dict(name='bbbb', minimum=2, maximum=3))
|
||||
r1.subs.append(("a", "a.alt"))
|
||||
doc.addRule(r1)
|
||||
# write the document
|
||||
doc.write(testDocPath)
|
||||
assert os.path.exists(testDocPath)
|
||||
# import it again
|
||||
new = DesignSpaceDocument()
|
||||
new.read(testDocPath)
|
||||
|
||||
new.check()
|
||||
assert new.default.location == {'width': 20.0, 'weight': 0.0}
|
||||
|
||||
# >>> 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
|
||||
axes = {}
|
||||
for axis in doc.axes:
|
||||
if axis.tag not in axes:
|
||||
axes[axis.tag] = []
|
||||
axes[axis.tag].append(axis.serialize())
|
||||
for axis in new.axes:
|
||||
if axis.tag[0] == "_":
|
||||
continue
|
||||
if axis.tag not in axes:
|
||||
axes[axis.tag] = []
|
||||
axes[axis.tag].append(axis.serialize())
|
||||
for v in axes.values():
|
||||
a, b = v
|
||||
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):
|
||||
tmpdir = str(tmpdir)
|
||||
testDocPath = os.path.join(tmpdir, "testUnicodes.designspace")
|
||||
testDocPath2 = os.path.join(tmpdir, "testUnicodes_roundtrip.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.location = dict(weight=0)
|
||||
doc.addSource(s1)
|
||||
# add master 2
|
||||
s2 = SourceDescriptor()
|
||||
s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath))
|
||||
s2.name = "master.ufo2"
|
||||
s2.location = dict(weight=1000)
|
||||
doc.addSource(s2)
|
||||
# add instance 1
|
||||
i1 = InstanceDescriptor()
|
||||
i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath))
|
||||
i1.name = "instance.ufo1"
|
||||
i1.location = dict(weight=500)
|
||||
glyphData = dict(name="arrow", mute=True, unicodes=[100, 200, 300])
|
||||
i1.glyphs['arrow'] = glyphData
|
||||
doc.addInstance(i1)
|
||||
# now we have sources and instances, but no axes yet.
|
||||
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"
|
||||
doc.addAxis(a1)
|
||||
# write the document
|
||||
doc.write(testDocPath)
|
||||
assert os.path.exists(testDocPath)
|
||||
# import it again
|
||||
new = DesignSpaceDocument()
|
||||
new.read(testDocPath)
|
||||
new.write(testDocPath2)
|
||||
# compare the file contents
|
||||
f1 = open(testDocPath, 'r', encoding='utf-8')
|
||||
t1 = f1.read()
|
||||
f1.close()
|
||||
f2 = open(testDocPath2, 'r', encoding='utf-8')
|
||||
t2 = f2.read()
|
||||
f2.close()
|
||||
assert t1 == t2
|
||||
# check the unicode values read from the document
|
||||
assert new.instances[0].glyphs['arrow']['unicodes'] == [100,200,300]
|
||||
|
||||
|
||||
def test_localisedNames(tmpdir):
|
||||
tmpdir = str(tmpdir)
|
||||
testDocPath = os.path.join(tmpdir, "testLocalisedNames.designspace")
|
||||
testDocPath2 = os.path.join(tmpdir, "testLocalisedNames_roundtrip.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.location = dict(weight=0)
|
||||
doc.addSource(s1)
|
||||
# add master 2
|
||||
s2 = SourceDescriptor()
|
||||
s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath))
|
||||
s2.name = "master.ufo2"
|
||||
s2.location = dict(weight=1000)
|
||||
doc.addSource(s2)
|
||||
# add instance 1
|
||||
i1 = InstanceDescriptor()
|
||||
i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath))
|
||||
i1.familyName = "Montserrat"
|
||||
i1.styleName = "SemiBold"
|
||||
i1.styleMapFamilyName = "Montserrat SemiBold"
|
||||
i1.styleMapStyleName = "Regular"
|
||||
i1.setFamilyName("Montserrat", "fr")
|
||||
i1.setFamilyName(u"モンセラート", "ja")
|
||||
i1.setStyleName("Demigras", "fr")
|
||||
i1.setStyleName(u"半ば", "ja")
|
||||
i1.setStyleMapStyleName(u"Standard", "de")
|
||||
i1.setStyleMapFamilyName("Montserrat Halbfett", "de")
|
||||
i1.setStyleMapFamilyName(u"モンセラート SemiBold", "ja")
|
||||
i1.name = "instance.ufo1"
|
||||
i1.location = dict(weight=500, spooky=666) # this adds a dimension that is not defined.
|
||||
i1.postScriptFontName = "InstancePostscriptName"
|
||||
glyphData = dict(name="arrow", mute=True, unicodes=[0x123])
|
||||
i1.glyphs['arrow'] = glyphData
|
||||
doc.addInstance(i1)
|
||||
# now we have sources and instances, but no axes yet.
|
||||
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 = 0
|
||||
a2.name = "width"
|
||||
a2.tag = "wdth"
|
||||
a2.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)]
|
||||
a2.labelNames[u'fr'] = u"Poids"
|
||||
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
|
||||
r1 = RuleDescriptor()
|
||||
r1.name = "named.rule.1"
|
||||
r1.conditions.append(dict(name='aaaa', minimum=0, maximum=1))
|
||||
r1.conditions.append(dict(name='bbbb', minimum=2, maximum=3))
|
||||
r1.subs.append(("a", "a.alt"))
|
||||
doc.addRule(r1)
|
||||
# write the document
|
||||
doc.write(testDocPath)
|
||||
assert os.path.exists(testDocPath)
|
||||
# import it again
|
||||
new = DesignSpaceDocument()
|
||||
new.read(testDocPath)
|
||||
new.write(testDocPath2)
|
||||
f1 = open(testDocPath, 'r', encoding='utf-8')
|
||||
t1 = f1.read()
|
||||
f1.close()
|
||||
f2 = open(testDocPath2, 'r', encoding='utf-8')
|
||||
t2 = f2.read()
|
||||
f2.close()
|
||||
assert t1 == t2
|
||||
|
||||
|
||||
def test_handleNoAxes(tmpdir):
|
||||
tmpdir = str(tmpdir)
|
||||
# test what happens if the designspacedocument has no axes element.
|
||||
testDocPath = os.path.join(tmpdir, "testNoAxes_source.designspace")
|
||||
testDocPath2 = os.path.join(tmpdir, "testNoAxes_recontructed.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")
|
||||
|
||||
# Case 1: No axes element in the document, but there are sources and instances
|
||||
doc = DesignSpaceDocument()
|
||||
|
||||
for name, value in [('One', 1),('Two', 2),('Three', 3)]:
|
||||
a = AxisDescriptor()
|
||||
a.minimum = 0
|
||||
a.maximum = 1000
|
||||
a.default = 0
|
||||
a.name = "axisName%s"%(name)
|
||||
a.tag = "ax_%d"%(value)
|
||||
doc.addAxis(a)
|
||||
|
||||
# add master 1
|
||||
s1 = SourceDescriptor()
|
||||
s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath))
|
||||
s1.name = "master.ufo1"
|
||||
s1.copyLib = True
|
||||
s1.copyInfo = True
|
||||
s1.copyFeatures = True
|
||||
s1.location = dict(axisNameOne=-1000, axisNameTwo=0, axisNameThree=1000)
|
||||
s1.familyName = "MasterFamilyName"
|
||||
s1.styleName = "MasterStyleNameOne"
|
||||
doc.addSource(s1)
|
||||
|
||||
# add master 2
|
||||
s2 = SourceDescriptor()
|
||||
s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath))
|
||||
s2.name = "master.ufo1"
|
||||
s2.copyLib = False
|
||||
s2.copyInfo = False
|
||||
s2.copyFeatures = False
|
||||
s2.location = dict(axisNameOne=1000, axisNameTwo=1000, axisNameThree=0)
|
||||
s2.familyName = "MasterFamilyName"
|
||||
s2.styleName = "MasterStyleNameTwo"
|
||||
doc.addSource(s2)
|
||||
|
||||
# add instance 1
|
||||
i1 = InstanceDescriptor()
|
||||
i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath))
|
||||
i1.familyName = "InstanceFamilyName"
|
||||
i1.styleName = "InstanceStyleName"
|
||||
i1.name = "instance.ufo1"
|
||||
i1.location = dict(axisNameOne=(-1000,500), axisNameTwo=100)
|
||||
i1.postScriptFontName = "InstancePostscriptName"
|
||||
i1.styleMapFamilyName = "InstanceStyleMapFamilyName"
|
||||
i1.styleMapStyleName = "InstanceStyleMapStyleName"
|
||||
doc.addInstance(i1)
|
||||
|
||||
doc.write(testDocPath)
|
||||
__removeAxesFromDesignSpace(testDocPath)
|
||||
verify = DesignSpaceDocument()
|
||||
verify.read(testDocPath)
|
||||
verify.write(testDocPath2)
|
||||
|
||||
|
||||
def test_pathNameResolve(tmpdir):
|
||||
tmpdir = str(tmpdir)
|
||||
# test how descriptor.path and descriptor.filename are resolved
|
||||
testDocPath1 = os.path.join(tmpdir, "testPathName_case1.designspace")
|
||||
testDocPath2 = os.path.join(tmpdir, "testPathName_case2.designspace")
|
||||
testDocPath3 = os.path.join(tmpdir, "testPathName_case3.designspace")
|
||||
testDocPath4 = os.path.join(tmpdir, "testPathName_case4.designspace")
|
||||
testDocPath5 = os.path.join(tmpdir, "testPathName_case5.designspace")
|
||||
testDocPath6 = os.path.join(tmpdir, "testPathName_case6.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")
|
||||
|
||||
# Case 1: filename and path are both empty. Nothing to calculate, nothing to put in the file.
|
||||
doc = DesignSpaceDocument()
|
||||
s = SourceDescriptor()
|
||||
s.filename = None
|
||||
s.path = None
|
||||
s.copyInfo = True
|
||||
s.location = dict(weight=0)
|
||||
s.familyName = "MasterFamilyName"
|
||||
s.styleName = "MasterStyleNameOne"
|
||||
doc.addSource(s)
|
||||
doc.write(testDocPath1)
|
||||
verify = DesignSpaceDocument()
|
||||
verify.read(testDocPath1)
|
||||
assert verify.sources[0].filename == None
|
||||
assert verify.sources[0].path == None
|
||||
|
||||
# Case 2: filename is empty, path points somewhere: calculate a new filename.
|
||||
doc = DesignSpaceDocument()
|
||||
s = SourceDescriptor()
|
||||
s.filename = None
|
||||
s.path = masterPath1
|
||||
s.copyInfo = True
|
||||
s.location = dict(weight=0)
|
||||
s.familyName = "MasterFamilyName"
|
||||
s.styleName = "MasterStyleNameOne"
|
||||
doc.addSource(s)
|
||||
doc.write(testDocPath2)
|
||||
verify = DesignSpaceDocument()
|
||||
verify.read(testDocPath2)
|
||||
assert verify.sources[0].filename == "masters/masterTest1.ufo"
|
||||
assert verify.sources[0].path == posix(masterPath1)
|
||||
|
||||
# Case 3: the filename is set, the path is None.
|
||||
doc = DesignSpaceDocument()
|
||||
s = SourceDescriptor()
|
||||
s.filename = "../somewhere/over/the/rainbow.ufo"
|
||||
s.path = None
|
||||
s.copyInfo = True
|
||||
s.location = dict(weight=0)
|
||||
s.familyName = "MasterFamilyName"
|
||||
s.styleName = "MasterStyleNameOne"
|
||||
doc.addSource(s)
|
||||
doc.write(testDocPath3)
|
||||
verify = DesignSpaceDocument()
|
||||
verify.read(testDocPath3)
|
||||
assert verify.sources[0].filename == "../somewhere/over/the/rainbow.ufo"
|
||||
# make the absolute path for filename so we can see if it matches the path
|
||||
p = os.path.abspath(os.path.join(os.path.dirname(testDocPath3), verify.sources[0].filename))
|
||||
assert verify.sources[0].path == posix(p)
|
||||
|
||||
# Case 4: the filename points to one file, the path points to another. The path takes precedence.
|
||||
doc = DesignSpaceDocument()
|
||||
s = SourceDescriptor()
|
||||
s.filename = "../somewhere/over/the/rainbow.ufo"
|
||||
s.path = masterPath1
|
||||
s.copyInfo = True
|
||||
s.location = dict(weight=0)
|
||||
s.familyName = "MasterFamilyName"
|
||||
s.styleName = "MasterStyleNameOne"
|
||||
doc.addSource(s)
|
||||
doc.write(testDocPath4)
|
||||
verify = DesignSpaceDocument()
|
||||
verify.read(testDocPath4)
|
||||
assert verify.sources[0].filename == "masters/masterTest1.ufo"
|
||||
|
||||
# Case 5: the filename is None, path has a value, update the filename
|
||||
doc = DesignSpaceDocument()
|
||||
s = SourceDescriptor()
|
||||
s.filename = None
|
||||
s.path = masterPath1
|
||||
s.copyInfo = True
|
||||
s.location = dict(weight=0)
|
||||
s.familyName = "MasterFamilyName"
|
||||
s.styleName = "MasterStyleNameOne"
|
||||
doc.addSource(s)
|
||||
doc.write(testDocPath5) # so that the document has a path
|
||||
doc.updateFilenameFromPath()
|
||||
assert doc.sources[0].filename == "masters/masterTest1.ufo"
|
||||
|
||||
# Case 6: the filename has a value, path has a value, update the filenames with force
|
||||
doc = DesignSpaceDocument()
|
||||
s = SourceDescriptor()
|
||||
s.filename = "../somewhere/over/the/rainbow.ufo"
|
||||
s.path = masterPath1
|
||||
s.copyInfo = True
|
||||
s.location = dict(weight=0)
|
||||
s.familyName = "MasterFamilyName"
|
||||
s.styleName = "MasterStyleNameOne"
|
||||
doc.write(testDocPath5) # so that the document has a path
|
||||
doc.addSource(s)
|
||||
assert doc.sources[0].filename == "../somewhere/over/the/rainbow.ufo"
|
||||
doc.updateFilenameFromPath(force=True)
|
||||
assert doc.sources[0].filename == "masters/masterTest1.ufo"
|
||||
|
||||
|
||||
def test_normalise():
|
||||
doc = DesignSpaceDocument()
|
||||
# write some axes
|
||||
a1 = AxisDescriptor()
|
||||
a1.minimum = -1000
|
||||
a1.maximum = 1000
|
||||
a1.default = 0
|
||||
a1.name = "aaa"
|
||||
a1.tag = "aaaa"
|
||||
doc.addAxis(a1)
|
||||
|
||||
assert doc.normalizeLocation(dict(aaa=0)) == {'aaa': 0.0}
|
||||
assert doc.normalizeLocation(dict(aaa=1000)) == {'aaa': 1.0}
|
||||
|
||||
# clipping beyond max values:
|
||||
assert doc.normalizeLocation(dict(aaa=1001)) == {'aaa': 1.0}
|
||||
assert doc.normalizeLocation(dict(aaa=500)) == {'aaa': 0.5}
|
||||
assert doc.normalizeLocation(dict(aaa=-1000)) == {'aaa': -1.0}
|
||||
assert doc.normalizeLocation(dict(aaa=-1001)) == {'aaa': -1.0}
|
||||
# anisotropic coordinates normalise to isotropic
|
||||
assert doc.normalizeLocation(dict(aaa=(1000, -1000))) == {'aaa': 1.0}
|
||||
doc.normalize()
|
||||
r = []
|
||||
for axis in doc.axes:
|
||||
r.append((axis.name, axis.minimum, axis.default, axis.maximum))
|
||||
r.sort()
|
||||
assert r == [('aaa', -1.0, 0.0, 1.0)]
|
||||
|
||||
doc = DesignSpaceDocument()
|
||||
# write some axes
|
||||
a2 = AxisDescriptor()
|
||||
a2.minimum = 100
|
||||
a2.maximum = 1000
|
||||
a2.default = 100
|
||||
a2.name = "bbb"
|
||||
doc.addAxis(a2)
|
||||
assert doc.normalizeLocation(dict(bbb=0)) == {'bbb': 0.0}
|
||||
assert doc.normalizeLocation(dict(bbb=1000)) == {'bbb': 1.0}
|
||||
# clipping beyond max values:
|
||||
assert doc.normalizeLocation(dict(bbb=1001)) == {'bbb': 1.0}
|
||||
assert doc.normalizeLocation(dict(bbb=500)) == {'bbb': 0.4444444444444444}
|
||||
assert doc.normalizeLocation(dict(bbb=-1000)) == {'bbb': 0.0}
|
||||
assert doc.normalizeLocation(dict(bbb=-1001)) == {'bbb': 0.0}
|
||||
# anisotropic coordinates normalise to isotropic
|
||||
assert doc.normalizeLocation(dict(bbb=(1000,-1000))) == {'bbb': 1.0}
|
||||
assert doc.normalizeLocation(dict(bbb=1001)) == {'bbb': 1.0}
|
||||
doc.normalize()
|
||||
r = []
|
||||
for axis in doc.axes:
|
||||
r.append((axis.name, axis.minimum, axis.default, axis.maximum))
|
||||
r.sort()
|
||||
assert r == [('bbb', 0.0, 0.0, 1.0)]
|
||||
|
||||
doc = DesignSpaceDocument()
|
||||
# write some axes
|
||||
a3 = AxisDescriptor()
|
||||
a3.minimum = -1000
|
||||
a3.maximum = 0
|
||||
a3.default = 0
|
||||
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': -1.0}
|
||||
assert doc.normalizeLocation(dict(ccc=-1001)) == {'ccc': -1.0}
|
||||
|
||||
doc.normalize()
|
||||
r = []
|
||||
for axis in doc.axes:
|
||||
r.append((axis.name, axis.minimum, axis.default, axis.maximum))
|
||||
r.sort()
|
||||
assert r == [('ccc', -1.0, 0.0, 0.0)]
|
||||
|
||||
|
||||
doc = DesignSpaceDocument()
|
||||
# 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()
|
||||
# write some axes
|
||||
a4 = AxisDescriptor()
|
||||
a4.minimum = 0
|
||||
a4.maximum = 1000
|
||||
a4.default = 0
|
||||
a4.name = "ddd"
|
||||
a4.map = [(0,100), (300, 500), (600, 500), (1000,900)]
|
||||
doc.addAxis(a4)
|
||||
doc.normalize()
|
||||
r = []
|
||||
for axis in doc.axes:
|
||||
r.append((axis.name, axis.map))
|
||||
r.sort()
|
||||
assert r == [('ddd', [(0, 0.1), (300, 0.5), (600, 0.5), (1000, 0.9)])]
|
||||
|
||||
|
||||
def test_rules(tmpdir):
|
||||
tmpdir = str(tmpdir)
|
||||
testDocPath = os.path.join(tmpdir, "testRules.designspace")
|
||||
testDocPath2 = os.path.join(tmpdir, "testRules_roundtrip.designspace")
|
||||
doc = DesignSpaceDocument()
|
||||
# write some axes
|
||||
a1 = AxisDescriptor()
|
||||
a1.tag = "taga"
|
||||
a1.name = "aaaa"
|
||||
a1.minimum = 0
|
||||
a1.maximum = 1000
|
||||
a1.default = 0
|
||||
doc.addAxis(a1)
|
||||
a2 = AxisDescriptor()
|
||||
a2.tag = "tagb"
|
||||
a2.name = "bbbb"
|
||||
a2.minimum = 0
|
||||
a2.maximum = 3000
|
||||
a2.default = 0
|
||||
doc.addAxis(a2)
|
||||
|
||||
r1 = RuleDescriptor()
|
||||
r1.name = "named.rule.1"
|
||||
r1.conditions.append(dict(name='aaaa', minimum=0, maximum=1000))
|
||||
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
|
||||
assert evaluateRule(r1, dict(aaaa = 500, bbbb = 0)) == True
|
||||
assert evaluateRule(r1, dict(aaaa = 0, bbbb = 0)) == True
|
||||
assert evaluateRule(r1, dict(aaaa = 1000, bbbb = 0)) == True
|
||||
assert evaluateRule(r1, dict(aaaa = 1000, bbbb = -100)) == False
|
||||
assert evaluateRule(r1, dict(aaaa = 1000.0001, bbbb = 0)) == False
|
||||
assert evaluateRule(r1, dict(aaaa = -0.0001, bbbb = 0)) == False
|
||||
assert evaluateRule(r1, dict(aaaa = -100, bbbb = 0)) == False
|
||||
assert processRules([r1], dict(aaaa = 500), ["a", "b", "c"]) == ['a.alt', 'b', 'c']
|
||||
assert processRules([r1], dict(aaaa = 500), ["a.alt", "b", "c"]) == ['a.alt', 'b', 'c']
|
||||
assert 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"))
|
||||
|
||||
assert evaluateRule(r2, dict(aaaa = 0)) == True
|
||||
assert evaluateRule(r2, dict(aaaa = -500)) == True
|
||||
assert 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"))
|
||||
|
||||
assert evaluateRule(r3, dict(aaaa = 0)) == False
|
||||
assert evaluateRule(r3, dict(aaaa = 1000)) == True
|
||||
assert 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"))
|
||||
|
||||
assert evaluateRule(r4, dict()) == True # is this what we expect though?
|
||||
assert evaluateRule(r4, dict(aaaa = 1000, bbbb = 0)) == True
|
||||
assert evaluateRule(r4, dict(aaaa = 0, bbbb = 0)) == False
|
||||
assert evaluateRule(r4, dict(aaaa = 1000, bbbb = 1000)) == False
|
||||
|
||||
a1 = AxisDescriptor()
|
||||
a1.minimum = 0
|
||||
a1.maximum = 1000
|
||||
a1.default = 0
|
||||
a1.name = "aaaa"
|
||||
a1.tag = "aaaa"
|
||||
b1 = AxisDescriptor()
|
||||
b1.minimum = 2000
|
||||
b1.maximum = 3000
|
||||
b1.default = 2000
|
||||
b1.name = "bbbb"
|
||||
b1.tag = "bbbb"
|
||||
doc.addAxis(a1)
|
||||
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.rules[0].conditions == [{'minimum': 0, 'maximum': 1000, 'name': 'aaaa'}, {'minimum': 0, 'maximum': 3000, 'name': 'bbbb'}]
|
||||
|
||||
assert doc.rules[0].subs == [('a', 'a.alt')]
|
||||
|
||||
doc.normalize()
|
||||
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'}]
|
||||
|
||||
doc.write(testDocPath)
|
||||
new = DesignSpaceDocument()
|
||||
|
||||
new.read(testDocPath)
|
||||
assert len(new.axes) == 4
|
||||
assert len(new.rules) == 1
|
||||
new.write(testDocPath2)
|
||||
|
||||
|
||||
def __removeAxesFromDesignSpace(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()
|
||||
start = d.find("<axes>")
|
||||
end = d.find("</axes>")+len("</axes>")
|
||||
n = d[0:start] + d[end:]
|
||||
f = open(path, 'w', encoding='utf-8')
|
||||
f.write(n)
|
||||
f.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def invalid_designspace():
|
||||
p = "testCheck.designspace"
|
||||
__removeAxesFromDesignSpace(p)
|
||||
yield p
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="The check method requires MutatorMath")
|
||||
def test_check(invalid_designspace, tmpdir):
|
||||
tmpdir = str(tmpdir)
|
||||
# check if the checks are checking
|
||||
testDocPath = os.path.join(tmpdir, invalid_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")
|
||||
|
||||
# no default selected
|
||||
doc = DesignSpaceDocument()
|
||||
# add master 1
|
||||
s1 = SourceDescriptor()
|
||||
s1.path = masterPath1
|
||||
s1.name = "master.ufo1"
|
||||
s1.location = dict(snap=0, pop=10)
|
||||
s1.familyName = "MasterFamilyName"
|
||||
s1.styleName = "MasterStyleNameOne"
|
||||
doc.addSource(s1)
|
||||
# add master 2
|
||||
s2 = SourceDescriptor()
|
||||
s2.path = masterPath2
|
||||
s2.name = "master.ufo2"
|
||||
s2.location = dict(snap=1000, pop=20)
|
||||
s2.familyName = "MasterFamilyName"
|
||||
s2.styleName = "MasterStyleNameTwo"
|
||||
doc.addSource(s2)
|
||||
doc.checkAxes()
|
||||
doc.getAxisOrder() == ['snap', 'pop']
|
||||
assert doc.default == None
|
||||
doc.checkDefault()
|
||||
assert doc.default.name == 'master.ufo1'
|
||||
|
||||
# default selected
|
||||
doc = DesignSpaceDocument()
|
||||
# add master 1
|
||||
s1 = SourceDescriptor()
|
||||
s1.path = masterPath1
|
||||
s1.name = "master.ufo1"
|
||||
s1.location = dict(snap=0, pop=10)
|
||||
s1.familyName = "MasterFamilyName"
|
||||
s1.styleName = "MasterStyleNameOne"
|
||||
doc.addSource(s1)
|
||||
# add master 2
|
||||
s2 = SourceDescriptor()
|
||||
s2.path = masterPath2
|
||||
s2.name = "master.ufo2"
|
||||
s2.copyInfo = True
|
||||
s2.location = dict(snap=1000, pop=20)
|
||||
s2.familyName = "MasterFamilyName"
|
||||
s2.styleName = "MasterStyleNameTwo"
|
||||
doc.addSource(s2)
|
||||
doc.checkAxes()
|
||||
assert doc.getAxisOrder() == ['snap', 'pop']
|
||||
assert doc.default == None
|
||||
doc.checkDefault()
|
||||
assert doc.default.name == 'master.ufo2'
|
||||
|
||||
# generate a doc without axes, save and read again
|
||||
doc = DesignSpaceDocument()
|
||||
# add master 1
|
||||
s1 = SourceDescriptor()
|
||||
s1.path = masterPath1
|
||||
s1.name = "master.ufo1"
|
||||
s1.location = dict(snap=0, pop=10)
|
||||
s1.familyName = "MasterFamilyName"
|
||||
s1.styleName = "MasterStyleNameOne"
|
||||
doc.addSource(s1)
|
||||
# add master 2
|
||||
s2 = SourceDescriptor()
|
||||
s2.path = masterPath2
|
||||
s2.name = "master.ufo2"
|
||||
s2.location = dict(snap=1000, pop=20)
|
||||
s2.familyName = "MasterFamilyName"
|
||||
s2.styleName = "MasterStyleNameTwo"
|
||||
doc.addSource(s2)
|
||||
doc.checkAxes()
|
||||
doc.write(testDocPath)
|
||||
__removeAxesFromDesignSpace(testDocPath)
|
||||
|
||||
new = DesignSpaceDocument()
|
||||
new.read(testDocPath)
|
||||
assert len(new.axes) == 2
|
||||
new.checkAxes()
|
||||
assert len(new.axes) == 2
|
||||
assert print([a.name for a in new.axes]) == ['snap', 'pop']
|
||||
new.write(testDocPath)
|
Loading…
x
Reference in New Issue
Block a user