diff options
author | Rasmus Andersson <rasmus@notion.se> | 2018-09-03 22:55:49 +0300 |
---|---|---|
committer | Rasmus Andersson <rasmus@notion.se> | 2018-09-03 22:55:49 +0300 |
commit | c833e252c925e8dd68108660710ca835d95daa6f (patch) | |
tree | 6b2e28264ed45efd7f054e453b622098d0d875b8 /misc/pylib/robofab/objects/objectsBase.pyx | |
parent | 8c1a4c181ef12000179dfec541f1af87e9b03122 (diff) | |
download | inter-c833e252c925e8dd68108660710ca835d95daa6f.tar.xz |
Major overhaul, moving from UFO2 to Glyphs and UFO3, plus a brand new and much simpler fontbuild
Diffstat (limited to 'misc/pylib/robofab/objects/objectsBase.pyx')
-rwxr-xr-x | misc/pylib/robofab/objects/objectsBase.pyx | 3426 |
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) - |