- mutators for info, kerning and glyphs are lazily constructed. Only make them if they're asked for. - getInfoMutator() makes / returns info mutator. - getKerningMutator() makes / returns kerning mutator - getGlyphMutator() makes / returns glyph mutator - loadFonts attempts to load the master UFOs and determine the defautl font by looking for the copyInfo flag, or if that is not found, by using mutator's findBias. Will warn if there is a conflct, but the copyInfo flag is leading. - makeInstance() returns a font object for the asked location. You can decide to save it or not elsewher. - generateUFO() makes UFOs on disk for all defined instances. Still largely untested. Note: this requires an updated mutatorMath.
418 lines
17 KiB
Python
418 lines
17 KiB
Python
from __future__ import print_function, division, absolute_import
|
|
|
|
from ufoLib import fontInfoAttributesVersion1, fontInfoAttributesVersion2, fontInfoAttributesVersion3
|
|
from pprint import pprint
|
|
|
|
"""
|
|
|
|
A subclassed DesignSpaceDocument that can
|
|
- process the document and generate finished UFOs with MutatorMath.
|
|
- read and write
|
|
- bypass and eventually replace the mutatormath ufo generator.
|
|
|
|
"""
|
|
|
|
|
|
from designSpaceDocument import DesignSpaceDocument, SourceDescriptor, InstanceDescriptor, AxisDescriptor
|
|
from defcon.objects.font import Font
|
|
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 os
|
|
|
|
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=2):
|
|
super(DesignSpaceProcessor, self).__init__(readerClass=None, writerClass=None, fontClass=None)
|
|
self.ufoVersion = ufoVersion # target UFO version
|
|
self.roundGeometry = False
|
|
self._glyphMutators = {}
|
|
self._infoMutator = None
|
|
self._kerningMutator = None
|
|
self.default = None # name of the default master
|
|
self.defaultLoc = None
|
|
self.fonts = {}
|
|
self.glyphNames = [] # list of all glyphnames
|
|
|
|
def generateUFO(self):
|
|
# makes the instances
|
|
self.loadFonts()
|
|
for instanceDescriptor in self.instances:
|
|
if instanceDescriptor.path is None:
|
|
continue
|
|
font = self.makeInstance(instanceDescriptor)
|
|
if not os.path.exists(os.path.dirname(instanceDescriptor.path)):
|
|
os.makedirs(os.path.dirname(instanceDescriptor.path))
|
|
font.save(instanceDescriptor.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, 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]
|
|
kerningItems.append((loc, self.mathKerningClass(sourceFont.kerning, sourceFont.groups)))
|
|
bias, self._kerningMutator = buildMutator(kerningItems, bias=self.defaultLoc)
|
|
return self._kerningMutator
|
|
|
|
def getGlyphMutator(self, glyphName):
|
|
""" Return a glyph mutator """
|
|
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
|
|
items.append((loc, self.mathGlyphClass(f[glyphName])))
|
|
bias, self._glyphMutators[glyphName] = buildMutator(items, bias=self.defaultLoc)
|
|
return self._glyphMutators[glyphName]
|
|
|
|
def loadFonts(self):
|
|
# Load the fonts and find the default candidate based on the info flag
|
|
defaultCandidate = None
|
|
for sourceDescriptor in self.sources:
|
|
names = set()
|
|
if not sourceDescriptor.name in self.fonts:
|
|
self.fonts[sourceDescriptor.name] = self._instantiateFont(sourceDescriptor.path)
|
|
names = names | set(self.fonts[sourceDescriptor.name].keys())
|
|
if sourceDescriptor.copyInfo:
|
|
# we choose you!
|
|
defaultCandidate = sourceDescriptor
|
|
self.glyphNames = list(names)
|
|
# find the default based on mutatorMath bias
|
|
masterLocations = [Location(src.location) for src in self.sources]
|
|
mutatorBias = biasFromLocations(masterLocations)
|
|
c = [src for src in self.sources if src.location==mutatorBias]
|
|
if c:
|
|
mutatorDefaultCandidate = c[0]
|
|
else:
|
|
mutatorDefaultCandidate = None
|
|
# what are we going to do?
|
|
if defaultCandidate is not None and mutatorDefaultCandidate.name != defaultCandidate.name:
|
|
# warn if we have a conflict
|
|
print("Note: conflicting default masters:\n\tUsing %s as default\n\tMutator found %s"%(defaultCandidate.name, mutatorDefaultCandidate.name))
|
|
if defaultCandidate is None and mutatorDefaultCandidate is not None:
|
|
# we didn't have a flag, use the one selected by mutator
|
|
defaultCandidate = mutatorDefaultCandidate
|
|
self.default = defaultCandidate
|
|
self.defaultLoc = Location(self.default.location)
|
|
|
|
def makeInstance(self, instanceDescriptor):
|
|
""" Generate a font object for this instance """
|
|
font = self._instantiateFont(None)
|
|
# make fonty things here
|
|
loc = Location(instanceDescriptor.location)
|
|
# make the kerning
|
|
if instanceDescriptor.kerning:
|
|
self.getKerningMutator().makeInstance(loc).extractKerning(font)
|
|
# make the info
|
|
if instanceDescriptor.info:
|
|
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
|
|
# 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:
|
|
font.lib.update(self.fonts[sourceDescriptor.name].lib)
|
|
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
|
|
for glyphName in self.glyphNames:
|
|
glyphMutator = self.getGlyphMutator(glyphName)
|
|
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}}],
|
|
# 'unicodeValue': 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))
|
|
glyphInstanceUnicode = glyphData.get("unicodeValue", font[glyphName].unicode)
|
|
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)
|
|
sourceGlyph = MathGlyph(self.fonts.get(sourceGlyphFont)[sourceGlyphName])
|
|
sourceGlyphLocation = Location(glyphMaster.get("location"))
|
|
items.append((sourceGlyphLocation, sourceGlyph))
|
|
bias, glyphMutator = buildMutator(items, bias=self.defaultLoc)
|
|
glyphInstanceObject = glyphMutator.makeInstance(glyphInstanceLocation)
|
|
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
|
|
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
|
|
|
|
def addGlyphs(font, s):
|
|
# we need to add the glyphs
|
|
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*2))
|
|
g.width = s
|
|
|
|
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")
|
|
|
|
# Two masters
|
|
f1 = Font()
|
|
addGlyphs(f1, 100)
|
|
f1.features.text = u"# features text from master 1"
|
|
|
|
f2 = Font()
|
|
addGlyphs(f2, 500)
|
|
f2.features.text = u"# features text from master 2"
|
|
|
|
|
|
fillInfo(f1)
|
|
f1.info.ascender = 400
|
|
f1.info.descender = -200
|
|
fillInfo(f2)
|
|
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"
|
|
|
|
# save
|
|
f1.save(path1, 2)
|
|
f2.save(path2, 2)
|
|
return path1, path2, path3, path4, path5
|
|
|
|
def test0(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
|
|
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)
|
|
d.addInstance(i)
|
|
d.write(docPath)
|
|
|
|
def test1(docPath):
|
|
# execute the test document
|
|
d = DesignSpaceProcessor()
|
|
d.read(docPath)
|
|
d.generateUFO()
|
|
|
|
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")
|
|
test0(docPath)
|
|
test1(docPath)
|