git-svn-id: http://svn.robofab.com/branches/ufo3k@261 b5fa9d6c-a76f-4ffd-b3cb-f825fc41095c
3438 lines
104 KiB
Python
Executable File
3438 lines
104 KiB
Python
Executable File
"""
|
|
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 fontTools.misc.arrayTools import updateBounds, pointInRect, unionRect, sectRect
|
|
from fontTools.pens.basePen import AbstractPen
|
|
|
|
|
|
#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((xa, ya),(xb, yb),v):
|
|
"""interpolate point by factor v"""
|
|
if not isinstance(v, tuple):
|
|
xv = v
|
|
yv = v
|
|
else:
|
|
xv, yv = v
|
|
return xa + (xb-xa) * xv, ya + (yb-ya) * yv
|
|
|
|
def _scalePointFromCenter((pointX, pointY), (scaleX, scaleY), (centerX, centerY)):
|
|
"""scale a point from a center point"""
|
|
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)
|
|
|
|
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((x, y)):
|
|
"""Round a vector"""
|
|
return int(round(x)), int(round(y))
|
|
|
|
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.glyphs:
|
|
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."""
|
|
from sets import Set
|
|
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
|
|
from sets import Set
|
|
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
|
|
from sets import Set
|
|
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 _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, (x, y), evenOdd=0):
|
|
"""determine if the point is in the black or white of the glyph"""
|
|
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, (x, y), contours=True, components=True, anchors=True):
|
|
"""Move a glyph's items that are flagged as True"""
|
|
x, y = roundPt((x, y))
|
|
for contour in self.contours:
|
|
contour.move((x, y))
|
|
for component in self.components:
|
|
component.move((x, y))
|
|
for anchor in self.anchors:
|
|
anchor.move((x, y))
|
|
|
|
def scale(self, (x, y), center=(0, 0)):
|
|
"""scale the glyph"""
|
|
for contour in self.contours:
|
|
contour.scale((x, y), center=center)
|
|
for component in self.components:
|
|
offset = component.offset
|
|
component.offset = _scalePointFromCenter(offset, (x, y), 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):
|
|
anchors = []
|
|
lastOn = None
|
|
for segment in self.segments:
|
|
anchor = segment.onCurve
|
|
anchor = (anchor.x, anchor.y)
|
|
# overlapping points can give false results, so filter them out
|
|
if anchor == lastOn:
|
|
continue
|
|
anchors.append(anchor)
|
|
lastOn = anchor
|
|
#overlapping moves can give false results, so filter them out
|
|
first = anchors[0]
|
|
last = anchors[-1]
|
|
if first == last:
|
|
del anchors[-1]
|
|
nPoints = len(anchors)
|
|
angles = []
|
|
for i1 in range(nPoints):
|
|
i2 = (i1 + 1) % nPoints
|
|
x1, y1 = anchors[i1]
|
|
x2, y2 = anchors[i2]
|
|
angles.append(math.atan2(y2-y1, x2-x1))
|
|
total = 0
|
|
pi = math.pi
|
|
pi2 = pi * 2
|
|
for i1 in range(nPoints):
|
|
i2 = (i1 + 1) % nPoints
|
|
d = ((angles[i2] - angles[i1] + pi) % pi2) - pi
|
|
total += d
|
|
return total < 0
|
|
|
|
clockwise = property(_get_clockwise, _set_clockwise, doc="direction of contour: 1=clockwise 0=counterclockwise")
|
|
|
|
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(point, '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, (x, y)):
|
|
"""move the contour"""
|
|
#this will be faster if we go straight to the points
|
|
for point in self.points:
|
|
point.move((x, y))
|
|
|
|
def scale(self,(x, y), center=(0, 0)):
|
|
"""scale the contour"""
|
|
#this will be faster if we go straight to the points
|
|
for point in self.points:
|
|
point.scale((x, y), 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, (x, y), 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=(x, y), 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, (x, y)):
|
|
"""move the segment"""
|
|
for point in self.points:
|
|
point.move((x, y))
|
|
|
|
def scale(self, (x, y), center=(0, 0)):
|
|
"""scale the segment"""
|
|
for point in self.points:
|
|
point.scale((x, y), 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, (x, y)):
|
|
"""Move the point"""
|
|
self.x, self.y = addPt((self.x, self.y), (x, y))
|
|
|
|
def scale(self, (x, y), center=(0, 0)):
|
|
"""scale the point"""
|
|
nX, nY = _scalePointFromCenter((self.x, self.y), (x, y), 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, (x, y)):
|
|
"""move the bPoint"""
|
|
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, (x, y), center=(0, 0)):
|
|
"""scale the bPoint"""
|
|
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, (x, y)):
|
|
"""Move the anchor"""
|
|
pX, pY = self.position
|
|
self.position = (pX+x, pY+y)
|
|
|
|
def scale(self, (x, y), center=(0, 0)):
|
|
"""scale the anchor"""
|
|
pos = self.position
|
|
self.position = _scalePointFromCenter(pos, (x, y), 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."""
|
|
from sets import Set
|
|
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):
|
|
from sets import Set
|
|
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):
|
|
from sets import Set
|
|
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)
|
|
|