573 lines
22 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, RuleDescriptor, processRules
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
#print("ufoProcessor reloads")
"""
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.
"""
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
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 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=readerClass, writerClass=writerClass, fontClass=fontClass)
self.ufoVersion = ufoVersion # target UFO version
self.roundGeometry = False
self._glyphMutators = {}
self._infoMutator = None
self._kerningMutator = None
self.fonts = {}
self.glyphNames = [] # list of all glyphnames
self.processRules = True
def generateUFO(self, processRules=True):
# makes the instances
# option to execute the rules
self.checkAxes()
self.checkDefault()
self.loadFonts()
for instanceDescriptor in self.instances:
if instanceDescriptor.path is None:
continue
font = self.makeInstance(instanceDescriptor, processRules)
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
if not glyphName in f:
# log this>
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
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())
self.glyphNames = list(names)
def makeInstance(self, instanceDescriptor, doRules=False):
""" 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))
try:
uniValue = glyphMutator[()][0].unicodes[0]
except IndexError:
uniValue = None
glyphInstanceUnicode = glyphData.get("unicodeValue", uniValue)
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, 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
font[glyphName].unicode = glyphInstanceUnicode
if doRules:
resultNames = processRules(self.rules, loc, self.glyphNames)
for oldName, newName in zip(self.glyphNames, resultNames):
if oldName != newName:
swapGlyphNames(font, oldName, newName)
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
g.unicode = 200 + step
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.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 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()
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"
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)
testSwap(docPath)