Cython is used to compile some hot paths into native Python extensions. These hot paths were identified through running ufocompile with the hotshot profiler and then converting file by file to Cython, starting with the "hottest" paths and continuing until returns were deminishing. This means that only a few Python files were converted to Cython. Closes #23 Closes #20 (really this time)
3112 lines
94 KiB
Python
Executable file
3112 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 robofab.misc import arrayTools
|
|
from robofab.pens.flPen import FLPointPen, FLPointContourPen
|
|
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)
|
|
if existingMask is not None:
|
|
pen = FLPointContourPen(existingMask)
|
|
existingMask.drawPoints(pen)
|
|
pen = FLPointContourPen(wrapped)
|
|
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."""
|
|
# the old implementation always updated the font.
|
|
# that proved to be very slow. so, the updating is
|
|
# now left up to the caller where it can be more
|
|
# efficiently managed.
|
|
g = NewGlyph(self._object, glyphName, clear, updateFont=False)
|
|
return RGlyph(g)
|
|
|
|
def insertGlyph(self, glyph, name=None):
|
|
"""Returns a new glyph that has been inserted into the font.
|
|
name = 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 name is None:
|
|
name = oldGlyph.name
|
|
# 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)
|
|
if isinstance(finalPath, unicode):
|
|
finalPath = finalPath.encode("utf-8")
|
|
# 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["public.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]
|
|
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()
|
|
# update the font
|
|
self.update()
|
|
# 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):
|
|
key = "public.glyphOrder"
|
|
glyphOrder = fontLib.get(key)
|
|
if glyphOrder is None:
|
|
key = "org.robofab.glyphOrder"
|
|
glyphOrder = fontLib.get(key)
|
|
if glyphOrder is not None:
|
|
# no need to keep track if the glyph order in lib once the font is loaded.
|
|
del fontLib[key]
|
|
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 clearContours(self):
|
|
self._object.Clear()
|
|
self._invalidateContours()
|
|
|
|
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:
|
|
newNode.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)
|
|
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="boolint", 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="boolint", 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 == "boolint":
|
|
value = int(bool(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 == "boolint":
|
|
value = bool(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:
|
|
warn("FontLab can only handle positive values for openTypeOS2WinDescent.")
|
|
value = abs(value)
|
|
self._object.ttinfo.os2_us_win_descent = 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.")
|
|
|