3104 lines
94 KiB
Python
Executable File
3104 lines
94 KiB
Python
Executable File
"""UFO implementation for the objects as used by FontLab 4.5 and higher"""
|
|
|
|
from FL import *
|
|
|
|
from robofab.tools.toolsFL import GlyphIndexTable, NewGlyph
|
|
from robofab.objects.objectsBase import BaseFont, BaseGlyph, BaseContour, BaseSegment,\
|
|
BasePoint, BaseBPoint, BaseAnchor, BaseGuide, BaseComponent, BaseKerning, BaseInfo, BaseFeatures, BaseGroups, BaseLib,\
|
|
roundPt, addPt, _box,\
|
|
MOVE, LINE, CORNER, CURVE, QCURVE, OFFCURVE,\
|
|
relativeBCPIn, relativeBCPOut, absoluteBCPIn, absoluteBCPOut,\
|
|
BasePostScriptFontHintValues, postScriptHintDataLibKey, BasePostScriptGlyphHintValues
|
|
from fontTools.misc import arrayTools
|
|
from robofab.pens.flPen import FLPointPen, 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["org.robofab.glyphOrder"] = glyphOrder
|
|
## export the features
|
|
if doFeatures and formatVersion == 1:
|
|
self._writeOpenTypeFeaturesToLib(fontLib)
|
|
if bar:
|
|
bar.tick()
|
|
writer.writeLib(fontLib)
|
|
if bar:
|
|
bar.tick()
|
|
# write the glyphs
|
|
if glyphs:
|
|
glyphSet = writer.getGlyphSet(glyphNameToFileNameFunc)
|
|
count = nonGlyphCount
|
|
for nakedGlyph in self.naked().glyphs:
|
|
if nakedGlyph.name not in glyphs:
|
|
continue
|
|
glyph = RGlyph(nakedGlyph)
|
|
if doHints:
|
|
hintStuff = _glyphHintsToDict(glyph.naked())
|
|
if hintStuff:
|
|
glyph.lib[postScriptHintDataLibKey] = hintStuff
|
|
glyphSet.writeGlyph(glyph.name, glyph, glyph.drawPoints)
|
|
# remove the hint dict from the lib
|
|
if doHints and glyph.lib.has_key(postScriptHintDataLibKey):
|
|
del glyph.lib[postScriptHintDataLibKey]
|
|
if bar and not count % 10:
|
|
bar.tick(count)
|
|
count = count + 1
|
|
glyphSet.writeContents()
|
|
# only blindly stop if the user says to
|
|
except KeyboardInterrupt:
|
|
if bar:
|
|
bar.close()
|
|
bar = None
|
|
# kill the bar
|
|
if bar:
|
|
bar.close()
|
|
|
|
def _writeOpenTypeFeaturesToLib(self, fontLib):
|
|
# this should only be used for UFO format version 1
|
|
flFont = self.naked()
|
|
cls = flFont.ot_classes
|
|
if cls is not None:
|
|
fontLib["org.robofab.opentype.classes"] = _normalizeLineEndings(cls).rstrip() + "\n"
|
|
if flFont.features:
|
|
features = {}
|
|
order = []
|
|
for feature in flFont.features:
|
|
order.append(feature.tag)
|
|
features[feature.tag] = _normalizeLineEndings(feature.value).rstrip() + "\n"
|
|
fontLib["org.robofab.opentype.features"] = features
|
|
fontLib["org.robofab.opentype.featureorder"] = order
|
|
|
|
def readUFO(self, path, doProgress=False,
|
|
doHints=False, doInfo=True, doKerning=True, doGroups=True, doLib=True, doFeatures=True, glyphs=None):
|
|
"""read a .ufo into the font"""
|
|
from robofab.pens.flPen import FLPointPen
|
|
from robofab.interface.all.dialogs import ProgressBar
|
|
# start up the reader
|
|
reader = ufoLib.UFOReader(path)
|
|
glyphSet = reader.getGlyphSet()
|
|
# get a list of glyphs that should be imported
|
|
if glyphs is None:
|
|
glyphs = glyphSet.keys()
|
|
# set up the progress bar
|
|
nonGlyphCount = [doInfo, doKerning, doGroups, doLib, doFeatures].count(True)
|
|
bar = None
|
|
if doProgress:
|
|
bar = ProgressBar("Importing UFO", nonGlyphCount + len(glyphs))
|
|
# start reading
|
|
try:
|
|
fontLib = reader.readLib()
|
|
# info
|
|
if doInfo:
|
|
reader.readInfo(self.info)
|
|
if bar:
|
|
bar.tick()
|
|
# glyphs
|
|
count = 1
|
|
glyphOrder = self._getGlyphOrderFromLib(fontLib, glyphSet)
|
|
for glyphName in glyphOrder:
|
|
if glyphName not in glyphs:
|
|
continue
|
|
glyph = self.newGlyph(glyphName, clear=True)
|
|
pen = FLPointPen(glyph.naked())
|
|
glyphSet.readGlyph(glyphName=glyphName, glyphObject=glyph, pointPen=pen)
|
|
if doHints:
|
|
hintData = glyph.lib.get(postScriptHintDataLibKey)
|
|
if hintData:
|
|
_dictHintsToGlyph(glyph.naked(), hintData)
|
|
# now that the hints have been extracted from the glyph
|
|
# there is no reason to keep the location in the lib.
|
|
if glyph.lib.has_key(postScriptHintDataLibKey):
|
|
del glyph.lib[postScriptHintDataLibKey]
|
|
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):
|
|
glyphOrder = fontLib.get("org.robofab.glyphOrder")
|
|
if glyphOrder is not None:
|
|
# no need to keep track if the glyph order in lib once the font is loaded.
|
|
del fontLib["org.robofab.glyphOrder"]
|
|
glyphNames = []
|
|
done = {}
|
|
for glyphName in glyphOrder:
|
|
if glyphName in glyphSet:
|
|
glyphNames.append(glyphName)
|
|
done[glyphName] = 1
|
|
allGlyphNames = glyphSet.keys()
|
|
allGlyphNames.sort()
|
|
for glyphName in allGlyphNames:
|
|
if glyphName not in done:
|
|
glyphNames.append(glyphName)
|
|
else:
|
|
glyphNames = glyphSet.keys()
|
|
glyphNames.sort()
|
|
return glyphNames
|
|
|
|
def _readOpenTypeFeaturesFromLib(self, fontLib, setFeatures=True):
|
|
# setFeatures may be False. in this case, this method
|
|
# should only clear the data from the lib.
|
|
classes = fontLib.get("org.robofab.opentype.classes")
|
|
if classes is not None:
|
|
del fontLib["org.robofab.opentype.classes"]
|
|
if setFeatures:
|
|
self.naked().ot_classes = classes
|
|
features = fontLib.get("org.robofab.opentype.features")
|
|
if features is not None:
|
|
order = fontLib.get("org.robofab.opentype.featureorder")
|
|
if order is None:
|
|
# for UFOs saved without the feature order, do the same as before.
|
|
order = features.keys()
|
|
order.sort()
|
|
else:
|
|
del fontLib["org.robofab.opentype.featureorder"]
|
|
del fontLib["org.robofab.opentype.features"]
|
|
#features = features.items()
|
|
orderedFeatures = []
|
|
for tag in order:
|
|
oneFeature = features.get(tag)
|
|
if oneFeature is not None:
|
|
orderedFeatures.append((tag, oneFeature))
|
|
if setFeatures:
|
|
self.naked().features.clean()
|
|
for tag, src in orderedFeatures:
|
|
self.naked().features.append(Feature(tag, src))
|
|
|
|
|
|
|
|
class RGlyph(BaseGlyph):
|
|
"""RoboFab wrapper for FL Glyph object"""
|
|
|
|
_title = "FLGlyph"
|
|
|
|
def __init__(self, flGlyph):
|
|
#BaseGlyph.__init__(self)
|
|
if flGlyph is None:
|
|
raise RoboFabError, "RGlyph: there's nothing to wrap!?"
|
|
self._object = flGlyph
|
|
self._lib = {}
|
|
self._contours = None
|
|
|
|
def __getitem__(self, index):
|
|
return self.contours[index]
|
|
|
|
def __delitem__(self, index):
|
|
self._object.DeleteContour(index)
|
|
self._invalidateContours()
|
|
|
|
def __len__(self):
|
|
return len(self.contours)
|
|
|
|
lib = property(_get_lib, _set_lib, doc="glyph lib object")
|
|
|
|
def _invalidateContours(self):
|
|
self._contours = None
|
|
|
|
def _buildContours(self):
|
|
self._contours = []
|
|
for contourIndex in range(self._object.GetContoursNumber()):
|
|
c = RContour(contourIndex)
|
|
c.setParent(self)
|
|
c._buildSegments()
|
|
self._contours.append(c)
|
|
|
|
#
|
|
# attribute handlers
|
|
#
|
|
|
|
def _get_index(self):
|
|
return self._object.parent.FindGlyph(self.name)
|
|
|
|
index = property(_get_index, doc="return the index of the glyph in the font")
|
|
|
|
def _get_name(self):
|
|
return self._object.name
|
|
|
|
def _set_name(self, value):
|
|
self._object.name = value
|
|
|
|
name = property(_get_name, _set_name, doc="name")
|
|
|
|
def _get_psName(self):
|
|
return self._object.name
|
|
|
|
def _set_psName(self, value):
|
|
self._object.name = value
|
|
|
|
psName = property(_get_psName, _set_psName, doc="name")
|
|
|
|
def _get_baseName(self):
|
|
return self._object.name.split('.')[0]
|
|
|
|
baseName = property(_get_baseName, doc="")
|
|
|
|
def _get_unicode(self):
|
|
return self._object.unicode
|
|
|
|
def _set_unicode(self, value):
|
|
self._object.unicode = value
|
|
|
|
unicode = property(_get_unicode, _set_unicode, doc="unicode")
|
|
|
|
def _get_unicodes(self):
|
|
return self._object.unicodes
|
|
|
|
def _set_unicodes(self, value):
|
|
self._object.unicodes = value
|
|
|
|
unicodes = property(_get_unicodes, _set_unicodes, doc="unicodes")
|
|
|
|
def _get_width(self):
|
|
return self._object.width
|
|
|
|
def _set_width(self, value):
|
|
value = int(round(value))
|
|
self._object.width = value
|
|
|
|
width = property(_get_width, _set_width, doc="the width")
|
|
|
|
def _get_box(self):
|
|
if not len(self.contours) and not len(self.components):
|
|
return (0, 0, 0, 0)
|
|
r = self._object.GetBoundingRect()
|
|
return (int(round(r.ll.x)), int(round(r.ll.y)), int(round(r.ur.x)), int(round(r.ur.y)))
|
|
|
|
box = property(_get_box, doc="box of glyph as a tuple (xMin, yMin, xMax, yMax)")
|
|
|
|
def _get_selected(self):
|
|
if fl.Selected(self._object.parent.FindGlyph(self._object.name)):
|
|
return 1
|
|
else:
|
|
return 0
|
|
|
|
def _set_selected(self, value):
|
|
fl.Select(self._object.name, value)
|
|
|
|
selected = property(_get_selected, _set_selected, doc="Select or deselect the glyph in the font window")
|
|
|
|
def _get_mark(self):
|
|
return self._object.mark
|
|
|
|
def _set_mark(self, value):
|
|
self._object.mark = value
|
|
|
|
mark = property(_get_mark, _set_mark, doc="mark")
|
|
|
|
def _get_note(self):
|
|
s = self._object.note
|
|
if s is None:
|
|
return s
|
|
return unicode(s, LOCAL_ENCODING)
|
|
|
|
def _set_note(self, value):
|
|
if value is None:
|
|
value = ''
|
|
if type(value) == type(u""):
|
|
value = value.encode(LOCAL_ENCODING)
|
|
self._object.note = value
|
|
|
|
note = property(_get_note, _set_note, doc="note")
|
|
|
|
def _get_psHints(self):
|
|
# get an object representing the postscript zone information
|
|
return PostScriptGlyphHintValues(self)
|
|
|
|
psHints = property(_get_psHints, doc="postscript hint data")
|
|
|
|
#
|
|
# necessary evil
|
|
#
|
|
|
|
def update(self):
|
|
"""Don't forget to update the glyph when you are done."""
|
|
fl.UpdateGlyph(self._object.parent.FindGlyph(self._object.name))
|
|
|
|
#
|
|
# methods to make RGlyph compatible with FL.Glyph
|
|
# ##are these still needed?
|
|
#
|
|
|
|
def GetBoundingRect(self, masterIndex):
|
|
"""FL compatibility"""
|
|
return self._object.GetBoundingRect(masterIndex)
|
|
|
|
def GetMetrics(self, masterIndex):
|
|
"""FL compatibility"""
|
|
return self._object.GetMetrics(masterIndex)
|
|
|
|
def SetMetrics(self, value, masterIndex):
|
|
"""FL compatibility"""
|
|
return self._object.SetMetrics(value, masterIndex)
|
|
|
|
#
|
|
# object builders
|
|
#
|
|
|
|
def _get_anchors(self):
|
|
return self.getAnchors()
|
|
|
|
anchors = property(_get_anchors, doc="allow for iteration through glyph.anchors")
|
|
|
|
def _get_components(self):
|
|
return self.getComponents()
|
|
|
|
components = property(_get_components, doc="allow for iteration through glyph.components")
|
|
|
|
def _get_contours(self):
|
|
if self._contours is None:
|
|
self._buildContours()
|
|
return self._contours
|
|
|
|
contours = property(_get_contours, doc="allow for iteration through glyph.contours")
|
|
|
|
def getAnchors(self):
|
|
"""Return a list of wrapped anchors in this RGlyph."""
|
|
anchors=[]
|
|
for i in range(len(self._object.anchors)):
|
|
a = RAnchor(self._object.anchors[i], i)
|
|
a.setParent(self)
|
|
anchors.append(a)
|
|
return anchors
|
|
|
|
def getComponents(self):
|
|
"""Return a list of wrapped components in this RGlyph."""
|
|
components=[]
|
|
for i in range(len(self._object.components)):
|
|
c = RComponent(self._object.components[i], i)
|
|
c.setParent(self)
|
|
components.append(c)
|
|
return components
|
|
|
|
def getVGuides(self):
|
|
"""Return a list of wrapped vertical guides in this RGlyph"""
|
|
vguides=[]
|
|
for i in range(len(self._object.vguides)):
|
|
g = RGuide(self._object.vguides[i], i)
|
|
g.setParent(self)
|
|
vguides.append(g)
|
|
return vguides
|
|
|
|
def getHGuides(self):
|
|
"""Return a list of wrapped horizontal guides in this RGlyph"""
|
|
hguides=[]
|
|
for i in range(len(self._object.hguides)):
|
|
g = RGuide(self._object.hguides[i], i)
|
|
g.setParent(self)
|
|
hguides.append(g)
|
|
return hguides
|
|
|
|
#
|
|
# tools
|
|
#
|
|
|
|
def getPointPen(self):
|
|
self._invalidateContours()
|
|
# Now just don't muck with glyph.contours before you're done drawing...
|
|
return FLPointPen(self)
|
|
|
|
def appendComponent(self, baseGlyph, offset=(0, 0), scale=(1, 1)):
|
|
"""Append a component to the glyph. x and y are optional offset values"""
|
|
offset = roundPt((offset[0], offset[1]))
|
|
p = FLPointPen(self.naked())
|
|
xx, yy = scale
|
|
dx, dy = offset
|
|
p.addComponent(baseGlyph, (xx, 0, 0, yy, dx, dy))
|
|
|
|
def appendAnchor(self, name, position):
|
|
"""Append an anchor to the glyph"""
|
|
value = roundPt((position[0], position[1]))
|
|
anchor = Anchor(name, value[0], value[1])
|
|
self._object.anchors.append(anchor)
|
|
|
|
def appendHGuide(self, position, angle=0):
|
|
"""Append a horizontal guide"""
|
|
position = int(round(position))
|
|
g = Guide(position, angle)
|
|
self._object.hguides.append(g)
|
|
|
|
def appendVGuide(self, position, angle=0):
|
|
"""Append a horizontal guide"""
|
|
position = int(round(position))
|
|
g = Guide(position, angle)
|
|
self._object.vguides.append(g)
|
|
|
|
def clearComponents(self):
|
|
"""Clear all components."""
|
|
self._object.components.clean()
|
|
|
|
def clearAnchors(self):
|
|
"""Clear all anchors."""
|
|
self._object.anchors.clean()
|
|
|
|
def clearHGuides(self):
|
|
"""Clear all horizontal guides."""
|
|
self._object.hguides.clean()
|
|
|
|
def clearVGuides(self):
|
|
"""Clear all vertical guides."""
|
|
self._object.vguides.clean()
|
|
|
|
def removeComponent(self, component):
|
|
"""Remove a specific component from the glyph. This only works
|
|
if the glyph does not have duplicate components in the same location."""
|
|
pos = (component.baseGlyph, component.offset, component.scale)
|
|
a = self.getComponents()
|
|
found = []
|
|
for i in a:
|
|
if (i.baseGlyph, i.offset, i.scale) == pos:
|
|
found.append(i)
|
|
if len(found) > 1:
|
|
raise RoboFabError, 'Found more than one possible component to remove'
|
|
elif len(found) == 1:
|
|
del self._object.components[found[0].index]
|
|
else:
|
|
raise RoboFabError, 'Component does not exist'
|
|
|
|
def removeContour(self, index):
|
|
"""remove a specific contour from the glyph"""
|
|
self._object.DeleteContour(index)
|
|
self._invalidateContours()
|
|
|
|
def removeAnchor(self, anchor):
|
|
"""Remove a specific anchor from the glyph. This only works
|
|
if the glyph does not have anchors with duplicate names
|
|
in exactly the same location with the same mark."""
|
|
pos = (anchor.name, anchor.position, anchor.mark)
|
|
a = self.getAnchors()
|
|
found = []
|
|
for i in a:
|
|
if (i.name, i.position, i.mark) == pos:
|
|
found.append(i)
|
|
if len(found) > 1:
|
|
raise RoboFabError, 'Found more than one possible anchor to remove'
|
|
elif len(found) == 1:
|
|
del self._object.anchors[found[0].index]
|
|
else:
|
|
raise RoboFabError, 'Anchor does not exist'
|
|
|
|
def removeHGuide(self, guide):
|
|
"""Remove a horizontal guide."""
|
|
pos = (guide.position, guide.angle)
|
|
for g in self.getHGuides():
|
|
if (g.position, g.angle) == pos:
|
|
del self._object.hguides[g.index]
|
|
break
|
|
|
|
def removeVGuide(self, guide):
|
|
"""Remove a vertical guide."""
|
|
pos = (guide.position, guide.angle)
|
|
for g in self.getVGuides():
|
|
if (g.position, g.angle) == pos:
|
|
del self._object.vguides[g.index]
|
|
break
|
|
|
|
def center(self, padding=None):
|
|
"""Equalise sidebearings, set to padding if wanted."""
|
|
left = self.leftMargin
|
|
right = self.rightMargin
|
|
if padding:
|
|
e_left = e_right = padding
|
|
else:
|
|
e_left = (left + right)/2
|
|
e_right = (left + right) - e_left
|
|
self.leftMargin= e_left
|
|
self.rightMargin= e_right
|
|
|
|
def removeOverlap(self):
|
|
"""Remove overlap"""
|
|
self._object.RemoveOverlap()
|
|
self._invalidateContours()
|
|
|
|
def decompose(self):
|
|
"""Decompose all components"""
|
|
self._object.Decompose()
|
|
self._invalidateContours()
|
|
|
|
##broken!
|
|
#def removeHints(self):
|
|
# """Remove the hints."""
|
|
# self._object.RemoveHints()
|
|
|
|
def autoHint(self):
|
|
"""Automatically generate type 1 hints."""
|
|
self._object.Autohint()
|
|
|
|
def move(self, (x, y), contours=True, components=True, anchors=True):
|
|
"""Move a glyph's items that are flagged as True"""
|
|
x, y = roundPt((x, y))
|
|
self._object.Shift(Point(x, y))
|
|
for c in self.getComponents():
|
|
c.move((x, y))
|
|
for a in self.getAnchors():
|
|
a.move((x, y))
|
|
|
|
def clear(self, contours=True, components=True, anchors=True, guides=True, hints=True):
|
|
"""Clear all items marked as true from the glyph"""
|
|
if contours:
|
|
self._object.Clear()
|
|
self._invalidateContours()
|
|
if components:
|
|
self._object.components.clean()
|
|
if anchors:
|
|
self._object.anchors.clean()
|
|
if guides:
|
|
self._object.hguides.clean()
|
|
self._object.vguides.clean()
|
|
if hints:
|
|
# RemoveHints requires an "integer mode" argument
|
|
# but it is not documented. from some simple experiments
|
|
# i deduced that
|
|
# 1 = horizontal hints and links,
|
|
# 2 = vertical hints and links
|
|
# 3 = all hints and links
|
|
self._object.RemoveHints(3)
|
|
|
|
#
|
|
# special treatment for GlyphMath support in FontLab
|
|
#
|
|
|
|
def _getMathDestination(self):
|
|
from robofab.objects.objectsRF import RGlyph as _RGlyph
|
|
return _RGlyph()
|
|
|
|
def copy(self, aParent=None):
|
|
"""Make a copy of this glyph.
|
|
Note: the copy is not a duplicate fontlab glyph, but
|
|
a RF RGlyph with the same outlines. The new glyph is
|
|
not part of the fontlab font in any way. Use font.appendGlyph(glyph)
|
|
to get it in a FontLab glyph again."""
|
|
from robofab.objects.objectsRF import RGlyph as _RGlyph
|
|
newGlyph = _RGlyph()
|
|
newGlyph.appendGlyph(self)
|
|
for attr in GLYPH_COPY_ATTRS:
|
|
value = getattr(self, attr)
|
|
setattr(newGlyph, attr, value)
|
|
# hints
|
|
doHints = False
|
|
parent = self.getParent()
|
|
if parent is not None and parent._supportHints:
|
|
hintStuff = _glyphHintsToDict(self.naked())
|
|
if hintStuff:
|
|
newGlyph.lib[postScriptHintDataLibKey] = hintStuff
|
|
if aParent is not None:
|
|
newGlyph.setParent(aParent)
|
|
elif self.getParent() is not None:
|
|
newGlyph.setParent(self.getParent())
|
|
return newGlyph
|
|
|
|
def __mul__(self, factor):
|
|
return self.copy() *factor
|
|
|
|
__rmul__ = __mul__
|
|
|
|
def __sub__(self, other):
|
|
return self.copy() - other.copy()
|
|
|
|
def __add__(self, other):
|
|
return self.copy() + other.copy()
|
|
|
|
|
|
|
|
class RContour(BaseContour):
|
|
|
|
"""RoboFab wrapper for non FL contour object"""
|
|
|
|
_title = "FLContour"
|
|
|
|
def __init__(self, index):
|
|
self._index = index
|
|
self._parentGlyph = None
|
|
self.segments = []
|
|
|
|
def __len__(self):
|
|
return len(self.points)
|
|
|
|
def _buildSegments(self):
|
|
#######################
|
|
# Notes about FL node contour structure
|
|
#######################
|
|
# for TT curves, FL lists them as seperate nodes:
|
|
# [move, off, off, off, line, off, off]
|
|
# and, this list is sequential. after the last on curve,
|
|
# it is possible (and likely) that there will be more offCurves
|
|
# in our segment object, these should be associated with the
|
|
# first segment in the contour.
|
|
#
|
|
# for PS curves, it is a very different scenerio.
|
|
# curve nodes contain points:
|
|
# [on, off, off]
|
|
# and the list is not in sequential order. the first point in
|
|
# the list is the on curve and the subsequent points are the off
|
|
# curve points leading up to that on curve.
|
|
#
|
|
# it is very important to remember these structures when trying
|
|
# to understand the code below
|
|
|
|
self.segments = []
|
|
offList = []
|
|
nodes = self._nakedParent.nodes
|
|
for index in range(self._nodeLength):
|
|
x = index + self._startNodeIndex
|
|
node = nodes[x]
|
|
# we do have a loose off curve. deal with it.
|
|
if node.type == flOFFCURVE:
|
|
offList.append(x)
|
|
# we are not dealing with a loose off curve
|
|
else:
|
|
s = RSegment(x)
|
|
s.setParent(self)
|
|
# but do we have a collection of loose off curves above?
|
|
# if so, apply them to the segment, and clear the list
|
|
if len(offList) != 0:
|
|
s._looseOffCurve = offList
|
|
offList = []
|
|
self.segments.append(s)
|
|
# do we have some off curves now that the contour is complete?
|
|
if len(offList) != 0:
|
|
# ugh. apply them to the first segment
|
|
self.segments[0]._looseOffCurve = offList
|
|
|
|
def setParent(self, parentGlyph):
|
|
self._parentGlyph = parentGlyph
|
|
|
|
def getParent(self):
|
|
return self._parentGlyph
|
|
|
|
def _get__nakedParent(self):
|
|
return self._parentGlyph.naked()
|
|
|
|
_nakedParent = property(_get__nakedParent, doc="")
|
|
|
|
def _get__startNodeIndex(self):
|
|
return self._nakedParent.GetContourBegin(self._index)
|
|
|
|
_startNodeIndex = property(_get__startNodeIndex, doc="")
|
|
|
|
def _get__nodeLength(self):
|
|
return self._nakedParent.GetContourLength(self._index)
|
|
|
|
_nodeLength = property(_get__nodeLength, doc="")
|
|
|
|
def _get__lastNodeIndex(self):
|
|
return self._startNodeIndex + self._nodeLength - 1
|
|
|
|
_lastNodeIndex = property(_get__lastNodeIndex, doc="")
|
|
|
|
def _previousNodeIndex(self, index):
|
|
return (index - 1) % self._nodeLength
|
|
|
|
def _nextNodeIndex(self, index):
|
|
return (index + 1) % self._nodeLength
|
|
|
|
def _getNode(self, index):
|
|
return self._nodes[index]
|
|
|
|
def _get__nodes(self):
|
|
nodes = []
|
|
for node in self._nakedParent.nodes[self._startNodeIndex:self._startNodeIndex+self._nodeLength-1]:
|
|
nodes.append(node)
|
|
return nodes
|
|
|
|
_nodes = property(_get__nodes, doc="")
|
|
|
|
def _get_points(self):
|
|
points = []
|
|
for segment in self.segments:
|
|
for point in segment.points:
|
|
points.append(point)
|
|
return points
|
|
|
|
points = property(_get_points, doc="")
|
|
|
|
def _get_bPoints(self):
|
|
bPoints = []
|
|
for segment in self.segments:
|
|
bp = RBPoint(segment.index)
|
|
bp.setParent(self)
|
|
bPoints.append(bp)
|
|
return bPoints
|
|
|
|
bPoints = property(_get_bPoints, doc="")
|
|
|
|
def _get_index(self):
|
|
return self._index
|
|
|
|
def _set_index(self, index):
|
|
if index != self._index:
|
|
self._nakedParent.ReorderContour(self._index, index)
|
|
# reorder and set the _index of the existing RContour objects
|
|
# this will be a better solution than reconstructing all the objects
|
|
# segment objects will still, sadly, have to be reconstructed
|
|
contourList = self.getParent().contours
|
|
contourList.insert(index, contourList.pop(self._index))
|
|
for i in range(len(contourList)):
|
|
contourList[i]._index = i
|
|
contourList[i]._buildSegments()
|
|
|
|
|
|
index = property(_get_index, _set_index, doc="the index of the contour")
|
|
|
|
def _get_selected(self):
|
|
selected = 0
|
|
nodes = self._nodes
|
|
for node in nodes:
|
|
if node.selected == 1:
|
|
selected = 1
|
|
break
|
|
return selected
|
|
|
|
def _set_selected(self, value):
|
|
if value == 1:
|
|
self._nakedParent.SelectContour(self._index)
|
|
else:
|
|
for node in self._nodes:
|
|
node.selected = value
|
|
|
|
selected = property(_get_selected, _set_selected, doc="selection of the contour: 1-selected or 0-unselected")
|
|
|
|
def appendSegment(self, segmentType, points, smooth=False):
|
|
segment = self.insertSegment(index=self._nodeLength, segmentType=segmentType, points=points, smooth=smooth)
|
|
return segment
|
|
|
|
def insertSegment(self, index, segmentType, points, smooth=False):
|
|
"""insert a seggment into the contour"""
|
|
# do a qcurve insertion
|
|
if segmentType == QCURVE:
|
|
count = 0
|
|
for point in points[:-1]:
|
|
newNode = Node(flOFFCURVE, Point(point[0], point[1]))
|
|
self._nakedParent.Insert(newNode, self._startNodeIndex + index + count)
|
|
count = count + 1
|
|
newNode = Node(flLINE, Point(points[-1][0], points[-1][1]))
|
|
self._nakedParent.Insert(newNode, self._startNodeIndex + index +len(points) - 1)
|
|
# do a regular insertion
|
|
else:
|
|
onX, onY = points[-1]
|
|
newNode = Node(_rfToFLSegmentType(segmentType), Point(onX, onY))
|
|
# fix the off curves in case the user is inserting a curve
|
|
# but is not specifying off curve points
|
|
if segmentType == CURVE and len(points) == 1:
|
|
pSeg = self._prevSegment(index)
|
|
pOn = pSeg.onCurve
|
|
newNode.points[1].Assign(Point(pOn.x, pOn.y))
|
|
newNode.points[2].Assign(Point(onX, onY))
|
|
for pointIndex in range(len(points[:-1])):
|
|
x, y = points[pointIndex]
|
|
newNode.points[1 + pointIndex].Assign(Point(x, y))
|
|
if smooth:
|
|
node.alignment = flSMOOTH
|
|
self._nakedParent.Insert(newNode, self._startNodeIndex + index)
|
|
self._buildSegments()
|
|
return self.segments[index]
|
|
|
|
def removeSegment(self, index):
|
|
"""remove a segment from the contour"""
|
|
segment = self.segments[index]
|
|
# we have a qcurve. umph.
|
|
if segment.type == QCURVE:
|
|
indexList = [segment._nodeIndex] + segment._looseOffCurve
|
|
indexList.sort()
|
|
indexList.reverse()
|
|
parent = self._nakedParent
|
|
for nodeIndex in indexList:
|
|
parent.DeleteNode(nodeIndex)
|
|
# we have a more sane structure to follow
|
|
else:
|
|
# store some info for later
|
|
next = self._nextSegment(index)
|
|
nextOffA = None
|
|
nextOffB = None
|
|
nextType = next.type
|
|
if nextType != LINE and nextType != MOVE:
|
|
pA = next.offCurve[0]
|
|
nextOffA = (pA.x, pA.y)
|
|
pB = next.offCurve[-1]
|
|
nextOffB = (pB.x, pB.y)
|
|
nodeIndex = segment._nodeIndex
|
|
self._nakedParent.DeleteNode(nodeIndex)
|
|
self._buildSegments()
|
|
# now we must override FL guessing about offCurves
|
|
next = self._nextSegment(index - 1)
|
|
nextType = next.type
|
|
if nextType != LINE and nextType != MOVE:
|
|
pA = next.offCurve[0]
|
|
pB = next.offCurve[-1]
|
|
pA.x, pA.y = nextOffA
|
|
pB.x, pB.y = nextOffB
|
|
|
|
def reverseContour(self):
|
|
"""reverse contour direction"""
|
|
self._nakedParent.ReverseContour(self._index)
|
|
self._buildSegments()
|
|
|
|
def setStartSegment(self, segmentIndex):
|
|
"""set the first node on the contour"""
|
|
self._nakedParent.SetStartNode(self._startNodeIndex + segmentIndex)
|
|
self.getParent()._invalidateContours()
|
|
self.getParent()._buildContours()
|
|
|
|
def copy(self, aParent=None):
|
|
"""Copy this object -- result is an ObjectsRF flavored object.
|
|
There is no way to make this work using FontLab objects.
|
|
Copy is mainly used for glyphmath.
|
|
"""
|
|
raise RoboFabError, "copy() for objectsFL.RContour is not implemented."
|
|
|
|
|
|
|
|
class RSegment(BaseSegment):
|
|
|
|
_title = "FLSegment"
|
|
|
|
def __init__(self, flNodeIndex):
|
|
BaseSegment.__init__(self)
|
|
self._nodeIndex = flNodeIndex
|
|
self._looseOffCurve = [] #a list of indexes to loose off curve nodes
|
|
|
|
def _get__node(self):
|
|
glyph = self.getParent()._nakedParent
|
|
return glyph.nodes[self._nodeIndex]
|
|
|
|
_node = property(_get__node, doc="")
|
|
|
|
def _get_qOffCurve(self):
|
|
nodes = self.getParent()._nakedParent.nodes
|
|
off = []
|
|
for x in self._looseOffCurve:
|
|
off.append(nodes[x])
|
|
return off
|
|
|
|
_qOffCurve = property(_get_qOffCurve, doc="free floating off curve nodes in the segment")
|
|
|
|
def _get_index(self):
|
|
contour = self.getParent()
|
|
return self._nodeIndex - contour._startNodeIndex
|
|
|
|
index = property(_get_index, doc="")
|
|
|
|
def _isQCurve(self):
|
|
# loose off curves only appear in q curves
|
|
if len(self._looseOffCurve) != 0:
|
|
return True
|
|
return False
|
|
|
|
def _get_type(self):
|
|
if self._isQCurve():
|
|
return QCURVE
|
|
return _flToRFSegmentType(self._node.type)
|
|
|
|
def _set_type(self, segmentType):
|
|
if self._isQCurve():
|
|
raise RoboFabError, 'qcurve point types cannot be changed'
|
|
oldNode = self._node
|
|
oldType = oldNode.type
|
|
oldPointType = _flToRFSegmentType(oldType)
|
|
if oldPointType == MOVE:
|
|
raise RoboFabError, '%s point types cannot be changed'%oldPointType
|
|
if segmentType == MOVE or segmentType == OFFCURVE:
|
|
raise RoboFabError, '%s point types cannot be assigned'%oldPointType
|
|
if oldPointType == segmentType:
|
|
return
|
|
oldNode.type = _rfToFLSegmentType(segmentType)
|
|
|
|
type = property(_get_type, _set_type, doc="")
|
|
|
|
def _get_smooth(self):
|
|
alignment = self._node.alignment
|
|
if alignment == flSMOOTH or alignment == flFIXED:
|
|
return True
|
|
return False
|
|
|
|
def _set_smooth(self, value):
|
|
if value:
|
|
self._node.alignment = flSMOOTH
|
|
else:
|
|
self._node.alignment = flSHARP
|
|
|
|
smooth = property(_get_smooth, _set_smooth, doc="")
|
|
|
|
def _get_points(self):
|
|
points = []
|
|
node = self._node
|
|
# gather the off curves
|
|
#
|
|
# are we dealing with a qCurve? ugh.
|
|
# gather the loose off curves
|
|
if self._isQCurve():
|
|
off = self._qOffCurve
|
|
x = 0
|
|
for n in off:
|
|
p = RPoint(0)
|
|
p.setParent(self)
|
|
p._qOffIndex = x
|
|
points.append(p)
|
|
x = x + 1
|
|
# otherwise get the points associated with the node
|
|
else:
|
|
index = 1
|
|
for point in node.points[1:]:
|
|
p = RPoint(index)
|
|
p.setParent(self)
|
|
points.append(p)
|
|
index = index + 1
|
|
# the last point should always be the on curve
|
|
p = RPoint(0)
|
|
p.setParent(self)
|
|
points.append(p)
|
|
return points
|
|
|
|
points = property(_get_points, doc="")
|
|
|
|
def _get_selected(self):
|
|
return self._node.selected
|
|
|
|
def _set_selected(self, value):
|
|
self._node.selected = value
|
|
|
|
selected = property(_get_selected, _set_selected, doc="")
|
|
|
|
def move(self, (x, y)):
|
|
x, y = roundPt((x, y))
|
|
self._node.Shift(Point(x, y))
|
|
if self._isQCurve():
|
|
qOff = self._qOffCurve
|
|
for node in qOff:
|
|
node.Shift(Point(x, y))
|
|
|
|
def copy(self, aParent=None):
|
|
"""Copy this object -- result is an ObjectsRF flavored object.
|
|
There is no way to make this work using FontLab objects.
|
|
Copy is mainly used for glyphmath.
|
|
"""
|
|
raise RoboFabError, "copy() for objectsFL.RSegment is not implemented."
|
|
|
|
|
|
|
|
class RPoint(BasePoint):
|
|
|
|
_title = "FLPoint"
|
|
|
|
def __init__(self, pointIndex):
|
|
#BasePoint.__init__(self)
|
|
self._pointIndex = pointIndex
|
|
self._qOffIndex = None
|
|
|
|
def _get__parentGlyph(self):
|
|
return self._parentContour.getParent()
|
|
|
|
_parentGlyph = property(_get__parentGlyph, doc="")
|
|
|
|
def _get__parentContour(self):
|
|
return self._parentSegment.getParent()
|
|
|
|
_parentContour = property(_get__parentContour, doc="")
|
|
|
|
def _get__parentSegment(self):
|
|
return self.getParent()
|
|
|
|
_parentSegment = property(_get__parentSegment, doc="")
|
|
|
|
def _get__node(self):
|
|
if self._qOffIndex is not None:
|
|
return self.getParent()._qOffCurve[self._qOffIndex]
|
|
return self.getParent()._node
|
|
|
|
_node = property(_get__node, doc="")
|
|
|
|
def _get__point(self):
|
|
return self._node.points[self._pointIndex]
|
|
|
|
_point = property(_get__point, doc="")
|
|
|
|
def _get_x(self):
|
|
return self._point.x
|
|
|
|
def _set_x(self, value):
|
|
value = int(round(value))
|
|
self._point.x = value
|
|
|
|
x = property(_get_x, _set_x, doc="")
|
|
|
|
def _get_y(self):
|
|
return self._point.y
|
|
|
|
def _set_y(self, value):
|
|
value = int(round(value))
|
|
self._point.y = value
|
|
|
|
y = property(_get_y, _set_y, doc="")
|
|
|
|
def _get_type(self):
|
|
if self._pointIndex == 0:
|
|
# FL store quad contour data as a list of off curves and lines
|
|
# (see note in RContour._buildSegments). So, we need to do
|
|
# a bit of trickery to return a decent point type.
|
|
# if the straight FL node type is off curve, it is a loose
|
|
# quad off curve. return that.
|
|
tp = _flToRFSegmentType(self._node.type)
|
|
if tp == OFFCURVE:
|
|
return OFFCURVE
|
|
# otherwise we are dealing with an on curve. in this case,
|
|
# we attempt to get the parent segment type and return it.
|
|
segment = self.getParent()
|
|
if segment is not None:
|
|
return segment.type
|
|
# we must not have a segment, fall back to straight conversion
|
|
return tp
|
|
return OFFCURVE
|
|
|
|
type = property(_get_type, doc="")
|
|
|
|
def _set_selected(self, value):
|
|
if self._pointIndex == 0:
|
|
self._node.selected = value
|
|
|
|
def _get_selected(self):
|
|
if self._pointIndex == 0:
|
|
return self._node.selected
|
|
return False
|
|
|
|
selected = property(_get_selected, _set_selected, doc="")
|
|
|
|
def move(self, (x, y)):
|
|
x, y = roundPt((x, y))
|
|
self._point.Shift(Point(x, y))
|
|
|
|
def scale(self, (x, y), center=(0, 0)):
|
|
centerX, centerY = roundPt(center)
|
|
point = self._point
|
|
point.x, point.y = _scalePointFromCenter((point.x, point.y), (x, y), (centerX, centerY))
|
|
|
|
def copy(self, aParent=None):
|
|
"""Copy this object -- result is an ObjectsRF flavored object.
|
|
There is no way to make this work using FontLab objects.
|
|
Copy is mainly used for glyphmath.
|
|
"""
|
|
raise RoboFabError, "copy() for objectsFL.RPoint is not implemented."
|
|
|
|
|
|
class RBPoint(BaseBPoint):
|
|
|
|
_title = "FLBPoint"
|
|
|
|
def __init__(self, segmentIndex):
|
|
#BaseBPoint.__init__(self)
|
|
self._segmentIndex = segmentIndex
|
|
|
|
def _get__parentSegment(self):
|
|
return self.getParent().segments[self._segmentIndex]
|
|
|
|
_parentSegment = property(_get__parentSegment, doc="")
|
|
|
|
def _get_index(self):
|
|
return self._segmentIndex
|
|
|
|
index = property(_get_index, doc="")
|
|
|
|
def _get_selected(self):
|
|
return self._parentSegment.selected
|
|
|
|
def _set_selected(self, value):
|
|
self._parentSegment.selected = value
|
|
|
|
selected = property(_get_selected, _set_selected, doc="")
|
|
|
|
def copy(self, aParent=None):
|
|
"""Copy this object -- result is an ObjectsRF flavored object.
|
|
There is no way to make this work using FontLab objects.
|
|
Copy is mainly used for glyphmath.
|
|
"""
|
|
raise RoboFabError, "copy() for objectsFL.RBPoint is not implemented."
|
|
|
|
|
|
class RComponent(BaseComponent):
|
|
|
|
"""RoboFab wrapper for FL Component object"""
|
|
|
|
_title = "FLComponent"
|
|
|
|
def __init__(self, flComponent, index):
|
|
BaseComponent.__init__(self)
|
|
self._object = flComponent
|
|
self._index=index
|
|
|
|
def _get_index(self):
|
|
return self._index
|
|
|
|
index = property(_get_index, doc="index of component")
|
|
|
|
def _get_baseGlyph(self):
|
|
return self._object.parent.parent[self._object.index].name
|
|
|
|
baseGlyph = property(_get_baseGlyph, doc="")
|
|
|
|
def _get_offset(self):
|
|
return (int(self._object.delta.x), int(self._object.delta.y))
|
|
|
|
def _set_offset(self, value):
|
|
value = roundPt((value[0], value[1]))
|
|
self._object.delta=Point(value[0], value[1])
|
|
|
|
offset = property(_get_offset, _set_offset, doc="the offset of the component")
|
|
|
|
def _get_scale(self):
|
|
return (self._object.scale.x, self._object.scale.y)
|
|
|
|
def _set_scale(self, (x, y)):
|
|
self._object.scale=Point(x, y)
|
|
|
|
scale = property(_get_scale, _set_scale, doc="the scale of the component")
|
|
|
|
def move(self, (x, y)):
|
|
"""Move the component"""
|
|
x, y = roundPt((x, y))
|
|
self._object.delta=Point(self._object.delta.x+x, self._object.delta.y+y)
|
|
|
|
def decompose(self):
|
|
"""Decompose the component"""
|
|
self._object.Paste()
|
|
|
|
def copy(self, aParent=None):
|
|
"""Copy this object -- result is an ObjectsRF flavored object.
|
|
There is no way to make this work using FontLab objects.
|
|
Copy is mainly used for glyphmath.
|
|
"""
|
|
raise RoboFabError, "copy() for objectsFL.RComponent is not implemented."
|
|
|
|
|
|
|
|
class RAnchor(BaseAnchor):
|
|
"""RoboFab wrapper for FL Anchor object"""
|
|
|
|
_title = "FLAnchor"
|
|
|
|
def __init__(self, flAnchor, index):
|
|
BaseAnchor.__init__(self)
|
|
self._object = flAnchor
|
|
self._index = index
|
|
|
|
def _get_y(self):
|
|
return self._object.y
|
|
|
|
def _set_y(self, value):
|
|
self._object.y = int(round(value))
|
|
|
|
y = property(_get_y, _set_y, doc="y")
|
|
|
|
def _get_x(self):
|
|
return self._object.x
|
|
|
|
def _set_x(self, value):
|
|
self._object.x = int(round(value))
|
|
|
|
x = property(_get_x, _set_x, doc="x")
|
|
|
|
def _get_name(self):
|
|
return self._object.name
|
|
|
|
def _set_name(self, value):
|
|
self._object.name = value
|
|
|
|
name = property(_get_name, _set_name, doc="name")
|
|
|
|
def _get_mark(self):
|
|
return self._object.mark
|
|
|
|
def _set_mark(self, value):
|
|
self._object.mark = value
|
|
|
|
mark = property(_get_mark, _set_mark, doc="mark")
|
|
|
|
def _get_index(self):
|
|
return self._index
|
|
|
|
index = property(_get_index, doc="index of the anchor")
|
|
|
|
def _get_position(self):
|
|
return (self._object.x, self._object.y)
|
|
|
|
def _set_position(self, value):
|
|
value = roundPt((value[0], value[1]))
|
|
self._object.x=value[0]
|
|
self._object.y=value[1]
|
|
|
|
position = property(_get_position, _set_position, doc="position of the anchor")
|
|
|
|
|
|
|
|
class RGuide(BaseGuide):
|
|
|
|
"""RoboFab wrapper for FL Guide object"""
|
|
|
|
_title = "FLGuide"
|
|
|
|
def __init__(self, flGuide, index):
|
|
BaseGuide.__init__(self)
|
|
self._object = flGuide
|
|
self._index = index
|
|
|
|
def __repr__(self):
|
|
# this is a doozy!
|
|
parent = "unknown_parent"
|
|
parentObject = self.getParent()
|
|
if parentObject is not None:
|
|
# do we have a font?
|
|
try:
|
|
parent = parentObject.info.postscriptFullName
|
|
except AttributeError:
|
|
# or do we have a glyph?
|
|
try:
|
|
parent = parentObject.name
|
|
# we must be an orphan
|
|
except AttributeError: pass
|
|
return "<Robofab guide wrapper for %s>"%parent
|
|
|
|
def _get_position(self):
|
|
return self._object.position
|
|
|
|
def _set_position(self, value):
|
|
self._object.position = value
|
|
|
|
position = property(_get_position, _set_position, doc="position")
|
|
|
|
def _get_angle(self):
|
|
return self._object.angle
|
|
|
|
def _set_angle(self, value):
|
|
self._object.angle = value
|
|
|
|
angle = property(_get_angle, _set_angle, doc="angle")
|
|
|
|
def _get_index(self):
|
|
return self._index
|
|
|
|
index = property(_get_index, doc="index of the guide")
|
|
|
|
|
|
class RGroups(BaseGroups):
|
|
|
|
"""RoboFab wrapper for FL group data"""
|
|
|
|
_title = "FLGroups"
|
|
|
|
def __init__(self, aDict):
|
|
self.update(aDict)
|
|
|
|
def __setitem__(self, key, value):
|
|
# override baseclass so that data is stored in FL classes
|
|
if not isinstance(key, str):
|
|
raise RoboFabError, 'key must be a string'
|
|
if not isinstance(value, list):
|
|
raise RoboFabError, 'group must be a list'
|
|
super(RGroups, self).__setitem__(key, value)
|
|
self._setFLGroups()
|
|
|
|
def __delitem__(self, key):
|
|
# override baseclass so that data is stored in FL classes
|
|
super(RGroups, self).__delitem__(key, value)
|
|
self._setFLGroups()
|
|
|
|
def _setFLGroups(self):
|
|
# set the group data into the font.
|
|
if self.getParent() is not None:
|
|
groups = []
|
|
for i in self.keys():
|
|
value = ' '.join(self[i])
|
|
groups.append(': '.join([i, value]))
|
|
groups.sort()
|
|
self.getParent().naked().classes = groups
|
|
|
|
def update(self, aDict):
|
|
# override baseclass so that data is stored in FL classes
|
|
super(RGroups, self).update(aDict)
|
|
self._setFLGroups()
|
|
|
|
def clear(self):
|
|
# override baseclass so that data is stored in FL classes
|
|
super(RGroups, self).clear()
|
|
self._setFLGroups()
|
|
|
|
def pop(self, key):
|
|
# override baseclass so that data is stored in FL classes
|
|
i = super(RGroups, self).pop(key)
|
|
self._setFLGroups()
|
|
return i
|
|
|
|
def popitem(self):
|
|
# override baseclass so that data is stored in FL classes
|
|
i = super(RGroups, self).popitem()
|
|
self._setFLGroups()
|
|
return i
|
|
|
|
def setdefault(self, key, value=None):
|
|
# override baseclass so that data is stored in FL classes
|
|
i = super(RGroups, self).setdefault(key, value)
|
|
self._setFLGroups()
|
|
return i
|
|
|
|
|
|
class RKerning(BaseKerning):
|
|
|
|
"""RoboFab wrapper for FL Kerning data"""
|
|
|
|
_title = "FLKerning"
|
|
|
|
def __setitem__(self, pair, value):
|
|
if not isinstance(pair, tuple):
|
|
raise RoboFabError, 'kerning pair must be a tuple: (left, right)'
|
|
else:
|
|
if len(pair) != 2:
|
|
raise RoboFabError, 'kerning pair must be a tuple: (left, right)'
|
|
else:
|
|
if value == 0:
|
|
if self._kerning.get(pair) is not None:
|
|
#see note about setting kerning values to 0 below
|
|
self._setFLKerning(pair, 0)
|
|
del self._kerning[pair]
|
|
else:
|
|
#self._kerning[pair] = value
|
|
self._setFLKerning(pair, value)
|
|
|
|
def _setFLKerning(self, pair, value):
|
|
# write a pair back into the font
|
|
#
|
|
# this is fairly speedy, but setting a pair to 0 is roughly
|
|
# 2-3 times slower than setting a real value. this is because
|
|
# of all the hoops that must be jumped through to keep FL
|
|
# from storing kerning pairs with a value of 0.
|
|
parentFont = self.getParent().naked()
|
|
left = parentFont[pair[0]]
|
|
right = parentFont.FindGlyph(pair[1])
|
|
# the left glyph doesn not exist
|
|
if left is None:
|
|
return
|
|
# the right glyph doesn not exist
|
|
if right == -1:
|
|
return
|
|
self._kerning[pair] = value
|
|
leftName = pair[0]
|
|
value = int(round(value))
|
|
# pairs set to 0 need to be handled carefully. FL will allow
|
|
# for pairs to have a value of 0 (!?), so we must catch them
|
|
# when they pop up and make sure that the pair is actually
|
|
# removed from the font.
|
|
if value == 0:
|
|
foundPair = False
|
|
# if the value is 0, we don't need to construct a pair
|
|
# we just need to make sure that the pair is not in the list
|
|
pairs = []
|
|
# so, go through all the pairs and add them to a new list
|
|
for flPair in left.kerning:
|
|
# we have found the pair. flag it.
|
|
if flPair.key == right:
|
|
foundPair = True
|
|
# not the pair. add it to the list.
|
|
else:
|
|
pairs.append((flPair.key, flPair.value))
|
|
# if we found it, write it back to the glyph.
|
|
if foundPair:
|
|
left.kerning = []
|
|
for p in pairs:
|
|
new = KerningPair(p[0], p[1])
|
|
left.kerning.append(new)
|
|
else:
|
|
# non-zero pairs are a bit easier to handle
|
|
# we just need to look to see if the pair exists
|
|
# if so, change the value and stop the loop.
|
|
# if not, add a new pair to the glyph
|
|
self._kerning[pair] = value
|
|
foundPair = False
|
|
for flPair in left.kerning:
|
|
if flPair.key == right:
|
|
flPair.value = value
|
|
foundPair = True
|
|
break
|
|
if not foundPair:
|
|
p = KerningPair(right, value)
|
|
left.kerning.append(p)
|
|
|
|
def update(self, kerningDict):
|
|
"""replace kerning data with the data in the given kerningDict"""
|
|
# override base class here for speed
|
|
parentFont = self.getParent().naked()
|
|
# add existing data to the new kerning dict is not being replaced
|
|
for pair in self.keys():
|
|
if not kerningDict.has_key(pair):
|
|
kerningDict[pair] = self._kerning[pair]
|
|
# now clear the existing kerning to make sure that
|
|
# all the kerning in residing in the glyphs is gone
|
|
self.clear()
|
|
self._kerning = kerningDict
|
|
kDict = {}
|
|
# nest the pairs into a dict keyed by the left glyph
|
|
# {'A':{'A':-10, 'B':20, ...}, 'B':{...}, ...}
|
|
for left, right in kerningDict.keys():
|
|
value = kerningDict[left, right]
|
|
if not left in kDict:
|
|
kDict[left] = {}
|
|
kDict[left][right] = value
|
|
for left in kDict.keys():
|
|
leftGlyph = parentFont[left]
|
|
if leftGlyph is not None:
|
|
for right in kDict[left].keys():
|
|
value = kDict[left][right]
|
|
if value != 0:
|
|
rightIndex = parentFont.FindGlyph(right)
|
|
if rightIndex != -1:
|
|
p = KerningPair(rightIndex, value)
|
|
leftGlyph.kerning.append(p)
|
|
|
|
def clear(self):
|
|
"""clear all kerning"""
|
|
# override base class here for speed
|
|
self._kerning = {}
|
|
for glyph in self.getParent().naked().glyphs:
|
|
glyph.kerning = []
|
|
|
|
def __add__(self, other):
|
|
"""Math operations on FL Kerning objects return RF Kerning objects
|
|
as they need to be orphaned objects and FL can't deal with that."""
|
|
from sets import Set
|
|
from robofab.objects.objectsRF import RKerning as _RKerning
|
|
new = _RKerning()
|
|
k = Set(self.keys()) | Set(other.keys())
|
|
for key in k:
|
|
new[key] = self.get(key, 0) + other.get(key, 0)
|
|
return new
|
|
|
|
def __sub__(self, other):
|
|
"""Math operations on FL Kerning objects return RF Kerning objects
|
|
as they need to be orphaned objects and FL can't deal with that."""
|
|
from sets import Set
|
|
from robofab.objects.objectsRF import RKerning as _RKerning
|
|
new = _RKerning()
|
|
k = Set(self.keys()) | Set(other.keys())
|
|
for key in k:
|
|
new[key] = self.get(key, 0) - other.get(key, 0)
|
|
return new
|
|
|
|
def __mul__(self, factor):
|
|
"""Math operations on FL Kerning objects return RF Kerning objects
|
|
as they need to be orphaned objects and FL can't deal with that."""
|
|
from robofab.objects.objectsRF import RKerning as _RKerning
|
|
new = _RKerning()
|
|
for name, value in self.items():
|
|
new[name] = value * factor
|
|
return new
|
|
|
|
__rmul__ = __mul__
|
|
|
|
def __div__(self, factor):
|
|
"""Math operations on FL Kerning objects return RF Kerning objects
|
|
as they need to be orphaned objects and FL can't deal with that."""
|
|
if factor == 0:
|
|
raise ZeroDivisionError
|
|
return self.__mul__(1.0/factor)
|
|
|
|
|
|
class RLib(BaseLib):
|
|
|
|
"""RoboFab wrapper for FL lib"""
|
|
|
|
# XXX: As of FL 4.6 the customdata field in glyph objects is busted.
|
|
# storing anything there causes the glyph to become uneditable.
|
|
# however, the customdata field in font objects is stable.
|
|
|
|
def __init__(self, aDict):
|
|
self.update(aDict)
|
|
|
|
def __setitem__(self, key, value):
|
|
# override baseclass so that data is stored in customdata field
|
|
super(RLib, self).__setitem__(key, value)
|
|
self._stashLib()
|
|
|
|
def __delitem__(self, key):
|
|
# override baseclass so that data is stored in customdata field
|
|
super(RLib, self).__delitem__(key)
|
|
self._stashLib()
|
|
|
|
def _stashLib(self):
|
|
# write the plist into the customdata field of the FL object
|
|
if self.getParent() is None:
|
|
return
|
|
if not self:
|
|
data = None
|
|
elif len(self) == 1 and "org.robofab.fontlab.customdata" in self:
|
|
data = self["org.robofab.fontlab.customdata"].data
|
|
else:
|
|
f = StringIO()
|
|
writePlist(self, f)
|
|
data = f.getvalue()
|
|
f.close()
|
|
parent = self.getParent()
|
|
parent.naked().customdata = data
|
|
|
|
def update(self, aDict):
|
|
# override baseclass so that data is stored in customdata field
|
|
super(RLib, self).update(aDict)
|
|
self._stashLib()
|
|
|
|
def clear(self):
|
|
# override baseclass so that data is stored in customdata field
|
|
super(RLib, self).clear()
|
|
self._stashLib()
|
|
|
|
def pop(self, key):
|
|
# override baseclass so that data is stored in customdata field
|
|
i = super(RLib, self).pop(key)
|
|
self._stashLib()
|
|
return i
|
|
|
|
def popitem(self):
|
|
# override baseclass so that data is stored in customdata field
|
|
i = super(RLib, self).popitem()
|
|
self._stashLib()
|
|
return i
|
|
|
|
def setdefault(self, key, value=None):
|
|
# override baseclass so that data is stored in customdata field
|
|
i = super(RLib, self).setdefault(key, value)
|
|
self._stashLib()
|
|
return i
|
|
|
|
|
|
def _infoMapDict(**kwargs):
|
|
default = dict(
|
|
nakedAttribute=None,
|
|
type=None,
|
|
requiresSetNum=False,
|
|
masterSpecific=False,
|
|
libLocation=None,
|
|
specialGetSet=False
|
|
)
|
|
default.update(kwargs)
|
|
return default
|
|
|
|
def _flipDict(d):
|
|
f = {}
|
|
for k, v in d.items():
|
|
f[v] = k
|
|
return f
|
|
|
|
_styleMapStyleName_fromFL = {
|
|
64 : "regular",
|
|
1 : "italic",
|
|
32 : "bold",
|
|
33 : "bold italic"
|
|
}
|
|
_styleMapStyleName_toFL = _flipDict(_styleMapStyleName_fromFL)
|
|
|
|
_postscriptWindowsCharacterSet_fromFL = {
|
|
0 : 1,
|
|
1 : 2,
|
|
2 : 3,
|
|
77 : 4,
|
|
128 : 5,
|
|
129 : 6,
|
|
130 : 7,
|
|
134 : 8,
|
|
136 : 9,
|
|
161 : 10,
|
|
162 : 11,
|
|
163 : 12,
|
|
177 : 13,
|
|
178 : 14,
|
|
186 : 15,
|
|
200 : 16,
|
|
204 : 17,
|
|
222 : 18,
|
|
238 : 19,
|
|
255 : 20,
|
|
}
|
|
_postscriptWindowsCharacterSet_toFL = _flipDict(_postscriptWindowsCharacterSet_fromFL)
|
|
|
|
_openTypeOS2Type_toFL = {
|
|
1 : 0x0002,
|
|
2 : 0x0004,
|
|
3 : 0x0008,
|
|
8 : 0x0100,
|
|
9 : 0x0200,
|
|
}
|
|
_openTypeOS2Type_fromFL = _flipDict(_openTypeOS2Type_toFL)
|
|
|
|
_openTypeOS2WidthClass_fromFL = {
|
|
"Ultra-condensed" : 1,
|
|
"Extra-condensed" : 2,
|
|
"Condensed" : 3,
|
|
"Semi-condensed" : 4,
|
|
"Medium (normal)" : 5,
|
|
"Semi-expanded" : 6,
|
|
"Expanded" : 7,
|
|
"Extra-expanded" : 8,
|
|
"Ultra-expanded" : 9,
|
|
}
|
|
_openTypeOS2WidthClass_toFL = _flipDict(_openTypeOS2WidthClass_fromFL)
|
|
|
|
_postscriptHintAttributes = set((
|
|
"postscriptBlueValues",
|
|
"postscriptOtherBlues",
|
|
"postscriptFamilyBlues",
|
|
"postscriptFamilyOtherBlues",
|
|
"postscriptStemSnapH",
|
|
"postscriptStemSnapV",
|
|
))
|
|
|
|
|
|
class RInfo(BaseInfo):
|
|
|
|
"""RoboFab wrapper for FL Font Info"""
|
|
|
|
_title = "FLInfo"
|
|
|
|
_ufoToFLAttrMapping = {
|
|
"familyName" : _infoMapDict(valueType=str, nakedAttribute="family_name"),
|
|
"styleName" : _infoMapDict(valueType=str, nakedAttribute="style_name"),
|
|
"styleMapFamilyName" : _infoMapDict(valueType=str, nakedAttribute="menu_name"),
|
|
"styleMapStyleName" : _infoMapDict(valueType=str, nakedAttribute="font_style", specialGetSet=True),
|
|
"versionMajor" : _infoMapDict(valueType=int, nakedAttribute="version_major"),
|
|
"versionMinor" : _infoMapDict(valueType=int, nakedAttribute="version_minor"),
|
|
"year" : _infoMapDict(valueType=int, nakedAttribute="year"),
|
|
"copyright" : _infoMapDict(valueType=str, nakedAttribute="copyright"),
|
|
"trademark" : _infoMapDict(valueType=str, nakedAttribute="trademark"),
|
|
"unitsPerEm" : _infoMapDict(valueType=int, nakedAttribute="upm"),
|
|
"descender" : _infoMapDict(valueType=int, nakedAttribute="descender", masterSpecific=True),
|
|
"xHeight" : _infoMapDict(valueType=int, nakedAttribute="x_height", masterSpecific=True),
|
|
"capHeight" : _infoMapDict(valueType=int, nakedAttribute="cap_height", masterSpecific=True),
|
|
"ascender" : _infoMapDict(valueType=int, nakedAttribute="ascender", masterSpecific=True),
|
|
"italicAngle" : _infoMapDict(valueType=float, nakedAttribute="italic_angle"),
|
|
"note" : _infoMapDict(valueType=str, nakedAttribute="note"),
|
|
"openTypeHeadCreated" : _infoMapDict(valueType=str, nakedAttribute=None, specialGetSet=True), # i can't figure out the ttinfo.head_creation values
|
|
"openTypeHeadLowestRecPPEM" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.head_lowest_rec_ppem"),
|
|
"openTypeHeadFlags" : _infoMapDict(valueType="intList", nakedAttribute=None), # There is an attribute (ttinfo.head_flags), but no user interface.
|
|
"openTypeHheaAscender" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.hhea_ascender"),
|
|
"openTypeHheaDescender" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.hhea_descender"),
|
|
"openTypeHheaLineGap" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.hhea_line_gap"),
|
|
"openTypeHheaCaretSlopeRise" : _infoMapDict(valueType=int, nakedAttribute=None),
|
|
"openTypeHheaCaretSlopeRun" : _infoMapDict(valueType=int, nakedAttribute=None),
|
|
"openTypeHheaCaretOffset" : _infoMapDict(valueType=int, nakedAttribute=None),
|
|
"openTypeNameDesigner" : _infoMapDict(valueType=str, nakedAttribute="designer"),
|
|
"openTypeNameDesignerURL" : _infoMapDict(valueType=str, nakedAttribute="designer_url"),
|
|
"openTypeNameManufacturer" : _infoMapDict(valueType=str, nakedAttribute="source"),
|
|
"openTypeNameManufacturerURL" : _infoMapDict(valueType=str, nakedAttribute="vendor_url"),
|
|
"openTypeNameLicense" : _infoMapDict(valueType=str, nakedAttribute="license"),
|
|
"openTypeNameLicenseURL" : _infoMapDict(valueType=str, nakedAttribute="license_url"),
|
|
"openTypeNameVersion" : _infoMapDict(valueType=str, nakedAttribute="tt_version"),
|
|
"openTypeNameUniqueID" : _infoMapDict(valueType=str, nakedAttribute="tt_u_id"),
|
|
"openTypeNameDescription" : _infoMapDict(valueType=str, nakedAttribute="notice"),
|
|
"openTypeNamePreferredFamilyName" : _infoMapDict(valueType=str, nakedAttribute="pref_family_name"),
|
|
"openTypeNamePreferredSubfamilyName" : _infoMapDict(valueType=str, nakedAttribute="pref_style_name"),
|
|
"openTypeNameCompatibleFullName" : _infoMapDict(valueType=str, nakedAttribute="mac_compatible"),
|
|
"openTypeNameSampleText" : _infoMapDict(valueType=str, nakedAttribute=None),
|
|
"openTypeNameWWSFamilyName" : _infoMapDict(valueType=str, nakedAttribute=None),
|
|
"openTypeNameWWSSubfamilyName" : _infoMapDict(valueType=str, nakedAttribute=None),
|
|
"openTypeOS2WidthClass" : _infoMapDict(valueType=int, nakedAttribute="width"),
|
|
"openTypeOS2WeightClass" : _infoMapDict(valueType=int, nakedAttribute="weight_code", specialGetSet=True),
|
|
"openTypeOS2Selection" : _infoMapDict(valueType="intList", nakedAttribute=None), # ttinfo.os2_fs_selection only returns 0
|
|
"openTypeOS2VendorID" : _infoMapDict(valueType=str, nakedAttribute="vendor"),
|
|
"openTypeOS2Panose" : _infoMapDict(valueType="intList", nakedAttribute="panose", specialGetSet=True),
|
|
"openTypeOS2FamilyClass" : _infoMapDict(valueType="intList", nakedAttribute="ttinfo.os2_s_family_class", specialGetSet=True),
|
|
"openTypeOS2UnicodeRanges" : _infoMapDict(valueType="intList", nakedAttribute="unicoderanges"),
|
|
"openTypeOS2CodePageRanges" : _infoMapDict(valueType="intList", nakedAttribute="codepages"),
|
|
"openTypeOS2TypoAscender" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_s_typo_ascender"),
|
|
"openTypeOS2TypoDescender" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_s_typo_descender"),
|
|
"openTypeOS2TypoLineGap" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_s_typo_line_gap"),
|
|
"openTypeOS2WinAscent" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_us_win_ascent"),
|
|
"openTypeOS2WinDescent" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_us_win_descent", specialGetSet=True),
|
|
"openTypeOS2Type" : _infoMapDict(valueType="intList", nakedAttribute="ttinfo.os2_fs_type", specialGetSet=True),
|
|
"openTypeOS2SubscriptXSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_subscript_x_size"),
|
|
"openTypeOS2SubscriptYSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_subscript_y_size"),
|
|
"openTypeOS2SubscriptXOffset" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_subscript_x_offset"),
|
|
"openTypeOS2SubscriptYOffset" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_subscript_y_offset"),
|
|
"openTypeOS2SuperscriptXSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_superscript_x_size"),
|
|
"openTypeOS2SuperscriptYSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_superscript_y_size"),
|
|
"openTypeOS2SuperscriptXOffset" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_superscript_x_offset"),
|
|
"openTypeOS2SuperscriptYOffset" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_superscript_y_offset"),
|
|
"openTypeOS2StrikeoutSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_strikeout_size"),
|
|
"openTypeOS2StrikeoutPosition" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_strikeout_position"),
|
|
"openTypeVheaVertTypoAscender" : _infoMapDict(valueType=int, nakedAttribute=None),
|
|
"openTypeVheaVertTypoDescender" : _infoMapDict(valueType=int, nakedAttribute=None),
|
|
"openTypeVheaVertTypoLineGap" : _infoMapDict(valueType=int, nakedAttribute=None),
|
|
"openTypeVheaCaretSlopeRise" : _infoMapDict(valueType=int, nakedAttribute=None),
|
|
"openTypeVheaCaretSlopeRun" : _infoMapDict(valueType=int, nakedAttribute=None),
|
|
"openTypeVheaCaretOffset" : _infoMapDict(valueType=int, nakedAttribute=None),
|
|
"postscriptFontName" : _infoMapDict(valueType=str, nakedAttribute="font_name"),
|
|
"postscriptFullName" : _infoMapDict(valueType=str, nakedAttribute="full_name"),
|
|
"postscriptSlantAngle" : _infoMapDict(valueType=float, nakedAttribute="slant_angle"),
|
|
"postscriptUniqueID" : _infoMapDict(valueType=int, nakedAttribute="unique_id"),
|
|
"postscriptUnderlineThickness" : _infoMapDict(valueType=int, nakedAttribute="underline_thickness"),
|
|
"postscriptUnderlinePosition" : _infoMapDict(valueType=int, nakedAttribute="underline_position"),
|
|
"postscriptIsFixedPitch" : _infoMapDict(valueType="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:
|
|
raise ValueError("FontLab can only handle positive values for openTypeOS2WinDescent.")
|
|
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.")
|
|
|