First commit for DesignSpaceProcessor is a subclass of DesignSpaceDocument.

- aims to do work the same way as mutatorMath.ufo.document
- generate all instances as UFO
- handle glyph masters, info, kerning, lib

More testing is needed.
This commit is contained in:
Erik 2016-11-30 14:45:41 +01:00
parent baac25990d
commit 0e51f99f37
2 changed files with 405 additions and 0 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ Lib/DesignSpaceDocument.egg-info
__pycache__
*.py[co]
.DS_Store
Lib/designSpaceDocument/automatic_testfonts

View File

@ -0,0 +1,404 @@
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 import MathGlyph, MathInfo, 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(self.__class__, self).__init__(readerClass=None, writerClass=None, fontClass=None)
self.glyphMutators = {}
self.ufoVersion = 2 # target UFO version
self.roundGeometry = False
self.infoMutator = None
self.kerningMutator = None
self.default = None # name of the default master
self.defaultLoc = None
self.ufoVersion = 2
self.fonts = {}
def process(self):
# make the instances
self._loadFonts()
self._buildMutators()
for instanceDescriptor in self.instances:
if instanceDescriptor.path is not None:
self.makeInstance(instanceDescriptor)
def _buildMutators(self):
infoItems = []
kerningItems = []
glyphItems = {}
for sourceDescriptor in self.sources:
loc = Location(sourceDescriptor.location)
f = self.fonts[sourceDescriptor.name]
infoItems.append((loc, self.mathInfoClass(f.info)))
kerningItems.append((loc, self.mathKerningClass(f.kerning, f.groups)))
for g in f:
if g.name in sourceDescriptor.mutedGlyphNames:
continue
if not g.name in glyphItems:
glyphItems[g.name] = []
glyphItems[g.name].append((loc, self.mathGlyphClass(g)))
bias, self.infoMutator = buildMutator(infoItems, bias=self.defaultLoc)
bias, self.kerningMutator = buildMutator(kerningItems, bias=self.defaultLoc)
for name, items in glyphItems.items():
bias, self.glyphMutators[name] = buildMutator(items, bias=self.defaultLoc)
def _loadFonts(self):
# find the default candidate based on the info flag
defaultCandidate = None
for sourceDescriptor in self.sources:
if not sourceDescriptor.name in self.fonts:
self.fonts[sourceDescriptor.name] = self._instantiateFont(sourceDescriptor.path)
if sourceDescriptor.copyInfo:
# we choose you!
defaultCandidate = sourceDescriptor
# 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:
print("c", 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 the UFO for this instance
if not os.path.exists(os.path.dirname(instanceDescriptor.path)):
os.makedirs(os.path.dirname(instanceDescriptor.path))
font = self._instantiateFont(None)
# make fonty things here
loc = Location(instanceDescriptor.location)
# calculated info
if instanceDescriptor.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
if instanceDescriptor.kerning:
kerning = self.kerningMutator.makeInstance(loc)
kerning.extractKerning(font)
# 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 name, glyphMutator in self.glyphMutators.items():
if name in instanceDescriptor.glyphs.keys():
glyphData = instanceDescriptor.glyphs[name]
else:
glyphData = {}
# {'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}
font.newGlyph(name)
font[name].clear()
if glyphData.get('mute', False):
# mute this glyph, skip
print("\tmuted: %s in %s"%(name, instanceDescriptor.name))
continue
glyphInstanceLocation = Location(glyphData.get("instanceLocation", instanceDescriptor.location))
glyphInstanceUnicode = glyphData.get("unicodeValue", font[name].unicode)
note = glyphData.get("note")
if note:
font[name] = note
masters = glyphData.get("masters", None)
if masters:
items = []
for glyphMaster in masters:
sourceGlyphFont = glyphMaster.get("font")
sourceGlyphName = glyphMaster.get("glyphName", name)
# print("using", sourceGlyphName)
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(name)
font[name].clear()
if self.roundGeometry:
try:
glyphInstanceObject = glyphInstanceObject.round()
except AttributeError:
pass
try:
glyphInstanceObject.extractGlyph(font[name], onlyGeometry=True)
except TypeError:
# this causes ruled glyphs to end up in the wrong glyphname
# but defcon2 objects don't support it
pPen = font[name].getPointPen()
font[name].clear()
glyphInstanceObject.drawPoints(pPen)
font[name].width = glyphInstanceObject.width
# save
font.save(instanceDescriptor.path, self.ufoVersion)
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, targetInfo, sourceInfo):
""" 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 1"
# 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.process()
#print(d.defaultCandidate)
# for w in range(0, 1000, 100):
# r = d.makeGlyph("glyphOne", dict(pop=w))
# print(w, r.width)
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)