summaryrefslogtreecommitdiff
path: root/misc/pylib/robofab/objects
diff options
context:
space:
mode:
Diffstat (limited to 'misc/pylib/robofab/objects')
-rwxr-xr-xmisc/pylib/robofab/objects/__init__.py15
-rwxr-xr-xmisc/pylib/robofab/objects/objectsBase.pyx3426
-rw-r--r--misc/pylib/robofab/objects/objectsFF.py1253
-rwxr-xr-xmisc/pylib/robofab/objects/objectsFL.py3112
-rwxr-xr-xmisc/pylib/robofab/objects/objectsRF.pyx1233
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"
+