Merge remote-tracking branch 'designSpaceDocument/master'

This commit is contained in:
Jany Belluz 2017-11-22 13:14:29 +00:00
commit 44727fe32a
9 changed files with 3951 additions and 0 deletions

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ htmlcov/
# OSX Finder
.DS_Store

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
<?xml version='1.0' encoding='utf-8'?>
<designspace format="3">
<axes>
<axis default="0" maximum="1000" minimum="0" name="weight" tag="wght">
<labelname xml:lang="fa-IR">قطر</labelname>
<labelname xml:lang="en">Wéíght</labelname>
</axis>
<axis default="0" maximum="1000" minimum="0" name="width" tag="wdth">
<labelname xml:lang="fr">Poids</labelname>
<map input="0" output="10" />
<map input="401" output="66" />
<map input="1000" output="990" />
</axis>
</axes>
<rules>
<rule name="named.rule.1">
<condition maximum="1" minimum="0" name="aaaa" />
<condition maximum="3" minimum="2" name="bbbb" />
<sub name="a" with="a.alt" />
</rule>
</rules>
<sources>
<source filename="masters/masterTest1.ufo" name="master.ufo1">
<info copy="1" />
<location>
<dimension name="weight" xvalue="0" />
<dimension name="width" xvalue="0" />
</location>
</source>
<source filename="masters/masterTest2.ufo" name="master.ufo2">
<location>
<dimension name="weight" xvalue="1000" />
<dimension name="width" xvalue="0" />
</location>
</source>
</sources>
<instances>
<instance familyname="Montserrat" filename="instances/instanceTest1.ufo" name="instance.ufo1" postscriptfontname="InstancePostscriptName" stylemapfamilyname="Montserrat SemiBold" stylemapstylename="Regular" stylename="SemiBold">
<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>
<location>
<dimension name="weight" xvalue="500" />
<dimension name="width" xvalue="0" />
</location>
<glyphs>
<glyph mute="1" name="arrow" unicode="0x123" />
</glyphs>
<kerning />
<info />
</instance>
</instances>
</designspace>

View File

@ -0,0 +1,57 @@
<?xml version='1.0' encoding='utf-8'?>
<designspace format="3">
<axes>
<axis default="0" maximum="1000" minimum="0" name="weight" tag="wght">
<labelname xml:lang="fa-IR">قطر</labelname>
<labelname xml:lang="en">Wéíght</labelname>
</axis>
<axis default="0" maximum="1000" minimum="0" name="width" tag="wdth">
<labelname xml:lang="fr">Poids</labelname>
<map input="0" output="10" />
<map input="401" output="66" />
<map input="1000" output="990" />
</axis>
</axes>
<rules>
<rule name="named.rule.1">
<condition maximum="1" minimum="0" name="aaaa" />
<condition maximum="3" minimum="2" name="bbbb" />
<sub name="a" with="a.alt" />
</rule>
</rules>
<sources>
<source filename="masters/masterTest1.ufo" name="master.ufo1">
<info copy="1" />
<location>
<dimension name="weight" xvalue="0" />
<dimension name="width" xvalue="0" />
</location>
</source>
<source filename="masters/masterTest2.ufo" name="master.ufo2">
<location>
<dimension name="weight" xvalue="1000" />
<dimension name="width" xvalue="0" />
</location>
</source>
</sources>
<instances>
<instance familyname="Montserrat" filename="instances/instanceTest1.ufo" name="instance.ufo1" postscriptfontname="InstancePostscriptName" stylemapfamilyname="Montserrat SemiBold" stylemapstylename="Regular" stylename="SemiBold">
<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>
<location>
<dimension name="weight" xvalue="500" />
<dimension name="width" xvalue="0" />
</location>
<glyphs>
<glyph mute="1" name="arrow" unicode="0x123" />
</glyphs>
<kerning />
<info />
</instance>
</instances>
</designspace>

View File

@ -0,0 +1,16 @@
<?xml version='1.0' encoding='utf-8'?>
<designspace format="3">
<axes>
<axis default="0" maximum="1" minimum="0" name="aaaa" tag="taga" />
<axis default="0" maximum="1" minimum="0" name="bbbb" tag="tagb" />
<axis default="0" maximum="1" minimum="0" name="aaaa" tag="aaaa" />
<axis default="0" maximum="1" minimum="0" name="bbbb" tag="bbbb" />
</axes>
<rules>
<rule name="named.rule.1">
<condition maximum="1" minimum="0" name="aaaa" />
<condition maximum="1" minimum="0" name="bbbb" />
<sub name="a" with="a.alt" />
</rule>
</rules>
</designspace>

