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