summaryrefslogtreecommitdiff
path: root/misc/pylib/robofab/objects
diff options
context:
space:
mode:
authorRasmus Andersson <rasmus@notion.se>2017-09-04 06:03:17 +0300
committerRasmus Andersson <rasmus@notion.se>2017-09-04 18:12:34 +0300
commit8234b62ab762637ef24c3398b4204a8ce8db31a7 (patch)
tree1c8df547021cdb58951630a015e4101ede46dbf1 /misc/pylib/robofab/objects
parent31ae014e0c827dd76696fdab7e4ca3fed9f6402b (diff)
downloadinter-8234b62ab762637ef24c3398b4204a8ce8db31a7.tar.xz
Speeds up font compilation by around 200%
Cython is used to compile some hot paths into native Python extensions. These hot paths were identified through running ufocompile with the hotshot profiler and then converting file by file to Cython, starting with the "hottest" paths and continuing until returns were deminishing. This means that only a few Python files were converted to Cython. Closes #23 Closes #20 (really this time)
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"
+