View File

@ -0,0 +1,16 @@
<?xml version='1.0' encoding='utf-8'?>
<designspace format="3">
<axes>
<axis default="0" maximum="1" minimum="0" name="aaaa" tag="taga" />
<axis default="0" maximum="1" minimum="0" name="bbbb" tag="tagb" />
<axis default="0" maximum="1" minimum="0" name="aaaa" tag="aaaa" />
<axis default="0" maximum="1" minimum="0" name="bbbb" tag="bbbb" />
</axes>
<rules>
<rule name="named.rule.1">
<condition maximum="1" minimum="0" name="aaaa" />
<condition maximum="1" minimum="0" name="bbbb" />
<sub name="a" with="a.alt" />
</rule>
</rules>
</designspace>

View File

@ -0,0 +1,766 @@
# coding: utf-8
from __future__ import print_function, division, absolute_import
from ufoLib import fontInfoAttributesVersion1, fontInfoAttributesVersion2, fontInfoAttributesVersion3
from pprint import pprint
import logging
"""
A subclassed DesignSpaceDocument that can
- process the document and generate finished UFOs with MutatorMath.
- read and write documents
- bypass and eventually replace the mutatormath ufo generator.
"""
from designSpaceDocument import DesignSpaceDocument, SourceDescriptor, InstanceDescriptor, AxisDescriptor, RuleDescriptor, processRules
from defcon.objects.font import Font
from defcon.pens.transformPointPen import TransformPointPen
from defcon.objects.component import _defaultTransformation
import defcon
from fontMath.mathGlyph import MathGlyph
from fontMath.mathInfo import MathInfo
from fontMath.mathKerning import MathKerning
from mutatorMath.objects.mutator import buildMutator
from mutatorMath.objects.location import biasFromLocations, Location
import plistlib
import os
"""
Swap the contents of two glyphs.
- contours
- components
- width
- group membership
- kerning
+ Remap components so that glyphs that reference either of the swapped glyphs maintain appearance
+ Keep the unicode value of the original glyph.
Notes
Parking the glyphs under a swapname is a bit lazy, but at least it guarantees the glyphs have the right parent.
"""
""" These are some UFO specific tools for use with Mutator.
build() is a convenience function for reading and executing a designspace file.
documentPath: filepath to the .designspace document
outputUFOFormatVersion: ufo format for output
verbose: True / False for lots or no feedback
logPath: filepath to a log file
progressFunc: an optional callback to report progress.
see mutatorMath.ufo.tokenProgressFunc
"""
def build(
documentPath,
outputUFOFormatVersion=3,
roundGeometry=True,
verbose=True, # not supported
logPath=None, # not supported
progressFunc=None, # not supported
processRules=True,
logger=None
):
"""
Simple builder for UFO designspaces.
"""
import os, glob
if os.path.isdir(documentPath):
# process all *.designspace documents in this folder
todo = glob.glob(os.path.join(documentPath, "*.designspace"))
else:
# process the
todo = [documentPath]
results = []
for path in todo:
reader = DesignSpaceProcessor(ufoVersion=outputUFOFormatVersion)
reader.roundGeometry = roundGeometry
reader.read(path)
try:
r = reader.generateUFO(processRules=processRules)
results.append(r)
except:
if logger:
logger.exception("ufoProcessor error")
#results += reader.generateUFO(processRules=processRules)
reader = None
return results
def getUFOVersion(ufoPath):
# <?xml version="1.0" encoding="UTF-8"?>
# <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
# <plist version="1.0">
# <dict>
# <key>creator</key>
# <string>org.robofab.ufoLib</string>
# <key>formatVersion</key>
# <integer>2</integer>
# </dict>
# </plist>
metaInfoPath = os.path.join(ufoPath, u"metainfo.plist")
p = plistlib.readPlist(metaInfoPath)
return p.get('formatVersion')
def swapGlyphNames(font, oldName, newName, swapNameExtension = "_______________swap"):
if not oldName in font or not newName in font:
return None
swapName = oldName + swapNameExtension
# park the old glyph
if not swapName in font:
font.newGlyph(swapName)
# swap the outlines
font[swapName].clear()
p = font[swapName].getPointPen()
font[oldName].drawPoints(p)
font[swapName].width = font[oldName].width
# lib?
font[oldName].clear()
p = font[oldName].getPointPen()
font[newName].drawPoints(p)
font[oldName].width = font[newName].width
font[newName].clear()
p = font[newName].getPointPen()
font[swapName].drawPoints(p)
font[newName].width = font[swapName].width
# remap the components
for g in font:
for c in g.components:
if c.baseGlyph == oldName:
c.baseGlyph = swapName
continue
for g in font:
for c in g.components:
if c.baseGlyph == newName:
c.baseGlyph = oldName
continue
for g in font:
for c in g.components:
if c.baseGlyph == swapName:
c.baseGlyph = newName
# change the names in groups
# the shapes will swap, that will invalidate the kerning
# so the names need to swap in the kerning as well.
newKerning = {}
for first, second in font.kerning.keys():
value = font.kerning[(first,second)]
if first == oldName:
first = newName
elif first == newName:
first = oldName
if second == oldName:
second = newName
elif second == newName:
second = oldName
newKerning[(first, second)] = value
font.kerning.clear()
font.kerning.update(newKerning)
for groupName, members in font.groups.items():
newMembers = []
for name in members:
if name == oldName:
newMembers.append(newName)
elif name == newName:
newMembers.append(oldName)
else:
newMembers.append(name)
font.groups[groupName] = newMembers
remove = []
for g in font:
if g.name.find(swapNameExtension)!=-1:
remove.append(g.name)
for r in remove:
del font[r]
class DecomposePointPen(object):
def __init__(self, glyphSet, outPointPen):
self._glyphSet = glyphSet
self._outPointPen = outPointPen
self.beginPath = outPointPen.beginPath
self.endPath = outPointPen.endPath
self.addPoint = outPointPen.addPoint
def addComponent(self, baseGlyphName, transformation):
if baseGlyphName in self._glyphSet:
baseGlyph = self._glyphSet[baseGlyphName]
if transformation == _defaultTransformation:
baseGlyph.drawPoints(self)
else:
transformPointPen = TransformPointPen(self, transformation)
baseGlyph.drawPoints(transformPointPen)
class DesignSpaceProcessor(DesignSpaceDocument):
"""
builder of glyphs from designspaces
validate the data
if it works, make a generating thing
"""
fontClass = defcon.Font
glyphClass = defcon.Glyph
libClass = defcon.Lib
glyphContourClass = defcon.Contour
glyphPointClass = defcon.Point
glyphComponentClass = defcon.Component
glyphAnchorClass = defcon.Anchor
kerningClass = defcon.Kerning
groupsClass = defcon.Groups
infoClass = defcon.Info
featuresClass = defcon.Features
mathInfoClass = MathInfo
mathGlyphClass = MathGlyph
mathKerningClass = MathKerning
def __init__(self, readerClass=None, writerClass=None, fontClass=None, ufoVersion=3):
super(DesignSpaceProcessor, self).__init__(readerClass=readerClass, writerClass=writerClass, fontClass=fontClass)
self.ufoVersion = ufoVersion # target UFO version
self.roundGeometry = False
self._glyphMutators = {}
self._infoMutator = None
self._kerningMutator = None
self._preppedAxes = None
self.fonts = {}
self._fontsLoaded = False
self.glyphNames = [] # list of all glyphnames
self.processRules = True
self.problems = [] # receptacle for problem notifications. Not big enough to break, but also not small enough to ignore.
def generateUFO(self, processRules=True):
# makes the instances
# option to execute the rules
# make sure we're not trying to overwrite a newer UFO format
self.loadFonts()
self.checkDefault()
v = 0
for instanceDescriptor in self.instances:
if instanceDescriptor.path is None:
continue
font = self.makeInstance(instanceDescriptor, processRules)
folder = os.path.dirname(instanceDescriptor.path)
path = instanceDescriptor.path
if not os.path.exists(folder):
os.makedirs(folder)
if os.path.exists(path):
existingUFOFormatVersion = getUFOVersion(path)
if existingUFOFormatVersion > self.ufoVersion:
self.problems.append(u"Cant overwrite existing UFO%d with UFO%d."%(existingUFOFormatVersion, self.ufoVersion))
continue
else:
font.save(path, self.ufoVersion)
self.problems.append("Generated %s as UFO%d"%(os.path.basename(path), self.ufoVersion))
def getInfoMutator(self):
""" Returns a info mutator """
if self._infoMutator:
return self._infoMutator
infoItems = []
for sourceDescriptor in self.sources:
loc = Location(sourceDescriptor.location)
sourceFont = self.fonts[sourceDescriptor.name]
infoItems.append((loc, self.mathInfoClass(sourceFont.info)))
bias, self._infoMutator = buildMutator(infoItems, axes=self._preppedAxes, bias=self.defaultLoc)
return self._infoMutator
def getKerningMutator(self):
""" Return a kerning mutator """
if self._kerningMutator:
return self._kerningMutator
kerningItems = []
for sourceDescriptor in self.sources:
loc = Location(sourceDescriptor.location)
sourceFont = self.fonts[sourceDescriptor.name]
# this makes assumptions about the groups of all sources being the same.
kerningItems.append((loc, self.mathKerningClass(sourceFont.kerning, sourceFont.groups)))
bias, self._kerningMutator = buildMutator(kerningItems, axes=self._preppedAxes, bias=self.defaultLoc)
return self._kerningMutator
def getGlyphMutator(self, glyphName, decomposeComponents=False):
""" Return a glyph mutator.defaultLoc
decomposeComponents = True causes the source glyphs to be decomposed first
before building the mutator. That gives you instances that do not depend
on a complete font. If you're calculating previews for instance.
"""
if glyphName in self._glyphMutators:
return self._glyphMutators[glyphName]
items = []
for sourceDescriptor in self.sources:
loc = Location(sourceDescriptor.location)
f = self.fonts[sourceDescriptor.name]
if glyphName in sourceDescriptor.mutedGlyphNames:
continue
if not glyphName in f:
# log this>
continue
sourceGlyphObject = f[glyphName]
if decomposeComponents:
temp = self.glyphClass()
p = temp.getPointPen()
dpp = DecomposePointPen(f, p)
sourceGlyphObject.drawPoints(dpp)
temp.width = sourceGlyphObject.width
temp.name = sourceGlyphObject.name
#temp.lib = sourceGlyphObject.lib
processThis = temp
else:
processThis = sourceGlyphObject
items.append((loc, self.mathGlyphClass(processThis)))
bias, self._glyphMutators[glyphName] = buildMutator(items, axes=self._preppedAxes, bias=self.defaultLoc)
return self._glyphMutators[glyphName]
def loadFonts(self, reload=False):
# Load the fonts and find the default candidate based on the info flag
if self._fontsLoaded and not reload:
return
names = set()
for sourceDescriptor in self.sources:
if not sourceDescriptor.name in self.fonts:
self.fonts[sourceDescriptor.name] = self._instantiateFont(sourceDescriptor.path)
self.problems.append("loaded master from %s, format %d"%(sourceDescriptor.path, getUFOVersion(sourceDescriptor.path)))
names = names | set(self.fonts[sourceDescriptor.name].keys())
self.glyphNames = list(names)
self._fontsLoaded = True
def makeInstance(self, instanceDescriptor, doRules=False, glyphNames=None):
""" Generate a font object for this instance """
font = self._instantiateFont(None)
self._preppedAxes = self._prepAxesForBender()
# make fonty things here
loc = Location(instanceDescriptor.location)
# groups,
if hasattr(self.fonts[self.default.name], "kerningGroupConversionRenameMaps"):
renameMap = self.fonts[self.default.name].kerningGroupConversionRenameMaps
self.problems.append("renameMap %s"%renameMap)
else:
renameMap = {}
font.kerningGroupConversionRenameMaps = renameMap
# make the kerning
if instanceDescriptor.kerning:
try:
self.getKerningMutator().makeInstance(loc).extractKerning(font)
except:
self.problems.append("Could not make kerning for %s"%loc)
# make the info
if instanceDescriptor.info:
try:
self.getInfoMutator().makeInstance(loc).extractInfo(font.info)
info = self._infoMutator.makeInstance(loc)
info.extractInfo(font.info)
font.info.familyName = instanceDescriptor.familyName
font.info.styleName = instanceDescriptor.styleName
font.info.postScriptFontName = instanceDescriptor.postScriptFontName
font.info.styleMapFamilyName = instanceDescriptor.styleMapFamilyName
font.info.styleMapStyleName = instanceDescriptor.styleMapStyleName
# localised names need to go to the right openTypeNameRecords
# records = []
# nameID = 1
# platformID =
# for languageCode, name in instanceDescriptor.localisedStyleMapFamilyName.items():
# # Name ID 1 (font family name) is found at the generic styleMapFamily attribute.
# records.append((nameID, ))
except:
self.problems.append("Could not make fontinfo for %s"%loc)
# copied info
for sourceDescriptor in self.sources:
if sourceDescriptor.copyInfo:
# this is the source
self._copyFontInfo(self.fonts[sourceDescriptor.name].info, font.info)
if sourceDescriptor.copyLib:
# excplicitly copy the font.lib items
for key, value in self.fonts[sourceDescriptor.name].lib.items():
font.lib[key] = value
if sourceDescriptor.copyFeatures:
featuresText = self.fonts[sourceDescriptor.name].features.text
if isinstance(featuresText, str):
font.features.text = u""+featuresText
elif isinstance(featuresText, unicode):
font.features.text = featuresText
# glyphs
if glyphNames:
selectedGlyphNames = glyphNames
else:
selectedGlyphNames = self.glyphNames
# add the glyphnames to the font.lib['public.glyphOrder']
if not 'public.glyphOrder' in font.lib.keys():
font.lib['public.glyphOrder'] = selectedGlyphNames
for glyphName in selectedGlyphNames:
try:
glyphMutator = self.getGlyphMutator(glyphName)
except:
self.problems.append("Could not make mutator for glyph %s"%glyphName)
continue
if glyphName in instanceDescriptor.glyphs.keys():
# reminder: this is what the glyphData can look like
# {'instanceLocation': {'custom': 0.0, 'weight': 824.0},
# 'masters': [{'font': 'master.Adobe VF Prototype.Master_0.0',
# 'glyphName': 'dollar.nostroke',
# 'location': {'custom': 0.0, 'weight': 0.0}},
# {'font': 'master.Adobe VF Prototype.Master_1.1',
# 'glyphName': 'dollar.nostroke',
# 'location': {'custom': 0.0, 'weight': 368.0}},
# {'font': 'master.Adobe VF Prototype.Master_2.2',
# 'glyphName': 'dollar.nostroke',
# 'location': {'custom': 0.0, 'weight': 1000.0}},
# {'font': 'master.Adobe VF Prototype.Master_3.3',
# 'glyphName': 'dollar.nostroke',
# 'location': {'custom': 100.0, 'weight': 1000.0}},
# {'font': 'master.Adobe VF Prototype.Master_0.4',
# 'glyphName': 'dollar.nostroke',
# 'location': {'custom': 100.0, 'weight': 0.0}},
# {'font': 'master.Adobe VF Prototype.Master_4.5',
# 'glyphName': 'dollar.nostroke',
# 'location': {'custom': 100.0, 'weight': 368.0}}],
# 'unicodes': [36]}
glyphData = instanceDescriptor.glyphs[glyphName]
else:
glyphData = {}
font.newGlyph(glyphName)
font[glyphName].clear()
if glyphData.get('mute', False):
# mute this glyph, skip
continue
glyphInstanceLocation = Location(glyphData.get("instanceLocation", instanceDescriptor.location))
try:
uniValues = glyphMutator[()][0].unicodes
except IndexError:
uniValues = []
glyphInstanceUnicodes = glyphData.get("unicodes", uniValues)
note = glyphData.get("note")
if note:
font[glyphName] = note
masters = glyphData.get("masters", None)
if masters:
items = []
for glyphMaster in masters:
sourceGlyphFont = glyphMaster.get("font")
sourceGlyphName = glyphMaster.get("glyphName", glyphName)
m = self.fonts.get(sourceGlyphFont)
if not sourceGlyphName in m:
continue
sourceGlyph = MathGlyph(m[sourceGlyphName])
sourceGlyphLocation = Location(glyphMaster.get("location"))
items.append((sourceGlyphLocation, sourceGlyph))
bias, glyphMutator = buildMutator(items, axes=self._preppedAxes, bias=self.defaultLoc)
try:
glyphInstanceObject = glyphMutator.makeInstance(glyphInstanceLocation)
except IndexError:
# alignment problem with the data?
print("Error making instance %s"%glyphName)
continue
font.newGlyph(glyphName)
font[glyphName].clear()
if self.roundGeometry:
try:
glyphInstanceObject = glyphInstanceObject.round()
except AttributeError:
pass
try:
glyphInstanceObject.extractGlyph(font[glyphName], onlyGeometry=True)
except TypeError:
# this causes ruled glyphs to end up in the wrong glyphname
# but defcon2 objects don't support it
pPen = font[glyphName].getPointPen()
font[glyphName].clear()
glyphInstanceObject.drawPoints(pPen)
font[glyphName].width = glyphInstanceObject.width
font[glyphName].unicodes = glyphInstanceUnicodes
if doRules:
resultNames = processRules(self.rules, loc, self.glyphNames)
for oldName, newName in zip(self.glyphNames, resultNames):
if oldName != newName:
swapGlyphNames(font, oldName, newName)
# copy the glyph lib?
#for sourceDescriptor in self.sources:
# if sourceDescriptor.copyLib:
# pass
# pass
# store designspace location in the font.lib
font.lib['designspace'] = list(instanceDescriptor.location.items())
return font
def _instantiateFont(self, path):
""" Return a instance of a font object with all the given subclasses"""
return self.fontClass(path,
libClass=self.libClass,
kerningClass=self.kerningClass,
groupsClass=self.groupsClass,
infoClass=self.infoClass,
featuresClass=self.featuresClass,
glyphClass=self.glyphClass,
glyphContourClass=self.glyphContourClass,
glyphPointClass=self.glyphPointClass,
glyphComponentClass=self.glyphComponentClass,
glyphAnchorClass=self.glyphAnchorClass)
def _copyFontInfo(self, sourceInfo, targetInfo):
""" Copy the non-calculating fields from the source info."""
infoAttributes = [
"versionMajor",
"versionMinor",
"copyright",
"trademark",
"note",
"openTypeGaspRangeRecords",
"openTypeHeadCreated",
"openTypeHeadFlags",
"openTypeNameDesigner",
"openTypeNameDesignerURL",
"openTypeNameManufacturer",
"openTypeNameManufacturerURL",
"openTypeNameLicense",
"openTypeNameLicenseURL",
"openTypeNameVersion",
"openTypeNameUniqueID",
"openTypeNameDescription",
"#openTypeNamePreferredFamilyName",
"#openTypeNamePreferredSubfamilyName",
"#openTypeNameCompatibleFullName",
"openTypeNameSampleText",
"openTypeNameWWSFamilyName",
"openTypeNameWWSSubfamilyName",
"openTypeNameRecords",
"openTypeOS2Selection",
"openTypeOS2VendorID",
"openTypeOS2Panose",
"openTypeOS2FamilyClass",
"openTypeOS2UnicodeRanges",
"openTypeOS2CodePageRanges",
"openTypeOS2Type",
"postscriptIsFixedPitch",
"postscriptForceBold",
"postscriptDefaultCharacter",
"postscriptWindowsCharacterSet"
]
for infoAttribute in infoAttributes:
copy = False
if self.ufoVersion == 1 and infoAttribute in fontInfoAttributesVersion1:
copy = True
elif self.ufoVersion == 2 and infoAttribute in fontInfoAttributesVersion2:
copy = True
elif self.ufoVersion == 3 and infoAttribute in fontInfoAttributesVersion3:
copy = True
if copy:
value = getattr(sourceInfo, infoAttribute)
setattr(targetInfo, infoAttribute, value)
if __name__ == "__main__":
# standalone test
import shutil
import os
from defcon.objects.font import Font
import logging
def addGlyphs(font, s):
# we need to add the glyphs
step = 0
for n in ['glyphOne', 'glyphTwo', 'glyphThree', 'glyphFour']:
font.newGlyph(n)
g = font[n]
p = g.getPen()
p.moveTo((0,0))
p.lineTo((s,0))
p.lineTo((s,s))
p.lineTo((0,s))
p.closePath()
g.move((0,s+step))
g.width = s
step += 50
for n, w in [('wide', 800), ('narrow', 100)]:
font.newGlyph(n)
g = font[n]
p = g.getPen()
p.moveTo((0,0))
p.lineTo((w,0))
p.lineTo((w,font.info.ascender))
p.lineTo((0,font.info.ascender))
p.closePath()
g.width = w
font.newGlyph("wide.component")
g = font["wide.component"]
comp = g.instantiateComponent()
comp.baseGlyph = "wide"
comp.offset = (0,0)
g.appendComponent(comp)
g.width = font['wide'].width
font.newGlyph("narrow.component")
g = font["narrow.component"]
comp = g.instantiateComponent()
comp.baseGlyph = "narrow"
comp.offset = (0,0)
g.appendComponent(comp)
g.width = font['narrow'].width
uniValue = 200
for g in font:
g.unicode = uniValue
uniValue += 1
def fillInfo(font):
font.info.unitsPerEm = 1000
font.info.ascender = 800
font.info.descender = -200
def makeTestFonts(rootPath):
""" Make some test fonts that have the kerning problem."""
path1 = os.path.join(rootPath, "geometryMaster1.ufo")
path2 = os.path.join(rootPath, "geometryMaster2.ufo")
path3 = os.path.join(rootPath, "my_test_instance_dir_one", "geometryInstance%3.3f.ufo")
path4 = os.path.join(rootPath, "my_test_instance_dir_two", "geometryInstanceAnisotropic1.ufo")
path5 = os.path.join(rootPath, "my_test_instance_dir_two", "geometryInstanceAnisotropic2.ufo")
f1 = Font()
fillInfo(f1)
addGlyphs(f1, 100)
f1.features.text = u"# features text from master 1"
f2 = Font()
fillInfo(f2)
addGlyphs(f2, 500)
f2.features.text = u"# features text from master 2"
f1.info.ascender = 400
f1.info.descender = -200
f2.info.ascender = 600
f2.info.descender = -100
f1.info.copyright = u"This is the copyright notice from master 1"
f2.info.copyright = u"This is the copyright notice from master 2"
f1.lib['ufoProcessor.test.lib.entry'] = "Lib entry for master 1"
f2.lib['ufoProcessor.test.lib.entry'] = "Lib entry for master 2"
f1.save(path1, 2)
f2.save(path2, 2)
return path1, path2, path3, path4, path5
def makeSwapFonts(rootPath):
""" Make some test fonts that have the kerning problem."""
path1 = os.path.join(rootPath, "Swap.ufo")
path2 = os.path.join(rootPath, "Swapped.ufo")
f1 = Font()
fillInfo(f1)
addGlyphs(f1, 100)
f1.features.text = u"# features text from master 1"
f1.info.ascender = 800
f1.info.descender = -200
f1.kerning[('glyphOne', 'glyphOne')] = -10
f1.kerning[('glyphTwo', 'glyphTwo')] = 10
f1.save(path1, 2)
return path1, path2
def testDocument(docPath):
# make the test fonts and a test document
testFontPath = os.path.join(os.getcwd(), "automatic_testfonts")
m1, m2, i1, i2, i3 = makeTestFonts(testFontPath)
d = DesignSpaceProcessor()
a = AxisDescriptor()
a.name = "pop"
a.minimum = 50
a.maximum = 1000
a.default = 0
a.tag = "pop*"
d.addAxis(a)
s1 = SourceDescriptor()
s1.path = m1
s1.location = dict(pop=a.minimum)
s1.name = "test.master.1"
s1.copyInfo = True
s1.copyFeatures = True
s1.copyLib = True
d.addSource(s1)
s2 = SourceDescriptor()
s2.path = m2
s2.location = dict(pop=1000)
s2.name = "test.master.2"
#s2.copyInfo = True
d.addSource(s2)
for counter in range(3):
factor = counter / 2
i = InstanceDescriptor()
v = a.minimum+factor*(a.maximum-a.minimum)
i.path = i1%v
i.familyName = "TestFamily"
i.styleName = "TestStyle_pop%3.3f"%(v)
i.name = "%s-%s"%(i.familyName, i.styleName)
i.location = dict(pop=v)
i.info = True
i.kerning = True
if counter == 2:
i.glyphs['glyphTwo'] = dict(name="glyphTwo", mute=True)
i.copyLib = True
if counter == 2:
i.glyphs['narrow'] = dict(instanceLocation=dict(pop=400), unicodes=[0x123, 0x124, 0x125])
d.addInstance(i)
d.write(docPath)
def testGenerateInstances(docPath):
# execute the test document
d = DesignSpaceProcessor()
d.read(docPath)
d.generateUFO()
if d.problems:
print(d.problems)
def testSwap(docPath):
srcPath, dstPath = makeSwapFonts(os.path.dirname(docPath))
f = Font(srcPath)
swapGlyphNames(f, "narrow", "wide")
f.info.styleName = "Swapped"
f.save(dstPath)
# test the results in newly opened fonts
old = Font(srcPath)
new = Font(dstPath)
assert new.kerning.get(("narrow", "narrow")) == old.kerning.get(("wide","wide"))
assert new.kerning.get(("wide", "wide")) == old.kerning.get(("narrow","narrow"))
# after the swap these widths should be the same
assert old['narrow'].width == new['wide'].width
assert old['wide'].width == new['narrow'].width
# The following test may be a bit counterintuitive:
# the rule swaps the glyphs, but we do not want glyphs that are not
# specifically affected by the rule to *appear* any different.
# So, components have to be remapped.
assert new['wide.component'].components[0].baseGlyph == "narrow"
assert new['narrow.component'].components[0].baseGlyph == "wide"
def testUnicodes(docPath):
# after executing testSwap there should be some test fonts
# let's check if the unicode values for glyph "narrow" arrive at the right place.
d = DesignSpaceProcessor()
d.read(docPath)
for instance in d.instances:
if os.path.exists(instance.path):
f = Font(instance.path)
if instance.name == "TestFamily-TestStyle_pop1000.000":
assert f['narrow'].unicodes == [291, 292, 293]
else:
assert f['narrow'].unicodes == [207]
else:
print("Missing test font at %s"%instance.path)
selfTest = True
if selfTest:
testRoot = os.path.join(os.getcwd(), "automatic_testfonts")
if os.path.exists(testRoot):
shutil.rmtree(testRoot)
docPath = os.path.join(testRoot, "automatic_test.designspace")
testDocument(docPath)
testGenerateInstances(docPath)
testSwap(docPath)
testUnicodes(docPath)

