fonttools/Lib/robofab/objects/objectsFL.py
Erik van Blokland 06fc7e9101 Seems like a typo error.
git-svn-id: http://svn.robofab.com/trunk@175 b5fa9d6c-a76f-4ffd-b3cb-f825fc41095c
2009-03-24 14:40:03 +00:00

3097 lines
94 KiB
Python
Executable File

"""UFO implementation for the objects as used by FontLab 4.5 and higher"""
from FL import *
from robofab.tools.toolsFL import GlyphIndexTable, NewGlyph
from robofab.objects.objectsBase import BaseFont, BaseGlyph, BaseContour, BaseSegment,\
BasePoint, BaseBPoint, BaseAnchor, BaseGuide, BaseComponent, BaseKerning, BaseInfo, BaseFeatures, BaseGroups, BaseLib,\
roundPt, addPt, _box,\
MOVE, LINE, CORNER, CURVE, QCURVE, OFFCURVE,\
relativeBCPIn, relativeBCPOut, absoluteBCPIn, absoluteBCPOut,\
BasePostScriptFontHintValues, postScriptHintDataLibKey, BasePostScriptGlyphHintValues
from fontTools.misc import arrayTools
from robofab.pens.flPen import FLPointPen
from robofab import RoboFabError
import os
from robofab.plistlib import Data, Dict, readPlist, writePlist
from StringIO import StringIO
from robofab import ufoLib
from warnings import warn
import datetime
from robofab.tools.fontlabFeatureSplitter import splitFeaturesForFontLab
try:
set
except NameError:
from sets import Set as set
# local encoding
if os.name in ["mac", "posix"]:
LOCAL_ENCODING = "macroman"
else:
LOCAL_ENCODING = "latin-1"
_IN_UFO_EXPORT = False
# a list of attributes that are to be copied when copying a glyph.
# this is used by glyph.copy and font.insertGlyph
GLYPH_COPY_ATTRS = [
"name",
"width",
"unicodes",
"note",
"lib",
]
# Generate Types
PC_TYPE1 = 'pctype1'
PC_MM = 'pcmm'
PC_TYPE1_ASCII = 'pctype1ascii'
PC_MM_ASCII = 'pcmmascii'
UNIX_ASCII = 'unixascii'
MAC_TYPE1 = 'mactype1'
OTF_CFF = 'otfcff'
OTF_TT = 'otfttf'
MAC_TT = 'macttf'
MAC_TT_DFONT = 'macttdfont'
# doc for these functions taken from: http://dev.fontlab.net/flpydoc/
# internal name (FontLab name, extension)
_flGenerateTypes ={ PC_TYPE1 : (ftTYPE1, 'pfb'), # PC Type 1 font (binary/PFB)
PC_MM : (ftTYPE1_MM, 'mm'), # PC MultipleMaster font (PFB)
PC_TYPE1_ASCII : (ftTYPE1ASCII, 'pfa'), # PC Type 1 font (ASCII/PFA)
PC_MM_ASCII : (ftTYPE1ASCII_MM, 'mm'), # PC MultipleMaster font (ASCII/PFA)
UNIX_ASCII : (ftTYPE1ASCII, 'pfa'), # UNIX ASCII font (ASCII/PFA)
OTF_TT : (ftTRUETYPE, 'ttf'), # PC TrueType/TT OpenType font (TTF)
OTF_CFF : (ftOPENTYPE, 'otf'), # PS OpenType (CFF-based) font (OTF)
MAC_TYPE1 : (ftMACTYPE1, 'suit'), # Mac Type 1 font (generates suitcase and LWFN file, optionally AFM)
MAC_TT : (ftMACTRUETYPE, 'ttf'), # Mac TrueType font (generates suitcase)
MAC_TT_DFONT : (ftMACTRUETYPE_DFONT, 'dfont'), # Mac TrueType font (generates suitcase with resources in data fork)
}
## FL Hint stuff
# this should not be referenced outside of this module
# since we may be changing the way this works in the future.
"""
FontLab implementation of psHints objects
Most of the FL methods relating to ps hints return a list of 16 items.
These values are for the 16 corners of a 4 axis multiple master.
The odd thing is that even single masters get these 16 values.
RoboFab doesn't access the MM masters, so by default, the psHints
object only works with the first element. If you want to access the other
values in the list, give a value between 0 and 15 for impliedMasterIndex
when creating the object.
From the FontLab docs:
http://dev.fontlab.net/flpydoc/
blue_fuzz
blue_scale
blue_shift
blue_values_num(integer) - number of defined blue values
blue_values[integer[integer]] - two-dimentional array of BlueValues
master index is top-level index
other_blues_num(integer) - number of defined OtherBlues values
other_blues[integer[integer]] - two-dimentional array of OtherBlues
master index is top-level index
family_blues_num(integer) - number of FamilyBlues records
family_blues[integer[integer]] - two-dimentional array of FamilyBlues
master index is top-level index
family_other_blues_num(integer) - number of FamilyOtherBlues records
family_other_blues[integer[integer]] - two-dimentional array of FamilyOtherBlues
master index is top-level index
force_bold[integer] - list of Force Bold values, one for
each master
stem_snap_h_num(integer)
stem_snap_h
stem_snap_v_num(integer)
stem_snap_v
"""
class PostScriptFontHintValues(BasePostScriptFontHintValues):
""" Wrapper for font-level PostScript hinting information for FontLab.
Blues values, stem values.
"""
def __init__(self, font=None, impliedMasterIndex=0):
self._object = font.naked()
self._masterIndex = impliedMasterIndex
def copy(self):
from robofab.objects.objectsRF import PostScriptFontHintValues as _PostScriptFontHintValues
return _PostScriptFontHintValues(data=self.asDict())
class PostScriptGlyphHintValues(BasePostScriptGlyphHintValues):
""" Wrapper for glyph-level PostScript hinting information for FontLab.
vStems, hStems.
"""
def __init__(self, glyph=None):
self._object = glyph.naked()
def copy(self):
from robofab.objects.objectsRF import PostScriptGlyphHintValues as _PostScriptGlyphHintValues
return _PostScriptGlyphHintValues(data=self.asDict())
def _hintObjectsToList(self, item):
data = []
done = []
for hint in item:
p = (hint.position, hint.width)
if p in done:
continue
data.append(p)
done.append(p)
data.sort()
return data
def _listToHintObjects(self, item):
hints = []
done = []
for pos, width in item:
if (pos, width) in done:
# we don't want to set duplicates
continue
hints.append(Hint(pos, width))
done.append((pos,width))
return hints
def _getVHints(self):
return self._hintObjectsToList(self._object.vhints)
def _setVHints(self, values):
# 1 = horizontal hints and links,
# 2 = vertical hints and links
# 3 = all hints and links
self._object.RemoveHints(2)
if values is None:
# just clearing it then
return
values.sort()
for hint in self._listToHintObjects(values):
self._object.vhints.append(hint)
def _getHHints(self):
return self._hintObjectsToList(self._object.hhints)
def _setHHints(self, values):
# 1 = horizontal hints and links,
# 2 = vertical hints and links
# 3 = all hints and links
self._object.RemoveHints(1)
if values is None:
# just clearing it then
return
values.sort()
for hint in self._listToHintObjects(values):
self._object.hhints.append(hint)
vHints = property(_getVHints, _setVHints, doc="postscript hints: vertical hint zones")
hHints = property(_getHHints, _setHHints, doc="postscript hints: horizontal hint zones")
def _glyphHintsToDict(glyph):
data = {}
##
## horizontal and vertical hints
##
# glyph.hhints and glyph.vhints returns a list of Hint objects.
# Hint objects have position and width attributes.
data['hHints'] = []
for index in xrange(len(glyph.hhints)):
hint = glyph.hhints[index]
data['hHints'].append((hint.position, hint.width))
if not data['hHints']:
del data['hHints']
data['vHints'] = []
for index in xrange(len(glyph.vhints)):
hint = glyph.vhints[index]
data['vHints'].append((hint.position, hint.width))
if not data['vHints']:
del data['vHints']
##
## horizontal and vertical links
##
# glyph.hlinks and glyph.vlinks returns a list of Link objects.
# Link objects have node1 and node2 attributes.
data['hLinks'] = []
for index in xrange(len(glyph.hlinks)):
link = glyph.hlinks[index]
d = { 'node1' : link.node1,
'node2' : link.node2,
}
data['hLinks'].append(d)
if not data['hLinks']:
del data['hLinks']
data['vLinks'] = []
for index in xrange(len(glyph.vlinks)):
link = glyph.vlinks[index]
d = { 'node1' : link.node1,
'node2' : link.node2,
}
data['vLinks'].append(d)
if not data['vLinks']:
del data['vLinks']
##
## replacement table
##
# glyph.replace_table returns a list of Replace objects.
# Replace objects have type and index attributes.
data['replaceTable'] = []
for index in xrange(len(glyph.replace_table)):
replace = glyph.replace_table[index]
d = { 'type' : replace.type,
'index' : replace.index,
}
data['replaceTable'].append(d)
if not data['replaceTable']:
del data['replaceTable']
# XXX
# need to support glyph.instructions and glyph.hdmx?
# they are not documented very well.
return data
def _dictHintsToGlyph(glyph, aDict):
# clear existing hints first
# RemoveHints requires an "integer mode" argument
# but it is not documented. from some simple experiments
# i deduced that
# 1 = horizontal hints and links,
# 2 = vertical hints and links
# 3 = all hints and links
glyph.RemoveHints(3)
##
## horizontal and vertical hints
##
if aDict.has_key('hHints'):
for d in aDict['hHints']:
glyph.hhints.append(Hint(d[0], d[1]))
if aDict.has_key('vHints'):
for d in aDict['vHints']:
glyph.vhints.append(Hint(d[0], d[1]))
##
## horizontal and vertical links
##
if aDict.has_key('hLinks'):
for d in aDict['hLinks']:
glyph.hlinks.append(Link(d['node1'], d['node2']))
if aDict.has_key('vLinks'):
for d in aDict['vLinks']:
glyph.vlinks.append(Link(d['node1'], d['node2']))
##
## replacement table
##
if aDict.has_key('replaceTable'):
for d in aDict['replaceTable']:
glyph.replace_table.append(Replace(d['type'], d['index']))
# FL Node Types
flMOVE = 17
flLINE = 1
flCURVE = 35
flOFFCURVE = 65
flSHARP = 0
# I have no idea what the difference between
# "smooth" and "fixed" is, but booth values
# are returned by FL
flSMOOTH = 4096
flFIXED = 12288
_flToRFSegmentDict = { flMOVE : MOVE,
flLINE : LINE,
flCURVE : CURVE,
flOFFCURVE : OFFCURVE
}
_rfToFLSegmentDict = {}
for k, v in _flToRFSegmentDict.items():
_rfToFLSegmentDict[v] = k
def _flToRFSegmentType(segmentType):
return _flToRFSegmentDict[segmentType]
def _rfToFLSegmentType(segmentType):
return _rfToFLSegmentDict[segmentType]
def _scalePointFromCenter((pointX, pointY), (scaleX, scaleY), (centerX, centerY)):
ogCenter = (centerX, centerY)
scaledCenter = (centerX * scaleX, centerY * scaleY)
shiftVal = (scaledCenter[0] - ogCenter[0], scaledCenter[1] - ogCenter[1])
scaledPointX = (pointX * scaleX) - shiftVal[0]
scaledPointY = (pointY * scaleY) - shiftVal[1]
return (scaledPointX, scaledPointY)
# Nostalgia code:
def CurrentFont():
"""Return a RoboFab font object for the currently selected font."""
f = fl.font
if f is not None:
return RFont(fl.font)
return None
def CurrentGlyph():
"""Return a RoboFab glyph object for the currently selected glyph."""
currentPath = fl.font.file_name
if fl.glyph is None:
return None
glyphName = fl.glyph.name
currentFont = None
# is this font already loaded as an RFont?
for font in AllFonts():
# ugh this won't work because AllFonts sees non RFonts as well....
if font.path == currentPath:
currentFont = font
break
xx = currentFont[glyphName]
#print "objectsFL.CurrentGlyph parent for %d"% id(xx), xx.getParent()
return xx
def OpenFont(path=None, note=None):
"""Open a font from a path."""
if path == None:
from robofab.interface.all.dialogs import GetFile
path = GetFile(note)
if path:
if path[-4:].lower() in ['.vfb', '.VFB', '.bak', '.BAK']:
f = Font(path)
fl.Add(f)
return RFont(f)
return None
def NewFont(familyName=None, styleName=None):
"""Make a new font"""
from FL import fl, Font
f = Font()
fl.Add(f)
rf = RFont(f)
if familyName is not None:
rf.info.familyName = familyName
if styleName is not None:
rf.info.styleName = styleName
return rf
def AllFonts():
"""Return a list of all open fonts."""
fontCount = len(fl)
all = []
for index in xrange(fontCount):
naked = fl[index]
all.append(RFont(naked))
return all
from robofab.world import CurrentGlyph
def getGlyphFromMask(g):
"""Get a Fab glyph object for the data in the mask layer."""
from robofab.objects.objectsFL import RGlyph as FL_RGlyph
from robofab.objects.objectsRF import RGlyph as RF_RGlyph
n = g.naked()
mask = n.mask
fg = FL_RGlyph(mask)
rf = RF_RGlyph()
pen = rf.getPointPen()
fg.drawPoints(pen)
rf.width = g.width # can we get to the mask glyph width without flipping the UI?
return rf
def setMaskToGlyph(maskGlyph, targetGlyph, clear=True):
"""Set the maskGlyph as a mask layer in targetGlyph.
maskGlyph is a FontLab or RoboFab RGlyph, orphaned or not.
targetGlyph is a FontLab RGLyph.
clear is a bool. False: keep the existing mask data, True: clear the existing mask data.
"""
from robofab.objects.objectsFL import RGlyph as FL_RGlyph
from FL import Glyph as FL_NakedGlyph
flGlyph = FL_NakedGlyph() # new, orphaned FL glyph
wrapped = FL_RGlyph(flGlyph) # rf wrapper for FL glyph
if not clear:
# copy the existing mask data first
existingMask = getGlyphFromMask(targetGlyph)
pen = wrapped.getPointPen() # get a pen for the wrapper
existingMask.drawPoints(pen)
pen = wrapped.getPointPen() # get a pen for the wrapper
maskGlyph.drawPoints(pen) # draw the data
targetGlyph.naked().mask = wrapped .naked()
targetGlyph.update()
# the lib getter and setter are shared by RFont and RGlyph
def _get_lib(self):
data = self._object.customdata
if data:
f = StringIO(data)
try:
pList = readPlist(f)
except: # XXX ugh, plistlib can raise lots of things
# Anyway, customdata does not contain valid plist data,
# but we don't need to toss it!
pList = {"org.robofab.fontlab.customdata": Data(data)}
else:
pList = {}
# pass it along to the lib object
l = RLib(pList)
l.setParent(self)
return l
def _set_lib(self, aDict):
l = RLib({})
l.setParent(self)
l.update(aDict)
def _normalizeLineEndings(s):
return s.replace("\r\n", "\n").replace("\r", "\n")
class RFont(BaseFont):
"""RoboFab UFO wrapper for FL Font object"""
_title = "FLFont"
def __init__(self, font=None):
BaseFont.__init__(self)
if font is None:
from FL import fl, Font
# rather than raise an error we could just start a new font.
font = Font()
fl.Add(font)
#raise RoboFabError, "RFont: there's nothing to wrap!?"
self._object = font
self._lib = {}
self._supportHints = True
self.psHints = PostScriptFontHintValues(self)
self.psHints.setParent(self)
def keys(self):
keys = {}
for glyph in self._object.glyphs:
glyphName = glyph.name
if glyphName in keys:
n = 1
while ("%s#%s" % (glyphName, n)) in keys:
n += 1
newGlyphName = "%s#%s" % (glyphName, n)
print "RoboFab encountered a duplicate glyph name, renaming %r to %r" % (glyphName, newGlyphName)
glyphName = newGlyphName
glyph.name = glyphName
keys[glyphName] = None
return keys.keys()
def has_key(self, glyphName):
glyph = self._object[glyphName]
if glyph is None:
return False
else:
return True
__contains__ = has_key
def __setitem__(self, glyphName, glyph):
self._object[glyphName] = glyph.naked()
def __cmp__(self, other):
if not hasattr(other, '_object'):
return -1
return self._compare(other)
# if self._object.file_name == other._object.file_name:
# # so, names match.
# # this will falsely identify two distinct "Untitled"
# # let's check some more
# return 0
# else:
# return -1
# def _get_psHints(self):
# h = PostScriptFontHintValues(self)
# h.setParent(self)
# return h
#
# psHints = property(_get_psHints, doc="font level postscript hint data")
def _get_info(self):
return RInfo(self._object)
info = property(_get_info, doc="font info object")
def _get_features(self):
return RFeatures(self._object)
features = property(_get_features, doc="features object")
def _get_kerning(self):
kerning = {}
f = self._object
for g in f.glyphs:
for p in g.kerning:
try:
key = (g.name, f[p.key].name)
kerning[key] = p.value
except AttributeError: pass #catch for TT exception
rk = RKerning(kerning)
rk.setParent(self)
return rk
kerning = property(_get_kerning, doc="a kerning object")
def _set_groups(self, aDict):
g = RGroups({})
g.setParent(self)
g.update(aDict)
def _get_groups(self):
groups = {}
for i in self._object.classes:
# test to make sure that the class is properly formatted
if i.find(':') == -1:
continue
key = i.split(':')[0]
value = i.split(':')[1].lstrip().split(' ')
groups[key] = value
rg = RGroups(groups)
rg.setParent(self)
return rg
groups = property(_get_groups, _set_groups, doc="a group object")
lib = property(_get_lib, _set_lib, doc="font lib object")
#
# attributes
#
def _get_fontIndex(self):
# find the index of the font
# by comparing the file_name
# to all open fonts. if the
# font has no file_name, meaning
# it is a new, unsaved font,
# return the index of the first
# font with no file_name.
selfFileName = self._object.file_name
fontCount = len(fl)
for index in xrange(fontCount):
other = fl[index]
if other.file_name == selfFileName:
return index
fontIndex = property(_get_fontIndex, doc="the fontindex for this font")
def _get_path(self):
return self._object.file_name
path = property(_get_path, doc="path to the font")
def _get_fileName(self):
if self.path is None:
return None
return os.path.split(self.path)
fileName = property(_get_fileName, doc="the font's file name")
def _get_selection(self):
# return a list of glyph names for glyphs selected in the font window
l=[]
for i in range(len(self._object.glyphs)):
if fl.Selected(i) == 1:
l.append(self._object[i].name)
return l
def _set_selection(self, list):
fl.Unselect()
for i in list:
fl.Select(i)
selection = property(_get_selection, _set_selection, doc="the glyph selection in the font window")
def _makeGlyphlist(self):
# To allow iterations through Font.glyphs. Should become really big in fonts with lotsa letters.
gl = []
for c in self:
gl.append(c)
return gl
def _get_glyphs(self):
return self._makeGlyphlist()
glyphs = property(_get_glyphs, doc="A list of all glyphs in the font, to allow iterations through Font.glyphs")
def update(self):
"""Don't forget to update the font when you are done."""
fl.UpdateFont(self.fontIndex)
def save(self, path=None):
"""Save the font, path is required."""
if not path:
if not self._object.file_name:
raise RoboFabError, "No destination path specified."
else:
path = self._object.file_name
fl.Save(self.fontIndex, path)
def close(self, save=False):
"""Close the font, saving is optional."""
if save:
self.save()
else:
self._object.modified = 0
fl.Close(self.fontIndex)
def getGlyph(self, glyphName):
# XXX may need to become private
flGlyph = self._object[glyphName]
if flGlyph is not None:
glyph = RGlyph(flGlyph)
glyph.setParent(self)
return glyph
return self.newGlyph(glyphName)
def newGlyph(self, glyphName, clear=True):
"""Make a new glyph"""
#if generate:
# g = GenerateGlyph(self._object, glyphName, replace=clear)
#else:
g = NewGlyph(self._object, glyphName, clear)
return RGlyph(g)
def insertGlyph(self, glyph, as=None):
"""Returns a new glyph that has been inserted into the font.
as = another glyphname if you want to insert as with that."""
from robofab.objects.objectsRF import RFont as _RFont
from robofab.objects.objectsRF import RGlyph as _RGlyph
oldGlyph = glyph
if as is None:
name = oldGlyph.name
else:
name = as
# clear the destination glyph if it exists.
if self.has_key(name):
self[name].clear()
# get the parent for the glyph
otherFont = oldGlyph.getParent()
# in some cases we will use the native
# FL method for appending a glyph.
useNative = True
testingNative = True
while testingNative:
# but, maybe it is an orphan glyph.
# in that case we should not use the native method.
if otherFont is None:
useNative = False
testingNative = False
# or maybe the glyph is coming from a NoneLab font
if otherFont is not None:
if isinstance(otherFont, _RFont):
useNative = False
testingNative = False
# but, it could be a copied FL glyph
# which is a NoneLab glyph that
# has a FontLab font as the parent
elif isinstance(otherFont, RFont):
useNative = False
testingNative = False
# or, maybe the glyph is being replaced, in which
# case the native method should not be used
# since FL will destroy any references to the glyph
if self.has_key(name):
useNative = False
testingNative = False
# if the glyph contains components the native
# method should not be used since FL does
# not reference glyphs in components by
# name, but by index (!!!).
if len(oldGlyph.components) != 0:
useNative = False
testingNative = False
testingNative = False
# finally, insert the glyph.
if useNative:
font = self.naked()
otherFont = oldGlyph.getParent().naked()
self.naked().glyphs.append(otherFont[name])
newGlyph = self.getGlyph(name)
else:
newGlyph = self.newGlyph(name)
newGlyph.appendGlyph(oldGlyph)
for attr in GLYPH_COPY_ATTRS:
if attr == "name":
value = name
else:
value = getattr(oldGlyph, attr)
setattr(newGlyph, attr, value)
if self._supportHints:
# now we need to transfer the hints from
# the old glyph to the new glyph. we'll do this
# via the dict to hint functions.
hintDict = {}
# if the glyph is a NoneLab glyph, then we need
# to extract the ps hints from the lib
if isinstance(oldGlyph, _RGlyph):
hintDict = oldGlyph.lib.get(postScriptHintDataLibKey, {})
# otherwise we need to extract the hint dict from the glyph
else:
hintDict = _glyphHintsToDict(oldGlyph.naked())
# now apply the hint data
if hintDict:
_dictHintsToGlyph(newGlyph.naked(), hintDict)
# delete any remaining hint data from the glyph lib
if newGlyph.lib.has_key(postScriptHintDataLibKey):
del newGlyph.lib[postScriptHintDataLibKey]
return newGlyph
def removeGlyph(self, glyphName):
"""remove a glyph from the font"""
index = self._object.FindGlyph(glyphName)
if index != -1:
del self._object.glyphs[index]
#
# opentype
#
def getOTClasses(self):
"""Return all OpenType classes as a dict. Relies on properly formatted classes."""
classes = {}
c = self._object.ot_classes
if c is None:
return classes
c = c.replace('\r', '').replace('\n', '').split(';')
for i in c:
if i.find('=') != -1:
value = []
i = i.replace(' = ', '=')
name = i.split('=')[0]
v = i.split('=')[1].replace('[', '').replace(']', '').split(' ')
#catch double spaces?
for j in v:
if len(j) > 0:
value.append(j)
classes[name] = value
return classes
def setOTClasses(self, dict):
"""Set all OpenType classes."""
l = []
for i in dict.keys():
l.append(''.join([i, ' = [', ' '.join(dict[i]), '];']))
self._object.ot_classes = '\n'.join(l)
def getOTClass(self, name):
"""Get a specific OpenType class."""
classes = self.getOTClasses()
return classes[name]
def setOTClass(self, name, list):
"""Set a specific OpenType class."""
classes = self.getOTClasses()
classes[name] = list
self.setOTClasses(classes)
def getOTFeatures(self):
"""Return all OpenType features as a dict keyed by name.
The value is a string of the text of the feature."""
features = {}
for i in self._object.features:
v = []
for j in i.value.replace('\r', '\n').split('\n'):
if j.find(i.tag) == -1:
v.append(j)
features[i.tag] = '\n'.join(v)
return features
def setOTFeatures(self, dict):
"""Set all OpenType features in the font."""
features= {}
for i in dict.keys():
f = []
f.append('feature %s {'%i)
f.append(dict[i])
f.append('} %s;'%i)
features[i] = '\n'.join(f)
self._object.features.clean()
for i in features.keys():
self._object.features.append(Feature(i, features[i]))
def getOTFeature(self, name):
"""return a specific OpenType feature."""
features = self.getOTFeatures()
return features[name]
def setOTFeature(self, name, text):
"""Set a specific OpenType feature."""
features = self.getOTFeatures()
features[name] = text
self.setOTFeatures(features)
#
# guides
#
def getVGuides(self):
"""Return a list of wrapped vertical guides in this RFont"""
vguides=[]
for i in range(len(self._object.vguides)):
g = RGuide(self._object.vguides[i], i)
g.setParent(self)
vguides.append(g)
return vguides
def getHGuides(self):
"""Return a list of wrapped horizontal guides in this RFont"""
hguides=[]
for i in range(len(self._object.hguides)):
g = RGuide(self._object.hguides[i], i)
g.setParent(self)
hguides.append(g)
return hguides
def appendHGuide(self, position, angle=0):
"""Append a horizontal guide"""
position = int(round(position))
angle = int(round(angle))
g=Guide(position, angle)
self._object.hguides.append(g)
def appendVGuide(self, position, angle=0):
"""Append a horizontal guide"""
position = int(round(position))
angle = int(round(angle))
g=Guide(position, angle)
self._object.vguides.append(g)
def removeHGuide(self, guide):
"""Remove a horizontal guide."""
pos = (guide.position, guide.angle)
for g in self.getHGuides():
if (g.position, g.angle) == pos:
del self._object.hguides[g.index]
break
def removeVGuide(self, guide):
"""Remove a vertical guide."""
pos = (guide.position, guide.angle)
for g in self.getVGuides():
if (g.position, g.angle) == pos:
del self._object.vguides[g.index]
break
def clearHGuides(self):
"""Clear all horizontal guides."""
self._object.hguides.clean()
def clearVGuides(self):
"""Clear all vertical guides."""
self._object.vguides.clean()
#
# generators
#
def generate(self, outputType, path=None):
"""
generate the font. outputType is the type of font to ouput.
--Ouput Types:
'pctype1' : PC Type 1 font (binary/PFB)
'pcmm' : PC MultipleMaster font (PFB)
'pctype1ascii' : PC Type 1 font (ASCII/PFA)
'pcmmascii' : PC MultipleMaster font (ASCII/PFA)
'unixascii' : UNIX ASCII font (ASCII/PFA)
'mactype1' : Mac Type 1 font (generates suitcase and LWFN file)
'otfcff' : PS OpenType (CFF-based) font (OTF)
'otfttf' : PC TrueType/TT OpenType font (TTF)
'macttf' : Mac TrueType font (generates suitcase)
'macttdfont' : Mac TrueType font (generates suitcase with resources in data fork)
(doc adapted from http://dev.fontlab.net/flpydoc/)
path can be a directory or a directory file name combo:
path="DirectoryA/DirectoryB"
path="DirectoryA/DirectoryB/MyFontName"
if no path is given, the file will be output in the same directory
as the vfb file. if no file name is given, the filename will be the
vfb file name with the appropriate suffix.
"""
outputType = outputType.lower()
if not _flGenerateTypes.has_key(outputType):
raise RoboFabError, "%s output type is not supported"%outputType
flOutputType, suffix = _flGenerateTypes[outputType]
if path is None:
filePath, fileName = os.path.split(self.path)
fileName = fileName.replace('.vfb', '')
else:
if os.path.isdir(path):
filePath = path
fileName = os.path.split(self.path)[1].replace('.vfb', '')
else:
filePath, fileName = os.path.split(path)
if '.' in fileName:
raise RoboFabError, "filename cannot contain periods.", fileName
fileName = '.'.join([fileName, suffix])
finalPath = os.path.join(filePath, fileName)
# generate is (oddly) an application level method
# rather than a font level method. because of this,
# the font must be the current font. so, make it so.
fl.ifont = self.fontIndex
fl.GenerateFont(flOutputType, finalPath)
def writeUFO(self, path=None, doProgress=False, glyphNameToFileNameFunc=None,
doHints=False, doInfo=True, doKerning=True, doGroups=True, doLib=True, doFeatures=True, glyphs=None, formatVersion=2):
from robofab.interface.all.dialogs import ProgressBar, Message
# special glyph name to file name conversion
if glyphNameToFileNameFunc is None:
glyphNameToFileNameFunc = self.getGlyphNameToFileNameFunc()
if glyphNameToFileNameFunc is None:
from robofab.tools.glyphNameSchemes import glyphNameToShortFileName
glyphNameToFileNameFunc = glyphNameToShortFileName
# get a valid path
if not path:
if self.path is None:
Message("Please save this font first before exporting to UFO...")
return
else:
path = ufoLib.makeUFOPath(self.path)
# get the glyphs to export
if glyphs is None:
glyphs = self.keys()
# if the file exists, check the format version.
# if the format version being written is different
# from the format version of the existing UFO
# and only some files are set to be written
# raise an error.
if os.path.exists(path):
if os.path.exists(os.path.join(path, "metainfo.plist")):
reader = ufoLib.UFOReader(path)
existingFormatVersion = reader.formatVersion
if formatVersion != existingFormatVersion:
if False in [doInfo, doKerning, doGroups, doLib, doFeatures, set(glyphs) == set(self.keys())]:
Message("When overwriting an existing UFO with a different format version all files must be written.")
return
# the lib must be written if format version is 1
if not doLib and formatVersion == 1:
Message("The lib must be written when exporting format version 1.")
return
# set up the progress bar
nonGlyphCount = [doInfo, doKerning, doGroups, doLib, doFeatures].count(True)
bar = None
if doProgress:
bar = ProgressBar("Exporting UFO", nonGlyphCount + len(glyphs))
# try writing
try:
writer = ufoLib.UFOWriter(path, formatVersion=formatVersion)
## We make a shallow copy if lib, since we add some stuff for export
## that doesn't need to be retained in memory.
fontLib = dict(self.lib)
# write the font info
if doInfo:
global _IN_UFO_EXPORT
_IN_UFO_EXPORT = True
writer.writeInfo(self.info)
_IN_UFO_EXPORT = False
if bar:
bar.tick()
# write the kerning
if doKerning:
writer.writeKerning(self.kerning.asDict())
if bar:
bar.tick()
# write the groups
if doGroups:
writer.writeGroups(self.groups)
if bar:
bar.tick()
# write the features
if doFeatures:
if formatVersion == 2:
writer.writeFeatures(self.features.text)
else:
self._writeOpenTypeFeaturesToLib(fontLib)
if bar:
bar.tick()
# write the lib
if doLib:
## Always export the postscript font hint values to the lib in format version 1
if formatVersion == 1:
d = self.psHints.asDict()
fontLib[postScriptHintDataLibKey] = d
## Export the glyph order to the lib
glyphOrder = [nakedGlyph.name for nakedGlyph in self.naked().glyphs]
fontLib["org.robofab.glyphOrder"] = glyphOrder
## export the features
if doFeatures and formatVersion == 1:
self._writeOpenTypeFeaturesToLib(fontLib)
if bar:
bar.tick()
writer.writeLib(fontLib)
if bar:
bar.tick()
# write the glyphs
if glyphs:
glyphSet = writer.getGlyphSet(glyphNameToFileNameFunc)
count = nonGlyphCount
for nakedGlyph in self.naked().glyphs:
if nakedGlyph.name not in glyphs:
continue
glyph = RGlyph(nakedGlyph)
if doHints:
hintStuff = _glyphHintsToDict(glyph.naked())
if hintStuff:
glyph.lib[postScriptHintDataLibKey] = hintStuff
glyphSet.writeGlyph(glyph.name, glyph, glyph.drawPoints)
# remove the hint dict from the lib
if doHints and glyph.lib.has_key(postScriptHintDataLibKey):
del glyph.lib[postScriptHintDataLibKey]
if bar and not count % 10:
bar.tick(count)
count = count + 1
glyphSet.writeContents()
# only blindly stop if the user says to
except KeyboardInterrupt:
if bar:
bar.close()
bar = None
# kill the bar
if bar:
bar.close()
def _writeOpenTypeFeaturesToLib(self, fontLib):
# this should only be used for UFO format version 1
flFont = self.naked()
cls = flFont.ot_classes
if cls is not None:
fontLib["org.robofab.opentype.classes"] = _normalizeLineEndings(cls).rstrip() + "\n"
if flFont.features:
features = {}
order = []
for feature in flFont.features:
order.append(feature.tag)
features[feature.tag] = _normalizeLineEndings(feature.value).rstrip() + "\n"
fontLib["org.robofab.opentype.features"] = features
fontLib["org.robofab.opentype.featureorder"] = order
def readUFO(self, path, doProgress=False,
doHints=False, doInfo=True, doKerning=True, doGroups=True, doLib=True, doFeatures=True, glyphs=None):
"""read a .ufo into the font"""
from robofab.pens.flPen import FLPointPen
from robofab.interface.all.dialogs import ProgressBar
# start up the reader
reader = ufoLib.UFOReader(path)
glyphSet = reader.getGlyphSet()
# get a list of glyphs that should be imported
if glyphs is None:
glyphs = glyphSet.keys()
# set up the progress bar
nonGlyphCount = [doInfo, doKerning, doGroups, doLib, doFeatures].count(True)
bar = None
if doProgress:
bar = ProgressBar("Importing UFO", nonGlyphCount + len(glyphs))
# start reading
try:
fontLib = reader.readLib()
# info
if doInfo:
reader.readInfo(self.info)
if bar:
bar.tick()
# glyphs
count = 1
glyphOrder = self._getGlyphOrderFromLib(fontLib, glyphSet)
for glyphName in glyphOrder:
if glyphName not in glyphs:
continue
glyph = self.newGlyph(glyphName, clear=True)
pen = FLPointPen(glyph.naked())
glyphSet.readGlyph(glyphName=glyphName, glyphObject=glyph, pointPen=pen)
if doHints:
hintData = glyph.lib.get(postScriptHintDataLibKey)
if hintData:
_dictHintsToGlyph(glyph.naked(), hintData)
# now that the hints have been extracted from the glyph
# there is no reason to keep the location in the lib.
if glyph.lib.has_key(postScriptHintDataLibKey):
del glyph.lib[postScriptHintDataLibKey]
glyph.update()
if bar and not count % 10:
bar.tick(count)
count = count + 1
# features
if doFeatures:
if reader.formatVersion == 1:
self._readOpenTypeFeaturesFromLib(fontLib)
else:
featureText = reader.readFeatures()
self.features.text = featureText
if bar:
bar.tick()
else:
# remove features stored in the lib
self._readOpenTypeFeaturesFromLib(fontLib, setFeatures=False)
# kerning
if doKerning:
self.kerning.clear()
self.kerning.update(reader.readKerning())
if bar:
bar.tick()
# groups
if doGroups:
self.groups.clear()
self.groups.update(reader.readGroups())
if bar:
bar.tick()
# hints in format version 1
if doHints and reader.formatVersion == 1:
self.psHints._loadFromLib(fontLib)
else:
# remove hint data stored in the lib
if fontLib.has_key(postScriptHintDataLibKey):
del fontLib[postScriptHintDataLibKey]
# lib
if doLib:
self.lib.clear()
self.lib.update(fontLib)
if bar:
bar.tick()
# only blindly stop if the user says to
except KeyboardInterrupt:
bar.close()
bar = None
# kill the bar
if bar:
bar.close()
def _getGlyphOrderFromLib(self, fontLib, glyphSet):
glyphOrder = fontLib.get("org.robofab.glyphOrder")
if glyphOrder is not None:
# no need to keep track if the glyph order in lib once the font is loaded.
del fontLib["org.robofab.glyphOrder"]
glyphNames = []
done = {}
for glyphName in glyphOrder:
if glyphName in glyphSet:
glyphNames.append(glyphName)
done[glyphName] = 1
allGlyphNames = glyphSet.keys()
allGlyphNames.sort()
for glyphName in allGlyphNames:
if glyphName not in done:
glyphNames.append(glyphName)
else:
glyphNames = glyphSet.keys()
glyphNames.sort()
return glyphNames
def _readOpenTypeFeaturesFromLib(self, fontLib, setFeatures=True):
# setFeatures may be False. in this case, this method
# should only clear the data from the lib.
classes = fontLib.get("org.robofab.opentype.classes")
if classes is not None:
del fontLib["org.robofab.opentype.classes"]
if setFeatures:
self.naked().ot_classes = classes
features = fontLib.get("org.robofab.opentype.features")
if features is not None:
order = fontLib.get("org.robofab.opentype.featureorder")
if order is None:
# for UFOs saved without the feature order, do the same as before.
order = features.keys()
order.sort()
else:
del fontLib["org.robofab.opentype.featureorder"]
del fontLib["org.robofab.opentype.features"]
#features = features.items()
orderedFeatures = []
for tag in order:
oneFeature = features.get(tag)
if oneFeature is not None:
orderedFeatures.append((tag, oneFeature))
if setFeatures:
self.naked().features.clean()
for tag, src in orderedFeatures:
self.naked().features.append(Feature(tag, src))
class RGlyph(BaseGlyph):
"""RoboFab wrapper for FL Glyph object"""
_title = "FLGlyph"
def __init__(self, flGlyph):
#BaseGlyph.__init__(self)
if flGlyph is None:
raise RoboFabError, "RGlyph: there's nothing to wrap!?"
self._object = flGlyph
self._lib = {}
self._contours = None
def __getitem__(self, index):
return self.contours[index]
def __delitem__(self, index):
self._object.DeleteContour(index)
self._invalidateContours()
def __len__(self):
return len(self.contours)
lib = property(_get_lib, _set_lib, doc="glyph lib object")
def _invalidateContours(self):
self._contours = None
def _buildContours(self):
self._contours = []
for contourIndex in range(self._object.GetContoursNumber()):
c = RContour(contourIndex)
c.setParent(self)
c._buildSegments()
self._contours.append(c)
#
# attribute handlers
#
def _get_index(self):
return self._object.parent.FindGlyph(self.name)
index = property(_get_index, doc="return the index of the glyph in the font")
def _get_name(self):
return self._object.name
def _set_name(self, value):
self._object.name = value
name = property(_get_name, _set_name, doc="name")
def _get_psName(self):
return self._object.name
def _set_psName(self, value):
self._object.name = value
psName = property(_get_psName, _set_psName, doc="name")
def _get_baseName(self):
return self._object.name.split('.')[0]
baseName = property(_get_baseName, doc="")
def _get_unicode(self):
return self._object.unicode
def _set_unicode(self, value):
self._object.unicode = value
unicode = property(_get_unicode, _set_unicode, doc="unicode")
def _get_unicodes(self):
return self._object.unicodes
def _set_unicodes(self, value):
self._object.unicodes = value
unicodes = property(_get_unicodes, _set_unicodes, doc="unicodes")
def _get_width(self):
return self._object.width
def _set_width(self, value):
value = int(round(value))
self._object.width = value
width = property(_get_width, _set_width, doc="the width")
def _get_box(self):
if not len(self.contours) and not len(self.components):
return (0, 0, 0, 0)
r = self._object.GetBoundingRect()
return (int(round(r.ll.x)), int(round(r.ll.y)), int(round(r.ur.x)), int(round(r.ur.y)))
box = property(_get_box, doc="box of glyph as a tuple (xMin, yMin, xMax, yMax)")
def _get_selected(self):
if fl.Selected(self._object.parent.FindGlyph(self._object.name)):
return 1
else:
return 0
def _set_selected(self, value):
fl.Select(self._object.name, value)
selected = property(_get_selected, _set_selected, doc="Select or deselect the glyph in the font window")
def _get_mark(self):
return self._object.mark
def _set_mark(self, value):
self._object.mark = value
mark = property(_get_mark, _set_mark, doc="mark")
def _get_note(self):
s = self._object.note
if s is None:
return s
return unicode(s, LOCAL_ENCODING)
def _set_note(self, value):
if value is None:
value = ''
if type(value) == type(u""):
value = value.encode(LOCAL_ENCODING)
self._object.note = value
note = property(_get_note, _set_note, doc="note")
def _get_psHints(self):
# get an object representing the postscript zone information
return PostScriptGlyphHintValues(self)
psHints = property(_get_psHints, doc="postscript hint data")
#
# necessary evil
#
def update(self):
"""Don't forget to update the glyph when you are done."""
fl.UpdateGlyph(self._object.parent.FindGlyph(self._object.name))
#
# methods to make RGlyph compatible with FL.Glyph
# ##are these still needed?
#
def GetBoundingRect(self, masterIndex):
"""FL compatibility"""
return self._object.GetBoundingRect(masterIndex)
def GetMetrics(self, masterIndex):
"""FL compatibility"""
return self._object.GetMetrics(masterIndex)
def SetMetrics(self, value, masterIndex):
"""FL compatibility"""
return self._object.SetMetrics(value, masterIndex)
#
# object builders
#
def _get_anchors(self):
return self.getAnchors()
anchors = property(_get_anchors, doc="allow for iteration through glyph.anchors")
def _get_components(self):
return self.getComponents()
components = property(_get_components, doc="allow for iteration through glyph.components")
def _get_contours(self):
if self._contours is None:
self._buildContours()
return self._contours
contours = property(_get_contours, doc="allow for iteration through glyph.contours")
def getAnchors(self):
"""Return a list of wrapped anchors in this RGlyph."""
anchors=[]
for i in range(len(self._object.anchors)):
a = RAnchor(self._object.anchors[i], i)
a.setParent(self)
anchors.append(a)
return anchors
def getComponents(self):
"""Return a list of wrapped components in this RGlyph."""
components=[]
for i in range(len(self._object.components)):
c = RComponent(self._object.components[i], i)
c.setParent(self)
components.append(c)
return components
def getVGuides(self):
"""Return a list of wrapped vertical guides in this RGlyph"""
vguides=[]
for i in range(len(self._object.vguides)):
g = RGuide(self._object.vguides[i], i)
g.setParent(self)
vguides.append(g)
return vguides
def getHGuides(self):
"""Return a list of wrapped horizontal guides in this RGlyph"""
hguides=[]
for i in range(len(self._object.hguides)):
g = RGuide(self._object.hguides[i], i)
g.setParent(self)
hguides.append(g)
return hguides
#
# tools
#
def getPointPen(self):
self._invalidateContours()
# Now just don't muck with glyph.contours before you're done drawing...
return FLPointPen(self)
def appendComponent(self, baseGlyph, offset=(0, 0), scale=(1, 1)):
"""Append a component to the glyph. x and y are optional offset values"""
offset = roundPt((offset[0], offset[1]))
p = FLPointPen(self.naked())
xx, yy = scale
dx, dy = offset
p.addComponent(baseGlyph, (xx, 0, 0, yy, dx, dy))
def appendAnchor(self, name, position):
"""Append an anchor to the glyph"""
value = roundPt((position[0], position[1]))
anchor = Anchor(name, value[0], value[1])
self._object.anchors.append(anchor)
def appendHGuide(self, position, angle=0):
"""Append a horizontal guide"""
position = int(round(position))
g = Guide(position, angle)
self._object.hguides.append(g)
def appendVGuide(self, position, angle=0):
"""Append a horizontal guide"""
position = int(round(position))
g = Guide(position, angle)
self._object.vguides.append(g)
def clearComponents(self):
"""Clear all components."""
self._object.components.clean()
def clearAnchors(self):
"""Clear all anchors."""
self._object.anchors.clean()
def clearHGuides(self):
"""Clear all horizontal guides."""
self._object.hguides.clean()
def clearVGuides(self):
"""Clear all vertical guides."""
self._object.vguides.clean()
def removeComponent(self, component):
"""Remove a specific component from the glyph. This only works
if the glyph does not have duplicate components in the same location."""
pos = (component.baseGlyph, component.offset, component.scale)
a = self.getComponents()
found = []
for i in a:
if (i.baseGlyph, i.offset, i.scale) == pos:
found.append(i)
if len(found) > 1:
raise RoboFabError, 'Found more than one possible component to remove'
elif len(found) == 1:
del self._object.components[found[0].index]
else:
raise RoboFabError, 'Component does not exist'
def removeContour(self, index):
"""remove a specific contour from the glyph"""
self._object.DeleteContour(index)
self._invalidateContours()
def removeAnchor(self, anchor):
"""Remove a specific anchor from the glyph. This only works
if the glyph does not have anchors with duplicate names
in exactly the same location with the same mark."""
pos = (anchor.name, anchor.position, anchor.mark)
a = self.getAnchors()
found = []
for i in a:
if (i.name, i.position, i.mark) == pos:
found.append(i)
if len(found) > 1:
raise RoboFabError, 'Found more than one possible anchor to remove'
elif len(found) == 1:
del self._object.anchors[found[0].index]
else:
raise RoboFabError, 'Anchor does not exist'
def removeHGuide(self, guide):
"""Remove a horizontal guide."""
pos = (guide.position, guide.angle)
for g in self.getHGuides():
if (g.position, g.angle) == pos:
del self._object.hguides[g.index]
break
def removeVGuide(self, guide):
"""Remove a vertical guide."""
pos = (guide.position, guide.angle)
for g in self.getVGuides():
if (g.position, g.angle) == pos:
del self._object.vguides[g.index]
break
def center(self, padding=None):
"""Equalise sidebearings, set to padding if wanted."""
left = self.leftMargin
right = self.rightMargin
if padding:
e_left = e_right = padding
else:
e_left = (left + right)/2
e_right = (left + right) - e_left
self.leftMargin= e_left
self.rightMargin= e_right
def removeOverlap(self):
"""Remove overlap"""
self._object.RemoveOverlap()
self._invalidateContours()
def decompose(self):
"""Decompose all components"""
self._object.Decompose()
self._invalidateContours()
##broken!
#def removeHints(self):
# """Remove the hints."""
# self._object.RemoveHints()
def autoHint(self):
"""Automatically generate type 1 hints."""
self._object.Autohint()
def move(self, (x, y), contours=True, components=True, anchors=True):
"""Move a glyph's items that are flagged as True"""
x, y = roundPt((x, y))
self._object.Shift(Point(x, y))
for c in self.getComponents():
c.move((x, y))
for a in self.getAnchors():
a.move((x, y))
def clear(self, contours=True, components=True, anchors=True, guides=True, hints=True):
"""Clear all items marked as true from the glyph"""
if contours:
self._object.Clear()
self._invalidateContours()
if components:
self._object.components.clean()
if anchors:
self._object.anchors.clean()
if guides:
self._object.hguides.clean()
self._object.vguides.clean()
if hints:
# RemoveHints requires an "integer mode" argument
# but it is not documented. from some simple experiments
# i deduced that
# 1 = horizontal hints and links,
# 2 = vertical hints and links
# 3 = all hints and links
self._object.RemoveHints(3)
#
# special treatment for GlyphMath support in FontLab
#
def _getMathDestination(self):
from robofab.objects.objectsRF import RGlyph as _RGlyph
return _RGlyph()
def copy(self, aParent=None):
"""Make a copy of this glyph.
Note: the copy is not a duplicate fontlab glyph, but
a RF RGlyph with the same outlines. The new glyph is
not part of the fontlab font in any way. Use font.appendGlyph(glyph)
to get it in a FontLab glyph again."""
from robofab.objects.objectsRF import RGlyph as _RGlyph
newGlyph = _RGlyph()
newGlyph.appendGlyph(self)
for attr in GLYPH_COPY_ATTRS:
value = getattr(self, attr)
setattr(newGlyph, attr, value)
# hints
doHints = False
parent = self.getParent()
if parent is not None and parent._supportHints:
hintStuff = _glyphHintsToDict(self.naked())
if hintStuff:
newGlyph.lib[postScriptHintDataLibKey] = hintStuff
if aParent is not None:
newGlyph.setParent(aParent)
elif self.getParent() is not None:
newGlyph.setParent(self.getParent())
return newGlyph
def __mul__(self, factor):
return self.copy() *factor
__rmul__ = __mul__
def __sub__(self, other):
return self.copy() - other.copy()
def __add__(self, other):
return self.copy() + other.copy()
class RContour(BaseContour):
"""RoboFab wrapper for non FL contour object"""
_title = "FLContour"
def __init__(self, index):
self._index = index
self._parentGlyph = None
self.segments = []
def __len__(self):
return len(self.points)
def _buildSegments(self):
#######################
# Notes about FL node contour structure
#######################
# for TT curves, FL lists them as seperate nodes:
# [move, off, off, off, line, off, off]
# and, this list is sequential. after the last on curve,
# it is possible (and likely) that there will be more offCurves
# in our segment object, these should be associated with the
# first segment in the contour.
#
# for PS curves, it is a very different scenerio.
# curve nodes contain points:
# [on, off, off]
# and the list is not in sequential order. the first point in
# the list is the on curve and the subsequent points are the off
# curve points leading up to that on curve.
#
# it is very important to remember these structures when trying
# to understand the code below
self.segments = []
offList = []
nodes = self._nakedParent.nodes
for index in range(self._nodeLength):
x = index + self._startNodeIndex
node = nodes[x]
# we do have a loose off curve. deal with it.
if node.type == flOFFCURVE:
offList.append(x)
# we are not dealing with a loose off curve
else:
s = RSegment(x)
s.setParent(self)
# but do we have a collection of loose off curves above?
# if so, apply them to the segment, and clear the list
if len(offList) != 0:
s._looseOffCurve = offList
offList = []
self.segments.append(s)
# do we have some off curves now that the contour is complete?
if len(offList) != 0:
# ugh. apply them to the first segment
self.segments[0]._looseOffCurve = offList
def setParent(self, parentGlyph):
self._parentGlyph = parentGlyph
def getParent(self):
return self._parentGlyph
def _get__nakedParent(self):
return self._parentGlyph.naked()
_nakedParent = property(_get__nakedParent, doc="")
def _get__startNodeIndex(self):
return self._nakedParent.GetContourBegin(self._index)
_startNodeIndex = property(_get__startNodeIndex, doc="")
def _get__nodeLength(self):
return self._nakedParent.GetContourLength(self._index)
_nodeLength = property(_get__nodeLength, doc="")
def _get__lastNodeIndex(self):
return self._startNodeIndex + self._nodeLength - 1
_lastNodeIndex = property(_get__lastNodeIndex, doc="")
def _previousNodeIndex(self, index):
return (index - 1) % self._nodeLength
def _nextNodeIndex(self, index):
return (index + 1) % self._nodeLength
def _getNode(self, index):
return self._nodes[index]
def _get__nodes(self):
nodes = []
for node in self._nakedParent.nodes[self._startNodeIndex:self._startNodeIndex+self._nodeLength-1]:
nodes.append(node)
return nodes
_nodes = property(_get__nodes, doc="")
def _get_points(self):
points = []
for segment in self.segments:
for point in segment.points:
points.append(point)
return points
points = property(_get_points, doc="")
def _get_bPoints(self):
bPoints = []
for segment in self.segments:
bp = RBPoint(segment.index)
bp.setParent(self)
bPoints.append(bp)
return bPoints
bPoints = property(_get_bPoints, doc="")
def _get_index(self):
return self._index
def _set_index(self, index):
if index != self._index:
self._nakedParent.ReorderContour(self._index, index)
# reorder and set the _index of the existing RContour objects
# this will be a better solution than reconstructing all the objects
# segment objects will still, sadly, have to be reconstructed
contourList = self.getParent().contours
contourList.insert(index, contourList.pop(self._index))
for i in range(len(contourList)):
contourList[i]._index = i
contourList[i]._buildSegments()
index = property(_get_index, _set_index, doc="the index of the contour")
def _get_selected(self):
selected = 0
nodes = self._nodes
for node in nodes:
if node.selected == 1:
selected = 1
break
return selected
def _set_selected(self, value):
if value == 1:
self._nakedParent.SelectContour(self._index)
else:
for node in self._nodes:
node.selected = value
selected = property(_get_selected, _set_selected, doc="selection of the contour: 1-selected or 0-unselected")
def appendSegment(self, segmentType, points, smooth=False):
segment = self.insertSegment(index=self._nodeLength, segmentType=segmentType, points=points, smooth=smooth)
return segment
def insertSegment(self, index, segmentType, points, smooth=False):
"""insert a seggment into the contour"""
# do a qcurve insertion
if segmentType == QCURVE:
count = 0
for point in points[:-1]:
newNode = Node(flOFFCURVE, Point(point[0], point[1]))
self._nakedParent.Insert(newNode, self._startNodeIndex + index + count)
count = count + 1
newNode = Node(flLINE, Point(points[-1][0], points[-1][1]))
self._nakedParent.Insert(newNode, self._startNodeIndex + index +len(points) - 1)
# do a regular insertion
else:
onX, onY = points[-1]
newNode = Node(_rfToFLSegmentType(segmentType), Point(onX, onY))
# fix the off curves in case the user is inserting a curve
# but is not specifying off curve points
if segmentType == CURVE and len(points) == 1:
pSeg = self._prevSegment(index)
pOn = pSeg.onCurve
newNode.points[1].Assign(Point(pOn.x, pOn.y))
newNode.points[2].Assign(Point(onX, onY))
for pointIndex in range(len(points[:-1])):
x, y = points[pointIndex]
newNode.points[1 + pointIndex].Assign(Point(x, y))
if smooth:
node.alignment = flSMOOTH
self._nakedParent.Insert(newNode, self._startNodeIndex + index)
self._buildSegments()
return self.segments[index]
def removeSegment(self, index):
"""remove a segment from the contour"""
segment = self.segments[index]
# we have a qcurve. umph.
if segment.type == QCURVE:
indexList = [segment._nodeIndex] + segment._looseOffCurve
indexList.sort()
indexList.reverse()
parent = self._nakedParent
for nodeIndex in indexList:
parent.DeleteNode(nodeIndex)
# we have a more sane structure to follow
else:
# store some info for later
next = self._nextSegment(index)
nextOffA = None
nextOffB = None
nextType = next.type
if nextType != LINE and nextType != MOVE:
pA = next.offCurve[0]
nextOffA = (pA.x, pA.y)
pB = next.offCurve[-1]
nextOffB = (pB.x, pB.y)
nodeIndex = segment._nodeIndex
self._nakedParent.DeleteNode(nodeIndex)
self._buildSegments()
# now we must override FL guessing about offCurves
next = self._nextSegment(index - 1)
nextType = next.type
if nextType != LINE and nextType != MOVE:
pA = next.offCurve[0]
pB = next.offCurve[-1]
pA.x, pA.y = nextOffA
pB.x, pB.y = nextOffB
def reverseContour(self):
"""reverse contour direction"""
self._nakedParent.ReverseContour(self._index)
self._buildSegments()
def setStartSegment(self, segmentIndex):
"""set the first node on the contour"""
self._nakedParent.SetStartNode(self._startNodeIndex + segmentIndex)
self.getParent()._invalidateContours()
self.getParent()._buildContours()
def copy(self, aParent=None):
"""Copy this object -- result is an ObjectsRF flavored object.
There is no way to make this work using FontLab objects.
Copy is mainly used for glyphmath.
"""
raise RoboFabError, "copy() for objectsFL.RContour is not implemented."
class RSegment(BaseSegment):
_title = "FLSegment"
def __init__(self, flNodeIndex):
BaseSegment.__init__(self)
self._nodeIndex = flNodeIndex
self._looseOffCurve = [] #a list of indexes to loose off curve nodes
def _get__node(self):
glyph = self.getParent()._nakedParent
return glyph.nodes[self._nodeIndex]
_node = property(_get__node, doc="")
def _get_qOffCurve(self):
nodes = self.getParent()._nakedParent.nodes
off = []
for x in self._looseOffCurve:
off.append(nodes[x])
return off
_qOffCurve = property(_get_qOffCurve, doc="free floating off curve nodes in the segment")
def _get_index(self):
contour = self.getParent()
return self._nodeIndex - contour._startNodeIndex
index = property(_get_index, doc="")
def _isQCurve(self):
# loose off curves only appear in q curves
if len(self._looseOffCurve) != 0:
return True
return False
def _get_type(self):
if self._isQCurve():
return QCURVE
return _flToRFSegmentType(self._node.type)
def _set_type(self, segmentType):
if self._isQCurve():
raise RoboFabError, 'qcurve point types cannot be changed'
oldNode = self._node
oldType = oldNode.type
oldPointType = _flToRFSegmentType(oldType)
if oldPointType == MOVE:
raise RoboFabError, '%s point types cannot be changed'%oldPointType
if segmentType == MOVE or segmentType == OFFCURVE:
raise RoboFabError, '%s point types cannot be assigned'%oldPointType
if oldPointType == segmentType:
return
oldNode.type = _rfToFLSegmentType(segmentType)
type = property(_get_type, _set_type, doc="")
def _get_smooth(self):
alignment = self._node.alignment
if alignment == flSMOOTH or alignment == flFIXED:
return True
return False
def _set_smooth(self, value):
if value:
self._node.alignment = flSMOOTH
else:
self._node.alignment = flSHARP
smooth = property(_get_smooth, _set_smooth, doc="")
def _get_points(self):
points = []
node = self._node
# gather the off curves
#
# are we dealing with a qCurve? ugh.
# gather the loose off curves
if self._isQCurve():
off = self._qOffCurve
x = 0
for n in off:
p = RPoint(0)
p.setParent(self)
p._qOffIndex = x
points.append(p)
x = x + 1
# otherwise get the points associated with the node
else:
index = 1
for point in node.points[1:]:
p = RPoint(index)
p.setParent(self)
points.append(p)
index = index + 1
# the last point should always be the on curve
p = RPoint(0)
p.setParent(self)
points.append(p)
return points
points = property(_get_points, doc="")
def _get_selected(self):
return self._node.selected
def _set_selected(self, value):
self._node.selected = value
selected = property(_get_selected, _set_selected, doc="")
def move(self, (x, y)):
x, y = roundPt((x, y))
self._node.Shift(Point(x, y))
if self._isQCurve():
qOff = self._qOffCurve
for node in qOff:
node.Shift(Point(x, y))
def copy(self, aParent=None):
"""Copy this object -- result is an ObjectsRF flavored object.
There is no way to make this work using FontLab objects.
Copy is mainly used for glyphmath.
"""
raise RoboFabError, "copy() for objectsFL.RSegment is not implemented."
class RPoint(BasePoint):
_title = "FLPoint"
def __init__(self, pointIndex):
#BasePoint.__init__(self)
self._pointIndex = pointIndex
self._qOffIndex = None
def _get__parentGlyph(self):
return self._parentContour.getParent()
_parentGlyph = property(_get__parentGlyph, doc="")
def _get__parentContour(self):
return self._parentSegment.getParent()
_parentContour = property(_get__parentContour, doc="")
def _get__parentSegment(self):
return self.getParent()
_parentSegment = property(_get__parentSegment, doc="")
def _get__node(self):
if self._qOffIndex is not None:
return self.getParent()._qOffCurve[self._qOffIndex]
return self.getParent()._node
_node = property(_get__node, doc="")
def _get__point(self):
return self._node.points[self._pointIndex]
_point = property(_get__point, doc="")
def _get_x(self):
return self._point.x
def _set_x(self, value):
value = int(round(value))
self._point.x = value
x = property(_get_x, _set_x, doc="")
def _get_y(self):
return self._point.y
def _set_y(self, value):
value = int(round(value))
self._point.y = value
y = property(_get_y, _set_y, doc="")
def _get_type(self):
if self._pointIndex == 0:
# FL store quad contour data as a list of off curves and lines
# (see note in RContour._buildSegments). So, we need to do
# a bit of trickery to return a decent point type.
# if the straight FL node type is off curve, it is a loose
# quad off curve. return that.
tp = _flToRFSegmentType(self._node.type)
if tp == OFFCURVE:
return OFFCURVE
# otherwise we are dealing with an on curve. in this case,
# we attempt to get the parent segment type and return it.
segment = self.getParent()
if segment is not None:
return segment.type
# we must not have a segment, fall back to straight conversion
return tp
return OFFCURVE
type = property(_get_type, doc="")
def _set_selected(self, value):
if self._pointIndex == 0:
self._node.selected = value
def _get_selected(self):
if self._pointIndex == 0:
return self._node.selected
return False
selected = property(_get_selected, _set_selected, doc="")
def move(self, (x, y)):
x, y = roundPt((x, y))
self._point.Shift(Point(x, y))
def scale(self, (x, y), center=(0, 0)):
centerX, centerY = roundPt(center)
point = self._point
point.x, point.y = _scalePointFromCenter((point.x, point.y), (x, y), (centerX, centerY))
def copy(self, aParent=None):
"""Copy this object -- result is an ObjectsRF flavored object.
There is no way to make this work using FontLab objects.
Copy is mainly used for glyphmath.
"""
raise RoboFabError, "copy() for objectsFL.RPoint is not implemented."
class RBPoint(BaseBPoint):
_title = "FLBPoint"
def __init__(self, segmentIndex):
#BaseBPoint.__init__(self)
self._segmentIndex = segmentIndex
def _get__parentSegment(self):
return self.getParent().segments[self._segmentIndex]
_parentSegment = property(_get__parentSegment, doc="")
def _get_index(self):
return self._segmentIndex
index = property(_get_index, doc="")
def _get_selected(self):
return self._parentSegment.selected
def _set_selected(self, value):
self._parentSegment.selected = value
selected = property(_get_selected, _set_selected, doc="")
def copy(self, aParent=None):
"""Copy this object -- result is an ObjectsRF flavored object.
There is no way to make this work using FontLab objects.
Copy is mainly used for glyphmath.
"""
raise RoboFabError, "copy() for objectsFL.RBPoint is not implemented."
class RComponent(BaseComponent):
"""RoboFab wrapper for FL Component object"""
_title = "FLComponent"
def __init__(self, flComponent, index):
BaseComponent.__init__(self)
self._object = flComponent
self._index=index
def _get_index(self):
return self._index
index = property(_get_index, doc="index of component")
def _get_baseGlyph(self):
return self._object.parent.parent[self._object.index].name
baseGlyph = property(_get_baseGlyph, doc="")
def _get_offset(self):
return (int(self._object.delta.x), int(self._object.delta.y))
def _set_offset(self, value):
value = roundPt((value[0], value[1]))
self._object.delta=Point(value[0], value[1])
offset = property(_get_offset, _set_offset, doc="the offset of the component")
def _get_scale(self):
return (self._object.scale.x, self._object.scale.y)
def _set_scale(self, (x, y)):
self._object.scale=Point(x, y)
scale = property(_get_scale, _set_scale, doc="the scale of the component")
def move(self, (x, y)):
"""Move the component"""
x, y = roundPt((x, y))
self._object.delta=Point(self._object.delta.x+x, self._object.delta.y+y)
def decompose(self):
"""Decompose the component"""
self._object.Paste()
def copy(self, aParent=None):
"""Copy this object -- result is an ObjectsRF flavored object.
There is no way to make this work using FontLab objects.
Copy is mainly used for glyphmath.
"""
raise RoboFabError, "copy() for objectsFL.RComponent is not implemented."
class RAnchor(BaseAnchor):
"""RoboFab wrapper for FL Anchor object"""
_title = "FLAnchor"
def __init__(self, flAnchor, index):
BaseAnchor.__init__(self)
self._object = flAnchor
self._index = index
def _get_y(self):
return self._object.y
def _set_y(self, value):
self._object.y = int(round(value))
y = property(_get_y, _set_y, doc="y")
def _get_x(self):
return self._object.x
def _set_x(self, value):
self._object.x = int(round(value))
x = property(_get_x, _set_x, doc="x")
def _get_name(self):
return self._object.name
def _set_name(self, value):
self._object.name = value
name = property(_get_name, _set_name, doc="name")
def _get_mark(self):
return self._object.mark
def _set_mark(self, value):
self._object.mark = value
mark = property(_get_mark, _set_mark, doc="mark")
def _get_index(self):
return self._index
index = property(_get_index, doc="index of the anchor")
def _get_position(self):
return (self._object.x, self._object.y)
def _set_position(self, value):
value = roundPt((value[0], value[1]))
self._object.x=value[0]
self._object.y=value[1]
position = property(_get_position, _set_position, doc="position of the anchor")
class RGuide(BaseGuide):
"""RoboFab wrapper for FL Guide object"""
_title = "FLGuide"
def __init__(self, flGuide, index):
BaseGuide.__init__(self)
self._object = flGuide
self._index = index
def __repr__(self):
# this is a doozy!
parent = "unknown_parent"
parentObject = self.getParent()
if parentObject is not None:
# do we have a font?
try:
parent = parentObject.info.postscriptFullName
except AttributeError:
# or do we have a glyph?
try:
parent = parentObject.name
# we must be an orphan
except AttributeError: pass
return "<Robofab guide wrapper for %s>"%parent
def _get_position(self):
return self._object.position
def _set_position(self, value):
self._object.position = value
position = property(_get_position, _set_position, doc="position")
def _get_angle(self):
return self._object.angle
def _set_angle(self, value):
self._object.angle = value
angle = property(_get_angle, _set_angle, doc="angle")
def _get_index(self):
return self._index
index = property(_get_index, doc="index of the guide")
class RGroups(BaseGroups):
"""RoboFab wrapper for FL group data"""
_title = "FLGroups"
def __init__(self, aDict):
self.update(aDict)
def __setitem__(self, key, value):
# override baseclass so that data is stored in FL classes
if not isinstance(key, str):
raise RoboFabError, 'key must be a string'
if not isinstance(value, list):
raise RoboFabError, 'group must be a list'
super(RGroups, self).__setitem__(key, value)
self._setFLGroups()
def __delitem__(self, key):
# override baseclass so that data is stored in FL classes
super(RGroups, self).__delitem__(key, value)
self._setFLGroups()
def _setFLGroups(self):
# set the group data into the font.
if self.getParent() is not None:
groups = []
for i in self.keys():
value = ' '.join(self[i])
groups.append(': '.join([i, value]))
groups.sort()
self.getParent().naked().classes = groups
def update(self, aDict):
# override baseclass so that data is stored in FL classes
super(RGroups, self).update(aDict)
self._setFLGroups()
def clear(self):
# override baseclass so that data is stored in FL classes
super(RGroups, self).clear()
self._setFLGroups()
def pop(self, key):
# override baseclass so that data is stored in FL classes
i = super(RGroups, self).pop(key)
self._setFLGroups()
return i
def popitem(self):
# override baseclass so that data is stored in FL classes
i = super(RGroups, self).popitem()
self._setFLGroups()
return i
def setdefault(self, key, value=None):
# override baseclass so that data is stored in FL classes
i = super(RGroups, self).setdefault(key, value)
self._setFLGroups()
return i
class RKerning(BaseKerning):
"""RoboFab wrapper for FL Kerning data"""
_title = "FLKerning"
def __setitem__(self, pair, value):
if not isinstance(pair, tuple):
raise RoboFabError, 'kerning pair must be a tuple: (left, right)'
else:
if len(pair) != 2:
raise RoboFabError, 'kerning pair must be a tuple: (left, right)'
else:
if value == 0:
if self._kerning.get(pair) is not None:
#see note about setting kerning values to 0 below
self._setFLKerning(pair, 0)
del self._kerning[pair]
else:
#self._kerning[pair] = value
self._setFLKerning(pair, value)
def _setFLKerning(self, pair, value):
# write a pair back into the font
#
# this is fairly speedy, but setting a pair to 0 is roughly
# 2-3 times slower than setting a real value. this is because
# of all the hoops that must be jumped through to keep FL
# from storing kerning pairs with a value of 0.
parentFont = self.getParent().naked()
left = parentFont[pair[0]]
right = parentFont.FindGlyph(pair[1])
# the left glyph doesn not exist
if left is None:
return
# the right glyph doesn not exist
if right == -1:
return
self._kerning[pair] = value
leftName = pair[0]
value = int(round(value))
# pairs set to 0 need to be handled carefully. FL will allow
# for pairs to have a value of 0 (!?), so we must catch them
# when they pop up and make sure that the pair is actually
# removed from the font.
if value == 0:
foundPair = False
# if the value is 0, we don't need to construct a pair
# we just need to make sure that the pair is not in the list
pairs = []
# so, go through all the pairs and add them to a new list
for flPair in left.kerning:
# we have found the pair. flag it.
if flPair.key == right:
foundPair = True
# not the pair. add it to the list.
else:
pairs.append((flPair.key, flPair.value))
# if we found it, write it back to the glyph.
if foundPair:
left.kerning = []
for p in pairs:
new = KerningPair(p[0], p[1])
left.kerning.append(new)
else:
# non-zero pairs are a bit easier to handle
# we just need to look to see if the pair exists
# if so, change the value and stop the loop.
# if not, add a new pair to the glyph
self._kerning[pair] = value
foundPair = False
for flPair in left.kerning:
if flPair.key == right:
flPair.value = value
foundPair = True
break
if not foundPair:
p = KerningPair(right, value)
left.kerning.append(p)
def update(self, kerningDict):
"""replace kerning data with the data in the given kerningDict"""
# override base class here for speed
parentFont = self.getParent().naked()
# add existing data to the new kerning dict is not being replaced
for pair in self.keys():
if not kerningDict.has_key(pair):
kerningDict[pair] = self._kerning[pair]
# now clear the existing kerning to make sure that
# all the kerning in residing in the glyphs is gone
self.clear()
self._kerning = kerningDict
kDict = {}
# nest the pairs into a dict keyed by the left glyph
# {'A':{'A':-10, 'B':20, ...}, 'B':{...}, ...}
for left, right in kerningDict.keys():
value = kerningDict[left, right]
if not left in kDict:
kDict[left] = {}
kDict[left][right] = value
for left in kDict.keys():
leftGlyph = parentFont[left]
if leftGlyph is not None:
for right in kDict[left].keys():
value = kDict[left][right]
if value != 0:
rightIndex = parentFont.FindGlyph(right)
if rightIndex != -1:
p = KerningPair(rightIndex, value)
leftGlyph.kerning.append(p)
def clear(self):
"""clear all kerning"""
# override base class here for speed
self._kerning = {}
for glyph in self.getParent().naked().glyphs:
glyph.kerning = []
def __add__(self, other):
"""Math operations on FL Kerning objects return RF Kerning objects
as they need to be orphaned objects and FL can't deal with that."""
from sets import Set
from robofab.objects.objectsRF import RKerning as _RKerning
new = _RKerning()
k = Set(self.keys()) | Set(other.keys())
for key in k:
new[key] = self.get(key, 0) + other.get(key, 0)
return new
def __sub__(self, other):
"""Math operations on FL Kerning objects return RF Kerning objects
as they need to be orphaned objects and FL can't deal with that."""
from sets import Set
from robofab.objects.objectsRF import RKerning as _RKerning
new = _RKerning()
k = Set(self.keys()) | Set(other.keys())
for key in k:
new[key] = self.get(key, 0) - other.get(key, 0)
return new
def __mul__(self, factor):
"""Math operations on FL Kerning objects return RF Kerning objects
as they need to be orphaned objects and FL can't deal with that."""
from robofab.objects.objectsRF import RKerning as _RKerning
new = _RKerning()
for name, value in self.items():
new[name] = value * factor
return new
__rmul__ = __mul__
def __div__(self, factor):
"""Math operations on FL Kerning objects return RF Kerning objects
as they need to be orphaned objects and FL can't deal with that."""
if factor == 0:
raise ZeroDivisionError
return self.__mul__(1.0/factor)
class RLib(BaseLib):
"""RoboFab wrapper for FL lib"""
# XXX: As of FL 4.6 the customdata field in glyph objects is busted.
# storing anything there causes the glyph to become uneditable.
# however, the customdata field in font objects is stable.
def __init__(self, aDict):
self.update(aDict)
def __setitem__(self, key, value):
# override baseclass so that data is stored in customdata field
super(RLib, self).__setitem__(key, value)
self._stashLib()
def __delitem__(self, key):
# override baseclass so that data is stored in customdata field
super(RLib, self).__delitem__(key)
self._stashLib()
def _stashLib(self):
# write the plist into the customdata field of the FL object
if self.getParent() is None:
return
if not self:
data = None
elif len(self) == 1 and "org.robofab.fontlab.customdata" in self:
data = self["org.robofab.fontlab.customdata"].data
else:
f = StringIO()
writePlist(self, f)
data = f.getvalue()
f.close()
parent = self.getParent()
parent.naked().customdata = data
def update(self, aDict):
# override baseclass so that data is stored in customdata field
super(RLib, self).update(aDict)
self._stashLib()
def clear(self):
# override baseclass so that data is stored in customdata field
super(RLib, self).clear()
self._stashLib()
def pop(self, key):
# override baseclass so that data is stored in customdata field
i = super(RLib, self).pop(key)
self._stashLib()
return i
def popitem(self):
# override baseclass so that data is stored in customdata field
i = super(RLib, self).popitem()
self._stashLib()
return i
def setdefault(self, key, value=None):
# override baseclass so that data is stored in customdata field
i = super(RLib, self).setdefault(key, value)
self._stashLib()
return i
def _infoMapDict(**kwargs):
default = dict(
nakedAttribute=None,
type=None,
requiresSetNum=False,
masterSpecific=False,
libLocation=None,
specialGetSet=False
)
default.update(kwargs)
return default
def _flipDict(d):
f = {}
for k, v in d.items():
f[v] = k
return f
_styleMapStyleName_fromFL = {
64 : "regular",
1 : "italic",
32 : "bold",
33 : "bold italic"
}
_styleMapStyleName_toFL = _flipDict(_styleMapStyleName_fromFL)
_postscriptWindowsCharacterSet_fromFL = {
0 : 1,
1 : 2,
2 : 3,
77 : 4,
128 : 5,
129 : 6,
130 : 7,
134 : 8,
136 : 9,
161 : 10,
162 : 11,
163 : 12,
177 : 13,
178 : 14,
186 : 15,
200 : 16,
204 : 17,
222 : 18,
238 : 19,
255 : 20,
}
_postscriptWindowsCharacterSet_toFL = _flipDict(_postscriptWindowsCharacterSet_fromFL)
_openTypeOS2Type_toFL = {
1 : 0x0002,
2 : 0x0004,
3 : 0x0008,
8 : 0x0100,
9 : 0x0200,
}
_openTypeOS2Type_fromFL = _flipDict(_openTypeOS2Type_toFL)
_openTypeOS2WidthClass_fromFL = {
"Ultra-condensed" : 1,
"Extra-condensed" : 2,
"Condensed" : 3,
"Semi-condensed" : 4,
"Medium (normal)" : 5,
"Semi-expanded" : 6,
"Expanded" : 7,
"Extra-expanded" : 8,
"Ultra-expanded" : 9,
}
_openTypeOS2WidthClass_toFL = _flipDict(_openTypeOS2WidthClass_fromFL)
_postscriptHintAttributes = set((
"postscriptBlueValues",
"postscriptOtherBlues",
"postscriptFamilyBlues",
"postscriptFamilyOtherBlues",
"postscriptStemSnapH",
"postscriptStemSnapV",
))
class RInfo(BaseInfo):
"""RoboFab wrapper for FL Font Info"""
_title = "FLInfo"
_ufoToFLAttrMapping = {
"familyName" : _infoMapDict(valueType=str, nakedAttribute="family_name"),
"styleName" : _infoMapDict(valueType=str, nakedAttribute="style_name"),
"styleMapFamilyName" : _infoMapDict(valueType=str, nakedAttribute="menu_name"),
"styleMapStyleName" : _infoMapDict(valueType=str, nakedAttribute="font_style", specialGetSet=True),
"versionMajor" : _infoMapDict(valueType=int, nakedAttribute="version_major"),
"versionMinor" : _infoMapDict(valueType=int, nakedAttribute="version_minor"),
"year" : _infoMapDict(valueType=int, nakedAttribute="year"),
"copyright" : _infoMapDict(valueType=str, nakedAttribute="copyright"),
"trademark" : _infoMapDict(valueType=str, nakedAttribute="trademark"),
"unitsPerEm" : _infoMapDict(valueType=int, nakedAttribute="upm"),
"descender" : _infoMapDict(valueType=int, nakedAttribute="descender", masterSpecific=True),
"xHeight" : _infoMapDict(valueType=int, nakedAttribute="x_height", masterSpecific=True),
"capHeight" : _infoMapDict(valueType=int, nakedAttribute="cap_height", masterSpecific=True),
"ascender" : _infoMapDict(valueType=int, nakedAttribute="ascender", masterSpecific=True),
"italicAngle" : _infoMapDict(valueType=float, nakedAttribute="italic_angle"),
"note" : _infoMapDict(valueType=str, nakedAttribute="note"),
"openTypeHeadCreated" : _infoMapDict(valueType=str, nakedAttribute=None, specialGetSet=True), # i can't figure out the ttinfo.head_creation values
"openTypeHeadLowestRecPPEM" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.head_lowest_rec_ppem"),
"openTypeHeadFlags" : _infoMapDict(valueType="intList", nakedAttribute=None), # There is an attribute (ttinfo.head_flags), but no user interface.
"openTypeHheaAscender" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.hhea_ascender"),
"openTypeHheaDescender" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.hhea_descender"),
"openTypeHheaLineGap" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.hhea_line_gap"),
"openTypeHheaCaretSlopeRise" : _infoMapDict(valueType=int, nakedAttribute=None),
"openTypeHheaCaretSlopeRun" : _infoMapDict(valueType=int, nakedAttribute=None),
"openTypeHheaCaretOffset" : _infoMapDict(valueType=int, nakedAttribute=None),
"openTypeNameDesigner" : _infoMapDict(valueType=str, nakedAttribute="designer"),
"openTypeNameDesignerURL" : _infoMapDict(valueType=str, nakedAttribute="designer_url"),
"openTypeNameManufacturer" : _infoMapDict(valueType=str, nakedAttribute="source"),
"openTypeNameManufacturerURL" : _infoMapDict(valueType=str, nakedAttribute="vendor_url"),
"openTypeNameLicense" : _infoMapDict(valueType=str, nakedAttribute="license"),
"openTypeNameLicenseURL" : _infoMapDict(valueType=str, nakedAttribute="license_url"),
"openTypeNameVersion" : _infoMapDict(valueType=str, nakedAttribute="tt_version"),
"openTypeNameUniqueID" : _infoMapDict(valueType=str, nakedAttribute="tt_u_id"),
"openTypeNameDescription" : _infoMapDict(valueType=str, nakedAttribute="notice"),
"openTypeNamePreferredFamilyName" : _infoMapDict(valueType=str, nakedAttribute="pref_family_name"),
"openTypeNamePreferredSubfamilyName" : _infoMapDict(valueType=str, nakedAttribute="pref_style_name"),
"openTypeNameCompatibleFullName" : _infoMapDict(valueType=str, nakedAttribute="mac_compatible"),
"openTypeNameSampleText" : _infoMapDict(valueType=str, nakedAttribute=None),
"openTypeNameWWSFamilyName" : _infoMapDict(valueType=str, nakedAttribute=None),
"openTypeNameWWSSubfamilyName" : _infoMapDict(valueType=str, nakedAttribute=None),
"openTypeOS2WidthClass" : _infoMapDict(valueType=int, nakedAttribute="width"),
"openTypeOS2WeightClass" : _infoMapDict(valueType=int, nakedAttribute="weight_code", specialGetSet=True),
"openTypeOS2Selection" : _infoMapDict(valueType="intList", nakedAttribute=None), # ttinfo.os2_fs_selection only returns 0
"openTypeOS2VendorID" : _infoMapDict(valueType=str, nakedAttribute="vendor"),
"openTypeOS2Panose" : _infoMapDict(valueType="intList", nakedAttribute="panose", specialGetSet=True),
"openTypeOS2FamilyClass" : _infoMapDict(valueType="intList", nakedAttribute="ttinfo.os2_s_family_class", specialGetSet=True),
"openTypeOS2UnicodeRanges" : _infoMapDict(valueType="intList", nakedAttribute="unicoderanges"),
"openTypeOS2CodePageRanges" : _infoMapDict(valueType="intList", nakedAttribute="codepages"),
"openTypeOS2TypoAscender" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_s_typo_ascender"),
"openTypeOS2TypoDescender" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_s_typo_descender"),
"openTypeOS2TypoLineGap" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_s_typo_line_gap"),
"openTypeOS2WinAscent" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_us_win_ascent"),
"openTypeOS2WinDescent" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_us_win_descent", specialGetSet=True),
"openTypeOS2Type" : _infoMapDict(valueType="intList", nakedAttribute="ttinfo.os2_fs_type", specialGetSet=True),
"openTypeOS2SubscriptXSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_subscript_x_size"),
"openTypeOS2SubscriptYSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_subscript_y_size"),
"openTypeOS2SubscriptXOffset" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_subscript_x_offset"),
"openTypeOS2SubscriptYOffset" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_subscript_y_offset"),
"openTypeOS2SuperscriptXSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_superscript_x_size"),
"openTypeOS2SuperscriptYSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_superscript_y_size"),
"openTypeOS2SuperscriptXOffset" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_superscript_x_offset"),
"openTypeOS2SuperscriptYOffset" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_superscript_y_offset"),
"openTypeOS2StrikeoutSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_strikeout_size"),
"openTypeOS2StrikeoutPosition" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_strikeout_position"),
"openTypeVheaVertTypoAscender" : _infoMapDict(valueType=int, nakedAttribute=None),
"openTypeVheaVertTypoDescender" : _infoMapDict(valueType=int, nakedAttribute=None),
"openTypeVheaVertTypoLineGap" : _infoMapDict(valueType=int, nakedAttribute=None),
"openTypeVheaCaretSlopeRise" : _infoMapDict(valueType=int, nakedAttribute=None),
"openTypeVheaCaretSlopeRun" : _infoMapDict(valueType=int, nakedAttribute=None),
"openTypeVheaCaretOffset" : _infoMapDict(valueType=int, nakedAttribute=None),
"postscriptFontName" : _infoMapDict(valueType=str, nakedAttribute="font_name"),
"postscriptFullName" : _infoMapDict(valueType=str, nakedAttribute="full_name"),
"postscriptSlantAngle" : _infoMapDict(valueType=float, nakedAttribute="slant_angle"),
"postscriptUniqueID" : _infoMapDict(valueType=int, nakedAttribute="unique_id"),
"postscriptUnderlineThickness" : _infoMapDict(valueType=int, nakedAttribute="underline_thickness"),
"postscriptUnderlinePosition" : _infoMapDict(valueType=int, nakedAttribute="underline_position"),
"postscriptIsFixedPitch" : _infoMapDict(valueType=bool, nakedAttribute="is_fixed_pitch"),
"postscriptBlueValues" : _infoMapDict(valueType="intList", nakedAttribute="blue_values", masterSpecific=True, requiresSetNum=True),
"postscriptOtherBlues" : _infoMapDict(valueType="intList", nakedAttribute="other_blues", masterSpecific=True, requiresSetNum=True),
"postscriptFamilyBlues" : _infoMapDict(valueType="intList", nakedAttribute="family_blues", masterSpecific=True, requiresSetNum=True),
"postscriptFamilyOtherBlues" : _infoMapDict(valueType="intList", nakedAttribute="family_other_blues", masterSpecific=True, requiresSetNum=True),
"postscriptStemSnapH" : _infoMapDict(valueType="intList", nakedAttribute="stem_snap_h", masterSpecific=True, requiresSetNum=True),
"postscriptStemSnapV" : _infoMapDict(valueType="intList", nakedAttribute="stem_snap_v", masterSpecific=True, requiresSetNum=True),
"postscriptBlueFuzz" : _infoMapDict(valueType=int, nakedAttribute="blue_fuzz", masterSpecific=True),
"postscriptBlueShift" : _infoMapDict(valueType=int, nakedAttribute="blue_shift", masterSpecific=True),
"postscriptBlueScale" : _infoMapDict(valueType=float, nakedAttribute="blue_scale", masterSpecific=True),
"postscriptForceBold" : _infoMapDict(valueType=bool, nakedAttribute="force_bold", masterSpecific=True),
"postscriptDefaultWidthX" : _infoMapDict(valueType=int, nakedAttribute="default_width", masterSpecific=True),
"postscriptNominalWidthX" : _infoMapDict(valueType=int, nakedAttribute=None),
"postscriptWeightName" : _infoMapDict(valueType=str, nakedAttribute="weight"),
"postscriptDefaultCharacter" : _infoMapDict(valueType=str, nakedAttribute="default_character"),
"postscriptWindowsCharacterSet" : _infoMapDict(valueType=int, nakedAttribute="ms_charset", specialGetSet=True),
"macintoshFONDFamilyID" : _infoMapDict(valueType=int, nakedAttribute="fond_id"),
"macintoshFONDName" : _infoMapDict(valueType=str, nakedAttribute="apple_name"),
}
_environmentOverrides = ["width", "openTypeOS2WidthClass"] # ugh.
def __init__(self, font):
super(RInfo, self).__init__()
self._object = font
def _environmentSetAttr(self, attr, value):
# special fontlab workarounds
if attr == "width":
warn("The width attribute has been deprecated. Use the new openTypeOS2WidthClass attribute.", DeprecationWarning)
attr = "openTypeOS2WidthClass"
if attr == "openTypeOS2WidthClass":
if isinstance(value, basestring) and value not in _openTypeOS2WidthClass_toFL:
print "The openTypeOS2WidthClass value \"%s\" cannot be found in the OpenType OS/2 usWidthClass specification. The value will be set into the FontLab file for now." % value
self._object.width = value
else:
self._object.width = _openTypeOS2WidthClass_toFL[value]
return
# get the attribute data
data = self._ufoToFLAttrMapping[attr]
flAttr = data["nakedAttribute"]
valueType = data["valueType"]
masterSpecific = data["masterSpecific"]
requiresSetNum = data["requiresSetNum"]
specialGetSet = data["specialGetSet"]
# warn about setting attributes not supported by FL
if flAttr is None:
print "The attribute %s is not supported by FontLab. This data will not be set." % attr
return
# make sure that the value is the proper type for FL
if valueType == "intList":
value = [int(i) for i in value]
elif valueType == str:
if value is None:
value = ""
value = value.encode(LOCAL_ENCODING)
elif valueType == int and not isinstance(value, int):
value = int(round(value))
elif not isinstance(value, valueType):
value = valueType(value)
# handle postscript hint bug in FL
if attr in _postscriptHintAttributes:
value = self._handlePSHintBug(attr, value)
# handle special cases
if specialGetSet:
attr = "_set_%s" % attr
method = getattr(self, attr)
return method(value)
# set the value
obj = self._object
if len(flAttr.split(".")) > 1:
flAttrList = flAttr.split(".")
for i in flAttrList[:-1]:
obj = getattr(obj, i)
flAttr = flAttrList[-1]
## set the foo_num attribute if necessary
if requiresSetNum:
numAttr = flAttr + "_num"
setattr(obj, numAttr, len(value))
## set master 0 if the data is master specific
if masterSpecific:
subObj = getattr(obj, flAttr)
if valueType == "intList":
for index, v in enumerate(value):
subObj[0][index] = v
else:
subObj[0] = value
## otherwise use a regular set
else:
setattr(obj, flAttr, value)
def _environmentGetAttr(self, attr):
# special fontlab workarounds
if attr == "width":
warn("The width attribute has been deprecated. Use the new openTypeOS2WidthClass attribute.", DeprecationWarning)
attr = "openTypeOS2WidthClass"
if attr == "openTypeOS2WidthClass":
value = self._object.width
if value not in _openTypeOS2WidthClass_fromFL:
print "The existing openTypeOS2WidthClass value \"%s\" cannot be found in the OpenType OS/2 usWidthClass specification." % value
return
else:
return _openTypeOS2WidthClass_fromFL[value]
# get the attribute data
data = self._ufoToFLAttrMapping[attr]
flAttr = data["nakedAttribute"]
valueType = data["valueType"]
masterSpecific = data["masterSpecific"]
specialGetSet = data["specialGetSet"]
# warn about setting attributes not supported by FL
if flAttr is None:
if not _IN_UFO_EXPORT:
print "The attribute %s is not supported by FontLab." % attr
return
# handle special cases
if specialGetSet:
attr = "_get_%s" % attr
method = getattr(self, attr)
return method()
# get the value
if len(flAttr.split(".")) > 1:
flAttrList = flAttr.split(".")
obj = self._object
for i in flAttrList:
obj = getattr(obj, i)
value = obj
else:
value = getattr(self._object, flAttr)
# grab the first master value if necessary
if masterSpecific:
value = value[0]
# convert if necessary
if valueType == "intList":
value = [int(i) for i in value]
elif valueType == str:
if value is None:
pass
else:
value = unicode(value, LOCAL_ENCODING)
elif not isinstance(value, valueType):
value = valueType(value)
return value
# ------------------------------
# individual attribute overrides
# ------------------------------
# styleMapStyleName
def _get_styleMapStyleName(self):
return _styleMapStyleName_fromFL[self._object.font_style]
def _set_styleMapStyleName(self, value):
value = _styleMapStyleName_toFL[value]
self._object.font_style = value
# # openTypeHeadCreated
#
# # fontlab epoch: 1969-12-31 19:00:00
#
# def _get_openTypeHeadCreated(self):
# value = self._object.ttinfo.head_creation
# epoch = datetime.datetime(1969, 12, 31, 19, 0, 0)
# delta = datetime.timedelta(seconds=value[0])
# t = epoch - delta
# string = "%s-%s-%s %s:%s:%s" % (str(t.year).zfill(4), str(t.month).zfill(2), str(t.day).zfill(2), str(t.hour).zfill(2), str(t.minute).zfill(2), str(t.second).zfill(2))
# return string
#
# def _set_openTypeHeadCreated(self, value):
# date, time = value.split(" ")
# year, month, day = [int(i) for i in date.split("-")]
# hour, minute, second = [int(i) for i in time.split(":")]
# value = datetime.datetime(year, month, day, hour, minute, second)
# epoch = datetime.datetime(1969, 12, 31, 19, 0, 0)
# delta = epoch - value
# seconds = delta.seconds
# self._object.ttinfo.head_creation[0] = seconds
# openTypeOS2WeightClass
def _get_openTypeOS2WeightClass(self):
value = self._object.weight_code
if value == -1:
value = None
return value
def _set_openTypeOS2WeightClass(self, value):
self._object.weight_code = value
# openTypeOS2WinDescent
def _get_openTypeOS2WinDescent(self):
return -self._object.ttinfo.os2_us_win_descent
def _set_openTypeOS2WinDescent(self, value):
if value > 0:
raise ValueError("FontLab can only handle negative values for openTypeOS2WinDescent.")
self._object.ttinfo.os2_us_win_descent = abs(value)
# openTypeOS2Type
def _get_openTypeOS2Type(self):
value = self._object.ttinfo.os2_fs_type
intList = []
for bit, bitNumber in _openTypeOS2Type_fromFL.items():
if value & bit:
intList.append(bitNumber)
return intList
def _set_openTypeOS2Type(self, values):
value = 0
for bitNumber in values:
bit = _openTypeOS2Type_toFL[bitNumber]
value = value | bit
self._object.ttinfo.os2_fs_type = value
# openTypeOS2Panose
def _get_openTypeOS2Panose(self):
return [i for i in self._object.panose]
def _set_openTypeOS2Panose(self, values):
for index, value in enumerate(values):
self._object.panose[index] = value
# openTypeOS2FamilyClass
def _get_openTypeOS2FamilyClass(self):
value = self._object.ttinfo.os2_s_family_class
for classID in range(15):
classValue = classID * 256
if classValue > value:
classID -= 1
classValue = classID * 256
break
subclassID = value - classValue
return [classID, subclassID]
def _set_openTypeOS2FamilyClass(self, values):
classID, subclassID = values
classID = classID * 256
value = classID + subclassID
self._object.ttinfo.os2_s_family_class = value
# postscriptWindowsCharacterSet
def _get_postscriptWindowsCharacterSet(self):
value = self._object.ms_charset
value = _postscriptWindowsCharacterSet_fromFL[value]
return value
def _set_postscriptWindowsCharacterSet(self, value):
value = _postscriptWindowsCharacterSet_toFL[value]
self._object.ms_charset = value
# -----------------
# FL bug workaround
# -----------------
def _handlePSHintBug(self, attribute, values):
"""Function to handle problems with FontLab not allowing the max number of
alignment zones to be set to the max number.
Input: the name of the zones and the values to be set
Output: a warning when there are too many values to be set
and the max values which FontLab will allow.
"""
originalValues = values
truncatedLength = None
if attribute in ("postscriptStemSnapH", "postscriptStemSnapV"):
if len(values) > 10:
values = values[:10]
truncatedLength = 10
elif attribute in ("postscriptBlueValues", "postscriptFamilyBlues"):
if len(values) > 12:
values = values[:12]
truncatedLength = 12
elif attribute in ("postscriptOtherBlues", "postscriptFamilyOtherBlues"):
if len(values) > 8:
values = values[:8]
truncatedLength = 8
if truncatedLength is not None:
print "* * * WARNING: FontLab will only accept %d %s items maximum from Python. Dropping values: %s." % (truncatedLength, attribute, str(originalValues[truncatedLength:]))
return values
class RFeatures(BaseFeatures):
_title = "FLFeatures"
def __init__(self, font):
super(RFeatures, self).__init__()
self._object = font
def _get_text(self):
naked = self._object
features = []
if naked.ot_classes:
features.append(_normalizeLineEndings(naked.ot_classes))
for feature in naked.features:
features.append(_normalizeLineEndings(feature.value))
return "".join(features)
def _set_text(self, value):
classes, features = splitFeaturesForFontLab(value)
naked = self._object
naked.ot_classes = classes
naked.features.clean()
for featureName, featureText in features:
f = Feature(featureName, featureText)
naked.features.append(f)
text = property(_get_text, _set_text, doc="raw feature text.")