root/trunk/robofab/Lib/robofab/objects/objectsBase.py

Revision 34, 101.2 kB (checked in by erik, 3 years ago)

Base object for font level postscript hint data such as the blue values and stems. See RF and FL modules for actual implementation.

  • Property svn:executable set to
Line 
1 """     
2 Base classes for the Unified Font Objects (UFO),
3 a series of classes that deal with fonts, glyphs,
4 contours and related things.
5
6 Unified Font Objects are:
7 - platform independent
8 - application independent
9
10 About Object Inheritance:
11 objectsFL and objectsRF objects inherit
12 methods and attributes from these objects.
13 In other words, if it is in here, you can
14 do it with the objectsFL and objectsRF.
15 """
16
17
18 from __future__ import generators
19
20
21 from robofab import RoboFabError
22 from fontTools.misc.arrayTools import updateBounds, pointInRect, unionRect, sectRect
23 from fontTools.pens.basePen import AbstractPen
24 import math
25 import copy
26
27 #constants for dealing with segments, points and bPoints
28 MOVE = 'move'
29 LINE = 'line'
30 CORNER = 'corner'
31 CURVE = 'curve'
32 QCURVE = 'qcurve'
33 OFFCURVE = 'offcurve'
34
35 DEGREE = 180 / math.pi
36
37
38
39 # the key for the postscript hint data stored in the UFO
40 postScriptHintDataLibKey = "org.robofab.postScriptHintData"
41
42
43 class BasePostScriptFontHintValues(object):
44         """ Base class for font-level postscript hinting information.
45                 Blues values, stem values.
46         """
47        
48         _attrs = {
49                 # some of these values can have only a certain number of elements
50                 'blueFuzz':     {'default': None, 'max':1},
51                 'blueScale':    {'default': None, 'max':1},
52                 'blueShift':    {'default': None, 'max':1},
53                 'forceBold':    {'default': None, 'max':1},
54                 'blueValues':   {'default': None, 'max':13},
55                 'otherBlues':   {'default': None, 'max':9},
56                 'familyBlues':  {'default': None, 'max':13},
57                 'familyOtherBlues': {'default': None, 'max':9},
58                 'vStems':               {'default': None, 'max':11},
59                 'hStems':               {'default': None, 'max':11},
60                 }
61                
62         def __init__(self):
63                 for name in self._attrs.keys():
64                         setattr(self, name, self._attrs[name])
65                
66         def getParent(self):
67                 """this method will be overwritten with a weakref if there is a parent."""
68                 return None
69
70         def setParent(self, parent):
71                 import weakref
72                 self.getParent = weakref.ref(parent)
73
74         def fromDict(self, data):
75                 for name in self._attrs:
76                         if name in data:
77                                 setattr(self, name, data[name])
78        
79         def asDict(self):
80                 d = {}
81                 for name in self._attrs:
82                         try:
83                                 value = getattr(self, name)
84                         except AttributeError:
85                                 print "%s attribute not supported"%name
86                                 continue
87                         if value is not None or not value:
88                                 d[name] = getattr(self, name)
89                 return d
90        
91         def __repr__(self):
92                 return "<PostScript Font Hints Values>"
93
94
95 class RoboFabInterpolationError(Exception): pass
96
97
98 def _interpolate(a,b,v):
99         """interpolate values by factor v"""
100         return a + (b-a) * v
101
102 def _interpolatePt((xa, ya),(xb, yb),v):
103         """interpolate point by factor v"""
104         if not isinstance(v, tuple):
105                 xv = v
106                 yv = v
107         else:
108                 xv, yv = v
109         return xa + (xb-xa) * xv, ya + (yb-ya) * yv
110
111 def _scalePointFromCenter((pointX, pointY), (scaleX, scaleY), (centerX, centerY)):
112         """scale a point from a center point"""
113         ogCenter = (centerX, centerY)
114         scaledCenter = (centerX * scaleX, centerY * scaleY)
115         shiftVal = (scaledCenter[0] - ogCenter[0], scaledCenter[1] - ogCenter[1])
116         scaledPointX = (pointX * scaleX) - shiftVal[0]
117         scaledPointY = (pointY * scaleY) - shiftVal[1]
118         return (scaledPointX, scaledPointY)
119
120 def _box(objectToMeasure, fontObject=None):
121         """calculate the bounds of the object and return it as a (xMin, yMin, xMax, yMax)"""
122         #from fontTools.pens.boundsPen import BoundsPen
123         from robofab.pens.boundsPen import BoundsPen
124         boundsPen = BoundsPen(glyphSet=fontObject)
125         objectToMeasure.draw(boundsPen)
126         bounds = boundsPen.bounds
127         if bounds is None:
128                 bounds = (0, 0, 0, 0)
129         return bounds
130
131 def roundPt((x, y)):
132         """Round a vector"""
133         return int(round(x)), int(round(y))
134
135 def addPt(ptA, ptB):
136         """Add two vectors"""
137         return ptA[0] + ptB[0], ptA[1] + ptB[1]
138
139 def subPt(ptA, ptB):
140         """Substract two vectors"""
141         return ptA[0] - ptB[0], ptA[1] - ptB[1]
142
143 def mulPt(ptA, scalar):
144         """Multiply a vector with scalar"""
145         if not isinstance(scalar, tuple):
146                 f1 = scalar
147                 f2 = scalar
148         else:
149                 f1, f2 = scalar
150         return ptA[0]*f1, ptA[1]*f2
151        
152 def relativeBCPIn(anchor, BCPIn):
153         """convert absolute incoming bcp value to a relative value"""
154         return (BCPIn[0] - anchor[0], BCPIn[1] - anchor[1])
155
156 def absoluteBCPIn(anchor, BCPIn):
157         """convert relative incoming bcp value to an absolute value"""
158         return (BCPIn[0] + anchor[0], BCPIn[1] + anchor[1])
159
160 def relativeBCPOut(anchor, BCPOut):
161         """convert absolute outgoing bcp value to a relative value"""
162         return (BCPOut[0] - anchor[0], BCPOut[1] - anchor[1])
163
164 def absoluteBCPOut(anchor, BCPOut):
165         """convert relative outgoing bcp value to an absolute value"""
166         return (BCPOut[0] + anchor[0], BCPOut[1] + anchor[1])
167
168 class FuzzyNumber(object):
169
170         def __init__(self, value, threshold):
171                 self.value = value
172                 self.threshold = threshold
173        
174         def __cmp__(self, other):
175                 if abs(self.value - other.value) < self.threshold:
176                         return 0
177                 else:
178                         return cmp(self.value, other.value)
179
180
181 class RBaseObject(object):
182        
183         """Base class for wrapper objects"""
184        
185         attrMap= {}
186         _title = "RoboFab Wrapper"
187        
188         def __init__(self):
189                 self._object = {}
190                 self.changed = False            # if the object needs to be saved
191                 self.selected = False
192                
193         def __len__(self):
194                 return len(self._object)
195        
196         def __repr__(self):
197                 try:
198                         name = `self._object`
199                 except:
200                         name = "None"
201                 return "<%s for %s>" %(self._title, name)
202        
203         def copy(self, aParent=None):
204                 """Duplicate this object. Pass an object for parenting if you want."""
205                 n = self.__class__()
206                 if aParent is not None:
207                         n.setParent(aParent)
208                 elif self.getParent() is not None:
209                         n.setParent(self.getParent())
210                 dont = ['getParent']
211                 for k in self.__dict__.keys():
212                         if k in dont:
213                                 continue
214                         elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)):
215                                 dup = self.__dict__[k].copy(n)
216                         else:
217                                 dup = copy.deepcopy(self.__dict__[k])
218                         setattr(n, k, dup)
219                 return n
220                        
221         def round(self):
222                 pass
223        
224         def isRobofab(self):
225                 """Presence of this method indicates a Robofab object"""
226                 return 1
227                
228         def naked(self):
229                 """Return the wrapped object itself, in case it is needed for direct access."""
230                 return self._object
231        
232         def setChanged(self, state=True):
233                 self.changed = state
234        
235         def getParent(self):
236                 """this method will be overwritten with a weakref if there is a parent."""
237                 return None
238        
239         def setParent(self, parent):
240                 import weakref
241                 self.getParent = weakref.ref(parent)
242                
243         def _writeXML(self, writer):
244                 pass
245
246         def dump(self, private=False):
247                 """Print a dump of this object to the std out."""
248                 from robofab.tools.objectDumper import dumpObject
249                 dumpObject(self, private)
250        
251
252
253 class BaseFont(RBaseObject):
254        
255         """Base class for all font objects."""
256        
257         _allFonts = []
258        
259         def __init__(self):
260                 import weakref
261                 RBaseObject.__init__(self)
262                 self.changed = False            # if the object needs to be saved
263                 self._allFonts.append(weakref.ref(self))
264                 self._supportHints = False
265                
266         def __repr__(self):
267                 try:
268                         name = self.info.fullName
269                 except AttributeError:
270                         name = "unnamed_font"
271                 return "<RFont font for %s>" %(name)
272        
273         def __eq__(self, other):
274                 #Compare this font with another, compare if they refer to the same file.
275                 return self._compare(other)
276                
277         def _compare(self, other):
278                 """Compare this font to other. RF and FL UFO implementations need
279                 slightly different ways of comparing fonts. This method does the
280                 basic stuff. Start with simple and quick comparisons, then move into
281                 detailed comparisons of glyphs."""
282                 if not hasattr(other, "fileName"):
283                         return False
284                 if self.fileName is not None and self.fileName == other.fileName:
285                         return True
286                 if self.fileName <> other.fileName:
287                         return False
288                 # this will falsely identify two distinct "Untitled" as equal
289                 # so test some more. A lot of work to please some dolt who
290                 # does not save his fonts while running scripts.
291                 try:
292                         if len(self) <> len(other):
293                                 return False
294                 except TypeError:
295                         return False
296                 # same name and length. start comparing glyphs
297                 namesSelf = self.keys()
298                 namesOther = other.keys()
299                 namesSelf.sort()
300                 namesOther.sort()
301                 for i in range(len(namesSelf)):
302                         if namesSelf[i] <> namesOther[i]:
303                                 return False
304                 for c in self:
305                         if not c == other[c.name]:
306                                 return False
307                 return True
308                
309         def keys(self):
310                 # must be implemented by subclass
311                 raise NotImplementedError
312
313         def __iter__(self):
314                 for glyphName in self.keys():
315                         yield self.getGlyph(glyphName)
316
317         def __getitem__(self, glyphName):
318                 return self.getGlyph(glyphName)
319
320         def _hasChanged(self):
321                 #mark the object as changed
322                 self.setChanged(True)
323        
324         def update(self):
325                 """update the font"""
326                 pass
327        
328         def close(self, save=1):
329                 """Close the font, saving is optional."""
330                 pass
331
332         def round(self):
333                 """round all of the points in all of the glyphs"""
334                 for glyph in self.glyphs:
335                         glyph.round()
336        
337         def autoUnicodes(self):
338                 """Using fontTools.agl, assign Unicode lists to all glyphs in the font"""
339                 for glyph in self:
340                         glyph.autoUnicodes()
341                        
342         def getCharacterMapping(self):
343                 """Create a dictionary of unicode -> [glyphname, ...] mappings.
344                 Note that this dict is created each time this method is called,
345                 which can make it expensive for larger fonts. All glyphs are loaded.
346                 Note that one glyph can have multiple unicode values,
347                 and a unicode value can have multiple glyphs pointing to it."""
348                 map = {}
349                 for glyph in self:
350                         for u in glyph.unicodes:
351                                 if not map.has_key(u):
352                                         map[u] = []
353                                 map[u].append(glyph.name)
354                 return map
355        
356         def getReverseComponentMapping(self):
357                 """
358                 Get a reversed map of component references in the font.
359                 {
360                 'A' : ['Aacute', 'Aring']
361                 'acute' : ['Aacute']
362                 'ring' : ['Aring']
363                 etc.
364                 }
365                 """
366                 map = {}
367                 for glyph in self:
368                         glyphName = glyph.name
369                         for component in glyph.components:
370                                 baseGlyphName = component.baseGlyph
371                                 if not map.has_key(baseGlyphName):
372                                         map[baseGlyphName] = []
373                                 map[baseGlyphName].append(glyphName)
374                 return map
375
376         def compileGlyph(self, glyphName, baseName, accentNames, \
377                 adjustWidth=False, preflight=False, printErrors=True):
378                 """Compile components into a new glyph using components and anchorpoints.
379                 font: the font
380                 glyphName: the name of the glyph where it all needs to go
381                 baseName: the name of the base glyph
382                 accentNames: a list of accentName, anchorName tuples, [('acute', 'top'), etc]
383                 """
384                 baseAnchors = ['left', 'right', 'top', 'bottom']
385                 accentAnchors = ['_left', '_right', '_top', '_bottom']
386                 anchors = {}
387                 errors = {}
388                 #grab the baseGlyph
389                 baseGlyph = self[baseName]
390                 #store the anchors in the baseGlyph
391                 for anchor in baseGlyph.getAnchors():
392                         if anchor.name in baseAnchors:
393                                 anchors[anchor.name] = anchor.position
394                 #make a destination glyph if it doesn't exist already
395                 destGlyph = self.newGlyph(glyphName, clear=True)
396                 if not preflight:
397                         #add the baseGlyph as a component to the destGlyph and set the width
398                         destGlyph.appendComponent(baseName)
399                         destGlyph.width = baseGlyph.width
400                 #go through the list of provided accentNames
401                 for accentName, accentPosition in accentNames:
402                         #grab the accent if it exists
403                         try:
404                                 accent = self[accentName]
405                         except IndexError:
406                                 errors["glyph '%s' is missing in font %s"%(accentName, self.fullName)] =  1
407                                 continue
408                         localAnchors = {}
409                         foundAnchor = None
410                         accX = accent.getAnchors()
411                         #temporarily store the anchors found in the accent
412                         for anchor in accX:
413                                 localAnchors[anchor.name] = anchor.position
414                         #look through the list of possible accent positions
415                         for anchorName in accentAnchors:
416                                 #if we have an anchor in the accent that matches something in the list
417                                 if anchorName in localAnchors:
418                                         #if this anchor name matches an anchor position that the user has requested, we have a winner
419                                         if ''.join(['_', accentPosition]) == anchorName:
420                                                 foundAnchor = anchorName
421                                                 break
422                         if foundAnchor:
423                                 #grab the coordinates of the found anchor
424                                 accentAnchorX, accentAnchorY = localAnchors[foundAnchor]
425                                 #get the coordinates for the cooresponding anchor in the baseGlyph
426                                 try:
427                                         baseX, baseY = anchors[foundAnchor[1:]]
428                                 except KeyError:
429                                         errors["anchor '%s' not found in glyph '%s' of font %s"%(foundAnchor[1:], baseName, self.info.fullName)]=1
430                                         continue
431                                 #calculate the accent componet offset values
432                                 xShift = baseX - accentAnchorX
433                                 yShift = baseY - accentAnchorY
434                                 #add the accent to the destination glyph
435                                 if not preflight:
436                                         destGlyph.appendComponent(accentName, offset=(xShift, yShift))
437                                 #if the found anchor the anchor that it was just aligned to, make the values for that anchor the new standard
438                                 if foundAnchor[1:] in localAnchors:
439                                         newX, newY = localAnchors[foundAnchor[1:]]
440                                         newX = newX+xShift
441                                         newY = newY+yShift
442                                         anchors[foundAnchor[1:]] = (newX, newY)
443                 #adjust the width if the user has requested it
444                 if adjustWidth and not preflight:
445                         for accentName, accentPosition in accentNames:
446                                 #accent might not be present in the font -- the user has been warned already
447                                 try:
448                                         accent = self[accentName]
449                                 except IndexError:
450                                         continue
451                                 if accent is None: continue
452                                 #not sure what this does...
453                                 #if accentPosition == 'right' or accentPosition == '_left':
454                                 #       for component in destGlyph.getComponents():
455                                 #               if component.baseName == accentName:
456                                
457                                 #set the right and left margins only if the accent has been added to the right or left
458                                 if accentPosition == 'right':
459                                         destGlyph.rightMargin = self[accentName].rightMargin
460                                 elif accentPosition == 'left':
461                                         destGlyph.leftMargin = self[accentName].leftMargin
462                         if preflight:
463                                 return errors.keys()
464                         if printErrors:
465                                 for px in errors.keys():
466                                         print px
467                 return destGlyph
468        
469         def generateGlyph(self, glyphName, replace=1, preflight=False, printErrors=True):
470                 """Generate a glyph and return it. Assembled from GlyphConstruction.txt"""
471                 from robofab.tools.toolsAll import readGlyphConstructions
472                 con = readGlyphConstructions()
473                 entry = con.get(glyphName, None)
474                 if not entry:
475                         print "glyph '%s' is not listed in the robofab/Data/GlyphConstruction.txt"%(glyphName)
476                         return
477                 baseName = con[glyphName][0]
478                 parts = con[glyphName][1:]
479                 return self.compileGlyph(glyphName, baseName, parts, adjustWidth=1, preflight=preflight, printErrors=printErrors)
480        
481         def interpolate(self, factor, minFont, maxFont, suppressError=True, analyzeOnly=False, doProgress=False):
482                 """Traditional interpolation method. Interpolates by factor between minFont and maxFont.
483                 suppressError will supress all tracebacks and analyze only will not perform the interpolation
484                 but it will analyze all glyphs and return a dict of problems."""
485                 from sets import Set
486                 errors = {}
487                 if not isinstance(factor, tuple):
488                         factor = factor, factor
489                 minGlyphNames = minFont.keys()
490                 maxGlyphNames = maxFont.keys()
491                 allGlyphNames = list(Set(minGlyphNames) | Set(maxGlyphNames))
492                 if doProgress:
493                         from robofab.interface.all.dialogs import ProgressBar
494                         progress = ProgressBar('Interpolating...', len(allGlyphNames))
495                         tickCount = 0
496                 # some dimensions and values
497                 self.info.ascender = _interpolate(minFont.info.ascender, maxFont.info.ascender, factor[1])
498                 self.info.descender = _interpolate(minFont.info.descender, maxFont.info.descender, factor[1])
499                 # check for the presence of the glyph in each of the fonts
500                 for glyphName in allGlyphNames:
501                         if doProgress:
502                                 progress.label(glyphName)
503                         fatalError = False
504                         if glyphName not in minGlyphNames:
505                                 fatalError = True
506                                 if not errors.has_key('Missing Glyphs'):
507                                         errors['Missing Glyphs'] = []
508                                 errors['Missing Glyphs'].append('Interpolation Error: %s not in %s'%(glyphName, minFont.info.fullName))
509                         if glyphName not in maxGlyphNames:
510                                 fatalError = True
511                                 if not errors.has_key('Missing Glyphs'):
512                                         errors['Missing Glyphs'] = []
513                                 errors['Missing Glyphs'].append('Interpolation Error: %s not in %s'%(glyphName, maxFont.info.fullName))
514                         # if no major problems, proceed.
515                         if not fatalError:
516                                 # remove the glyph since FontLab has a problem with
517                                 # interpolating an existing glyph that contains
518                                 # some contour data.
519                                 oldLib = {}
520                                 oldMark = None
521                                 oldNote = None
522                                 if self.has_key(glyphName):
523                                         glyph = self[glyphName]
524                                         oldLib = dict(glyph.lib)
525                                         oldMark = glyph.mark
526                                         oldNote = glyph.note
527                                         self.removeGlyph(glyphName)
528                                 selfGlyph = self.newGlyph(glyphName)
529                                 selfGlyph.lib.update(oldLib)
530                                 if oldMark != None:
531                                         selfGlyph.mark = oldMark
532                                 selfGlyph.note = oldNote
533                                 min = minFont[glyphName]
534                                 max = maxFont[glyphName]
535                                 ok, glyphErrors = selfGlyph.interpolate(factor, min, max, suppressError=suppressError, analyzeOnly=analyzeOnly)
536                                 if not errors.has_key('Glyph Errors'):
537                                         errors['Glyph Errors'] = {}
538                                 errors['Glyph Errors'][glyphName] = glyphErrors
539                         if doProgress:
540                                 progress.tick(tickCount)
541                                 tickCount = tickCount + 1
542                 if doProgress:
543                         progress.close()
544                 return errors
545
546         def getGlyphNameToFileNameFunc(self):
547                 funcName = self.lib.get("org.robofab.glyphNameToFileNameFuncName")
548                 if funcName is None:
549                         return None
550                 parts = funcName.split(".")
551                 module = ".".join(parts[:-1])
552                 try:
553                         item = __import__(module)
554                         for sub in parts[1:]:
555                                 item = getattr(item, sub)
556                 except (ImportError, AttributeError):
557                         from warnings import warn
558                         warn("Can't find glyph name to file name converter function, "
559                                 "falling back to default scheme (%s)" % funcName, RoboFabWarning)
560                         return None
561                 else:
562                         return item
563
564
565 class BaseGlyph(RBaseObject):
566        
567         """Base class for all glyph objects."""
568        
569         def __init__(self):
570                 RBaseObject.__init__(self)
571                 #self.contours = []
572                 #self.components = []
573                 #self.anchors = []
574                 #self.width = 0
575                 #self.note = None
576                 ##self.unicodes = []
577                 #self.selected = None
578                 self.changed = False            # if the object needs to be saved
579        
580         def __repr__(self):
581                 font = "unnamed_font"
582                 glyph = "unnamed_glyph"
583                 fontParent = self.getParent()
584                 if fontParent is not None:
585                         try:
586                                 font = fontParent.info.fullName
587                         except AttributeError:
588                                 pass
589                 try:
590                         glyph = self.name
591                 except AttributeError:
592                         pass
593                 return "<RGlyph for %s.%s>" %(font, glyph)
594                
595         #
596         # Glyph Math
597         #
598        
599         def _getMathData(self):
600                 from robofab.pens.mathPens import GetMathDataPointPen
601                 pen = GetMathDataPointPen()
602                 self.drawPoints(pen)
603                 data = pen.getData()
604                 return data
605        
606         def _setMathData(self, data, destination=None):
607                 from robofab.pens.mathPens import CurveSegmentFilterPointPen
608                 if destination is None:
609                         newGlyph = self._mathCopy()
610                 else:
611                         newGlyph = destination
612                 newGlyph.clear()
613                 #
614                 # draw the data onto the glyph
615                 pointPen = newGlyph.getPointPen()
616                 filterPen = CurveSegmentFilterPointPen(pointPen)
617                 for contour in data['contours']:
618                         filterPen.beginPath()
619                         for segmentType, pt, smooth, name in contour:
620                                 filterPen.addPoint(pt=pt, segmentType=segmentType, smooth=smooth, name=name)
621                         filterPen.endPath()
622                 for baseName, transformation in data['components']:
623                         filterPen.addComponent(baseName, transformation)
624                 for pt, name in data['anchors']:
625                         filterPen.beginPath()
626                         filterPen.addPoint(pt=pt, segmentType="move", smooth=False, name=name)
627                         filterPen.endPath()
628                 newGlyph.width = data['width']
629                 #
630                 return newGlyph
631        
632         def _getMathDestination(self):
633                 # make a new, empty glyph
634                 return self.__class__()
635        
636         def _mathCopy(self):
637                 # copy self without contour, component and anchor data
638                 glyph = self._getMathDestination()
639                 glyph.name = self.name
640                 glyph.unicodes = list(self.unicodes)
641                 glyph.width = self.width
642                 glyph.note = self.note
643                 glyph.lib = dict(self.lib)
644                 return glyph
645        
646         def _processMathOne(self, otherGlyph, funct):
647                 # used by: __add__, __sub__
648                 #
649                 newData =       {
650                                 'contours':[],
651                                 'components':[],
652                                 'anchors':[],
653                                 'width':None
654                                 }
655                 selfData = self._getMathData()
656                 otherData = otherGlyph._getMathData()
657                 #
658                 # contours
659                 selfContours = selfData['contours']
660                 otherContours = otherData['contours']
661                 newContours = newData['contours']
662                 if len(selfContours) > 0:
663                         for contourIndex in xrange(len(selfContours)):
664                                 newContours.append([])
665                                 selfContour = selfContours[contourIndex]
666                                 otherContour = otherContours[contourIndex]
667                                 for pointIndex in xrange(len(selfContour)):
668                                         segType, pt, smooth, name = selfContour[pointIndex]
669                                         newX, newY = funct(selfContour[pointIndex][1], otherContour[pointIndex][1])
670                                         newContours[-1].append((segType, (newX, newY), smooth, name))
671                 # anchors
672                 selfAnchors = selfData['anchors']
673                 otherAnchors = otherData['anchors']
674                 newAnchors = newData['anchors']
675                 if len(selfAnchors) > 0:
676                         selfAnchors, otherAnchors = self._mathAnchorCompare(selfAnchors, otherAnchors)
677                         anchorNames = selfAnchors.keys()
678                         for anchorName in anchorNames:
679                                 selfAnchorList = selfAnchors[anchorName]
680                                 otherAnchorList = otherAnchors[anchorName]
681                                 for i in range(len(selfAnchorList)):
682                                         selfAnchor = selfAnchorList[i]
683                                         otherAnchor = otherAnchorList[i]
684                                         newAnchor = funct(selfAnchor, otherAnchor)
685                                         newAnchors.append((newAnchor, anchorName))
686                 # components
687                 selfComponents = selfData['components']
688                 otherComponents = otherData['components']
689                 newComponents = newData['components']
690                 if len(selfComponents) > 0:
691                         selfComponents, otherComponents = self._mathComponentCompare(selfComponents, otherComponents)
692                         componentNames = selfComponents.keys()
693                         for componentName in componentNames:
694                                 selfComponentList = selfComponents[componentName]
695                                 otherComponentList = otherComponents[componentName]
696                                 for i in range(len(selfComponentList)):
697                                         # transformation breakdown: xScale, xyScale, yxScale, yScale, xOffset, yOffset
698                                         selfXScale, selfXYScale, selfYXScale, selfYScale, selfXOffset, selfYOffset = selfComponentList[i]
699                                         otherXScale, otherXYScale, otherYXScale, otherYScale, otherXOffset, otherYOffset = otherComponentList[i]
700                                         newXScale, newXYScale = funct((selfXScale, selfXYScale), (otherXScale, otherXYScale))
701                                         newYXScale, newYScale = funct((selfYXScale, selfYScale), (otherYXScale, otherYScale))
702                                         newXOffset, newYOffset = funct((selfXOffset, selfYOffset), (otherXOffset, otherYOffset))
703                                         newComponents.append((componentName, (newXScale, newXYScale, newYXScale, newYScale, newXOffset, newYOffset)))
704                 return newData
705        
706         def _processMathTwo(self, factor, funct):
707                 # used by: __mul__, __div__
708                 #
709                 newData =       {
710                                 'contours':[],
711                                 'components':[],
712                                 'anchors':[],
713                                 'width':None
714                                 }
715                 selfData = self._getMathData()
716                 # contours
717                 selfContours = selfData['contours']
718                 newContours = newData['contours']
719                 for selfContour in selfContours:
720                         newContours.append([])
721                         for segType, pt, smooth, name in selfContour:
722                                 newX, newY = funct(pt, factor)
723                                 newContours[-1].append((segType, (newX, newY), smooth, name))
724                 # anchors
725                 selfAnchors = selfData['anchors']
726                 newAnchors = newData['anchors']
727                 for pt, anchorName in selfAnchors:
728                         newPt = funct(pt, factor)
729                         newAnchors.append((newPt, anchorName))
730                 # components
731                 selfComponents = selfData['components']
732                 newComponents = newData['components']
733                 for baseName, transformation in selfComponents:
734                         xScale, xyScale, yxScale, yScale, xOffset, yOffset = transformation
735                         newXOffset, newYOffset = funct((xOffset, yOffset), factor)
736                         newXScale, newYScale = funct((xScale, yScale), factor)
737                         newXYScale, newYXScale = funct((xyScale, yxScale), factor)
738                         newComponents.append((baseName, (newXScale, newXYScale, newYXScale, newYScale, newXOffset, newYOffset)))
739                 # return the data
740                 return newData
741        
742         def _mathAnchorCompare(self, selfMathAnchors, otherMathAnchors):
743                 # collect compatible anchors
744                 from sets import Set
745                 selfAnchors = {}
746                 for pt, name in selfMathAnchors:
747                         if not selfAnchors.has_key(name):
748                                 selfAnchors[name] = []
749                         selfAnchors[name].append(pt)
750                 otherAnchors = {}
751                 for pt, name in otherMathAnchors:
752                         if not otherAnchors.has_key(name):
753                                 otherAnchors[name] = []
754                         otherAnchors[name].append(pt)
755                 compatAnchors = Set(selfAnchors.keys()) & Set(otherAnchors.keys())
756                 finalSelfAnchors = {}
757                 finalOtherAnchors = {}
758                 for name in compatAnchors:
759                         if not finalSelfAnchors.has_key(name):
760                                 finalSelfAnchors[name] = []
761                         if not finalOtherAnchors.has_key(name):
762                                 finalOtherAnchors[name] = []
763                         selfList = selfAnchors[name]
764                         otherList = otherAnchors[name]
765                         selfCount = len(selfList)
766                         otherCount = len(otherList)
767                         if selfCount != otherCount:
768                                 r = range(min(selfCount, otherCount))
769                         else:
770                                 r = range(selfCount)
771                         for i in r:
772                                 finalSelfAnchors[name].append(selfList[i])
773                                 finalOtherAnchors[name].append(otherList[i])
774                 return finalSelfAnchors, finalOtherAnchors
775        
776         def _mathComponentCompare(self, selfMathComponents, otherMathComponents):
777                 # collect compatible components
778                 from sets import Set
779                 selfComponents = {}
780                 for baseName, transformation in selfMathComponents:
781                         if not selfComponents.has_key(baseName):
782                                 selfComponents[baseName] = []
783                         selfComponents[baseName].append(transformation)
784                 otherComponents = {}
785                 for baseName, transformation in otherMathComponents:
786                         if not otherComponents.has_key(baseName):
787                                 otherComponents[baseName] = []
788                         otherComponents[baseName].append(transformation)
789                 compatComponents = Set(selfComponents.keys()) & Set(otherComponents.keys())
790                 finalSelfComponents = {}
791                 finalOtherComponents = {}
792                 for baseName in compatComponents:
793                         if not finalSelfComponents.has_key(baseName):
794                                 finalSelfComponents[baseName] = []
795                         if not finalOtherComponents.has_key(baseName):
796                                 finalOtherComponents[baseName] = []
797                         selfList = selfComponents[baseName]
798                         otherList = otherComponents[baseName]
799                         selfCount = len(selfList)
800                         otherCount = len(otherList)
801                         if selfCount != otherCount:
802                                 r = range(min(selfCount, otherCount))
803                         else:
804                                 r = range(selfCount)
805                         for i in r:
806                                 finalSelfComponents[baseName].append(selfList[i])
807                                 finalOtherComponents[baseName].append(otherList[i])
808                 return finalSelfComponents, finalOtherComponents
809                
810         def __mul__(self, factor):
811                 assert isinstance(factor, (int, float, tuple)), "Glyphs can only be multiplied by int, float or a 2-tuple."
812                 if not isinstance(factor, tuple):
813                         factor = (factor, factor)
814                 data = self._processMathTwo(factor, mulPt)
815                 data['width'] = self.width * factor[0]
816                 return self._setMathData(data)
817
818         __rmul__ = __mul__
819
820         def __div__(self, factor):
821                 assert isinstance(factor, (int, float, tuple)), "Glyphs can only be divided by int, float or a 2-tuple."
822                 # calculate reverse factor, and cause nice ZeroDivisionError if it can't
823                 if isinstance(factor, tuple):
824                         reverse = 1.0/factor[0], 1.0/factor[1]
825                 else:
826                         reverse = 1.0/factor
827                 return self.__mul__(reverse)
828
829         def __add__(self, other):
830                 assert isinstance(other, BaseGlyph), "Glyphs can only be added to other glyphs."
831                 data = self._processMathOne(other, addPt)
832                 data['width'] = self.width + other.width
833                 return self._setMathData(data)
834
835         def __sub__(self, other):
836                 assert isinstance(other, BaseGlyph), "Glyphs can only be substracted from other glyphs."
837                 data = self._processMathOne(other, subPt)
838                 data['width'] = self.width + other.width
839                 return self._setMathData(data)
840        
841         #
842         # Interpolation
843         #
844        
845         def interpolate(self, factor, minGlyph, maxGlyph, suppressError=True, analyzeOnly=False):
846                 """Traditional interpolation method. Interpolates by factor between minGlyph and maxGlyph.
847                 suppressError will supress all tracebacks and analyze only will not perform the interpolation
848                 but it will analyze all glyphs and return a dict of problems."""
849                 if not isinstance(factor, tuple):
850                         factor = factor, factor
851                 fatalError = False
852                 if analyzeOnly:
853                         ok, errors = minGlyph.isCompatible(maxGlyph)
854                         return ok, errors
855                 minData = None
856                 maxData = None
857                 minName = minGlyph.name
858                 maxName = maxGlyph.name
859                 try:
860                         minData = minGlyph._getMathData()
861                         maxData = maxGlyph._getMathData()
862                         newContours = self._interpolateContours(factor, minData['contours'], maxData['contours'])
863                         newComponents = self._interpolateComponents(factor, minData['components'], maxData['components'])
864                         newAnchors = self._interpolateAnchors(factor, minData['anchors'], maxData['anchors'])
865                         newWidth = _interpolate(minGlyph.width, maxGlyph.width, factor[0])
866                         newData = {
867                                         'contours':newContours,
868                                         'components':newComponents,
869                                         'anchors':newAnchors,
870                                         'width':newWidth
871                                         }
872                         self._setMathData(newData, self)
873                 except IndexError:
874                         if not suppressError:
875                                 ok, errors = minGlyph.isCompatible(maxGlyph)
876                                 ok = not ok
877                                 return ok, errors
878                 self.update()
879                 return False, []
880        
881         def isCompatible(self, otherGlyph, report=True):
882                 """Return a bool value if the glyph is compatible with otherGlyph.
883                 With report = True, isCompatible will return a report of what's wrong.
884                 The interpolate method requires absolute equality between contour data.
885                 Absolute equality is preferred among component and anchor data, but
886                 it is NOT required. Interpolation between components and anchors
887                 will only deal with compatible data and incompatible data will be
888                 ignored. This method reflects this system."""
889                 selfName = self.name
890                 selfData = self._getMathData()
891                 otherName = otherGlyph.name
892                 otherData = otherGlyph._getMathData()
893                 compatible, errors = self._isCompatibleInternal(selfName, otherName, selfData, otherData)
894                 if report:
895                         return compatible, errors
896                 return compatible
897                                
898         def _isCompatibleInternal(self, selfName, otherName, selfData, otherData):
899                 fatalError = False
900                 errors = []
901                 ## contours
902                 # any contour incompatibilities
903                 # result in fatal errors
904                 selfContours = selfData['contours']
905                 otherContours = otherData['contours']
906                 if len(selfContours) != len(otherContours):
907                         fatalError = True
908                         errors.append("Fatal error: glyph %s and glyph %s don't have the same number of contours." %(selfName, otherName))
909                 else:
910                         for contourIndex in xrange(len(selfContours)):
911                                 selfContour = selfContours[contourIndex]
912                                 otherContour = otherContours[contourIndex]
913                                 if len(selfContour) != len(otherContour):
914                                         fatalError = True
915                                         errors.append("Fatal error: contour %d in glyph %s and glyph %s don't have the same number of segments." %(contourIndex, selfName, otherName))
916                 ## components
917                 # component incompatibilities
918                 # do not result in fatal errors
919                 selfComponents = selfData['components']
920                 otherComponents = otherData['components']
921                 if len(selfComponents) != len(otherComponents):
922                         errors.append("Error: glyph %s and glyph %s don't have the same number of components." %(selfName, otherName))
923                 for componentIndex in xrange(min(len(selfComponents), len(otherComponents))):
924                         selfBaseName, selfTransformation = selfComponents[componentIndex]
925                         otherBaseName, otherTransformation = otherComponents[componentIndex]
926                         if selfBaseName != otherBaseName:
927                                 errors.append("Error: component %d in glyph %s and glyph %s don't have the same base glyph." %(componentIndex, selfName, otherName))
928                 ## anchors
929                 # anchor incompatibilities
930                 # do not result in fatal errors
931                 selfAnchors = selfData['anchors']
932                 otherAnchors = otherData['anchors']
933                 if len(selfAnchors) != len(otherAnchors):
934                         errors.append("Error: glyph %s and glyph %s don't have the same number of anchors." %(selfName, otherName))
935                 for anchorIndex in xrange(min(len(selfAnchors), len(otherAnchors))):
936                         selfPt, selfAnchorName = selfAnchors[anchorIndex]
937                         otherPt, otherAnchorName = otherAnchors[anchorIndex]
938                         if selfAnchorName != otherAnchorName:
939                                 errors.append("Error: anchor %d in glyph %s and glyph %s don't have the same name." %(anchorIndex, selfName, otherName))
940                 return not fatalError, errors
941        
942         def _interpolateContours(self, factor, minContours, maxContours):
943                 newContours = []
944                 for contourIndex in xrange(len(minContours)):
945                         minContour = minContours[contourIndex]
946                         maxContour = maxContours[contourIndex]
947                         newContours.append([])
948                         for pointIndex in xrange(len(minContour)):
949                                 segType, pt, smooth, name = minContour[pointIndex]
950                                 minPoint = minContour[pointIndex][1]
951                                 maxPoint = maxContour[pointIndex][1]
952                                 newX, newY = _interpolatePt(minPoint, maxPoint, factor)
953                                 newContours[-1].append((segType, (newX, newY), smooth, name))
954                 return newContours
955
956         def _interpolateComponents(self, factor, minComponents, maxComponents):
957                 newComponents = []
958                 minComponents, maxComponents = self._mathComponentCompare(minComponents, maxComponents)
959                 componentNames = minComponents.keys()
960                 for componentName in componentNames:
961                         minComponentList = minComponents[componentName]
962                         maxComponentList = maxComponents[componentName]
963                         for i in xrange(len(minComponentList)):
964                                 # transformation breakdown: xScale, xyScale, yxScale, yScale, xOffset, yOffset
965                                 minXScale, minXYScale, minYXScale, minYScale, minXOffset, minYOffset = minComponentList[i]
966                                 maxXScale, maxXYScale, maxYXScale, maxYScale, maxXOffset, maxYOffset = maxComponentList[i]
967                                 newXScale, newXYScale = _interpolatePt((minXScale, minXYScale), (maxXScale, maxXYScale), factor)
968                                 newYXScale, newYScale = _interpolatePt((minYXScale, minYScale), (maxYXScale, maxYScale), factor)
969                                 newXOffset, newYOffset = _interpolatePt((minXOffset, minYOffset), (maxXOffset, maxYOffset), factor)
970                                 newComponents.append((componentName, (newXScale, newXYScale, newYXScale, newYScale, newXOffset, newYOffset)))
971                 return newComponents
972
973         def _interpolateAnchors(self, factor, minAnchors, maxAnchors):
974                 newAnchors = []
975                 minAnchors, maxAnchors = self._mathAnchorCompare(minAnchors, maxAnchors)
976                 anchorNames = minAnchors.keys()
977                 for anchorName in anchorNames:
978                         minAnchorList = minAnchors[anchorName]
979                         maxAnchorList = maxAnchors[anchorName]
980                         for i in range(len(minAnchorList)):
981                                 minAnchor = minAnchorList[i]
982                                 maxAnchor = maxAnchorList[i]
983                                 newAnchor = _interpolatePt(minAnchor, maxAnchor, factor)
984                                 newAnchors.append((newAnchor, anchorName))
985                 return newAnchors
986        
987         #
988         # comparisons
989         #
990
991         def __eq__(self, other):
992                 if isinstance(other, BaseGlyph):
993                         return self._getDigest() == other._getDigest()
994                 return False
995
996         def _getDigest(self, pointsOnly=False):
997                 """Calculate a digest of coordinates, points, things in this glyph.
998                 With pointsOnly == True the digest consists of a flat tuple of all
999                 coordinate pairs in the glyph, without the order of contours.
1000                 """
1001                 from robofab.pens.digestPen import DigestPointPen
1002                 mp = DigestPointPen()
1003                 self.drawPoints(mp)
1004                 if pointsOnly:
1005                         return "%s|%d|%s"%(mp.getDigestPointsOnly(), self.width, self.unicode)
1006                 else:
1007                         return "%s|%d|%s"%(mp.getDigest(), self.width, self.unicode)
1008        
1009         def _getStructure(self):
1010                 """Calculate a digest of points, things in this glyph, but NOT coordinates."""
1011                 from robofab.pens.digestPen import DigestPointStructurePen
1012                 mp = DigestPointStructurePen()
1013                 self.drawPoints(mp)
1014                 return mp.getDigest()
1015        
1016         def _hasChanged(self):
1017                 """mark the object and it's parent as changed"""
1018                 self.setChanged(True)
1019                 if self.getParent() is not None:
1020                         self.getParent()._hasChanged()
1021        
1022         def _get_box(self):
1023                 bounds = _box(self, fontObject=self.getParent())
1024                 return bounds
1025        
1026         box = property(_get_box, doc="the bounding box of the glyph: (xMin, yMin, xMax, yMax)")
1027                        
1028         def _get_leftMargin(self):
1029                 if self.isEmpty():
1030                         return 0
1031                 xMin, yMin, xMax, yMax = self.box
1032                 return xMin
1033                
1034         def _set_leftMargin(self, value):
1035                 if self.isEmpty():
1036                         self.width = self.width + value
1037                 else:
1038                         diff = value - self.leftMargin
1039                         self.move((diff, 0))
1040                         self.width = self.width + diff
1041        
1042         leftMargin = property(_get_leftMargin, _set_leftMargin, doc="the left margin")
1043
1044         def _get_rightMargin(self):
1045                 if self.isEmpty():
1046                         return self.width
1047                 xMin, yMin, xMax, yMax = self.box
1048                 return self.width - xMax
1049                
1050         def _set_rightMargin(self, value):
1051                 if self.isEmpty():
1052                         self.width = value
1053                 else:
1054                         xMin, yMin, xMax, yMax = self.box
1055                         self.width = xMax + value
1056
1057         rightMargin = property(_get_rightMargin, _set_rightMargin, doc="the right margin")
1058        
1059         def copy(self, aParent=None):
1060                 """Duplicate this glyph"""
1061                 n = self.__class__()
1062                 if aParent is not None:
1063                         n.setParent(aParent)
1064                 dont = ['_object', 'getParent']
1065                 for k in self.__dict__.keys():
1066                         ok = True
1067                         if k in dont:
1068                                 continue
1069                         elif k == "contours":
1070                                 dup = []
1071                                 for i in self.contours:
1072                                         dup.append(i.copy(n))
1073                         elif k == "components":
1074                                 dup = []
1075                                 for i in self.components:
1076                                         dup.append(i.copy(n))
1077                         elif k == "anchors":
1078                                 dup = []
1079                                 for i in self.anchors:
1080                                         dup.append(i.copy(n))
1081                         elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)):
1082                                 dup = self.__dict__[k].copy(n)
1083                         else:
1084                                 dup = copy.deepcopy(self.__dict__[k])
1085                         if ok:
1086                                 setattr(n, k, dup)
1087                 return n
1088        
1089         def _setParentTree(self):
1090                 """Set the parents of all contained and dependent objects (and their dependents) right."""
1091                 for item in self.contours:
1092                         item.setParent(self)
1093                         item._setParentTree()
1094                 for item in self.components:
1095                         item.setParent(self)
1096                 for items in self.anchors:
1097                         item.setParent(self)
1098
1099         def getGlyph(self, glyphName):
1100                 """Provided there is a font parent for this glyph, return a sibling glyph."""
1101                 if glyphName == self.name:
1102                         return self
1103                 if self.getParent() is not None:
1104                         return self.getParent()[glyphName]
1105                 return None
1106                
1107         def getPen(self):
1108                 """Return a Pen object for creating an outline in this glyph."""
1109                 from robofab.pens.adapterPens import SegmentToPointPen
1110                 return SegmentToPointPen(self.getPointPen())
1111
1112         def getPointPen(self):
1113                 """Return a PointPen object for creating an outline in this glyph."""
1114                 raise NotImplementedError, "getPointPen() must be implemented by subclass"
1115        
1116         def deSelect(self):
1117                 """Set all selected attrs in glyph to False: for the glyph, components, anchors, points."""
1118                 for a in self.anchors:
1119                         a.selected = False
1120                 for a in self.components:
1121                         a.selected = False
1122                 for c in self.contours:
1123                         for p in c.points:
1124                                 p.selected = False
1125                 self.selected = False
1126
1127         def isEmpty(self):
1128                 """return true if the glyph has no contours or components"""
1129                 if len(self.contours) + len(self.components) == 0:
1130                         return True
1131                 else:
1132                         return False
1133        
1134         def _saveToGlyphSet(self, glyphSet, glyphName=None, force=False):
1135                 """Save the glyph to GlyphSet, a private method that's part of the saving process."""
1136                 # save stuff in the lib first
1137                 if force or self.changed:
1138                         if glyphName is None:
1139                                 glyphName = self.name
1140                         glyphSet.writeGlyph(glyphName, self, self.drawPoints)
1141                        
1142         def update(self):
1143                 """update the glyph"""
1144                 pass
1145        
1146         def draw(self, pen):
1147                 """draw the object with a RoboFab segment pen"""
1148                 try:
1149                         pen.setWidth(self.width)
1150                         if self.note is not None:
1151                                 pen.setNote(self.note)
1152                 except AttributeError:
1153                         # FontTools pens don't have these methods
1154                         pass
1155                 for a in self.anchors:
1156                         a.draw(pen)
1157                 for c in self.contours:
1158                         c.draw(pen)
1159                 for c in self.components:
1160                         c.draw(pen)
1161                 try:
1162                         pen.doneDrawing()
1163                 except AttributeError:
1164                         # FontTools pens don't have a doneDrawing() method
1165                         pass
1166                
1167         def drawPoints(self, pen):
1168                 """draw the object with a point pen"""
1169                 for a in self.anchors:
1170                         a.drawPoints(pen)
1171                 for c in self.contours:
1172                         c.drawPoints(pen)
1173                 for c in self.components:
1174                         c.drawPoints(pen)
1175        
1176         def appendContour(self, aContour, offset=(0, 0)):
1177                 """append a contour to the glyph"""
1178                 x, y = offset
1179                 pen = self.getPointPen()
1180                 aContour.drawPoints(pen)
1181                 self.contours[-1].move((x, y))
1182        
1183         def appendGlyph(self, aGlyph, offset=(0, 0)):
1184                 """append another glyph to the glyph"""
1185                 x, y = offset
1186                 pen = self.getPointPen()
1187                 #to handle the offsets, move the source glyph and then move it back!
1188                 aGlyph.move((x, y))
1189                 aGlyph.drawPoints(pen)
1190                 aGlyph.move((-x, -y))
1191                
1192         def round(self):
1193                 """round all coordinates in all contours, components and anchors"""
1194                 for n in self.contours:
1195                         n.round()
1196                 for n in self.components:
1197                         n.round()
1198                 for n in self.anchors:
1199                         n.round()
1200                 self.width = int(round(self.width))
1201                
1202         def autoUnicodes(self):
1203                 """Using fontTools.agl, assign Unicode list to the glyph"""
1204                 from fontTools.agl import AGL2UV
1205                 if AGL2UV.has_key(self.name):
1206                         self.unicode = AGL2UV[self.name]
1207                         self._hasChanged()
1208                        
1209         def pointInside(self, (x, y), evenOdd=0):
1210                 """determine if the point is in the black or white of the glyph"""
1211                 from fontTools.pens.pointInsidePen import PointInsidePen
1212                 font = self.getParent()
1213                 piPen = PointInsidePen(glyphSet=font, testPoint=(x, y), evenOdd=evenOdd)
1214                 self.draw(piPen)
1215                 return piPen.getResult()
1216        
1217         def correctDirection(self, trueType=False):
1218                 """corect the direction of the contours in the glyph."""
1219                 #this is a bit slow, but i'm not sure how much more it can be optimized.
1220                 #it also has a bug somewhere that is causeing some contours to be set incorrectly.
1221                 #try to run it on the copyright symbol to see the problem. hm.
1222                 #
1223                 #establish the default direction that an outer contour should follow
1224                 #i believe for TT this is clockwise and for PS it is counter
1225                 #i could be wrong about this, i need to double check.
1226                 from fontTools.pens.pointInsidePen import PointInsidePen
1227                 baseDirection = 0
1228                 if trueType:
1229                         baseDirection = 1
1230                 #we don't need to do all the work if the contour count is < 2
1231                 count = len(self.contours)
1232                 if count == 0:
1233                         return
1234                 elif count == 1:
1235                         self.contours[0].clockwise = baseDirection
1236                         return
1237                 #store up needed before we start
1238                 #i think the .box calls are eating a big chunk of the time
1239                 contourDict = {}
1240                 for contourIndex in range(len(self.contours)):
1241                         contour = self.contours[contourIndex]
1242                         contourDict[contourIndex] = {'box':contour.box, 'dir':contour.clockwise, 'hit':[], 'notHit':[]}
1243                 #now, for every contour, determine which contours it intersects
1244                 #as we go, we will also store contours that it doesn't intersct
1245                 #and we store this value for both contours
1246                 allIndexes = contourDict.keys()
1247                 for contourIndex in allIndexes:
1248                         for otherContourIndex in allIndexes:
1249                                 if otherContourIndex != contourIndex:
1250                                         if contourIndex not in contourDict[otherContourIndex]['hit'] and contourIndex not in contourDict[otherContourIndex]['notHit']:
1251                                                 xMin1, yMin1, xMax1, yMax1 = contourDict[contourIndex]['box']
1252                                                 xMin2, yMin2, xMax2, yMax2= contourDict[otherContourIndex]['box']       
1253                                                 hit, pos = sectRect((xMin1, yMin1, xMax1, yMax1), (xMin2, yMin2, xMax2, yMax2))
1254                                                 if hit == 1:
1255                                                         contourDict[contourIndex]['hit'].append(otherContourIndex)
1256                                                         contourDict[otherContourIndex]['hit'].append(contourIndex)
1257                                                 else:
1258                                                         contourDict[contourIndex]['notHit'].append(otherContourIndex)
1259                                                         contourDict[otherContourIndex]['notHit'].append(contourIndex)
1260                 #set up the pen here to shave a bit of time
1261                 font = self.getParent()
1262                 piPen = PointInsidePen(glyphSet=font, testPoint=(0, 0), evenOdd=0)
1263                 #now do the pointInside work
1264                 for contourIndex in allIndexes:
1265                         direction = baseDirection
1266                         contour = self.contours[contourIndex]
1267                         startPoint = contour.segments[0].onCurve
1268                         if startPoint is not None:      #skip TT paths with no onCurve
1269                                 if len(contourDict[contourIndex]['hit']) != 0:
1270                                         for otherContourIndex in contourDict[contourIndex]['hit']:
1271                                                 piPen.setTestPoint(testPoint=(startPoint.x, startPoint.y))
1272                                                 otherContour = self.contours[otherContourIndex]
1273                                                 otherContour.draw(piPen)
1274                                                 direction = direction + piPen.getResult()
1275                         newDirection = direction % 2
1276                         #now set the direction if we need to
1277                         if newDirection != contourDict[contourIndex]['dir']:
1278                                 contour.reverseContour()
1279        
1280         def autoContourOrder(self):
1281                 """attempt to sort the contours based on their centers"""
1282                 # sort is based on (in this order):
1283                 # - the (negative) point count
1284                 # - the (negative) segment count
1285                 # - fuzzy x value of the center of the contour
1286                 # - fuzzy y value of the center of the contour
1287                 # - the (negative) surface of the bounding box of the contour: width * height
1288                 # the latter is a safety net for for instances like a very thin 'O' where the
1289                 # x centers could be close enough to rely on the y for the sort which could
1290                 # very well be the same for both contours. We use the _negative_ of the surface
1291                 # to ensure that larger contours appear first, which seems more natural.
1292                 tempContourList = []
1293                 contourList = []
1294                 xThreshold = None
1295                 yThreshold = None
1296                 for contour in self.contours:
1297                         xMin, yMin, xMax, yMax = contour.box
1298                         width = xMax - xMin
1299                         height = yMax - yMin
1300                         xC = 0.5 * (xMin + xMax)
1301                         yC = 0.5 * (yMin + yMax)
1302                         xTh = abs(width * .5)
1303                         yTh = abs(height * .5)
1304                         if xThreshold is None or xThreshold > xTh:
1305                                 xThreshold = xTh
1306                         if yThreshold is None or yThreshold > yTh:
1307                                 yThreshold = yTh
1308                         tempContourList.append((-len(contour.points), -len(contour.segments), xC, yC, -(width * height), contour))
1309                 for points, segments, x, y, surface, contour in tempContourList:
1310                         contourList.append((points, segments, FuzzyNumber(x, xThreshold), FuzzyNumber(y, yThreshold), surface, contour))
1311                 contourList.sort()
1312                 for i in range(len(contourList)):
1313                         points, segments, xO, yO, surface, contour = contourList[i]
1314                         contour.index = i
1315        
1316         def rasterize(self, cellSize=50, xMin=None, yMin=None, xMax=None, yMax=None):
1317                 """
1318                 Slice the glyph into a grid based on the cell size.
1319                 It returns a list of lists containing bool values
1320                 that indicate the black (True) or white (False)
1321                 value of that particular cell.  These lists are
1322                 arranged from top to bottom of the glyph and
1323                 proceed from left to right.
1324                 This is an expensive operation!
1325                 """
1326                 from fontTools.pens.pointInsidePen import PointInsidePen
1327                 piPen = PointInsidePen(glyphSet=self.getParent(), testPoint=(0, 0), evenOdd=0)
1328                 if xMin is None or yMin is None or xMax is None or yMax is None:
1329                         _xMin, _yMin, _xMax, _yMax = self.box
1330                         if xMin is None:
1331                                 xMin = _xMin
1332                         if yMin is None:
1333                                 yMin = _yMin
1334                         if xMax is None:
1335                                 xMax = _xMax
1336                         if yMax is None:
1337                                 yMax = _yMax
1338                 #
1339                 hitXMax = False
1340                 hitYMin = False
1341                 xSlice = 0
1342                 ySlice = 0
1343                 halfCellSize = cellSize / 2.0
1344                 #
1345                 map = []
1346                 #
1347                 while not hitYMin:
1348                         map.append([])
1349                         yScan = -(ySlice * cellSize) + yMax - halfCellSize
1350                         if yScan < yMin:
1351                                 hitYMin = True
1352                         while not hitXMax:
1353                                 xScan = (xSlice * cellSize) + xMin - halfCellSize
1354                                 if xScan > xMax:
1355                                         hitXMax = True
1356                                 piPen.setTestPoint((xScan, yScan))
1357                                 self.draw(piPen)
1358                                 test = piPen.getResult()
1359                                 if test:
1360                                         map[-1].append(True)
1361                                 else:
1362                                         map[-1].append(False)
1363                                 xSlice = xSlice + 1
1364                         hitXMax = False
1365                         xSlice = 0
1366                         ySlice = ySlice + 1
1367                 return map
1368
1369         def move(self, (x, y), contours=True, components=True, anchors=True):
1370                 """Move a glyph's items that are flagged as True"""
1371                 x, y = roundPt((x, y))
1372                 for contour in self.contours:
1373                         contour.move((x, y))
1374                 for component in self.components:
1375                         component.move((x, y))
1376                 for anchor in self.anchors:
1377                         anchor.move((x, y))
1378                        
1379         def scale(self, (x, y), center=(0, 0)):
1380                 """scale the glyph"""
1381                 for contour in self.contours:
1382                         contour.scale((x, y), center=center)
1383                 for component in self.components:
1384                         offset = component.offset
1385                         component.offset = _scalePointFromCenter(offset, (x, y), center)
1386                         sX, sY = component.scale
1387                         component.scale = (sX*x, sY*y)
1388                 for anchor in self.anchors:
1389                         anchor.scale((x, y), center=center)
1390
1391         def transform(self, matrix):
1392                 """Transform this glyph.
1393                 Use a Transform matrix object from
1394                 robofab.transform"""
1395                 n = []
1396                 for c in self.contours:
1397                         c.transform(matrix)
1398                 for a in self.anchors:
1399                         a.transform(matrix)
1400                        
1401         def rotate(self, angle, offset=None):
1402                 """rotate the glyph"""
1403                 from fontTools.misc.transform import Identity
1404                 radAngle = angle / DEGREE       # convert from degrees to radians
1405                 if offset is None:
1406                         offset = (0,0)
1407                 rT = Identity.translate(offset[0], offset[1])
1408                 rT = rT.rotate(radAngle)
1409                 rT = rT.translate(-offset[0], -offset[1])
1410                 self.transform(rT)     
1411        
1412         def skew(self, angle, offset=None):
1413                 """skew the glyph"""
1414                 from fontTools.misc.transform import Identity
1415                 radAngle = angle / DEGREE       # convert from degrees to radians
1416                 if offset is None:
1417                         offset = (0,0)
1418                 rT = Identity.translate(offset[0], offset[1])
1419                 rT = rT.skew(radAngle)
1420                 self.transform(rT)
1421        
1422        
1423 class BaseContour(RBaseObject):
1424        
1425         """Base class for all contour objects."""
1426        
1427         def __init__(self):
1428                 RBaseObject.__init__(self)
1429                 #self.index = None
1430                 self.changed = False            # if the object needs to be saved
1431                
1432         def __repr__(self):
1433                 font = "unnamed_font"
1434                 glyph = "unnamed_glyph"
1435                 glyphParent = self.getParent()
1436                 if glyphParent is not None:
1437                         try:
1438                                 glyph = glyphParent.name
1439                         except AttributeError: pass
1440                         fontParent = glyphParent.getParent()
1441                         if fontParent is not None:
1442                                 try:
1443                                         font = fontParent.info.fullName
1444                                 except AttributeError: pass
1445                 try:
1446                         idx = `self.index`
1447                 except ValueError:
1448                         # XXXX
1449                         idx = "XXX"
1450                 return "<RContour for %s.%s[%s]>"%(font, glyph, idx)
1451                
1452         def __len__(self):
1453                 return len(self.segments)
1454        
1455         def __mul__(self, factor):
1456                 from warnings import warn
1457                 warn("Contour math has been deprecated and is slated for removal.", DeprecationWarning)
1458                 n = self.copy()
1459                 n.segments = []
1460                 for i in range(len(self.segments)):
1461                         n.segments.append(self.segments[i] * factor)
1462                 n._setParentTree()
1463                 return n
1464        
1465         __rmul__ = __mul__
1466
1467         def __add__(self, other):
1468                 from warnings import warn
1469                 warn("Contour math has been deprecated and is slated for removal.", DeprecationWarning)
1470                 n = self.copy()
1471                 n.segments = []
1472                 for i in range(len(self.segments)):
1473                         n.segments.append(self.segments[i] + other.segments[i])
1474                 n._setParentTree()
1475                 return n
1476
1477         def __sub__(self, other):
1478                 from warnings import warn
1479                 warn("Contour math has been deprecated and is slated for removal.", DeprecationWarning)
1480                 n = self.copy()
1481                 n.segments = []
1482                 for i in range(len(self.segments)):
1483                         n.segments.append(self.segments[i] - other.segments[i])
1484                 n._setParentTree()
1485                 return n
1486                
1487         def __getitem__(self, index):
1488                 return self.segments[index]
1489                
1490         def _hasChanged(self):
1491                 """mark the object and it's parent as changed"""
1492                 self.setChanged(True)
1493                 if self.getParent() is not None:
1494                         self.getParent()._hasChanged()
1495        
1496         def _nextSegment(self, segmentIndex):
1497                 return self.segments[(segmentIndex + 1) % len(self.segments)]
1498        
1499         def _prevSegment(self, segmentIndex):
1500                 segments = self.segments
1501                 return self.segments[(segmentIndex - 1) % len(self.segments)]
1502                
1503         def _get_box(self):
1504                 bounds = _box(self)
1505                 return bounds
1506        
1507         box = property(_get_box, doc="the bounding box for the contour")
1508
1509         def _set_clockwise(self, value):
1510                 if self.clockwise != value:
1511                         self.reverseContour()
1512                        
1513         def _get_clockwise(self):
1514                 anchors = []
1515                 lastOn = None
1516                 for segment in self.segments:
1517                         anchor = segment.onCurve
1518                         anchor = (anchor.x, anchor.y)
1519                         # overlapping points can give false results, so filter them out
1520                         if anchor == lastOn:
1521                                 continue
1522                         anchors.append(anchor)
1523                         lastOn = anchor
1524                 #overlapping moves can give false results, so filter them out
1525                 first = anchors[0]
1526                 last = anchors[-1]
1527                 if first == last:
1528                         del anchors[-1]
1529                 nPoints = len(anchors)
1530                 angles = []
1531                 for i1 in range(nPoints):
1532                         i2 = (i1 + 1) % nPoints
1533                         x1, y1 = anchors[i1]
1534                         x2, y2 = anchors[i2]
1535                         angles.append(math.atan2(y2-y1, x2-x1))
1536                 total = 0
1537                 pi = math.pi
1538                 pi2 = pi * 2
1539                 for i1 in range(nPoints):
1540                         i2 = (i1 + 1) % nPoints
1541                         d = ((angles[i2] - angles[i1] + pi) % pi2) - pi
1542                         total += d
1543                 return total < 0
1544
1545         clockwise = property(_get_clockwise, _set_clockwise, doc="direction of contour: 1=clockwise 0=counterclockwise")
1546
1547         def copy(self, aParent=None):
1548                 """Duplicate this contour"""
1549                 n = self.__class__()
1550                 if aParent is not None:
1551                         n.setParent(aParent)
1552                 elif self.getParent() is not None:
1553                         n.setParent(self.getParent())
1554                 dont = ['_object', 'points', 'bPoints', 'getParent']
1555                 for k in self.__dict__.keys():
1556                         ok = True
1557                         if k in dont:
1558                                 continue
1559                         elif k == "segments":
1560                                 dup = []
1561                                 for i in self.segments:
1562                                         dup.append(i.copy(n))
1563                         elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)):
1564                                 dup = self.__dict__[k].copy(n)
1565                         else:
1566                                 dup = copy.deepcopy(self.__dict__[k])
1567                         if ok:
1568                                 setattr(n, k, dup)
1569                 return n
1570                
1571         def _setParentTree(self):
1572                 """Set the parents of all contained and dependent objects (and their dependents) right."""
1573                 for item in self.segments:
1574                         item.setParent(self)
1575
1576         def round(self):
1577                 """round the value of all points in the contour"""
1578                 for n in self.points:
1579                         n.round()
1580        
1581         def draw(self, pen):
1582                 """draw the object with a fontTools pen"""
1583                 firstOn = self.segments[0].onCurve
1584                 firstType = self.segments[0].type
1585                 lastOn = self.segments[-1].onCurve
1586                 # this is a special exception for FontLab
1587                 # FL can have a contour that does not contain a move.
1588                 # this will only happen if the contour begins with a qcurve.
1589                 # in this case, we move to the segment's on curve,
1590                 # then we iterate through the rest of the points,
1591                 # then we add the first qcurve and finally we
1592                 # close the path. after this, i say "ugh."
1593                 if firstType == QCURVE:
1594                         pen.moveTo((firstOn.x, firstOn.y))
1595                         for segment in self.segments[1:]:
1596                                 segmentType = segment.type
1597                                 pt = segment.onCurve.x, segment.onCurve.y
1598                                 if segmentType == LINE:
1599                                         pen.lineTo(pt)
1600                                 elif segmentType == CURVE:
1601                                         pts = [(point.x, point.y) for point in segment.points]
1602                                         pen.curveTo(*pts)
1603                                 elif segmentType == QCURVE:
1604                                         pts = [(point.x, point.y) for point in segment.points]
1605                                         pen.qCurveTo(*pts)
1606                                 else:
1607                                         assert 0, "unsupported segment type"
1608                         pts = [(point.x, point.y) for point in self.segments[0].points]
1609                         pen.qCurveTo(*pts)
1610                         pen.closePath()
1611                 else:
1612                         if firstType == MOVE and (firstOn.x, firstOn.y) == (lastOn.x, lastOn.y):
1613                                 closed = True
1614                         else:
1615                                 closed = True
1616                         for segment in self.segments:
1617                                 segmentType = segment.type
1618                                 pt = segment.onCurve.x, segment.onCurve.y
1619                                 if segmentType == MOVE:
1620                                         pen.moveTo(pt)
1621                                 elif segmentType == LINE:
1622                                         pen.lineTo(pt)
1623                                 elif segmentType == CURVE:
1624                                         pts = [(point.x, point.y) for point in segment.points]
1625                                         pen.curveTo(*pts)
1626                                 elif segmentType == QCURVE:
1627                                         pts = [(point.x, point.y) for point in segment.points]
1628                                         pen.qCurveTo(*pts)
1629                                 else:
1630                                         assert 0, "unsupported segment type"
1631                         if closed:
1632                                 pen.closePath()
1633                         else:
1634                                 pen.endPath()
1635        
1636         def drawPoints(self, pen):
1637                 """draw the object with a point pen"""
1638                 pen.beginPath()
1639                 lastOn = self.segments[-1].onCurve
1640                 didLastOn = False
1641                 flQCurveException = False
1642                 lastIndex = len(self.segments) - 1
1643                 for i in range(len(self.segments)):
1644                         segment = self.segments[i]
1645                         segmentType = segment.type
1646                         # the new protocol states that we start with an onCurve
1647                         # so, if we have a move and a nd a last point overlapping,
1648                         # add the last point to the beginning and skip the move
1649                         if segmentType == MOVE and (segment.onCurve.x, segment.onCurve.y) == (lastOn.x, lastOn.y):
1650                                 point = self.segments[-1].onCurve
1651                                 name = getattr(point, 'name', None)
1652                                 pen.addPoint((point.x, point.y), point.type, smooth=self.segments[-1].smooth, name=name)
1653                                 didLastOn = True
1654                                 continue
1655                         # this is an exception for objectsFL
1656                         # the problem is that quad contours are
1657                         # represented differently that they are in
1658                         # objectsRF:
1659                         #       FL: [qcurve, qcurve, qcurve, qcurve]
1660                         #       RF: [move, qcurve, qcurve, qcurve, qcurve]
1661                         # so, we need to catch this, and shift the offCurves to
1662                         # to the end of the contour
1663                         if i == 0 and segmentType == QCURVE:
1664                                 flQCurveException = True
1665                         if segmentType == MOVE:
1666                                 segmentType = LINE
1667                         ## the offCurves
1668                         if i == 0 and flQCurveException:
1669                                 pass
1670                         else:
1671                                 for point in segment.offCurve:
1672                                         name = getattr(point, 'name', None)
1673                                         pen.addPoint((point.x, point.y), segmentType=None, smooth=None, name=name, selected=point.selected)
1674                         ## the onCurve
1675                         # skip the last onCurve if it was used as the move
1676                         if i == lastIndex and didLastOn:
1677                                 continue
1678                         point = segment.onCurve
1679                         name = getattr(point, 'name', None)
1680                         pen.addPoint((point.x, point.y), segmentType, smooth=segment.smooth, name=name, selected=point.selected)
1681                 # if we have the special qCurve case with objectsFL
1682                 # take care of the offCurves associated with the first contour
1683                 if flQCurveException:
1684                         for point in self.segments[0].offCurve:
1685                                 name = getattr(point, 'name', None)
1686                                 pen.addPoint((point.x, point.y), segmentType=None, smooth=None, name=name, selected=point.selected)
1687                 pen.endPath()
1688                
1689         def move(self, (x, y)):
1690                 """move the contour"""
1691                 #this will be faster if we go straight to the points
1692                 for point in self.points:
1693                         point.move((x, y))
1694        
1695         def scale(self,(x, y), center=(0, 0)):
1696                 """scale the contour"""
1697                 #this will be faster if we go straight to the points
1698                 for point in self.points:
1699                         point.scale((x, y), center=center)
1700        
1701         def transform(self, matrix):
1702                 """Transform this contour.
1703                 Use a Transform matrix object from
1704                 robofab.transform"""
1705                 n = []
1706                 for s in self.segments:
1707                         s.transform(matrix)
1708                        
1709         def rotate(self, angle, offset=None):
1710                 """rotate the contour"""
1711                 from fontTools.misc.transform import Identity
1712                 radAngle = angle / DEGREE       # convert from degrees to radians
1713                 if offset is None:
1714                         offset = (0,0)
1715                 rT = Identity.translate(offset[0], offset[1])
1716                 rT = rT.rotate(radAngle)
1717                 self.transform(rT)     
1718        
1719         def skew(self, angle, offset=None):
1720                 """skew the contour"""
1721                 from fontTools.misc.transform import Identity
1722                 radAngle = angle / DEGREE       # convert from degrees to radians
1723                 if offset is None:
1724                         offset = (0,0)
1725                 rT = Identity.translate(offset[0], offset[1])
1726                 rT = rT.skew(radAngle)
1727                 self.transform(rT)
1728        
1729         def pointInside(self, (x, y), evenOdd=0):
1730                 """determine if the point is inside or ouside of the contour"""
1731                 from fontTools.pens.pointInsidePen import PointInsidePen
1732                 glyph = self.getParent()
1733                 font = glyph.getParent()
1734                 piPen = PointInsidePen(glyphSet=font, testPoint=(x, y), evenOdd=evenOdd)
1735                 self.draw(piPen)
1736                 return piPen.getResult()
1737                        
1738         def autoStartSegment(self):
1739                 """automatically set the lower left point of the contour as the first point."""
1740                 #adapted from robofog
1741                 startIndex = 0
1742                 startSegment = self.segments[0]
1743                 for i in range(len(self.segments)):
1744                         segment = self.segments[i]
1745                         startOn = startSegment.onCurve
1746                         on = segment.onCurve
1747                         if on.y <= startOn.y:
1748                                 if on.y == startOn.y:
1749                                         if on.x < startOn.x:
1750                                                 startSegment = segment
1751                                                 startIndex = i
1752                                 else:
1753                                         startSegment = segment
1754                                         startIndex = i
1755                 if startIndex != 0:
1756                         self.setStartSegment(startIndex)
1757                
1758         def appendBPoint(self, pointType, anchor, bcpIn=(0, 0), bcpOut=(0, 0)):
1759                 """append a bPoint to the contour"""
1760                 self.insertBPoint(len(self.segments), pointType=pointType, anchor=anchor, bcpIn=bcpIn, bcpOut=bcpOut)
1761                
1762         def insertBPoint(self, index, pointType, anchor, bcpIn=(0, 0), bcpOut=(0, 0)):
1763                 """insert a bPoint at index on the contour"""
1764                 #insert a CURVE point that we can work with
1765                 nextSegment = self._nextSegment(index-1)
1766                 if nextSegment.type == QCURVE:
1767                         return 
1768                 if nextSegment.type == MOVE:
1769                         prevSegment = self.segments[index-1]
1770                         prevOn = prevSegment.onCurve
1771                         if bcpIn != (0, 0):
1772                                 new = self.appendSegment(CURVE, [(prevOn.x, prevOn.y), absoluteBCPIn(anchor, bcpIn), anchor], smooth=False)
1773                                 if pointType == CURVE:
1774                                         new.smooth = True
1775                         else:
1776                                 new = self.appendSegment(LINE, [anchor], smooth=False)
1777                         #if the user wants an outgoing bcp, we must add a CURVE ontop of the move
1778                         if bcpOut != (0, 0):
1779                                 nextOn = nextSegment.onCurve
1780                                 self.appendSegment(CURVE, [absoluteBCPOut(anchor, bcpOut), (nextOn.x, nextOn.y), (nextOn.x, nextOn.y)], smooth=False)
1781                 else:
1782                         #handle the bcps
1783                         if nextSegment.type != CURVE:
1784                                 prevSegment = self.segments[index-1]
1785                                 prevOn = prevSegment.onCurve
1786                                 prevOutX, prevOutY = (prevOn.x, prevOn.y)
1787                         else:
1788                                 prevOut = nextSegment.offCurve[0]
1789                                 prevOutX, prevOutY = (prevOut.x, prevOut.y)
1790                         self.insertSegment(index, segmentType=CURVE, points=[(prevOutX, prevOutY), anchor, anchor], smooth=False)
1791                         newSegment = self.segments[index]
1792                         prevSegment = self._prevSegment(index)
1793                         nextSegment = self._nextSegment(index)
1794                         if nextSegment.type == MOVE:
1795                                 raise RoboFabError, 'still working out curving at the end of a contour'
1796                         elif nextSegment.type == QCURVE:
1797                                 return
1798                         #set the new incoming bcp
1799                         newIn = newSegment.offCurve[1]
1800                         nIX, nIY = absoluteBCPIn(anchor, bcpIn)
1801                         newIn.x = nIX
1802                         newIn.y = nIY
1803                         #set the new outgoing bcp
1804                         hasCurve = True
1805                         if nextSegment.type != CURVE:
1806                                 if bcpOut != (0, 0):
1807                                         nextSegment.type = CURVE
1808                                         hasCurve = True
1809                                 else:
1810                                         hasCurve = False
1811                         if hasCurve:
1812                                 newOut = nextSegment.offCurve[0]
1813                                 nOX, nOY = absoluteBCPOut(anchor, bcpOut)
1814                                 newOut.x = nOX
1815                                 newOut.y = nOY
1816                         #now check to see if we can convert the CURVE segment to a LINE segment
1817                         newAnchor = newSegment.onCurve
1818                         newA = newSegment.offCurve[0]
1819                         newB = newSegment.offCurve[1]
1820                         nextAnchor = nextSegment.onCurve
1821                         prevAnchor = prevSegment.onCurve
1822                         if (prevAnchor.x, prevAnchor.y) == (newA.x, newA.y) and (newAnchor.x, newAnchor.y) == (newB.x, newB.y):
1823                                 newSegment.type = LINE
1824                         #the user wants a smooth segment               
1825                         if pointType == CURVE:
1826                                 newSegment.smooth = True
1827
1828
1829 class BaseSegment(RBaseObject):
1830        
1831         """Base class for all segment objects"""
1832        
1833         def __init__(self):
1834                 self.changed = False
1835        
1836         def __repr__(self):
1837                 font = "unnamed_font"
1838                 glyph = "unnamed_glyph"
1839                 contourIndex = "unknown_contour"
1840                 contourParent = self.getParent()
1841                 if contourParent is not None:
1842                         try:
1843                                 contourIndex = `contourParent.index`
1844                         except AttributeError: pass
1845                         glyphParent = contourParent.getParent()
1846                         if glyphParent is not None:
1847                                 try:
1848                                         glyph = glyphParent.name
1849                                 except AttributeError: pass
1850                                 fontParent = glyphParent.getParent()
1851                                 if fontParent is not None:
1852                                         try:
1853                                                 font = fontParent.info.fullName
1854                                         except AttributeError: pass
1855                 try:
1856                         idx = `self.index`
1857                 except ValueError:
1858                         idx = "XXX"
1859                 return "<RSegment for %s.%s[%s][%s]>"%(font, glyph, contourIndex, idx)
1860                
1861         def __mul__(self, factor):
1862                 from warnings import warn
1863                 warn("Segment math has been deprecated and is slated for removal.", DeprecationWarning)
1864                 n = self.copy()
1865                 n.points = []
1866                 for i in range(len(self.points)):
1867                         n.points.append(self.points[i] * factor)
1868                 n._setParentTree()
1869                 return n
1870
1871         __rmul__ = __mul__
1872
1873         def __add__(self, other):
1874                 from warnings import warn
1875                 warn("Segment math has been deprecated and is slated for removal.", DeprecationWarning)
1876                 n = self.copy()
1877                 n.points = []
1878                 for i in range(len(self.points)):
1879                         n.points.append(self.points[i] + other.points[i])
1880                 return n
1881
1882         def __sub__(self, other):
1883                 from warnings import warn
1884                 warn("Segment math has been deprecated and is slated for removal.", DeprecationWarning)
1885                 n = self.copy()
1886                 n.points = []
1887                 for i in range(len(self.points)):
1888                         n.points.append(self.points[i] - other.points[i])
1889                 return n
1890                
1891         def _hasChanged(self):
1892                 """mark the object and it's parent as changed"""
1893                 self.setChanged(True)
1894                 if self.getParent() is not None:
1895                         self.getParent()._hasChanged()
1896        
1897         def copy(self, aParent=None):
1898                 """Duplicate this segment"""
1899                 n = self.__class__()
1900                 if aParent is not None:
1901                         n.setParent(aParent)
1902                 elif self.getParent() is not None:
1903                         n.setParent(self.getParent())
1904                 dont = ['_object', 'getParent', 'offCurve', 'onCurve']
1905                 for k in self.__dict__.keys():
1906                         ok = True
1907                         if k in dont:
1908                                 continue
1909                         if k == "points":
1910                                 dup = []
1911                                 for i in self.points:
1912                                         dup.append(i.copy(n))
1913                         elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)):
1914                                 dup = self.__dict__[k].copy(n)
1915                         else:
1916                                 dup = copy.deepcopy(self.__dict__[k])
1917                         if ok:
1918                                 setattr(n, k, dup)
1919                 return n
1920        
1921         def _setParentTree(self):
1922                 """Set the parents of all contained and dependent objects (and their dependents) right."""
1923                 for item in self.points:
1924                         item.setParent(self)
1925
1926         def round(self):
1927                 """round all points in the segment"""
1928                 for point in self.points:
1929                         point.round()
1930        
1931         def move(self, (x, y)):
1932                 """move the segment"""
1933                 for point in self.points:
1934                         point.move((x, y))
1935        
1936         def scale(self, (x, y), center=(0, 0)):
1937                 """scale the segment"""
1938                 for point in self.points:
1939                         point.scale((x, y), center=center)
1940
1941         def transform(self, matrix):
1942                 """Transform this segment.
1943                 Use a Transform matrix object from
1944                 robofab.transform"""
1945                 n = []
1946                 for p in self.points:
1947                         p.transform(matrix)
1948        
1949         def _get_onCurve(self):
1950                 return self.points[-1]
1951        
1952         def _get_offCurve(self):
1953                 return self.points[:-1]
1954                
1955         offCurve = property(_get_offCurve, doc="on curve point for the segment")
1956         onCurve = property(_get_onCurve, doc="list of off curve points for the segment")
1957
1958
1959                
1960 class BasePoint(RBaseObject):
1961        
1962         """Base class for point objects."""
1963        
1964         def __init__(self):
1965                 #RBaseObject.__init__(self)
1966                 self.changed = False            # if the object needs to be saved
1967                 self.selected = False
1968                
1969         def __repr__(self):
1970                 font = "unnamed_font"
1971                 glyph = "unnamed_glyph"
1972                 contourIndex = "unknown_contour"
1973                 segmentIndex = "unknown_segment"
1974                 segmentParent = self.getParent()
1975                 if segmentParent is not None:
1976                         try:
1977                                 segmentIndex = `segmentParent.index`
1978                         except AttributeError: pass
1979                         contourParent = self.getParent().getParent()
1980                         if contourParent is not None:
1981                                 try:
1982                                         contourIndex = `contourParent.index`
1983                                 except AttributeError: pass
1984                                 glyphParent = contourParent.getParent()
1985                                 if glyphParent is not None:
1986                                         try:
1987                                                 glyph = glyphParent.name
1988                                         except AttributeError: pass
1989                                         fontParent = glyphParent.getParent()
1990                                         if fontParent is not None:
1991                                                 try:
1992                                                         font = fontParent.info.fullName
1993                                                 except AttributeError: pass
1994                 return "<RPoint for %s.%s[%s][%s]>"%(font, glyph, contourIndex, segmentIndex)
1995        
1996         def __add__(self, other):
1997                 from warnings import warn
1998                 warn("Point math has been deprecated and is slated for removal.", DeprecationWarning)
1999                 #Add one point to another
2000                 n = self.copy()
2001                 n.x, n.y = addPt((self.x, self.y), (other.x, other.y))
2002                 return n
2003
2004         def __sub__(self, other):
2005                 from warnings import warn
2006                 warn("Point math has been deprecated and is slated for removal.", DeprecationWarning)
2007                 #Subtract one point from another
2008                 n = self.copy()
2009                 n.x, n.y = subPt((self.x, self.y), (other.x, other.y))
2010                 return n
2011
2012         def __mul__(self, factor):
2013                 from warnings import warn
2014                 warn("Point math has been deprecated and is slated for removal.", DeprecationWarning)
2015                 #Multiply the point with factor. Factor can be a tuple of 2 *(f1, f2)
2016                 n = self.copy()
2017                 n.x, n.y = mulPt((self.x, self.y), factor)
2018                 return n
2019                
2020         __rmul__ = __mul__
2021
2022         def _hasChanged(self):
2023                 #mark the object and it's parent as changed
2024                 self.setChanged(True)
2025                 if self.getParent() is not None:
2026                         self.getParent()._hasChanged()
2027        
2028         def copy(self, aParent=None):
2029                 """Duplicate this point"""
2030                 n = self.__class__()
2031                 if aParent is not None:
2032                         n.setParent(aParent)
2033                 elif self.getParent() is not None:
2034                         n.setParent(self.getParent())
2035                 dont = ['getParent', 'offCurve', 'onCurve']
2036                 for k in self.__dict__.keys():
2037                         ok = True
2038                         if k in dont:
2039                                 continue
2040                         elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)):
2041                                 dup = self.__dict__[k].copy(n)
2042                         else:
2043                                 dup = copy.deepcopy(self.__dict__[k])
2044                         if ok:
2045                                 setattr(n, k, dup)
2046                 return n
2047
2048         def select(self, state=True):
2049                 """Set the selection of this point.
2050                 XXXX This method should be a lot more versatile, dealing with
2051                 different kinds of selection, select the bcp's seperately etc.
2052                 But that's for later when we need it more. For now it's just
2053                 one flag for the entire thing."""
2054                 self.selected = state
2055        
2056         def round(self):
2057                 """round the values in the point"""
2058                 self.x, self.y = roundPt((self.x, self.y))
2059
2060         def move(self, (x, y)):
2061                 """Move the point"""
2062                 self.x, self.y = addPt((self.x, self.y), (x, y))
2063                
2064         def scale(self, (x, y), center=(0, 0)):
2065                 """scale the point"""
2066                 nX, nY = _scalePointFromCenter((self.x, self.y), (x, y), center)
2067                 self.x = nX
2068                 self.y = nY
2069
2070         def transform(self, matrix):
2071                 """Transform this point. Use a Transform matrix
2072                 object from fontTools.misc.transform"""
2073                 self.x, self.y = matrix.transformPoint((self.x, self.y))
2074
2075
2076 class BaseBPoint(RBaseObject):
2077
2078         """Base class for bPoints objects."""
2079
2080         def __init__(self):
2081                 RBaseObject.__init__(self)
2082                 self.changed = False            # if the object needs to be saved
2083                 self.selected = False
2084                
2085         def __repr__(self):
2086                 font = "unnamed_font"
2087                 glyph = "unnamed_glyph"
2088                 contourIndex = "unknown_contour"
2089                 segmentIndex = "unknown_segment"
2090                 segmentParent = self.getParent()
2091                 if segmentParent is not None:
2092                         try:
2093                                 segmentIndex = `segmentParent.index`
2094                         except AttributeError: pass
2095                         contourParent = segmentParent.getParent()
2096                         if contourParent is not None:
2097                                 try:
2098                                         contourIndex = `contourParent.index`
2099                                 except AttributeError: pass
2100                                 glyphParent = contourParent.getParent()
2101                                 if glyphParent is not None:
2102                                         try:
2103                                                 glyph = glyphParent.name
2104                                         except AttributeError: pass
2105                                         fontParent = glyphParent.getParent()
2106                                         if fontParent is not None:
2107                                                 try:
2108                                                         font = fontParent.info.fullName
2109                                                 except AttributeError: pass
2110                 return "<RBPoint for %s.%s[%s][%s][%s]>"%(font, glyph, contourIndex, segmentIndex, `self.index`)
2111
2112        
2113         def __add__(self, other):
2114                 from warnings import warn
2115                 warn("BPoint math has been deprecated and is slated for removal.", DeprecationWarning)
2116                 #Add one bPoint to another
2117                 n = self.copy()
2118                 n.anchor = addPt(self.anchor, other.anchor)
2119                 n.bcpIn = addPt(self.bcpIn, other.bcpIn)
2120                 n.bcpOut = addPt(self.bcpOut, other.bcpOut)
2121                 return n
2122
2123         def __sub__(self, other):
2124                 from warnings import warn
2125                 warn("BPoint math has been deprecated and is slated for removal.", DeprecationWarning)
2126                 #Subtract one bPoint from another
2127                 n = self.copy()
2128                 n.anchor = subPt(self.anchor, other.anchor)
2129                 n.bcpIn = subPt(self.bcpIn, other.bcpIn)
2130                 n.bcpOut = subPt(self.bcpOut, other.bcpOut)
2131                 return n
2132
2133         def __mul__(self, factor):
2134                 from warnings import warn
2135                 warn("BPoint math has been deprecated and is slated for removal.", DeprecationWarning)
2136                 #Multiply the bPoint with factor. Factor can be a tuple of 2 *(f1, f2)
2137                 n = self.copy()
2138                 n.anchor = mulPt(self.anchor, factor)
2139                 n.bcpIn = mulPt(self.bcpIn, factor)
2140                 n.bcpOut = mulPt(self.bcpOut, factor)
2141                 return n
2142                
2143         __rmul__ = __mul__
2144
2145         def _hasChanged(self):
2146                 #mark the object and it's parent as changed
2147                 self.setChanged(True)
2148                 if self.getParent() is not None:
2149                         self.getParent()._hasChanged()
2150        
2151         def select(self, state=True):
2152                 """Set the selection of this point.
2153                 XXXX This method should be a lot more versatile, dealing with
2154                 different kinds of selection, select the bcp's seperately etc.
2155                 But that's for later when we need it more. For now it's just
2156                 one flag for the entire thing."""
2157                 self.selected = state
2158        
2159         def round(self):
2160                 """Round the coordinates to integers"""
2161                 self.anchor = roundPt(self.anchor)
2162                 pSeg = self._parentSegment
2163                 if pSeg.type != MOVE:
2164                         self.bcpIn = roundPt(self.bcpIn)
2165                 if pSeg.getParent()._nextSegment(pSeg.index).type != MOVE:
2166                         self.bcpOut = roundPt(self.bcpOut)
2167        
2168         def move(self, (x, y)):
2169                 """move the bPoint"""
2170                 bcpIn = self.bcpIn
2171                 bcpOut = self.bcpOut
2172                 self.anchor = (self.anchor[0] + x, self.anchor[1] + y)
2173                 pSeg = self._parentSegment
2174                 if pSeg.type != MOVE:
2175                         self.bcpIn = bcpIn
2176                 if pSeg.getParent()._nextSegment(pSeg.index).type != MOVE:
2177                         self.bcpOut = bcpOut
2178        
2179         def scale(self, (x, y), center=(0, 0)):
2180                 """scale the bPoint"""
2181                 centerX, centerY = center
2182                 ogCenter = (centerX, centerY)
2183                 scaledCenter = (centerX * x, centerY * y)
2184                 shiftVal = (scaledCenter[0] - ogCenter[0], scaledCenter[1] - ogCenter[1])
2185                 anchor = self.anchor
2186                 bcpIn = self.bcpIn
2187                 bcpOut = self.bcpOut
2188                 self.anchor = ((anchor[0] * x) - shiftVal[0], (anchor[1] * y) - shiftVal[1])
2189                 pSeg = self._parentSegment
2190                 if pSeg.type != MOVE:
2191                         self.bcpIn = ((bcpIn[0] * x), (bcpIn[1] * y))
2192                 if pSeg.getParent()._nextSegment(pSeg.index).type != MOVE:
2193                         self.bcpOut = ((bcpOut[0] * x), (bcpOut[1] * y))
2194        
2195         def transform(self, matrix):
2196                 """Transform this point. Use a Transform matrix
2197                 object from fontTools.misc.transform"""
2198                 self.anchor = matrix.transformPoint(self.anchor)
2199                 pSeg = self._parentSegment
2200                 if pSeg.type != MOVE:
2201                         self.bcpIn = matrix.transformPoint(self.bcpIn)
2202                 if pSeg.getParent()._nextSegment(pSeg.index).type != MOVE:
2203                         self.bcpOut = matrix.transformPoint(self.bcpOut)
2204
2205         def _get__anchorPoint(self):
2206                 return self._parentSegment.onCurve
2207        
2208         _anchorPoint = property(_get__anchorPoint, doc="the oncurve point in the parent segment")
2209
2210         def _get_anchor(self):
2211                 point = self._anchorPoint
2212                 return (point.x, point.y)
2213        
2214         def _set_anchor(self, value):
2215                 x, y = value
2216                 point = self._anchorPoint
2217                 point.x = x
2218                 point.y = y
2219                
2220         anchor = property(_get_anchor, _set_anchor, doc="the position of the anchor")
2221
2222         def _get_bcpIn(self):
2223                 pSeg = self._parentSegment
2224                 pCount = len(pSeg.offCurve)
2225                 if pCount == 2:
2226                         p = pSeg.offCurve[1]
2227                         pOn = pSeg.onCurve
2228                         return relativeBCPIn((pOn.x, pOn.y), (p.x, p.y))
2229                 else:
2230                         return (0, 0)
2231        
2232         def _set_bcpIn(self, value):
2233                 x, y = (absoluteBCPIn(self.anchor, value))
2234                 pSeg = self._parentSegment
2235                 if pSeg.type == MOVE:
2236                         #the user wants to have a bcp leading into the MOVE
2237                         if value == (0, 0) and self.bcpOut == (0, 0):
2238                                 #we have a straight line between the two anchors
2239                                 pass
2240                         else:
2241                                 #we need to insert a new CURVE segment ontop of the move
2242                                 contour = self._parentSegment.getParent()
2243                                 #set the prev segment outgoing bcp to the onCurve
2244                                 prevSeg = contour._prevSegment(self._parentSegment.index)
2245                                 prevOn = prevSeg.onCurve
2246                                 contour.appendSegment(CURVE, [(prevOn.x, prevOn.y), (x, y), self.anchor], smooth=False)
2247                 else:
2248                         pCount = len(pSeg.offCurve)
2249                         if pCount == 2:
2250                                 #if the two points in the offCurvePoints list are located at the
2251                                 #anchor coordinates we can switch to a LINE segment type
2252                                 if value == (0, 0) and self.bcpOut == (0, 0):
2253                                         pSeg.type = LINE
2254                                         pSeg.smooth = False
2255                                 else:
2256                                         pSeg.offCurve[1].x = x
2257                                         pSeg.offCurve[1].y = y
2258                         elif value != (0, 0):
2259                                 pSeg.type = CURVE
2260                                 pSeg.offCurve[1].x = x
2261                                 pSeg.offCurve[1].y = y
2262                        
2263         bcpIn = property(_get_bcpIn, _set_bcpIn, doc="the (x,y) for the incoming bcp")
2264
2265         def _get_bcpOut(self):
2266                 pSeg = self._parentSegment
2267                 nextSeg = pSeg.getParent()._nextSegment(pSeg.index)
2268                 nsCount = len(nextSeg.offCurve)
2269                 if nsCount == 2:
2270                         p = nextSeg.offCurve[0]
2271                         return relativeBCPOut(self.anchor, (p.x, p.y))
2272                 else:
2273                         return (0, 0)
2274                        
2275         def _set_bcpOut(self, value):
2276                 x, y = (absoluteBCPOut(self.anchor, value))
2277                 pSeg = self._parentSegment
2278                 nextSeg = pSeg.getParent()._nextSegment(pSeg.index)
2279                 if nextSeg.type == MOVE:
2280                         if value == (0, 0) and self.bcpIn == (0, 0):
2281                                 pass
2282                         else:                           
2283                                 #we need to insert a new CURVE segment ontop of the move
2284                                 contour = self._parentSegment.getParent()
2285                                 nextOn = nextSeg.onCurve
2286                                 contour.appendSegment(CURVE, [(x, y), (nextOn.x, nextOn.y), (nextOn.x, nextOn.y)], smooth=False)
2287                 else:
2288                         nsCount = len(nextSeg.offCurve)
2289                         if nsCount == 2:
2290                                 #if the two points in the offCurvePoints list are located at the
2291                                 #anchor coordinates we can switch to a LINE segment type
2292                                 if value == (0, 0) and self.bcpIn == (0, 0):
2293                                         nextSeg.type = LINE
2294                                         nextSeg.smooth = False
2295                                 else:
2296                                         nextSeg.offCurve[0].x = x
2297                                         nextSeg.offCurve[0].y = y
2298                         elif value != (0, 0):
2299                                 nextSeg.type = CURVE
2300                                 nextSeg.offCurve[0].x = x
2301                                 nextSeg.offCurve[0].y = y
2302                
2303         bcpOut = property(_get_bcpOut, _set_bcpOut, doc="the (x,y) for the outgoing bcp")
2304
2305         def _get_type(self):
2306                 pType = self._parentSegment.type
2307                 bpType = CORNER
2308                 if pType == CURVE:
2309                         if self._parentSegment.smooth:
2310                                 bpType = CURVE
2311                 return bpType
2312        
2313         def _set_type(self, pointType):
2314                 pSeg = self._parentSegment
2315                 segType = pSeg.type
2316                 #user wants a curve where there is a line
2317                 if pointType == CURVE and segType == LINE:
2318                         pSeg.type = CURVE
2319                         pSeg.smooth = True
2320                 #the anchor is a curve segment. so, all we need to do is turn the smooth off
2321                 elif pointType == CORNER and segType == CURVE:
2322                         pSeg.smooth = False
2323
2324         type = property(_get_type, _set_type, doc="the type of bPoint, either 'corner' or 'curve'")
2325
2326        
2327 class BaseComponent(RBaseObject):
2328        
2329         """Base class for all component objects."""
2330        
2331         def __init__(self):
2332                 RBaseObject.__init__(self)
2333                 self.changed = False            # if the object needs to be saved
2334                 self.selected = False
2335                
2336         def __repr__(self):
2337                 font = "unnamed_font"
2338                 glyph = "unnamed_glyph"
2339                 glyphParent = self.getParent()
2340                 if glyphParent is not None:
2341                         try:
2342                                 glyph = glyphParent.name
2343                         except AttributeError: pass
2344                         fontParent = glyphParent.getParent()
2345                         if fontParent is not None:
2346                                 try:
2347                                         font = fontParent.info.fullName
2348                                 except AttributeError: pass
2349                 return "<RComponent for %s.%s.components[%s]>"%(font, glyph, `self.index`)
2350                
2351         def _hasChanged(self):
2352                 """mark the object and it's parent as changed"""
2353                 self.setChanged(True)
2354                 if self.getParent() is not None:
2355                         self.getParent()._hasChanged()
2356        
2357         def copy(self, aParent=None):
2358                 """Duplicate this component."""
2359                 n = self.__class__()
2360                 if aParent is not None:
2361                         n.setParent(aParent)
2362                 elif self.getParent() is not None:
2363                         n.setParent(self.getParent())
2364                 dont = ['getParent', '_object']
2365                 for k in self.__dict__.keys():
2366                         if k in dont:
2367                                 continue
2368                         elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)):
2369                                 dup = self.__dict__[k].copy(n)
2370                         else:
2371                                 dup = copy.deepcopy(self.__dict__[k])
2372                         setattr(n, k, dup)
2373                 return n
2374
2375         def __add__(self, other):
2376                 from warnings import warn
2377                 warn("Component math has been deprecated and is slated for removal.", DeprecationWarning)
2378                 #Add one Component to another
2379                 n = self.copy()
2380                 n.offset = addPt(self.offset, other.offset)
2381                 n.scale = addPt(self.scale, other.scale)
2382                 return n
2383
2384         def __sub__(self, other):
2385                 from warnings import warn
2386                 warn("Component math has been deprecated and is slated for removal.", DeprecationWarning)
2387                 #Subtract one Component from another
2388                 n = self.copy()
2389                 n.offset = subPt(self.offset, other.offset)
2390                 n.scale = subPt(self.scale, other.scale)
2391                 return n
2392
2393         def __mul__(self, factor):
2394                 from warnings import warn
2395                 warn("Component math has been deprecated and is slated for removal.", DeprecationWarning)
2396                 #Multiply the Component with factor. Factor can be a tuple of 2 *(f1, f2)
2397                 n = self.copy()
2398                 n.offset = mulPt(self.offset, factor)
2399                 n.scale = mulPt(self.scale, factor)
2400                 return n
2401                
2402         __rmul__ = __mul__
2403        
2404         def _get_box(self):
2405                 parentGlyph = self.getParent()
2406                 # the component is an orphan
2407                 if parentGlyph is None:
2408                         return None
2409                 parentFont = parentGlyph.getParent()
2410                 # the glyph that contains the component
2411                 # does not hae a parent
2412                 if parentFont is None:
2413                         return None
2414                 # the font does not have a glyph
2415                 # that matches the glyph that
2416                 # this component references
2417                 if not parentFont.has_key(self.baseGlyph):
2418                         return None
2419                 return _box(self, parentFont)
2420        
2421         box = property(_get_box, doc="the bounding box of the component: (xMin, yMin, xMax, yMax)")
2422
2423         def round(self):
2424                 """round the offset values"""
2425                 self.offset = roundPt(self.offset)
2426                 self._hasChanged()
2427        
2428         def draw(self, pen):
2429                 """Segment pen drawing method."""
2430                 if isinstance(pen, AbstractPen):
2431                         # It's a FontTools pen, which for addComponent is identical
2432                         # to PointPen.
2433                         self.drawPoints(pen)
2434                 else:
2435                         # It's an "old" 'Fab pen
2436                         pen.addComponent(self.baseGlyph, self.offset, self.scale)
2437        
2438         def drawPoints(self, pen):
2439                 """draw the object with a point pen"""
2440                 oX, oY = self.offset
2441                 sX, sY = self.scale
2442                 #xScale, xyScale, yxScale, yScale, xOffset, yOffset
2443                 pen.addComponent(self.baseGlyph, (sX, 0, 0, sY, oX, oY))
2444
2445
2446 class BaseAnchor(RBaseObject):
2447        
2448         """Base class for all anchor point objects."""
2449        
2450         def __init__(self):
2451                 RBaseObject.__init__(self)
2452                 self.changed = False            # if the object needs to be saved
2453                 self.selected = False
2454        
2455         def __repr__(self):
2456                 font = "unnamed_font"
2457                 glyph = "unnamed_glyph"
2458                 glyphParent = self.getParent()
2459                 if glyphParent is not None:
2460                         try:
2461                                 glyph = glyphParent.name
2462                         except AttributeError: pass
2463                         fontParent = glyphParent.getParent()
2464                         if fontParent is not None:
2465                                 try:
2466                                         font = fontParent.info.fullName
2467                                 except AttributeError: pass
2468                 return "<RAnchor for %s.%s.anchors[%s]>"%(font, glyph, `self.index`)
2469
2470         def __add__(self, other):
2471                 from warnings import warn
2472                 warn("Anchor math has been deprecated and is slated for removal.", DeprecationWarning)
2473                 #Add one anchor to another
2474                 n = self.copy()
2475                 n.x, n.y = addPt((self.x, self.y), (other.x, other.y))
2476                 return n
2477
2478         def __sub__(self, other):
2479                 from warnings import warn
2480                 warn("Anchor math has been deprecated and is slated for removal.", DeprecationWarning)
2481                 #Substract one anchor from another
2482                 n = self.copy()
2483                 n.x, n.y = subPt((self.x, self.y), (other.x, other.y))
2484                 return n
2485
2486         def __mul__(self, factor):
2487                 from warnings import warn
2488                 warn("Anchor math has been deprecated and is slated for removal.", DeprecationWarning)
2489                 #Multiply the anchor with factor. Factor can be a tuple of 2 *(f1, f2)
2490                 n = self.copy()
2491                 n.x, n.y = mulPt((self.x, self.y), factor)
2492                 return n
2493                
2494         __rmul__ = __mul__
2495
2496         def _hasChanged(self):
2497                 #mark the object and it's parent as changed
2498                 self.setChanged(True)
2499                 if self.getParent() is not None:
2500                         self.getParent()._hasChanged()
2501
2502         def copy(self, aParent=None):
2503                 """Duplicate this anchor."""
2504                 n = self.__class__()
2505                 if aParent is not None:
2506                         n.setParent(aParent)
2507                 elif self.getParent() is not None:
2508                         n.setParent(self.getParent())
2509                 dont = ['getParent', '_object']
2510                 for k in self.__dict__.keys():
2511                         if k in dont:
2512                                 continue
2513                         elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)):
2514                                 dup = self.__dict__[k].copy(n)
2515                         else:
2516                                 dup = copy.deepcopy(self.__dict__[k])
2517                         setattr(n, k, dup)
2518                 return n
2519
2520         def round(self):
2521                 """round the values in the anchor"""
2522                 self.x, self.y = roundPt((self.x, self.y))
2523                 self._hasChanged()
2524                
2525         def draw(self, pen):
2526                 """Draw the object onto a segment pen"""
2527                 if isinstance(pen, AbstractPen):
2528                         # It's a FontTools pen
2529                         pen.moveTo((self.x, self.y))
2530                         pen.endPath()
2531                 else:
2532                         # It's an "old" 'Fab pen
2533                         pen.addAnchor(self.name, (self.x, self.y))
2534        
2535         def drawPoints(self, pen):
2536                 """draw the object with a point pen"""
2537                 pen.beginPath()
2538                 pen.addPoint((self.x, self.y), segmentType="move", smooth=False, name=self.name)
2539                 pen.endPath()
2540                
2541         def move(self, (x, y)):
2542                 """Move the anchor"""
2543                 pX, pY = self.position
2544                 self.position = (pX+x, pY+y)
2545                
2546         def scale(self, (x, y), center=(0, 0)):
2547                 """scale the anchor"""
2548                 pos = self.position
2549                 self.position = _scalePointFromCenter(pos, (x, y), center)
2550
2551         def transform(self, matrix):
2552                 """Transform this anchor. Use a Transform matrix
2553                 object from fontTools.misc.transform"""
2554                 self.x, self.y = matrix.transformPoint((self.x, self.y))
2555
2556
2557 class BaseGuide(RBaseObject):
2558        
2559         """Base class for all guide objects."""
2560        
2561         def __init__(self):
2562                 RBaseObject.__init__(self)
2563                 self.changed = False            # if the object needs to be saved
2564                 self.selected = False
2565
2566                
2567
2568                        
2569 class BaseInfo(RBaseObject):
2570        
2571         """Base class for all font.info objects."""
2572        
2573         def __init__(self):
2574                 RBaseObject.__init__(self)
2575        
2576         def __repr__(self):
2577                 font = self.fullName
2578                 return "<RInfo for %s>"%font
2579
2580         def _get_familyName(self):
2581                 raise NotImplementedError
2582        
2583         def _set_familyName(self, value):
2584                 raise NotImplementedError
2585        
2586         familyName = property(_get_familyName, _set_familyName, doc="family name")
2587        
2588         def _get_styleName(self):
2589                 raise NotImplementedError
2590        
2591         def _set_styleName(self, value):
2592                 raise NotImplementedError
2593        
2594         styleName = property(_get_styleName, _set_styleName, doc="style name")
2595        
2596         def _get_fullName(self):
2597                 raise NotImplementedError
2598        
2599         def _set_fullName(self, value):
2600                 raise NotImplementedError
2601        
2602         fullName = property(_get_fullName, _set_fullName, doc="full name")
2603        
2604         def _get_fontName(self):
2605                 raise NotImplementedError
2606        
2607         def _set_fontName(self, value):
2608                 raise NotImplementedError
2609        
2610         fontName = property(_get_fontName, _set_fontName, doc="font name")
2611        
2612         def _get_menuName(self):
2613                 raise NotImplementedError
2614        
2615         def _set_menuName(self, value):
2616                 raise NotImplementedError
2617        
2618         menuName = property(_get_menuName, _set_menuName, doc="menu name")
2619        
2620         def _get_fondName(self):
2621                 raise NotImplementedError
2622        
2623         def _set_fondName(self, value):
2624                 raise NotImplementedError
2625        
2626         fondName = property(_get_fondName, _set_fondName, doc="fond name")
2627        
2628         def _get_otFamilyName(self):
2629                 raise NotImplementedError
2630        
2631         def _set_otFamilyName(self, value):
2632                 raise NotImplementedError
2633        
2634         otFamilyName = property(_get_otFamilyName, _set_otFamilyName, doc="OpenType family name")
2635        
2636         def _get_otStyleName(self):
2637                 raise NotImplementedError
2638        
2639         def _set_otStyleName(self, value):
2640                 raise NotImplementedError
2641        
2642         otStyleName = property(_get_otStyleName, _set_otStyleName, doc="OpenType style name")
2643        
2644         def _get_otMacName(self):
2645                 raise NotImplementedError
2646        
2647         def _set_otMacName(self, value):
2648                 raise NotImplementedError
2649        
2650         otMacName = property(_get_otMacName, _set_otMacName, doc="Mac specific OpenType name")
2651        
2652         def _get_weightValue(self):
2653                 raise NotImplementedError
2654        
2655         def _set_weightValue(self, value):
2656                 raise NotImplementedError
2657        
2658         weightValue = property(_get_weightValue, _set_weightValue, doc="weight value")
2659        
2660         def _get_weightName(self):
2661                 raise NotImplementedError
2662        
2663         def _set_weightName(self, value):
2664                 raise NotImplementedError
2665        
2666         weightName = property(_get_weightName, _set_weightName, doc="weight name")
2667        
2668         def _get_widthName(self):
2669                 raise NotImplementedError
2670        
2671         def _set_widthName(self, value):
2672                 raise NotImplementedError
2673        
2674         widthName = property(_get_widthName, _set_widthName, doc="width name")
2675        
2676         def _get_fontStyle(self):
2677                 raise NotImplementedError
2678        
2679         def _set_fontStyle(self, value):
2680                 raise NotImplementedError
2681        
2682         fontStyle = property(_get_fontStyle, _set_fontStyle, doc="font style")
2683        
2684         def _get_msCharSet(self):
2685                 raise NotImplementedError
2686        
2687         def _set_msCharSet(self, value):
2688                 raise NotImplementedError
2689        
2690         msCharSet = property(_get_msCharSet, _set_msCharSet, doc="ms charset")
2691        
2692         def _get_note(self):
2693                 raise NotImplementedError
2694        
2695         def _set_note(self, value):
2696                 raise NotImplementedError
2697        
2698         note = property(_get_note, _set_note, doc="note")
2699        
2700         def _get_fondID(self):
2701                 raise NotImplementedError
2702        
2703         def _set_fondID(self, value):
2704                 raise NotImplementedError
2705        
2706         fondID = property(_get_fondID, _set_fondID, doc="fond id number")
2707        
2708         def _get_uniqueID(self):
2709                 raise NotImplementedError
2710        
2711         def _set_uniqueID(self, value):
2712                 raise NotImplementedError
2713        
2714         uniqueID = property(_get_uniqueID, _set_uniqueID, doc="unique id number")
2715        
2716         def _get_versionMajor(self):
2717                 raise NotImplementedError
2718        
2719         def _set_versionMajor(self, value):
2720                 raise NotImplementedError
2721        
2722         versionMajor = property(_get_versionMajor, _set_versionMajor, doc="version major")
2723        
2724         def _get_versionMinor(self):
2725                 raise NotImplementedError
2726        
2727         def _set_versionMinor(self, value):
2728                 raise NotImplementedError
2729        
2730         versionMinor = property(_get_versionMinor, _set_versionMinor, doc="version minor")
2731        
2732         def _get_year(self):
2733                 raise NotImplementedError
2734        
2735         def _set_year(self, value):
2736                 raise NotImplementedError
2737        
2738         year = property(_get_year, _set_year, doc="year")
2739        
2740         def _get_copyright(self):
2741                 raise NotImplementedError
2742        
2743         def _set_copyright(self, value):
2744                 raise NotImplementedError
2745        
2746         copyright = property(_get_copyright, _set_copyright, doc="copyright")
2747        
2748         def _get_notice(self):
2749                 raise NotImplementedError
2750        
2751         def _set_notice(self, value):
2752                 raise NotImplementedError
2753        
2754         notice = property(_get_notice, _set_notice, doc="notice")
2755        
2756         def _get_trademark(self):
2757                 raise NotImplementedError
2758        
2759         def _set_trademark(self, value):
2760                 raise NotImplementedError
2761        
2762         trademark = property(_get_trademark, _set_trademark, doc="trademark")
2763        
2764         def _get_license(self):
2765                 raise NotImplementedError
2766        
2767         def _set_license(self, value):
2768                 raise NotImplementedError
2769        
2770         license = property(_get_license, _set_license, doc="license")
2771        
2772         def _get_licenseURL(self):
2773                 raise NotImplementedError
2774        
2775         def _set_licenseURL(self, value):
2776                 raise NotImplementedError
2777        
2778         licenseURL = property(_get_licenseURL, _set_licenseURL, doc="license url")
2779        
2780         def _get_createdBy(self):
2781                 raise NotImplementedError
2782        
2783         def _set_createdBy(self, value):
2784                 raise NotImplementedError
2785        
2786         createdBy = property(_get_createdBy, _set_createdBy, doc="source")
2787        
2788         def _get_designer(self):
2789                 raise NotImplementedError
2790        
2791         def _set_designer(self, value):
2792                 raise NotImplementedError
2793        
2794         designer = property(_get_designer, _set_designer, doc="designer")
2795        
2796         def _get_designerURL(self):
2797                 raise NotImplementedError
2798        
2799         def _set_designerURL(self, value):
2800                 raise NotImplementedError
2801        
2802         designerURL = property(_get_designerURL, _set_designerURL, doc="designer url")
2803        
2804         def _get_vendorURL(self):
2805                 raise NotImplementedError
2806        
2807         def _set_vendorURL(self, value):
2808                 raise NotImplementedError
2809        
2810         vendorURL = property(_get_vendorURL, _set_vendorURL, doc="vendor url")
2811        
2812         def _get_ttVendor(self):
2813                 raise NotImplementedError
2814        
2815         def _set_ttVendor(self, value):
2816                 raise NotImplementedError
2817        
2818         ttVendor = property(_get_ttVendor, _set_ttVendor, doc="vendor")
2819        
2820         def _get_ttUniqueID(self):
2821                 raise NotImplementedError
2822        
2823         def _set_ttUniqueID(self, value):
2824                 raise NotImplementedError
2825        
2826         ttUniqueID = property(_get_ttUniqueID, _set_ttUniqueID, doc="TrueType unique id number")
2827        
2828         def _get_ttVersion(self):
2829                 raise NotImplementedError
2830        
2831         def _set_ttVersion(self, value):
2832                 raise NotImplementedError
2833        
2834         ttVersion = property(_get_ttVersion, _set_ttVersion, doc="TrueType version")
2835        
2836         def _get_unitsPerEm(self):
2837                 raise NotImplementedError
2838        
2839         def _set_unitsPerEm(self):
2840                 raise NotImplementedError
2841        
2842         unitsPerEm = property(_get_unitsPerEm, _set_unitsPerEm, doc="unitsPerEm value")
2843        
2844         def _get_ascender(self):
2845                 raise NotImplementedError
2846        
2847         def _set_ascender(self, value):
2848                 raise NotImplementedError
2849        
2850         ascender = property(_get_ascender, _set_ascender, doc="ascender value")
2851        
2852         def _get_descender(self):
2853                 raise NotImplementedError
2854        
2855         def _set_descender(self, value):
2856                 raise NotImplementedError
2857        
2858         descender = property(_get_descender, _set_descender, doc="descender value")
2859        
2860         def _get_capHeight(self):
2861                 raise NotImplementedError
2862        
2863         def _set_capHeight(self, value):
2864                 raise NotImplementedError
2865        
2866         capHeight = property(_get_capHeight, _set_capHeight, doc="cap height value")
2867        
2868         def _get_xHeight(self):
2869                 raise NotImplementedError
2870        
2871         def _set_xHeight(self, value):
2872                 raise NotImplementedError
2873        
2874         xHeight = property(_get_xHeight, _set_xHeight, doc="x height value")
2875        
2876         def _get_defaultWidth(self):
2877                 raise NotImplementedError
2878        
2879         def _set_defaultWidth(self, value):
2880                 raise NotImplementedError
2881        
2882         defaultWidth = property(_get_defaultWidth, _set_defaultWidth, doc="default width value")
2883        
2884         def _get_italicAngle(self):
2885                 raise NotImplementedError
2886        
2887         def _set_italicAngle(self, value):
2888                 raise NotImplementedError
2889        
2890         italicAngle = property(_get_italicAngle, _set_italicAngle, doc="italic_angle")
2891        
2892         def _get_slantAngle(self):
2893                 raise NotImplementedError
2894        
2895         def _set_slantAngle(self, value):
2896                 raise NotImplementedError
2897        
2898         slantAngle = property(_get_slantAngle, _set_slantAngle, doc="slant_angle")
2899        
2900         def autoNaming(self, familyName=None, styleName=None):
2901                 """Automatically set the font naming info based on family and style names."""
2902
2903                 if familyName is None:
2904                         if not self.familyName:
2905                                 raise RoboFabError, "Family name and style name must be complete"
2906                 else:
2907                         self.familyName = familyName
2908                 if styleName is None:
2909                         if not self.styleName:
2910                                 raise RoboFabError, "Family name and style name must be complete"
2911                 else:
2912                         self.styleName = styleName
2913                 family = self.familyName
2914                 style = self.styleName         
2915                 self.fullName = ' '.join((family, style))
2916                 self.fontName = '-'.join((family, style)).replace(' ', '')
2917                 self.fondName = family
2918                 self.menuName = ' '.join((family, style))
2919                 self.otFamilyName = family
2920                 self.otStyleName = style
2921                 self.otMacName = ' '.join((family, style))
2922                 #self._hasChanged()
2923                
2924 class BaseGroups(dict):
2925        
2926         """Base class for all RFont.groups objects"""
2927        
2928         def __init__(self):
2929                 pass
2930                
2931         def __repr__(self):
2932                 font = "unnamed_font"
2933                 fontParent = self.getParent()
2934                 if fontParent is not None:
2935                         try:
2936                                 font = fontParent.info.fullName
2937                         except AttributeError: pass
2938                 return "<RGroups for %s>"%font
2939
2940         def getParent(self):
2941                 """this method will be overwritten with a weakref if there is a parent."""
2942                 pass
2943        
2944         def setParent(self, parent):
2945                 import weakref
2946                 self.__dict__['getParent'] = weakref.ref(parent)
2947        
2948         def __setitem__(self, key, value):
2949                 #override base class to insure proper data is being stored
2950                 if not isinstance(key, str):
2951                         raise RoboFabError, 'key must be a string'
2952                 if not isinstance(value, list):
2953                         raise RoboFabError, 'group must be a list'
2954                 super(BaseGroups, self).__setitem__(key, value)
2955                
2956         def findGlyph(self, glyphName):
2957                 """return a list of all groups contianing glyphName"""
2958                 found = []
2959                 for i in self.keys():
2960                         l = self[i]
2961                         if glyphName in l:
2962                                 found.append(i)
2963                 return found
2964                
2965
2966 class BaseLib(dict):
2967        
2968         """Base class for all lib objects"""
2969        
2970         def __init__(self):
2971                 pass
2972        
2973         def __repr__(self):
2974                 #this is a doozy!
2975                 parent = "unknown_parent"
2976                 parentObject = self.getParent()
2977                 if parentObject is not None:
2978                         #do we have a font?
2979                         try:
2980                                 parent = parentObject.info.fullName
2981                         except AttributeError:
2982                                 #or do we have a glyph?
2983                                 try:
2984                                         parent = parentObject.name
2985                                 #we must be an orphan
2986                                 except AttributeError: pass
2987                 return "<RLib for %s>"%parent
2988
2989         def getParent(self):
2990                 """this method will be overwritten with a weakref if there is a parent."""
2991                 pass
2992        
2993         def setParent(self, parent):
2994                 import weakref
2995                 self.__dict__['getParent'] = weakref.ref(parent)
2996
2997         def copy(self, aParent=None):
2998                 """Duplicate this lib."""
2999                 n = self.__class__()
3000                 if aParent is not None:
3001                         n.setParent(aParent)
3002                 elif self.getParent() is not None:
3003                         n.setParent(self.getParent())
3004                 for k in self.keys():
3005                         n[k] = copy.deepcopy(self[k])
3006                 return n
3007        
3008                
3009 class BaseKerning(RBaseObject):
3010        
3011         """Base class for all kerning objects. Object behaves like a dict but has
3012         some special kerning specific tricks."""
3013        
3014         def __init__(self, kerningDict=None):
3015                 if not kerningDict:
3016                         kerningDict = {}
3017                 self._kerning = kerningDict
3018                 self.changed = False            # if the object needs to be saved
3019        
3020         def __repr__(self):
3021                 font = "unnamed_font"
3022                 fontParent = self.getParent()
3023                 if fontParent is not None:
3024                         try:
3025                                 font = fontParent.info.fullName
3026                         except AttributeError: pass
3027                 return "<RKerning for %s>"%font
3028                        
3029         def __getitem__(self, key):
3030                 if isinstance(key, tuple):
3031                         pair = key
3032                         return self.get(pair)
3033                 elif isinstance(key, str):
3034                         raise RoboFabError, 'kerning pair must be a tuple: (left, right)'
3035                 else:
3036                         keys = self.keys()
3037                         if key > len(keys):
3038                                 raise IndexError
3039                         keys.sort()
3040                         pair = keys[key]
3041                 if not self._kerning.has_key(pair):
3042                         raise IndexError
3043                 else:
3044                         return pair
3045        
3046         def __setitem__(self, pair, value):
3047                 if not isinstance(pair, tuple):
3048                         raise RoboFabError, 'kerning pair must be a tuple: (left, right)'
3049                 else:
3050                         if len(pair) != 2:
3051                                 raise RoboFabError, 'kerning pair must be a tuple: (left, right)'
3052                         else:
3053                                 if value == 0:
3054                                         if self._kerning.get(pair) is not None:
3055                                                 del self._kerning[pair]
3056                                 else:
3057                                         self._kerning[pair] = value
3058                                 self._hasChanged()
3059                
3060         def __len__(self):
3061                 return len(self._kerning.keys())
3062                
3063         def _hasChanged(self):
3064                 """mark the object and it's parent as changed"""
3065                 self.setChanged(True)
3066                 if self.getParent() is not None:
3067                         self.getParent()._hasChanged()
3068        
3069         def keys(self):
3070                 """return list of kerning pairs"""
3071                 return self._kerning.keys()
3072                
3073         def values(self):
3074                 """return a list of kerning values"""
3075                 return self._kerning.values()
3076        
3077         def items(self):
3078                 """return a list of kerning items"""
3079                 return self._kerning.items()
3080        
3081         def has_key(self, pair):
3082                 return self._kerning.has_key(pair)
3083        
3084         def get(self, pair, default=None):
3085                 """get a value. return None if the pair does not exist"""
3086                 value = self._kerning.get(pair, default)
3087                 return value
3088                
3089         def remove(self, pair):
3090                 """remove a kerning pair"""
3091                 self[pair] = 0
3092        
3093         def getAverage(self):
3094                 """return average of all kerning pairs"""
3095                 if len(self) == 0:
3096                         return 0
3097                 value = 0
3098                 for i in self.values():
3099                         value = value + i
3100                 return value / float(len(self))
3101        
3102         def getExtremes(self):
3103                 """return the lowest and highest kerning values"""
3104                 if len(self) == 0:
3105                         return 0
3106                 values = self.values()
3107                 values.append(0)
3108                 values.sort()
3109                 return (values[0], values[-1])
3110                
3111         def update(self, kerningDict):
3112                 """replace kerning data with the data in the given kerningDict"""
3113                 for pair in kerningDict.keys():
3114                         self[pair] = kerningDict[pair]
3115        
3116         def clear(self):
3117                 """clear all kerning"""
3118                 self._kerning = {}
3119                
3120         def add(self, value):
3121                 """add value to all kerning pairs"""
3122                 for pair in self.keys():
3123                         self[pair] = self[pair] + value
3124                
3125         def scale(self, value):
3126                 """scale all kernng pairs by value"""
3127                 for pair in self.keys():
3128                         self[pair] = self[pair] * value
3129                        
3130         def minimize(self, minimum=10):
3131                 """eliminate pairs with value less than minimum"""
3132                 for pair in self.keys():
3133                         if abs(self[pair]) < minimum:
3134                                 self[pair] = 0
3135        
3136         def eliminate(self, leftGlyphsToEliminate=None, rightGlyphsToEliminate=None, analyzeOnly=False):
3137                 """eliminate pairs containing a left glyph that is in the leftGlyphsToEliminate list
3138                 or a right glyph that is in the rightGlyphsToELiminate list.
3139                 sideGlyphsToEliminate can be a string: 'a' or list: ['a', 'b'].
3140                 analyzeOnly will not remove pairs. it will return a count
3141                 of all pairs that would be removed."""
3142                 if analyzeOnly:
3143                         count = 0
3144                 lgte = leftGlyphsToEliminate
3145                 rgte = rightGlyphsToEliminate
3146                 if isinstance(lgte, str):
3147                         lgte = [lgte]
3148                 if isinstance(rgte, str):
3149                         rgte = [rgte]
3150                 for pair in self.keys():
3151                         left, right = pair
3152                         if left in lgte or right in rgte:
3153                                 if analyzeOnly:
3154                                         count = count + 1
3155                                 else:
3156                                         self[pair] = 0
3157                 if analyzeOnly:
3158                         return count
3159                 else:
3160                         return None
3161                                
3162         def interpolate(self, sourceDictOne, sourceDictTwo, value, clearExisting=True):
3163                 """interpolate the kerning between sourceDictOne
3164                 and sourceDictTwo. clearExisting will clear existing
3165                 kerning first."""
3166                 from sets import Set
3167                 if isinstance(value, tuple):
3168                         # in case the value is a x, y tuple: use the x only.
3169                         value = value[0]
3170                 if clearExisting:
3171                         self.clear()
3172                 pairs = Set(sourceDictOne.keys()) | Set(sourceDictTwo.keys())
3173                 for pair in pairs:
3174                         s1 = sourceDictOne.get(pair, 0)
3175                         s2 = sourceDictTwo.get(pair, 0)
3176                         self[pair] = _interpolate(s1, s2, value)
3177        
3178         def round(self, multiple=10):
3179                 """round the kerning pair values to increments of multiple"""
3180                 for pair in self.keys():
3181                         value = self[pair]
3182                         self[pair] = int(round(value / float(multiple))) * multiple
3183        
3184         def occurrenceCount(self, glyphsToCount):
3185                 """return a dict with glyphs as keys and the number of
3186                 occurances of that glyph in the kerning pairs as the value
3187                 glyphsToCount can be a string: 'a' or list: ['a', 'b']"""
3188                 gtc = glyphsToCount
3189                 if isinstance(gtc, str):
3190                         gtc = [gtc]
3191                 gtcDict = {}
3192                 for glyph in gtc:
3193                         gtcDict[glyph] = 0
3194                 for pair in self.keys():
3195                         left, right = pair
3196                         if not gtcDict.get(left):
3197                                 gtcDict[left] = 0
3198                         if not gtcDict.get(right):
3199                                 gtcDict[right] = 0
3200                         gtcDict[left] = gtcDict[left] + 1
3201                         gtcDict[right] = gtcDict[right] + 1
3202                 found = {}
3203                 for glyphName in gtc:
3204                         found[glyphName] = gtcDict[glyphName]
3205                 return found
3206        
3207         def getLeft(self, glyphName):
3208                 """Return a list of kerns with glyphName as left character."""
3209                 hits = []
3210                 for k, v in self.items():
3211                         if k[0] == glyphName:
3212                                 hits.append((k, v))
3213                 return hits
3214                                
3215         def getRight(self, glyphName):
3216                 """Return a list of kerns with glyphName as left character."""
3217                 hits = []
3218                 for k, v in self.items():
3219                         if k[1] == glyphName:
3220                                 hits.append((k, v))
3221                 return hits
3222                
3223         def combine(self, kerningDicts, overwriteExisting=True):
3224                 """combine two or more kerning dictionaries.
3225                 overwrite exsisting duplicate pairs if overwriteExisting=True"""
3226                 if isinstance(kerningDicts, dict):
3227                         kerningDicts = [kerningDicts]
3228                 for kd in kerningDicts:
3229                         for pair in kd.keys():
3230                                 exists = self.has_key(pair)
3231                                 if exists and overwriteExisting:
3232                                         self[pair] = kd[pair]
3233                                 elif not exists:
3234                                         self[pair] = kd[pair]
3235                                        
3236         def swapNames(self, swapTable):
3237                 """change glyph names in all kerning pairs based on swapTable.
3238                 swapTable = {'BeforeName':'AfterName', ...}"""
3239                 for pair in self.keys():
3240                         foundInstance = False
3241                         left, right = pair
3242                         if swapTable.has_key(left):
3243                                 left = swapTable[left]
3244                                 foundInstance = True
3245                         if swapTable.has_key(right):
3246                                 right = swapTable[right]
3247                                 foundInstance = True
3248                         if foundInstance:
3249                                 self[(left, right)] = self[pair]
3250                                 self[pair] = 0
3251                                
3252         def explodeClasses(self, leftClassDict=None, rightClassDict=None, analyzeOnly=False):
3253                 """turn class kerns into real kerning pairs. classes should
3254                 be defined in dicts: {'O':['C', 'G', 'Q'], 'H':['B', 'D', 'E', 'F', 'I']}.
3255                 analyzeOnly will not remove pairs. it will return a count
3256                 of all pairs that would be added"""
3257                 if not leftClassDict:
3258                         leftClassDict = {}
3259                 if not rightClassDict:
3260                         rightClassDict = {}
3261                 if analyzeOnly:
3262                         count = 0
3263                 for pair in self.keys():
3264                         left, right = pair
3265                         value = self[pair]
3266                         if leftClassDict.get(left) and rightClassDict.get(right):
3267                                 allLeft = leftClassDict[left] + [left]
3268                                 allRight = rightClassDict[right] + [right]
3269                                 for leftSub in allLeft:
3270                                         for rightSub in allRight:
3271                                                 if analyzeOnly:
3272                                                         count = count + 1
3273                                                 else:
3274                                                         self[(leftSub, rightSub)] = value
3275                         elif leftClassDict.get(left) and not rightClassDict.get(right):
3276                                 allLeft = leftClassDict[left] + [left]
3277                                 for leftSub in allLeft:
3278                                         if analyzeOnly:
3279                                                 count = count + 1
3280                                         else:
3281                                                 self[(leftSub, right)] = value
3282                         elif rightClassDict.get(right) and not leftClassDict.get(left):
3283                                 allRight = rightClassDict[right] + [right]
3284                                 for rightSub in allRight:
3285                                         if analyzeOnly:
3286                                                 count = count + 1
3287                                         else:
3288                                                 self[(left, rightSub)] = value
3289                 if analyzeOnly:
3290                         return count
3291                 else:
3292                         return None
3293                                        
3294         def implodeClasses(self, leftClassDict=None, rightClassDict=None, analyzeOnly=False):
3295                 """condense the number of kerning pairs by applying classes.
3296                 this will eliminate all pairs containg the classed glyphs leaving
3297                 pairs that contain the key glyphs behind. analyzeOnly will not
3298                 remove pairs. it will return a count of all pairs that would be removed."""
3299                 if not leftClassDict:
3300                         leftClassDict = {}
3301                 if not rightClassDict:
3302                         rightClassDict = {}
3303                 leftImplode = []
3304                 rightImplode = []
3305                 for value in leftClassDict.values():
3306                         leftImplode = leftImplode + value
3307                 for value in rightClassDict.values():
3308                         rightImplode = rightImplode + value
3309                 analyzed = self.eliminate(leftGlyphsToEliminate=leftImplode, rightGlyphsToEliminate=rightImplode, analyzeOnly=analyzeOnly)
3310                 if analyzeOnly:
3311                         return analyzed
3312                 else:
3313                         return None
3314                
3315         def importAFM(self, path, clearExisting=True):
3316                 """Import kerning pairs from an AFM file. clearExisting=True will
3317                 clear all exising kerning"""
3318                 from fontTools.afmLib import AFM
3319                 #a nasty hack to fix line ending problems
3320                 f = open(path, 'rb')
3321                 text = f.read().replace('\r', '\n')
3322                 f.close()
3323                 f = open(path, 'wb')
3324                 f.write(text)
3325                 f.close()
3326                 #/nasty hack
3327                 kerning = AFM(path)._kerning
3328                 if clearExisting:
3329                         self.clear()
3330                 for pair in kerning.keys():
3331                         self[pair] = kerning[pair]     
3332                                
3333         def asDict(self, returnIntegers=True):
3334                 """return the object as a dictionary"""
3335                 if not returnIntegers:
3336                         return self._kerning
3337                 else:
3338                         #duplicate the kerning dict so that we aren't destroying it
3339                         kerning = {}
3340                         for pair in self.keys():
3341                                 kerning[pair] = int(round(self[pair]))
3342                         return kerning
3343
3344         def __add__(self, other):
3345                 from sets import Set
3346                 new = self.__class__()
3347                 k = Set(self.keys()) | Set(other.keys())
3348                 for key in k:
3349                         new[key] = self.get(key, 0) + other.get(key, 0)
3350                 return new
3351        
3352         def __sub__(self, other):
3353                 from sets import Set
3354                 new = self.__class__()
3355                 k = Set(self.keys()) | Set(other.keys())
3356                 for key in k:
3357                         new[key] = self.get(key, 0) - other.get(key, 0)
3358                 return new
3359
3360         def __mul__(self, factor):
3361                 new = self.__class__()
3362                 for name, value in self.items():
3363                         new[name] = value * factor
3364                 return new
3365        
3366         __rmul__ = __mul__
3367
3368         def __div__(self, factor):
3369                 if factor == 0:
3370                         raise ZeroDivisionError
3371                 return self.__mul__(1.0/factor)
3372        
3373        
Note: See TracBrowser for help on using the browser.