591
README.md Normal file
View File

@ -0,0 +1,591 @@
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.
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.
```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.
```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
* `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
```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.
```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.
```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.
```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.
```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
* Define a single axis
* Child element of `axes`
### 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.
```xml
<axis name="weight" tag="wght" minimum="1" maximum="1000" default="400">
```
# 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
* `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
```xml
<labelname xml:lang="fa-IR">قطر</labelname>
<labelname xml:lang="en">Wéíght</labelname>
```
# 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
```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:
```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
* Defines a coordinate in the design space.
* Dictionary of axisname: axisvalue
* Used in `source`, `instance` and `glyph` elements.
# 2.1 dimension element
* Child element of `location`
### 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
```xml
<location>
<dimension name="width" xvalue="0.000000" />
<dimension name="weight" xvalue="0.000000" yvalue="0.003" />
</location>
```
# 3. source element
* Defines a single font that contributes to the designspace.
* Child element of `sources`
### 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.
# 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
# 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.**
# 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
# 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
* `mute`: optional attribute, number 1 or 0. Indicate if this glyph should be ignored as a master.
* `<glyph mute="1" name="A"/>`
* MutatorMath only
# 3.5 kerning element
* `<kerning mute="1" />`
* Can appear in `source` as well as in `instance` elements.
### 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
```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
* 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
* `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
```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>
```
# 4.1 glyphs element
* Container for `glyph` elements.
* Optional
* MutatorMath only.
# 4.2 glyph element
* Child element of `glyphs`
* May contain a `location` element.
### 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.
# 4.2.1 note element
* String. The value corresponds to glyph.note in UFO.
# 4.2.2 masters element
* Container for `master` elements
* These `master` elements define an alternative set of glyph masters for this glyph.
# 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
```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
* `glyphname`: the name of the alternate master glyph.
* `source`: the identifier name of the source this master glyph needs to be loaded from
### Example
```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>
```
# 5.0 rules element
* Container for `rule` elements
# 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
* `name`: required, string. A unique name that can be used to identify this rule if it needs to be referenced elsewhere.
# 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.
# 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
* `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
```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
## 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
* The package is rather new and changes are to be expected.

183
scripting.md Normal file
View File

@ -0,0 +1,183 @@
# 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:
```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.
```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.
```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).
```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.
```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:
```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.
```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.
```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.
```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.
```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
```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
```python
doc.checkAxes()
```
This is how you check the default font.
```python
doc.checkDefault()
```
# Generating?
You can generate the UFO's with MutatorMath:
```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.