fonttools/Lib/robofab/objects/objectsBase.py
Tal Leming f84f2c50ba Merge recent trunk changes.
git-svn-id: http://svn.robofab.com/branches/ufo3k@261 b5fa9d6c-a76f-4ffd-b3cb-f825fc41095c
2011-09-10 17:30:28 +00:00

3438 lines
104 KiB
Python
Executable File

"""
Base classes for the Unified Font Objects (UFO),
a series of classes that deal with fonts, glyphs,
contours and related things.
Unified Font Objects are:
- platform independent
- application independent
About Object Inheritance:
objectsFL and objectsRF objects inherit
methods and attributes from these objects.
In other words, if it is in here, you can
do it with the objectsFL and objectsRF.
"""
from __future__ import generators
from __future__ import division
from warnings import warn
import math
import copy
from robofab import ufoLib
from robofab import RoboFabError
from fontTools.misc.arrayTools import updateBounds, pointInRect, unionRect, sectRect
from fontTools.pens.basePen import AbstractPen
#constants for dealing with segments, points and bPoints
MOVE = 'move'
LINE = 'line'
CORNER = 'corner'
CURVE = 'curve'
QCURVE = 'qcurve'
OFFCURVE = 'offcurve'
DEGREE = 180 / math.pi
# the key for the postscript hint data stored in the UFO
postScriptHintDataLibKey = "org.robofab.postScriptHintData"
# from http://svn.typesupply.com/packages/fontMath/mathFunctions.py
def add(v1, v2):
return v1 + v2
def sub(v1, v2):
return v1 - v2
def mul(v, f):
return v * f
def div(v, f):
return v / f
def issequence(x):
"Is x a sequence? We say it is if it has a __getitem__ method."
return hasattr(x, '__getitem__')
class BasePostScriptHintValues(object):
""" Base class for postscript hinting information.
"""
def __init__(self, data=None):
if data is not None:
self.fromDict(data)
else:
for name in self._attributeNames.keys():
setattr(self, name, self._attributeNames[name]['default'])
def getParent(self):
"""this method will be overwritten with a weakref if there is a parent."""
return None
def setParent(self, parent):
import weakref
self.getParent = weakref.ref(parent)
def isEmpty(self):
"""Check all attrs and decide if they're all empty."""
empty = True
for name in self._attributeNames:
if getattr(self, name):
empty = False
break
return empty
def clear(self):
"""Set all attributes to default / empty"""
for name in self._attributeNames:
setattr(self, name, self._attributeNames[name]['default'])
def _loadFromLib(self, lib):
data = lib.get(postScriptHintDataLibKey)
if data is not None:
self.fromDict(data)
def _saveToLib(self, lib):
parent = self.getParent()
if parent is not None:
parent.setChanged(True)
hintsDict = self.asDict()
if hintsDict:
lib[postScriptHintDataLibKey] = hintsDict
def fromDict(self, data):
for name in self._attributeNames:
if name in data:
setattr(self, name, data[name])
def asDict(self):
d = {}
for name in self._attributeNames:
try:
value = getattr(self, name)
except AttributeError:
print "%s attribute not supported"%name
continue
if value:
d[name] = getattr(self, name)
return d
def update(self, other):
assert isinstance(other, BasePostScriptHintValues)
for name in self._attributeNames.keys():
v = getattr(other, name)
if v is not None:
setattr(self, name, v)
def __repr__(self):
return "<Base PS Hint Data>"
def copy(self, aParent=None):
"""Duplicate this object. Pass an object for parenting if you want."""
n = self.__class__(data=self.asDict())
if aParent is not None:
n.setParent(aParent)
elif self.getParent() is not None:
n.setParent(self.getParent())
dont = ['getParent']
for k in self.__dict__.keys():
if k in dont:
continue
dup = copy.deepcopy(self.__dict__[k])
setattr(n, k, dup)
return n
class BasePostScriptGlyphHintValues(BasePostScriptHintValues):
""" Base class for glyph-level postscript hinting information.
vStems, hStems
"""
_attributeNames = {
# some of these values can have only a certain number of elements
'vHints': {'default': None, 'max':100, 'isVertical':True},
'hHints': {'default': None, 'max':100, 'isVertical':False},
}
def __init__(self, data=None):
if data is not None:
self.fromDict(data)
else:
for name in self._attributeNames.keys():
setattr(self, name, self._attributeNames[name]['default'])
def __repr__(self):
return "<PostScript Glyph Hints Values>"
def round(self):
"""Round the values to reasonable values.
- stems are rounded to int
"""
for name, values in self._attributeNames.items():
v = getattr(self, name)
if v is None:
continue
new = []
for n in v:
new.append((int(round(n[0])), int(round(n[1]))))
setattr(self, name, new)
# math operations for psHint object
# Note: math operations can change integers to floats.
def __add__(self, other):
assert isinstance(other, BasePostScriptHintValues)
copied = self.copy()
self._processMathOne(copied, other, add)
return copied
def __sub__(self, other):
assert isinstance(other, BasePostScriptHintValues)
copied = self.copy()
self._processMathOne(copied, other, sub)
return copied
def __mul__(self, factor):
#if isinstance(factor, tuple):
# factor = factor[0]
copiedInfo = self.copy()
self._processMathTwo(copiedInfo, factor, mul)
return copiedInfo
__rmul__ = __mul__
def __div__(self, factor):
#if isinstance(factor, tuple):
# factor = factor[0]
copiedInfo = self.copy()
self._processMathTwo(copiedInfo, factor, div)
return copiedInfo
__rdiv__ = __div__
def _processMathOne(self, copied, other, funct):
for name, values in self._attributeNames.items():
a = None
b = None
v = None
if hasattr(copied, name):
a = getattr(copied, name)
if hasattr(other, name):
b = getattr(other, name)
if a is not None and b is not None:
if len(a) != len(b):
# can't do math with non matching zones
continue
l = len(a)
for i in range(l):
if v is None:
v = []
ai = a[i]
bi = b[i]
l2 = min(len(ai), len(bi))
v2 = [funct(ai[j], bi[j]) for j in range(l2)]
v.append(v2)
if v is not None:
setattr(copied, name, v)
def _processMathTwo(self, copied, factor, funct):
for name, values in self._attributeNames.items():
a = None
b = None
v = None
isVertical = self._attributeNames[name]['isVertical']
splitFactor = factor
if isinstance(factor, tuple):
#print "mathtwo", name, funct, factor, isVertical
if isVertical:
splitFactor = factor[1]
else:
splitFactor = factor[0]
if hasattr(copied, name):
a = getattr(copied, name)
if a is not None:
for i in range(len(a)):
if v is None:
v = []
v2 = [funct(a[i][j], splitFactor) for j in range(len(a[i]))]
v.append(v2)
if v is not None:
setattr(copied, name, v)
class BasePostScriptFontHintValues(BasePostScriptHintValues):
""" Base class for font-level postscript hinting information.
Blues values, stem values.
"""
_attributeNames = {
# some of these values can have only a certain number of elements
# default: what the value should be when initialised
# max: the maximum number of items this attribute is allowed to have
# isVertical: the vertical relevance
'blueFuzz': {'default': None, 'max':1, 'isVertical':True},
'blueScale': {'default': None, 'max':1, 'isVertical':True},
'blueShift': {'default': None, 'max':1, 'isVertical':True},
'forceBold': {'default': None, 'max':1, 'isVertical':False},
'blueValues': {'default': None, 'max':7, 'isVertical':True},
'otherBlues': {'default': None, 'max':5, 'isVertical':True},
'familyBlues': {'default': None, 'max':7, 'isVertical':True},
'familyOtherBlues': {'default': None, 'max':5, 'isVertical':True},
'vStems': {'default': None, 'max':6, 'isVertical':True},
'hStems': {'default': None, 'max':11, 'isVertical':False},
}
def __init__(self, data=None):
if data is not None:
self.fromDict(data)
def __repr__(self):
return "<PostScript Font Hints Values>"
# route attribute calls to info object
def _bluesToPairs(self, values):
values.sort()
finalValues = []
for value in values:
if not finalValues or len(finalValues[-1]) == 2:
finalValues.append([])
finalValues[-1].append(value)
return finalValues
def _bluesFromPairs(self, values):
finalValues = []
for value1, value2 in values:
finalValues.append(value1)
finalValues.append(value2)
finalValues.sort()
return finalValues
def _get_blueValues(self):
values = self.getParent().info.postscriptBlueValues
if values is None:
values = []
values = self._bluesToPairs(values)
return values
def _set_blueValues(self, values):
if values is None:
values = []
values = self._bluesFromPairs(values)
self.getParent().info.postscriptBlueValues = values
blueValues = property(_get_blueValues, _set_blueValues)
def _get_otherBlues(self):
values = self.getParent().info.postscriptOtherBlues
if values is None:
values = []
values = self._bluesToPairs(values)
return values
def _set_otherBlues(self, values):
if values is None:
values = []
values = self._bluesFromPairs(values)
self.getParent().info.postscriptOtherBlues = values
otherBlues = property(_get_otherBlues, _set_otherBlues)
def _get_familyBlues(self):
values = self.getParent().info.postscriptFamilyBlues
if values is None:
values = []
values = self._bluesToPairs(values)
return values
def _set_familyBlues(self, values):
if values is None:
values = []
values = self._bluesFromPairs(values)
self.getParent().info.postscriptFamilyBlues = values
familyBlues = property(_get_familyBlues, _set_familyBlues)
def _get_familyOtherBlues(self):
values = self.getParent().info.postscriptFamilyOtherBlues
if values is None:
values = []
values = self._bluesToPairs(values)
return values
def _set_familyOtherBlues(self, values):
if values is None:
values = []
values = self._bluesFromPairs(values)
self.getParent().info.postscriptFamilyOtherBlues = values
familyOtherBlues = property(_get_familyOtherBlues, _set_familyOtherBlues)
def _get_vStems(self):
return self.getParent().info.postscriptStemSnapV
def _set_vStems(self, value):
if value is None:
value = []
self.getParent().info.postscriptStemSnapV = list(value)
vStems = property(_get_vStems, _set_vStems)
def _get_hStems(self):
return self.getParent().info.postscriptStemSnapH
def _set_hStems(self, value):
if value is None:
value = []
self.getParent().info.postscriptStemSnapH = list(value)
hStems = property(_get_hStems, _set_hStems)
def _get_blueScale(self):
return self.getParent().info.postscriptBlueScale
def _set_blueScale(self, value):
self.getParent().info.postscriptBlueScale = value
blueScale = property(_get_blueScale, _set_blueScale)
def _get_blueShift(self):
return self.getParent().info.postscriptBlueShift
def _set_blueShift(self, value):
self.getParent().info.postscriptBlueShift = value
blueShift = property(_get_blueShift, _set_blueShift)
def _get_blueFuzz(self):
return self.getParent().info.postscriptBlueFuzz
def _set_blueFuzz(self, value):
self.getParent().info.postscriptBlueFuzz = value
blueFuzz = property(_get_blueFuzz, _set_blueFuzz)
def _get_forceBold(self):
return self.getParent().info.postscriptForceBold
def _set_forceBold(self, value):
self.getParent().info.postscriptForceBold = value
forceBold = property(_get_forceBold, _set_forceBold)
def round(self):
"""Round the values to reasonable values.
- blueScale is not rounded, it is a float
- forceBold is set to False if -0.5 < value < 0.5. Otherwise it will be True.
- blueShift, blueFuzz are rounded to int
- stems are rounded to int
- blues are rounded to int
"""
for name, values in self._attributeNames.items():
if name == "blueScale":
continue
elif name == "forceBold":
v = getattr(self, name)
if v is None:
continue
if -0.5 <= v <= 0.5:
setattr(self, name, False)
else:
setattr(self, name, True)
elif name in ['blueFuzz', 'blueShift']:
v = getattr(self, name)
if v is None:
continue
setattr(self, name, int(round(v)))
elif name in ['hStems', 'vStems']:
v = getattr(self, name)
if v is None:
continue
new = []
for n in v:
new.append(int(round(n)))
setattr(self, name, new)
else:
v = getattr(self, name)
if v is None:
continue
new = []
for n in v:
new.append([int(round(m)) for m in n])
setattr(self, name, new)
class RoboFabInterpolationError(Exception): pass
def _interpolate(a,b,v):
"""interpolate values by factor v"""
return a + (b-a) * v
def _interpolatePt((xa, ya),(xb, yb),v):
"""interpolate point by factor v"""
if not isinstance(v, tuple):
xv = v
yv = v
else:
xv, yv = v
return xa + (xb-xa) * xv, ya + (yb-ya) * yv
def _scalePointFromCenter((pointX, pointY), (scaleX, scaleY), (centerX, centerY)):
"""scale a point from a center point"""
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)
def _box(objectToMeasure, fontObject=None):
"""calculate the bounds of the object and return it as a (xMin, yMin, xMax, yMax)"""
#from fontTools.pens.boundsPen import BoundsPen
from robofab.pens.boundsPen import BoundsPen
boundsPen = BoundsPen(glyphSet=fontObject)
objectToMeasure.draw(boundsPen)
bounds = boundsPen.bounds
if bounds is None:
bounds = (0, 0, 0, 0)
return bounds
def roundPt((x, y)):
"""Round a vector"""
return int(round(x)), int(round(y))
def addPt(ptA, ptB):
"""Add two vectors"""
return ptA[0] + ptB[0], ptA[1] + ptB[1]
def subPt(ptA, ptB):
"""Substract two vectors"""
return ptA[0] - ptB[0], ptA[1] - ptB[1]
def mulPt(ptA, scalar):
"""Multiply a vector with scalar"""
if not isinstance(scalar, tuple):
f1 = scalar
f2 = scalar
else:
f1, f2 = scalar
return ptA[0]*f1, ptA[1]*f2
def relativeBCPIn(anchor, BCPIn):
"""convert absolute incoming bcp value to a relative value"""
return (BCPIn[0] - anchor[0], BCPIn[1] - anchor[1])
def absoluteBCPIn(anchor, BCPIn):
"""convert relative incoming bcp value to an absolute value"""
return (BCPIn[0] + anchor[0], BCPIn[1] + anchor[1])
def relativeBCPOut(anchor, BCPOut):
"""convert absolute outgoing bcp value to a relative value"""
return (BCPOut[0] - anchor[0], BCPOut[1] - anchor[1])
def absoluteBCPOut(anchor, BCPOut):
"""convert relative outgoing bcp value to an absolute value"""
return (BCPOut[0] + anchor[0], BCPOut[1] + anchor[1])
class FuzzyNumber(object):
def __init__(self, value, threshold):
self.value = value
self.threshold = threshold
def __cmp__(self, other):
if abs(self.value - other.value) < self.threshold:
return 0
else:
return cmp(self.value, other.value)
class RBaseObject(object):
"""Base class for wrapper objects"""
attrMap= {}
_title = "RoboFab Wrapper"
def __init__(self):
self._object = {}
self.changed = False # if the object needs to be saved
self.selected = False
def __len__(self):
return len(self._object)
def __repr__(self):
try:
name = `self._object`
except:
name = "None"
return "<%s for %s>" %(self._title, name)
def copy(self, aParent=None):
"""Duplicate this object. Pass an object for parenting if you want."""
n = self.__class__()
if aParent is not None:
n.setParent(aParent)
elif self.getParent() is not None:
n.setParent(self.getParent())
dont = ['getParent']
for k in self.__dict__.keys():
if k in dont:
continue
elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)):
dup = self.__dict__[k].copy(n)
else:
dup = copy.deepcopy(self.__dict__[k])
setattr(n, k, dup)
return n
def round(self):
pass
def isRobofab(self):
"""Presence of this method indicates a Robofab object"""
return 1
def naked(self):
"""Return the wrapped object itself, in case it is needed for direct access."""
return self._object
def setChanged(self, state=True):
self.changed = state
def getParent(self):
"""this method will be overwritten with a weakref if there is a parent."""
return None
def setParent(self, parent):
import weakref
self.getParent = weakref.ref(parent)
def _writeXML(self, writer):
pass
def dump(self, private=False):
"""Print a dump of this object to the std out."""
from robofab.tools.objectDumper import dumpObject
dumpObject(self, private)
class BaseFont(RBaseObject):
"""Base class for all font objects."""
_allFonts = []
def __init__(self):
import weakref
RBaseObject.__init__(self)
self.changed = False # if the object needs to be saved
self._allFonts.append(weakref.ref(self))
self._supportHints = False
def __repr__(self):
try:
name = self.info.postscriptFullName
except AttributeError:
name = "unnamed_font"
return "<RFont font for %s>" %(name)
def __eq__(self, other):
#Compare this font with another, compare if they refer to the same file.
return self._compare(other)
def _compare(self, other):
"""Compare this font to other. RF and FL UFO implementations need
slightly different ways of comparing fonts. This method does the
basic stuff. Start with simple and quick comparisons, then move into
detailed comparisons of glyphs."""
if not hasattr(other, "fileName"):
return False
if self.fileName is not None and self.fileName == other.fileName:
return True
if self.fileName <> other.fileName:
return False
# this will falsely identify two distinct "Untitled" as equal
# so test some more. A lot of work to please some dolt who
# does not save his fonts while running scripts.
try:
if len(self) <> len(other):
return False
except TypeError:
return False
# same name and length. start comparing glyphs
namesSelf = self.keys()
namesOther = other.keys()
namesSelf.sort()
namesOther.sort()
for i in range(len(namesSelf)):
if namesSelf[i] <> namesOther[i]:
return False
for c in self:
if not c == other[c.name]:
return False
return True
def keys(self):
# must be implemented by subclass
raise NotImplementedError
def __iter__(self):
for glyphName in self.keys():
yield self.getGlyph(glyphName)
def __getitem__(self, glyphName):
return self.getGlyph(glyphName)
def __contains__(self, glyphName):
return self.has_key(glyphName)
def _hasChanged(self):
#mark the object as changed
self.setChanged(True)
def update(self):
"""update the font"""
pass
def close(self, save=1):
"""Close the font, saving is optional."""
pass
def round(self):
"""round all of the points in all of the glyphs"""
for glyph in self.glyphs:
glyph.round()
def autoUnicodes(self):
"""Using fontTools.agl, assign Unicode lists to all glyphs in the font"""
for glyph in self:
glyph.autoUnicodes()
def getCharacterMapping(self):
"""Create a dictionary of unicode -> [glyphname, ...] mappings.
Note that this dict is created each time this method is called,
which can make it expensive for larger fonts. All glyphs are loaded.
Note that one glyph can have multiple unicode values,
and a unicode value can have multiple glyphs pointing to it."""
map = {}
for glyph in self:
for u in glyph.unicodes:
if not map.has_key(u):
map[u] = []
map[u].append(glyph.name)
return map
def getReverseComponentMapping(self):
"""
Get a reversed map of component references in the font.
{
'A' : ['Aacute', 'Aring']
'acute' : ['Aacute']
'ring' : ['Aring']
etc.
}
"""
map = {}
for glyph in self:
glyphName = glyph.name
for component in glyph.components:
baseGlyphName = component.baseGlyph
if not map.has_key(baseGlyphName):
map[baseGlyphName] = []
map[baseGlyphName].append(glyphName)
return map
def compileGlyph(self, glyphName, baseName, accentNames, \
adjustWidth=False, preflight=False, printErrors=True):
"""Compile components into a new glyph using components and anchorpoints.
glyphName: the name of the glyph where it all needs to go
baseName: the name of the base glyph
accentNames: a list of accentName, anchorName tuples, [('acute', 'top'), etc]
"""
anchors = {}
errors = {}
baseGlyph = self[baseName]
for anchor in baseGlyph.getAnchors():
anchors[anchor.name] = anchor.position
destGlyph = self.newGlyph(glyphName, clear=True)
destGlyph.appendComponent(baseName)
destGlyph.width = baseGlyph.width
for accentName, anchorName in accentNames:
try:
accent = self[accentName]
except IndexError:
errors["glyph '%s' is missing in font %s"%(accentName, self.info.fullName)] = 1
continue
shift = None
for accentAnchor in accent.getAnchors():
if '_'+anchorName == accentAnchor.name:
shift = anchors[anchorName][0] - accentAnchor.position[0], anchors[anchorName][1] - accentAnchor.position[1]
destGlyph.appendComponent(accentName, offset=shift)
break
if shift is not None:
for accentAnchor in accent.getAnchors():
if accentAnchor.name in anchors:
anchors[accentAnchor.name] = shift[0]+accentAnchor.position[0], shift[1]+accentAnchor.position[1]
if printErrors:
for px in errors.keys():
print px
return destGlyph
def generateGlyph(self, glyphName, replace=1, preflight=False, printErrors=True):
"""Generate a glyph and return it. Assembled from GlyphConstruction.txt"""
from robofab.tools.toolsAll import readGlyphConstructions
con = readGlyphConstructions()
entry = con.get(glyphName, None)
if not entry:
print "glyph '%s' is not listed in the robofab/Data/GlyphConstruction.txt"%(glyphName)
return
baseName = con[glyphName][0]
parts = con[glyphName][1:]
return self.compileGlyph(glyphName, baseName, parts, adjustWidth=1, preflight=preflight, printErrors=printErrors)
def interpolate(self, factor, minFont, maxFont, suppressError=True, analyzeOnly=False, doProgress=False):
"""Traditional interpolation method. Interpolates by factor between minFont and maxFont.
suppressError will supress all tracebacks and analyze only will not perform the interpolation
but it will analyze all glyphs and return a dict of problems."""
from sets import Set
errors = {}
if not isinstance(factor, tuple):
factor = factor, factor
minGlyphNames = minFont.keys()
maxGlyphNames = maxFont.keys()
allGlyphNames = list(Set(minGlyphNames) | Set(maxGlyphNames))
if doProgress:
from robofab.interface.all.dialogs import ProgressBar
progress = ProgressBar('Interpolating...', len(allGlyphNames))
tickCount = 0
# some dimensions and values
self.info.ascender = _interpolate(minFont.info.ascender, maxFont.info.ascender, factor[1])
self.info.descender = _interpolate(minFont.info.descender, maxFont.info.descender, factor[1])
# check for the presence of the glyph in each of the fonts
for glyphName in allGlyphNames:
if doProgress:
progress.label(glyphName)
fatalError = False
if glyphName not in minGlyphNames:
fatalError = True
if not errors.has_key('Missing Glyphs'):
errors['Missing Glyphs'] = []
errors['Missing Glyphs'].append('Interpolation Error: %s not in %s'%(glyphName, minFont.info.postscriptFullName))
if glyphName not in maxGlyphNames:
fatalError = True
if not errors.has_key('Missing Glyphs'):
errors['Missing Glyphs'] = []
errors['Missing Glyphs'].append('Interpolation Error: %s not in %s'%(glyphName, maxFont.info.postscriptFullName))
# if no major problems, proceed.
if not fatalError:
# remove the glyph since FontLab has a problem with
# interpolating an existing glyph that contains
# some contour data.
oldLib = {}
oldMark = None
oldNote = None
if self.has_key(glyphName):
glyph = self[glyphName]
oldLib = dict(glyph.lib)
oldMark = glyph.mark
oldNote = glyph.note
self.removeGlyph(glyphName)
selfGlyph = self.newGlyph(glyphName)
selfGlyph.lib.update(oldLib)
if oldMark != None:
selfGlyph.mark = oldMark
selfGlyph.note = oldNote
min = minFont[glyphName]
max = maxFont[glyphName]
ok, glyphErrors = selfGlyph.interpolate(factor, min, max, suppressError=suppressError, analyzeOnly=analyzeOnly)
if not errors.has_key('Glyph Errors'):
errors['Glyph Errors'] = {}
errors['Glyph Errors'][glyphName] = glyphErrors
if doProgress:
progress.tick(tickCount)
tickCount = tickCount + 1
if doProgress:
progress.close()
return errors
def getGlyphNameToFileNameFunc(self):
funcName = self.lib.get("org.robofab.glyphNameToFileNameFuncName")
if funcName is None:
return None
parts = funcName.split(".")
module = ".".join(parts[:-1])
try:
item = __import__(module)
for sub in parts[1:]:
item = getattr(item, sub)
except (ImportError, AttributeError):
warn("Can't find glyph name to file name converter function, "
"falling back to default scheme (%s)" % funcName, RoboFabWarning)
return None
else:
return item
class BaseGlyph(RBaseObject):
"""Base class for all glyph objects."""
def __init__(self):
RBaseObject.__init__(self)
#self.contours = []
#self.components = []
#self.anchors = []
#self.width = 0
#self.note = None
##self.unicodes = []
#self.selected = None
self.changed = False # if the object needs to be saved
def __repr__(self):
font = "unnamed_font"
glyph = "unnamed_glyph"
fontParent = self.getParent()
if fontParent is not None:
try:
font = fontParent.info.postscriptFullName
except AttributeError:
pass
try:
glyph = self.name
except AttributeError:
pass
return "<RGlyph for %s.%s>" %(font, glyph)
#
# Glyph Math
#
def _getMathData(self):
from robofab.pens.mathPens import GetMathDataPointPen
pen = GetMathDataPointPen()
self.drawPoints(pen)
data = pen.getData()
return data
def _setMathData(self, data, destination=None):
from robofab.pens.mathPens import CurveSegmentFilterPointPen
if destination is None:
newGlyph = self._mathCopy()
else:
newGlyph = destination
newGlyph.clear()
#
# draw the data onto the glyph
pointPen = newGlyph.getPointPen()
filterPen = CurveSegmentFilterPointPen(pointPen)
for contour in data['contours']:
filterPen.beginPath()
for segmentType, pt, smooth, name in contour:
filterPen.addPoint(pt=pt, segmentType=segmentType, smooth=smooth, name=name)
filterPen.endPath()
for baseName, transformation in data['components']:
filterPen.addComponent(baseName, transformation)
for pt, name in data['anchors']:
filterPen.beginPath()
filterPen.addPoint(pt=pt, segmentType="move", smooth=False, name=name)
filterPen.endPath()
newGlyph.width = data['width']
psHints = data.get('psHints')
if psHints is not None:
newGlyph.psHints.update(psHints)
#
return newGlyph
def _getMathDestination(self):
# make a new, empty glyph
return self.__class__()
def _mathCopy(self):
# copy self without contour, component and anchor data
glyph = self._getMathDestination()
glyph.name = self.name
glyph.unicodes = list(self.unicodes)
glyph.width = self.width
glyph.note = self.note
glyph.lib = dict(self.lib)
return glyph
def _processMathOne(self, otherGlyph, funct):
# used by: __add__, __sub__
#
newData = {
'contours':[],
'components':[],
'anchors':[],
'width':None
}
selfData = self._getMathData()
otherData = otherGlyph._getMathData()
#
# contours
selfContours = selfData['contours']
otherContours = otherData['contours']
newContours = newData['contours']
if len(selfContours) > 0:
for contourIndex in xrange(len(selfContours)):
newContours.append([])
selfContour = selfContours[contourIndex]
otherContour = otherContours[contourIndex]
for pointIndex in xrange(len(selfContour)):
segType, pt, smooth, name = selfContour[pointIndex]
newX, newY = funct(selfContour[pointIndex][1], otherContour[pointIndex][1])
newContours[-1].append((segType, (newX, newY), smooth, name))
# anchors
selfAnchors = selfData['anchors']
otherAnchors = otherData['anchors']
newAnchors = newData['anchors']
if len(selfAnchors) > 0:
selfAnchors, otherAnchors = self._mathAnchorCompare(selfAnchors, otherAnchors)
anchorNames = selfAnchors.keys()
for anchorName in anchorNames:
selfAnchorList = selfAnchors[anchorName]
otherAnchorList = otherAnchors[anchorName]
for i in range(len(selfAnchorList)):
selfAnchor = selfAnchorList[i]
otherAnchor = otherAnchorList[i]
newAnchor = funct(selfAnchor, otherAnchor)
newAnchors.append((newAnchor, anchorName))
# components
selfComponents = selfData['components']
otherComponents = otherData['components']
newComponents = newData['components']
if len(selfComponents) > 0:
selfComponents, otherComponents = self._mathComponentCompare(selfComponents, otherComponents)
componentNames = selfComponents.keys()
for componentName in componentNames:
selfComponentList = selfComponents[componentName]
otherComponentList = otherComponents[componentName]
for i in range(len(selfComponentList)):
# transformation breakdown: xScale, xyScale, yxScale, yScale, xOffset, yOffset
selfXScale, selfXYScale, selfYXScale, selfYScale, selfXOffset, selfYOffset = selfComponentList[i]
otherXScale, otherXYScale, otherYXScale, otherYScale, otherXOffset, otherYOffset = otherComponentList[i]
newXScale, newXYScale = funct((selfXScale, selfXYScale), (otherXScale, otherXYScale))
newYXScale, newYScale = funct((selfYXScale, selfYScale), (otherYXScale, otherYScale))
newXOffset, newYOffset = funct((selfXOffset, selfYOffset), (otherXOffset, otherYOffset))
newComponents.append((componentName, (newXScale, newXYScale, newYXScale, newYScale, newXOffset, newYOffset)))
return newData
def _processMathTwo(self, factor, funct):
# used by: __mul__, __div__
#
newData = {
'contours':[],
'components':[],
'anchors':[],
'width':None
}
selfData = self._getMathData()
# contours
selfContours = selfData['contours']
newContours = newData['contours']
for selfContour in selfContours:
newContours.append([])
for segType, pt, smooth, name in selfContour:
newX, newY = funct(pt, factor)
newContours[-1].append((segType, (newX, newY), smooth, name))
# anchors
selfAnchors = selfData['anchors']
newAnchors = newData['anchors']
for pt, anchorName in selfAnchors:
newPt = funct(pt, factor)
newAnchors.append((newPt, anchorName))
# components
selfComponents = selfData['components']
newComponents = newData['components']
for baseName, transformation in selfComponents:
xScale, xyScale, yxScale, yScale, xOffset, yOffset = transformation
newXOffset, newYOffset = funct((xOffset, yOffset), factor)
newXScale, newYScale = funct((xScale, yScale), factor)
newXYScale, newYXScale = funct((xyScale, yxScale), factor)
newComponents.append((baseName, (newXScale, newXYScale, newYXScale, newYScale, newXOffset, newYOffset)))
# return the data
return newData
def _mathAnchorCompare(self, selfMathAnchors, otherMathAnchors):
# collect compatible anchors
from sets import Set
selfAnchors = {}
for pt, name in selfMathAnchors:
if not selfAnchors.has_key(name):
selfAnchors[name] = []
selfAnchors[name].append(pt)
otherAnchors = {}
for pt, name in otherMathAnchors:
if not otherAnchors.has_key(name):
otherAnchors[name] = []
otherAnchors[name].append(pt)
compatAnchors = Set(selfAnchors.keys()) & Set(otherAnchors.keys())
finalSelfAnchors = {}
finalOtherAnchors = {}
for name in compatAnchors:
if not finalSelfAnchors.has_key(name):
finalSelfAnchors[name] = []
if not finalOtherAnchors.has_key(name):
finalOtherAnchors[name] = []
selfList = selfAnchors[name]
otherList = otherAnchors[name]
selfCount = len(selfList)
otherCount = len(otherList)
if selfCount != otherCount:
r = range(min(selfCount, otherCount))
else:
r = range(selfCount)
for i in r:
finalSelfAnchors[name].append(selfList[i])
finalOtherAnchors[name].append(otherList[i])
return finalSelfAnchors, finalOtherAnchors
def _mathComponentCompare(self, selfMathComponents, otherMathComponents):
# collect compatible components
from sets import Set
selfComponents = {}
for baseName, transformation in selfMathComponents:
if not selfComponents.has_key(baseName):
selfComponents[baseName] = []
selfComponents[baseName].append(transformation)
otherComponents = {}
for baseName, transformation in otherMathComponents:
if not otherComponents.has_key(baseName):
otherComponents[baseName] = []
otherComponents[baseName].append(transformation)
compatComponents = Set(selfComponents.keys()) & Set(otherComponents.keys())
finalSelfComponents = {}
finalOtherComponents = {}
for baseName in compatComponents:
if not finalSelfComponents.has_key(baseName):
finalSelfComponents[baseName] = []
if not finalOtherComponents.has_key(baseName):
finalOtherComponents[baseName] = []
selfList = selfComponents[baseName]
otherList = otherComponents[baseName]
selfCount = len(selfList)
otherCount = len(otherList)
if selfCount != otherCount:
r = range(min(selfCount, otherCount))
else:
r = range(selfCount)
for i in r:
finalSelfComponents[baseName].append(selfList[i])
finalOtherComponents[baseName].append(otherList[i])
return finalSelfComponents, finalOtherComponents
def __mul__(self, factor):
assert isinstance(factor, (int, float, tuple)), "Glyphs can only be multiplied by int, float or a 2-tuple."
if not isinstance(factor, tuple):
factor = (factor, factor)
data = self._processMathTwo(factor, mulPt)
data['width'] = self.width * factor[0]
# psHints
if not self.psHints.isEmpty():
newPsHints = self.psHints * factor
data['psHints'] = newPsHints
return self._setMathData(data)
__rmul__ = __mul__
def __div__(self, factor):
assert isinstance(factor, (int, float, tuple)), "Glyphs can only be divided by int, float or a 2-tuple."
# calculate reverse factor, and cause nice ZeroDivisionError if it can't
if isinstance(factor, tuple):
reverse = 1.0/factor[0], 1.0/factor[1]
else:
reverse = 1.0/factor
return self.__mul__(reverse)
def __add__(self, other):
assert isinstance(other, BaseGlyph), "Glyphs can only be added to other glyphs."
data = self._processMathOne(other, addPt)
data['width'] = self.width + other.width
return self._setMathData(data)
def __sub__(self, other):
assert isinstance(other, BaseGlyph), "Glyphs can only be substracted from other glyphs."
data = self._processMathOne(other, subPt)
data['width'] = self.width + other.width
return self._setMathData(data)
#
# Interpolation
#
def interpolate(self, factor, minGlyph, maxGlyph, suppressError=True, analyzeOnly=False):
"""Traditional interpolation method. Interpolates by factor between minGlyph and maxGlyph.
suppressError will supress all tracebacks and analyze only will not perform the interpolation
but it will analyze all glyphs and return a dict of problems."""
if not isinstance(factor, tuple):
factor = factor, factor
fatalError = False
if analyzeOnly:
ok, errors = minGlyph.isCompatible(maxGlyph)
return ok, errors
minData = None
maxData = None
minName = minGlyph.name
maxName = maxGlyph.name
try:
minData = minGlyph._getMathData()
maxData = maxGlyph._getMathData()
newContours = self._interpolateContours(factor, minData['contours'], maxData['contours'])
newComponents = self._interpolateComponents(factor, minData['components'], maxData['components'])
newAnchors = self._interpolateAnchors(factor, minData['anchors'], maxData['anchors'])
newWidth = _interpolate(minGlyph.width, maxGlyph.width, factor[0])
newData = {
'contours':newContours,
'components':newComponents,
'anchors':newAnchors,
'width':newWidth
}
self._setMathData(newData, self)
except IndexError:
if not suppressError:
ok, errors = minGlyph.isCompatible(maxGlyph)
ok = not ok
return ok, errors
self.update()
return False, []
def isCompatible(self, otherGlyph, report=True):
"""Return a bool value if the glyph is compatible with otherGlyph.
With report = True, isCompatible will return a report of what's wrong.
The interpolate method requires absolute equality between contour data.
Absolute equality is preferred among component and anchor data, but
it is NOT required. Interpolation between components and anchors
will only deal with compatible data and incompatible data will be
ignored. This method reflects this system."""
selfName = self.name
selfData = self._getMathData()
otherName = otherGlyph.name
otherData = otherGlyph._getMathData()
compatible, errors = self._isCompatibleInternal(selfName, otherName, selfData, otherData)
if report:
return compatible, errors
return compatible
def _isCompatibleInternal(self, selfName, otherName, selfData, otherData):
fatalError = False
errors = []
## contours
# any contour incompatibilities
# result in fatal errors
selfContours = selfData['contours']
otherContours = otherData['contours']
if len(selfContours) != len(otherContours):
fatalError = True
errors.append("Fatal error: glyph %s and glyph %s don't have the same number of contours." %(selfName, otherName))
else:
for contourIndex in xrange(len(selfContours)):
selfContour = selfContours[contourIndex]
otherContour = otherContours[contourIndex]
if len(selfContour) != len(otherContour):
fatalError = True
errors.append("Fatal error: contour %d in glyph %s and glyph %s don't have the same number of segments." %(contourIndex, selfName, otherName))
## components
# component incompatibilities
# do not result in fatal errors
selfComponents = selfData['components']
otherComponents = otherData['components']
if len(selfComponents) != len(otherComponents):
errors.append("Error: glyph %s and glyph %s don't have the same number of components." %(selfName, otherName))
for componentIndex in xrange(min(len(selfComponents), len(otherComponents))):
selfBaseName, selfTransformation = selfComponents[componentIndex]
otherBaseName, otherTransformation = otherComponents[componentIndex]
if selfBaseName != otherBaseName:
errors.append("Error: component %d in glyph %s and glyph %s don't have the same base glyph." %(componentIndex, selfName, otherName))
## anchors
# anchor incompatibilities
# do not result in fatal errors
selfAnchors = selfData['anchors']
otherAnchors = otherData['anchors']
if len(selfAnchors) != len(otherAnchors):
errors.append("Error: glyph %s and glyph %s don't have the same number of anchors." %(selfName, otherName))
for anchorIndex in xrange(min(len(selfAnchors), len(otherAnchors))):
selfPt, selfAnchorName = selfAnchors[anchorIndex]
otherPt, otherAnchorName = otherAnchors[anchorIndex]
if selfAnchorName != otherAnchorName:
errors.append("Error: anchor %d in glyph %s and glyph %s don't have the same name." %(anchorIndex, selfName, otherName))
return not fatalError, errors
def _interpolateContours(self, factor, minContours, maxContours):
newContours = []
for contourIndex in xrange(len(minContours)):
minContour = minContours[contourIndex]
maxContour = maxContours[contourIndex]
newContours.append([])
for pointIndex in xrange(len(minContour)):
segType, pt, smooth, name = minContour[pointIndex]
minPoint = minContour[pointIndex][1]
maxPoint = maxContour[pointIndex][1]
newX, newY = _interpolatePt(minPoint, maxPoint, factor)
newContours[-1].append((segType, (newX, newY), smooth, name))
return newContours
def _interpolateComponents(self, factor, minComponents, maxComponents):
newComponents = []
minComponents, maxComponents = self._mathComponentCompare(minComponents, maxComponents)
componentNames = minComponents.keys()
for componentName in componentNames:
minComponentList = minComponents[componentName]
maxComponentList = maxComponents[componentName]
for i in xrange(len(minComponentList)):
# transformation breakdown: xScale, xyScale, yxScale, yScale, xOffset, yOffset
minXScale, minXYScale, minYXScale, minYScale, minXOffset, minYOffset = minComponentList[i]
maxXScale, maxXYScale, maxYXScale, maxYScale, maxXOffset, maxYOffset = maxComponentList[i]
newXScale, newXYScale = _interpolatePt((minXScale, minXYScale), (maxXScale, maxXYScale), factor)
newYXScale, newYScale = _interpolatePt((minYXScale, minYScale), (maxYXScale, maxYScale), factor)
newXOffset, newYOffset = _interpolatePt((minXOffset, minYOffset), (maxXOffset, maxYOffset), factor)
newComponents.append((componentName, (newXScale, newXYScale, newYXScale, newYScale, newXOffset, newYOffset)))
return newComponents
def _interpolateAnchors(self, factor, minAnchors, maxAnchors):
newAnchors = []
minAnchors, maxAnchors = self._mathAnchorCompare(minAnchors, maxAnchors)
anchorNames = minAnchors.keys()
for anchorName in anchorNames:
minAnchorList = minAnchors[anchorName]
maxAnchorList = maxAnchors[anchorName]
for i in range(len(minAnchorList)):
minAnchor = minAnchorList[i]
maxAnchor = maxAnchorList[i]
newAnchor = _interpolatePt(minAnchor, maxAnchor, factor)
newAnchors.append((newAnchor, anchorName))
return newAnchors
#
# comparisons
#
def __eq__(self, other):
if isinstance(other, BaseGlyph):
return self._getDigest() == other._getDigest()
return False
def _getDigest(self, pointsOnly=False):
"""Calculate a digest of coordinates, points, things in this glyph.
With pointsOnly == True the digest consists of a flat tuple of all
coordinate pairs in the glyph, without the order of contours.
"""
from robofab.pens.digestPen import DigestPointPen
mp = DigestPointPen()
self.drawPoints(mp)
if pointsOnly:
return "%s|%d|%s"%(mp.getDigestPointsOnly(), self.width, self.unicode)
else:
return "%s|%d|%s"%(mp.getDigest(), self.width, self.unicode)
def _getStructure(self):
"""Calculate a digest of points, things in this glyph, but NOT coordinates."""
from robofab.pens.digestPen import DigestPointStructurePen
mp = DigestPointStructurePen()
self.drawPoints(mp)
return mp.getDigest()
def _hasChanged(self):
"""mark the object and it's parent as changed"""
self.setChanged(True)
if self.getParent() is not None:
self.getParent()._hasChanged()
def _get_box(self):
bounds = _box(self, fontObject=self.getParent())
return bounds
box = property(_get_box, doc="the bounding box of the glyph: (xMin, yMin, xMax, yMax)")
def _get_leftMargin(self):
if self.isEmpty():
return 0
xMin, yMin, xMax, yMax = self.box
return xMin
def _set_leftMargin(self, value):
if self.isEmpty():
self.width = self.width + value
else:
diff = value - self.leftMargin
self.move((diff, 0))
self.width = self.width + diff
leftMargin = property(_get_leftMargin, _set_leftMargin, doc="the left margin")
def _get_rightMargin(self):
if self.isEmpty():
return self.width
xMin, yMin, xMax, yMax = self.box
return self.width - xMax
def _set_rightMargin(self, value):
if self.isEmpty():
self.width = value
else:
xMin, yMin, xMax, yMax = self.box
self.width = xMax + value
rightMargin = property(_get_rightMargin, _set_rightMargin, doc="the right margin")
def copy(self, aParent=None):
"""Duplicate this glyph"""
n = self.__class__()
if aParent is not None:
n.setParent(aParent)
dont = ['_object', 'getParent']
for k in self.__dict__.keys():
ok = True
if k in dont:
continue
elif k == "contours":
dup = []
for i in self.contours:
dup.append(i.copy(n))
elif k == "components":
dup = []
for i in self.components:
dup.append(i.copy(n))
elif k == "anchors":
dup = []
for i in self.anchors:
dup.append(i.copy(n))
elif k == "psHints":
dup = self.psHints.copy()
elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)):
dup = self.__dict__[k].copy(n)
else:
dup = copy.deepcopy(self.__dict__[k])
if ok:
setattr(n, k, dup)
return n
def _setParentTree(self):
"""Set the parents of all contained and dependent objects (and their dependents) right."""
for item in self.contours:
item.setParent(self)
item._setParentTree()
for item in self.components:
item.setParent(self)
for items in self.anchors:
item.setParent(self)
def getGlyph(self, glyphName):
"""Provided there is a font parent for this glyph, return a sibling glyph."""
if glyphName == self.name:
return self
if self.getParent() is not None:
return self.getParent()[glyphName]
return None
def getPen(self):
"""Return a Pen object for creating an outline in this glyph."""
from robofab.pens.adapterPens import SegmentToPointPen
return SegmentToPointPen(self.getPointPen())
def getPointPen(self):
"""Return a PointPen object for creating an outline in this glyph."""
raise NotImplementedError, "getPointPen() must be implemented by subclass"
def deSelect(self):
"""Set all selected attrs in glyph to False: for the glyph, components, anchors, points."""
for a in self.anchors:
a.selected = False
for a in self.components:
a.selected = False
for c in self.contours:
for p in c.points:
p.selected = False
self.selected = False
def isEmpty(self):
"""return true if the glyph has no contours or components"""
if len(self.contours) + len(self.components) == 0:
return True
else:
return False
def _saveToGlyphSet(self, glyphSet, glyphName=None, force=False):
"""Save the glyph to GlyphSet, a private method that's part of the saving process."""
# save stuff in the lib first
if force or self.changed:
if glyphName is None:
glyphName = self.name
glyphSet.writeGlyph(glyphName, self, self.drawPoints)
def update(self):
"""update the glyph"""
pass
def draw(self, pen):
"""draw the object with a RoboFab segment pen"""
try:
pen.setWidth(self.width)
if self.note is not None:
pen.setNote(self.note)
except AttributeError:
# FontTools pens don't have these methods
pass
for a in self.anchors:
a.draw(pen)
for c in self.contours:
c.draw(pen)
for c in self.components:
c.draw(pen)
try:
pen.doneDrawing()
except AttributeError:
# FontTools pens don't have a doneDrawing() method
pass
def drawPoints(self, pen):
"""draw the object with a point pen"""
for a in self.anchors:
a.drawPoints(pen)
for c in self.contours:
c.drawPoints(pen)
for c in self.components:
c.drawPoints(pen)
def appendContour(self, aContour, offset=(0, 0)):
"""append a contour to the glyph"""
x, y = offset
pen = self.getPointPen()
aContour.drawPoints(pen)
self.contours[-1].move((x, y))
def appendGlyph(self, aGlyph, offset=(0, 0)):
"""append another glyph to the glyph"""
x, y = offset
pen = self.getPointPen()
#to handle the offsets, move the source glyph and then move it back!
aGlyph.move((x, y))
aGlyph.drawPoints(pen)
aGlyph.move((-x, -y))
def round(self):
"""round all coordinates in all contours, components and anchors"""
for n in self.contours:
n.round()
for n in self.components:
n.round()
for n in self.anchors:
n.round()
self.width = int(round(self.width))
def autoUnicodes(self):
"""Using fontTools.agl, assign Unicode list to the glyph"""
from fontTools.agl import AGL2UV
if AGL2UV.has_key(self.name):
self.unicode = AGL2UV[self.name]
self._hasChanged()
def pointInside(self, (x, y), evenOdd=0):
"""determine if the point is in the black or white of the glyph"""
from fontTools.pens.pointInsidePen import PointInsidePen
font = self.getParent()
piPen = PointInsidePen(glyphSet=font, testPoint=(x, y), evenOdd=evenOdd)
self.draw(piPen)
return piPen.getResult()
def correctDirection(self, trueType=False):
"""corect the direction of the contours in the glyph."""
#this is a bit slow, but i'm not sure how much more it can be optimized.
#it also has a bug somewhere that is causeing some contours to be set incorrectly.
#try to run it on the copyright symbol to see the problem. hm.
#
#establish the default direction that an outer contour should follow
#i believe for TT this is clockwise and for PS it is counter
#i could be wrong about this, i need to double check.
from fontTools.pens.pointInsidePen import PointInsidePen
baseDirection = 0
if trueType:
baseDirection = 1
#we don't need to do all the work if the contour count is < 2
count = len(self.contours)
if count == 0:
return
elif count == 1:
self.contours[0].clockwise = baseDirection
return
#store up needed before we start
#i think the .box calls are eating a big chunk of the time
contourDict = {}
for contourIndex in range(len(self.contours)):
contour = self.contours[contourIndex]
contourDict[contourIndex] = {'box':contour.box, 'dir':contour.clockwise, 'hit':[], 'notHit':[]}
#now, for every contour, determine which contours it intersects
#as we go, we will also store contours that it doesn't intersct
#and we store this value for both contours
allIndexes = contourDict.keys()
for contourIndex in allIndexes:
for otherContourIndex in allIndexes:
if otherContourIndex != contourIndex:
if contourIndex not in contourDict[otherContourIndex]['hit'] and contourIndex not in contourDict[otherContourIndex]['notHit']:
xMin1, yMin1, xMax1, yMax1 = contourDict[contourIndex]['box']
xMin2, yMin2, xMax2, yMax2= contourDict[otherContourIndex]['box']
hit, pos = sectRect((xMin1, yMin1, xMax1, yMax1), (xMin2, yMin2, xMax2, yMax2))
if hit == 1:
contourDict[contourIndex]['hit'].append(otherContourIndex)
contourDict[otherContourIndex]['hit'].append(contourIndex)
else:
contourDict[contourIndex]['notHit'].append(otherContourIndex)
contourDict[otherContourIndex]['notHit'].append(contourIndex)
#set up the pen here to shave a bit of time
font = self.getParent()
piPen = PointInsidePen(glyphSet=font, testPoint=(0, 0), evenOdd=0)
#now do the pointInside work
for contourIndex in allIndexes:
direction = baseDirection
contour = self.contours[contourIndex]
startPoint = contour.segments[0].onCurve
if startPoint is not None: #skip TT paths with no onCurve
if len(contourDict[contourIndex]['hit']) != 0:
for otherContourIndex in contourDict[contourIndex]['hit']:
piPen.setTestPoint(testPoint=(startPoint.x, startPoint.y))
otherContour = self.contours[otherContourIndex]
otherContour.draw(piPen)
direction = direction + piPen.getResult()
newDirection = direction % 2
#now set the direction if we need to
if newDirection != contourDict[contourIndex]['dir']:
contour.reverseContour()
def autoContourOrder(self):
"""attempt to sort the contours based on their centers"""
# sort is based on (in this order):
# - the (negative) point count
# - the (negative) segment count
# - fuzzy x value of the center of the contour
# - fuzzy y value of the center of the contour
# - the (negative) surface of the bounding box of the contour: width * height
# the latter is a safety net for for instances like a very thin 'O' where the
# x centers could be close enough to rely on the y for the sort which could
# very well be the same for both contours. We use the _negative_ of the surface
# to ensure that larger contours appear first, which seems more natural.
tempContourList = []
contourList = []
xThreshold = None
yThreshold = None
for contour in self.contours:
xMin, yMin, xMax, yMax = contour.box
width = xMax - xMin
height = yMax - yMin
xC = 0.5 * (xMin + xMax)
yC = 0.5 * (yMin + yMax)
xTh = abs(width * .5)
yTh = abs(height * .5)
if xThreshold is None or xThreshold > xTh:
xThreshold = xTh
if yThreshold is None or yThreshold > yTh:
yThreshold = yTh
tempContourList.append((-len(contour.points), -len(contour.segments), xC, yC, -(width * height), contour))
for points, segments, x, y, surface, contour in tempContourList:
contourList.append((points, segments, FuzzyNumber(x, xThreshold), FuzzyNumber(y, yThreshold), surface, contour))
contourList.sort()
for i in range(len(contourList)):
points, segments, xO, yO, surface, contour = contourList[i]
contour.index = i
def rasterize(self, cellSize=50, xMin=None, yMin=None, xMax=None, yMax=None):
"""
Slice the glyph into a grid based on the cell size.
It returns a list of lists containing bool values
that indicate the black (True) or white (False)
value of that particular cell. These lists are
arranged from top to bottom of the glyph and
proceed from left to right.
This is an expensive operation!
"""
from fontTools.pens.pointInsidePen import PointInsidePen
piPen = PointInsidePen(glyphSet=self.getParent(), testPoint=(0, 0), evenOdd=0)
if xMin is None or yMin is None or xMax is None or yMax is None:
_xMin, _yMin, _xMax, _yMax = self.box
if xMin is None:
xMin = _xMin
if yMin is None:
yMin = _yMin
if xMax is None:
xMax = _xMax
if yMax is None:
yMax = _yMax
#
hitXMax = False
hitYMin = False
xSlice = 0
ySlice = 0
halfCellSize = cellSize / 2.0
#
map = []
#
while not hitYMin:
map.append([])
yScan = -(ySlice * cellSize) + yMax - halfCellSize
if yScan < yMin:
hitYMin = True
while not hitXMax:
xScan = (xSlice * cellSize) + xMin - halfCellSize
if xScan > xMax:
hitXMax = True
piPen.setTestPoint((xScan, yScan))
self.draw(piPen)
test = piPen.getResult()
if test:
map[-1].append(True)
else:
map[-1].append(False)
xSlice = xSlice + 1
hitXMax = False
xSlice = 0
ySlice = ySlice + 1
return map
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))
for contour in self.contours:
contour.move((x, y))
for component in self.components:
component.move((x, y))
for anchor in self.anchors:
anchor.move((x, y))
def scale(self, (x, y), center=(0, 0)):
"""scale the glyph"""
for contour in self.contours:
contour.scale((x, y), center=center)
for component in self.components:
offset = component.offset
component.offset = _scalePointFromCenter(offset, (x, y), center)
sX, sY = component.scale
component.scale = (sX*x, sY*y)
for anchor in self.anchors:
anchor.scale((x, y), center=center)
def transform(self, matrix):
"""Transform this glyph.
Use a Transform matrix object from
robofab.transform"""
n = []
for c in self.contours:
c.transform(matrix)
for a in self.anchors:
a.transform(matrix)
def rotate(self, angle, offset=None):
"""rotate the glyph"""
from fontTools.misc.transform import Identity
radAngle = angle / DEGREE # convert from degrees to radians
if offset is None:
offset = (0,0)
rT = Identity.translate(offset[0], offset[1])
rT = rT.rotate(radAngle)
rT = rT.translate(-offset[0], -offset[1])
self.transform(rT)
def skew(self, angle, offset=None):
"""skew the glyph"""
from fontTools.misc.transform import Identity
radAngle = angle / DEGREE # convert from degrees to radians
if offset is None:
offset = (0,0)
rT = Identity.translate(offset[0], offset[1])
rT = rT.skew(radAngle)
self.transform(rT)
class BaseContour(RBaseObject):
"""Base class for all contour objects."""
def __init__(self):
RBaseObject.__init__(self)
#self.index = None
self.changed = False # if the object needs to be saved
def __repr__(self):
font = "unnamed_font"
glyph = "unnamed_glyph"
glyphParent = self.getParent()
if glyphParent is not None:
try:
glyph = glyphParent.name
except AttributeError: pass
fontParent = glyphParent.getParent()
if fontParent is not None:
try:
font = fontParent.info.postscriptFullName
except AttributeError: pass
try:
idx = `self.index`
except ValueError:
# XXXX
idx = "XXX"
return "<RContour for %s.%s[%s]>"%(font, glyph, idx)
def __len__(self):
return len(self.segments)
def __mul__(self, factor):
warn("Contour math has been deprecated and is slated for removal.", DeprecationWarning)
n = self.copy()
n.segments = []
for i in range(len(self.segments)):
n.segments.append(self.segments[i] * factor)
n._setParentTree()
return n
__rmul__ = __mul__
def __add__(self, other):
warn("Contour math has been deprecated and is slated for removal.", DeprecationWarning)
n = self.copy()
n.segments = []
for i in range(len(self.segments)):
n.segments.append(self.segments[i] + other.segments[i])
n._setParentTree()
return n
def __sub__(self, other):
warn("Contour math has been deprecated and is slated for removal.", DeprecationWarning)
n = self.copy()
n.segments = []
for i in range(len(self.segments)):
n.segments.append(self.segments[i] - other.segments[i])
n._setParentTree()
return n
def __getitem__(self, index):
return self.segments[index]
def _hasChanged(self):
"""mark the object and it's parent as changed"""
self.setChanged(True)
if self.getParent() is not None:
self.getParent()._hasChanged()
def _nextSegment(self, segmentIndex):
return self.segments[(segmentIndex + 1) % len(self.segments)]
def _prevSegment(self, segmentIndex):
segments = self.segments
return self.segments[(segmentIndex - 1) % len(self.segments)]
def _get_box(self):
bounds = _box(self)
return bounds
box = property(_get_box, doc="the bounding box for the contour")
def _set_clockwise(self, value):
if self.clockwise != value:
self.reverseContour()
def _get_clockwise(self):
anchors = []
lastOn = None
for segment in self.segments:
anchor = segment.onCurve
anchor = (anchor.x, anchor.y)
# overlapping points can give false results, so filter them out
if anchor == lastOn:
continue
anchors.append(anchor)
lastOn = anchor
#overlapping moves can give false results, so filter them out
first = anchors[0]
last = anchors[-1]
if first == last:
del anchors[-1]
nPoints = len(anchors)
angles = []
for i1 in range(nPoints):
i2 = (i1 + 1) % nPoints
x1, y1 = anchors[i1]
x2, y2 = anchors[i2]
angles.append(math.atan2(y2-y1, x2-x1))
total = 0
pi = math.pi
pi2 = pi * 2
for i1 in range(nPoints):
i2 = (i1 + 1) % nPoints
d = ((angles[i2] - angles[i1] + pi) % pi2) - pi
total += d
return total < 0
clockwise = property(_get_clockwise, _set_clockwise, doc="direction of contour: 1=clockwise 0=counterclockwise")
def copy(self, aParent=None):
"""Duplicate this contour"""
n = self.__class__()
if aParent is not None:
n.setParent(aParent)
elif self.getParent() is not None:
n.setParent(self.getParent())
dont = ['_object', 'points', 'bPoints', 'getParent']
for k in self.__dict__.keys():
ok = True
if k in dont:
continue
elif k == "segments":
dup = []
for i in self.segments:
dup.append(i.copy(n))
elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)):
dup = self.__dict__[k].copy(n)
else:
dup = copy.deepcopy(self.__dict__[k])
if ok:
setattr(n, k, dup)
return n
def _setParentTree(self):
"""Set the parents of all contained and dependent objects (and their dependents) right."""
for item in self.segments:
item.setParent(self)
def round(self):
"""round the value of all points in the contour"""
for n in self.points:
n.round()
def draw(self, pen):
"""draw the object with a fontTools pen"""
firstOn = self.segments[0].onCurve
firstType = self.segments[0].type
lastOn = self.segments[-1].onCurve
# this is a special exception for FontLab
# FL can have a contour that does not contain a move.
# this will only happen if the contour begins with a qcurve.
# in this case, we move to the segment's on curve,
# then we iterate through the rest of the points,
# then we add the first qcurve and finally we
# close the path. after this, i say "ugh."
if firstType == QCURVE:
pen.moveTo((firstOn.x, firstOn.y))
for segment in self.segments[1:]:
segmentType = segment.type
pt = segment.onCurve.x, segment.onCurve.y
if segmentType == LINE:
pen.lineTo(pt)
elif segmentType == CURVE:
pts = [(point.x, point.y) for point in segment.points]
pen.curveTo(*pts)
elif segmentType == QCURVE:
pts = [(point.x, point.y) for point in segment.points]
pen.qCurveTo(*pts)
else:
assert 0, "unsupported segment type"
pts = [(point.x, point.y) for point in self.segments[0].points]
pen.qCurveTo(*pts)
pen.closePath()
else:
if firstType == MOVE and (firstOn.x, firstOn.y) == (lastOn.x, lastOn.y):
closed = True
else:
closed = True
for segment in self.segments:
segmentType = segment.type
pt = segment.onCurve.x, segment.onCurve.y
if segmentType == MOVE:
pen.moveTo(pt)
elif segmentType == LINE:
pen.lineTo(pt)
elif segmentType == CURVE:
pts = [(point.x, point.y) for point in segment.points]
pen.curveTo(*pts)
elif segmentType == QCURVE:
pts = [(point.x, point.y) for point in segment.points]
pen.qCurveTo(*pts)
else:
assert 0, "unsupported segment type"
if closed:
pen.closePath()
else:
pen.endPath()
def drawPoints(self, pen):
"""draw the object with a point pen"""
pen.beginPath()
lastOn = self.segments[-1].onCurve
didLastOn = False
flQCurveException = False
lastIndex = len(self.segments) - 1
for i in range(len(self.segments)):
segment = self.segments[i]
segmentType = segment.type
# the new protocol states that we start with an onCurve
# so, if we have a move and a nd a last point overlapping,
# add the last point to the beginning and skip the move
if segmentType == MOVE and (segment.onCurve.x, segment.onCurve.y) == (lastOn.x, lastOn.y):
point = self.segments[-1].onCurve
name = getattr(point, 'name', None)
pen.addPoint((point.x, point.y), point.type, smooth=self.segments[-1].smooth, name=name)
didLastOn = True
continue
# this is an exception for objectsFL
# the problem is that quad contours are
# represented differently that they are in
# objectsRF:
# FL: [qcurve, qcurve, qcurve, qcurve]
# RF: [move, qcurve, qcurve, qcurve, qcurve]
# so, we need to catch this, and shift the offCurves to
# to the end of the contour
if i == 0 and segmentType == QCURVE:
flQCurveException = True
if segmentType == MOVE:
segmentType = LINE
## the offCurves
if i == 0 and flQCurveException:
pass
else:
for point in segment.offCurve:
name = getattr(point, 'name', None)
pen.addPoint((point.x, point.y), segmentType=None, smooth=None, name=name, selected=point.selected)
## the onCurve
# skip the last onCurve if it was used as the move
if i == lastIndex and didLastOn:
continue
point = segment.onCurve
name = getattr(point, 'name', None)
pen.addPoint((point.x, point.y), segmentType, smooth=segment.smooth, name=name, selected=point.selected)
# if we have the special qCurve case with objectsFL
# take care of the offCurves associated with the first contour
if flQCurveException:
for point in self.segments[0].offCurve:
name = getattr(point, 'name', None)
pen.addPoint((point.x, point.y), segmentType=None, smooth=None, name=name, selected=point.selected)
pen.endPath()
def move(self, (x, y)):
"""move the contour"""
#this will be faster if we go straight to the points
for point in self.points:
point.move((x, y))
def scale(self,(x, y), center=(0, 0)):
"""scale the contour"""
#this will be faster if we go straight to the points
for point in self.points:
point.scale((x, y), center=center)
def transform(self, matrix):
"""Transform this contour.
Use a Transform matrix object from
robofab.transform"""
n = []
for s in self.segments:
s.transform(matrix)
def rotate(self, angle, offset=None):
"""rotate the contour"""
from fontTools.misc.transform import Identity
radAngle = angle / DEGREE # convert from degrees to radians
if offset is None:
offset = (0,0)
rT = Identity.translate(offset[0], offset[1])
rT = rT.rotate(radAngle)
self.transform(rT)
def skew(self, angle, offset=None):
"""skew the contour"""
from fontTools.misc.transform import Identity
radAngle = angle / DEGREE # convert from degrees to radians
if offset is None:
offset = (0,0)
rT = Identity.translate(offset[0], offset[1])
rT = rT.skew(radAngle)
self.transform(rT)
def pointInside(self, (x, y), evenOdd=0):
"""determine if the point is inside or ouside of the contour"""
from fontTools.pens.pointInsidePen import PointInsidePen
glyph = self.getParent()
font = glyph.getParent()
piPen = PointInsidePen(glyphSet=font, testPoint=(x, y), evenOdd=evenOdd)
self.draw(piPen)
return piPen.getResult()
def autoStartSegment(self):
"""automatically set the lower left point of the contour as the first point."""
#adapted from robofog
startIndex = 0
startSegment = self.segments[0]
for i in range(len(self.segments)):
segment = self.segments[i]
startOn = startSegment.onCurve
on = segment.onCurve
if on.y <= startOn.y:
if on.y == startOn.y:
if on.x < startOn.x:
startSegment = segment
startIndex = i
else:
startSegment = segment
startIndex = i
if startIndex != 0:
self.setStartSegment(startIndex)
def appendBPoint(self, pointType, anchor, bcpIn=(0, 0), bcpOut=(0, 0)):
"""append a bPoint to the contour"""
self.insertBPoint(len(self.segments), pointType=pointType, anchor=anchor, bcpIn=bcpIn, bcpOut=bcpOut)
def insertBPoint(self, index, pointType, anchor, bcpIn=(0, 0), bcpOut=(0, 0)):
"""insert a bPoint at index on the contour"""
#insert a CURVE point that we can work with
nextSegment = self._nextSegment(index-1)
if nextSegment.type == QCURVE:
return
if nextSegment.type == MOVE:
prevSegment = self.segments[index-1]
prevOn = prevSegment.onCurve
if bcpIn != (0, 0):
new = self.appendSegment(CURVE, [(prevOn.x, prevOn.y), absoluteBCPIn(anchor, bcpIn), anchor], smooth=False)
if pointType == CURVE:
new.smooth = True
else:
new = self.appendSegment(LINE, [anchor], smooth=False)
#if the user wants an outgoing bcp, we must add a CURVE ontop of the move
if bcpOut != (0, 0):
nextOn = nextSegment.onCurve
self.appendSegment(CURVE, [absoluteBCPOut(anchor, bcpOut), (nextOn.x, nextOn.y), (nextOn.x, nextOn.y)], smooth=False)
else:
#handle the bcps
if nextSegment.type != CURVE:
prevSegment = self.segments[index-1]
prevOn = prevSegment.onCurve
prevOutX, prevOutY = (prevOn.x, prevOn.y)
else:
prevOut = nextSegment.offCurve[0]
prevOutX, prevOutY = (prevOut.x, prevOut.y)
self.insertSegment(index, segmentType=CURVE, points=[(prevOutX, prevOutY), anchor, anchor], smooth=False)
newSegment = self.segments[index]
prevSegment = self._prevSegment(index)
nextSegment = self._nextSegment(index)
if nextSegment.type == MOVE:
raise RoboFabError, 'still working out curving at the end of a contour'
elif nextSegment.type == QCURVE:
return
#set the new incoming bcp
newIn = newSegment.offCurve[1]
nIX, nIY = absoluteBCPIn(anchor, bcpIn)
newIn.x = nIX
newIn.y = nIY
#set the new outgoing bcp
hasCurve = True
if nextSegment.type != CURVE:
if bcpOut != (0, 0):
nextSegment.type = CURVE
hasCurve = True
else:
hasCurve = False
if hasCurve:
newOut = nextSegment.offCurve[0]
nOX, nOY = absoluteBCPOut(anchor, bcpOut)
newOut.x = nOX
newOut.y = nOY
#now check to see if we can convert the CURVE segment to a LINE segment
newAnchor = newSegment.onCurve
newA = newSegment.offCurve[0]
newB = newSegment.offCurve[1]
nextAnchor = nextSegment.onCurve
prevAnchor = prevSegment.onCurve
if (prevAnchor.x, prevAnchor.y) == (newA.x, newA.y) and (newAnchor.x, newAnchor.y) == (newB.x, newB.y):
newSegment.type = LINE
#the user wants a smooth segment
if pointType == CURVE:
newSegment.smooth = True
class BaseSegment(RBaseObject):
"""Base class for all segment objects"""
def __init__(self):
self.changed = False
def __repr__(self):
font = "unnamed_font"
glyph = "unnamed_glyph"
contourIndex = "unknown_contour"
contourParent = self.getParent()
if contourParent is not None:
try:
contourIndex = `contourParent.index`
except AttributeError: pass
glyphParent = contourParent.getParent()
if glyphParent is not None:
try:
glyph = glyphParent.name
except AttributeError: pass
fontParent = glyphParent.getParent()
if fontParent is not None:
try:
font = fontParent.info.postscriptFullName
except AttributeError: pass
try:
idx = `self.index`
except ValueError:
idx = "XXX"
return "<RSegment for %s.%s[%s][%s]>"%(font, glyph, contourIndex, idx)
def __mul__(self, factor):
warn("Segment math has been deprecated and is slated for removal.", DeprecationWarning)
n = self.copy()
n.points = []
for i in range(len(self.points)):
n.points.append(self.points[i] * factor)
n._setParentTree()
return n
__rmul__ = __mul__
def __add__(self, other):
warn("Segment math has been deprecated and is slated for removal.", DeprecationWarning)
n = self.copy()
n.points = []
for i in range(len(self.points)):
n.points.append(self.points[i] + other.points[i])
return n
def __sub__(self, other):
warn("Segment math has been deprecated and is slated for removal.", DeprecationWarning)
n = self.copy()
n.points = []
for i in range(len(self.points)):
n.points.append(self.points[i] - other.points[i])
return n
def _hasChanged(self):
"""mark the object and it's parent as changed"""
self.setChanged(True)
if self.getParent() is not None:
self.getParent()._hasChanged()
def copy(self, aParent=None):
"""Duplicate this segment"""
n = self.__class__()
if aParent is not None:
n.setParent(aParent)
elif self.getParent() is not None:
n.setParent(self.getParent())
dont = ['_object', 'getParent', 'offCurve', 'onCurve']
for k in self.__dict__.keys():
ok = True
if k in dont:
continue
if k == "points":
dup = []
for i in self.points:
dup.append(i.copy(n))
elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)):
dup = self.__dict__[k].copy(n)
else:
dup = copy.deepcopy(self.__dict__[k])
if ok:
setattr(n, k, dup)
return n
def _setParentTree(self):
"""Set the parents of all contained and dependent objects (and their dependents) right."""
for item in self.points:
item.setParent(self)
def round(self):
"""round all points in the segment"""
for point in self.points:
point.round()
def move(self, (x, y)):
"""move the segment"""
for point in self.points:
point.move((x, y))
def scale(self, (x, y), center=(0, 0)):
"""scale the segment"""
for point in self.points:
point.scale((x, y), center=center)
def transform(self, matrix):
"""Transform this segment.
Use a Transform matrix object from
robofab.transform"""
n = []
for p in self.points:
p.transform(matrix)
def _get_onCurve(self):
return self.points[-1]
def _get_offCurve(self):
return self.points[:-1]
offCurve = property(_get_offCurve, doc="on curve point for the segment")
onCurve = property(_get_onCurve, doc="list of off curve points for the segment")
class BasePoint(RBaseObject):
"""Base class for point objects."""
def __init__(self):
#RBaseObject.__init__(self)
self.changed = False # if the object needs to be saved
self.selected = False
def __repr__(self):
font = "unnamed_font"
glyph = "unnamed_glyph"
contourIndex = "unknown_contour"
segmentIndex = "unknown_segment"
segmentParent = self.getParent()
if segmentParent is not None:
try:
segmentIndex = `segmentParent.index`
except AttributeError: pass
contourParent = self.getParent().getParent()
if contourParent is not None:
try:
contourIndex = `contourParent.index`
except AttributeError: pass
glyphParent = contourParent.getParent()
if glyphParent is not None:
try:
glyph = glyphParent.name
except AttributeError: pass
fontParent = glyphParent.getParent()
if fontParent is not None:
try:
font = fontParent.info.postscriptFullName
except AttributeError: pass
return "<RPoint for %s.%s[%s][%s]>"%(font, glyph, contourIndex, segmentIndex)
def __add__(self, other):
warn("Point math has been deprecated and is slated for removal.", DeprecationWarning)
#Add one point to another
n = self.copy()
n.x, n.y = addPt((self.x, self.y), (other.x, other.y))
return n
def __sub__(self, other):
warn("Point math has been deprecated and is slated for removal.", DeprecationWarning)
#Subtract one point from another
n = self.copy()
n.x, n.y = subPt((self.x, self.y), (other.x, other.y))
return n
def __mul__(self, factor):
warn("Point math has been deprecated and is slated for removal.", DeprecationWarning)
#Multiply the point with factor. Factor can be a tuple of 2 *(f1, f2)
n = self.copy()
n.x, n.y = mulPt((self.x, self.y), factor)
return n
__rmul__ = __mul__
def _hasChanged(self):
#mark the object and it's parent as changed
self.setChanged(True)
if self.getParent() is not None:
self.getParent()._hasChanged()
def copy(self, aParent=None):
"""Duplicate this point"""
n = self.__class__()
if aParent is not None:
n.setParent(aParent)
elif self.getParent() is not None:
n.setParent(self.getParent())
dont = ['getParent', 'offCurve', 'onCurve']
for k in self.__dict__.keys():
ok = True
if k in dont:
continue
elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)):
dup = self.__dict__[k].copy(n)
else:
dup = copy.deepcopy(self.__dict__[k])
if ok:
setattr(n, k, dup)
return n
def select(self, state=True):
"""Set the selection of this point.
XXXX This method should be a lot more versatile, dealing with
different kinds of selection, select the bcp's seperately etc.
But that's for later when we need it more. For now it's just
one flag for the entire thing."""
self.selected = state
def round(self):
"""round the values in the point"""
self.x, self.y = roundPt((self.x, self.y))
def move(self, (x, y)):
"""Move the point"""
self.x, self.y = addPt((self.x, self.y), (x, y))
def scale(self, (x, y), center=(0, 0)):
"""scale the point"""
nX, nY = _scalePointFromCenter((self.x, self.y), (x, y), center)
self.x = nX
self.y = nY
def transform(self, matrix):
"""Transform this point. Use a Transform matrix
object from fontTools.misc.transform"""
self.x, self.y = matrix.transformPoint((self.x, self.y))
class BaseBPoint(RBaseObject):
"""Base class for bPoints objects."""
def __init__(self):
RBaseObject.__init__(self)
self.changed = False # if the object needs to be saved
self.selected = False
def __repr__(self):
font = "unnamed_font"
glyph = "unnamed_glyph"
contourIndex = "unknown_contour"
segmentIndex = "unknown_segment"
segmentParent = self.getParent()
if segmentParent is not None:
try:
segmentIndex = `segmentParent.index`
except AttributeError: pass
contourParent = segmentParent.getParent()
if contourParent is not None:
try:
contourIndex = `contourParent.index`
except AttributeError: pass
glyphParent = contourParent.getParent()
if glyphParent is not None:
try:
glyph = glyphParent.name
except AttributeError: pass
fontParent = glyphParent.getParent()
if fontParent is not None:
try:
font = fontParent.info.postscriptFullName
except AttributeError: pass
return "<RBPoint for %s.%s[%s][%s][%s]>"%(font, glyph, contourIndex, segmentIndex, `self.index`)
def __add__(self, other):
warn("BPoint math has been deprecated and is slated for removal.", DeprecationWarning)
#Add one bPoint to another
n = self.copy()
n.anchor = addPt(self.anchor, other.anchor)
n.bcpIn = addPt(self.bcpIn, other.bcpIn)
n.bcpOut = addPt(self.bcpOut, other.bcpOut)
return n
def __sub__(self, other):
warn("BPoint math has been deprecated and is slated for removal.", DeprecationWarning)
#Subtract one bPoint from another
n = self.copy()
n.anchor = subPt(self.anchor, other.anchor)
n.bcpIn = subPt(self.bcpIn, other.bcpIn)
n.bcpOut = subPt(self.bcpOut, other.bcpOut)
return n
def __mul__(self, factor):
warn("BPoint math has been deprecated and is slated for removal.", DeprecationWarning)
#Multiply the bPoint with factor. Factor can be a tuple of 2 *(f1, f2)
n = self.copy()
n.anchor = mulPt(self.anchor, factor)
n.bcpIn = mulPt(self.bcpIn, factor)
n.bcpOut = mulPt(self.bcpOut, factor)
return n
__rmul__ = __mul__
def _hasChanged(self):
#mark the object and it's parent as changed
self.setChanged(True)
if self.getParent() is not None:
self.getParent()._hasChanged()
def select(self, state=True):
"""Set the selection of this point.
XXXX This method should be a lot more versatile, dealing with
different kinds of selection, select the bcp's seperately etc.
But that's for later when we need it more. For now it's just
one flag for the entire thing."""
self.selected = state
def round(self):
"""Round the coordinates to integers"""
self.anchor = roundPt(self.anchor)
pSeg = self._parentSegment
if pSeg.type != MOVE:
self.bcpIn = roundPt(self.bcpIn)
if pSeg.getParent()._nextSegment(pSeg.index).type != MOVE:
self.bcpOut = roundPt(self.bcpOut)
def move(self, (x, y)):
"""move the bPoint"""
bcpIn = self.bcpIn
bcpOut = self.bcpOut
self.anchor = (self.anchor[0] + x, self.anchor[1] + y)
pSeg = self._parentSegment
if pSeg.type != MOVE:
self.bcpIn = bcpIn
if pSeg.getParent()._nextSegment(pSeg.index).type != MOVE:
self.bcpOut = bcpOut
def scale(self, (x, y), center=(0, 0)):
"""scale the bPoint"""
centerX, centerY = center
ogCenter = (centerX, centerY)
scaledCenter = (centerX * x, centerY * y)
shiftVal = (scaledCenter[0] - ogCenter[0], scaledCenter[1] - ogCenter[1])
anchor = self.anchor
bcpIn = self.bcpIn
bcpOut = self.bcpOut
self.anchor = ((anchor[0] * x) - shiftVal[0], (anchor[1] * y) - shiftVal[1])
pSeg = self._parentSegment
if pSeg.type != MOVE:
self.bcpIn = ((bcpIn[0] * x), (bcpIn[1] * y))
if pSeg.getParent()._nextSegment(pSeg.index).type != MOVE:
self.bcpOut = ((bcpOut[0] * x), (bcpOut[1] * y))
def transform(self, matrix):
"""Transform this point. Use a Transform matrix
object from fontTools.misc.transform"""
self.anchor = matrix.transformPoint(self.anchor)
pSeg = self._parentSegment
if pSeg.type != MOVE:
self.bcpIn = matrix.transformPoint(self.bcpIn)
if pSeg.getParent()._nextSegment(pSeg.index).type != MOVE:
self.bcpOut = matrix.transformPoint(self.bcpOut)
def _get__anchorPoint(self):
return self._parentSegment.onCurve
_anchorPoint = property(_get__anchorPoint, doc="the oncurve point in the parent segment")
def _get_anchor(self):
point = self._anchorPoint
return (point.x, point.y)
def _set_anchor(self, value):
x, y = value
point = self._anchorPoint
point.x = x
point.y = y
anchor = property(_get_anchor, _set_anchor, doc="the position of the anchor")
def _get_bcpIn(self):
pSeg = self._parentSegment
pCount = len(pSeg.offCurve)
if pCount == 2:
p = pSeg.offCurve[1]
pOn = pSeg.onCurve
return relativeBCPIn((pOn.x, pOn.y), (p.x, p.y))
else:
return (0, 0)
def _set_bcpIn(self, value):
x, y = (absoluteBCPIn(self.anchor, value))
pSeg = self._parentSegment
if pSeg.type == MOVE:
#the user wants to have a bcp leading into the MOVE
if value == (0, 0) and self.bcpOut == (0, 0):
#we have a straight line between the two anchors
pass
else:
#we need to insert a new CURVE segment ontop of the move
contour = self._parentSegment.getParent()
#set the prev segment outgoing bcp to the onCurve
prevSeg = contour._prevSegment(self._parentSegment.index)
prevOn = prevSeg.onCurve
contour.appendSegment(CURVE, [(prevOn.x, prevOn.y), (x, y), self.anchor], smooth=False)
else:
pCount = len(pSeg.offCurve)
if pCount == 2:
#if the two points in the offCurvePoints list are located at the
#anchor coordinates we can switch to a LINE segment type
if value == (0, 0) and self.bcpOut == (0, 0):
pSeg.type = LINE
pSeg.smooth = False
else:
pSeg.offCurve[1].x = x
pSeg.offCurve[1].y = y
elif value != (0, 0):
pSeg.type = CURVE
pSeg.offCurve[1].x = x
pSeg.offCurve[1].y = y
bcpIn = property(_get_bcpIn, _set_bcpIn, doc="the (x,y) for the incoming bcp")
def _get_bcpOut(self):
pSeg = self._parentSegment
nextSeg = pSeg.getParent()._nextSegment(pSeg.index)
nsCount = len(nextSeg.offCurve)
if nsCount == 2:
p = nextSeg.offCurve[0]
return relativeBCPOut(self.anchor, (p.x, p.y))
else:
return (0, 0)
def _set_bcpOut(self, value):
x, y = (absoluteBCPOut(self.anchor, value))
pSeg = self._parentSegment
nextSeg = pSeg.getParent()._nextSegment(pSeg.index)
if nextSeg.type == MOVE:
if value == (0, 0) and self.bcpIn == (0, 0):
pass
else:
#we need to insert a new CURVE segment ontop of the move
contour = self._parentSegment.getParent()
nextOn = nextSeg.onCurve
contour.appendSegment(CURVE, [(x, y), (nextOn.x, nextOn.y), (nextOn.x, nextOn.y)], smooth=False)
else:
nsCount = len(nextSeg.offCurve)
if nsCount == 2:
#if the two points in the offCurvePoints list are located at the
#anchor coordinates we can switch to a LINE segment type
if value == (0, 0) and self.bcpIn == (0, 0):
nextSeg.type = LINE
nextSeg.smooth = False
else:
nextSeg.offCurve[0].x = x
nextSeg.offCurve[0].y = y
elif value != (0, 0):
nextSeg.type = CURVE
nextSeg.offCurve[0].x = x
nextSeg.offCurve[0].y = y
bcpOut = property(_get_bcpOut, _set_bcpOut, doc="the (x,y) for the outgoing bcp")
def _get_type(self):
pType = self._parentSegment.type
bpType = CORNER
if pType == CURVE:
if self._parentSegment.smooth:
bpType = CURVE
return bpType
def _set_type(self, pointType):
pSeg = self._parentSegment
segType = pSeg.type
#user wants a curve where there is a line
if pointType == CURVE and segType == LINE:
pSeg.type = CURVE
pSeg.smooth = True
#the anchor is a curve segment. so, all we need to do is turn the smooth off
elif pointType == CORNER and segType == CURVE:
pSeg.smooth = False
type = property(_get_type, _set_type, doc="the type of bPoint, either 'corner' or 'curve'")
class BaseComponent(RBaseObject):
"""Base class for all component objects."""
def __init__(self):
RBaseObject.__init__(self)
self.changed = False # if the object needs to be saved
self.selected = False
def __repr__(self):
font = "unnamed_font"
glyph = "unnamed_glyph"
glyphParent = self.getParent()
if glyphParent is not None:
try:
glyph = glyphParent.name
except AttributeError: pass
fontParent = glyphParent.getParent()
if fontParent is not None:
try:
font = fontParent.info.postscriptFullName
except AttributeError: pass
return "<RComponent for %s.%s.components[%s]>"%(font, glyph, `self.index`)
def _hasChanged(self):
"""mark the object and it's parent as changed"""
self.setChanged(True)
if self.getParent() is not None:
self.getParent()._hasChanged()
def copy(self, aParent=None):
"""Duplicate this component."""
n = self.__class__()
if aParent is not None:
n.setParent(aParent)
elif self.getParent() is not None:
n.setParent(self.getParent())
dont = ['getParent', '_object']
for k in self.__dict__.keys():
if k in dont:
continue
elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)):
dup = self.__dict__[k].copy(n)
else:
dup = copy.deepcopy(self.__dict__[k])
setattr(n, k, dup)
return n
def __add__(self, other):
warn("Component math has been deprecated and is slated for removal.", DeprecationWarning)
#Add one Component to another
n = self.copy()
n.offset = addPt(self.offset, other.offset)
n.scale = addPt(self.scale, other.scale)
return n
def __sub__(self, other):
warn("Component math has been deprecated and is slated for removal.", DeprecationWarning)
#Subtract one Component from another
n = self.copy()
n.offset = subPt(self.offset, other.offset)
n.scale = subPt(self.scale, other.scale)
return n
def __mul__(self, factor):
warn("Component math has been deprecated and is slated for removal.", DeprecationWarning)
#Multiply the Component with factor. Factor can be a tuple of 2 *(f1, f2)
n = self.copy()
n.offset = mulPt(self.offset, factor)
n.scale = mulPt(self.scale, factor)
return n
__rmul__ = __mul__
def _get_box(self):
parentGlyph = self.getParent()
# the component is an orphan
if parentGlyph is None:
return None
parentFont = parentGlyph.getParent()
# the glyph that contains the component
# does not hae a parent
if parentFont is None:
return None
# the font does not have a glyph
# that matches the glyph that
# this component references
if not parentFont.has_key(self.baseGlyph):
return None
return _box(self, parentFont)
box = property(_get_box, doc="the bounding box of the component: (xMin, yMin, xMax, yMax)")
def round(self):
"""round the offset values"""
self.offset = roundPt(self.offset)
self._hasChanged()
def draw(self, pen):
"""Segment pen drawing method."""
if isinstance(pen, AbstractPen):
# It's a FontTools pen, which for addComponent is identical
# to PointPen.
self.drawPoints(pen)
else:
# It's an "old" 'Fab pen
pen.addComponent(self.baseGlyph, self.offset, self.scale)
def drawPoints(self, pen):
"""draw the object with a point pen"""
oX, oY = self.offset
sX, sY = self.scale
#xScale, xyScale, yxScale, yScale, xOffset, yOffset
pen.addComponent(self.baseGlyph, (sX, 0, 0, sY, oX, oY))
class BaseAnchor(RBaseObject):
"""Base class for all anchor point objects."""
def __init__(self):
RBaseObject.__init__(self)
self.changed = False # if the object needs to be saved
self.selected = False
def __repr__(self):
font = "unnamed_font"
glyph = "unnamed_glyph"
glyphParent = self.getParent()
if glyphParent is not None:
try:
glyph = glyphParent.name
except AttributeError: pass
fontParent = glyphParent.getParent()
if fontParent is not None:
try:
font = fontParent.info.postscriptFullName
except AttributeError: pass
return "<RAnchor for %s.%s.anchors[%s]>"%(font, glyph, `self.index`)
def __add__(self, other):
warn("Anchor math has been deprecated and is slated for removal.", DeprecationWarning)
#Add one anchor to another
n = self.copy()
n.x, n.y = addPt((self.x, self.y), (other.x, other.y))
return n
def __sub__(self, other):
warn("Anchor math has been deprecated and is slated for removal.", DeprecationWarning)
#Substract one anchor from another
n = self.copy()
n.x, n.y = subPt((self.x, self.y), (other.x, other.y))
return n
def __mul__(self, factor):
warn("Anchor math has been deprecated and is slated for removal.", DeprecationWarning)
#Multiply the anchor with factor. Factor can be a tuple of 2 *(f1, f2)
n = self.copy()
n.x, n.y = mulPt((self.x, self.y), factor)
return n
__rmul__ = __mul__
def _hasChanged(self):
#mark the object and it's parent as changed
self.setChanged(True)
if self.getParent() is not None:
self.getParent()._hasChanged()
def copy(self, aParent=None):
"""Duplicate this anchor."""
n = self.__class__()
if aParent is not None:
n.setParent(aParent)
elif self.getParent() is not None:
n.setParent(self.getParent())
dont = ['getParent', '_object']
for k in self.__dict__.keys():
if k in dont:
continue
elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)):
dup = self.__dict__[k].copy(n)
else:
dup = copy.deepcopy(self.__dict__[k])
setattr(n, k, dup)
return n
def round(self):
"""round the values in the anchor"""
self.x, self.y = roundPt((self.x, self.y))
self._hasChanged()
def draw(self, pen):
"""Draw the object onto a segment pen"""
if isinstance(pen, AbstractPen):
# It's a FontTools pen
pen.moveTo((self.x, self.y))
pen.endPath()
else:
# It's an "old" 'Fab pen
pen.addAnchor(self.name, (self.x, self.y))
def drawPoints(self, pen):
"""draw the object with a point pen"""
pen.beginPath()
pen.addPoint((self.x, self.y), segmentType="move", smooth=False, name=self.name)
pen.endPath()
def move(self, (x, y)):
"""Move the anchor"""
pX, pY = self.position
self.position = (pX+x, pY+y)
def scale(self, (x, y), center=(0, 0)):
"""scale the anchor"""
pos = self.position
self.position = _scalePointFromCenter(pos, (x, y), center)
def transform(self, matrix):
"""Transform this anchor. Use a Transform matrix
object from fontTools.misc.transform"""
self.x, self.y = matrix.transformPoint((self.x, self.y))
class BaseGuide(RBaseObject):
"""Base class for all guide objects."""
def __init__(self):
RBaseObject.__init__(self)
self.changed = False # if the object needs to be saved
self.selected = False
class BaseInfo(RBaseObject):
_baseAttributes = ["_object", "changed", "selected", "getParent"]
_deprecatedAttributes = ufoLib.deprecatedFontInfoAttributesVersion2
_infoAttributes = ufoLib.fontInfoAttributesVersion2
# subclasses may define a list of environment
# specific attributes that can be retrieved or set.
_environmentAttributes = []
# subclasses may define a list of attributes
# that should not follow the standard get/set
# order provided by __setattr__ and __getattr__.
# for these attributes, the environment specific
# set and get methods must handle this value
# without any pre-call validation.
# (yeah. this is because of some FontLab dumbness.)
_environmentOverrides = []
def __setattr__(self, attr, value):
# check to see if the attribute has been
# deprecated. if so, warn the caller and
# update the attribute and value.
if attr in self._deprecatedAttributes:
newAttr, newValue = ufoLib.convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value)
note = "The %s attribute has been deprecated. Use the new %s attribute." % (attr, newAttr)
warn(note, DeprecationWarning)
attr = newAttr
value = newValue
# setting a known attribute
if attr in self._infoAttributes or attr in self._environmentAttributes:
# lightly test the validity of the value
if value is not None:
isValidValue = ufoLib.validateFontInfoVersion2ValueForAttribute(attr, value)
if not isValidValue:
raise RoboFabError("Invalid value (%s) for attribute (%s)." % (repr(value), attr))
# use the environment specific info attr set
# method if it is defined.
if hasattr(self, "_environmentSetAttr"):
self._environmentSetAttr(attr, value)
# fallback to super
else:
super(BaseInfo, self).__setattr__(attr, value)
# unknown attribute, test to see if it is a python attr
elif attr in self.__dict__ or attr in self._baseAttributes:
super(BaseInfo, self).__setattr__(attr, value)
# raise an attribute error
else:
raise AttributeError("Unknown attribute %s." % attr)
# subclasses with environment specific attr setting can
# implement this method. __setattr__ will call it if present.
# def _environmentSetAttr(self, attr, value):
# pass
def __getattr__(self, attr):
if attr in self._environmentOverrides:
return self._environmentGetAttr(attr)
# check to see if the attribute has been
# deprecated. if so, warn the caller and
# flag the value as needing conversion.
needValueConversionTo1 = False
if attr in self._deprecatedAttributes:
oldAttr = attr
oldValue = attr
newAttr, x = ufoLib.convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, None)
note = "The %s attribute has been deprecated. Use the new %s attribute." % (attr, newAttr)
warn(note, DeprecationWarning)
attr = newAttr
needValueConversionTo1 = True
# getting a known attribute
if attr in self._infoAttributes or attr in self._environmentAttributes:
# use the environment specific info attr get
# method if it is defined.
if hasattr(self, "_environmentGetAttr"):
value = self._environmentGetAttr(attr)
# fallback to super
else:
try:
value = super(BaseInfo, self).__getattribute__(attr)
except AttributeError:
return None
if needValueConversionTo1:
oldAttr, value = ufoLib.convertFontInfoValueForAttributeFromVersion2ToVersion1(attr, value)
return value
# raise an attribute error
else:
raise AttributeError("Unknown attribute %s." % attr)
# subclasses with environment specific attr retrieval can
# implement this method. __getattr__ will call it if present.
# it should return the requested value.
# def _environmentGetAttr(self, attr):
# pass
class BaseFeatures(RBaseObject):
def __init__(self):
RBaseObject.__init__(self)
self._text = ""
def _get_text(self):
return self._text
def _set_text(self, value):
assert isinstance(value, basestring)
self._text = value
text = property(_get_text, _set_text, doc="raw feature text.")
class BaseGroups(dict):
"""Base class for all RFont.groups objects"""
def __init__(self):
pass
def __repr__(self):
font = "unnamed_font"
fontParent = self.getParent()
if fontParent is not None:
try:
font = fontParent.info.postscriptFullName
except AttributeError: pass
return "<RGroups for %s>"%font
def getParent(self):
"""this method will be overwritten with a weakref if there is a parent."""
pass
def setParent(self, parent):
import weakref
self.__dict__['getParent'] = weakref.ref(parent)
def __setitem__(self, key, value):
#override base class to insure proper data is being stored
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(BaseGroups, self).__setitem__(key, value)
def findGlyph(self, glyphName):
"""return a list of all groups contianing glyphName"""
found = []
for i in self.keys():
l = self[i]
if glyphName in l:
found.append(i)
return found
class BaseLib(dict):
"""Base class for all lib objects"""
def __init__(self):
pass
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 "<RLib for %s>"%parent
def getParent(self):
"""this method will be overwritten with a weakref if there is a parent."""
pass
def setParent(self, parent):
import weakref
self.__dict__['getParent'] = weakref.ref(parent)
def copy(self, aParent=None):
"""Duplicate this lib."""
n = self.__class__()
if aParent is not None:
n.setParent(aParent)
elif self.getParent() is not None:
n.setParent(self.getParent())
for k in self.keys():
n[k] = copy.deepcopy(self[k])
return n
class BaseKerning(RBaseObject):
"""Base class for all kerning objects. Object behaves like a dict but has
some special kerning specific tricks."""
def __init__(self, kerningDict=None):
if not kerningDict:
kerningDict = {}
self._kerning = kerningDict
self.changed = False # if the object needs to be saved
def __repr__(self):
font = "unnamed_font"
fontParent = self.getParent()
if fontParent is not None:
try:
font = fontParent.info.postscriptFullName
except AttributeError: pass
return "<RKerning for %s>"%font
def __getitem__(self, key):
if isinstance(key, tuple):
pair = key
return self.get(pair)
elif isinstance(key, str):
raise RoboFabError, 'kerning pair must be a tuple: (left, right)'
else:
keys = self.keys()
if key > len(keys):
raise IndexError
keys.sort()
pair = keys[key]
if not self._kerning.has_key(pair):
raise IndexError
else:
return pair
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:
del self._kerning[pair]
else:
self._kerning[pair] = value
self._hasChanged()
def __len__(self):
return len(self._kerning.keys())
def _hasChanged(self):
"""mark the object and it's parent as changed"""
self.setChanged(True)
if self.getParent() is not None:
self.getParent()._hasChanged()
def keys(self):
"""return list of kerning pairs"""
return self._kerning.keys()
def values(self):
"""return a list of kerning values"""
return self._kerning.values()
def items(self):
"""return a list of kerning items"""
return self._kerning.items()
def has_key(self, pair):
return self._kerning.has_key(pair)
def get(self, pair, default=None):
"""get a value. return None if the pair does not exist"""
value = self._kerning.get(pair, default)
return value
def remove(self, pair):
"""remove a kerning pair"""
self[pair] = 0
def getAverage(self):
"""return average of all kerning pairs"""
if len(self) == 0:
return 0
value = 0
for i in self.values():
value = value + i
return value / float(len(self))
def getExtremes(self):
"""return the lowest and highest kerning values"""
if len(self) == 0:
return 0
values = self.values()
values.append(0)
values.sort()
return (values[0], values[-1])
def update(self, kerningDict):
"""replace kerning data with the data in the given kerningDict"""
for pair in kerningDict.keys():
self[pair] = kerningDict[pair]
def clear(self):
"""clear all kerning"""
self._kerning = {}
def add(self, value):
"""add value to all kerning pairs"""
for pair in self.keys():
self[pair] = self[pair] + value
def scale(self, value):
"""scale all kernng pairs by value"""
for pair in self.keys():
self[pair] = self[pair] * value
def minimize(self, minimum=10):
"""eliminate pairs with value less than minimum"""
for pair in self.keys():
if abs(self[pair]) < minimum:
self[pair] = 0
def eliminate(self, leftGlyphsToEliminate=None, rightGlyphsToEliminate=None, analyzeOnly=False):
"""eliminate pairs containing a left glyph that is in the leftGlyphsToEliminate list
or a right glyph that is in the rightGlyphsToELiminate list.
sideGlyphsToEliminate can be a string: 'a' or list: ['a', 'b'].
analyzeOnly will not remove pairs. it will return a count
of all pairs that would be removed."""
if analyzeOnly:
count = 0
lgte = leftGlyphsToEliminate
rgte = rightGlyphsToEliminate
if isinstance(lgte, str):
lgte = [lgte]
if isinstance(rgte, str):
rgte = [rgte]
for pair in self.keys():
left, right = pair
if left in lgte or right in rgte:
if analyzeOnly:
count = count + 1
else:
self[pair] = 0
if analyzeOnly:
return count
else:
return None
def interpolate(self, sourceDictOne, sourceDictTwo, value, clearExisting=True):
"""interpolate the kerning between sourceDictOne
and sourceDictTwo. clearExisting will clear existing
kerning first."""
from sets import Set
if isinstance(value, tuple):
# in case the value is a x, y tuple: use the x only.
value = value[0]
if clearExisting:
self.clear()
pairs = Set(sourceDictOne.keys()) | Set(sourceDictTwo.keys())
for pair in pairs:
s1 = sourceDictOne.get(pair, 0)
s2 = sourceDictTwo.get(pair, 0)
self[pair] = _interpolate(s1, s2, value)
def round(self, multiple=10):
"""round the kerning pair values to increments of multiple"""
for pair in self.keys():
value = self[pair]
self[pair] = int(round(value / float(multiple))) * multiple
def occurrenceCount(self, glyphsToCount):
"""return a dict with glyphs as keys and the number of
occurances of that glyph in the kerning pairs as the value
glyphsToCount can be a string: 'a' or list: ['a', 'b']"""
gtc = glyphsToCount
if isinstance(gtc, str):
gtc = [gtc]
gtcDict = {}
for glyph in gtc:
gtcDict[glyph] = 0
for pair in self.keys():
left, right = pair
if not gtcDict.get(left):
gtcDict[left] = 0
if not gtcDict.get(right):
gtcDict[right] = 0
gtcDict[left] = gtcDict[left] + 1
gtcDict[right] = gtcDict[right] + 1
found = {}
for glyphName in gtc:
found[glyphName] = gtcDict[glyphName]
return found
def getLeft(self, glyphName):
"""Return a list of kerns with glyphName as left character."""
hits = []
for k, v in self.items():
if k[0] == glyphName:
hits.append((k, v))
return hits
def getRight(self, glyphName):
"""Return a list of kerns with glyphName as left character."""
hits = []
for k, v in self.items():
if k[1] == glyphName:
hits.append((k, v))
return hits
def combine(self, kerningDicts, overwriteExisting=True):
"""combine two or more kerning dictionaries.
overwrite exsisting duplicate pairs if overwriteExisting=True"""
if isinstance(kerningDicts, dict):
kerningDicts = [kerningDicts]
for kd in kerningDicts:
for pair in kd.keys():
exists = self.has_key(pair)
if exists and overwriteExisting:
self[pair] = kd[pair]
elif not exists:
self[pair] = kd[pair]
def swapNames(self, swapTable):
"""change glyph names in all kerning pairs based on swapTable.
swapTable = {'BeforeName':'AfterName', ...}"""
for pair in self.keys():
foundInstance = False
left, right = pair
if swapTable.has_key(left):
left = swapTable[left]
foundInstance = True
if swapTable.has_key(right):
right = swapTable[right]
foundInstance = True
if foundInstance:
self[(left, right)] = self[pair]
self[pair] = 0
def explodeClasses(self, leftClassDict=None, rightClassDict=None, analyzeOnly=False):
"""turn class kerns into real kerning pairs. classes should
be defined in dicts: {'O':['C', 'G', 'Q'], 'H':['B', 'D', 'E', 'F', 'I']}.
analyzeOnly will not remove pairs. it will return a count
of all pairs that would be added"""
if not leftClassDict:
leftClassDict = {}
if not rightClassDict:
rightClassDict = {}
if analyzeOnly:
count = 0
for pair in self.keys():
left, right = pair
value = self[pair]
if leftClassDict.get(left) and rightClassDict.get(right):
allLeft = leftClassDict[left] + [left]
allRight = rightClassDict[right] + [right]
for leftSub in allLeft:
for rightSub in allRight:
if analyzeOnly:
count = count + 1
else:
self[(leftSub, rightSub)] = value
elif leftClassDict.get(left) and not rightClassDict.get(right):
allLeft = leftClassDict[left] + [left]
for leftSub in allLeft:
if analyzeOnly:
count = count + 1
else:
self[(leftSub, right)] = value
elif rightClassDict.get(right) and not leftClassDict.get(left):
allRight = rightClassDict[right] + [right]
for rightSub in allRight:
if analyzeOnly:
count = count + 1
else:
self[(left, rightSub)] = value
if analyzeOnly:
return count
else:
return None
def implodeClasses(self, leftClassDict=None, rightClassDict=None, analyzeOnly=False):
"""condense the number of kerning pairs by applying classes.
this will eliminate all pairs containg the classed glyphs leaving
pairs that contain the key glyphs behind. analyzeOnly will not
remove pairs. it will return a count of all pairs that would be removed."""
if not leftClassDict:
leftClassDict = {}
if not rightClassDict:
rightClassDict = {}
leftImplode = []
rightImplode = []
for value in leftClassDict.values():
leftImplode = leftImplode + value
for value in rightClassDict.values():
rightImplode = rightImplode + value
analyzed = self.eliminate(leftGlyphsToEliminate=leftImplode, rightGlyphsToEliminate=rightImplode, analyzeOnly=analyzeOnly)
if analyzeOnly:
return analyzed
else:
return None
def importAFM(self, path, clearExisting=True):
"""Import kerning pairs from an AFM file. clearExisting=True will
clear all exising kerning"""
from fontTools.afmLib import AFM
#a nasty hack to fix line ending problems
f = open(path, 'rb')
text = f.read().replace('\r', '\n')
f.close()
f = open(path, 'wb')
f.write(text)
f.close()
#/nasty hack
kerning = AFM(path)._kerning
if clearExisting:
self.clear()
for pair in kerning.keys():
self[pair] = kerning[pair]
def asDict(self, returnIntegers=True):
"""return the object as a dictionary"""
if not returnIntegers:
return self._kerning
else:
#duplicate the kerning dict so that we aren't destroying it
kerning = {}
for pair in self.keys():
kerning[pair] = int(round(self[pair]))
return kerning
def __add__(self, other):
from sets import Set
new = self.__class__()
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):
from sets import Set
new = self.__class__()
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):
new = self.__class__()
for name, value in self.items():
new[name] = value * factor
return new
__rmul__ = __mul__
def __div__(self, factor):
if factor == 0:
raise ZeroDivisionError
return self.__mul__(1.0/factor)