summaryrefslogtreecommitdiff
path: root/misc/pylib/robofab/objects/objectsBase.pyx
diff options
context:
space:
mode:
Diffstat (limited to 'misc/pylib/robofab/objects/objectsBase.pyx')
-rwxr-xr-xmisc/pylib/robofab/objects/objectsBase.pyx3426
1 files changed, 3426 insertions, 0 deletions
diff --git a/misc/pylib/robofab/objects/objectsBase.pyx b/misc/pylib/robofab/objects/objectsBase.pyx
new file mode 100755
index 000000000..3154fb3f4
--- /dev/null
+++ b/misc/pylib/robofab/objects/objectsBase.pyx
@@ -0,0 +1,3426 @@
+"""
+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 robofab.misc.arrayTools import updateBounds, pointInRect, unionRect, sectRect
+from fontTools.pens.basePen import AbstractPen
+from fontTools.pens.areaPen import AreaPen
+from ..exceptions import RoboFabError, RoboFabWarning
+
+try:
+ set
+except NameError:
+ from sets import Set as set
+
+#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(a, b, v):
+ """interpolate point by factor v"""
+ xa, ya = a
+ xb, yb = b
+ if not isinstance(v, tuple):
+ xv = v
+ yv = v
+ else:
+ xv, yv = v
+ return xa + (xb-xa) * xv, ya + (yb-ya) * yv
+
+def _scalePointFromCenter(pt, scale, center):
+ """scale a point from a center point"""
+ pointX, pointY = pt
+ scaleX, scaleY = scale
+ centerX, centerY = center
+ ogCenter = center
+ 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(pt):
+ """Round a vector"""
+ return int(round(pt[0])), int(round(pt[1]))
+
+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:
+ 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."""
+ 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
+ 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
+ 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 __ne__(self, other):
+ return not self.__eq__(other)
+
+ 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, pt, evenOdd=0):
+ """determine if the point is in the black or white of the glyph"""
+ x, y = pt
+ 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, pt, contours=True, components=True, anchors=True):
+ """Move a glyph's items that are flagged as True"""
+ x, y = roundPt(pt)
+ if contours:
+ for contour in self.contours:
+ contour.move((x, y))
+ if components:
+ for component in self.components:
+ component.move((x, y))
+ if anchors:
+ for anchor in self.anchors:
+ anchor.move((x, y))
+
+ def scale(self, pt, center=(0, 0)):
+ """scale the glyph"""
+ x, y = pt
+ for contour in self.contours:
+ contour.scale((x, y), center=center)
+ for component in self.components:
+ offset = component.offset
+ component.offset = _scalePointFromCenter(offset, pt, 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):
+ pen = AreaPen(self)
+ self.draw(pen)
+ return pen.value < 0
+
+ clockwise = property(_get_clockwise, _set_clockwise, doc="direction of contour: positive=counterclockwise negative=clockwise")
+
+ 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(segment.onCurve, '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, pt):
+ """move the contour"""
+ #this will be faster if we go straight to the points
+ for point in self.points:
+ point.move(pt)
+
+ def scale(self, pt, center=(0, 0)):
+ """scale the contour"""
+ #this will be faster if we go straight to the points
+ for point in self.points:
+ point.scale(pt, 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, pt, 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=pt, 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, pt):
+ """move the segment"""
+ for point in self.points:
+ point.move(pt)
+
+ def scale(self, pt, center=(0, 0)):
+ """scale the segment"""
+ for point in self.points:
+ point.scale(pt, 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, pt):
+ """Move the point"""
+ self.x, self.y = addPt((self.x, self.y), pt)
+
+ def scale(self, pt, center=(0, 0)):
+ """scale the point"""
+ nX, nY = _scalePointFromCenter((self.x, self.y), pt, 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, pt):
+ """move the bPoint"""
+ x, y = pt
+ 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, pt, center=(0, 0)):
+ """scale the bPoint"""
+ x, y = pt
+ 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, pt):
+ """Move the anchor"""
+ x, y = pt
+ pX, pY = self.position
+ self.position = (pX+x, pY+y)
+
+ def scale(self, pt, center=(0, 0)):
+ """scale the anchor"""
+ pos = self.position
+ self.position = _scalePointFromCenter(pos, pt, 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."""
+ 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):
+ 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):
+ 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)
+