diff options
Diffstat (limited to 'misc/pylib/robofab/objects')
-rwxr-xr-x | misc/pylib/robofab/objects/__init__.py | 15 | ||||
-rwxr-xr-x | misc/pylib/robofab/objects/objectsBase.pyx | 3426 | ||||
-rw-r--r-- | misc/pylib/robofab/objects/objectsFF.py | 1253 | ||||
-rwxr-xr-x | misc/pylib/robofab/objects/objectsFL.py | 3112 | ||||
-rwxr-xr-x | misc/pylib/robofab/objects/objectsRF.pyx | 1233 |
5 files changed, 9039 insertions, 0 deletions
diff --git a/misc/pylib/robofab/objects/__init__.py b/misc/pylib/robofab/objects/__init__.py new file mode 100755 index 000000000..ad85fd002 --- /dev/null +++ b/misc/pylib/robofab/objects/__init__.py @@ -0,0 +1,15 @@ +""" + +Directory for modules supporting + + Unified + + Font + + Objects + +""" + + + + diff --git a/misc/pylib/robofab/objects/objectsBase.pyx b/misc/pylib/robofab/objects/objectsBase.pyx new file mode 100755 index 000000000..3154fb3f4 --- /dev/null +++ b/misc/pylib/robofab/objects/objectsBase.pyx @@ -0,0 +1,3426 @@ +""" +Base classes for the Unified Font Objects (UFO), +a series of classes that deal with fonts, glyphs, +contours and related things. + +Unified Font Objects are: +- platform independent +- application independent + +About Object Inheritance: +objectsFL and objectsRF objects inherit +methods and attributes from these objects. +In other words, if it is in here, you can +do it with the objectsFL and objectsRF. +""" + + +from __future__ import generators +from __future__ import division + +from warnings import warn +import math +import copy + +from robofab import ufoLib +from robofab import RoboFabError +from robofab.misc.arrayTools import updateBounds, pointInRect, unionRect, sectRect +from fontTools.pens.basePen import AbstractPen +from fontTools.pens.areaPen import AreaPen +from ..exceptions import RoboFabError, RoboFabWarning + +try: + set +except NameError: + from sets import Set as set + +#constants for dealing with segments, points and bPoints +MOVE = 'move' +LINE = 'line' +CORNER = 'corner' +CURVE = 'curve' +QCURVE = 'qcurve' +OFFCURVE = 'offcurve' + +DEGREE = 180 / math.pi + + + +# the key for the postscript hint data stored in the UFO +postScriptHintDataLibKey = "org.robofab.postScriptHintData" + +# from http://svn.typesupply.com/packages/fontMath/mathFunctions.py + +def add(v1, v2): + return v1 + v2 + +def sub(v1, v2): + return v1 - v2 + +def mul(v, f): + return v * f + +def div(v, f): + return v / f + +def issequence(x): + "Is x a sequence? We say it is if it has a __getitem__ method." + return hasattr(x, '__getitem__') + + + +class BasePostScriptHintValues(object): + """ Base class for postscript hinting information. + """ + + def __init__(self, data=None): + if data is not None: + self.fromDict(data) + else: + for name in self._attributeNames.keys(): + setattr(self, name, self._attributeNames[name]['default']) + + def getParent(self): + """this method will be overwritten with a weakref if there is a parent.""" + return None + + def setParent(self, parent): + import weakref + self.getParent = weakref.ref(parent) + + def isEmpty(self): + """Check all attrs and decide if they're all empty.""" + empty = True + for name in self._attributeNames: + if getattr(self, name): + empty = False + break + return empty + + def clear(self): + """Set all attributes to default / empty""" + for name in self._attributeNames: + setattr(self, name, self._attributeNames[name]['default']) + + def _loadFromLib(self, lib): + data = lib.get(postScriptHintDataLibKey) + if data is not None: + self.fromDict(data) + + def _saveToLib(self, lib): + parent = self.getParent() + if parent is not None: + parent.setChanged(True) + hintsDict = self.asDict() + if hintsDict: + lib[postScriptHintDataLibKey] = hintsDict + + def fromDict(self, data): + for name in self._attributeNames: + if name in data: + setattr(self, name, data[name]) + + def asDict(self): + d = {} + for name in self._attributeNames: + try: + value = getattr(self, name) + except AttributeError: + print "%s attribute not supported"%name + continue + if value: + d[name] = getattr(self, name) + return d + + def update(self, other): + assert isinstance(other, BasePostScriptHintValues) + for name in self._attributeNames.keys(): + v = getattr(other, name) + if v is not None: + setattr(self, name, v) + + def __repr__(self): + return "<Base PS Hint Data>" + + def copy(self, aParent=None): + """Duplicate this object. Pass an object for parenting if you want.""" + n = self.__class__(data=self.asDict()) + if aParent is not None: + n.setParent(aParent) + elif self.getParent() is not None: + n.setParent(self.getParent()) + dont = ['getParent'] + for k in self.__dict__.keys(): + if k in dont: + continue + dup = copy.deepcopy(self.__dict__[k]) + setattr(n, k, dup) + return n + +class BasePostScriptGlyphHintValues(BasePostScriptHintValues): + """ Base class for glyph-level postscript hinting information. + vStems, hStems + """ + _attributeNames = { + # some of these values can have only a certain number of elements + 'vHints': {'default': None, 'max':100, 'isVertical':True}, + 'hHints': {'default': None, 'max':100, 'isVertical':False}, + } + + def __init__(self, data=None): + if data is not None: + self.fromDict(data) + else: + for name in self._attributeNames.keys(): + setattr(self, name, self._attributeNames[name]['default']) + + def __repr__(self): + return "<PostScript Glyph Hints Values>" + + def round(self): + """Round the values to reasonable values. + - stems are rounded to int + """ + for name, values in self._attributeNames.items(): + v = getattr(self, name) + if v is None: + continue + new = [] + for n in v: + new.append((int(round(n[0])), int(round(n[1])))) + setattr(self, name, new) + + # math operations for psHint object + # Note: math operations can change integers to floats. + def __add__(self, other): + assert isinstance(other, BasePostScriptHintValues) + copied = self.copy() + self._processMathOne(copied, other, add) + return copied + + def __sub__(self, other): + assert isinstance(other, BasePostScriptHintValues) + copied = self.copy() + self._processMathOne(copied, other, sub) + return copied + + def __mul__(self, factor): + #if isinstance(factor, tuple): + # factor = factor[0] + copiedInfo = self.copy() + self._processMathTwo(copiedInfo, factor, mul) + return copiedInfo + + __rmul__ = __mul__ + + def __div__(self, factor): + #if isinstance(factor, tuple): + # factor = factor[0] + copiedInfo = self.copy() + self._processMathTwo(copiedInfo, factor, div) + return copiedInfo + + __rdiv__ = __div__ + + def _processMathOne(self, copied, other, funct): + for name, values in self._attributeNames.items(): + a = None + b = None + v = None + if hasattr(copied, name): + a = getattr(copied, name) + if hasattr(other, name): + b = getattr(other, name) + if a is not None and b is not None: + if len(a) != len(b): + # can't do math with non matching zones + continue + l = len(a) + for i in range(l): + if v is None: + v = [] + ai = a[i] + bi = b[i] + l2 = min(len(ai), len(bi)) + v2 = [funct(ai[j], bi[j]) for j in range(l2)] + v.append(v2) + if v is not None: + setattr(copied, name, v) + + def _processMathTwo(self, copied, factor, funct): + for name, values in self._attributeNames.items(): + a = None + b = None + v = None + isVertical = self._attributeNames[name]['isVertical'] + splitFactor = factor + if isinstance(factor, tuple): + #print "mathtwo", name, funct, factor, isVertical + if isVertical: + splitFactor = factor[1] + else: + splitFactor = factor[0] + if hasattr(copied, name): + a = getattr(copied, name) + if a is not None: + for i in range(len(a)): + if v is None: + v = [] + v2 = [funct(a[i][j], splitFactor) for j in range(len(a[i]))] + v.append(v2) + if v is not None: + setattr(copied, name, v) + + +class BasePostScriptFontHintValues(BasePostScriptHintValues): + """ Base class for font-level postscript hinting information. + Blues values, stem values. + """ + + _attributeNames = { + # some of these values can have only a certain number of elements + # default: what the value should be when initialised + # max: the maximum number of items this attribute is allowed to have + # isVertical: the vertical relevance + 'blueFuzz': {'default': None, 'max':1, 'isVertical':True}, + 'blueScale': {'default': None, 'max':1, 'isVertical':True}, + 'blueShift': {'default': None, 'max':1, 'isVertical':True}, + 'forceBold': {'default': None, 'max':1, 'isVertical':False}, + 'blueValues': {'default': None, 'max':7, 'isVertical':True}, + 'otherBlues': {'default': None, 'max':5, 'isVertical':True}, + 'familyBlues': {'default': None, 'max':7, 'isVertical':True}, + 'familyOtherBlues': {'default': None, 'max':5, 'isVertical':True}, + 'vStems': {'default': None, 'max':6, 'isVertical':True}, + 'hStems': {'default': None, 'max':11, 'isVertical':False}, + } + + def __init__(self, data=None): + if data is not None: + self.fromDict(data) + + def __repr__(self): + return "<PostScript Font Hints Values>" + + # route attribute calls to info object + + def _bluesToPairs(self, values): + values.sort() + finalValues = [] + for value in values: + if not finalValues or len(finalValues[-1]) == 2: + finalValues.append([]) + finalValues[-1].append(value) + return finalValues + + def _bluesFromPairs(self, values): + finalValues = [] + for value1, value2 in values: + finalValues.append(value1) + finalValues.append(value2) + finalValues.sort() + return finalValues + + def _get_blueValues(self): + values = self.getParent().info.postscriptBlueValues + if values is None: + values = [] + values = self._bluesToPairs(values) + return values + + def _set_blueValues(self, values): + if values is None: + values = [] + values = self._bluesFromPairs(values) + self.getParent().info.postscriptBlueValues = values + + blueValues = property(_get_blueValues, _set_blueValues) + + def _get_otherBlues(self): + values = self.getParent().info.postscriptOtherBlues + if values is None: + values = [] + values = self._bluesToPairs(values) + return values + + def _set_otherBlues(self, values): + if values is None: + values = [] + values = self._bluesFromPairs(values) + self.getParent().info.postscriptOtherBlues = values + + otherBlues = property(_get_otherBlues, _set_otherBlues) + + def _get_familyBlues(self): + values = self.getParent().info.postscriptFamilyBlues + if values is None: + values = [] + values = self._bluesToPairs(values) + return values + + def _set_familyBlues(self, values): + if values is None: + values = [] + values = self._bluesFromPairs(values) + self.getParent().info.postscriptFamilyBlues = values + + familyBlues = property(_get_familyBlues, _set_familyBlues) + + def _get_familyOtherBlues(self): + values = self.getParent().info.postscriptFamilyOtherBlues + if values is None: + values = [] + values = self._bluesToPairs(values) + return values + + def _set_familyOtherBlues(self, values): + if values is None: + values = [] + values = self._bluesFromPairs(values) + self.getParent().info.postscriptFamilyOtherBlues = values + + familyOtherBlues = property(_get_familyOtherBlues, _set_familyOtherBlues) + + def _get_vStems(self): + return self.getParent().info.postscriptStemSnapV + + def _set_vStems(self, value): + if value is None: + value = [] + self.getParent().info.postscriptStemSnapV = list(value) + + vStems = property(_get_vStems, _set_vStems) + + def _get_hStems(self): + return self.getParent().info.postscriptStemSnapH + + def _set_hStems(self, value): + if value is None: + value = [] + self.getParent().info.postscriptStemSnapH = list(value) + + hStems = property(_get_hStems, _set_hStems) + + def _get_blueScale(self): + return self.getParent().info.postscriptBlueScale + + def _set_blueScale(self, value): + self.getParent().info.postscriptBlueScale = value + + blueScale = property(_get_blueScale, _set_blueScale) + + def _get_blueShift(self): + return self.getParent().info.postscriptBlueShift + + def _set_blueShift(self, value): + self.getParent().info.postscriptBlueShift = value + + blueShift = property(_get_blueShift, _set_blueShift) + + def _get_blueFuzz(self): + return self.getParent().info.postscriptBlueFuzz + + def _set_blueFuzz(self, value): + self.getParent().info.postscriptBlueFuzz = value + + blueFuzz = property(_get_blueFuzz, _set_blueFuzz) + + def _get_forceBold(self): + return self.getParent().info.postscriptForceBold + + def _set_forceBold(self, value): + self.getParent().info.postscriptForceBold = value + + forceBold = property(_get_forceBold, _set_forceBold) + + def round(self): + """Round the values to reasonable values. + - blueScale is not rounded, it is a float + - forceBold is set to False if -0.5 < value < 0.5. Otherwise it will be True. + - blueShift, blueFuzz are rounded to int + - stems are rounded to int + - blues are rounded to int + """ + for name, values in self._attributeNames.items(): + if name == "blueScale": + continue + elif name == "forceBold": + v = getattr(self, name) + if v is None: + continue + if -0.5 <= v <= 0.5: + setattr(self, name, False) + else: + setattr(self, name, True) + elif name in ['blueFuzz', 'blueShift']: + v = getattr(self, name) + if v is None: + continue + setattr(self, name, int(round(v))) + elif name in ['hStems', 'vStems']: + v = getattr(self, name) + if v is None: + continue + new = [] + for n in v: + new.append(int(round(n))) + setattr(self, name, new) + else: + v = getattr(self, name) + if v is None: + continue + new = [] + for n in v: + new.append([int(round(m)) for m in n]) + setattr(self, name, new) + + + +class RoboFabInterpolationError(Exception): pass + + +def _interpolate(a,b,v): + """interpolate values by factor v""" + return a + (b-a) * v + +def _interpolatePt(a, b, v): + """interpolate point by factor v""" + xa, ya = a + xb, yb = b + if not isinstance(v, tuple): + xv = v + yv = v + else: + xv, yv = v + return xa + (xb-xa) * xv, ya + (yb-ya) * yv + +def _scalePointFromCenter(pt, scale, center): + """scale a point from a center point""" + pointX, pointY = pt + scaleX, scaleY = scale + centerX, centerY = center + ogCenter = center + scaledCenter = (centerX * scaleX, centerY * scaleY) + shiftVal = (scaledCenter[0] - ogCenter[0], scaledCenter[1] - ogCenter[1]) + scaledPointX = (pointX * scaleX) - shiftVal[0] + scaledPointY = (pointY * scaleY) - shiftVal[1] + return (scaledPointX, scaledPointY) + +def _box(objectToMeasure, fontObject=None): + """calculate the bounds of the object and return it as a (xMin, yMin, xMax, yMax)""" + #from fontTools.pens.boundsPen import BoundsPen + from robofab.pens.boundsPen import BoundsPen + boundsPen = BoundsPen(glyphSet=fontObject) + objectToMeasure.draw(boundsPen) + bounds = boundsPen.bounds + if bounds is None: + bounds = (0, 0, 0, 0) + return bounds + +def roundPt(pt): + """Round a vector""" + return int(round(pt[0])), int(round(pt[1])) + +def addPt(ptA, ptB): + """Add two vectors""" + return ptA[0] + ptB[0], ptA[1] + ptB[1] + +def subPt(ptA, ptB): + """Substract two vectors""" + return ptA[0] - ptB[0], ptA[1] - ptB[1] + +def mulPt(ptA, scalar): + """Multiply a vector with scalar""" + if not isinstance(scalar, tuple): + f1 = scalar + f2 = scalar + else: + f1, f2 = scalar + return ptA[0]*f1, ptA[1]*f2 + +def relativeBCPIn(anchor, BCPIn): + """convert absolute incoming bcp value to a relative value""" + return (BCPIn[0] - anchor[0], BCPIn[1] - anchor[1]) + +def absoluteBCPIn(anchor, BCPIn): + """convert relative incoming bcp value to an absolute value""" + return (BCPIn[0] + anchor[0], BCPIn[1] + anchor[1]) + +def relativeBCPOut(anchor, BCPOut): + """convert absolute outgoing bcp value to a relative value""" + return (BCPOut[0] - anchor[0], BCPOut[1] - anchor[1]) + +def absoluteBCPOut(anchor, BCPOut): + """convert relative outgoing bcp value to an absolute value""" + return (BCPOut[0] + anchor[0], BCPOut[1] + anchor[1]) + +class FuzzyNumber(object): + + def __init__(self, value, threshold): + self.value = value + self.threshold = threshold + + def __cmp__(self, other): + if abs(self.value - other.value) < self.threshold: + return 0 + else: + return cmp(self.value, other.value) + + +class RBaseObject(object): + + """Base class for wrapper objects""" + + attrMap= {} + _title = "RoboFab Wrapper" + + def __init__(self): + self._object = {} + self.changed = False # if the object needs to be saved + self.selected = False + + def __len__(self): + return len(self._object) + + def __repr__(self): + try: + name = `self._object` + except: + name = "None" + return "<%s for %s>" %(self._title, name) + + def copy(self, aParent=None): + """Duplicate this object. Pass an object for parenting if you want.""" + n = self.__class__() + if aParent is not None: + n.setParent(aParent) + elif self.getParent() is not None: + n.setParent(self.getParent()) + dont = ['getParent'] + for k in self.__dict__.keys(): + if k in dont: + continue + elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)): + dup = self.__dict__[k].copy(n) + else: + dup = copy.deepcopy(self.__dict__[k]) + setattr(n, k, dup) + return n + + def round(self): + pass + + def isRobofab(self): + """Presence of this method indicates a Robofab object""" + return 1 + + def naked(self): + """Return the wrapped object itself, in case it is needed for direct access.""" + return self._object + + def setChanged(self, state=True): + self.changed = state + + def getParent(self): + """this method will be overwritten with a weakref if there is a parent.""" + return None + + def setParent(self, parent): + import weakref + self.getParent = weakref.ref(parent) + + def _writeXML(self, writer): + pass + + def dump(self, private=False): + """Print a dump of this object to the std out.""" + from robofab.tools.objectDumper import dumpObject + dumpObject(self, private) + + + +class BaseFont(RBaseObject): + + """Base class for all font objects.""" + + _allFonts = [] + + def __init__(self): + import weakref + RBaseObject.__init__(self) + self.changed = False # if the object needs to be saved + self._allFonts.append(weakref.ref(self)) + self._supportHints = False + + def __repr__(self): + try: + name = self.info.postscriptFullName + except AttributeError: + name = "unnamed_font" + return "<RFont font for %s>" %(name) + + def __eq__(self, other): + #Compare this font with another, compare if they refer to the same file. + return self._compare(other) + + def _compare(self, other): + """Compare this font to other. RF and FL UFO implementations need + slightly different ways of comparing fonts. This method does the + basic stuff. Start with simple and quick comparisons, then move into + detailed comparisons of glyphs.""" + if not hasattr(other, "fileName"): + return False + if self.fileName is not None and self.fileName == other.fileName: + return True + if self.fileName <> other.fileName: + return False + # this will falsely identify two distinct "Untitled" as equal + # so test some more. A lot of work to please some dolt who + # does not save his fonts while running scripts. + try: + if len(self) <> len(other): + return False + except TypeError: + return False + # same name and length. start comparing glyphs + namesSelf = self.keys() + namesOther = other.keys() + namesSelf.sort() + namesOther.sort() + for i in range(len(namesSelf)): + if namesSelf[i] <> namesOther[i]: + return False + for c in self: + if not c == other[c.name]: + return False + return True + + def keys(self): + # must be implemented by subclass + raise NotImplementedError + + def __iter__(self): + for glyphName in self.keys(): + yield self.getGlyph(glyphName) + + def __getitem__(self, glyphName): + return self.getGlyph(glyphName) + + def __contains__(self, glyphName): + return self.has_key(glyphName) + + def _hasChanged(self): + #mark the object as changed + self.setChanged(True) + + def update(self): + """update the font""" + pass + + def close(self, save=1): + """Close the font, saving is optional.""" + pass + + def round(self): + """round all of the points in all of the glyphs""" + for glyph in self: + glyph.round() + + def autoUnicodes(self): + """Using fontTools.agl, assign Unicode lists to all glyphs in the font""" + for glyph in self: + glyph.autoUnicodes() + + def getCharacterMapping(self): + """Create a dictionary of unicode -> [glyphname, ...] mappings. + Note that this dict is created each time this method is called, + which can make it expensive for larger fonts. All glyphs are loaded. + Note that one glyph can have multiple unicode values, + and a unicode value can have multiple glyphs pointing to it.""" + map = {} + for glyph in self: + for u in glyph.unicodes: + if not map.has_key(u): + map[u] = [] + map[u].append(glyph.name) + return map + + def getReverseComponentMapping(self): + """ + Get a reversed map of component references in the font. + { + 'A' : ['Aacute', 'Aring'] + 'acute' : ['Aacute'] + 'ring' : ['Aring'] + etc. + } + """ + map = {} + for glyph in self: + glyphName = glyph.name + for component in glyph.components: + baseGlyphName = component.baseGlyph + if not map.has_key(baseGlyphName): + map[baseGlyphName] = [] + map[baseGlyphName].append(glyphName) + return map + + def compileGlyph(self, glyphName, baseName, accentNames, \ + adjustWidth=False, preflight=False, printErrors=True): + """Compile components into a new glyph using components and anchorpoints. + glyphName: the name of the glyph where it all needs to go + baseName: the name of the base glyph + accentNames: a list of accentName, anchorName tuples, [('acute', 'top'), etc] + """ + anchors = {} + errors = {} + baseGlyph = self[baseName] + for anchor in baseGlyph.getAnchors(): + anchors[anchor.name] = anchor.position + destGlyph = self.newGlyph(glyphName, clear=True) + destGlyph.appendComponent(baseName) + destGlyph.width = baseGlyph.width + for accentName, anchorName in accentNames: + try: + accent = self[accentName] + except IndexError: + errors["glyph '%s' is missing in font %s"%(accentName, self.info.fullName)] = 1 + continue + shift = None + for accentAnchor in accent.getAnchors(): + if '_'+anchorName == accentAnchor.name: + shift = anchors[anchorName][0] - accentAnchor.position[0], anchors[anchorName][1] - accentAnchor.position[1] + destGlyph.appendComponent(accentName, offset=shift) + break + if shift is not None: + for accentAnchor in accent.getAnchors(): + if accentAnchor.name in anchors: + anchors[accentAnchor.name] = shift[0]+accentAnchor.position[0], shift[1]+accentAnchor.position[1] + if printErrors: + for px in errors.keys(): + print px + return destGlyph + + def generateGlyph(self, glyphName, replace=1, preflight=False, printErrors=True): + """Generate a glyph and return it. Assembled from GlyphConstruction.txt""" + from robofab.tools.toolsAll import readGlyphConstructions + con = readGlyphConstructions() + entry = con.get(glyphName, None) + if not entry: + print "glyph '%s' is not listed in the robofab/Data/GlyphConstruction.txt"%(glyphName) + return + baseName = con[glyphName][0] + parts = con[glyphName][1:] + return self.compileGlyph(glyphName, baseName, parts, adjustWidth=1, preflight=preflight, printErrors=printErrors) + + def interpolate(self, factor, minFont, maxFont, suppressError=True, analyzeOnly=False, doProgress=False): + """Traditional interpolation method. Interpolates by factor between minFont and maxFont. + suppressError will supress all tracebacks and analyze only will not perform the interpolation + but it will analyze all glyphs and return a dict of problems.""" + errors = {} + if not isinstance(factor, tuple): + factor = factor, factor + minGlyphNames = minFont.keys() + maxGlyphNames = maxFont.keys() + allGlyphNames = list(set(minGlyphNames) | set(maxGlyphNames)) + if doProgress: + from robofab.interface.all.dialogs import ProgressBar + progress = ProgressBar('Interpolating...', len(allGlyphNames)) + tickCount = 0 + # some dimensions and values + self.info.ascender = _interpolate(minFont.info.ascender, maxFont.info.ascender, factor[1]) + self.info.descender = _interpolate(minFont.info.descender, maxFont.info.descender, factor[1]) + # check for the presence of the glyph in each of the fonts + for glyphName in allGlyphNames: + if doProgress: + progress.label(glyphName) + fatalError = False + if glyphName not in minGlyphNames: + fatalError = True + if not errors.has_key('Missing Glyphs'): + errors['Missing Glyphs'] = [] + errors['Missing Glyphs'].append('Interpolation Error: %s not in %s'%(glyphName, minFont.info.postscriptFullName)) + if glyphName not in maxGlyphNames: + fatalError = True + if not errors.has_key('Missing Glyphs'): + errors['Missing Glyphs'] = [] + errors['Missing Glyphs'].append('Interpolation Error: %s not in %s'%(glyphName, maxFont.info.postscriptFullName)) + # if no major problems, proceed. + if not fatalError: + # remove the glyph since FontLab has a problem with + # interpolating an existing glyph that contains + # some contour data. + oldLib = {} + oldMark = None + oldNote = None + if self.has_key(glyphName): + glyph = self[glyphName] + oldLib = dict(glyph.lib) + oldMark = glyph.mark + oldNote = glyph.note + self.removeGlyph(glyphName) + selfGlyph = self.newGlyph(glyphName) + selfGlyph.lib.update(oldLib) + if oldMark != None: + selfGlyph.mark = oldMark + selfGlyph.note = oldNote + min = minFont[glyphName] + max = maxFont[glyphName] + ok, glyphErrors = selfGlyph.interpolate(factor, min, max, suppressError=suppressError, analyzeOnly=analyzeOnly) + if not errors.has_key('Glyph Errors'): + errors['Glyph Errors'] = {} + errors['Glyph Errors'][glyphName] = glyphErrors + if doProgress: + progress.tick(tickCount) + tickCount = tickCount + 1 + if doProgress: + progress.close() + return errors + + def getGlyphNameToFileNameFunc(self): + funcName = self.lib.get("org.robofab.glyphNameToFileNameFuncName") + if funcName is None: + return None + parts = funcName.split(".") + module = ".".join(parts[:-1]) + try: + item = __import__(module) + for sub in parts[1:]: + item = getattr(item, sub) + except (ImportError, AttributeError): + warn("Can't find glyph name to file name converter function, " + "falling back to default scheme (%s)" % funcName, RoboFabWarning) + return None + else: + return item + + +class BaseGlyph(RBaseObject): + + """Base class for all glyph objects.""" + + def __init__(self): + RBaseObject.__init__(self) + #self.contours = [] + #self.components = [] + #self.anchors = [] + #self.width = 0 + #self.note = None + ##self.unicodes = [] + #self.selected = None + self.changed = False # if the object needs to be saved + + def __repr__(self): + font = "unnamed_font" + glyph = "unnamed_glyph" + fontParent = self.getParent() + if fontParent is not None: + try: + font = fontParent.info.postscriptFullName + except AttributeError: + pass + try: + glyph = self.name + except AttributeError: + pass + return "<RGlyph for %s.%s>" %(font, glyph) + + # + # Glyph Math + # + + def _getMathData(self): + from robofab.pens.mathPens import GetMathDataPointPen + pen = GetMathDataPointPen() + self.drawPoints(pen) + data = pen.getData() + return data + + def _setMathData(self, data, destination=None): + from robofab.pens.mathPens import CurveSegmentFilterPointPen + if destination is None: + newGlyph = self._mathCopy() + else: + newGlyph = destination + newGlyph.clear() + # + # draw the data onto the glyph + pointPen = newGlyph.getPointPen() + filterPen = CurveSegmentFilterPointPen(pointPen) + for contour in data['contours']: + filterPen.beginPath() + for segmentType, pt, smooth, name in contour: + filterPen.addPoint(pt=pt, segmentType=segmentType, smooth=smooth, name=name) + filterPen.endPath() + for baseName, transformation in data['components']: + filterPen.addComponent(baseName, transformation) + for pt, name in data['anchors']: + filterPen.beginPath() + filterPen.addPoint(pt=pt, segmentType="move", smooth=False, name=name) + filterPen.endPath() + newGlyph.width = data['width'] + psHints = data.get('psHints') + if psHints is not None: + newGlyph.psHints.update(psHints) + # + return newGlyph + + def _getMathDestination(self): + # make a new, empty glyph + return self.__class__() + + def _mathCopy(self): + # copy self without contour, component and anchor data + glyph = self._getMathDestination() + glyph.name = self.name + glyph.unicodes = list(self.unicodes) + glyph.width = self.width + glyph.note = self.note + glyph.lib = dict(self.lib) + return glyph + + def _processMathOne(self, otherGlyph, funct): + # used by: __add__, __sub__ + # + newData = { + 'contours':[], + 'components':[], + 'anchors':[], + 'width':None + } + selfData = self._getMathData() + otherData = otherGlyph._getMathData() + # + # contours + selfContours = selfData['contours'] + otherContours = otherData['contours'] + newContours = newData['contours'] + if len(selfContours) > 0: + for contourIndex in xrange(len(selfContours)): + newContours.append([]) + selfContour = selfContours[contourIndex] + otherContour = otherContours[contourIndex] + for pointIndex in xrange(len(selfContour)): + segType, pt, smooth, name = selfContour[pointIndex] + newX, newY = funct(selfContour[pointIndex][1], otherContour[pointIndex][1]) + newContours[-1].append((segType, (newX, newY), smooth, name)) + # anchors + selfAnchors = selfData['anchors'] + otherAnchors = otherData['anchors'] + newAnchors = newData['anchors'] + if len(selfAnchors) > 0: + selfAnchors, otherAnchors = self._mathAnchorCompare(selfAnchors, otherAnchors) + anchorNames = selfAnchors.keys() + for anchorName in anchorNames: + selfAnchorList = selfAnchors[anchorName] + otherAnchorList = otherAnchors[anchorName] + for i in range(len(selfAnchorList)): + selfAnchor = selfAnchorList[i] + otherAnchor = otherAnchorList[i] + newAnchor = funct(selfAnchor, otherAnchor) + newAnchors.append((newAnchor, anchorName)) + # components + selfComponents = selfData['components'] + otherComponents = otherData['components'] + newComponents = newData['components'] + if len(selfComponents) > 0: + selfComponents, otherComponents = self._mathComponentCompare(selfComponents, otherComponents) + componentNames = selfComponents.keys() + for componentName in componentNames: + selfComponentList = selfComponents[componentName] + otherComponentList = otherComponents[componentName] + for i in range(len(selfComponentList)): + # transformation breakdown: xScale, xyScale, yxScale, yScale, xOffset, yOffset + selfXScale, selfXYScale, selfYXScale, selfYScale, selfXOffset, selfYOffset = selfComponentList[i] + otherXScale, otherXYScale, otherYXScale, otherYScale, otherXOffset, otherYOffset = otherComponentList[i] + newXScale, newXYScale = funct((selfXScale, selfXYScale), (otherXScale, otherXYScale)) + newYXScale, newYScale = funct((selfYXScale, selfYScale), (otherYXScale, otherYScale)) + newXOffset, newYOffset = funct((selfXOffset, selfYOffset), (otherXOffset, otherYOffset)) + newComponents.append((componentName, (newXScale, newXYScale, newYXScale, newYScale, newXOffset, newYOffset))) + return newData + + def _processMathTwo(self, factor, funct): + # used by: __mul__, __div__ + # + newData = { + 'contours':[], + 'components':[], + 'anchors':[], + 'width':None + } + selfData = self._getMathData() + # contours + selfContours = selfData['contours'] + newContours = newData['contours'] + for selfContour in selfContours: + newContours.append([]) + for segType, pt, smooth, name in selfContour: + newX, newY = funct(pt, factor) + newContours[-1].append((segType, (newX, newY), smooth, name)) + # anchors + selfAnchors = selfData['anchors'] + newAnchors = newData['anchors'] + for pt, anchorName in selfAnchors: + newPt = funct(pt, factor) + newAnchors.append((newPt, anchorName)) + # components + selfComponents = selfData['components'] + newComponents = newData['components'] + for baseName, transformation in selfComponents: + xScale, xyScale, yxScale, yScale, xOffset, yOffset = transformation + newXOffset, newYOffset = funct((xOffset, yOffset), factor) + newXScale, newYScale = funct((xScale, yScale), factor) + newXYScale, newYXScale = funct((xyScale, yxScale), factor) + newComponents.append((baseName, (newXScale, newXYScale, newYXScale, newYScale, newXOffset, newYOffset))) + # return the data + return newData + + def _mathAnchorCompare(self, selfMathAnchors, otherMathAnchors): + # collect compatible anchors + selfAnchors = {} + for pt, name in selfMathAnchors: + if not selfAnchors.has_key(name): + selfAnchors[name] = [] + selfAnchors[name].append(pt) + otherAnchors = {} + for pt, name in otherMathAnchors: + if not otherAnchors.has_key(name): + otherAnchors[name] = [] + otherAnchors[name].append(pt) + compatAnchors = set(selfAnchors.keys()) & set(otherAnchors.keys()) + finalSelfAnchors = {} + finalOtherAnchors = {} + for name in compatAnchors: + if not finalSelfAnchors.has_key(name): + finalSelfAnchors[name] = [] + if not finalOtherAnchors.has_key(name): + finalOtherAnchors[name] = [] + selfList = selfAnchors[name] + otherList = otherAnchors[name] + selfCount = len(selfList) + otherCount = len(otherList) + if selfCount != otherCount: + r = range(min(selfCount, otherCount)) + else: + r = range(selfCount) + for i in r: + finalSelfAnchors[name].append(selfList[i]) + finalOtherAnchors[name].append(otherList[i]) + return finalSelfAnchors, finalOtherAnchors + + def _mathComponentCompare(self, selfMathComponents, otherMathComponents): + # collect compatible components + selfComponents = {} + for baseName, transformation in selfMathComponents: + if not selfComponents.has_key(baseName): + selfComponents[baseName] = [] + selfComponents[baseName].append(transformation) + otherComponents = {} + for baseName, transformation in otherMathComponents: + if not otherComponents.has_key(baseName): + otherComponents[baseName] = [] + otherComponents[baseName].append(transformation) + compatComponents = set(selfComponents.keys()) & set(otherComponents.keys()) + finalSelfComponents = {} + finalOtherComponents = {} + for baseName in compatComponents: + if not finalSelfComponents.has_key(baseName): + finalSelfComponents[baseName] = [] + if not finalOtherComponents.has_key(baseName): + finalOtherComponents[baseName] = [] + selfList = selfComponents[baseName] + otherList = otherComponents[baseName] + selfCount = len(selfList) + otherCount = len(otherList) + if selfCount != otherCount: + r = range(min(selfCount, otherCount)) + else: + r = range(selfCount) + for i in r: + finalSelfComponents[baseName].append(selfList[i]) + finalOtherComponents[baseName].append(otherList[i]) + return finalSelfComponents, finalOtherComponents + + def __mul__(self, factor): + assert isinstance(factor, (int, float, tuple)), "Glyphs can only be multiplied by int, float or a 2-tuple." + if not isinstance(factor, tuple): + factor = (factor, factor) + data = self._processMathTwo(factor, mulPt) + data['width'] = self.width * factor[0] + # psHints + if not self.psHints.isEmpty(): + newPsHints = self.psHints * factor + data['psHints'] = newPsHints + return self._setMathData(data) + + __rmul__ = __mul__ + + def __div__(self, factor): + assert isinstance(factor, (int, float, tuple)), "Glyphs can only be divided by int, float or a 2-tuple." + # calculate reverse factor, and cause nice ZeroDivisionError if it can't + if isinstance(factor, tuple): + reverse = 1.0/factor[0], 1.0/factor[1] + else: + reverse = 1.0/factor + return self.__mul__(reverse) + + def __add__(self, other): + assert isinstance(other, BaseGlyph), "Glyphs can only be added to other glyphs." + data = self._processMathOne(other, addPt) + data['width'] = self.width + other.width + return self._setMathData(data) + + def __sub__(self, other): + assert isinstance(other, BaseGlyph), "Glyphs can only be substracted from other glyphs." + data = self._processMathOne(other, subPt) + data['width'] = self.width + other.width + return self._setMathData(data) + + # + # Interpolation + # + + def interpolate(self, factor, minGlyph, maxGlyph, suppressError=True, analyzeOnly=False): + """Traditional interpolation method. Interpolates by factor between minGlyph and maxGlyph. + suppressError will supress all tracebacks and analyze only will not perform the interpolation + but it will analyze all glyphs and return a dict of problems.""" + if not isinstance(factor, tuple): + factor = factor, factor + fatalError = False + if analyzeOnly: + ok, errors = minGlyph.isCompatible(maxGlyph) + return ok, errors + minData = None + maxData = None + minName = minGlyph.name + maxName = maxGlyph.name + try: + minData = minGlyph._getMathData() + maxData = maxGlyph._getMathData() + newContours = self._interpolateContours(factor, minData['contours'], maxData['contours']) + newComponents = self._interpolateComponents(factor, minData['components'], maxData['components']) + newAnchors = self._interpolateAnchors(factor, minData['anchors'], maxData['anchors']) + newWidth = _interpolate(minGlyph.width, maxGlyph.width, factor[0]) + newData = { + 'contours':newContours, + 'components':newComponents, + 'anchors':newAnchors, + 'width':newWidth + } + self._setMathData(newData, self) + except IndexError: + if not suppressError: + ok, errors = minGlyph.isCompatible(maxGlyph) + ok = not ok + return ok, errors + self.update() + return False, [] + + def isCompatible(self, otherGlyph, report=True): + """Return a bool value if the glyph is compatible with otherGlyph. + With report = True, isCompatible will return a report of what's wrong. + The interpolate method requires absolute equality between contour data. + Absolute equality is preferred among component and anchor data, but + it is NOT required. Interpolation between components and anchors + will only deal with compatible data and incompatible data will be + ignored. This method reflects this system.""" + selfName = self.name + selfData = self._getMathData() + otherName = otherGlyph.name + otherData = otherGlyph._getMathData() + compatible, errors = self._isCompatibleInternal(selfName, otherName, selfData, otherData) + if report: + return compatible, errors + return compatible + + def _isCompatibleInternal(self, selfName, otherName, selfData, otherData): + fatalError = False + errors = [] + ## contours + # any contour incompatibilities + # result in fatal errors + selfContours = selfData['contours'] + otherContours = otherData['contours'] + if len(selfContours) != len(otherContours): + fatalError = True + errors.append("Fatal error: glyph %s and glyph %s don't have the same number of contours." %(selfName, otherName)) + else: + for contourIndex in xrange(len(selfContours)): + selfContour = selfContours[contourIndex] + otherContour = otherContours[contourIndex] + if len(selfContour) != len(otherContour): + fatalError = True + errors.append("Fatal error: contour %d in glyph %s and glyph %s don't have the same number of segments." %(contourIndex, selfName, otherName)) + ## components + # component incompatibilities + # do not result in fatal errors + selfComponents = selfData['components'] + otherComponents = otherData['components'] + if len(selfComponents) != len(otherComponents): + errors.append("Error: glyph %s and glyph %s don't have the same number of components." %(selfName, otherName)) + for componentIndex in xrange(min(len(selfComponents), len(otherComponents))): + selfBaseName, selfTransformation = selfComponents[componentIndex] + otherBaseName, otherTransformation = otherComponents[componentIndex] + if selfBaseName != otherBaseName: + errors.append("Error: component %d in glyph %s and glyph %s don't have the same base glyph." %(componentIndex, selfName, otherName)) + ## anchors + # anchor incompatibilities + # do not result in fatal errors + selfAnchors = selfData['anchors'] + otherAnchors = otherData['anchors'] + if len(selfAnchors) != len(otherAnchors): + errors.append("Error: glyph %s and glyph %s don't have the same number of anchors." %(selfName, otherName)) + for anchorIndex in xrange(min(len(selfAnchors), len(otherAnchors))): + selfPt, selfAnchorName = selfAnchors[anchorIndex] + otherPt, otherAnchorName = otherAnchors[anchorIndex] + if selfAnchorName != otherAnchorName: + errors.append("Error: anchor %d in glyph %s and glyph %s don't have the same name." %(anchorIndex, selfName, otherName)) + return not fatalError, errors + + def _interpolateContours(self, factor, minContours, maxContours): + newContours = [] + for contourIndex in xrange(len(minContours)): + minContour = minContours[contourIndex] + maxContour = maxContours[contourIndex] + newContours.append([]) + for pointIndex in xrange(len(minContour)): + segType, pt, smooth, name = minContour[pointIndex] + minPoint = minContour[pointIndex][1] + maxPoint = maxContour[pointIndex][1] + newX, newY = _interpolatePt(minPoint, maxPoint, factor) + newContours[-1].append((segType, (newX, newY), smooth, name)) + return newContours + + def _interpolateComponents(self, factor, minComponents, maxComponents): + newComponents = [] + minComponents, maxComponents = self._mathComponentCompare(minComponents, maxComponents) + componentNames = minComponents.keys() + for componentName in componentNames: + minComponentList = minComponents[componentName] + maxComponentList = maxComponents[componentName] + for i in xrange(len(minComponentList)): + # transformation breakdown: xScale, xyScale, yxScale, yScale, xOffset, yOffset + minXScale, minXYScale, minYXScale, minYScale, minXOffset, minYOffset = minComponentList[i] + maxXScale, maxXYScale, maxYXScale, maxYScale, maxXOffset, maxYOffset = maxComponentList[i] + newXScale, newXYScale = _interpolatePt((minXScale, minXYScale), (maxXScale, maxXYScale), factor) + newYXScale, newYScale = _interpolatePt((minYXScale, minYScale), (maxYXScale, maxYScale), factor) + newXOffset, newYOffset = _interpolatePt((minXOffset, minYOffset), (maxXOffset, maxYOffset), factor) + newComponents.append((componentName, (newXScale, newXYScale, newYXScale, newYScale, newXOffset, newYOffset))) + return newComponents + + def _interpolateAnchors(self, factor, minAnchors, maxAnchors): + newAnchors = [] + minAnchors, maxAnchors = self._mathAnchorCompare(minAnchors, maxAnchors) + anchorNames = minAnchors.keys() + for anchorName in anchorNames: + minAnchorList = minAnchors[anchorName] + maxAnchorList = maxAnchors[anchorName] + for i in range(len(minAnchorList)): + minAnchor = minAnchorList[i] + maxAnchor = maxAnchorList[i] + newAnchor = _interpolatePt(minAnchor, maxAnchor, factor) + newAnchors.append((newAnchor, anchorName)) + return newAnchors + + # + # comparisons + # + + def __eq__(self, other): + if isinstance(other, BaseGlyph): + return self._getDigest() == other._getDigest() + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def _getDigest(self, pointsOnly=False): + """Calculate a digest of coordinates, points, things in this glyph. + With pointsOnly == True the digest consists of a flat tuple of all + coordinate pairs in the glyph, without the order of contours. + """ + from robofab.pens.digestPen import DigestPointPen + mp = DigestPointPen() + self.drawPoints(mp) + if pointsOnly: + return "%s|%d|%s"%(mp.getDigestPointsOnly(), self.width, self.unicode) + else: + return "%s|%d|%s"%(mp.getDigest(), self.width, self.unicode) + + def _getStructure(self): + """Calculate a digest of points, things in this glyph, but NOT coordinates.""" + from robofab.pens.digestPen import DigestPointStructurePen + mp = DigestPointStructurePen() + self.drawPoints(mp) + return mp.getDigest() + + def _hasChanged(self): + """mark the object and it's parent as changed""" + self.setChanged(True) + if self.getParent() is not None: + self.getParent()._hasChanged() + + def _get_box(self): + bounds = _box(self, fontObject=self.getParent()) + return bounds + + box = property(_get_box, doc="the bounding box of the glyph: (xMin, yMin, xMax, yMax)") + + def _get_leftMargin(self): + if self.isEmpty(): + return 0 + xMin, yMin, xMax, yMax = self.box + return xMin + + def _set_leftMargin(self, value): + if self.isEmpty(): + self.width = self.width + value + else: + diff = value - self.leftMargin + self.move((diff, 0)) + self.width = self.width + diff + + leftMargin = property(_get_leftMargin, _set_leftMargin, doc="the left margin") + + def _get_rightMargin(self): + if self.isEmpty(): + return self.width + xMin, yMin, xMax, yMax = self.box + return self.width - xMax + + def _set_rightMargin(self, value): + if self.isEmpty(): + self.width = value + else: + xMin, yMin, xMax, yMax = self.box + self.width = xMax + value + + rightMargin = property(_get_rightMargin, _set_rightMargin, doc="the right margin") + + def copy(self, aParent=None): + """Duplicate this glyph""" + n = self.__class__() + if aParent is not None: + n.setParent(aParent) + dont = ['_object', 'getParent'] + for k in self.__dict__.keys(): + ok = True + if k in dont: + continue + elif k == "contours": + dup = [] + for i in self.contours: + dup.append(i.copy(n)) + elif k == "components": + dup = [] + for i in self.components: + dup.append(i.copy(n)) + elif k == "anchors": + dup = [] + for i in self.anchors: + dup.append(i.copy(n)) + elif k == "psHints": + dup = self.psHints.copy() + elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)): + dup = self.__dict__[k].copy(n) + else: + dup = copy.deepcopy(self.__dict__[k]) + if ok: + setattr(n, k, dup) + return n + + def _setParentTree(self): + """Set the parents of all contained and dependent objects (and their dependents) right.""" + for item in self.contours: + item.setParent(self) + item._setParentTree() + for item in self.components: + item.setParent(self) + for items in self.anchors: + item.setParent(self) + + def getGlyph(self, glyphName): + """Provided there is a font parent for this glyph, return a sibling glyph.""" + if glyphName == self.name: + return self + if self.getParent() is not None: + return self.getParent()[glyphName] + return None + + def getPen(self): + """Return a Pen object for creating an outline in this glyph.""" + from robofab.pens.adapterPens import SegmentToPointPen + return SegmentToPointPen(self.getPointPen()) + + def getPointPen(self): + """Return a PointPen object for creating an outline in this glyph.""" + raise NotImplementedError, "getPointPen() must be implemented by subclass" + + def deSelect(self): + """Set all selected attrs in glyph to False: for the glyph, components, anchors, points.""" + for a in self.anchors: + a.selected = False + for a in self.components: + a.selected = False + for c in self.contours: + for p in c.points: + p.selected = False + self.selected = False + + def isEmpty(self): + """return true if the glyph has no contours or components""" + if len(self.contours) + len(self.components) == 0: + return True + else: + return False + + def _saveToGlyphSet(self, glyphSet, glyphName=None, force=False): + """Save the glyph to GlyphSet, a private method that's part of the saving process.""" + # save stuff in the lib first + if force or self.changed: + if glyphName is None: + glyphName = self.name + glyphSet.writeGlyph(glyphName, self, self.drawPoints) + + def update(self): + """update the glyph""" + pass + + def draw(self, pen): + """draw the object with a RoboFab segment pen""" + try: + pen.setWidth(self.width) + if self.note is not None: + pen.setNote(self.note) + except AttributeError: + # FontTools pens don't have these methods + pass + for a in self.anchors: + a.draw(pen) + for c in self.contours: + c.draw(pen) + for c in self.components: + c.draw(pen) + try: + pen.doneDrawing() + except AttributeError: + # FontTools pens don't have a doneDrawing() method + pass + + def drawPoints(self, pen): + """draw the object with a point pen""" + for a in self.anchors: + a.drawPoints(pen) + for c in self.contours: + c.drawPoints(pen) + for c in self.components: + c.drawPoints(pen) + + def appendContour(self, aContour, offset=(0, 0)): + """append a contour to the glyph""" + x, y = offset + pen = self.getPointPen() + aContour.drawPoints(pen) + self.contours[-1].move((x, y)) + + def appendGlyph(self, aGlyph, offset=(0, 0)): + """append another glyph to the glyph""" + x, y = offset + pen = self.getPointPen() + #to handle the offsets, move the source glyph and then move it back! + aGlyph.move((x, y)) + aGlyph.drawPoints(pen) + aGlyph.move((-x, -y)) + + def round(self): + """round all coordinates in all contours, components and anchors""" + for n in self.contours: + n.round() + for n in self.components: + n.round() + for n in self.anchors: + n.round() + self.width = int(round(self.width)) + + def autoUnicodes(self): + """Using fontTools.agl, assign Unicode list to the glyph""" + from fontTools.agl import AGL2UV + if AGL2UV.has_key(self.name): + self.unicode = AGL2UV[self.name] + self._hasChanged() + + def pointInside(self, pt, evenOdd=0): + """determine if the point is in the black or white of the glyph""" + x, y = pt + from fontTools.pens.pointInsidePen import PointInsidePen + font = self.getParent() + piPen = PointInsidePen(glyphSet=font, testPoint=(x, y), evenOdd=evenOdd) + self.draw(piPen) + return piPen.getResult() + + def correctDirection(self, trueType=False): + """corect the direction of the contours in the glyph.""" + #this is a bit slow, but i'm not sure how much more it can be optimized. + #it also has a bug somewhere that is causeing some contours to be set incorrectly. + #try to run it on the copyright symbol to see the problem. hm. + # + #establish the default direction that an outer contour should follow + #i believe for TT this is clockwise and for PS it is counter + #i could be wrong about this, i need to double check. + from fontTools.pens.pointInsidePen import PointInsidePen + baseDirection = 0 + if trueType: + baseDirection = 1 + #we don't need to do all the work if the contour count is < 2 + count = len(self.contours) + if count == 0: + return + elif count == 1: + self.contours[0].clockwise = baseDirection + return + #store up needed before we start + #i think the .box calls are eating a big chunk of the time + contourDict = {} + for contourIndex in range(len(self.contours)): + contour = self.contours[contourIndex] + contourDict[contourIndex] = {'box':contour.box, 'dir':contour.clockwise, 'hit':[], 'notHit':[]} + #now, for every contour, determine which contours it intersects + #as we go, we will also store contours that it doesn't intersct + #and we store this value for both contours + allIndexes = contourDict.keys() + for contourIndex in allIndexes: + for otherContourIndex in allIndexes: + if otherContourIndex != contourIndex: + if contourIndex not in contourDict[otherContourIndex]['hit'] and contourIndex not in contourDict[otherContourIndex]['notHit']: + xMin1, yMin1, xMax1, yMax1 = contourDict[contourIndex]['box'] + xMin2, yMin2, xMax2, yMax2= contourDict[otherContourIndex]['box'] + hit, pos = sectRect((xMin1, yMin1, xMax1, yMax1), (xMin2, yMin2, xMax2, yMax2)) + if hit == 1: + contourDict[contourIndex]['hit'].append(otherContourIndex) + contourDict[otherContourIndex]['hit'].append(contourIndex) + else: + contourDict[contourIndex]['notHit'].append(otherContourIndex) + contourDict[otherContourIndex]['notHit'].append(contourIndex) + #set up the pen here to shave a bit of time + font = self.getParent() + piPen = PointInsidePen(glyphSet=font, testPoint=(0, 0), evenOdd=0) + #now do the pointInside work + for contourIndex in allIndexes: + direction = baseDirection + contour = self.contours[contourIndex] + startPoint = contour.segments[0].onCurve + if startPoint is not None: #skip TT paths with no onCurve + if len(contourDict[contourIndex]['hit']) != 0: + for otherContourIndex in contourDict[contourIndex]['hit']: + piPen.setTestPoint(testPoint=(startPoint.x, startPoint.y)) + otherContour = self.contours[otherContourIndex] + otherContour.draw(piPen) + direction = direction + piPen.getResult() + newDirection = direction % 2 + #now set the direction if we need to + if newDirection != contourDict[contourIndex]['dir']: + contour.reverseContour() + + def autoContourOrder(self): + """attempt to sort the contours based on their centers""" + # sort is based on (in this order): + # - the (negative) point count + # - the (negative) segment count + # - fuzzy x value of the center of the contour + # - fuzzy y value of the center of the contour + # - the (negative) surface of the bounding box of the contour: width * height + # the latter is a safety net for for instances like a very thin 'O' where the + # x centers could be close enough to rely on the y for the sort which could + # very well be the same for both contours. We use the _negative_ of the surface + # to ensure that larger contours appear first, which seems more natural. + tempContourList = [] + contourList = [] + xThreshold = None + yThreshold = None + for contour in self.contours: + xMin, yMin, xMax, yMax = contour.box + width = xMax - xMin + height = yMax - yMin + xC = 0.5 * (xMin + xMax) + yC = 0.5 * (yMin + yMax) + xTh = abs(width * .5) + yTh = abs(height * .5) + if xThreshold is None or xThreshold > xTh: + xThreshold = xTh + if yThreshold is None or yThreshold > yTh: + yThreshold = yTh + tempContourList.append((-len(contour.points), -len(contour.segments), xC, yC, -(width * height), contour)) + for points, segments, x, y, surface, contour in tempContourList: + contourList.append((points, segments, FuzzyNumber(x, xThreshold), FuzzyNumber(y, yThreshold), surface, contour)) + contourList.sort() + for i in range(len(contourList)): + points, segments, xO, yO, surface, contour = contourList[i] + contour.index = i + + def rasterize(self, cellSize=50, xMin=None, yMin=None, xMax=None, yMax=None): + """ + Slice the glyph into a grid based on the cell size. + It returns a list of lists containing bool values + that indicate the black (True) or white (False) + value of that particular cell. These lists are + arranged from top to bottom of the glyph and + proceed from left to right. + This is an expensive operation! + """ + from fontTools.pens.pointInsidePen import PointInsidePen + piPen = PointInsidePen(glyphSet=self.getParent(), testPoint=(0, 0), evenOdd=0) + if xMin is None or yMin is None or xMax is None or yMax is None: + _xMin, _yMin, _xMax, _yMax = self.box + if xMin is None: + xMin = _xMin + if yMin is None: + yMin = _yMin + if xMax is None: + xMax = _xMax + if yMax is None: + yMax = _yMax + # + hitXMax = False + hitYMin = False + xSlice = 0 + ySlice = 0 + halfCellSize = cellSize / 2.0 + # + map = [] + # + while not hitYMin: + map.append([]) + yScan = -(ySlice * cellSize) + yMax - halfCellSize + if yScan < yMin: + hitYMin = True + while not hitXMax: + xScan = (xSlice * cellSize) + xMin - halfCellSize + if xScan > xMax: + hitXMax = True + piPen.setTestPoint((xScan, yScan)) + self.draw(piPen) + test = piPen.getResult() + if test: + map[-1].append(True) + else: + map[-1].append(False) + xSlice = xSlice + 1 + hitXMax = False + xSlice = 0 + ySlice = ySlice + 1 + return map + + def move(self, pt, contours=True, components=True, anchors=True): + """Move a glyph's items that are flagged as True""" + x, y = roundPt(pt) + if contours: + for contour in self.contours: + contour.move((x, y)) + if components: + for component in self.components: + component.move((x, y)) + if anchors: + for anchor in self.anchors: + anchor.move((x, y)) + + def scale(self, pt, center=(0, 0)): + """scale the glyph""" + x, y = pt + for contour in self.contours: + contour.scale((x, y), center=center) + for component in self.components: + offset = component.offset + component.offset = _scalePointFromCenter(offset, pt, center) + sX, sY = component.scale + component.scale = (sX*x, sY*y) + for anchor in self.anchors: + anchor.scale((x, y), center=center) + + def transform(self, matrix): + """Transform this glyph. + Use a Transform matrix object from + robofab.transform""" + n = [] + for c in self.contours: + c.transform(matrix) + for a in self.anchors: + a.transform(matrix) + + def rotate(self, angle, offset=None): + """rotate the glyph""" + from fontTools.misc.transform import Identity + radAngle = angle / DEGREE # convert from degrees to radians + if offset is None: + offset = (0,0) + rT = Identity.translate(offset[0], offset[1]) + rT = rT.rotate(radAngle) + rT = rT.translate(-offset[0], -offset[1]) + self.transform(rT) + + def skew(self, angle, offset=None): + """skew the glyph""" + from fontTools.misc.transform import Identity + radAngle = angle / DEGREE # convert from degrees to radians + if offset is None: + offset = (0,0) + rT = Identity.translate(offset[0], offset[1]) + rT = rT.skew(radAngle) + self.transform(rT) + + +class BaseContour(RBaseObject): + + """Base class for all contour objects.""" + + def __init__(self): + RBaseObject.__init__(self) + #self.index = None + self.changed = False # if the object needs to be saved + + def __repr__(self): + font = "unnamed_font" + glyph = "unnamed_glyph" + glyphParent = self.getParent() + if glyphParent is not None: + try: + glyph = glyphParent.name + except AttributeError: pass + fontParent = glyphParent.getParent() + if fontParent is not None: + try: + font = fontParent.info.postscriptFullName + except AttributeError: pass + try: + idx = `self.index` + except ValueError: + # XXXX + idx = "XXX" + return "<RContour for %s.%s[%s]>"%(font, glyph, idx) + + def __len__(self): + return len(self.segments) + + def __mul__(self, factor): + warn("Contour math has been deprecated and is slated for removal.", DeprecationWarning) + n = self.copy() + n.segments = [] + for i in range(len(self.segments)): + n.segments.append(self.segments[i] * factor) + n._setParentTree() + return n + + __rmul__ = __mul__ + + def __add__(self, other): + warn("Contour math has been deprecated and is slated for removal.", DeprecationWarning) + n = self.copy() + n.segments = [] + for i in range(len(self.segments)): + n.segments.append(self.segments[i] + other.segments[i]) + n._setParentTree() + return n + + def __sub__(self, other): + warn("Contour math has been deprecated and is slated for removal.", DeprecationWarning) + n = self.copy() + n.segments = [] + for i in range(len(self.segments)): + n.segments.append(self.segments[i] - other.segments[i]) + n._setParentTree() + return n + + def __getitem__(self, index): + return self.segments[index] + + def _hasChanged(self): + """mark the object and it's parent as changed""" + self.setChanged(True) + if self.getParent() is not None: + self.getParent()._hasChanged() + + def _nextSegment(self, segmentIndex): + return self.segments[(segmentIndex + 1) % len(self.segments)] + + def _prevSegment(self, segmentIndex): + segments = self.segments + return self.segments[(segmentIndex - 1) % len(self.segments)] + + def _get_box(self): + bounds = _box(self) + return bounds + + box = property(_get_box, doc="the bounding box for the contour") + + def _set_clockwise(self, value): + if self.clockwise != value: + self.reverseContour() + + def _get_clockwise(self): + pen = AreaPen(self) + self.draw(pen) + return pen.value < 0 + + clockwise = property(_get_clockwise, _set_clockwise, doc="direction of contour: positive=counterclockwise negative=clockwise") + + def copy(self, aParent=None): + """Duplicate this contour""" + n = self.__class__() + if aParent is not None: + n.setParent(aParent) + elif self.getParent() is not None: + n.setParent(self.getParent()) + dont = ['_object', 'points', 'bPoints', 'getParent'] + for k in self.__dict__.keys(): + ok = True + if k in dont: + continue + elif k == "segments": + dup = [] + for i in self.segments: + dup.append(i.copy(n)) + elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)): + dup = self.__dict__[k].copy(n) + else: + dup = copy.deepcopy(self.__dict__[k]) + if ok: + setattr(n, k, dup) + return n + + def _setParentTree(self): + """Set the parents of all contained and dependent objects (and their dependents) right.""" + for item in self.segments: + item.setParent(self) + + def round(self): + """round the value of all points in the contour""" + for n in self.points: + n.round() + + def draw(self, pen): + """draw the object with a fontTools pen""" + firstOn = self.segments[0].onCurve + firstType = self.segments[0].type + lastOn = self.segments[-1].onCurve + # this is a special exception for FontLab + # FL can have a contour that does not contain a move. + # this will only happen if the contour begins with a qcurve. + # in this case, we move to the segment's on curve, + # then we iterate through the rest of the points, + # then we add the first qcurve and finally we + # close the path. after this, i say "ugh." + if firstType == QCURVE: + pen.moveTo((firstOn.x, firstOn.y)) + for segment in self.segments[1:]: + segmentType = segment.type + pt = segment.onCurve.x, segment.onCurve.y + if segmentType == LINE: + pen.lineTo(pt) + elif segmentType == CURVE: + pts = [(point.x, point.y) for point in segment.points] + pen.curveTo(*pts) + elif segmentType == QCURVE: + pts = [(point.x, point.y) for point in segment.points] + pen.qCurveTo(*pts) + else: + assert 0, "unsupported segment type" + pts = [(point.x, point.y) for point in self.segments[0].points] + pen.qCurveTo(*pts) + pen.closePath() + else: + if firstType == MOVE and (firstOn.x, firstOn.y) == (lastOn.x, lastOn.y): + closed = True + else: + closed = True + for segment in self.segments: + segmentType = segment.type + pt = segment.onCurve.x, segment.onCurve.y + if segmentType == MOVE: + pen.moveTo(pt) + elif segmentType == LINE: + pen.lineTo(pt) + elif segmentType == CURVE: + pts = [(point.x, point.y) for point in segment.points] + pen.curveTo(*pts) + elif segmentType == QCURVE: + pts = [(point.x, point.y) for point in segment.points] + pen.qCurveTo(*pts) + else: + assert 0, "unsupported segment type" + if closed: + pen.closePath() + else: + pen.endPath() + + def drawPoints(self, pen): + """draw the object with a point pen""" + pen.beginPath() + lastOn = self.segments[-1].onCurve + didLastOn = False + flQCurveException = False + lastIndex = len(self.segments) - 1 + for i in range(len(self.segments)): + segment = self.segments[i] + segmentType = segment.type + # the new protocol states that we start with an onCurve + # so, if we have a move and a nd a last point overlapping, + # add the last point to the beginning and skip the move + if segmentType == MOVE and (segment.onCurve.x, segment.onCurve.y) == (lastOn.x, lastOn.y): + point = self.segments[-1].onCurve + name = getattr(segment.onCurve, 'name', None) + pen.addPoint((point.x, point.y), point.type, smooth=self.segments[-1].smooth, name=name) + didLastOn = True + continue + # this is an exception for objectsFL + # the problem is that quad contours are + # represented differently that they are in + # objectsRF: + # FL: [qcurve, qcurve, qcurve, qcurve] + # RF: [move, qcurve, qcurve, qcurve, qcurve] + # so, we need to catch this, and shift the offCurves to + # to the end of the contour + if i == 0 and segmentType == QCURVE: + flQCurveException = True + if segmentType == MOVE: + segmentType = LINE + ## the offCurves + if i == 0 and flQCurveException: + pass + else: + for point in segment.offCurve: + name = getattr(point, 'name', None) + pen.addPoint((point.x, point.y), segmentType=None, smooth=None, name=name, selected=point.selected) + ## the onCurve + # skip the last onCurve if it was used as the move + if i == lastIndex and didLastOn: + continue + point = segment.onCurve + name = getattr(point, 'name', None) + pen.addPoint((point.x, point.y), segmentType, smooth=segment.smooth, name=name, selected=point.selected) + # if we have the special qCurve case with objectsFL + # take care of the offCurves associated with the first contour + if flQCurveException: + for point in self.segments[0].offCurve: + name = getattr(point, 'name', None) + pen.addPoint((point.x, point.y), segmentType=None, smooth=None, name=name, selected=point.selected) + pen.endPath() + + def move(self, pt): + """move the contour""" + #this will be faster if we go straight to the points + for point in self.points: + point.move(pt) + + def scale(self, pt, center=(0, 0)): + """scale the contour""" + #this will be faster if we go straight to the points + for point in self.points: + point.scale(pt, center=center) + + def transform(self, matrix): + """Transform this contour. + Use a Transform matrix object from + robofab.transform""" + n = [] + for s in self.segments: + s.transform(matrix) + + def rotate(self, angle, offset=None): + """rotate the contour""" + from fontTools.misc.transform import Identity + radAngle = angle / DEGREE # convert from degrees to radians + if offset is None: + offset = (0,0) + rT = Identity.translate(offset[0], offset[1]) + rT = rT.rotate(radAngle) + self.transform(rT) + + def skew(self, angle, offset=None): + """skew the contour""" + from fontTools.misc.transform import Identity + radAngle = angle / DEGREE # convert from degrees to radians + if offset is None: + offset = (0,0) + rT = Identity.translate(offset[0], offset[1]) + rT = rT.skew(radAngle) + self.transform(rT) + + def pointInside(self, pt, evenOdd=0): + """determine if the point is inside or ouside of the contour""" + from fontTools.pens.pointInsidePen import PointInsidePen + glyph = self.getParent() + font = glyph.getParent() + piPen = PointInsidePen(glyphSet=font, testPoint=pt, evenOdd=evenOdd) + self.draw(piPen) + return piPen.getResult() + + def autoStartSegment(self): + """automatically set the lower left point of the contour as the first point.""" + #adapted from robofog + startIndex = 0 + startSegment = self.segments[0] + for i in range(len(self.segments)): + segment = self.segments[i] + startOn = startSegment.onCurve + on = segment.onCurve + if on.y <= startOn.y: + if on.y == startOn.y: + if on.x < startOn.x: + startSegment = segment + startIndex = i + else: + startSegment = segment + startIndex = i + if startIndex != 0: + self.setStartSegment(startIndex) + + def appendBPoint(self, pointType, anchor, bcpIn=(0, 0), bcpOut=(0, 0)): + """append a bPoint to the contour""" + self.insertBPoint(len(self.segments), pointType=pointType, anchor=anchor, bcpIn=bcpIn, bcpOut=bcpOut) + + def insertBPoint(self, index, pointType, anchor, bcpIn=(0, 0), bcpOut=(0, 0)): + """insert a bPoint at index on the contour""" + #insert a CURVE point that we can work with + nextSegment = self._nextSegment(index-1) + if nextSegment.type == QCURVE: + return + if nextSegment.type == MOVE: + prevSegment = self.segments[index-1] + prevOn = prevSegment.onCurve + if bcpIn != (0, 0): + new = self.appendSegment(CURVE, [(prevOn.x, prevOn.y), absoluteBCPIn(anchor, bcpIn), anchor], smooth=False) + if pointType == CURVE: + new.smooth = True + else: + new = self.appendSegment(LINE, [anchor], smooth=False) + #if the user wants an outgoing bcp, we must add a CURVE ontop of the move + if bcpOut != (0, 0): + nextOn = nextSegment.onCurve + self.appendSegment(CURVE, [absoluteBCPOut(anchor, bcpOut), (nextOn.x, nextOn.y), (nextOn.x, nextOn.y)], smooth=False) + else: + #handle the bcps + if nextSegment.type != CURVE: + prevSegment = self.segments[index-1] + prevOn = prevSegment.onCurve + prevOutX, prevOutY = (prevOn.x, prevOn.y) + else: + prevOut = nextSegment.offCurve[0] + prevOutX, prevOutY = (prevOut.x, prevOut.y) + self.insertSegment(index, segmentType=CURVE, points=[(prevOutX, prevOutY), anchor, anchor], smooth=False) + newSegment = self.segments[index] + prevSegment = self._prevSegment(index) + nextSegment = self._nextSegment(index) + if nextSegment.type == MOVE: + raise RoboFabError, 'still working out curving at the end of a contour' + elif nextSegment.type == QCURVE: + return + #set the new incoming bcp + newIn = newSegment.offCurve[1] + nIX, nIY = absoluteBCPIn(anchor, bcpIn) + newIn.x = nIX + newIn.y = nIY + #set the new outgoing bcp + hasCurve = True + if nextSegment.type != CURVE: + if bcpOut != (0, 0): + nextSegment.type = CURVE + hasCurve = True + else: + hasCurve = False + if hasCurve: + newOut = nextSegment.offCurve[0] + nOX, nOY = absoluteBCPOut(anchor, bcpOut) + newOut.x = nOX + newOut.y = nOY + #now check to see if we can convert the CURVE segment to a LINE segment + newAnchor = newSegment.onCurve + newA = newSegment.offCurve[0] + newB = newSegment.offCurve[1] + nextAnchor = nextSegment.onCurve + prevAnchor = prevSegment.onCurve + if (prevAnchor.x, prevAnchor.y) == (newA.x, newA.y) and (newAnchor.x, newAnchor.y) == (newB.x, newB.y): + newSegment.type = LINE + #the user wants a smooth segment + if pointType == CURVE: + newSegment.smooth = True + + +class BaseSegment(RBaseObject): + + """Base class for all segment objects""" + + def __init__(self): + self.changed = False + + def __repr__(self): + font = "unnamed_font" + glyph = "unnamed_glyph" + contourIndex = "unknown_contour" + contourParent = self.getParent() + if contourParent is not None: + try: + contourIndex = `contourParent.index` + except AttributeError: pass + glyphParent = contourParent.getParent() + if glyphParent is not None: + try: + glyph = glyphParent.name + except AttributeError: pass + fontParent = glyphParent.getParent() + if fontParent is not None: + try: + font = fontParent.info.postscriptFullName + except AttributeError: pass + try: + idx = `self.index` + except ValueError: + idx = "XXX" + return "<RSegment for %s.%s[%s][%s]>"%(font, glyph, contourIndex, idx) + + def __mul__(self, factor): + warn("Segment math has been deprecated and is slated for removal.", DeprecationWarning) + n = self.copy() + n.points = [] + for i in range(len(self.points)): + n.points.append(self.points[i] * factor) + n._setParentTree() + return n + + __rmul__ = __mul__ + + def __add__(self, other): + warn("Segment math has been deprecated and is slated for removal.", DeprecationWarning) + n = self.copy() + n.points = [] + for i in range(len(self.points)): + n.points.append(self.points[i] + other.points[i]) + return n + + def __sub__(self, other): + warn("Segment math has been deprecated and is slated for removal.", DeprecationWarning) + n = self.copy() + n.points = [] + for i in range(len(self.points)): + n.points.append(self.points[i] - other.points[i]) + return n + + def _hasChanged(self): + """mark the object and it's parent as changed""" + self.setChanged(True) + if self.getParent() is not None: + self.getParent()._hasChanged() + + def copy(self, aParent=None): + """Duplicate this segment""" + n = self.__class__() + if aParent is not None: + n.setParent(aParent) + elif self.getParent() is not None: + n.setParent(self.getParent()) + dont = ['_object', 'getParent', 'offCurve', 'onCurve'] + for k in self.__dict__.keys(): + ok = True + if k in dont: + continue + if k == "points": + dup = [] + for i in self.points: + dup.append(i.copy(n)) + elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)): + dup = self.__dict__[k].copy(n) + else: + dup = copy.deepcopy(self.__dict__[k]) + if ok: + setattr(n, k, dup) + return n + + def _setParentTree(self): + """Set the parents of all contained and dependent objects (and their dependents) right.""" + for item in self.points: + item.setParent(self) + + def round(self): + """round all points in the segment""" + for point in self.points: + point.round() + + def move(self, pt): + """move the segment""" + for point in self.points: + point.move(pt) + + def scale(self, pt, center=(0, 0)): + """scale the segment""" + for point in self.points: + point.scale(pt, center=center) + + def transform(self, matrix): + """Transform this segment. + Use a Transform matrix object from + robofab.transform""" + n = [] + for p in self.points: + p.transform(matrix) + + def _get_onCurve(self): + return self.points[-1] + + def _get_offCurve(self): + return self.points[:-1] + + offCurve = property(_get_offCurve, doc="on curve point for the segment") + onCurve = property(_get_onCurve, doc="list of off curve points for the segment") + + + +class BasePoint(RBaseObject): + + """Base class for point objects.""" + + def __init__(self): + #RBaseObject.__init__(self) + self.changed = False # if the object needs to be saved + self.selected = False + + def __repr__(self): + font = "unnamed_font" + glyph = "unnamed_glyph" + contourIndex = "unknown_contour" + segmentIndex = "unknown_segment" + segmentParent = self.getParent() + if segmentParent is not None: + try: + segmentIndex = `segmentParent.index` + except AttributeError: pass + contourParent = self.getParent().getParent() + if contourParent is not None: + try: + contourIndex = `contourParent.index` + except AttributeError: pass + glyphParent = contourParent.getParent() + if glyphParent is not None: + try: + glyph = glyphParent.name + except AttributeError: pass + fontParent = glyphParent.getParent() + if fontParent is not None: + try: + font = fontParent.info.postscriptFullName + except AttributeError: pass + return "<RPoint for %s.%s[%s][%s]>"%(font, glyph, contourIndex, segmentIndex) + + def __add__(self, other): + warn("Point math has been deprecated and is slated for removal.", DeprecationWarning) + #Add one point to another + n = self.copy() + n.x, n.y = addPt((self.x, self.y), (other.x, other.y)) + return n + + def __sub__(self, other): + warn("Point math has been deprecated and is slated for removal.", DeprecationWarning) + #Subtract one point from another + n = self.copy() + n.x, n.y = subPt((self.x, self.y), (other.x, other.y)) + return n + + def __mul__(self, factor): + warn("Point math has been deprecated and is slated for removal.", DeprecationWarning) + #Multiply the point with factor. Factor can be a tuple of 2 *(f1, f2) + n = self.copy() + n.x, n.y = mulPt((self.x, self.y), factor) + return n + + __rmul__ = __mul__ + + def _hasChanged(self): + #mark the object and it's parent as changed + self.setChanged(True) + if self.getParent() is not None: + self.getParent()._hasChanged() + + def copy(self, aParent=None): + """Duplicate this point""" + n = self.__class__() + if aParent is not None: + n.setParent(aParent) + elif self.getParent() is not None: + n.setParent(self.getParent()) + dont = ['getParent', 'offCurve', 'onCurve'] + for k in self.__dict__.keys(): + ok = True + if k in dont: + continue + elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)): + dup = self.__dict__[k].copy(n) + else: + dup = copy.deepcopy(self.__dict__[k]) + if ok: + setattr(n, k, dup) + return n + + def select(self, state=True): + """Set the selection of this point. + XXXX This method should be a lot more versatile, dealing with + different kinds of selection, select the bcp's seperately etc. + But that's for later when we need it more. For now it's just + one flag for the entire thing.""" + self.selected = state + + def round(self): + """round the values in the point""" + self.x, self.y = roundPt((self.x, self.y)) + + def move(self, pt): + """Move the point""" + self.x, self.y = addPt((self.x, self.y), pt) + + def scale(self, pt, center=(0, 0)): + """scale the point""" + nX, nY = _scalePointFromCenter((self.x, self.y), pt, center) + self.x = nX + self.y = nY + + def transform(self, matrix): + """Transform this point. Use a Transform matrix + object from fontTools.misc.transform""" + self.x, self.y = matrix.transformPoint((self.x, self.y)) + + +class BaseBPoint(RBaseObject): + + """Base class for bPoints objects.""" + + def __init__(self): + RBaseObject.__init__(self) + self.changed = False # if the object needs to be saved + self.selected = False + + def __repr__(self): + font = "unnamed_font" + glyph = "unnamed_glyph" + contourIndex = "unknown_contour" + segmentIndex = "unknown_segment" + segmentParent = self.getParent() + if segmentParent is not None: + try: + segmentIndex = `segmentParent.index` + except AttributeError: pass + contourParent = segmentParent.getParent() + if contourParent is not None: + try: + contourIndex = `contourParent.index` + except AttributeError: pass + glyphParent = contourParent.getParent() + if glyphParent is not None: + try: + glyph = glyphParent.name + except AttributeError: pass + fontParent = glyphParent.getParent() + if fontParent is not None: + try: + font = fontParent.info.postscriptFullName + except AttributeError: pass + return "<RBPoint for %s.%s[%s][%s][%s]>"%(font, glyph, contourIndex, segmentIndex, `self.index`) + + + def __add__(self, other): + warn("BPoint math has been deprecated and is slated for removal.", DeprecationWarning) + #Add one bPoint to another + n = self.copy() + n.anchor = addPt(self.anchor, other.anchor) + n.bcpIn = addPt(self.bcpIn, other.bcpIn) + n.bcpOut = addPt(self.bcpOut, other.bcpOut) + return n + + def __sub__(self, other): + warn("BPoint math has been deprecated and is slated for removal.", DeprecationWarning) + #Subtract one bPoint from another + n = self.copy() + n.anchor = subPt(self.anchor, other.anchor) + n.bcpIn = subPt(self.bcpIn, other.bcpIn) + n.bcpOut = subPt(self.bcpOut, other.bcpOut) + return n + + def __mul__(self, factor): + warn("BPoint math has been deprecated and is slated for removal.", DeprecationWarning) + #Multiply the bPoint with factor. Factor can be a tuple of 2 *(f1, f2) + n = self.copy() + n.anchor = mulPt(self.anchor, factor) + n.bcpIn = mulPt(self.bcpIn, factor) + n.bcpOut = mulPt(self.bcpOut, factor) + return n + + __rmul__ = __mul__ + + def _hasChanged(self): + #mark the object and it's parent as changed + self.setChanged(True) + if self.getParent() is not None: + self.getParent()._hasChanged() + + def select(self, state=True): + """Set the selection of this point. + XXXX This method should be a lot more versatile, dealing with + different kinds of selection, select the bcp's seperately etc. + But that's for later when we need it more. For now it's just + one flag for the entire thing.""" + self.selected = state + + def round(self): + """Round the coordinates to integers""" + self.anchor = roundPt(self.anchor) + pSeg = self._parentSegment + if pSeg.type != MOVE: + self.bcpIn = roundPt(self.bcpIn) + if pSeg.getParent()._nextSegment(pSeg.index).type != MOVE: + self.bcpOut = roundPt(self.bcpOut) + + def move(self, pt): + """move the bPoint""" + x, y = pt + bcpIn = self.bcpIn + bcpOut = self.bcpOut + self.anchor = (self.anchor[0] + x, self.anchor[1] + y) + pSeg = self._parentSegment + if pSeg.type != MOVE: + self.bcpIn = bcpIn + if pSeg.getParent()._nextSegment(pSeg.index).type != MOVE: + self.bcpOut = bcpOut + + def scale(self, pt, center=(0, 0)): + """scale the bPoint""" + x, y = pt + centerX, centerY = center + ogCenter = (centerX, centerY) + scaledCenter = (centerX * x, centerY * y) + shiftVal = (scaledCenter[0] - ogCenter[0], scaledCenter[1] - ogCenter[1]) + anchor = self.anchor + bcpIn = self.bcpIn + bcpOut = self.bcpOut + self.anchor = ((anchor[0] * x) - shiftVal[0], (anchor[1] * y) - shiftVal[1]) + pSeg = self._parentSegment + if pSeg.type != MOVE: + self.bcpIn = ((bcpIn[0] * x), (bcpIn[1] * y)) + if pSeg.getParent()._nextSegment(pSeg.index).type != MOVE: + self.bcpOut = ((bcpOut[0] * x), (bcpOut[1] * y)) + + def transform(self, matrix): + """Transform this point. Use a Transform matrix + object from fontTools.misc.transform""" + self.anchor = matrix.transformPoint(self.anchor) + pSeg = self._parentSegment + if pSeg.type != MOVE: + self.bcpIn = matrix.transformPoint(self.bcpIn) + if pSeg.getParent()._nextSegment(pSeg.index).type != MOVE: + self.bcpOut = matrix.transformPoint(self.bcpOut) + + def _get__anchorPoint(self): + return self._parentSegment.onCurve + + _anchorPoint = property(_get__anchorPoint, doc="the oncurve point in the parent segment") + + def _get_anchor(self): + point = self._anchorPoint + return (point.x, point.y) + + def _set_anchor(self, value): + x, y = value + point = self._anchorPoint + point.x = x + point.y = y + + anchor = property(_get_anchor, _set_anchor, doc="the position of the anchor") + + def _get_bcpIn(self): + pSeg = self._parentSegment + pCount = len(pSeg.offCurve) + if pCount == 2: + p = pSeg.offCurve[1] + pOn = pSeg.onCurve + return relativeBCPIn((pOn.x, pOn.y), (p.x, p.y)) + else: + return (0, 0) + + def _set_bcpIn(self, value): + x, y = (absoluteBCPIn(self.anchor, value)) + pSeg = self._parentSegment + if pSeg.type == MOVE: + #the user wants to have a bcp leading into the MOVE + if value == (0, 0) and self.bcpOut == (0, 0): + #we have a straight line between the two anchors + pass + else: + #we need to insert a new CURVE segment ontop of the move + contour = self._parentSegment.getParent() + #set the prev segment outgoing bcp to the onCurve + prevSeg = contour._prevSegment(self._parentSegment.index) + prevOn = prevSeg.onCurve + contour.appendSegment(CURVE, [(prevOn.x, prevOn.y), (x, y), self.anchor], smooth=False) + else: + pCount = len(pSeg.offCurve) + if pCount == 2: + #if the two points in the offCurvePoints list are located at the + #anchor coordinates we can switch to a LINE segment type + if value == (0, 0) and self.bcpOut == (0, 0): + pSeg.type = LINE + pSeg.smooth = False + else: + pSeg.offCurve[1].x = x + pSeg.offCurve[1].y = y + elif value != (0, 0): + pSeg.type = CURVE + pSeg.offCurve[1].x = x + pSeg.offCurve[1].y = y + + bcpIn = property(_get_bcpIn, _set_bcpIn, doc="the (x,y) for the incoming bcp") + + def _get_bcpOut(self): + pSeg = self._parentSegment + nextSeg = pSeg.getParent()._nextSegment(pSeg.index) + nsCount = len(nextSeg.offCurve) + if nsCount == 2: + p = nextSeg.offCurve[0] + return relativeBCPOut(self.anchor, (p.x, p.y)) + else: + return (0, 0) + + def _set_bcpOut(self, value): + x, y = (absoluteBCPOut(self.anchor, value)) + pSeg = self._parentSegment + nextSeg = pSeg.getParent()._nextSegment(pSeg.index) + if nextSeg.type == MOVE: + if value == (0, 0) and self.bcpIn == (0, 0): + pass + else: + #we need to insert a new CURVE segment ontop of the move + contour = self._parentSegment.getParent() + nextOn = nextSeg.onCurve + contour.appendSegment(CURVE, [(x, y), (nextOn.x, nextOn.y), (nextOn.x, nextOn.y)], smooth=False) + else: + nsCount = len(nextSeg.offCurve) + if nsCount == 2: + #if the two points in the offCurvePoints list are located at the + #anchor coordinates we can switch to a LINE segment type + if value == (0, 0) and self.bcpIn == (0, 0): + nextSeg.type = LINE + nextSeg.smooth = False + else: + nextSeg.offCurve[0].x = x + nextSeg.offCurve[0].y = y + elif value != (0, 0): + nextSeg.type = CURVE + nextSeg.offCurve[0].x = x + nextSeg.offCurve[0].y = y + + bcpOut = property(_get_bcpOut, _set_bcpOut, doc="the (x,y) for the outgoing bcp") + + def _get_type(self): + pType = self._parentSegment.type + bpType = CORNER + if pType == CURVE: + if self._parentSegment.smooth: + bpType = CURVE + return bpType + + def _set_type(self, pointType): + pSeg = self._parentSegment + segType = pSeg.type + #user wants a curve where there is a line + if pointType == CURVE and segType == LINE: + pSeg.type = CURVE + pSeg.smooth = True + #the anchor is a curve segment. so, all we need to do is turn the smooth off + elif pointType == CORNER and segType == CURVE: + pSeg.smooth = False + + type = property(_get_type, _set_type, doc="the type of bPoint, either 'corner' or 'curve'") + + +class BaseComponent(RBaseObject): + + """Base class for all component objects.""" + + def __init__(self): + RBaseObject.__init__(self) + self.changed = False # if the object needs to be saved + self.selected = False + + def __repr__(self): + font = "unnamed_font" + glyph = "unnamed_glyph" + glyphParent = self.getParent() + if glyphParent is not None: + try: + glyph = glyphParent.name + except AttributeError: pass + fontParent = glyphParent.getParent() + if fontParent is not None: + try: + font = fontParent.info.postscriptFullName + except AttributeError: pass + return "<RComponent for %s.%s.components[%s]>"%(font, glyph, `self.index`) + + def _hasChanged(self): + """mark the object and it's parent as changed""" + self.setChanged(True) + if self.getParent() is not None: + self.getParent()._hasChanged() + + def copy(self, aParent=None): + """Duplicate this component.""" + n = self.__class__() + if aParent is not None: + n.setParent(aParent) + elif self.getParent() is not None: + n.setParent(self.getParent()) + dont = ['getParent', '_object'] + for k in self.__dict__.keys(): + if k in dont: + continue + elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)): + dup = self.__dict__[k].copy(n) + else: + dup = copy.deepcopy(self.__dict__[k]) + setattr(n, k, dup) + return n + + def __add__(self, other): + warn("Component math has been deprecated and is slated for removal.", DeprecationWarning) + #Add one Component to another + n = self.copy() + n.offset = addPt(self.offset, other.offset) + n.scale = addPt(self.scale, other.scale) + return n + + def __sub__(self, other): + warn("Component math has been deprecated and is slated for removal.", DeprecationWarning) + #Subtract one Component from another + n = self.copy() + n.offset = subPt(self.offset, other.offset) + n.scale = subPt(self.scale, other.scale) + return n + + def __mul__(self, factor): + warn("Component math has been deprecated and is slated for removal.", DeprecationWarning) + #Multiply the Component with factor. Factor can be a tuple of 2 *(f1, f2) + n = self.copy() + n.offset = mulPt(self.offset, factor) + n.scale = mulPt(self.scale, factor) + return n + + __rmul__ = __mul__ + + def _get_box(self): + parentGlyph = self.getParent() + # the component is an orphan + if parentGlyph is None: + return None + parentFont = parentGlyph.getParent() + # the glyph that contains the component + # does not hae a parent + if parentFont is None: + return None + # the font does not have a glyph + # that matches the glyph that + # this component references + if not parentFont.has_key(self.baseGlyph): + return None + return _box(self, parentFont) + + box = property(_get_box, doc="the bounding box of the component: (xMin, yMin, xMax, yMax)") + + def round(self): + """round the offset values""" + self.offset = roundPt(self.offset) + self._hasChanged() + + def draw(self, pen): + """Segment pen drawing method.""" + if isinstance(pen, AbstractPen): + # It's a FontTools pen, which for addComponent is identical + # to PointPen. + self.drawPoints(pen) + else: + # It's an "old" 'Fab pen + pen.addComponent(self.baseGlyph, self.offset, self.scale) + + def drawPoints(self, pen): + """draw the object with a point pen""" + oX, oY = self.offset + sX, sY = self.scale + #xScale, xyScale, yxScale, yScale, xOffset, yOffset + pen.addComponent(self.baseGlyph, (sX, 0, 0, sY, oX, oY)) + + +class BaseAnchor(RBaseObject): + + """Base class for all anchor point objects.""" + + def __init__(self): + RBaseObject.__init__(self) + self.changed = False # if the object needs to be saved + self.selected = False + + def __repr__(self): + font = "unnamed_font" + glyph = "unnamed_glyph" + glyphParent = self.getParent() + if glyphParent is not None: + try: + glyph = glyphParent.name + except AttributeError: pass + fontParent = glyphParent.getParent() + if fontParent is not None: + try: + font = fontParent.info.postscriptFullName + except AttributeError: pass + return "<RAnchor for %s.%s.anchors[%s]>"%(font, glyph, `self.index`) + + def __add__(self, other): + warn("Anchor math has been deprecated and is slated for removal.", DeprecationWarning) + #Add one anchor to another + n = self.copy() + n.x, n.y = addPt((self.x, self.y), (other.x, other.y)) + return n + + def __sub__(self, other): + warn("Anchor math has been deprecated and is slated for removal.", DeprecationWarning) + #Substract one anchor from another + n = self.copy() + n.x, n.y = subPt((self.x, self.y), (other.x, other.y)) + return n + + def __mul__(self, factor): + warn("Anchor math has been deprecated and is slated for removal.", DeprecationWarning) + #Multiply the anchor with factor. Factor can be a tuple of 2 *(f1, f2) + n = self.copy() + n.x, n.y = mulPt((self.x, self.y), factor) + return n + + __rmul__ = __mul__ + + def _hasChanged(self): + #mark the object and it's parent as changed + self.setChanged(True) + if self.getParent() is not None: + self.getParent()._hasChanged() + + def copy(self, aParent=None): + """Duplicate this anchor.""" + n = self.__class__() + if aParent is not None: + n.setParent(aParent) + elif self.getParent() is not None: + n.setParent(self.getParent()) + dont = ['getParent', '_object'] + for k in self.__dict__.keys(): + if k in dont: + continue + elif isinstance(self.__dict__[k], (RBaseObject, BaseLib)): + dup = self.__dict__[k].copy(n) + else: + dup = copy.deepcopy(self.__dict__[k]) + setattr(n, k, dup) + return n + + def round(self): + """round the values in the anchor""" + self.x, self.y = roundPt((self.x, self.y)) + self._hasChanged() + + def draw(self, pen): + """Draw the object onto a segment pen""" + if isinstance(pen, AbstractPen): + # It's a FontTools pen + pen.moveTo((self.x, self.y)) + pen.endPath() + else: + # It's an "old" 'Fab pen + pen.addAnchor(self.name, (self.x, self.y)) + + def drawPoints(self, pen): + """draw the object with a point pen""" + pen.beginPath() + pen.addPoint((self.x, self.y), segmentType="move", smooth=False, name=self.name) + pen.endPath() + + def move(self, pt): + """Move the anchor""" + x, y = pt + pX, pY = self.position + self.position = (pX+x, pY+y) + + def scale(self, pt, center=(0, 0)): + """scale the anchor""" + pos = self.position + self.position = _scalePointFromCenter(pos, pt, center) + + def transform(self, matrix): + """Transform this anchor. Use a Transform matrix + object from fontTools.misc.transform""" + self.x, self.y = matrix.transformPoint((self.x, self.y)) + + +class BaseGuide(RBaseObject): + + """Base class for all guide objects.""" + + def __init__(self): + RBaseObject.__init__(self) + self.changed = False # if the object needs to be saved + self.selected = False + + +class BaseInfo(RBaseObject): + + _baseAttributes = ["_object", "changed", "selected", "getParent"] + _deprecatedAttributes = ufoLib.deprecatedFontInfoAttributesVersion2 + _infoAttributes = ufoLib.fontInfoAttributesVersion2 + # subclasses may define a list of environment + # specific attributes that can be retrieved or set. + _environmentAttributes = [] + # subclasses may define a list of attributes + # that should not follow the standard get/set + # order provided by __setattr__ and __getattr__. + # for these attributes, the environment specific + # set and get methods must handle this value + # without any pre-call validation. + # (yeah. this is because of some FontLab dumbness.) + _environmentOverrides = [] + + def __setattr__(self, attr, value): + # check to see if the attribute has been + # deprecated. if so, warn the caller and + # update the attribute and value. + if attr in self._deprecatedAttributes: + newAttr, newValue = ufoLib.convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value) + note = "The %s attribute has been deprecated. Use the new %s attribute." % (attr, newAttr) + warn(note, DeprecationWarning) + attr = newAttr + value = newValue + # setting a known attribute + if attr in self._infoAttributes or attr in self._environmentAttributes: + # lightly test the validity of the value + if value is not None: + isValidValue = ufoLib.validateFontInfoVersion2ValueForAttribute(attr, value) + if not isValidValue: + raise RoboFabError("Invalid value (%s) for attribute (%s)." % (repr(value), attr)) + # use the environment specific info attr set + # method if it is defined. + if hasattr(self, "_environmentSetAttr"): + self._environmentSetAttr(attr, value) + # fallback to super + else: + super(BaseInfo, self).__setattr__(attr, value) + # unknown attribute, test to see if it is a python attr + elif attr in self.__dict__ or attr in self._baseAttributes: + super(BaseInfo, self).__setattr__(attr, value) + # raise an attribute error + else: + raise AttributeError("Unknown attribute %s." % attr) + + # subclasses with environment specific attr setting can + # implement this method. __setattr__ will call it if present. + # def _environmentSetAttr(self, attr, value): + # pass + + def __getattr__(self, attr): + if attr in self._environmentOverrides: + return self._environmentGetAttr(attr) + # check to see if the attribute has been + # deprecated. if so, warn the caller and + # flag the value as needing conversion. + needValueConversionTo1 = False + if attr in self._deprecatedAttributes: + oldAttr = attr + oldValue = attr + newAttr, x = ufoLib.convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, None) + note = "The %s attribute has been deprecated. Use the new %s attribute." % (attr, newAttr) + warn(note, DeprecationWarning) + attr = newAttr + needValueConversionTo1 = True + # getting a known attribute + if attr in self._infoAttributes or attr in self._environmentAttributes: + # use the environment specific info attr get + # method if it is defined. + if hasattr(self, "_environmentGetAttr"): + value = self._environmentGetAttr(attr) + # fallback to super + else: + try: + value = super(BaseInfo, self).__getattribute__(attr) + except AttributeError: + return None + if needValueConversionTo1: + oldAttr, value = ufoLib.convertFontInfoValueForAttributeFromVersion2ToVersion1(attr, value) + return value + # raise an attribute error + else: + raise AttributeError("Unknown attribute %s." % attr) + + # subclasses with environment specific attr retrieval can + # implement this method. __getattr__ will call it if present. + # it should return the requested value. + # def _environmentGetAttr(self, attr): + # pass + +class BaseFeatures(RBaseObject): + + def __init__(self): + RBaseObject.__init__(self) + self._text = "" + + def _get_text(self): + return self._text + + def _set_text(self, value): + assert isinstance(value, basestring) + self._text = value + + text = property(_get_text, _set_text, doc="raw feature text.") + + +class BaseGroups(dict): + + """Base class for all RFont.groups objects""" + + def __init__(self): + pass + + def __repr__(self): + font = "unnamed_font" + fontParent = self.getParent() + if fontParent is not None: + try: + font = fontParent.info.postscriptFullName + except AttributeError: pass + return "<RGroups for %s>"%font + + def getParent(self): + """this method will be overwritten with a weakref if there is a parent.""" + pass + + def setParent(self, parent): + import weakref + self.__dict__['getParent'] = weakref.ref(parent) + + def __setitem__(self, key, value): + #override base class to insure proper data is being stored + if not isinstance(key, str): + raise RoboFabError, 'key must be a string' + if not isinstance(value, list): + raise RoboFabError, 'group must be a list' + super(BaseGroups, self).__setitem__(key, value) + + def findGlyph(self, glyphName): + """return a list of all groups contianing glyphName""" + found = [] + for i in self.keys(): + l = self[i] + if glyphName in l: + found.append(i) + return found + + +class BaseLib(dict): + + """Base class for all lib objects""" + + def __init__(self): + pass + + def __repr__(self): + #this is a doozy! + parent = "unknown_parent" + parentObject = self.getParent() + if parentObject is not None: + #do we have a font? + try: + parent = parentObject.info.postscriptFullName + except AttributeError: + #or do we have a glyph? + try: + parent = parentObject.name + #we must be an orphan + except AttributeError: pass + return "<RLib for %s>"%parent + + def getParent(self): + """this method will be overwritten with a weakref if there is a parent.""" + pass + + def setParent(self, parent): + import weakref + self.__dict__['getParent'] = weakref.ref(parent) + + def copy(self, aParent=None): + """Duplicate this lib.""" + n = self.__class__() + if aParent is not None: + n.setParent(aParent) + elif self.getParent() is not None: + n.setParent(self.getParent()) + for k in self.keys(): + n[k] = copy.deepcopy(self[k]) + return n + + +class BaseKerning(RBaseObject): + + """Base class for all kerning objects. Object behaves like a dict but has + some special kerning specific tricks.""" + + def __init__(self, kerningDict=None): + if not kerningDict: + kerningDict = {} + self._kerning = kerningDict + self.changed = False # if the object needs to be saved + + def __repr__(self): + font = "unnamed_font" + fontParent = self.getParent() + if fontParent is not None: + try: + font = fontParent.info.postscriptFullName + except AttributeError: pass + return "<RKerning for %s>"%font + + def __getitem__(self, key): + if isinstance(key, tuple): + pair = key + return self.get(pair) + elif isinstance(key, str): + raise RoboFabError, 'kerning pair must be a tuple: (left, right)' + else: + keys = self.keys() + if key > len(keys): + raise IndexError + keys.sort() + pair = keys[key] + if not self._kerning.has_key(pair): + raise IndexError + else: + return pair + + def __setitem__(self, pair, value): + if not isinstance(pair, tuple): + raise RoboFabError, 'kerning pair must be a tuple: (left, right)' + else: + if len(pair) != 2: + raise RoboFabError, 'kerning pair must be a tuple: (left, right)' + else: + if value == 0: + if self._kerning.get(pair) is not None: + del self._kerning[pair] + else: + self._kerning[pair] = value + self._hasChanged() + + def __len__(self): + return len(self._kerning.keys()) + + def _hasChanged(self): + """mark the object and it's parent as changed""" + self.setChanged(True) + if self.getParent() is not None: + self.getParent()._hasChanged() + + def keys(self): + """return list of kerning pairs""" + return self._kerning.keys() + + def values(self): + """return a list of kerning values""" + return self._kerning.values() + + def items(self): + """return a list of kerning items""" + return self._kerning.items() + + def has_key(self, pair): + return self._kerning.has_key(pair) + + def get(self, pair, default=None): + """get a value. return None if the pair does not exist""" + value = self._kerning.get(pair, default) + return value + + def remove(self, pair): + """remove a kerning pair""" + self[pair] = 0 + + def getAverage(self): + """return average of all kerning pairs""" + if len(self) == 0: + return 0 + value = 0 + for i in self.values(): + value = value + i + return value / float(len(self)) + + def getExtremes(self): + """return the lowest and highest kerning values""" + if len(self) == 0: + return 0 + values = self.values() + values.append(0) + values.sort() + return (values[0], values[-1]) + + def update(self, kerningDict): + """replace kerning data with the data in the given kerningDict""" + for pair in kerningDict.keys(): + self[pair] = kerningDict[pair] + + def clear(self): + """clear all kerning""" + self._kerning = {} + + def add(self, value): + """add value to all kerning pairs""" + for pair in self.keys(): + self[pair] = self[pair] + value + + def scale(self, value): + """scale all kernng pairs by value""" + for pair in self.keys(): + self[pair] = self[pair] * value + + def minimize(self, minimum=10): + """eliminate pairs with value less than minimum""" + for pair in self.keys(): + if abs(self[pair]) < minimum: + self[pair] = 0 + + def eliminate(self, leftGlyphsToEliminate=None, rightGlyphsToEliminate=None, analyzeOnly=False): + """eliminate pairs containing a left glyph that is in the leftGlyphsToEliminate list + or a right glyph that is in the rightGlyphsToELiminate list. + sideGlyphsToEliminate can be a string: 'a' or list: ['a', 'b']. + analyzeOnly will not remove pairs. it will return a count + of all pairs that would be removed.""" + if analyzeOnly: + count = 0 + lgte = leftGlyphsToEliminate + rgte = rightGlyphsToEliminate + if isinstance(lgte, str): + lgte = [lgte] + if isinstance(rgte, str): + rgte = [rgte] + for pair in self.keys(): + left, right = pair + if left in lgte or right in rgte: + if analyzeOnly: + count = count + 1 + else: + self[pair] = 0 + if analyzeOnly: + return count + else: + return None + + def interpolate(self, sourceDictOne, sourceDictTwo, value, clearExisting=True): + """interpolate the kerning between sourceDictOne + and sourceDictTwo. clearExisting will clear existing + kerning first.""" + if isinstance(value, tuple): + # in case the value is a x, y tuple: use the x only. + value = value[0] + if clearExisting: + self.clear() + pairs = set(sourceDictOne.keys()) | set(sourceDictTwo.keys()) + for pair in pairs: + s1 = sourceDictOne.get(pair, 0) + s2 = sourceDictTwo.get(pair, 0) + self[pair] = _interpolate(s1, s2, value) + + def round(self, multiple=10): + """round the kerning pair values to increments of multiple""" + for pair in self.keys(): + value = self[pair] + self[pair] = int(round(value / float(multiple))) * multiple + + def occurrenceCount(self, glyphsToCount): + """return a dict with glyphs as keys and the number of + occurances of that glyph in the kerning pairs as the value + glyphsToCount can be a string: 'a' or list: ['a', 'b']""" + gtc = glyphsToCount + if isinstance(gtc, str): + gtc = [gtc] + gtcDict = {} + for glyph in gtc: + gtcDict[glyph] = 0 + for pair in self.keys(): + left, right = pair + if not gtcDict.get(left): + gtcDict[left] = 0 + if not gtcDict.get(right): + gtcDict[right] = 0 + gtcDict[left] = gtcDict[left] + 1 + gtcDict[right] = gtcDict[right] + 1 + found = {} + for glyphName in gtc: + found[glyphName] = gtcDict[glyphName] + return found + + def getLeft(self, glyphName): + """Return a list of kerns with glyphName as left character.""" + hits = [] + for k, v in self.items(): + if k[0] == glyphName: + hits.append((k, v)) + return hits + + def getRight(self, glyphName): + """Return a list of kerns with glyphName as left character.""" + hits = [] + for k, v in self.items(): + if k[1] == glyphName: + hits.append((k, v)) + return hits + + def combine(self, kerningDicts, overwriteExisting=True): + """combine two or more kerning dictionaries. + overwrite exsisting duplicate pairs if overwriteExisting=True""" + if isinstance(kerningDicts, dict): + kerningDicts = [kerningDicts] + for kd in kerningDicts: + for pair in kd.keys(): + exists = self.has_key(pair) + if exists and overwriteExisting: + self[pair] = kd[pair] + elif not exists: + self[pair] = kd[pair] + + def swapNames(self, swapTable): + """change glyph names in all kerning pairs based on swapTable. + swapTable = {'BeforeName':'AfterName', ...}""" + for pair in self.keys(): + foundInstance = False + left, right = pair + if swapTable.has_key(left): + left = swapTable[left] + foundInstance = True + if swapTable.has_key(right): + right = swapTable[right] + foundInstance = True + if foundInstance: + self[(left, right)] = self[pair] + self[pair] = 0 + + def explodeClasses(self, leftClassDict=None, rightClassDict=None, analyzeOnly=False): + """turn class kerns into real kerning pairs. classes should + be defined in dicts: {'O':['C', 'G', 'Q'], 'H':['B', 'D', 'E', 'F', 'I']}. + analyzeOnly will not remove pairs. it will return a count + of all pairs that would be added""" + if not leftClassDict: + leftClassDict = {} + if not rightClassDict: + rightClassDict = {} + if analyzeOnly: + count = 0 + for pair in self.keys(): + left, right = pair + value = self[pair] + if leftClassDict.get(left) and rightClassDict.get(right): + allLeft = leftClassDict[left] + [left] + allRight = rightClassDict[right] + [right] + for leftSub in allLeft: + for rightSub in allRight: + if analyzeOnly: + count = count + 1 + else: + self[(leftSub, rightSub)] = value + elif leftClassDict.get(left) and not rightClassDict.get(right): + allLeft = leftClassDict[left] + [left] + for leftSub in allLeft: + if analyzeOnly: + count = count + 1 + else: + self[(leftSub, right)] = value + elif rightClassDict.get(right) and not leftClassDict.get(left): + allRight = rightClassDict[right] + [right] + for rightSub in allRight: + if analyzeOnly: + count = count + 1 + else: + self[(left, rightSub)] = value + if analyzeOnly: + return count + else: + return None + + def implodeClasses(self, leftClassDict=None, rightClassDict=None, analyzeOnly=False): + """condense the number of kerning pairs by applying classes. + this will eliminate all pairs containg the classed glyphs leaving + pairs that contain the key glyphs behind. analyzeOnly will not + remove pairs. it will return a count of all pairs that would be removed.""" + if not leftClassDict: + leftClassDict = {} + if not rightClassDict: + rightClassDict = {} + leftImplode = [] + rightImplode = [] + for value in leftClassDict.values(): + leftImplode = leftImplode + value + for value in rightClassDict.values(): + rightImplode = rightImplode + value + analyzed = self.eliminate(leftGlyphsToEliminate=leftImplode, rightGlyphsToEliminate=rightImplode, analyzeOnly=analyzeOnly) + if analyzeOnly: + return analyzed + else: + return None + + def importAFM(self, path, clearExisting=True): + """Import kerning pairs from an AFM file. clearExisting=True will + clear all exising kerning""" + from fontTools.afmLib import AFM + #a nasty hack to fix line ending problems + f = open(path, 'rb') + text = f.read().replace('\r', '\n') + f.close() + f = open(path, 'wb') + f.write(text) + f.close() + #/nasty hack + kerning = AFM(path)._kerning + if clearExisting: + self.clear() + for pair in kerning.keys(): + self[pair] = kerning[pair] + + def asDict(self, returnIntegers=True): + """return the object as a dictionary""" + if not returnIntegers: + return self._kerning + else: + #duplicate the kerning dict so that we aren't destroying it + kerning = {} + for pair in self.keys(): + kerning[pair] = int(round(self[pair])) + return kerning + + def __add__(self, other): + new = self.__class__() + k = set(self.keys()) | set(other.keys()) + for key in k: + new[key] = self.get(key, 0) + other.get(key, 0) + return new + + def __sub__(self, other): + new = self.__class__() + k = set(self.keys()) | set(other.keys()) + for key in k: + new[key] = self.get(key, 0) - other.get(key, 0) + return new + + def __mul__(self, factor): + new = self.__class__() + for name, value in self.items(): + new[name] = value * factor + return new + + __rmul__ = __mul__ + + def __div__(self, factor): + if factor == 0: + raise ZeroDivisionError + return self.__mul__(1.0/factor) + diff --git a/misc/pylib/robofab/objects/objectsFF.py b/misc/pylib/robofab/objects/objectsFF.py new file mode 100644 index 000000000..4ed6aae1e --- /dev/null +++ b/misc/pylib/robofab/objects/objectsFF.py @@ -0,0 +1,1253 @@ + + +__DEBUG__ = True +__version__ = "0.2" + +""" + RoboFab API Objects for FontForge + http://fontforge.sourceforge.net + + FontForge python docs: + http://fontforge.sourceforge.net/python.html + + Note: This is dead. EvB: "objectsFF.py is very dead and should only serve as an example of "dead" + + History + Version zero. May 2007. EvB + Experiment to see how far the API can be made to work. + + 0.1 extended testing and comparisons for attributes. + 0.2 checked into svn. Still quite raw. Lots of print statements and tests at the end. + + Notes + This code is best used with fontforge compiled as a python extension. + + FontForge Python API: + __doc__ + str(object) -> string + + Return a nice string representation of the object. + If the argument is a string, the return value is the same object. + + __file__ + str(object) -> string + + Return a nice string representation of the object. + If the argument is a string, the return value is the same object. + + __name__ + str(object) -> string + + Return a nice string representation of the object. + If the argument is a string, the return value is the same object. + + activeFont + If invoked from the UI, this returns the currently active font. When not in UI this returns None + + activeFontInUI + If invoked from the UI, this returns the currently active font. When not in UI this returns None + + activeGlyph + If invoked from the UI, this returns the currently active glyph (or None) + + ask + Pops up a dialog asking the user a question and providing a set of buttons for the user to reply with + + askChoices + Pops up a dialog asking the user a question and providing a scrolling list for the user to reply with + + askString + Pops up a dialog asking the user a question and providing a textfield for the user to reply with + + contour + fontforge Contour objects + + contouriter + None + + cvt + fontforge cvt objects + + defaultOtherSubrs + Use FontForge's default "othersubrs" functions for Type1 fonts + + font + FontForge Font object + + fontiter + None + + fonts + Returns a tuple of all loaded fonts + + fontsInFile + Returns a tuple containing the names of any fonts in an external file + + getPrefs + Get FontForge preference items + + glyph + FontForge GlyphPen object + + glyphPen + FontForge Glyph object + + hasSpiro + Returns whether this fontforge has access to Raph Levien's spiro package + + hasUserInterface + Returns whether this fontforge session has a user interface (True if it has opened windows) or is just running a script (False) + + hooks + dict() -> new empty dictionary. + dict(mapping) -> new dictionary initialized from a mapping object's + (key, value) pairs. + dict(seq) -> new dictionary initialized as if via: + d = {} + for k, v in seq: + d[k] = v + dict(**kwargs) -> new dictionary initialized with the name=value pairs + in the keyword argument list. For example: dict(one=1, two=2) + + layer + fontforge Layer objects + + layeriter + None + + loadEncodingFile + Load an encoding file into the list of encodings + + loadNamelist + Load a namelist into the list of namelists + + loadNamelistDir + Load a directory of namelist files into the list of namelists + + loadPlugin + Load a FontForge plugin + + loadPluginDir + Load a directory of FontForge plugin files + + loadPrefs + Load FontForge preference items + + logWarning + Adds a non-fatal message to the Warnings window + + open + Opens a font and returns it + + openFilename + Pops up a file picker dialog asking the user for a filename to open + + parseTTInstrs + Takes a string and parses it into a tuple of truetype instruction bytes + + point + fontforge Point objects + + postError + Pops up an error dialog box with the given title and message + + postNotice + Pops up an notice window with the given title and message + + preloadCidmap + Load a cidmap file + + printSetup + Prepare to print a font sample (select default printer or file, page size, etc.) + + private + FontForge private dictionary + + privateiter + None + + readOtherSubrsFile + Read from a file, "othersubrs" functions for Type1 fonts + + registerImportExport + Adds an import/export spline conversion module + + registerMenuItem + Adds a menu item (which runs a python script) to the font or glyph (or both) windows -- in the Tools menu + + saveFilename + Pops up a file picker dialog asking the user for a filename to use for saving + + savePrefs + Save FontForge preference items + + selection + fontforge selection objects + + setPrefs + Set FontForge preference items + + spiroCorner + int(x[, base]) -> integer + + Convert a string or number to an integer, if possible. A floating point + argument will be truncated towards zero (this does not include a string + representation of a floating point number!) When converting a string, use + the optional base. It is an error to supply a base when converting a + non-string. If the argument is outside the integer range a long object + will be returned instead. + + spiroG2 + int(x[, base]) -> integer + + Convert a string or number to an integer, if possible. A floating point + argument will be truncated towards zero (this does not include a string + representation of a floating point number!) When converting a string, use + the optional base. It is an error to supply a base when converting a + non-string. If the argument is outside the integer range a long object + will be returned instead. + + spiroG4 + int(x[, base]) -> integer + + Convert a string or number to an integer, if possible. A floating point + argument will be truncated towards zero (this does not include a string + representation of a floating point number!) When converting a string, use + the optional base. It is an error to supply a base when converting a + non-string. If the argument is outside the integer range a long object + will be returned instead. + + spiroLeft + int(x[, base]) -> integer + + Convert a string or number to an integer, if possible. A floating point + argument will be truncated towards zero (this does not include a string + representation of a floating point number!) When converting a string, use + the optional base. It is an error to supply a base when converting a + non-string. If the argument is outside the integer range a long object + will be returned instead. + + spiroOpen + int(x[, base]) -> integer + + Convert a string or number to an integer, if possible. A floating point + argument will be truncated towards zero (this does not include a string + representation of a floating point number!) When converting a string, use + the optional base. It is an error to supply a base when converting a + non-string. If the argument is outside the integer range a long object + will be returned instead. + + spiroRight + int(x[, base]) -> integer + + Convert a string or number to an integer, if possible. A floating point + argument will be truncated towards zero (this does not include a string + representation of a floating point number!) When converting a string, use + the optional base. It is an error to supply a base when converting a + non-string. If the argument is outside the integer range a long object + will be returned instead. + + unParseTTInstrs + Takes a tuple of truetype instruction bytes and converts to a human readable string + + unicodeFromName + Given a name, look it up in the namelists and find what unicode code point it maps to (returns -1 if not found) + + version + Returns a string containing the current version of FontForge, as 20061116 + + + + +Problems: + XXX: reading glif from UFO: is the contour order changed in some way? + + +ToDo: + - segments ? + + +""" + +import os +from robofab.objects.objectsBase import BaseFont, BaseGlyph, BaseContour, BaseSegment,\ + BasePoint, BaseBPoint, BaseAnchor, BaseGuide, BaseComponent, BaseKerning, BaseInfo, BaseGroups, BaseLib,\ + roundPt, addPt, _box,\ + MOVE, LINE, CORNER, CURVE, QCURVE, OFFCURVE,\ + relativeBCPIn, relativeBCPOut, absoluteBCPIn, absoluteBCPOut + +from robofab.objects.objectsRF import RGlyph as _RGlyph + +import fontforge +import psMat + + +# a list of attributes that are to be copied when copying a glyph. +# this is used by glyph.copy and font.insertGlyph +GLYPH_COPY_ATTRS = [ + "name", + "width", + "unicodes", + "note", + "lib", + ] + + + +def CurrentFont(): + if fontforge.hasUserInterface(): + _font = fontforge.activeFontInUI() + return RFont(_font) + if __DEBUG__: + print "CurrentFont(): fontforge not running with user interface," + return None + +def OpenFont(fontPath): + obj = fontforge.open(fontPath) + if __DEBUG__: + print "OpenFont", fontPath + print "result:", obj + return RFont(obj) + +def NewFont(fontPath=None): + _font = fontforge.font() + if __DEBUG__: + print "NewFont", fontPath + print "result:", _font + return RFont(_font) + + + + +class RFont(BaseFont): + def __init__(self, font=None): + if font is None: + # make a new font + pass + else: + self._object = font + + # ----------------------------------------------------------------- + # + # access + + def keys(self): + """FF implements __iter__ for the font object - better?""" + return [n.glyphname for n in self._object.glyphs()] + + def has_key(self, glyphName): + return glyphName in self + + def _get_info(self): + return RInfo(self._object) + + info = property(_get_info, doc="font info object") + + def __iter__(self): + for glyphName in self.keys(): + yield self.getGlyph(glyphName) + + + # ----------------------------------------------------------------- + # + # file + + def _get_path(self): + return self._object.path + + path = property(_get_path, doc="path of this file") + + def __contains__(self, glyphName): + return glyphName in self.keys() + + def save(self, path=None): + """Save this font as sfd file. + XXX: how to set a sfd path if is none + """ + if path is not None: + # trying to save it somewhere else + _path = path + else: + _path = self.path + if os.path.splitext(_path)[-1] != ".sfd": + _path = os.path.splitext(_path)[0]+".sfd" + if __DEBUG__: + print "RFont.save() to", _path + self._object.save(_path) + + def naked(self): + return self._object + + def close(self): + if __DEBUG__: + print "RFont.close()" + self._object.close() + + + # ----------------------------------------------------------------- + # + # generate + + def dummyGeneratePreHook(self, *args): + print "dummyGeneratePreHook", args + + def dummyGeneratePostHook(self, *args): + print "dummyGeneratePostHook", args + + def generate(self, outputType, path=None): + """ + generate the font. outputType is the type of font to ouput. + --Ouput Types: + 'pctype1' : PC Type 1 font (binary/PFB) + 'pcmm' : PC MultipleMaster font (PFB) + 'pctype1ascii' : PC Type 1 font (ASCII/PFA) + 'pcmmascii' : PC MultipleMaster font (ASCII/PFA) + 'unixascii' : UNIX ASCII font (ASCII/PFA) + 'mactype1' : Mac Type 1 font (generates suitcase and LWFN file) + 'otfcff' : PS OpenType (CFF-based) font (OTF) + 'otfttf' : PC TrueType/TT OpenType font (TTF) + 'macttf' : Mac TrueType font (generates suitcase) + 'macttdfont' : Mac TrueType font (generates suitcase with resources in data fork) + (doc adapted from http://dev.fontlab.net/flpydoc/) + + path can be a directory or a directory file name combo: + path="DirectoryA/DirectoryB" + path="DirectoryA/DirectoryB/MyFontName" + if no path is given, the file will be output in the same directory + as the vfb file. if no file name is given, the filename will be the + vfb file name with the appropriate suffix. + """ + + extensions = { + 'pctype1': 'pfm', + 'otfcff': 'otf', + } + + if __DEBUG__: + print "font.generate", outputType, path + + # set pre and post hooks (necessary?) + temp = getattr(self._object, "temporary") + if temp is None: + self._object.temporary = {} + else: + if type(self._object.temporary)!=dict: + self._object.temporary = {} + self._object.temporary['generateFontPreHook'] = self.dummyGeneratePreHook + self._object.temporary['generateFontPostHook'] = self.dummyGeneratePostHook + + # make a path for the destination + if path is None: + fileName = os.path.splitext(os.path.basename(self.path))[0] + dirName = os.path.dirname(self.path) + extension = extensions.get(outputType) + if extension is not None: + fileName = "%s.%s"%(fileName, extension) + else: + if __DEBUG__: + print "can't generate font in %s format"%outputType + return + path = os.path.join(dirName, fileName) + + # prepare OTF fields + generateFlags = [] + generateFlags.append('opentype') + # generate + self._object.generate(filename=path, flags=generateFlags) + if __DEBUG__: + print "font.generate():", path + return path + + + # ----------------------------------------------------------------- + # + # kerning stuff + + def _get_kerning(self): + kerning = {} + f = self._object + for g in f.glyphs: + for p in g.kerning: + try: + key = (g.name, f[p.key].name) + kerning[key] = p.value + except AttributeError: pass #catch for TT exception + rk = RKerning(kerning) + rk.setParent(self) + return rk + + kerning = property(_get_kerning, doc="a kerning object") + + # ----------------------------------------------------------------- + # + # glyph stuff + + def getGlyph(self, glyphName): + try: + ffGlyph = self._object[glyphName] + except TypeError: + print "font.getGlyph, can't find glyphName, returning new glyph" + return self.newGlyph(glyphName) + glyph = RGlyph(ffGlyph) + glyph.setParent(self) + return glyph + + def newGlyph(self, glyphName, clear=True): + """Make a new glyph + + Notes: not sure how to make a new glyph without an encoded name. + createChar() seems to be intended for that, but when I pass it -1 + for the unicode, it complains that it wants -1. Perhaps a bug? + """ + # is the glyph already there? + glyph = None + if glyphName in self: + if clear: + self._object[glyphName].clear() + return self[glyphName] + else: + # is the glyph in an encodable place: + slot = self._object.findEncodingSlot(glyphName) + if slot == -1: + # not encoded + print "font.newGlyph: unencoded slot", slot, glyphName + glyph = self._object.createChar(-1, glyphName) + else: + glyph = self._object.createMappedChar(glyphName) + glyph = RGlyph(self._object[glyphName]) + glyph.setParent(self) + return glyph + + def removeGlyph(self, glyphName): + self._object.removeGlyph(glyphName) + + + + +class RGlyph(BaseGlyph): + """Fab wrapper for FF Glyph object""" + def __init__(self, ffGlyph=None): + if ffGlyph is None: + raise RoboFabError + self._object = ffGlyph + # XX anchors seem to be supported, but in a different way + # XX so, I will ignore them for now to get something working. + self.anchors = [] + self.lib = {} + + def naked(self): + return self._object + + def setChanged(self): + self._object.changed() + + + # ----------------------------------------------------------------- + # + # attributes + + def _get_name(self): + return self._object.glyphname + def _set_name(self, value): + self._object.glyphname = value + name = property(_get_name, _set_name, doc="name") + + def _get_note(self): + return self._object.comment + def _set_note(self, note): + self._object.comment = note + note = property(_get_note, _set_note, doc="note") + + def _get_width(self): + return self._object.width + def _set_width(self, width): + self._object.width = width + width = property(_get_width, _set_width, doc="width") + + def _get_leftMargin(self): + return self._object.left_side_bearing + def _set_leftMargin(self, leftMargin): + self._object.left_side_bearing = leftMargin + leftMargin = property(_get_leftMargin, _set_leftMargin, doc="leftMargin") + + def _get_rightMargin(self): + return self._object.right_side_bearing + def _set_rightMargin(self, rightMargin): + self._object.right_side_bearing = rightMargin + rightMargin = property(_get_rightMargin, _set_rightMargin, doc="rightMargin") + + def _get_unicodes(self): + return [self._object.unicode] + def _set_unicodes(self, unicodes): + assert len(unicodes)==1 + self._object.unicode = unicodes[0] + unicodes = property(_get_unicodes, _set_unicodes, doc="unicodes") + + def _get_unicode(self): + return self._object.unicode + def _set_unicode(self, unicode): + self._object.unicode = unicode + unicode = property(_get_unicode, _set_unicode, doc="unicode") + + def _get_box(self): + bounds = self._object.boundingBox() + return bounds + box = property(_get_box, doc="the bounding box of the glyph: (xMin, yMin, xMax, yMax)") + + def _get_mark(self): + """color of the glyph box in the font view. This accepts a 6 hex digit number. + + XXX the FL implementation accepts a + """ + import colorsys + r = (self._object.color&0xff0000)>>16 + g = (self._object.color&0xff00)>>8 + g = (self._object.color&0xff)>>4 + return colorsys.rgb_to_hsv( r, g, b)[0] + + def _set_mark(self, markColor=-1): + import colorsys + self._object.color = colorSys.hsv_to_rgb(markColor, 1, 1) + + mark = property(_get_mark, _set_mark, doc="the color of the glyph box in the font view") + + + # ----------------------------------------------------------------- + # + # pen, drawing + + def getPen(self): + return self._object.glyphPen() + + def __getPointPen(self): + """Return a point pen. + + Note: FontForge doesn't support segment pen, so return an adapter. + """ + from robofab.pens.adapterPens import PointToSegmentPen + segmentPen = self._object.glyphPen() + return PointToSegmentPen(segmentPen) + + def getPointPen(self): + from robofab.pens.rfUFOPen import RFUFOPointPen + pen = RFUFOPointPen(self) + #print "getPointPen", pen, pen.__class__, dir(pen) + return pen + + def draw(self, pen): + """draw + + """ + self._object.draw(pen) + pen = None + + def drawPoints(self, pen): + """drawPoints + + Note: FontForge implements glyph.draw, but not glyph.drawPoints. + """ + from robofab.pens.adapterPens import PointToSegmentPen, SegmentToPointPen + adapter = SegmentToPointPen(pen) + self._object.draw(adapter) + pen = None + + def appendGlyph(self, other): + pen = self.getPen() + other.draw(pen) + + # ----------------------------------------------------------------- + # + # glyphmath + + def round(self): + self._object.round() + + def _getMathDestination(self): + from robofab.objects.objectsRF import RGlyph as _RGlyph + return _RGlyph() + + 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 __mul__(self, factor): + if __DEBUG__: + print "glyphmath mul", factor + return self.copy() *factor + + __rmul__ = __mul__ + + def __sub__(self, other): + if __DEBUG__: + print "glyphmath sub", other, other.__class__ + return self.copy() - other.copy() + + def __add__(self, other): + if __DEBUG__: + print "glyphmath add", other, other.__class__ + return self.copy() + other.copy() + + def getParent(self): + return self + + def copy(self, aParent=None): + """Make a copy of this glyph. + Note: the copy is not a duplicate fontlab glyph, but + a RF RGlyph with the same outlines. The new glyph is + not part of the fontlab font in any way. Use font.appendGlyph(glyph) + to get it in a FontLab glyph again.""" + from robofab.objects.objectsRF import RGlyph as _RGlyph + newGlyph = _RGlyph() + newGlyph.appendGlyph(self) + for attr in GLYPH_COPY_ATTRS: + value = getattr(self, attr) + setattr(newGlyph, attr, value) + parent = self.getParent() + if aParent is not None: + newGlyph.setParent(aParent) + elif self.getParent() is not None: + newGlyph.setParent(self.getParent()) + return newGlyph + + def _get_contours(self): + # find the contour data and wrap it + + """get the contours in this glyph""" + contours = [] + for n in range(len(self._object.foreground)): + item = self._object.foreground[n] + rc = RContour(item, n) + rc.setParent(self) + contours.append(rc) + #print contours + return contours + + contours = property(_get_contours, doc="allow for iteration through glyph.contours") + + # ----------------------------------------------------------------- + # + # transformations + + def move(self, (x, y)): + matrix = psMat.translate((x,y)) + self._object.transform(matrix) + + def scale(self, (x, y), center=(0,0)): + matrix = psMat.scale(x,y) + self._object.transform(matrix) + + def transform(self, matrix): + self._object.transform(matrix) + + def rotate(self, angle, offset=None): + matrix = psMat.rotate(angle) + self._object.transform(matrix) + + def skew(self, angle, offset=None): + matrix = psMat.skew(angle) + self._object.transform(matrix) + + # ----------------------------------------------------------------- + # + # components stuff + + def decompose(self): + self._object.unlinkRef() + + # ----------------------------------------------------------------- + # + # unicode stuff + + def autoUnicodes(self): + if __DEBUG__: + print "objectsFF.RGlyph.autoUnicodes() not implemented yet." + + # ----------------------------------------------------------------- + # + # contour stuff + + def removeOverlap(self): + self._object.removeOverlap() + + def correctDirection(self, trueType=False): + # no option for trueType, really. + self._object.correctDirection() + + def clear(self): + self._object.clear() + + def __getitem__(self, index): + return self.contours[index] + + +class RContour(BaseContour): + def __init__(self, contour, index=None): + self._object = contour + self.index = index + + def _get_points(self): + pts = [] + for pt in self._object: + wpt = RPoint(pt) + wpt.setParent(self) + pts.append(wpt) + return pts + + points = property(_get_points, doc="get contour points") + + def _get_box(self): + return self._object.boundingBox() + + box = property(_get_box, doc="get contour bounding box") + + def __len__(self): + return len(self._object) + + def __getitem__(self, index): + return self.points[index] + + + +class RPoint(BasePoint): + + def __init__(self, pointObject): + self._object = pointObject + + def _get_x(self): + return self._object.x + + def _set_x(self, value): + self._object.x = value + + x = property(_get_x, _set_x, doc="") + + def _get_y(self): + return self._object.y + + def _set_y(self, value): + self._object.y = value + + y = property(_get_y, _set_y, doc="") + + def _get_type(self): + if self._object.on_curve == 0: + return OFFCURVE + + # XXX not always curve + return CURVE + + def _set_type(self, value): + self._type = value + self._hasChanged() + + type = property(_get_type, _set_type, doc="") + + 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.fullName + except AttributeError: pass + return "<RPoint for %s.%s[%s]>"%(font, glyph, contourIndex) + + +class RInfo(BaseInfo): + def __init__(self, font): + BaseInfo.__init__(self) + self._object = font + + def _get_familyName(self): + return self._object.familyname + def _set_familyName(self, value): + self._object.familyname = value + familyName = property(_get_familyName, _set_familyName, doc="familyname") + + def _get_fondName(self): + return self._object.fondname + def _set_fondName(self, value): + self._object.fondname = value + fondName = property(_get_fondName, _set_fondName, doc="fondname") + + def _get_fontName(self): + return self._object.fontname + def _set_fontName(self, value): + self._object.fontname = value + fontName = property(_get_fontName, _set_fontName, doc="fontname") + + # styleName doesn't have a specific field, FF has a whole sfnt dict. + # implement fullName because a repr depends on it + def _get_fullName(self): + return self._object.fullname + def _set_fullName(self, value): + self._object.fullname = value + fullName = property(_get_fullName, _set_fullName, doc="fullname") + + def _get_unitsPerEm(self): + return self._object.em + def _set_unitsPerEm(self, value): + self._object.em = value + unitsPerEm = property(_get_unitsPerEm, _set_unitsPerEm, doc="unitsPerEm value") + + def _get_ascender(self): + return self._object.ascent + def _set_ascender(self, value): + self._object.ascent = value + ascender = property(_get_ascender, _set_ascender, doc="ascender value") + + def _get_descender(self): + return -self._object.descent + def _set_descender(self, value): + self._object.descent = -value + descender = property(_get_descender, _set_descender, doc="descender value") + + def _get_copyright(self): + return self._object.copyright + def _set_copyright(self, value): + self._object.copyright = value + copyright = property(_get_copyright, _set_copyright, doc="copyright") + + + +class RKerning(BaseKerning): + + """ Object representing the kerning. + This is going to need some thinking about. + """ + + +__all__ = [ 'RFont', 'RGlyph', 'RContour', 'RPoint', 'RInfo', + 'OpenFont', 'CurrentFont', 'NewFont', 'CurrentFont' + ] + + + +if __name__ == "__main__": + import os + from robofab.objects.objectsRF import RFont as _RFont + from sets import Set + + def dumpFontForgeAPI(testFontPath, printModule=False, + printFont=False, printGlyph=False, + printLayer=False, printContour=False, printPoint=False): + def printAPI(item, name): + print + print "-"*80 + print "API of", item + names = dir(item) + names.sort() + print + + if printAPI: + for n in names: + print + print "%s.%s"%(name, n) + try: + print getattr(item, n).__doc__ + except: + print "# error showing", n + # module + if printModule: + print "module file:", fontforge.__file__ + print "version:", fontforge.version() + print "module doc:", fontforge.__doc__ + print "has User Interface:", fontforge.hasUserInterface() + print "has Spiro:", fontforge.hasSpiro() + printAPI(fontforge, "fontforge") + + # font + fontObj = fontforge.open(testFontPath) + if printFont: + printAPI(fontObj, "font") + + # glyph + glyphObj = fontObj["A"] + if printGlyph: + printAPI(glyphObj, "glyph") + + # layer + layerObj = glyphObj.foreground + if printLayer: + printAPI(layerObj, "layer") + + # contour + contourObj = layerObj[0] + if printContour: + printAPI(contourObj, "contour") + + # point + if printPoint: + pointObj = contourObj[0] + printAPI(pointObj, "point") + + + # other objects + penObj = glyphObj.glyphPen() + printAPI(penObj, "glyphPen") + + # use your own paths here. + demoRoot = "/Users/erik/Develop/Mess/FontForge/objectsFF_work/" + UFOPath = os.path.join(demoRoot, "Test.ufo") + SFDPath = os.path.join(demoRoot, "Test_realSFD2.sfd") + + #dumpFontForgeAPI(UFOPath, printPoint=True) + + # should return None + CurrentFont() + + def compareAttr(obj1, obj2, attrName, isMethod=False): + if isMethod: + a = getattr(obj1, attrName)() + b = getattr(obj2, attrName)() + else: + a = getattr(obj1, attrName) + b = getattr(obj2, attrName) + if a == b and a is not None and b is not None: + print "\tattr %s ok"%attrName, a + return True + else: + print "\t?\t%s error:"%attrName, "%s:"%obj1.__class__, a, "%s:"%obj2.__class__, b + return False + + f = OpenFont(UFOPath) + #f = OpenFont(SFDPath) + ref = _RFont(UFOPath) + + if False: + print + print "test font attributes" + compareAttr(f, ref, "path") + + a = Set(f.keys()) + b = Set(ref.keys()) + print "glyphs in ref, not in f", b.difference(a) + print "glyphs in f, not in ref", a.difference(b) + + print "A" in f, "A" in ref + print f.has_key("A"), ref.has_key("A") + + print + print "test font info attributes" + compareAttr(f.info, ref.info, "ascender") + compareAttr(f.info, ref.info, "descender") + compareAttr(f.info, ref.info, "unitsPerEm") + compareAttr(f.info, ref.info, "copyright") + compareAttr(f.info, ref.info, "fullName") + compareAttr(f.info, ref.info, "familyName") + compareAttr(f.info, ref.info, "fondName") + compareAttr(f.info, ref.info, "fontName") + + # crash + f.save() + + otfOutputPath = os.path.join(demoRoot, "test_ouput.otf") + ufoOutputPath = os.path.join(demoRoot, "test_ouput.ufo") + # generate without path, should end up in the source folder + + f['A'].removeOverlap() + f.generate('otfcff') #, otfPath) + f.generate('pctype1') #, otfPath) + + # generate with path. Type is taken from the extension. + f.generate('otfcff', otfOutputPath) #, otfPath) + f.generate(None, ufoOutputPath) #, otfPath) + + featurePath = os.path.join(demoRoot, "testFeatureOutput.fea") + f.naked().generateFeatureFile(featurePath) + + if False: + # new glyphs + # unencoded + print "new glyph: unencoded", f.newGlyph("test_unencoded_glyph") + # encoded + print "new glyph: encoded", f.newGlyph("Adieresis") + # existing + print "new glyph: existing", f.newGlyph("K") + + print + print "test glyph attributes" + compareAttr(f['A'], ref['A'], "width") + compareAttr(f['A'], ref['A'], "unicode") + compareAttr(f['A'], ref['A'], "name") + compareAttr(f['A'], ref['A'], "box") + compareAttr(f['A'], ref['A'], "leftMargin") + compareAttr(f['A'], ref['A'], "rightMargin") + + if False: + print + print "comparing glyph digests" + failed = [] + for n in f.keys(): + g1 = f[n] + #g1.round() + g2 = ref[n] + #g2.round() + d1 = g1._getDigest() + d2 = g2._getDigest() + if d1 != d2: + failed.append(n) + #print "f: ", d1 + #print "ref: ", d2 + print "digest failed for %s"%". ".join(failed) + + g3 = f['A'] *.333 + print g3 + print g3._getDigest() + f.save() + + if False: + print + print "test contour attributes" + compareAttr(f['A'].contours[0], ref['A'].contours[0], "index") + + #for c in f['A'].contours: + # for p in c.points: + # print p, p.type + + # test with a glyph with just 1 contour so we can be sure we're comparing the same thing + compareAttr(f['C'].contours[0], ref['C'].contours[0], "box") + compareAttr(f['C'].contours[0], ref['C'].contours[0], "__len__", isMethod=True) + + ptf = f['C'].contours[0].points[0] + ptref = ref['C'].contours[0].points[0] + print "x, y", (ptf.x, ptf.y) == (ptref.x, ptref.y), (ptref.x, ptref.y) + print 'type', ptf.type, ptref.type + + print "point inside", f['A'].pointInside((50,10)), ref['A'].pointInside((50,10)) + + + print ref.kerning.keys() + + class GlyphLookupWrapper(dict): + """A wrapper for the lookups / subtable data in a FF glyph. + A lot of data is stored there, so it helps to have something to sort things out. + """ + def __init__(self, ffGlyph): + self._object = ffGlyph + self.refresh() + + def __repr__(self): + return "<GlyphLookupWrapper for %s, %d keys>"%(self._object.glyphname, len(self)) + + def refresh(self): + """Pick some of the values apart.""" + lookups = self._object.getPosSub('*') + for t in lookups: + print 'lookup', t + lookupName = t[0] + lookupType = t[1] + if not lookupName in self: + self[lookupName] = [] + self[lookupName].append(t[1:]) + + def getKerning(self): + """Get a regular kerning dict for this glyph""" + d = {} + left = self._object.glyphname + for name in self.keys(): + for item in self[name]: + print 'item', item + if item[0]!="Pair": + continue + #print 'next glyph:', item[1] + #print 'first glyph x Pos:', item[2] + #print 'first glyph y Pos:', item[3] + #print 'first glyph h Adv:', item[4] + #print 'first glyph v Adv:', item[5] + + #print 'second glyph x Pos:', item[6] + #print 'second glyph y Pos:', item[7] + #print 'second glyph h Adv:', item[8] + #print 'second glyph v Adv:', item[9] + right = item[1] + d[(left, right)] = item[4] + return d + + def setKerning(self, kernDict): + """Set the values of a regular kerning dict to the lookups in a FF glyph.""" + for left, right in kernDict.keys(): + if left != self._object.glyphname: + # should we filter the dict before it gets here? + # easier just to filter it here. + continue + + + + # lets try to find the kerning + A = f['A'].naked() + positionTypes = [ "Position", "Pair", "Substitution", "AltSubs", "MultSubs","Ligature"] + print A.getPosSub('*') + #for t in A.getPosSub('*'): + # print 'lookup subtable name:', t[0] + # print 'positioning type:', t[1] + # if t[1]in positionTypes: + # print 'next glyph:', t[2] + # print 'first glyph x Pos:', t[3] + # print 'first glyph y Pos:', t[4] + # print 'first glyph h Adv:', t[5] + # print 'first glyph v Adv:', t[6] + + # print 'second glyph x Pos:', t[7] + # print 'second glyph y Pos:', t[8] + # print 'second glyph h Adv:', t[9] + # print 'second glyph v Adv:', t[10] + + gw = GlyphLookupWrapper(A) + print gw + print gw.keys() + print gw.getKerning() + + name = "'kern' Horizontal Kerning in Latin lookup 0 subtable" + item = (name, 'quoteright', 0, 0, -200, 0, 0, 0, 0, 0) + + A.removePosSub(name) + apply(A.addPosSub, item) + + + print "after", A.getPosSub('*') + + fn = f.naked() + + name = "'kern' Horizontal Kerning in Latin lookup 0" + + + print "before removing stuff", fn.gpos_lookups + print "removing stuff", fn.removeLookup(name) + print "after removing stuff", fn.gpos_lookups + + flags = () + feature_script_lang = (("kern",(("latn",("dflt")),)),) + print fn.addLookup('kern', 'gpos_pair', flags, feature_script_lang) + print fn.addLookupSubtable('kern', 'myKerning') + + + print fn.gpos_lookups + A.addPosSub('myKerning', 'A', 0, 0, -400, 0, 0, 0, 0, 0) + A.addPosSub('myKerning', 'B', 0, 0, 200, 0, 0, 0, 0, 0) + A.addPosSub('myKerning', 'C', 0, 0, 10, 0, 0, 0, 0, 0) + A.addPosSub('myKerning', 'A', 0, 0, 77, 0, 0, 0, 0, 0) + + + gw = GlyphLookupWrapper(A) + print gw + print gw.keys() + print gw.getKerning() + diff --git a/misc/pylib/robofab/objects/objectsFL.py b/misc/pylib/robofab/objects/objectsFL.py new file mode 100755 index 000000000..3b78ddc14 --- /dev/null +++ b/misc/pylib/robofab/objects/objectsFL.py @@ -0,0 +1,3112 @@ +"""UFO implementation for the objects as used by FontLab 4.5 and higher""" + +from FL import * + +from robofab.tools.toolsFL import GlyphIndexTable, NewGlyph +from robofab.objects.objectsBase import BaseFont, BaseGlyph, BaseContour, BaseSegment,\ + BasePoint, BaseBPoint, BaseAnchor, BaseGuide, BaseComponent, BaseKerning, BaseInfo, BaseFeatures, BaseGroups, BaseLib,\ + roundPt, addPt, _box,\ + MOVE, LINE, CORNER, CURVE, QCURVE, OFFCURVE,\ + relativeBCPIn, relativeBCPOut, absoluteBCPIn, absoluteBCPOut,\ + BasePostScriptFontHintValues, postScriptHintDataLibKey, BasePostScriptGlyphHintValues +from robofab.misc import arrayTools +from robofab.pens.flPen import FLPointPen, FLPointContourPen +from robofab import RoboFabError +import os +from robofab.plistlib import Data, Dict, readPlist, writePlist +from StringIO import StringIO +from robofab import ufoLib +from warnings import warn +import datetime +from robofab.tools.fontlabFeatureSplitter import splitFeaturesForFontLab + + +try: + set +except NameError: + from sets import Set as set + +# local encoding +if os.name in ["mac", "posix"]: + LOCAL_ENCODING = "macroman" +else: + LOCAL_ENCODING = "latin-1" + +_IN_UFO_EXPORT = False + +# a list of attributes that are to be copied when copying a glyph. +# this is used by glyph.copy and font.insertGlyph +GLYPH_COPY_ATTRS = [ + "name", + "width", + "unicodes", + "note", + "lib", + ] + +# Generate Types +PC_TYPE1 = 'pctype1' +PC_MM = 'pcmm' +PC_TYPE1_ASCII = 'pctype1ascii' +PC_MM_ASCII = 'pcmmascii' +UNIX_ASCII = 'unixascii' +MAC_TYPE1 = 'mactype1' +OTF_CFF = 'otfcff' +OTF_TT = 'otfttf' +MAC_TT = 'macttf' +MAC_TT_DFONT = 'macttdfont' + +# doc for these functions taken from: http://dev.fontlab.net/flpydoc/ +# internal name (FontLab name, extension) +_flGenerateTypes ={ PC_TYPE1 : (ftTYPE1, 'pfb'), # PC Type 1 font (binary/PFB) + PC_MM : (ftTYPE1_MM, 'mm'), # PC MultipleMaster font (PFB) + PC_TYPE1_ASCII : (ftTYPE1ASCII, 'pfa'), # PC Type 1 font (ASCII/PFA) + PC_MM_ASCII : (ftTYPE1ASCII_MM, 'mm'), # PC MultipleMaster font (ASCII/PFA) + UNIX_ASCII : (ftTYPE1ASCII, 'pfa'), # UNIX ASCII font (ASCII/PFA) + OTF_TT : (ftTRUETYPE, 'ttf'), # PC TrueType/TT OpenType font (TTF) + OTF_CFF : (ftOPENTYPE, 'otf'), # PS OpenType (CFF-based) font (OTF) + MAC_TYPE1 : (ftMACTYPE1, 'suit'), # Mac Type 1 font (generates suitcase and LWFN file, optionally AFM) + MAC_TT : (ftMACTRUETYPE, 'ttf'), # Mac TrueType font (generates suitcase) + MAC_TT_DFONT : (ftMACTRUETYPE_DFONT, 'dfont'), # Mac TrueType font (generates suitcase with resources in data fork) + } + +## FL Hint stuff +# this should not be referenced outside of this module +# since we may be changing the way this works in the future. + + +""" + + FontLab implementation of psHints objects + + Most of the FL methods relating to ps hints return a list of 16 items. + These values are for the 16 corners of a 4 axis multiple master. + The odd thing is that even single masters get these 16 values. + RoboFab doesn't access the MM masters, so by default, the psHints + object only works with the first element. If you want to access the other + values in the list, give a value between 0 and 15 for impliedMasterIndex + when creating the object. + + From the FontLab docs: + http://dev.fontlab.net/flpydoc/ + + blue_fuzz + blue_scale + blue_shift + + blue_values_num(integer) - number of defined blue values + blue_values[integer[integer]] - two-dimentional array of BlueValues + master index is top-level index + + other_blues_num(integer) - number of defined OtherBlues values + other_blues[integer[integer]] - two-dimentional array of OtherBlues + master index is top-level index + + family_blues_num(integer) - number of FamilyBlues records + family_blues[integer[integer]] - two-dimentional array of FamilyBlues + master index is top-level index + + family_other_blues_num(integer) - number of FamilyOtherBlues records + family_other_blues[integer[integer]] - two-dimentional array of FamilyOtherBlues + master index is top-level index + + force_bold[integer] - list of Force Bold values, one for + each master + stem_snap_h_num(integer) + stem_snap_h + stem_snap_v_num(integer) + stem_snap_v + """ + +class PostScriptFontHintValues(BasePostScriptFontHintValues): + """ Wrapper for font-level PostScript hinting information for FontLab. + Blues values, stem values. + """ + def __init__(self, font=None, impliedMasterIndex=0): + self._object = font.naked() + self._masterIndex = impliedMasterIndex + + def copy(self): + from robofab.objects.objectsRF import PostScriptFontHintValues as _PostScriptFontHintValues + return _PostScriptFontHintValues(data=self.asDict()) + + +class PostScriptGlyphHintValues(BasePostScriptGlyphHintValues): + """ Wrapper for glyph-level PostScript hinting information for FontLab. + vStems, hStems. + """ + def __init__(self, glyph=None): + self._object = glyph.naked() + + def copy(self): + from robofab.objects.objectsRF import PostScriptGlyphHintValues as _PostScriptGlyphHintValues + return _PostScriptGlyphHintValues(data=self.asDict()) + + def _hintObjectsToList(self, item): + data = [] + done = [] + for hint in item: + p = (hint.position, hint.width) + if p in done: + continue + data.append(p) + done.append(p) + data.sort() + return data + + def _listToHintObjects(self, item): + hints = [] + done = [] + for pos, width in item: + if (pos, width) in done: + # we don't want to set duplicates + continue + hints.append(Hint(pos, width)) + done.append((pos,width)) + return hints + + def _getVHints(self): + return self._hintObjectsToList(self._object.vhints) + + def _setVHints(self, values): + # 1 = horizontal hints and links, + # 2 = vertical hints and links + # 3 = all hints and links + self._object.RemoveHints(2) + if values is None: + # just clearing it then + return + values.sort() + for hint in self._listToHintObjects(values): + self._object.vhints.append(hint) + + def _getHHints(self): + return self._hintObjectsToList(self._object.hhints) + + def _setHHints(self, values): + # 1 = horizontal hints and links, + # 2 = vertical hints and links + # 3 = all hints and links + self._object.RemoveHints(1) + if values is None: + # just clearing it then + return + values.sort() + for hint in self._listToHintObjects(values): + self._object.hhints.append(hint) + + vHints = property(_getVHints, _setVHints, doc="postscript hints: vertical hint zones") + hHints = property(_getHHints, _setHHints, doc="postscript hints: horizontal hint zones") + + + +def _glyphHintsToDict(glyph): + data = {} + ## + ## horizontal and vertical hints + ## + # glyph.hhints and glyph.vhints returns a list of Hint objects. + # Hint objects have position and width attributes. + data['hHints'] = [] + for index in xrange(len(glyph.hhints)): + hint = glyph.hhints[index] + data['hHints'].append((hint.position, hint.width)) + if not data['hHints']: + del data['hHints'] + data['vHints'] = [] + for index in xrange(len(glyph.vhints)): + hint = glyph.vhints[index] + data['vHints'].append((hint.position, hint.width)) + if not data['vHints']: + del data['vHints'] + ## + ## horizontal and vertical links + ## + # glyph.hlinks and glyph.vlinks returns a list of Link objects. + # Link objects have node1 and node2 attributes. + data['hLinks'] = [] + for index in xrange(len(glyph.hlinks)): + link = glyph.hlinks[index] + d = { 'node1' : link.node1, + 'node2' : link.node2, + } + data['hLinks'].append(d) + if not data['hLinks']: + del data['hLinks'] + data['vLinks'] = [] + for index in xrange(len(glyph.vlinks)): + link = glyph.vlinks[index] + d = { 'node1' : link.node1, + 'node2' : link.node2, + } + data['vLinks'].append(d) + if not data['vLinks']: + del data['vLinks'] + ## + ## replacement table + ## + # glyph.replace_table returns a list of Replace objects. + # Replace objects have type and index attributes. + data['replaceTable'] = [] + for index in xrange(len(glyph.replace_table)): + replace = glyph.replace_table[index] + d = { 'type' : replace.type, + 'index' : replace.index, + } + data['replaceTable'].append(d) + if not data['replaceTable']: + del data['replaceTable'] + # XXX + # need to support glyph.instructions and glyph.hdmx? + # they are not documented very well. + return data + +def _dictHintsToGlyph(glyph, aDict): + # clear existing hints first + # RemoveHints requires an "integer mode" argument + # but it is not documented. from some simple experiments + # i deduced that + # 1 = horizontal hints and links, + # 2 = vertical hints and links + # 3 = all hints and links + glyph.RemoveHints(3) + ## + ## horizontal and vertical hints + ## + if aDict.has_key('hHints'): + for d in aDict['hHints']: + glyph.hhints.append(Hint(d[0], d[1])) + if aDict.has_key('vHints'): + for d in aDict['vHints']: + glyph.vhints.append(Hint(d[0], d[1])) + ## + ## horizontal and vertical links + ## + if aDict.has_key('hLinks'): + for d in aDict['hLinks']: + glyph.hlinks.append(Link(d['node1'], d['node2'])) + if aDict.has_key('vLinks'): + for d in aDict['vLinks']: + glyph.vlinks.append(Link(d['node1'], d['node2'])) + ## + ## replacement table + ## + if aDict.has_key('replaceTable'): + for d in aDict['replaceTable']: + glyph.replace_table.append(Replace(d['type'], d['index'])) + +# FL Node Types +flMOVE = 17 +flLINE = 1 +flCURVE = 35 +flOFFCURVE = 65 +flSHARP = 0 +# I have no idea what the difference between +# "smooth" and "fixed" is, but booth values +# are returned by FL +flSMOOTH = 4096 +flFIXED = 12288 + + +_flToRFSegmentDict = { flMOVE : MOVE, + flLINE : LINE, + flCURVE : CURVE, + flOFFCURVE : OFFCURVE + } + +_rfToFLSegmentDict = {} +for k, v in _flToRFSegmentDict.items(): + _rfToFLSegmentDict[v] = k + +def _flToRFSegmentType(segmentType): + return _flToRFSegmentDict[segmentType] + +def _rfToFLSegmentType(segmentType): + return _rfToFLSegmentDict[segmentType] + +def _scalePointFromCenter((pointX, pointY), (scaleX, scaleY), (centerX, centerY)): + ogCenter = (centerX, centerY) + scaledCenter = (centerX * scaleX, centerY * scaleY) + shiftVal = (scaledCenter[0] - ogCenter[0], scaledCenter[1] - ogCenter[1]) + scaledPointX = (pointX * scaleX) - shiftVal[0] + scaledPointY = (pointY * scaleY) - shiftVal[1] + return (scaledPointX, scaledPointY) + +# Nostalgia code: +def CurrentFont(): + """Return a RoboFab font object for the currently selected font.""" + f = fl.font + if f is not None: + return RFont(fl.font) + return None + +def CurrentGlyph(): + """Return a RoboFab glyph object for the currently selected glyph.""" + currentPath = fl.font.file_name + if fl.glyph is None: + return None + glyphName = fl.glyph.name + currentFont = None + # is this font already loaded as an RFont? + for font in AllFonts(): + # ugh this won't work because AllFonts sees non RFonts as well.... + if font.path == currentPath: + currentFont = font + break + xx = currentFont[glyphName] + #print "objectsFL.CurrentGlyph parent for %d"% id(xx), xx.getParent() + return xx + +def OpenFont(path=None, note=None): + """Open a font from a path.""" + if path == None: + from robofab.interface.all.dialogs import GetFile + path = GetFile(note) + if path: + if path[-4:].lower() in ['.vfb', '.VFB', '.bak', '.BAK']: + f = Font(path) + fl.Add(f) + return RFont(f) + return None + +def NewFont(familyName=None, styleName=None): + """Make a new font""" + from FL import fl, Font + f = Font() + fl.Add(f) + rf = RFont(f) + if familyName is not None: + rf.info.familyName = familyName + if styleName is not None: + rf.info.styleName = styleName + return rf + +def AllFonts(): + """Return a list of all open fonts.""" + fontCount = len(fl) + all = [] + for index in xrange(fontCount): + naked = fl[index] + all.append(RFont(naked)) + return all + + from robofab.world import CurrentGlyph + +def getGlyphFromMask(g): + """Get a Fab glyph object for the data in the mask layer.""" + from robofab.objects.objectsFL import RGlyph as FL_RGlyph + from robofab.objects.objectsRF import RGlyph as RF_RGlyph + n = g.naked() + mask = n.mask + fg = FL_RGlyph(mask) + rf = RF_RGlyph() + pen = rf.getPointPen() + fg.drawPoints(pen) + rf.width = g.width # can we get to the mask glyph width without flipping the UI? + return rf + +def setMaskToGlyph(maskGlyph, targetGlyph, clear=True): + """Set the maskGlyph as a mask layer in targetGlyph. + maskGlyph is a FontLab or RoboFab RGlyph, orphaned or not. + targetGlyph is a FontLab RGLyph. + clear is a bool. False: keep the existing mask data, True: clear the existing mask data. + """ + from robofab.objects.objectsFL import RGlyph as FL_RGlyph + from FL import Glyph as FL_NakedGlyph + flGlyph = FL_NakedGlyph() # new, orphaned FL glyph + wrapped = FL_RGlyph(flGlyph) # rf wrapper for FL glyph + if not clear: + # copy the existing mask data first + existingMask = getGlyphFromMask(targetGlyph) + if existingMask is not None: + pen = FLPointContourPen(existingMask) + existingMask.drawPoints(pen) + pen = FLPointContourPen(wrapped) + maskGlyph.drawPoints(pen) # draw the data + targetGlyph.naked().mask = wrapped .naked() + targetGlyph.update() + +# the lib getter and setter are shared by RFont and RGlyph +def _get_lib(self): + data = self._object.customdata + if data: + f = StringIO(data) + try: + pList = readPlist(f) + except: # XXX ugh, plistlib can raise lots of things + # Anyway, customdata does not contain valid plist data, + # but we don't need to toss it! + pList = {"org.robofab.fontlab.customdata": Data(data)} + else: + pList = {} + # pass it along to the lib object + l = RLib(pList) + l.setParent(self) + return l + +def _set_lib(self, aDict): + l = RLib({}) + l.setParent(self) + l.update(aDict) + + +def _normalizeLineEndings(s): + return s.replace("\r\n", "\n").replace("\r", "\n") + + +class RFont(BaseFont): + """RoboFab UFO wrapper for FL Font object""" + + _title = "FLFont" + + def __init__(self, font=None): + BaseFont.__init__(self) + if font is None: + from FL import fl, Font + # rather than raise an error we could just start a new font. + font = Font() + fl.Add(font) + #raise RoboFabError, "RFont: there's nothing to wrap!?" + self._object = font + self._lib = {} + self._supportHints = True + self.psHints = PostScriptFontHintValues(self) + self.psHints.setParent(self) + + def keys(self): + keys = {} + for glyph in self._object.glyphs: + glyphName = glyph.name + if glyphName in keys: + n = 1 + while ("%s#%s" % (glyphName, n)) in keys: + n += 1 + newGlyphName = "%s#%s" % (glyphName, n) + print "RoboFab encountered a duplicate glyph name, renaming %r to %r" % (glyphName, newGlyphName) + glyphName = newGlyphName + glyph.name = glyphName + keys[glyphName] = None + return keys.keys() + + def has_key(self, glyphName): + glyph = self._object[glyphName] + if glyph is None: + return False + else: + return True + + __contains__ = has_key + + def __setitem__(self, glyphName, glyph): + self._object[glyphName] = glyph.naked() + + def __cmp__(self, other): + if not hasattr(other, '_object'): + return -1 + return self._compare(other) + # if self._object.file_name == other._object.file_name: + # # so, names match. + # # this will falsely identify two distinct "Untitled" + # # let's check some more + # return 0 + # else: + # return -1 + + +# def _get_psHints(self): +# h = PostScriptFontHintValues(self) +# h.setParent(self) +# return h +# +# psHints = property(_get_psHints, doc="font level postscript hint data") + + def _get_info(self): + return RInfo(self._object) + + info = property(_get_info, doc="font info object") + + def _get_features(self): + return RFeatures(self._object) + + features = property(_get_features, doc="features object") + + def _get_kerning(self): + kerning = {} + f = self._object + for g in f.glyphs: + for p in g.kerning: + try: + key = (g.name, f[p.key].name) + kerning[key] = p.value + except AttributeError: pass #catch for TT exception + rk = RKerning(kerning) + rk.setParent(self) + return rk + + kerning = property(_get_kerning, doc="a kerning object") + + def _set_groups(self, aDict): + g = RGroups({}) + g.setParent(self) + g.update(aDict) + + def _get_groups(self): + groups = {} + for i in self._object.classes: + # test to make sure that the class is properly formatted + if i.find(':') == -1: + continue + key = i.split(':')[0] + value = i.split(':')[1].lstrip().split(' ') + groups[key] = value + rg = RGroups(groups) + rg.setParent(self) + return rg + + groups = property(_get_groups, _set_groups, doc="a group object") + + lib = property(_get_lib, _set_lib, doc="font lib object") + + # + # attributes + # + + def _get_fontIndex(self): + # find the index of the font + # by comparing the file_name + # to all open fonts. if the + # font has no file_name, meaning + # it is a new, unsaved font, + # return the index of the first + # font with no file_name. + selfFileName = self._object.file_name + fontCount = len(fl) + for index in xrange(fontCount): + other = fl[index] + if other.file_name == selfFileName: + return index + + fontIndex = property(_get_fontIndex, doc="the fontindex for this font") + + def _get_path(self): + return self._object.file_name + + path = property(_get_path, doc="path to the font") + + def _get_fileName(self): + if self.path is None: + return None + return os.path.split(self.path) + + fileName = property(_get_fileName, doc="the font's file name") + + def _get_selection(self): + # return a list of glyph names for glyphs selected in the font window + l=[] + for i in range(len(self._object.glyphs)): + if fl.Selected(i) == 1: + l.append(self._object[i].name) + return l + + def _set_selection(self, list): + fl.Unselect() + for i in list: + fl.Select(i) + + selection = property(_get_selection, _set_selection, doc="the glyph selection in the font window") + + + def _makeGlyphlist(self): + # To allow iterations through Font.glyphs. Should become really big in fonts with lotsa letters. + gl = [] + for c in self: + gl.append(c) + return gl + + def _get_glyphs(self): + return self._makeGlyphlist() + + glyphs = property(_get_glyphs, doc="A list of all glyphs in the font, to allow iterations through Font.glyphs") + + def update(self): + """Don't forget to update the font when you are done.""" + fl.UpdateFont(self.fontIndex) + + def save(self, path=None): + """Save the font, path is required.""" + if not path: + if not self._object.file_name: + raise RoboFabError, "No destination path specified." + else: + path = self._object.file_name + fl.Save(self.fontIndex, path) + + def close(self, save=False): + """Close the font, saving is optional.""" + if save: + self.save() + else: + self._object.modified = 0 + fl.Close(self.fontIndex) + + def getGlyph(self, glyphName): + # XXX may need to become private + flGlyph = self._object[glyphName] + if flGlyph is not None: + glyph = RGlyph(flGlyph) + glyph.setParent(self) + return glyph + return self.newGlyph(glyphName) + + def newGlyph(self, glyphName, clear=True): + """Make a new glyph.""" + # the old implementation always updated the font. + # that proved to be very slow. so, the updating is + # now left up to the caller where it can be more + # efficiently managed. + g = NewGlyph(self._object, glyphName, clear, updateFont=False) + return RGlyph(g) + + def insertGlyph(self, glyph, name=None): + """Returns a new glyph that has been inserted into the font. + name = another glyphname if you want to insert as with that.""" + from robofab.objects.objectsRF import RFont as _RFont + from robofab.objects.objectsRF import RGlyph as _RGlyph + oldGlyph = glyph + if name is None: + name = oldGlyph.name + # clear the destination glyph if it exists. + if self.has_key(name): + self[name].clear() + # get the parent for the glyph + otherFont = oldGlyph.getParent() + # in some cases we will use the native + # FL method for appending a glyph. + useNative = True + testingNative = True + while testingNative: + # but, maybe it is an orphan glyph. + # in that case we should not use the native method. + if otherFont is None: + useNative = False + testingNative = False + # or maybe the glyph is coming from a NoneLab font + if otherFont is not None: + if isinstance(otherFont, _RFont): + useNative = False + testingNative = False + # but, it could be a copied FL glyph + # which is a NoneLab glyph that + # has a FontLab font as the parent + elif isinstance(otherFont, RFont): + useNative = False + testingNative = False + # or, maybe the glyph is being replaced, in which + # case the native method should not be used + # since FL will destroy any references to the glyph + if self.has_key(name): + useNative = False + testingNative = False + # if the glyph contains components the native + # method should not be used since FL does + # not reference glyphs in components by + # name, but by index (!!!). + if len(oldGlyph.components) != 0: + useNative = False + testingNative = False + testingNative = False + # finally, insert the glyph. + if useNative: + font = self.naked() + otherFont = oldGlyph.getParent().naked() + self.naked().glyphs.append(otherFont[name]) + newGlyph = self.getGlyph(name) + else: + newGlyph = self.newGlyph(name) + newGlyph.appendGlyph(oldGlyph) + for attr in GLYPH_COPY_ATTRS: + if attr == "name": + value = name + else: + value = getattr(oldGlyph, attr) + setattr(newGlyph, attr, value) + if self._supportHints: + # now we need to transfer the hints from + # the old glyph to the new glyph. we'll do this + # via the dict to hint functions. + hintDict = {} + # if the glyph is a NoneLab glyph, then we need + # to extract the ps hints from the lib + if isinstance(oldGlyph, _RGlyph): + hintDict = oldGlyph.lib.get(postScriptHintDataLibKey, {}) + # otherwise we need to extract the hint dict from the glyph + else: + hintDict = _glyphHintsToDict(oldGlyph.naked()) + # now apply the hint data + if hintDict: + _dictHintsToGlyph(newGlyph.naked(), hintDict) + # delete any remaining hint data from the glyph lib + if newGlyph.lib.has_key(postScriptHintDataLibKey): + del newGlyph.lib[postScriptHintDataLibKey] + return newGlyph + + def removeGlyph(self, glyphName): + """remove a glyph from the font""" + index = self._object.FindGlyph(glyphName) + if index != -1: + del self._object.glyphs[index] + + # + # opentype + # + + def getOTClasses(self): + """Return all OpenType classes as a dict. Relies on properly formatted classes.""" + classes = {} + c = self._object.ot_classes + if c is None: + return classes + c = c.replace('\r', '').replace('\n', '').split(';') + for i in c: + if i.find('=') != -1: + value = [] + i = i.replace(' = ', '=') + name = i.split('=')[0] + v = i.split('=')[1].replace('[', '').replace(']', '').split(' ') + #catch double spaces? + for j in v: + if len(j) > 0: + value.append(j) + classes[name] = value + return classes + + def setOTClasses(self, dict): + """Set all OpenType classes.""" + l = [] + for i in dict.keys(): + l.append(''.join([i, ' = [', ' '.join(dict[i]), '];'])) + self._object.ot_classes = '\n'.join(l) + + def getOTClass(self, name): + """Get a specific OpenType class.""" + classes = self.getOTClasses() + return classes[name] + + def setOTClass(self, name, list): + """Set a specific OpenType class.""" + classes = self.getOTClasses() + classes[name] = list + self.setOTClasses(classes) + + def getOTFeatures(self): + """Return all OpenType features as a dict keyed by name. + The value is a string of the text of the feature.""" + features = {} + for i in self._object.features: + v = [] + for j in i.value.replace('\r', '\n').split('\n'): + if j.find(i.tag) == -1: + v.append(j) + features[i.tag] = '\n'.join(v) + return features + + def setOTFeatures(self, dict): + """Set all OpenType features in the font.""" + features= {} + for i in dict.keys(): + f = [] + f.append('feature %s {'%i) + f.append(dict[i]) + f.append('} %s;'%i) + features[i] = '\n'.join(f) + self._object.features.clean() + for i in features.keys(): + self._object.features.append(Feature(i, features[i])) + + def getOTFeature(self, name): + """return a specific OpenType feature.""" + features = self.getOTFeatures() + return features[name] + + def setOTFeature(self, name, text): + """Set a specific OpenType feature.""" + features = self.getOTFeatures() + features[name] = text + self.setOTFeatures(features) + + # + # guides + # + + def getVGuides(self): + """Return a list of wrapped vertical guides in this RFont""" + vguides=[] + for i in range(len(self._object.vguides)): + g = RGuide(self._object.vguides[i], i) + g.setParent(self) + vguides.append(g) + return vguides + + def getHGuides(self): + """Return a list of wrapped horizontal guides in this RFont""" + hguides=[] + for i in range(len(self._object.hguides)): + g = RGuide(self._object.hguides[i], i) + g.setParent(self) + hguides.append(g) + return hguides + + def appendHGuide(self, position, angle=0): + """Append a horizontal guide""" + position = int(round(position)) + angle = int(round(angle)) + g=Guide(position, angle) + self._object.hguides.append(g) + + def appendVGuide(self, position, angle=0): + """Append a horizontal guide""" + position = int(round(position)) + angle = int(round(angle)) + g=Guide(position, angle) + self._object.vguides.append(g) + + def removeHGuide(self, guide): + """Remove a horizontal guide.""" + pos = (guide.position, guide.angle) + for g in self.getHGuides(): + if (g.position, g.angle) == pos: + del self._object.hguides[g.index] + break + + def removeVGuide(self, guide): + """Remove a vertical guide.""" + pos = (guide.position, guide.angle) + for g in self.getVGuides(): + if (g.position, g.angle) == pos: + del self._object.vguides[g.index] + break + + def clearHGuides(self): + """Clear all horizontal guides.""" + self._object.hguides.clean() + + def clearVGuides(self): + """Clear all vertical guides.""" + self._object.vguides.clean() + + + # + # generators + # + + def generate(self, outputType, path=None): + """ + generate the font. outputType is the type of font to ouput. + --Ouput Types: + 'pctype1' : PC Type 1 font (binary/PFB) + 'pcmm' : PC MultipleMaster font (PFB) + 'pctype1ascii' : PC Type 1 font (ASCII/PFA) + 'pcmmascii' : PC MultipleMaster font (ASCII/PFA) + 'unixascii' : UNIX ASCII font (ASCII/PFA) + 'mactype1' : Mac Type 1 font (generates suitcase and LWFN file) + 'otfcff' : PS OpenType (CFF-based) font (OTF) + 'otfttf' : PC TrueType/TT OpenType font (TTF) + 'macttf' : Mac TrueType font (generates suitcase) + 'macttdfont' : Mac TrueType font (generates suitcase with resources in data fork) + (doc adapted from http://dev.fontlab.net/flpydoc/) + + path can be a directory or a directory file name combo: + path="DirectoryA/DirectoryB" + path="DirectoryA/DirectoryB/MyFontName" + if no path is given, the file will be output in the same directory + as the vfb file. if no file name is given, the filename will be the + vfb file name with the appropriate suffix. + """ + outputType = outputType.lower() + if not _flGenerateTypes.has_key(outputType): + raise RoboFabError, "%s output type is not supported"%outputType + flOutputType, suffix = _flGenerateTypes[outputType] + if path is None: + filePath, fileName = os.path.split(self.path) + fileName = fileName.replace('.vfb', '') + else: + if os.path.isdir(path): + filePath = path + fileName = os.path.split(self.path)[1].replace('.vfb', '') + else: + filePath, fileName = os.path.split(path) + if '.' in fileName: + raise RoboFabError, "filename cannot contain periods.", fileName + fileName = '.'.join([fileName, suffix]) + finalPath = os.path.join(filePath, fileName) + if isinstance(finalPath, unicode): + finalPath = finalPath.encode("utf-8") + # generate is (oddly) an application level method + # rather than a font level method. because of this, + # the font must be the current font. so, make it so. + fl.ifont = self.fontIndex + fl.GenerateFont(flOutputType, finalPath) + + def writeUFO(self, path=None, doProgress=False, glyphNameToFileNameFunc=None, + doHints=False, doInfo=True, doKerning=True, doGroups=True, doLib=True, doFeatures=True, glyphs=None, formatVersion=2): + from robofab.interface.all.dialogs import ProgressBar, Message + # special glyph name to file name conversion + if glyphNameToFileNameFunc is None: + glyphNameToFileNameFunc = self.getGlyphNameToFileNameFunc() + if glyphNameToFileNameFunc is None: + from robofab.tools.glyphNameSchemes import glyphNameToShortFileName + glyphNameToFileNameFunc = glyphNameToShortFileName + # get a valid path + if not path: + if self.path is None: + Message("Please save this font first before exporting to UFO...") + return + else: + path = ufoLib.makeUFOPath(self.path) + # get the glyphs to export + if glyphs is None: + glyphs = self.keys() + # if the file exists, check the format version. + # if the format version being written is different + # from the format version of the existing UFO + # and only some files are set to be written + # raise an error. + if os.path.exists(path): + if os.path.exists(os.path.join(path, "metainfo.plist")): + reader = ufoLib.UFOReader(path) + existingFormatVersion = reader.formatVersion + if formatVersion != existingFormatVersion: + if False in [doInfo, doKerning, doGroups, doLib, doFeatures, set(glyphs) == set(self.keys())]: + Message("When overwriting an existing UFO with a different format version all files must be written.") + return + # the lib must be written if format version is 1 + if not doLib and formatVersion == 1: + Message("The lib must be written when exporting format version 1.") + return + # set up the progress bar + nonGlyphCount = [doInfo, doKerning, doGroups, doLib, doFeatures].count(True) + bar = None + if doProgress: + bar = ProgressBar("Exporting UFO", nonGlyphCount + len(glyphs)) + # try writing + try: + writer = ufoLib.UFOWriter(path, formatVersion=formatVersion) + ## We make a shallow copy if lib, since we add some stuff for export + ## that doesn't need to be retained in memory. + fontLib = dict(self.lib) + # write the font info + if doInfo: + global _IN_UFO_EXPORT + _IN_UFO_EXPORT = True + writer.writeInfo(self.info) + _IN_UFO_EXPORT = False + if bar: + bar.tick() + # write the kerning + if doKerning: + writer.writeKerning(self.kerning.asDict()) + if bar: + bar.tick() + # write the groups + if doGroups: + writer.writeGroups(self.groups) + if bar: + bar.tick() + # write the features + if doFeatures: + if formatVersion == 2: + writer.writeFeatures(self.features.text) + else: + self._writeOpenTypeFeaturesToLib(fontLib) + if bar: + bar.tick() + # write the lib + if doLib: + ## Always export the postscript font hint values to the lib in format version 1 + if formatVersion == 1: + d = self.psHints.asDict() + fontLib[postScriptHintDataLibKey] = d + ## Export the glyph order to the lib + glyphOrder = [nakedGlyph.name for nakedGlyph in self.naked().glyphs] + fontLib["public.glyphOrder"] = glyphOrder + ## export the features + if doFeatures and formatVersion == 1: + self._writeOpenTypeFeaturesToLib(fontLib) + if bar: + bar.tick() + writer.writeLib(fontLib) + if bar: + bar.tick() + # write the glyphs + if glyphs: + glyphSet = writer.getGlyphSet(glyphNameToFileNameFunc) + count = nonGlyphCount + for nakedGlyph in self.naked().glyphs: + if nakedGlyph.name not in glyphs: + continue + glyph = RGlyph(nakedGlyph) + if doHints: + hintStuff = _glyphHintsToDict(glyph.naked()) + if hintStuff: + glyph.lib[postScriptHintDataLibKey] = hintStuff + glyphSet.writeGlyph(glyph.name, glyph, glyph.drawPoints) + # remove the hint dict from the lib + if doHints and glyph.lib.has_key(postScriptHintDataLibKey): + del glyph.lib[postScriptHintDataLibKey] + if bar and not count % 10: + bar.tick(count) + count = count + 1 + glyphSet.writeContents() + # only blindly stop if the user says to + except KeyboardInterrupt: + if bar: + bar.close() + bar = None + # kill the bar + if bar: + bar.close() + + def _writeOpenTypeFeaturesToLib(self, fontLib): + # this should only be used for UFO format version 1 + flFont = self.naked() + cls = flFont.ot_classes + if cls is not None: + fontLib["org.robofab.opentype.classes"] = _normalizeLineEndings(cls).rstrip() + "\n" + if flFont.features: + features = {} + order = [] + for feature in flFont.features: + order.append(feature.tag) + features[feature.tag] = _normalizeLineEndings(feature.value).rstrip() + "\n" + fontLib["org.robofab.opentype.features"] = features + fontLib["org.robofab.opentype.featureorder"] = order + + def readUFO(self, path, doProgress=False, + doHints=False, doInfo=True, doKerning=True, doGroups=True, doLib=True, doFeatures=True, glyphs=None): + """read a .ufo into the font""" + from robofab.pens.flPen import FLPointPen + from robofab.interface.all.dialogs import ProgressBar + # start up the reader + reader = ufoLib.UFOReader(path) + glyphSet = reader.getGlyphSet() + # get a list of glyphs that should be imported + if glyphs is None: + glyphs = glyphSet.keys() + # set up the progress bar + nonGlyphCount = [doInfo, doKerning, doGroups, doLib, doFeatures].count(True) + bar = None + if doProgress: + bar = ProgressBar("Importing UFO", nonGlyphCount + len(glyphs)) + # start reading + try: + fontLib = reader.readLib() + # info + if doInfo: + reader.readInfo(self.info) + if bar: + bar.tick() + # glyphs + count = 1 + glyphOrder = self._getGlyphOrderFromLib(fontLib, glyphSet) + for glyphName in glyphOrder: + if glyphName not in glyphs: + continue + glyph = self.newGlyph(glyphName, clear=True) + pen = FLPointPen(glyph.naked()) + glyphSet.readGlyph(glyphName=glyphName, glyphObject=glyph, pointPen=pen) + if doHints: + hintData = glyph.lib.get(postScriptHintDataLibKey) + if hintData: + _dictHintsToGlyph(glyph.naked(), hintData) + # now that the hints have been extracted from the glyph + # there is no reason to keep the location in the lib. + if glyph.lib.has_key(postScriptHintDataLibKey): + del glyph.lib[postScriptHintDataLibKey] + if bar and not count % 10: + bar.tick(count) + count = count + 1 + # features + if doFeatures: + if reader.formatVersion == 1: + self._readOpenTypeFeaturesFromLib(fontLib) + else: + featureText = reader.readFeatures() + self.features.text = featureText + if bar: + bar.tick() + else: + # remove features stored in the lib + self._readOpenTypeFeaturesFromLib(fontLib, setFeatures=False) + # kerning + if doKerning: + self.kerning.clear() + self.kerning.update(reader.readKerning()) + if bar: + bar.tick() + # groups + if doGroups: + self.groups.clear() + self.groups.update(reader.readGroups()) + if bar: + bar.tick() + # hints in format version 1 + if doHints and reader.formatVersion == 1: + self.psHints._loadFromLib(fontLib) + else: + # remove hint data stored in the lib + if fontLib.has_key(postScriptHintDataLibKey): + del fontLib[postScriptHintDataLibKey] + # lib + if doLib: + self.lib.clear() + self.lib.update(fontLib) + if bar: + bar.tick() + # update the font + self.update() + # only blindly stop if the user says to + except KeyboardInterrupt: + bar.close() + bar = None + # kill the bar + if bar: + bar.close() + + def _getGlyphOrderFromLib(self, fontLib, glyphSet): + key = "public.glyphOrder" + glyphOrder = fontLib.get(key) + if glyphOrder is None: + key = "org.robofab.glyphOrder" + glyphOrder = fontLib.get(key) + if glyphOrder is not None: + # no need to keep track if the glyph order in lib once the font is loaded. + del fontLib[key] + glyphNames = [] + done = {} + for glyphName in glyphOrder: + if glyphName in glyphSet: + glyphNames.append(glyphName) + done[glyphName] = 1 + allGlyphNames = glyphSet.keys() + allGlyphNames.sort() + for glyphName in allGlyphNames: + if glyphName not in done: + glyphNames.append(glyphName) + else: + glyphNames = glyphSet.keys() + glyphNames.sort() + return glyphNames + + def _readOpenTypeFeaturesFromLib(self, fontLib, setFeatures=True): + # setFeatures may be False. in this case, this method + # should only clear the data from the lib. + classes = fontLib.get("org.robofab.opentype.classes") + if classes is not None: + del fontLib["org.robofab.opentype.classes"] + if setFeatures: + self.naked().ot_classes = classes + features = fontLib.get("org.robofab.opentype.features") + if features is not None: + order = fontLib.get("org.robofab.opentype.featureorder") + if order is None: + # for UFOs saved without the feature order, do the same as before. + order = features.keys() + order.sort() + else: + del fontLib["org.robofab.opentype.featureorder"] + del fontLib["org.robofab.opentype.features"] + #features = features.items() + orderedFeatures = [] + for tag in order: + oneFeature = features.get(tag) + if oneFeature is not None: + orderedFeatures.append((tag, oneFeature)) + if setFeatures: + self.naked().features.clean() + for tag, src in orderedFeatures: + self.naked().features.append(Feature(tag, src)) + + + +class RGlyph(BaseGlyph): + """RoboFab wrapper for FL Glyph object""" + + _title = "FLGlyph" + + def __init__(self, flGlyph): + #BaseGlyph.__init__(self) + if flGlyph is None: + raise RoboFabError, "RGlyph: there's nothing to wrap!?" + self._object = flGlyph + self._lib = {} + self._contours = None + + def __getitem__(self, index): + return self.contours[index] + + def __delitem__(self, index): + self._object.DeleteContour(index) + self._invalidateContours() + + def __len__(self): + return len(self.contours) + + lib = property(_get_lib, _set_lib, doc="glyph lib object") + + def _invalidateContours(self): + self._contours = None + + def _buildContours(self): + self._contours = [] + for contourIndex in range(self._object.GetContoursNumber()): + c = RContour(contourIndex) + c.setParent(self) + c._buildSegments() + self._contours.append(c) + + # + # attribute handlers + # + + def _get_index(self): + return self._object.parent.FindGlyph(self.name) + + index = property(_get_index, doc="return the index of the glyph in the font") + + def _get_name(self): + return self._object.name + + def _set_name(self, value): + self._object.name = value + + name = property(_get_name, _set_name, doc="name") + + def _get_psName(self): + return self._object.name + + def _set_psName(self, value): + self._object.name = value + + psName = property(_get_psName, _set_psName, doc="name") + + def _get_baseName(self): + return self._object.name.split('.')[0] + + baseName = property(_get_baseName, doc="") + + def _get_unicode(self): + return self._object.unicode + + def _set_unicode(self, value): + self._object.unicode = value + + unicode = property(_get_unicode, _set_unicode, doc="unicode") + + def _get_unicodes(self): + return self._object.unicodes + + def _set_unicodes(self, value): + self._object.unicodes = value + + unicodes = property(_get_unicodes, _set_unicodes, doc="unicodes") + + def _get_width(self): + return self._object.width + + def _set_width(self, value): + value = int(round(value)) + self._object.width = value + + width = property(_get_width, _set_width, doc="the width") + + def _get_box(self): + if not len(self.contours) and not len(self.components): + return (0, 0, 0, 0) + r = self._object.GetBoundingRect() + return (int(round(r.ll.x)), int(round(r.ll.y)), int(round(r.ur.x)), int(round(r.ur.y))) + + box = property(_get_box, doc="box of glyph as a tuple (xMin, yMin, xMax, yMax)") + + def _get_selected(self): + if fl.Selected(self._object.parent.FindGlyph(self._object.name)): + return 1 + else: + return 0 + + def _set_selected(self, value): + fl.Select(self._object.name, value) + + selected = property(_get_selected, _set_selected, doc="Select or deselect the glyph in the font window") + + def _get_mark(self): + return self._object.mark + + def _set_mark(self, value): + self._object.mark = value + + mark = property(_get_mark, _set_mark, doc="mark") + + def _get_note(self): + s = self._object.note + if s is None: + return s + return unicode(s, LOCAL_ENCODING) + + def _set_note(self, value): + if value is None: + value = '' + if type(value) == type(u""): + value = value.encode(LOCAL_ENCODING) + self._object.note = value + + note = property(_get_note, _set_note, doc="note") + + def _get_psHints(self): + # get an object representing the postscript zone information + return PostScriptGlyphHintValues(self) + + psHints = property(_get_psHints, doc="postscript hint data") + + # + # necessary evil + # + + def update(self): + """Don't forget to update the glyph when you are done.""" + fl.UpdateGlyph(self._object.parent.FindGlyph(self._object.name)) + + # + # methods to make RGlyph compatible with FL.Glyph + # ##are these still needed? + # + + def GetBoundingRect(self, masterIndex): + """FL compatibility""" + return self._object.GetBoundingRect(masterIndex) + + def GetMetrics(self, masterIndex): + """FL compatibility""" + return self._object.GetMetrics(masterIndex) + + def SetMetrics(self, value, masterIndex): + """FL compatibility""" + return self._object.SetMetrics(value, masterIndex) + + # + # object builders + # + + def _get_anchors(self): + return self.getAnchors() + + anchors = property(_get_anchors, doc="allow for iteration through glyph.anchors") + + def _get_components(self): + return self.getComponents() + + components = property(_get_components, doc="allow for iteration through glyph.components") + + def _get_contours(self): + if self._contours is None: + self._buildContours() + return self._contours + + contours = property(_get_contours, doc="allow for iteration through glyph.contours") + + def getAnchors(self): + """Return a list of wrapped anchors in this RGlyph.""" + anchors=[] + for i in range(len(self._object.anchors)): + a = RAnchor(self._object.anchors[i], i) + a.setParent(self) + anchors.append(a) + return anchors + + def getComponents(self): + """Return a list of wrapped components in this RGlyph.""" + components=[] + for i in range(len(self._object.components)): + c = RComponent(self._object.components[i], i) + c.setParent(self) + components.append(c) + return components + + def getVGuides(self): + """Return a list of wrapped vertical guides in this RGlyph""" + vguides=[] + for i in range(len(self._object.vguides)): + g = RGuide(self._object.vguides[i], i) + g.setParent(self) + vguides.append(g) + return vguides + + def getHGuides(self): + """Return a list of wrapped horizontal guides in this RGlyph""" + hguides=[] + for i in range(len(self._object.hguides)): + g = RGuide(self._object.hguides[i], i) + g.setParent(self) + hguides.append(g) + return hguides + + # + # tools + # + + def getPointPen(self): + self._invalidateContours() + # Now just don't muck with glyph.contours before you're done drawing... + return FLPointPen(self) + + def appendComponent(self, baseGlyph, offset=(0, 0), scale=(1, 1)): + """Append a component to the glyph. x and y are optional offset values""" + offset = roundPt((offset[0], offset[1])) + p = FLPointPen(self.naked()) + xx, yy = scale + dx, dy = offset + p.addComponent(baseGlyph, (xx, 0, 0, yy, dx, dy)) + + def appendAnchor(self, name, position): + """Append an anchor to the glyph""" + value = roundPt((position[0], position[1])) + anchor = Anchor(name, value[0], value[1]) + self._object.anchors.append(anchor) + + def appendHGuide(self, position, angle=0): + """Append a horizontal guide""" + position = int(round(position)) + g = Guide(position, angle) + self._object.hguides.append(g) + + def appendVGuide(self, position, angle=0): + """Append a horizontal guide""" + position = int(round(position)) + g = Guide(position, angle) + self._object.vguides.append(g) + + def clearContours(self): + self._object.Clear() + self._invalidateContours() + + def clearComponents(self): + """Clear all components.""" + self._object.components.clean() + + def clearAnchors(self): + """Clear all anchors.""" + self._object.anchors.clean() + + def clearHGuides(self): + """Clear all horizontal guides.""" + self._object.hguides.clean() + + def clearVGuides(self): + """Clear all vertical guides.""" + self._object.vguides.clean() + + def removeComponent(self, component): + """Remove a specific component from the glyph. This only works + if the glyph does not have duplicate components in the same location.""" + pos = (component.baseGlyph, component.offset, component.scale) + a = self.getComponents() + found = [] + for i in a: + if (i.baseGlyph, i.offset, i.scale) == pos: + found.append(i) + if len(found) > 1: + raise RoboFabError, 'Found more than one possible component to remove' + elif len(found) == 1: + del self._object.components[found[0].index] + else: + raise RoboFabError, 'Component does not exist' + + def removeContour(self, index): + """remove a specific contour from the glyph""" + self._object.DeleteContour(index) + self._invalidateContours() + + def removeAnchor(self, anchor): + """Remove a specific anchor from the glyph. This only works + if the glyph does not have anchors with duplicate names + in exactly the same location with the same mark.""" + pos = (anchor.name, anchor.position, anchor.mark) + a = self.getAnchors() + found = [] + for i in a: + if (i.name, i.position, i.mark) == pos: + found.append(i) + if len(found) > 1: + raise RoboFabError, 'Found more than one possible anchor to remove' + elif len(found) == 1: + del self._object.anchors[found[0].index] + else: + raise RoboFabError, 'Anchor does not exist' + + def removeHGuide(self, guide): + """Remove a horizontal guide.""" + pos = (guide.position, guide.angle) + for g in self.getHGuides(): + if (g.position, g.angle) == pos: + del self._object.hguides[g.index] + break + + def removeVGuide(self, guide): + """Remove a vertical guide.""" + pos = (guide.position, guide.angle) + for g in self.getVGuides(): + if (g.position, g.angle) == pos: + del self._object.vguides[g.index] + break + + def center(self, padding=None): + """Equalise sidebearings, set to padding if wanted.""" + left = self.leftMargin + right = self.rightMargin + if padding: + e_left = e_right = padding + else: + e_left = (left + right)/2 + e_right = (left + right) - e_left + self.leftMargin= e_left + self.rightMargin= e_right + + def removeOverlap(self): + """Remove overlap""" + self._object.RemoveOverlap() + self._invalidateContours() + + def decompose(self): + """Decompose all components""" + self._object.Decompose() + self._invalidateContours() + + ##broken! + #def removeHints(self): + # """Remove the hints.""" + # self._object.RemoveHints() + + def autoHint(self): + """Automatically generate type 1 hints.""" + self._object.Autohint() + + def move(self, (x, y), contours=True, components=True, anchors=True): + """Move a glyph's items that are flagged as True""" + x, y = roundPt((x, y)) + self._object.Shift(Point(x, y)) + for c in self.getComponents(): + c.move((x, y)) + for a in self.getAnchors(): + a.move((x, y)) + + def clear(self, contours=True, components=True, anchors=True, guides=True, hints=True): + """Clear all items marked as true from the glyph""" + if contours: + self._object.Clear() + self._invalidateContours() + if components: + self._object.components.clean() + if anchors: + self._object.anchors.clean() + if guides: + self._object.hguides.clean() + self._object.vguides.clean() + if hints: + # RemoveHints requires an "integer mode" argument + # but it is not documented. from some simple experiments + # i deduced that + # 1 = horizontal hints and links, + # 2 = vertical hints and links + # 3 = all hints and links + self._object.RemoveHints(3) + + # + # special treatment for GlyphMath support in FontLab + # + + def _getMathDestination(self): + from robofab.objects.objectsRF import RGlyph as _RGlyph + return _RGlyph() + + def copy(self, aParent=None): + """Make a copy of this glyph. + Note: the copy is not a duplicate fontlab glyph, but + a RF RGlyph with the same outlines. The new glyph is + not part of the fontlab font in any way. Use font.appendGlyph(glyph) + to get it in a FontLab glyph again.""" + from robofab.objects.objectsRF import RGlyph as _RGlyph + newGlyph = _RGlyph() + newGlyph.appendGlyph(self) + for attr in GLYPH_COPY_ATTRS: + value = getattr(self, attr) + setattr(newGlyph, attr, value) + # hints + doHints = False + parent = self.getParent() + if parent is not None and parent._supportHints: + hintStuff = _glyphHintsToDict(self.naked()) + if hintStuff: + newGlyph.lib[postScriptHintDataLibKey] = hintStuff + if aParent is not None: + newGlyph.setParent(aParent) + elif self.getParent() is not None: + newGlyph.setParent(self.getParent()) + return newGlyph + + def __mul__(self, factor): + return self.copy() *factor + + __rmul__ = __mul__ + + def __sub__(self, other): + return self.copy() - other.copy() + + def __add__(self, other): + return self.copy() + other.copy() + + + +class RContour(BaseContour): + + """RoboFab wrapper for non FL contour object""" + + _title = "FLContour" + + def __init__(self, index): + self._index = index + self._parentGlyph = None + self.segments = [] + + def __len__(self): + return len(self.points) + + def _buildSegments(self): + ####################### + # Notes about FL node contour structure + ####################### + # for TT curves, FL lists them as seperate nodes: + # [move, off, off, off, line, off, off] + # and, this list is sequential. after the last on curve, + # it is possible (and likely) that there will be more offCurves + # in our segment object, these should be associated with the + # first segment in the contour. + # + # for PS curves, it is a very different scenerio. + # curve nodes contain points: + # [on, off, off] + # and the list is not in sequential order. the first point in + # the list is the on curve and the subsequent points are the off + # curve points leading up to that on curve. + # + # it is very important to remember these structures when trying + # to understand the code below + + self.segments = [] + offList = [] + nodes = self._nakedParent.nodes + for index in range(self._nodeLength): + x = index + self._startNodeIndex + node = nodes[x] + # we do have a loose off curve. deal with it. + if node.type == flOFFCURVE: + offList.append(x) + # we are not dealing with a loose off curve + else: + s = RSegment(x) + s.setParent(self) + # but do we have a collection of loose off curves above? + # if so, apply them to the segment, and clear the list + if len(offList) != 0: + s._looseOffCurve = offList + offList = [] + self.segments.append(s) + # do we have some off curves now that the contour is complete? + if len(offList) != 0: + # ugh. apply them to the first segment + self.segments[0]._looseOffCurve = offList + + def setParent(self, parentGlyph): + self._parentGlyph = parentGlyph + + def getParent(self): + return self._parentGlyph + + def _get__nakedParent(self): + return self._parentGlyph.naked() + + _nakedParent = property(_get__nakedParent, doc="") + + def _get__startNodeIndex(self): + return self._nakedParent.GetContourBegin(self._index) + + _startNodeIndex = property(_get__startNodeIndex, doc="") + + def _get__nodeLength(self): + return self._nakedParent.GetContourLength(self._index) + + _nodeLength = property(_get__nodeLength, doc="") + + def _get__lastNodeIndex(self): + return self._startNodeIndex + self._nodeLength - 1 + + _lastNodeIndex = property(_get__lastNodeIndex, doc="") + + def _previousNodeIndex(self, index): + return (index - 1) % self._nodeLength + + def _nextNodeIndex(self, index): + return (index + 1) % self._nodeLength + + def _getNode(self, index): + return self._nodes[index] + + def _get__nodes(self): + nodes = [] + for node in self._nakedParent.nodes[self._startNodeIndex:self._startNodeIndex+self._nodeLength-1]: + nodes.append(node) + return nodes + + _nodes = property(_get__nodes, doc="") + + def _get_points(self): + points = [] + for segment in self.segments: + for point in segment.points: + points.append(point) + return points + + points = property(_get_points, doc="") + + def _get_bPoints(self): + bPoints = [] + for segment in self.segments: + bp = RBPoint(segment.index) + bp.setParent(self) + bPoints.append(bp) + return bPoints + + bPoints = property(_get_bPoints, doc="") + + def _get_index(self): + return self._index + + def _set_index(self, index): + if index != self._index: + self._nakedParent.ReorderContour(self._index, index) + # reorder and set the _index of the existing RContour objects + # this will be a better solution than reconstructing all the objects + # segment objects will still, sadly, have to be reconstructed + contourList = self.getParent().contours + contourList.insert(index, contourList.pop(self._index)) + for i in range(len(contourList)): + contourList[i]._index = i + contourList[i]._buildSegments() + + + index = property(_get_index, _set_index, doc="the index of the contour") + + def _get_selected(self): + selected = 0 + nodes = self._nodes + for node in nodes: + if node.selected == 1: + selected = 1 + break + return selected + + def _set_selected(self, value): + if value == 1: + self._nakedParent.SelectContour(self._index) + else: + for node in self._nodes: + node.selected = value + + selected = property(_get_selected, _set_selected, doc="selection of the contour: 1-selected or 0-unselected") + + def appendSegment(self, segmentType, points, smooth=False): + segment = self.insertSegment(index=self._nodeLength, segmentType=segmentType, points=points, smooth=smooth) + return segment + + def insertSegment(self, index, segmentType, points, smooth=False): + """insert a seggment into the contour""" + # do a qcurve insertion + if segmentType == QCURVE: + count = 0 + for point in points[:-1]: + newNode = Node(flOFFCURVE, Point(point[0], point[1])) + self._nakedParent.Insert(newNode, self._startNodeIndex + index + count) + count = count + 1 + newNode = Node(flLINE, Point(points[-1][0], points[-1][1])) + self._nakedParent.Insert(newNode, self._startNodeIndex + index +len(points) - 1) + # do a regular insertion + else: + onX, onY = points[-1] + newNode = Node(_rfToFLSegmentType(segmentType), Point(onX, onY)) + # fix the off curves in case the user is inserting a curve + # but is not specifying off curve points + if segmentType == CURVE and len(points) == 1: + pSeg = self._prevSegment(index) + pOn = pSeg.onCurve + newNode.points[1].Assign(Point(pOn.x, pOn.y)) + newNode.points[2].Assign(Point(onX, onY)) + for pointIndex in range(len(points[:-1])): + x, y = points[pointIndex] + newNode.points[1 + pointIndex].Assign(Point(x, y)) + if smooth: + newNode.alignment = flSMOOTH + self._nakedParent.Insert(newNode, self._startNodeIndex + index) + self._buildSegments() + return self.segments[index] + + def removeSegment(self, index): + """remove a segment from the contour""" + segment = self.segments[index] + # we have a qcurve. umph. + if segment.type == QCURVE: + indexList = [segment._nodeIndex] + segment._looseOffCurve + indexList.sort() + indexList.reverse() + parent = self._nakedParent + for nodeIndex in indexList: + parent.DeleteNode(nodeIndex) + # we have a more sane structure to follow + else: + # store some info for later + next = self._nextSegment(index) + nextOffA = None + nextOffB = None + nextType = next.type + if nextType != LINE and nextType != MOVE: + pA = next.offCurve[0] + nextOffA = (pA.x, pA.y) + pB = next.offCurve[-1] + nextOffB = (pB.x, pB.y) + nodeIndex = segment._nodeIndex + self._nakedParent.DeleteNode(nodeIndex) + self._buildSegments() + # now we must override FL guessing about offCurves + next = self._nextSegment(index - 1) + nextType = next.type + if nextType != LINE and nextType != MOVE: + pA = next.offCurve[0] + pB = next.offCurve[-1] + pA.x, pA.y = nextOffA + pB.x, pB.y = nextOffB + + def reverseContour(self): + """reverse contour direction""" + self._nakedParent.ReverseContour(self._index) + self._buildSegments() + + def setStartSegment(self, segmentIndex): + """set the first node on the contour""" + self._nakedParent.SetStartNode(self._startNodeIndex + segmentIndex) + self.getParent()._invalidateContours() + self.getParent()._buildContours() + + def copy(self, aParent=None): + """Copy this object -- result is an ObjectsRF flavored object. + There is no way to make this work using FontLab objects. + Copy is mainly used for glyphmath. + """ + raise RoboFabError, "copy() for objectsFL.RContour is not implemented." + + + +class RSegment(BaseSegment): + + _title = "FLSegment" + + def __init__(self, flNodeIndex): + BaseSegment.__init__(self) + self._nodeIndex = flNodeIndex + self._looseOffCurve = [] #a list of indexes to loose off curve nodes + + def _get__node(self): + glyph = self.getParent()._nakedParent + return glyph.nodes[self._nodeIndex] + + _node = property(_get__node, doc="") + + def _get_qOffCurve(self): + nodes = self.getParent()._nakedParent.nodes + off = [] + for x in self._looseOffCurve: + off.append(nodes[x]) + return off + + _qOffCurve = property(_get_qOffCurve, doc="free floating off curve nodes in the segment") + + def _get_index(self): + contour = self.getParent() + return self._nodeIndex - contour._startNodeIndex + + index = property(_get_index, doc="") + + def _isQCurve(self): + # loose off curves only appear in q curves + if len(self._looseOffCurve) != 0: + return True + return False + + def _get_type(self): + if self._isQCurve(): + return QCURVE + return _flToRFSegmentType(self._node.type) + + def _set_type(self, segmentType): + if self._isQCurve(): + raise RoboFabError, 'qcurve point types cannot be changed' + oldNode = self._node + oldType = oldNode.type + oldPointType = _flToRFSegmentType(oldType) + if oldPointType == MOVE: + raise RoboFabError, '%s point types cannot be changed'%oldPointType + if segmentType == MOVE or segmentType == OFFCURVE: + raise RoboFabError, '%s point types cannot be assigned'%oldPointType + if oldPointType == segmentType: + return + oldNode.type = _rfToFLSegmentType(segmentType) + + type = property(_get_type, _set_type, doc="") + + def _get_smooth(self): + alignment = self._node.alignment + if alignment == flSMOOTH or alignment == flFIXED: + return True + return False + + def _set_smooth(self, value): + if value: + self._node.alignment = flSMOOTH + else: + self._node.alignment = flSHARP + + smooth = property(_get_smooth, _set_smooth, doc="") + + def _get_points(self): + points = [] + node = self._node + # gather the off curves + # + # are we dealing with a qCurve? ugh. + # gather the loose off curves + if self._isQCurve(): + off = self._qOffCurve + x = 0 + for n in off: + p = RPoint(0) + p.setParent(self) + p._qOffIndex = x + points.append(p) + x = x + 1 + # otherwise get the points associated with the node + else: + index = 1 + for point in node.points[1:]: + p = RPoint(index) + p.setParent(self) + points.append(p) + index = index + 1 + # the last point should always be the on curve + p = RPoint(0) + p.setParent(self) + points.append(p) + return points + + points = property(_get_points, doc="") + + def _get_selected(self): + return self._node.selected + + def _set_selected(self, value): + self._node.selected = value + + selected = property(_get_selected, _set_selected, doc="") + + def move(self, (x, y)): + x, y = roundPt((x, y)) + self._node.Shift(Point(x, y)) + if self._isQCurve(): + qOff = self._qOffCurve + for node in qOff: + node.Shift(Point(x, y)) + + def copy(self, aParent=None): + """Copy this object -- result is an ObjectsRF flavored object. + There is no way to make this work using FontLab objects. + Copy is mainly used for glyphmath. + """ + raise RoboFabError, "copy() for objectsFL.RSegment is not implemented." + + + +class RPoint(BasePoint): + + _title = "FLPoint" + + def __init__(self, pointIndex): + #BasePoint.__init__(self) + self._pointIndex = pointIndex + self._qOffIndex = None + + def _get__parentGlyph(self): + return self._parentContour.getParent() + + _parentGlyph = property(_get__parentGlyph, doc="") + + def _get__parentContour(self): + return self._parentSegment.getParent() + + _parentContour = property(_get__parentContour, doc="") + + def _get__parentSegment(self): + return self.getParent() + + _parentSegment = property(_get__parentSegment, doc="") + + def _get__node(self): + if self._qOffIndex is not None: + return self.getParent()._qOffCurve[self._qOffIndex] + return self.getParent()._node + + _node = property(_get__node, doc="") + + def _get__point(self): + return self._node.points[self._pointIndex] + + _point = property(_get__point, doc="") + + def _get_x(self): + return self._point.x + + def _set_x(self, value): + value = int(round(value)) + self._point.x = value + + x = property(_get_x, _set_x, doc="") + + def _get_y(self): + return self._point.y + + def _set_y(self, value): + value = int(round(value)) + self._point.y = value + + y = property(_get_y, _set_y, doc="") + + def _get_type(self): + if self._pointIndex == 0: + # FL store quad contour data as a list of off curves and lines + # (see note in RContour._buildSegments). So, we need to do + # a bit of trickery to return a decent point type. + # if the straight FL node type is off curve, it is a loose + # quad off curve. return that. + tp = _flToRFSegmentType(self._node.type) + if tp == OFFCURVE: + return OFFCURVE + # otherwise we are dealing with an on curve. in this case, + # we attempt to get the parent segment type and return it. + segment = self.getParent() + if segment is not None: + return segment.type + # we must not have a segment, fall back to straight conversion + return tp + return OFFCURVE + + type = property(_get_type, doc="") + + def _set_selected(self, value): + if self._pointIndex == 0: + self._node.selected = value + + def _get_selected(self): + if self._pointIndex == 0: + return self._node.selected + return False + + selected = property(_get_selected, _set_selected, doc="") + + def move(self, (x, y)): + x, y = roundPt((x, y)) + self._point.Shift(Point(x, y)) + + def scale(self, (x, y), center=(0, 0)): + centerX, centerY = roundPt(center) + point = self._point + point.x, point.y = _scalePointFromCenter((point.x, point.y), (x, y), (centerX, centerY)) + + def copy(self, aParent=None): + """Copy this object -- result is an ObjectsRF flavored object. + There is no way to make this work using FontLab objects. + Copy is mainly used for glyphmath. + """ + raise RoboFabError, "copy() for objectsFL.RPoint is not implemented." + + +class RBPoint(BaseBPoint): + + _title = "FLBPoint" + + def __init__(self, segmentIndex): + #BaseBPoint.__init__(self) + self._segmentIndex = segmentIndex + + def _get__parentSegment(self): + return self.getParent().segments[self._segmentIndex] + + _parentSegment = property(_get__parentSegment, doc="") + + def _get_index(self): + return self._segmentIndex + + index = property(_get_index, doc="") + + def _get_selected(self): + return self._parentSegment.selected + + def _set_selected(self, value): + self._parentSegment.selected = value + + selected = property(_get_selected, _set_selected, doc="") + + def copy(self, aParent=None): + """Copy this object -- result is an ObjectsRF flavored object. + There is no way to make this work using FontLab objects. + Copy is mainly used for glyphmath. + """ + raise RoboFabError, "copy() for objectsFL.RBPoint is not implemented." + + +class RComponent(BaseComponent): + + """RoboFab wrapper for FL Component object""" + + _title = "FLComponent" + + def __init__(self, flComponent, index): + BaseComponent.__init__(self) + self._object = flComponent + self._index=index + + def _get_index(self): + return self._index + + index = property(_get_index, doc="index of component") + + def _get_baseGlyph(self): + return self._object.parent.parent[self._object.index].name + + baseGlyph = property(_get_baseGlyph, doc="") + + def _get_offset(self): + return (int(self._object.delta.x), int(self._object.delta.y)) + + def _set_offset(self, value): + value = roundPt((value[0], value[1])) + self._object.delta=Point(value[0], value[1]) + + offset = property(_get_offset, _set_offset, doc="the offset of the component") + + def _get_scale(self): + return (self._object.scale.x, self._object.scale.y) + + def _set_scale(self, (x, y)): + self._object.scale=Point(x, y) + + scale = property(_get_scale, _set_scale, doc="the scale of the component") + + def move(self, (x, y)): + """Move the component""" + x, y = roundPt((x, y)) + self._object.delta=Point(self._object.delta.x+x, self._object.delta.y+y) + + def decompose(self): + """Decompose the component""" + self._object.Paste() + + def copy(self, aParent=None): + """Copy this object -- result is an ObjectsRF flavored object. + There is no way to make this work using FontLab objects. + Copy is mainly used for glyphmath. + """ + raise RoboFabError, "copy() for objectsFL.RComponent is not implemented." + + + +class RAnchor(BaseAnchor): + """RoboFab wrapper for FL Anchor object""" + + _title = "FLAnchor" + + def __init__(self, flAnchor, index): + BaseAnchor.__init__(self) + self._object = flAnchor + self._index = index + + def _get_y(self): + return self._object.y + + def _set_y(self, value): + self._object.y = int(round(value)) + + y = property(_get_y, _set_y, doc="y") + + def _get_x(self): + return self._object.x + + def _set_x(self, value): + self._object.x = int(round(value)) + + x = property(_get_x, _set_x, doc="x") + + def _get_name(self): + return self._object.name + + def _set_name(self, value): + self._object.name = value + + name = property(_get_name, _set_name, doc="name") + + def _get_mark(self): + return self._object.mark + + def _set_mark(self, value): + self._object.mark = value + + mark = property(_get_mark, _set_mark, doc="mark") + + def _get_index(self): + return self._index + + index = property(_get_index, doc="index of the anchor") + + def _get_position(self): + return (self._object.x, self._object.y) + + def _set_position(self, value): + value = roundPt((value[0], value[1])) + self._object.x=value[0] + self._object.y=value[1] + + position = property(_get_position, _set_position, doc="position of the anchor") + + + +class RGuide(BaseGuide): + + """RoboFab wrapper for FL Guide object""" + + _title = "FLGuide" + + def __init__(self, flGuide, index): + BaseGuide.__init__(self) + self._object = flGuide + self._index = index + + 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 "<Robofab guide wrapper for %s>"%parent + + def _get_position(self): + return self._object.position + + def _set_position(self, value): + self._object.position = value + + position = property(_get_position, _set_position, doc="position") + + def _get_angle(self): + return self._object.angle + + def _set_angle(self, value): + self._object.angle = value + + angle = property(_get_angle, _set_angle, doc="angle") + + def _get_index(self): + return self._index + + index = property(_get_index, doc="index of the guide") + + +class RGroups(BaseGroups): + + """RoboFab wrapper for FL group data""" + + _title = "FLGroups" + + def __init__(self, aDict): + self.update(aDict) + + def __setitem__(self, key, value): + # override baseclass so that data is stored in FL classes + 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(RGroups, self).__setitem__(key, value) + self._setFLGroups() + + def __delitem__(self, key): + # override baseclass so that data is stored in FL classes + super(RGroups, self).__delitem__(key) + self._setFLGroups() + + def _setFLGroups(self): + # set the group data into the font. + if self.getParent() is not None: + groups = [] + for i in self.keys(): + value = ' '.join(self[i]) + groups.append(': '.join([i, value])) + groups.sort() + self.getParent().naked().classes = groups + + def update(self, aDict): + # override baseclass so that data is stored in FL classes + super(RGroups, self).update(aDict) + self._setFLGroups() + + def clear(self): + # override baseclass so that data is stored in FL classes + super(RGroups, self).clear() + self._setFLGroups() + + def pop(self, key): + # override baseclass so that data is stored in FL classes + i = super(RGroups, self).pop(key) + self._setFLGroups() + return i + + def popitem(self): + # override baseclass so that data is stored in FL classes + i = super(RGroups, self).popitem() + self._setFLGroups() + return i + + def setdefault(self, key, value=None): + # override baseclass so that data is stored in FL classes + i = super(RGroups, self).setdefault(key, value) + self._setFLGroups() + return i + + +class RKerning(BaseKerning): + + """RoboFab wrapper for FL Kerning data""" + + _title = "FLKerning" + + 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: + #see note about setting kerning values to 0 below + self._setFLKerning(pair, 0) + del self._kerning[pair] + else: + #self._kerning[pair] = value + self._setFLKerning(pair, value) + + def _setFLKerning(self, pair, value): + # write a pair back into the font + # + # this is fairly speedy, but setting a pair to 0 is roughly + # 2-3 times slower than setting a real value. this is because + # of all the hoops that must be jumped through to keep FL + # from storing kerning pairs with a value of 0. + parentFont = self.getParent().naked() + left = parentFont[pair[0]] + right = parentFont.FindGlyph(pair[1]) + # the left glyph doesn not exist + if left is None: + return + # the right glyph doesn not exist + if right == -1: + return + self._kerning[pair] = value + leftName = pair[0] + value = int(round(value)) + # pairs set to 0 need to be handled carefully. FL will allow + # for pairs to have a value of 0 (!?), so we must catch them + # when they pop up and make sure that the pair is actually + # removed from the font. + if value == 0: + foundPair = False + # if the value is 0, we don't need to construct a pair + # we just need to make sure that the pair is not in the list + pairs = [] + # so, go through all the pairs and add them to a new list + for flPair in left.kerning: + # we have found the pair. flag it. + if flPair.key == right: + foundPair = True + # not the pair. add it to the list. + else: + pairs.append((flPair.key, flPair.value)) + # if we found it, write it back to the glyph. + if foundPair: + left.kerning = [] + for p in pairs: + new = KerningPair(p[0], p[1]) + left.kerning.append(new) + else: + # non-zero pairs are a bit easier to handle + # we just need to look to see if the pair exists + # if so, change the value and stop the loop. + # if not, add a new pair to the glyph + self._kerning[pair] = value + foundPair = False + for flPair in left.kerning: + if flPair.key == right: + flPair.value = value + foundPair = True + break + if not foundPair: + p = KerningPair(right, value) + left.kerning.append(p) + + def update(self, kerningDict): + """replace kerning data with the data in the given kerningDict""" + # override base class here for speed + parentFont = self.getParent().naked() + # add existing data to the new kerning dict is not being replaced + for pair in self.keys(): + if not kerningDict.has_key(pair): + kerningDict[pair] = self._kerning[pair] + # now clear the existing kerning to make sure that + # all the kerning in residing in the glyphs is gone + self.clear() + self._kerning = kerningDict + kDict = {} + # nest the pairs into a dict keyed by the left glyph + # {'A':{'A':-10, 'B':20, ...}, 'B':{...}, ...} + for left, right in kerningDict.keys(): + value = kerningDict[left, right] + if not left in kDict: + kDict[left] = {} + kDict[left][right] = value + for left in kDict.keys(): + leftGlyph = parentFont[left] + if leftGlyph is not None: + for right in kDict[left].keys(): + value = kDict[left][right] + if value != 0: + rightIndex = parentFont.FindGlyph(right) + if rightIndex != -1: + p = KerningPair(rightIndex, value) + leftGlyph.kerning.append(p) + + def clear(self): + """clear all kerning""" + # override base class here for speed + self._kerning = {} + for glyph in self.getParent().naked().glyphs: + glyph.kerning = [] + + def __add__(self, other): + """Math operations on FL Kerning objects return RF Kerning objects + as they need to be orphaned objects and FL can't deal with that.""" + from sets import Set + from robofab.objects.objectsRF import RKerning as _RKerning + new = _RKerning() + 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): + """Math operations on FL Kerning objects return RF Kerning objects + as they need to be orphaned objects and FL can't deal with that.""" + from sets import Set + from robofab.objects.objectsRF import RKerning as _RKerning + new = _RKerning() + 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): + """Math operations on FL Kerning objects return RF Kerning objects + as they need to be orphaned objects and FL can't deal with that.""" + from robofab.objects.objectsRF import RKerning as _RKerning + new = _RKerning() + for name, value in self.items(): + new[name] = value * factor + return new + + __rmul__ = __mul__ + + def __div__(self, factor): + """Math operations on FL Kerning objects return RF Kerning objects + as they need to be orphaned objects and FL can't deal with that.""" + if factor == 0: + raise ZeroDivisionError + return self.__mul__(1.0/factor) + + +class RLib(BaseLib): + + """RoboFab wrapper for FL lib""" + + # XXX: As of FL 4.6 the customdata field in glyph objects is busted. + # storing anything there causes the glyph to become uneditable. + # however, the customdata field in font objects is stable. + + def __init__(self, aDict): + self.update(aDict) + + def __setitem__(self, key, value): + # override baseclass so that data is stored in customdata field + super(RLib, self).__setitem__(key, value) + self._stashLib() + + def __delitem__(self, key): + # override baseclass so that data is stored in customdata field + super(RLib, self).__delitem__(key) + self._stashLib() + + def _stashLib(self): + # write the plist into the customdata field of the FL object + if self.getParent() is None: + return + if not self: + data = None + elif len(self) == 1 and "org.robofab.fontlab.customdata" in self: + data = self["org.robofab.fontlab.customdata"].data + else: + f = StringIO() + writePlist(self, f) + data = f.getvalue() + f.close() + parent = self.getParent() + parent.naked().customdata = data + + def update(self, aDict): + # override baseclass so that data is stored in customdata field + super(RLib, self).update(aDict) + self._stashLib() + + def clear(self): + # override baseclass so that data is stored in customdata field + super(RLib, self).clear() + self._stashLib() + + def pop(self, key): + # override baseclass so that data is stored in customdata field + i = super(RLib, self).pop(key) + self._stashLib() + return i + + def popitem(self): + # override baseclass so that data is stored in customdata field + i = super(RLib, self).popitem() + self._stashLib() + return i + + def setdefault(self, key, value=None): + # override baseclass so that data is stored in customdata field + i = super(RLib, self).setdefault(key, value) + self._stashLib() + return i + + +def _infoMapDict(**kwargs): + default = dict( + nakedAttribute=None, + type=None, + requiresSetNum=False, + masterSpecific=False, + libLocation=None, + specialGetSet=False + ) + default.update(kwargs) + return default + +def _flipDict(d): + f = {} + for k, v in d.items(): + f[v] = k + return f + +_styleMapStyleName_fromFL = { + 64 : "regular", + 1 : "italic", + 32 : "bold", + 33 : "bold italic" +} +_styleMapStyleName_toFL = _flipDict(_styleMapStyleName_fromFL) + +_postscriptWindowsCharacterSet_fromFL = { + 0 : 1, + 1 : 2, + 2 : 3, + 77 : 4, + 128 : 5, + 129 : 6, + 130 : 7, + 134 : 8, + 136 : 9, + 161 : 10, + 162 : 11, + 163 : 12, + 177 : 13, + 178 : 14, + 186 : 15, + 200 : 16, + 204 : 17, + 222 : 18, + 238 : 19, + 255 : 20, +} +_postscriptWindowsCharacterSet_toFL = _flipDict(_postscriptWindowsCharacterSet_fromFL) + +_openTypeOS2Type_toFL = { + 1 : 0x0002, + 2 : 0x0004, + 3 : 0x0008, + 8 : 0x0100, + 9 : 0x0200, +} +_openTypeOS2Type_fromFL = _flipDict(_openTypeOS2Type_toFL) + +_openTypeOS2WidthClass_fromFL = { + "Ultra-condensed" : 1, + "Extra-condensed" : 2, + "Condensed" : 3, + "Semi-condensed" : 4, + "Medium (normal)" : 5, + "Semi-expanded" : 6, + "Expanded" : 7, + "Extra-expanded" : 8, + "Ultra-expanded" : 9, +} +_openTypeOS2WidthClass_toFL = _flipDict(_openTypeOS2WidthClass_fromFL) + +_postscriptHintAttributes = set(( + "postscriptBlueValues", + "postscriptOtherBlues", + "postscriptFamilyBlues", + "postscriptFamilyOtherBlues", + "postscriptStemSnapH", + "postscriptStemSnapV", +)) + + +class RInfo(BaseInfo): + + """RoboFab wrapper for FL Font Info""" + + _title = "FLInfo" + + _ufoToFLAttrMapping = { + "familyName" : _infoMapDict(valueType=str, nakedAttribute="family_name"), + "styleName" : _infoMapDict(valueType=str, nakedAttribute="style_name"), + "styleMapFamilyName" : _infoMapDict(valueType=str, nakedAttribute="menu_name"), + "styleMapStyleName" : _infoMapDict(valueType=str, nakedAttribute="font_style", specialGetSet=True), + "versionMajor" : _infoMapDict(valueType=int, nakedAttribute="version_major"), + "versionMinor" : _infoMapDict(valueType=int, nakedAttribute="version_minor"), + "year" : _infoMapDict(valueType=int, nakedAttribute="year"), + "copyright" : _infoMapDict(valueType=str, nakedAttribute="copyright"), + "trademark" : _infoMapDict(valueType=str, nakedAttribute="trademark"), + "unitsPerEm" : _infoMapDict(valueType=int, nakedAttribute="upm"), + "descender" : _infoMapDict(valueType=int, nakedAttribute="descender", masterSpecific=True), + "xHeight" : _infoMapDict(valueType=int, nakedAttribute="x_height", masterSpecific=True), + "capHeight" : _infoMapDict(valueType=int, nakedAttribute="cap_height", masterSpecific=True), + "ascender" : _infoMapDict(valueType=int, nakedAttribute="ascender", masterSpecific=True), + "italicAngle" : _infoMapDict(valueType=float, nakedAttribute="italic_angle"), + "note" : _infoMapDict(valueType=str, nakedAttribute="note"), + "openTypeHeadCreated" : _infoMapDict(valueType=str, nakedAttribute=None, specialGetSet=True), # i can't figure out the ttinfo.head_creation values + "openTypeHeadLowestRecPPEM" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.head_lowest_rec_ppem"), + "openTypeHeadFlags" : _infoMapDict(valueType="intList", nakedAttribute=None), # There is an attribute (ttinfo.head_flags), but no user interface. + "openTypeHheaAscender" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.hhea_ascender"), + "openTypeHheaDescender" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.hhea_descender"), + "openTypeHheaLineGap" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.hhea_line_gap"), + "openTypeHheaCaretSlopeRise" : _infoMapDict(valueType=int, nakedAttribute=None), + "openTypeHheaCaretSlopeRun" : _infoMapDict(valueType=int, nakedAttribute=None), + "openTypeHheaCaretOffset" : _infoMapDict(valueType=int, nakedAttribute=None), + "openTypeNameDesigner" : _infoMapDict(valueType=str, nakedAttribute="designer"), + "openTypeNameDesignerURL" : _infoMapDict(valueType=str, nakedAttribute="designer_url"), + "openTypeNameManufacturer" : _infoMapDict(valueType=str, nakedAttribute="source"), + "openTypeNameManufacturerURL" : _infoMapDict(valueType=str, nakedAttribute="vendor_url"), + "openTypeNameLicense" : _infoMapDict(valueType=str, nakedAttribute="license"), + "openTypeNameLicenseURL" : _infoMapDict(valueType=str, nakedAttribute="license_url"), + "openTypeNameVersion" : _infoMapDict(valueType=str, nakedAttribute="tt_version"), + "openTypeNameUniqueID" : _infoMapDict(valueType=str, nakedAttribute="tt_u_id"), + "openTypeNameDescription" : _infoMapDict(valueType=str, nakedAttribute="notice"), + "openTypeNamePreferredFamilyName" : _infoMapDict(valueType=str, nakedAttribute="pref_family_name"), + "openTypeNamePreferredSubfamilyName" : _infoMapDict(valueType=str, nakedAttribute="pref_style_name"), + "openTypeNameCompatibleFullName" : _infoMapDict(valueType=str, nakedAttribute="mac_compatible"), + "openTypeNameSampleText" : _infoMapDict(valueType=str, nakedAttribute=None), + "openTypeNameWWSFamilyName" : _infoMapDict(valueType=str, nakedAttribute=None), + "openTypeNameWWSSubfamilyName" : _infoMapDict(valueType=str, nakedAttribute=None), + "openTypeOS2WidthClass" : _infoMapDict(valueType=int, nakedAttribute="width"), + "openTypeOS2WeightClass" : _infoMapDict(valueType=int, nakedAttribute="weight_code", specialGetSet=True), + "openTypeOS2Selection" : _infoMapDict(valueType="intList", nakedAttribute=None), # ttinfo.os2_fs_selection only returns 0 + "openTypeOS2VendorID" : _infoMapDict(valueType=str, nakedAttribute="vendor"), + "openTypeOS2Panose" : _infoMapDict(valueType="intList", nakedAttribute="panose", specialGetSet=True), + "openTypeOS2FamilyClass" : _infoMapDict(valueType="intList", nakedAttribute="ttinfo.os2_s_family_class", specialGetSet=True), + "openTypeOS2UnicodeRanges" : _infoMapDict(valueType="intList", nakedAttribute="unicoderanges"), + "openTypeOS2CodePageRanges" : _infoMapDict(valueType="intList", nakedAttribute="codepages"), + "openTypeOS2TypoAscender" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_s_typo_ascender"), + "openTypeOS2TypoDescender" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_s_typo_descender"), + "openTypeOS2TypoLineGap" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_s_typo_line_gap"), + "openTypeOS2WinAscent" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_us_win_ascent"), + "openTypeOS2WinDescent" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_us_win_descent", specialGetSet=True), + "openTypeOS2Type" : _infoMapDict(valueType="intList", nakedAttribute="ttinfo.os2_fs_type", specialGetSet=True), + "openTypeOS2SubscriptXSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_subscript_x_size"), + "openTypeOS2SubscriptYSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_subscript_y_size"), + "openTypeOS2SubscriptXOffset" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_subscript_x_offset"), + "openTypeOS2SubscriptYOffset" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_subscript_y_offset"), + "openTypeOS2SuperscriptXSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_superscript_x_size"), + "openTypeOS2SuperscriptYSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_superscript_y_size"), + "openTypeOS2SuperscriptXOffset" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_superscript_x_offset"), + "openTypeOS2SuperscriptYOffset" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_superscript_y_offset"), + "openTypeOS2StrikeoutSize" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_strikeout_size"), + "openTypeOS2StrikeoutPosition" : _infoMapDict(valueType=int, nakedAttribute="ttinfo.os2_y_strikeout_position"), + "openTypeVheaVertTypoAscender" : _infoMapDict(valueType=int, nakedAttribute=None), + "openTypeVheaVertTypoDescender" : _infoMapDict(valueType=int, nakedAttribute=None), + "openTypeVheaVertTypoLineGap" : _infoMapDict(valueType=int, nakedAttribute=None), + "openTypeVheaCaretSlopeRise" : _infoMapDict(valueType=int, nakedAttribute=None), + "openTypeVheaCaretSlopeRun" : _infoMapDict(valueType=int, nakedAttribute=None), + "openTypeVheaCaretOffset" : _infoMapDict(valueType=int, nakedAttribute=None), + "postscriptFontName" : _infoMapDict(valueType=str, nakedAttribute="font_name"), + "postscriptFullName" : _infoMapDict(valueType=str, nakedAttribute="full_name"), + "postscriptSlantAngle" : _infoMapDict(valueType=float, nakedAttribute="slant_angle"), + "postscriptUniqueID" : _infoMapDict(valueType=int, nakedAttribute="unique_id"), + "postscriptUnderlineThickness" : _infoMapDict(valueType=int, nakedAttribute="underline_thickness"), + "postscriptUnderlinePosition" : _infoMapDict(valueType=int, nakedAttribute="underline_position"), + "postscriptIsFixedPitch" : _infoMapDict(valueType="boolint", nakedAttribute="is_fixed_pitch"), + "postscriptBlueValues" : _infoMapDict(valueType="intList", nakedAttribute="blue_values", masterSpecific=True, requiresSetNum=True), + "postscriptOtherBlues" : _infoMapDict(valueType="intList", nakedAttribute="other_blues", masterSpecific=True, requiresSetNum=True), + "postscriptFamilyBlues" : _infoMapDict(valueType="intList", nakedAttribute="family_blues", masterSpecific=True, requiresSetNum=True), + "postscriptFamilyOtherBlues" : _infoMapDict(valueType="intList", nakedAttribute="family_other_blues", masterSpecific=True, requiresSetNum=True), + "postscriptStemSnapH" : _infoMapDict(valueType="intList", nakedAttribute="stem_snap_h", masterSpecific=True, requiresSetNum=True), + "postscriptStemSnapV" : _infoMapDict(valueType="intList", nakedAttribute="stem_snap_v", masterSpecific=True, requiresSetNum=True), + "postscriptBlueFuzz" : _infoMapDict(valueType=int, nakedAttribute="blue_fuzz", masterSpecific=True), + "postscriptBlueShift" : _infoMapDict(valueType=int, nakedAttribute="blue_shift", masterSpecific=True), + "postscriptBlueScale" : _infoMapDict(valueType=float, nakedAttribute="blue_scale", masterSpecific=True), + "postscriptForceBold" : _infoMapDict(valueType="boolint", nakedAttribute="force_bold", masterSpecific=True), + "postscriptDefaultWidthX" : _infoMapDict(valueType=int, nakedAttribute="default_width", masterSpecific=True), + "postscriptNominalWidthX" : _infoMapDict(valueType=int, nakedAttribute=None), + "postscriptWeightName" : _infoMapDict(valueType=str, nakedAttribute="weight"), + "postscriptDefaultCharacter" : _infoMapDict(valueType=str, nakedAttribute="default_character"), + "postscriptWindowsCharacterSet" : _infoMapDict(valueType=int, nakedAttribute="ms_charset", specialGetSet=True), + "macintoshFONDFamilyID" : _infoMapDict(valueType=int, nakedAttribute="fond_id"), + "macintoshFONDName" : _infoMapDict(valueType=str, nakedAttribute="apple_name"), + } + _environmentOverrides = ["width", "openTypeOS2WidthClass"] # ugh. + + def __init__(self, font): + super(RInfo, self).__init__() + self._object = font + + def _environmentSetAttr(self, attr, value): + # special fontlab workarounds + if attr == "width": + warn("The width attribute has been deprecated. Use the new openTypeOS2WidthClass attribute.", DeprecationWarning) + attr = "openTypeOS2WidthClass" + if attr == "openTypeOS2WidthClass": + if isinstance(value, basestring) and value not in _openTypeOS2WidthClass_toFL: + print "The openTypeOS2WidthClass value \"%s\" cannot be found in the OpenType OS/2 usWidthClass specification. The value will be set into the FontLab file for now." % value + self._object.width = value + else: + self._object.width = _openTypeOS2WidthClass_toFL[value] + return + # get the attribute data + data = self._ufoToFLAttrMapping[attr] + flAttr = data["nakedAttribute"] + valueType = data["valueType"] + masterSpecific = data["masterSpecific"] + requiresSetNum = data["requiresSetNum"] + specialGetSet = data["specialGetSet"] + # warn about setting attributes not supported by FL + if flAttr is None: + print "The attribute %s is not supported by FontLab. This data will not be set." % attr + return + # make sure that the value is the proper type for FL + if valueType == "intList": + value = [int(i) for i in value] + elif valueType == "boolint": + value = int(bool(value)) + elif valueType == str: + if value is None: + value = "" + value = value.encode(LOCAL_ENCODING) + elif valueType == int and not isinstance(value, int): + value = int(round(value)) + elif not isinstance(value, valueType): + value = valueType(value) + # handle postscript hint bug in FL + if attr in _postscriptHintAttributes: + value = self._handlePSHintBug(attr, value) + # handle special cases + if specialGetSet: + attr = "_set_%s" % attr + method = getattr(self, attr) + return method(value) + # set the value + obj = self._object + if len(flAttr.split(".")) > 1: + flAttrList = flAttr.split(".") + for i in flAttrList[:-1]: + obj = getattr(obj, i) + flAttr = flAttrList[-1] + ## set the foo_num attribute if necessary + if requiresSetNum: + numAttr = flAttr + "_num" + setattr(obj, numAttr, len(value)) + ## set master 0 if the data is master specific + if masterSpecific: + subObj = getattr(obj, flAttr) + if valueType == "intList": + for index, v in enumerate(value): + subObj[0][index] = v + else: + subObj[0] = value + ## otherwise use a regular set + else: + setattr(obj, flAttr, value) + + def _environmentGetAttr(self, attr): + # special fontlab workarounds + if attr == "width": + warn("The width attribute has been deprecated. Use the new openTypeOS2WidthClass attribute.", DeprecationWarning) + attr = "openTypeOS2WidthClass" + if attr == "openTypeOS2WidthClass": + value = self._object.width + if value not in _openTypeOS2WidthClass_fromFL: + print "The existing openTypeOS2WidthClass value \"%s\" cannot be found in the OpenType OS/2 usWidthClass specification." % value + return + else: + return _openTypeOS2WidthClass_fromFL[value] + # get the attribute data + data = self._ufoToFLAttrMapping[attr] + flAttr = data["nakedAttribute"] + valueType = data["valueType"] + masterSpecific = data["masterSpecific"] + specialGetSet = data["specialGetSet"] + # warn about setting attributes not supported by FL + if flAttr is None: + if not _IN_UFO_EXPORT: + print "The attribute %s is not supported by FontLab." % attr + return + # handle special cases + if specialGetSet: + attr = "_get_%s" % attr + method = getattr(self, attr) + return method() + # get the value + if len(flAttr.split(".")) > 1: + flAttrList = flAttr.split(".") + obj = self._object + for i in flAttrList: + obj = getattr(obj, i) + value = obj + else: + value = getattr(self._object, flAttr) + # grab the first master value if necessary + if masterSpecific: + value = value[0] + # convert if necessary + if valueType == "intList": + value = [int(i) for i in value] + elif valueType == "boolint": + value = bool(value) + elif valueType == str: + if value is None: + pass + else: + value = unicode(value, LOCAL_ENCODING) + elif not isinstance(value, valueType): + value = valueType(value) + return value + + # ------------------------------ + # individual attribute overrides + # ------------------------------ + + # styleMapStyleName + + def _get_styleMapStyleName(self): + return _styleMapStyleName_fromFL[self._object.font_style] + + def _set_styleMapStyleName(self, value): + value = _styleMapStyleName_toFL[value] + self._object.font_style = value + +# # openTypeHeadCreated +# +# # fontlab epoch: 1969-12-31 19:00:00 +# +# def _get_openTypeHeadCreated(self): +# value = self._object.ttinfo.head_creation +# epoch = datetime.datetime(1969, 12, 31, 19, 0, 0) +# delta = datetime.timedelta(seconds=value[0]) +# t = epoch - delta +# string = "%s-%s-%s %s:%s:%s" % (str(t.year).zfill(4), str(t.month).zfill(2), str(t.day).zfill(2), str(t.hour).zfill(2), str(t.minute).zfill(2), str(t.second).zfill(2)) +# return string +# +# def _set_openTypeHeadCreated(self, value): +# date, time = value.split(" ") +# year, month, day = [int(i) for i in date.split("-")] +# hour, minute, second = [int(i) for i in time.split(":")] +# value = datetime.datetime(year, month, day, hour, minute, second) +# epoch = datetime.datetime(1969, 12, 31, 19, 0, 0) +# delta = epoch - value +# seconds = delta.seconds +# self._object.ttinfo.head_creation[0] = seconds + + # openTypeOS2WeightClass + + def _get_openTypeOS2WeightClass(self): + value = self._object.weight_code + if value == -1: + value = None + return value + + def _set_openTypeOS2WeightClass(self, value): + self._object.weight_code = value + + # openTypeOS2WinDescent + + def _get_openTypeOS2WinDescent(self): + return self._object.ttinfo.os2_us_win_descent + + def _set_openTypeOS2WinDescent(self, value): + if value < 0: + warn("FontLab can only handle positive values for openTypeOS2WinDescent.") + value = abs(value) + self._object.ttinfo.os2_us_win_descent = value + + # openTypeOS2Type + + def _get_openTypeOS2Type(self): + value = self._object.ttinfo.os2_fs_type + intList = [] + for bit, bitNumber in _openTypeOS2Type_fromFL.items(): + if value & bit: + intList.append(bitNumber) + return intList + + def _set_openTypeOS2Type(self, values): + value = 0 + for bitNumber in values: + bit = _openTypeOS2Type_toFL[bitNumber] + value = value | bit + self._object.ttinfo.os2_fs_type = value + + # openTypeOS2Panose + + def _get_openTypeOS2Panose(self): + return [i for i in self._object.panose] + + def _set_openTypeOS2Panose(self, values): + for index, value in enumerate(values): + self._object.panose[index] = value + + # openTypeOS2FamilyClass + + def _get_openTypeOS2FamilyClass(self): + value = self._object.ttinfo.os2_s_family_class + for classID in range(15): + classValue = classID * 256 + if classValue > value: + classID -= 1 + classValue = classID * 256 + break + subclassID = value - classValue + return [classID, subclassID] + + def _set_openTypeOS2FamilyClass(self, values): + classID, subclassID = values + classID = classID * 256 + value = classID + subclassID + self._object.ttinfo.os2_s_family_class = value + + # postscriptWindowsCharacterSet + + def _get_postscriptWindowsCharacterSet(self): + value = self._object.ms_charset + value = _postscriptWindowsCharacterSet_fromFL[value] + return value + + def _set_postscriptWindowsCharacterSet(self, value): + value = _postscriptWindowsCharacterSet_toFL[value] + self._object.ms_charset = value + + # ----------------- + # FL bug workaround + # ----------------- + + def _handlePSHintBug(self, attribute, values): + """Function to handle problems with FontLab not allowing the max number of + alignment zones to be set to the max number. + Input: the name of the zones and the values to be set + Output: a warning when there are too many values to be set + and the max values which FontLab will allow. + """ + originalValues = values + truncatedLength = None + if attribute in ("postscriptStemSnapH", "postscriptStemSnapV"): + if len(values) > 10: + values = values[:10] + truncatedLength = 10 + elif attribute in ("postscriptBlueValues", "postscriptFamilyBlues"): + if len(values) > 12: + values = values[:12] + truncatedLength = 12 + elif attribute in ("postscriptOtherBlues", "postscriptFamilyOtherBlues"): + if len(values) > 8: + values = values[:8] + truncatedLength = 8 + if truncatedLength is not None: + print "* * * WARNING: FontLab will only accept %d %s items maximum from Python. Dropping values: %s." % (truncatedLength, attribute, str(originalValues[truncatedLength:])) + return values + + +class RFeatures(BaseFeatures): + + _title = "FLFeatures" + + def __init__(self, font): + super(RFeatures, self).__init__() + self._object = font + + def _get_text(self): + naked = self._object + features = [] + if naked.ot_classes: + features.append(_normalizeLineEndings(naked.ot_classes)) + for feature in naked.features: + features.append(_normalizeLineEndings(feature.value)) + return "".join(features) + + def _set_text(self, value): + classes, features = splitFeaturesForFontLab(value) + naked = self._object + naked.ot_classes = classes + naked.features.clean() + for featureName, featureText in features: + f = Feature(featureName, featureText) + naked.features.append(f) + + text = property(_get_text, _set_text, doc="raw feature text.") + diff --git a/misc/pylib/robofab/objects/objectsRF.pyx b/misc/pylib/robofab/objects/objectsRF.pyx new file mode 100755 index 000000000..50e1f13a7 --- /dev/null +++ b/misc/pylib/robofab/objects/objectsRF.pyx @@ -0,0 +1,1233 @@ +"""UFO for GlifLib""" + +from robofab import RoboFabError, RoboFabWarning +from robofab.objects.objectsBase import BaseFont, BaseKerning, BaseGroups, BaseInfo, BaseFeatures, BaseLib,\ + BaseGlyph, BaseContour, BaseSegment, BasePoint, BaseBPoint, BaseAnchor, BaseGuide, BaseComponent, \ + relativeBCPIn, relativeBCPOut, absoluteBCPIn, absoluteBCPOut, _box,\ + _interpolate, _interpolatePt, roundPt, addPt,\ + MOVE, LINE, CORNER, CURVE, QCURVE, OFFCURVE,\ + BasePostScriptFontHintValues, postScriptHintDataLibKey, BasePostScriptGlyphHintValues + +import os + + +__all__ = [ "CurrentFont", + "CurrentGlyph", 'OpenFont', + 'RFont', 'RGlyph', 'RContour', + 'RPoint', 'RBPoint', 'RAnchor', + 'RComponent' + ] + + + +def CurrentFont(): + return None + +def CurrentGlyph(): + return None + +def OpenFont(path=None, note=None): + """Open a font from a path. If path is not given, present the user with a dialog.""" + if not note: + note = 'select a .ufo directory' + if not path: + from robofab.interface.all.dialogs import GetFolder + path = GetFolder(note) + if path: + try: + return RFont(path) + except OSError: + from robofab.interface.all.dialogs import Message + Message("%s is not a valid .UFO font. But considering it's all XML, why don't you have a look inside with a simple text editor."%(path)) + else: + return None + +def NewFont(familyName=None, styleName=None): + """Make a new font""" + new = RFont() + if familyName is not None: + new.info.familyName = familyName + if styleName is not None: + new.info.styleName = styleName + return new + +def AllFonts(): + """AllFonts can't work in plain python usage. It's really up to some sort of application + to keep track of which fonts are open.""" + raise NotImplementedError + + +class PostScriptFontHintValues(BasePostScriptFontHintValues): + """ Font level PostScript hints object for objectsRF usage. + If there are values in the lib, use those. + If there are no values in the lib, use defaults. + + The psHints attribute for objectsRF.RFont is basically just the + data read from the Lib. When the object saves to UFO, the + hints are written back to the lib, which is then saved. + + """ + + def __init__(self, aFont=None, data=None): + self.setParent(aFont) + BasePostScriptFontHintValues.__init__(self) + if aFont is not None: + # in version 1, this data was stored in the lib + # if it is still there, guess that it is correct + # move it to font info and remove it from the lib. + libData = aFont.lib.get(postScriptHintDataLibKey) + if libData is not None: + self.fromDict(libData) + del libData[postScriptHintDataLibKey] + if data is not None: + self.fromDict(data) + +def getPostScriptHintDataFromLib(aFont, fontLib): + hintData = fontLib.get(postScriptHintDataLibKey) + psh = PostScriptFontHintValues(aFont) + psh.fromDict(hintData) + return psh + +class PostScriptGlyphHintValues(BasePostScriptGlyphHintValues): + """ Glyph level PostScript hints object for objectsRF usage. + If there are values in the lib, use those. + If there are no values in the lib, be empty. + + """ + def __init__(self, aGlyph=None, data=None): + # read the data from the glyph.lib, it won't be anywhere else + BasePostScriptGlyphHintValues.__init__(self) + if aGlyph is not None: + self.setParent(aGlyph) + self._loadFromLib(aGlyph.lib) + if data is not None: + self.fromDict(data) + + +class RFont(BaseFont): + """UFO font object which reads and writes glif, and keeps the data in memory in between. + Bahviour: + - comparable to Font + - comparable to GlyphSet so that it can be passed to Glif widgets + """ + + _title = "RoboFabFont" + + def __init__(self, path=None): + BaseFont.__init__(self) + if path is not None: + self._path = os.path.normpath(os.path.abspath(path)) + else: + self._path = None + self._object = {} + + self._glyphSet = None + self._scheduledForDeletion = [] # this is a place for storing glyphs that need to be removed when the font is saved + + self.kerning = RKerning() + self.kerning.setParent(self) + self.info = RInfo() + self.info.setParent(self) + self.features = RFeatures() + self.features.setParent(self) + self.groups = RGroups() + self.groups.setParent(self) + self.lib = RLib() + self.lib.setParent(self) + if path: + self._loadData(path) + else: + self.psHints = PostScriptFontHintValues(self) + self.psHints.setParent(self) + + def __setitem__(self, glyphName, glyph): + """Set a glyph at key.""" + self._object[glyphName] = glyph + + def __cmp__(self, other): + """Compare this font with another, compare if they refer to the same file.""" + if not hasattr(other, '_path'): + return -1 + if self._object._path == other._object._path and self._object._path is not None: + return 0 + else: + return -1 + + def __len__(self): + if self._glyphSet is None: + return 0 + return len(self._glyphSet) + + def _loadData(self, path): + from robofab.ufoLib import UFOReader + reader = UFOReader(path) + fontLib = reader.readLib() + # info + reader.readInfo(self.info) + # kerning + self.kerning.update(reader.readKerning()) + self.kerning.setChanged(False) + # groups + self.groups.update(reader.readGroups()) + # features + if reader.formatVersion == 1: + # migrate features from the lib + features = [] + classes = fontLib.get("org.robofab.opentype.classes") + if classes is not None: + del fontLib["org.robofab.opentype.classes"] + features.append(classes) + splitFeatures = fontLib.get("org.robofab.opentype.features") + if splitFeatures is not None: + order = fontLib.get("org.robofab.opentype.featureorder") + if order is None: + order = splitFeatures.keys() + order.sort() + else: + del fontLib["org.robofab.opentype.featureorder"] + del fontLib["org.robofab.opentype.features"] + for tag in order: + oneFeature = splitFeatures.get(tag) + if oneFeature is not None: + features.append(oneFeature) + features = "\n".join(features) + else: + features = reader.readFeatures() + self.features.text = features + # hint data + self.psHints = PostScriptFontHintValues(self) + if postScriptHintDataLibKey in fontLib: + del fontLib[postScriptHintDataLibKey] + # lib + self.lib.update(fontLib) + # glyphs + self._glyphSet = reader.getGlyphSet() + self._hasNotChanged(doGlyphs=False) + + def _loadGlyph(self, glyphName): + """Load a single glyph from the glyphSet, on request.""" + from robofab.pens.rfUFOPen import RFUFOPointPen + g = RGlyph() + g.name = glyphName + pen = RFUFOPointPen(g) + self._glyphSet.readGlyph(glyphName=glyphName, glyphObject=g, pointPen=pen) + g.setParent(self) + g.psHints._loadFromLib(g.lib) + self._object[glyphName] = g + self._object[glyphName]._hasNotChanged() + return g + + #def _prepareSaveDir(self, dir): + # path = os.path.join(dir, 'glyphs') + # if not os.path.exists(path): + # os.makedirs(path) + + def _hasNotChanged(self, doGlyphs=True): + #set the changed state of the font + if doGlyphs: + for glyph in self: + glyph._hasNotChanged() + self.setChanged(False) + + # + # attributes + # + + def _get_path(self): + return self._path + + path = property(_get_path, doc="path of the font") + + # + # methods for imitating GlyphSet? + # + + def keys(self): + # the keys are the superset of self._objects.keys() and + # self._glyphSet.keys(), minus self._scheduledForDeletion + keys = self._object.keys() + if self._glyphSet is not None: + keys.extend(self._glyphSet.keys()) + d = dict() + for glyphName in keys: + d[glyphName] = None + for glyphName in self._scheduledForDeletion: + if glyphName in d: + del d[glyphName] + return d.keys() + + def has_key(self, glyphName): + # XXX ditto, see above. + if self._glyphSet is not None: + hasGlyph = glyphName in self._object or glyphName in self._glyphSet + else: + hasGlyph = glyphName in self._object + return hasGlyph and not glyphName in self._scheduledForDeletion + + __contains__ = has_key + + def getWidth(self, glyphName): + if self._object.has_key(glyphName): + return self._object[glyphName].width + raise IndexError # or return None? + + def getReverseComponentMapping(self): + """ + Get a reversed map of component references in the font. + { + 'A' : ['Aacute', 'Aring'] + 'acute' : ['Aacute'] + 'ring' : ['Aring'] + etc. + } + """ + # a NON-REVERESED map is stored in the lib. + # this is done because a reveresed map could + # contain faulty data. for example: "Aacute" contains + # a component that references "A". Glyph "Aacute" is + # then deleted. The reverse map would still say that + # "A" is referenced by "Aacute" even though the + # glyph has been deleted. So, the stored lib works like this: + # { + # 'Aacute' : [ + # # the last known mod time of the GLIF + # 1098706856.75, + # # component references in a glyph + # ['A', 'acute'] + # ] + # } + import time + import os + import re + componentSearch_RE = re.compile( + "<component\s+" # <component + "[^>]*?" # anything EXCEPT > + "base\s*=\s*[\"\']" # base=" + "(.*?)" # foo + "[\"\']" # " + ) + rightNow = time.time() + libKey = "org.robofab.componentMapping" + previousMap = None + if self.lib.has_key(libKey): + previousMap = self.lib[libKey] + basicMap = {} + reverseMap = {} + for glyphName in self.keys(): + componentsToMap = None + modTime = None + # get the previous bits of data + previousModTime = None + previousList = None + if previousMap is not None and previousMap.has_key(glyphName): + previousModTime, previousList = previousMap[glyphName] + # the glyph has been loaded. + # simply get the components from it. + if self._object.has_key(glyphName): + componentsToMap = [component.baseGlyph for component in self._object[glyphName].components] + # the glyph has not been loaded. + else: + glyphPath = os.path.join(self._glyphSet.dirName, self._glyphSet.contents[glyphName]) + scanGlyph = True + # test the modified time of the GLIF + fileModTime = os.path.getmtime(glyphPath) + if previousModTime is not None and fileModTime == previousModTime: + # the GLIF almost* certianly has not changed. + # *theoretically, a user could replace a GLIF + # with another GLIF that has precisely the same + # mod time. + scanGlyph = False + componentsToMap = previousList + modTime = previousModTime + else: + # the GLIF is different + modTime = fileModTime + if scanGlyph: + # use regex to extract component + # base glyphs from the file + f = open(glyphPath, 'rb') + data = f.read() + f.close() + componentsToMap = componentSearch_RE.findall(data) + if componentsToMap is not None: + # store the non-reversed map + basicMap[glyphName] = (modTime, componentsToMap) + # reverse the map for the user + if componentsToMap: + for baseGlyphName in componentsToMap: + if not reverseMap.has_key(baseGlyphName): + reverseMap[baseGlyphName] = [] + reverseMap[baseGlyphName].append(glyphName) + # if a glyph has been loaded, we do not store data about it in the lib. + # this is done becuase there is not way to determine the proper mod time + # for a loaded glyph. + if modTime is None: + del basicMap[glyphName] + # store the map in the lib for re-use + self.lib[libKey] = basicMap + return reverseMap + + + def save(self, destDir=None, doProgress=False, formatVersion=2): + """Save the Font in UFO format.""" + # XXX note that when doing "save as" by specifying the destDir argument + # _all_ glyphs get loaded into memory. This could be optimized by either + # copying those .glif files that have not been edited or (not sure how + # well that would work) by simply clearing out self._objects after the + # save. + from robofab.ufoLib import UFOWriter + from robofab.tools.fontlabFeatureSplitter import splitFeaturesForFontLab + # if no destination is given, or if + # the given destination is the current + # path, this is not a save as operation + if destDir is None or destDir == self._path: + saveAs = False + destDir = self._path + else: + saveAs = True + # start a progress bar + nonGlyphCount = 5 + bar = None + if doProgress: + from robofab.interface.all.dialogs import ProgressBar + bar = ProgressBar("Exporting UFO", nonGlyphCount + len(self._object.keys())) + # write + writer = UFOWriter(destDir, formatVersion=formatVersion) + try: + # make a shallow copy of the lib. stuff may be added to it. + fontLib = dict(self.lib) + # info + if bar: + bar.label("Saving info...") + writer.writeInfo(self.info) + if bar: + bar.tick() + # kerning + if self.kerning.changed or saveAs: + if bar: + bar.label("Saving kerning...") + writer.writeKerning(self.kerning.asDict()) + if bar: + bar.tick() + # groups + if bar: + bar.label("Saving groups...") + writer.writeGroups(self.groups) + if bar: + bar.tick() + # features + if bar: + bar.label("Saving features...") + features = self.features.text + if features is None: + features = "" + if formatVersion == 2: + writer.writeFeatures(features) + elif formatVersion == 1: + classes, features = splitFeaturesForFontLab(features) + if classes: + fontLib["org.robofab.opentype.classes"] = classes.strip() + "\n" + if features: + featureDict = {} + for featureName, featureText in features: + featureDict[featureName] = featureText.strip() + "\n" + fontLib["org.robofab.opentype.features"] = featureDict + fontLib["org.robofab.opentype.featureorder"] = [featureName for featureName, featureText in features] + if bar: + bar.tick() + # lib + if formatVersion == 1: + fontLib[postScriptHintDataLibKey] = self.psHints.asDict() + if bar: + bar.label("Saving lib...") + writer.writeLib(fontLib) + if bar: + bar.tick() + # glyphs + glyphNameToFileNameFunc = self.getGlyphNameToFileNameFunc() + + glyphSet = writer.getGlyphSet(glyphNameToFileNameFunc) + if len(self._scheduledForDeletion) != 0: + if bar: + bar.label("Removing deleted glyphs...") + for glyphName in self._scheduledForDeletion: + if glyphSet.has_key(glyphName): + glyphSet.deleteGlyph(glyphName) + if bar: + bar.tick() + if bar: + bar.label("Saving glyphs...") + count = nonGlyphCount + if saveAs: + glyphNames = self.keys() + else: + glyphNames = self._object.keys() + for glyphName in glyphNames: + glyph = self[glyphName] + glyph.psHints._saveToLib(glyph.lib) + glyph._saveToGlyphSet(glyphSet, glyphName=glyphName, force=saveAs) + if bar and not count % 10: + bar.tick(count) + count = count + 1 + glyphSet.writeContents() + self._glyphSet = glyphSet + # only blindly stop if the user says to + except KeyboardInterrupt: + bar.close() + bar = None + # kill the progress bar + if bar: + bar.close() + # reset internal stuff + self._path = destDir + self._scheduledForDeletion = [] + self.setChanged(False) + + def newGlyph(self, glyphName, clear=True): + """Make a new glyph with glyphName + if the glyph exists and clear=True clear the glyph""" + if clear and glyphName in self: + g = self[glyphName] + g.clear() + w = self.info.postscriptDefaultWidthX + if w is None: + w = 0 + g.width = w + return g + g = RGlyph() + g.setParent(self) + g.name = glyphName + w = self.info.postscriptDefaultWidthX + if w is None: + w = 0 + g.width = w + g._hasChanged() + self._object[glyphName] = g + # is the user adding a glyph that has the same + # name as one that was deleted earlier? + if glyphName in self._scheduledForDeletion: + self._scheduledForDeletion.remove(glyphName) + return self.getGlyph(glyphName) + + def insertGlyph(self, glyph, name=None): + """returns a new glyph that has been inserted into the font""" + if name is None: + name = glyph.name + glyph = glyph.copy() + glyph.name = name + glyph.setParent(self) + glyph._hasChanged() + self._object[name] = glyph + # is the user adding a glyph that has the same + # name as one that was deleted earlier? + if name in self._scheduledForDeletion: + self._scheduledForDeletion.remove(name) + return self.getGlyph(name) + + def removeGlyph(self, glyphName): + """remove a glyph from the font""" + # XXX! Potential issue with removing glyphs. + # if a glyph is removed from a font, but it is still referenced + # by a component, it will give pens some trouble. + # where does the resposibility for catching this fall? + # the removeGlyph method? the addComponent method + # of the various pens? somewhere else? hm... tricky. + # + #we won't actually remove it, we will just store it for removal + # but only if the glyph does exist + if self.has_key(glyphName) and glyphName not in self._scheduledForDeletion: + self._scheduledForDeletion.append(glyphName) + # now delete the object + if self._object.has_key(glyphName): + del self._object[glyphName] + self._hasChanged() + + def getGlyph(self, glyphName): + # XXX getGlyph may have to become private, to avoid duplication + # with __getitem__ + n = None + if self._object.has_key(glyphName): + # have we served this glyph before? it should be in _object + n = self._object[glyphName] + else: + # haven't served it before, is it in the glyphSet then? + if self._glyphSet is not None and glyphName in self._glyphSet: + # yes, read the .glif file from disk + n = self._loadGlyph(glyphName) + if n is None: + raise KeyError, glyphName + return n + + +class RGlyph(BaseGlyph): + + _title = "RGlyph" + + def __init__(self): + BaseGlyph.__init__(self) + self.contours = [] + self.components = [] + self.anchors = [] + self._unicodes = [] + self.width = 0 + self.note = None + self._name = "Unnamed Glyph" + self.selected = False + self._properties = None + self._lib = RLib() + self._lib.setParent(self) + self.psHints = PostScriptGlyphHintValues() + self.psHints.setParent(self) + + def __len__(self): + return len(self.contours) + + def __getitem__(self, index): + if index < len(self.contours): + return self.contours[index] + raise IndexError + + def _hasNotChanged(self): + for contour in self.contours: + contour.setChanged(False) + for segment in contour.segments: + segment.setChanged(False) + for point in segment.points: + point.setChanged(False) + for component in self.components: + component.setChanged(False) + for anchor in self.anchors: + anchor.setChanged(False) + self.setChanged(False) + + # + # attributes + # + + def _get_lib(self): + return self._lib + + def _set_lib(self, obj): + self._lib.clear() + self._lib.update(obj) + + lib = property(_get_lib, _set_lib) + + def _get_name(self): + return self._name + + def _set_name(self, value): + prevName = self._name + newName = value + if newName == prevName: + return + self._name = newName + self.setChanged(True) + font = self.getParent() + if font is not None: + # but, this glyph could be linked to a + # FontLab font, because objectsFL.RGlyph.copy() + # creates an objectsRF.RGlyph with the parent + # set to an objectsFL.RFont object. so, check to see + # if this is a legitimate RFont before trying to + # do the objectsRF.RFont glyph name change + if isinstance(font, RFont): + font._object[newName] = self + # is the user changing a glyph's name to the + # name of a glyph that was deleted earlier? + if newName in font._scheduledForDeletion: + font._scheduledForDeletion.remove(newName) + font.removeGlyph(prevName) + + name = property(_get_name, _set_name) + + def _get_unicodes(self): + return self._unicodes + + def _set_unicodes(self, value): + if not isinstance(value, list): + raise RoboFabError, "unicodes must be a list" + self._unicodes = value + self._hasChanged() + + unicodes = property(_get_unicodes, _set_unicodes, doc="all unicode values for the glyph") + + def _get_unicode(self): + if len(self._unicodes) == 0: + return None + return self._unicodes[0] + + def _set_unicode(self, value): + uni = self._unicodes + if value is not None: + if value not in uni: + self.unicodes.insert(0, value) + elif uni.index(value) != 0: + uni.insert(0, uni.pop(uni.index(value))) + self.unicodes = uni + + unicode = property(_get_unicode, _set_unicode, doc="first unicode value for the glyph") + + def getPointPen(self): + from robofab.pens.rfUFOPen import RFUFOPointPen + return RFUFOPointPen(self) + + def appendComponent(self, baseGlyph, offset=(0, 0), scale=(1, 1)): + """append a component to the glyph""" + new = RComponent(baseGlyph, offset, scale) + new.setParent(self) + self.components.append(new) + self._hasChanged() + + def appendAnchor(self, name, position, mark=None): + """append an anchor to the glyph""" + new = RAnchor(name, position, mark) + new.setParent(self) + self.anchors.append(new) + self._hasChanged() + + def removeContour(self, index): + """remove a specific contour from the glyph""" + del self.contours[index] + self._hasChanged() + + def removeAnchor(self, anchor): + """remove a specific anchor from the glyph""" + del self.anchors[anchor.index] + self._hasChanged() + + def removeComponent(self, component): + """remove a specific component from the glyph""" + del self.components[component.index] + self._hasChanged() + + def center(self, padding=None): + """Equalise sidebearings, set to padding if wanted.""" + left = self.leftMargin + right = self.rightMargin + if padding: + e_left = e_right = padding + else: + e_left = (left + right)/2 + e_right = (left + right) - e_left + self.leftMargin = e_left + self.rightMargin = e_right + + def decompose(self): + """Decompose all components""" + for i in range(len(self.components)): + self.components[-1].decompose() + self._hasChanged() + + def clear(self, contours=True, components=True, anchors=True, guides=True): + """Clear all items marked as True from the glyph""" + if contours: + self.clearContours() + if components: + self.clearComponents() + if anchors: + self.clearAnchors() + if guides: + self.clearHGuides() + self.clearVGuides() + + def clearContours(self): + """clear all contours""" + self.contours = [] + self._hasChanged() + + def clearComponents(self): + """clear all components""" + self.components = [] + self._hasChanged() + + def clearAnchors(self): + """clear all anchors""" + self.anchors = [] + self._hasChanged() + + def clearHGuides(self): + """clear all horizontal guides""" + self.hGuides = [] + self._hasChanged() + + def clearVGuides(self): + """clear all vertical guides""" + self.vGuides = [] + self._hasChanged() + + def getAnchors(self): + return self.anchors + + def getComponents(self): + return self.components + + # + # stuff related to Glyph Properties + # + + + +class RContour(BaseContour): + + _title = "RoboFabContour" + + def __init__(self, object=None): + #BaseContour.__init__(self) + self.segments = [] + self.selected = False + + def __len__(self): + return len(self.segments) + + def __getitem__(self, index): + if index < len(self.segments): + return self.segments[index] + raise IndexError + + def _get_index(self): + return self.getParent().contours.index(self) + + def _set_index(self, index): + ogIndex = self.index + if index != ogIndex: + contourList = self.getParent().contours + contourList.insert(index, contourList.pop(ogIndex)) + + + index = property(_get_index, _set_index, doc="index of the contour") + + def _get_points(self): + points = [] + for segment in self.segments: + for point in segment.points: + points.append(point) + return points + + points = property(_get_points, doc="view the contour as a list of points") + + def _get_bPoints(self): + bPoints = [] + for segment in self.segments: + segType = segment.type + if segType == MOVE: + bType = CORNER + elif segType == LINE: + bType = CORNER + elif segType == CURVE: + if segment.smooth: + bType = CURVE + else: + bType = CORNER + else: + raise RoboFabError, "encountered unknown segment type" + b = RBPoint() + b.setParent(segment) + bPoints.append(b) + return bPoints + + bPoints = property(_get_bPoints, doc="view the contour as a list of bPoints") + + def appendSegment(self, segmentType, points, smooth=False): + """append a segment to the contour""" + segment = self.insertSegment(index=len(self.segments), segmentType=segmentType, points=points, smooth=smooth) + return segment + + def insertSegment(self, index, segmentType, points, smooth=False): + """insert a segment into the contour""" + segment = RSegment(segmentType, points, smooth) + segment.setParent(self) + self.segments.insert(index, segment) + self._hasChanged() + return segment + + def removeSegment(self, index): + """remove a segment from the contour""" + del self.segments[index] + self._hasChanged() + + def reverseContour(self): + """reverse the contour""" + from robofab.pens.reverseContourPointPen import ReverseContourPointPen + index = self.index + glyph = self.getParent() + pen = glyph.getPointPen() + reversePen = ReverseContourPointPen(pen) + self.drawPoints(reversePen) + # we've drawn the reversed contour onto our parent glyph, + # so it sits at the end of the contours list: + newContour = glyph.contours.pop(-1) + for segment in newContour.segments: + segment.setParent(self) + self.segments = newContour.segments + self._hasChanged() + + def setStartSegment(self, segmentIndex): + """set the first segment on the contour""" + # this obviously does not support open contours + if len(self.segments) < 2: + return + if segmentIndex == 0: + return + if segmentIndex > len(self.segments)-1: + raise IndexError, 'segment index not in segments list' + oldStart = self.segments[0] + oldLast = self.segments[-1] + #check to see if the contour ended with a curve on top of the move + #if we find one delete it, + if oldLast.type == CURVE or oldLast.type == QCURVE: + startOn = oldStart.onCurve + lastOn = oldLast.onCurve + if startOn.x == lastOn.x and startOn.y == lastOn.y: + del self.segments[0] + # since we deleted the first contour, the segmentIndex needs to shift + segmentIndex = segmentIndex - 1 + # if we DO have a move left over, we need to convert it to a line + if self.segments[0].type == MOVE: + self.segments[0].type = LINE + # slice up the segments and reassign them to the contour + segments = self.segments[segmentIndex:] + self.segments = segments + self.segments[:segmentIndex] + # now, draw the contour onto the parent glyph + glyph = self.getParent() + pen = glyph.getPointPen() + self.drawPoints(pen) + # we've drawn the new contour onto our parent glyph, + # so it sits at the end of the contours list: + newContour = glyph.contours.pop(-1) + for segment in newContour.segments: + segment.setParent(self) + self.segments = newContour.segments + self._hasChanged() + + +class RSegment(BaseSegment): + + _title = "RoboFabSegment" + + def __init__(self, segmentType=None, points=[], smooth=False): + BaseSegment.__init__(self) + self.selected = False + self.points = [] + self.smooth = smooth + if points: + #the points in the segment should be RPoints, so create those objects + for point in points[:-1]: + x, y = point + p = RPoint(x, y, pointType=OFFCURVE) + p.setParent(self) + self.points.append(p) + aX, aY = points[-1] + p = RPoint(aX, aY, segmentType) + p.setParent(self) + self.points.append(p) + + def _get_type(self): + return self.points[-1].type + + def _set_type(self, pointType): + onCurve = self.points[-1] + ocType = onCurve.type + if ocType == pointType: + return + #we are converting a cubic line into a cubic curve + if pointType == CURVE and ocType == LINE: + onCurve.type = pointType + parent = self.getParent() + prev = parent._prevSegment(self.index) + p1 = RPoint(prev.onCurve.x, prev.onCurve.y, pointType=OFFCURVE) + p1.setParent(self) + p2 = RPoint(onCurve.x, onCurve.y, pointType=OFFCURVE) + p2.setParent(self) + self.points.insert(0, p2) + self.points.insert(0, p1) + #we are converting a cubic move to a curve + elif pointType == CURVE and ocType == MOVE: + onCurve.type = pointType + parent = self.getParent() + prev = parent._prevSegment(self.index) + p1 = RPoint(prev.onCurve.x, prev.onCurve.y, pointType=OFFCURVE) + p1.setParent(self) + p2 = RPoint(onCurve.x, onCurve.y, pointType=OFFCURVE) + p2.setParent(self) + self.points.insert(0, p2) + self.points.insert(0, p1) + #we are converting a quad curve to a cubic curve + elif pointType == CURVE and ocType == QCURVE: + onCurve.type == CURVE + #we are converting a cubic curve into a cubic line + elif pointType == LINE and ocType == CURVE: + p = self.points.pop(-1) + self.points = [p] + onCurve.type = pointType + self.smooth = False + #we are converting a cubic move to a line + elif pointType == LINE and ocType == MOVE: + onCurve.type = pointType + #we are converting a quad curve to a line: + elif pointType == LINE and ocType == QCURVE: + p = self.points.pop(-1) + self.points = [p] + onCurve.type = pointType + self.smooth = False + # we are converting to a quad curve where just about anything is legal + elif pointType == QCURVE: + onCurve.type = pointType + else: + raise RoboFabError, 'unknown segment type' + + type = property(_get_type, _set_type, doc="type of the segment") + + def _get_index(self): + return self.getParent().segments.index(self) + + index = property(_get_index, doc="index of the segment") + + def insertPoint(self, index, pointType, point): + x, y = point + p = RPoint(x, y, pointType=pointType) + p.setParent(self) + self.points.insert(index, p) + self._hasChanged() + + def removePoint(self, index): + del self.points[index] + self._hasChanged() + + +class RBPoint(BaseBPoint): + + _title = "RoboFabBPoint" + + def _setAnchorChanged(self, value): + self._anchorPoint.setChanged(value) + + def _setNextChanged(self, value): + self._nextOnCurve.setChanged(value) + + def _get__parentSegment(self): + return self.getParent() + + _parentSegment = property(_get__parentSegment, doc="") + + def _get__nextOnCurve(self): + pSeg = self._parentSegment + contour = pSeg.getParent() + #could this potentially return an incorrect index? say, if two segments are exactly the same? + return contour.segments[(contour.segments.index(pSeg) + 1) % len(contour.segments)] + + _nextOnCurve = property(_get__nextOnCurve, doc="") + + def _get_index(self): + return self._parentSegment.index + + index = property(_get_index, doc="index of the bPoint on the contour") + + +class RPoint(BasePoint): + + _title = "RoboFabPoint" + + def __init__(self, x=0, y=0, pointType=None, name=None): + self.selected = False + self._type = pointType + self._x = x + self._y = y + self._name = name + + def _get_x(self): + return self._x + + def _set_x(self, value): + self._x = value + self._hasChanged() + + x = property(_get_x, _set_x, doc="") + + def _get_y(self): + return self._y + + def _set_y(self, value): + self._y = value + self._hasChanged() + + y = property(_get_y, _set_y, doc="") + + def _get_type(self): + return self._type + + def _set_type(self, value): + self._type = value + self._hasChanged() + + type = property(_get_type, _set_type, doc="") + + def _get_name(self): + return self._name + + def _set_name(self, value): + self._name = value + self._hasChanged() + + name = property(_get_name, _set_name, doc="") + + +class RAnchor(BaseAnchor): + + _title = "RoboFabAnchor" + + def __init__(self, name=None, position=None, mark=None): + BaseAnchor.__init__(self) + self.selected = False + self.name = name + if position is None: + self.x = self.y = None + else: + self.x, self.y = position + self.mark = mark + + def _get_index(self): + if self.getParent() is None: return None + return self.getParent().anchors.index(self) + + index = property(_get_index, doc="index of the anchor") + + def _get_position(self): + return (self.x, self.y) + + def _set_position(self, value): + self.x = value[0] + self.y = value[1] + self._hasChanged() + + position = property(_get_position, _set_position, doc="position of the anchor") + + def move(self, pt): + """Move the anchor""" + self.x = self.x + pt[0] + self.y = self.y + pt[0] + self._hasChanged() + + +class RComponent(BaseComponent): + + _title = "RoboFabComponent" + + def __init__(self, baseGlyphName=None, offset=(0,0), scale=(1,1), transform=None): + BaseComponent.__init__(self) + self.selected = False + self._baseGlyph = baseGlyphName + self._offset = offset + self._scale = scale + if transform is None: + xx, yy = scale + dx, dy = offset + self.transformation = (xx, 0, 0, yy, dx, dy) + else: + self.transformation = transform + + def _get_index(self): + if self.getParent() is None: return None + return self.getParent().components.index(self) + + index = property(_get_index, doc="index of the component") + + def _get_baseGlyph(self): + return self._baseGlyph + + def _set_baseGlyph(self, glyphName): + # XXXX needs to be implemented in objectsFL for symmetricity's sake. Eventually. + self._baseGlyph = glyphName + self._hasChanged() + + baseGlyph = property(_get_baseGlyph, _set_baseGlyph, doc="") + + def _get_offset(self): + """ Get the offset component of the transformation.=""" + (xx, xy, yx, yy, dx, dy) = self._transformation + return dx, dy + + def _set_offset(self, value): + """ Set the offset component of the transformation.""" + (xx, xy, yx, yy, dx, dy) = self._transformation + self._transformation = (xx, xy, yx, yy, value[0], value[1]) + self._hasChanged() + + offset = property(_get_offset, _set_offset, doc="the offset of the component") + + def _get_scale(self): + """ Return the scale components of the transformation.""" + (xx, xy, yx, yy, dx, dy) = self._transformation + return xx, yy + + def _set_scale(self, scale): + """ Set the scale component of the transformation. + Note: setting this value effectively makes the xy and yx values meaningless. + We're assuming that if you're setting the xy and yx values, you will use + the transformation attribute rather than the scale and offset attributes. + """ + xScale, yScale = scale + (xx, xy, yx, yy, dx, dy) = self._transformation + self._transformation = (xScale, xy, yx, yScale, dx, dy) + self._hasChanged() + + scale = property(_get_scale, _set_scale, doc="the scale of the component") + + def _get_transformation(self): + return self._transformation + + def _set_transformation(self, transformation): + assert len(transformation)==6, "Transformation matrix must have 6 values" + self._transformation = transformation + + transformation = property(_get_transformation, _set_transformation, doc="the transformation matrix of the component") + + def move(self, pt): + """Move the component""" + (xx, xy, yx, yy, dx, dy) = self._transformation + self._transformation = (xx, xy, yx, yy, dx+pt[0], dy+pt[1]) + self._hasChanged() + + def decompose(self): + """Decompose the component""" + baseGlyphName = self.baseGlyph + parentGlyph = self.getParent() + # if there is no parent glyph, there is nothing to decompose to + if baseGlyphName is not None and parentGlyph is not None: + parentFont = parentGlyph.getParent() + # we must have a parent glyph with the baseGlyph + # if not, we will simply remove the component from + # the parent glyph thereby decomposing the component + # to nothing. + if parentFont is not None and parentFont.has_key(baseGlyphName): + from robofab.pens.adapterPens import TransformPointPen + baseGlyph = parentFont[baseGlyphName] + for contour in baseGlyph.contours: + pointPen = parentGlyph.getPointPen() + transPen = TransformPointPen(pointPen, self._transformation) + contour.drawPoints(transPen) + parentGlyph.components.remove(self) + + +class RKerning(BaseKerning): + + _title = "RoboFabKerning" + + +class RGroups(BaseGroups): + + _title = "RoboFabGroups" + +class RLib(BaseLib): + + _title = "RoboFabLib" + + +class RInfo(BaseInfo): + + _title = "RoboFabFontInfo" + +class RFeatures(BaseFeatures): + + _title = "RoboFabFeatures" + |