2008-02-02 12:37:51 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
glifLib.py -- Generic module for reading and writing the .glif format.
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
More info about the .glif format (GLyphInterchangeFormat) can be found here:
|
|
|
|
|
2011-06-18 17:08:25 +00:00
|
|
|
http://unifiedfontobject.org
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
The main class in this module is GlyphSet. It manages a set of .glif files
|
|
|
|
in a folder. It offers two ways to read glyph data, and one way to write
|
|
|
|
glyph data. See the class doc string for details.
|
|
|
|
"""
|
|
|
|
|
|
|
|
import os
|
2011-09-28 01:01:39 +00:00
|
|
|
from cStringIO import StringIO
|
2011-09-18 12:16:25 +00:00
|
|
|
from xmlTreeBuilder import buildTree, stripCharacterData
|
2008-01-07 17:40:34 +00:00
|
|
|
from robofab.pens.pointPen import AbstractPointPen
|
2011-09-28 16:17:53 +00:00
|
|
|
from plistlib import readPlist, writePlistToString
|
2011-09-28 01:01:39 +00:00
|
|
|
from filenames import userNameToFileName
|
2011-10-06 19:23:48 +00:00
|
|
|
from validators import isDictEnough, genericTypeValidator, colorValidator, guidelinesValidator, identifierValidator, imageValidator, libValidator
|
2011-09-28 18:07:13 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
set
|
|
|
|
except NameError:
|
|
|
|
from sets import Set as set
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
__all__ = [
|
|
|
|
"GlyphSet",
|
|
|
|
"GlifLibError",
|
|
|
|
"readGlyphFromString", "writeGlyphToString",
|
|
|
|
"glyphNameToFileName"
|
|
|
|
]
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
class GlifLibError(Exception): pass
|
|
|
|
|
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# -------------------------
|
|
|
|
# Reading and Writing Modes
|
|
|
|
# -------------------------
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
if os.name == "mac":
|
|
|
|
WRITE_MODE = "wb" # use unix line endings, even with Classic MacPython
|
|
|
|
READ_MODE = "rb"
|
|
|
|
else:
|
|
|
|
WRITE_MODE = "w"
|
|
|
|
READ_MODE = "r"
|
|
|
|
|
2011-09-29 14:09:45 +00:00
|
|
|
# ---------
|
|
|
|
# Constants
|
|
|
|
# ---------
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 15:29:10 +00:00
|
|
|
LAYERINFO_FILENAME = "layerinfo.plist"
|
2011-09-29 14:09:45 +00:00
|
|
|
supportedUFOFormatVersions = [1, 2, 3]
|
2011-09-28 17:21:07 +00:00
|
|
|
supportedGLIFFormatVersions = [1, 2]
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 13:16:25 +00:00
|
|
|
# ------------
|
|
|
|
# Simple Glyph
|
|
|
|
# ------------
|
|
|
|
|
|
|
|
class Glyph(object):
|
|
|
|
|
|
|
|
"""
|
|
|
|
Minimal glyph object. It has no glyph attributes until either
|
|
|
|
the draw() or the drawPoint() method has been called.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, glyphName, glyphSet):
|
|
|
|
self.glyphName = glyphName
|
|
|
|
self.glyphSet = glyphSet
|
|
|
|
|
|
|
|
def draw(self, pen):
|
|
|
|
"""
|
|
|
|
Draw this glyph onto a *FontTools* Pen.
|
|
|
|
"""
|
|
|
|
from robofab.pens.adapterPens import PointToSegmentPen
|
|
|
|
pointPen = PointToSegmentPen(pen)
|
|
|
|
self.drawPoints(pointPen)
|
|
|
|
|
|
|
|
def drawPoints(self, pointPen):
|
|
|
|
"""
|
|
|
|
Draw this glyph onto a PointPen.
|
|
|
|
"""
|
|
|
|
self.glyphSet.readGlyph(self.glyphName, self, pointPen)
|
|
|
|
|
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# ---------
|
|
|
|
# Glyph Set
|
|
|
|
# ---------
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 00:46:51 +00:00
|
|
|
class GlyphSet(object):
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
GlyphSet manages a set of .glif files inside one directory.
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
GlyphSet's constructor takes a path to an existing directory as it's
|
|
|
|
first argument. Reading glyph data can either be done through the
|
|
|
|
readGlyph() method, or by using GlyphSet's dictionary interface, where
|
|
|
|
the keys are glyph names and the values are (very) simple glyph objects.
|
|
|
|
|
|
|
|
To write a glyph to the glyph set, you use the writeGlyph() method.
|
|
|
|
The simple glyph objects returned through the dict interface do not
|
2011-06-18 17:08:25 +00:00
|
|
|
support writing, they are just a convenient way to get at the glyph data.
|
2008-01-07 17:40:34 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
glyphClass = Glyph
|
|
|
|
|
2011-09-28 12:52:10 +00:00
|
|
|
def __init__(self, dirName, glyphNameToFileNameFunc=None, ufoFormatVersion=3):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
'dirName' should be a path to an existing directory.
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
The optional 'glyphNameToFileNameFunc' argument must be a callback
|
|
|
|
function that takes two arguments: a glyph name and the GlyphSet
|
|
|
|
instance. It should return a file name (including the .glif
|
|
|
|
extension). The glyphNameToFileName function is called whenever
|
|
|
|
a file name is created for a given glyph name.
|
|
|
|
"""
|
|
|
|
self.dirName = dirName
|
2011-09-29 14:09:45 +00:00
|
|
|
if ufoFormatVersion not in supportedUFOFormatVersions:
|
|
|
|
raise GlifLibError("Unsupported UFO format version: %s" % ufoFormatVersion)
|
2011-09-28 12:52:10 +00:00
|
|
|
self.ufoFormatVersion = ufoFormatVersion
|
2008-01-07 17:40:34 +00:00
|
|
|
if glyphNameToFileNameFunc is None:
|
|
|
|
glyphNameToFileNameFunc = glyphNameToFileName
|
|
|
|
self.glyphNameToFileName = glyphNameToFileNameFunc
|
2011-09-29 13:26:25 +00:00
|
|
|
self.rebuildContents()
|
2008-01-07 17:40:34 +00:00
|
|
|
self._reverseContents = None
|
2011-06-18 17:08:25 +00:00
|
|
|
self._glifCache = {}
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
def rebuildContents(self):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
2011-09-29 00:55:34 +00:00
|
|
|
Rebuild the contents dict by loading contents.plist.
|
2008-01-07 17:40:34 +00:00
|
|
|
"""
|
2011-09-29 00:55:34 +00:00
|
|
|
contentsPath = os.path.join(self.dirName, "contents.plist")
|
|
|
|
if not os.path.exists(contentsPath):
|
2011-09-29 13:26:25 +00:00
|
|
|
# missing, consider the glyphset empty.
|
|
|
|
contents = {}
|
|
|
|
else:
|
|
|
|
contents = self._readPlist(contentsPath)
|
2011-09-29 00:55:34 +00:00
|
|
|
# validate the contents
|
|
|
|
invalidFormat = False
|
|
|
|
if not isinstance(contents, dict):
|
|
|
|
invalidFormat = True
|
|
|
|
else:
|
|
|
|
for name, fileName in contents.items():
|
|
|
|
if not isinstance(name, basestring):
|
|
|
|
invalidFormat = True
|
|
|
|
if not isinstance(fileName, basestring):
|
|
|
|
invalidFormat = True
|
|
|
|
elif not os.path.exists(os.path.join(self.dirName, fileName)):
|
|
|
|
raise GlifLibError("contents.plist references a file that does not exist: %s" % fileName)
|
|
|
|
if invalidFormat:
|
|
|
|
raise GlifLibError("contents.plist is not properly formatted")
|
|
|
|
self.contents = contents
|
2008-01-07 17:40:34 +00:00
|
|
|
self._reverseContents = None
|
|
|
|
|
|
|
|
def getReverseContents(self):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Return a reversed dict of self.contents, mapping file names to
|
2008-01-07 17:40:34 +00:00
|
|
|
glyph names. This is primarily an aid for custom glyph name to file
|
|
|
|
name schemes that want to make sure they don't generate duplicate
|
|
|
|
file names. The file names are converted to lowercase so we can
|
|
|
|
reliably check for duplicates that only differ in case, which is
|
|
|
|
important for case-insensitive file systems.
|
|
|
|
"""
|
|
|
|
if self._reverseContents is None:
|
|
|
|
d = {}
|
|
|
|
for k, v in self.contents.iteritems():
|
|
|
|
d[v.lower()] = k
|
|
|
|
self._reverseContents = d
|
|
|
|
return self._reverseContents
|
|
|
|
|
|
|
|
def writeContents(self):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Write the contents.plist file out to disk. Call this method when
|
2008-01-07 17:40:34 +00:00
|
|
|
you're done writing glyphs.
|
|
|
|
"""
|
|
|
|
contentsPath = os.path.join(self.dirName, "contents.plist")
|
|
|
|
# We need to force Unix line endings, even in OS9 MacPython in FL,
|
|
|
|
# so we do the writing to file ourselves.
|
|
|
|
plist = writePlistToString(self.contents)
|
|
|
|
f = open(contentsPath, WRITE_MODE)
|
|
|
|
f.write(plist)
|
|
|
|
f.close()
|
|
|
|
|
2011-09-28 12:52:10 +00:00
|
|
|
# layer info
|
|
|
|
|
|
|
|
def readLayerInfo(self, info):
|
|
|
|
path = os.path.join(self.dirName, LAYERINFO_FILENAME)
|
|
|
|
if not os.path.exists(path):
|
|
|
|
return
|
2011-09-28 15:29:10 +00:00
|
|
|
infoDict = self._readPlist(path)
|
|
|
|
if not isinstance(infoDict, dict):
|
|
|
|
raise GlifLibError("layerinfo.plist is not properly formatted.")
|
|
|
|
infoDict = validateLayerInfoVersion3Data(infoDict)
|
2011-09-28 12:52:10 +00:00
|
|
|
# populate the object
|
2011-09-28 15:29:10 +00:00
|
|
|
for attr, value in infoDict.items():
|
2011-09-28 12:52:10 +00:00
|
|
|
try:
|
|
|
|
setattr(info, attr, value)
|
|
|
|
except AttributeError:
|
|
|
|
raise GlifLibError("The supplied layer info object does not support setting a necessary attribute (%s)." % attr)
|
|
|
|
|
|
|
|
def writeLayerInfo(self, info):
|
|
|
|
if self.ufoFormatVersion < 3:
|
|
|
|
raise GlifLibError("layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersion)
|
|
|
|
# gather data
|
|
|
|
infoData = {}
|
|
|
|
for attr in layerInfoVersion3ValueData.keys():
|
|
|
|
if hasattr(info, attr):
|
|
|
|
try:
|
|
|
|
value = getattr(info, attr)
|
|
|
|
except AttributeError:
|
|
|
|
raise GlifLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr)
|
|
|
|
if value is None:
|
|
|
|
continue
|
|
|
|
infoData[attr] = value
|
|
|
|
# validate
|
|
|
|
infoData = validateLayerInfoVersion3Data(infoData)
|
|
|
|
# write file
|
|
|
|
path = os.path.join(self.dirName, LAYERINFO_FILENAME)
|
|
|
|
plist = writePlistToString(infoData)
|
|
|
|
f = open(path, WRITE_MODE)
|
|
|
|
f.write(plist)
|
|
|
|
f.close()
|
|
|
|
|
2011-06-18 17:08:25 +00:00
|
|
|
# read caching
|
|
|
|
|
|
|
|
def getGLIF(self, glyphName):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Get the raw GLIF text for a given glyph name. This only works
|
2011-06-18 17:08:25 +00:00
|
|
|
for GLIF files that are already on disk.
|
|
|
|
|
|
|
|
This method is useful in situations when the raw XML needs to be
|
|
|
|
read from a glyph set for a particular glyph before fully parsing
|
|
|
|
it into an object structure via the readGlyph method.
|
|
|
|
|
|
|
|
Internally, this method will load a GLIF the first time it is
|
|
|
|
called and then cache it. The next time this method is called
|
|
|
|
the GLIF will be pulled from the cache if the file's modification
|
|
|
|
time has not changed since the GLIF was cached. For memory
|
|
|
|
efficiency, the cached GLIF will be purged by various other methods
|
|
|
|
such as readGlyph.
|
|
|
|
"""
|
|
|
|
needRead = False
|
|
|
|
fileName = self.contents.get(glyphName)
|
|
|
|
path = None
|
|
|
|
if fileName is not None:
|
|
|
|
path = os.path.join(self.dirName, fileName)
|
|
|
|
if glyphName not in self._glifCache:
|
|
|
|
needRead = True
|
|
|
|
elif fileName is not None and os.path.getmtime(path) != self._glifCache[glyphName][1]:
|
|
|
|
needRead = True
|
|
|
|
if needRead:
|
|
|
|
fileName = self.contents[glyphName]
|
|
|
|
if not os.path.exists(path):
|
|
|
|
raise KeyError, glyphName
|
|
|
|
f = open(path, "rb")
|
|
|
|
text = f.read()
|
|
|
|
f.close()
|
|
|
|
self._glifCache[glyphName] = (text, os.path.getmtime(path))
|
|
|
|
return self._glifCache[glyphName][0]
|
|
|
|
|
|
|
|
def _purgeCachedGLIF(self, glyphName):
|
|
|
|
if glyphName in self._glifCache:
|
|
|
|
del self._glifCache[glyphName]
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
# reading/writing API
|
|
|
|
|
|
|
|
def readGlyph(self, glyphName, glyphObject=None, pointPen=None):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Read a .glif file for 'glyphName' from the glyph set. The
|
2008-01-07 17:40:34 +00:00
|
|
|
'glyphObject' argument can be any kind of object (even None);
|
|
|
|
the readGlyph() method will attempt to set the following
|
|
|
|
attributes on it:
|
2011-10-03 16:24:24 +00:00
|
|
|
"width" the advance with of the glyph
|
|
|
|
"height" the advance height of the glyph
|
|
|
|
"unicodes" a list of unicode values for this glyph
|
|
|
|
"note" a string
|
|
|
|
"lib" a dictionary containing custom data
|
|
|
|
"image" a dictionary containing image data
|
|
|
|
"guidelines" a list of guideline data dictionaries
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
All attributes are optional, in two ways:
|
|
|
|
1) An attribute *won't* be set if the .glif file doesn't
|
|
|
|
contain data for it. 'glyphObject' will have to deal
|
|
|
|
with default values itself.
|
|
|
|
2) If setting the attribute fails with an AttributeError
|
|
|
|
(for example if the 'glyphObject' attribute is read-
|
|
|
|
only), readGlyph() will not propagate that exception,
|
|
|
|
but ignore that attribute.
|
|
|
|
|
|
|
|
To retrieve outline information, you need to pass an object
|
|
|
|
conforming to the PointPen protocol as the 'pointPen' argument.
|
|
|
|
This argument may be None if you don't need the outline data.
|
|
|
|
|
|
|
|
readGlyph() will raise KeyError if the glyph is not present in
|
|
|
|
the glyph set.
|
|
|
|
"""
|
2011-06-18 17:08:25 +00:00
|
|
|
text = self.getGLIF(glyphName)
|
|
|
|
self._purgeCachedGLIF(glyphName)
|
|
|
|
tree = _glifTreeFromFile(StringIO(text))
|
2011-10-07 13:54:14 +00:00
|
|
|
if self.ufoFormatVersion < 3:
|
|
|
|
formatVersions = (1,)
|
|
|
|
else:
|
|
|
|
formatVersions = (1, 2)
|
|
|
|
_readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions)
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-29 14:09:45 +00:00
|
|
|
def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=None):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Write a .glif file for 'glyphName' to the glyph set. The
|
2008-01-07 17:40:34 +00:00
|
|
|
'glyphObject' argument can be any kind of object (even None);
|
|
|
|
the writeGlyph() method will attempt to get the following
|
|
|
|
attributes from it:
|
2011-10-03 16:24:24 +00:00
|
|
|
"width" the advance with of the glyph
|
|
|
|
"height" the advance height of the glyph
|
|
|
|
"unicodes" a list of unicode values for this glyph
|
|
|
|
"note" a string
|
|
|
|
"lib" a dictionary containing custom data
|
|
|
|
"image" a dictionary containing image data
|
|
|
|
"guidelines" a list of guideline data dictionaries
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
All attributes are optional: if 'glyphObject' doesn't
|
|
|
|
have the attribute, it will simply be skipped.
|
|
|
|
|
|
|
|
To write outline data to the .glif file, writeGlyph() needs
|
|
|
|
a function (any callable object actually) that will take one
|
|
|
|
argument: an object that conforms to the PointPen protocol.
|
|
|
|
The function will be called by writeGlyph(); it has to call the
|
|
|
|
proper PointPen methods to transfer the outline to the .glif file.
|
2011-09-29 14:09:45 +00:00
|
|
|
|
|
|
|
The GLIF format version will be chosen based on the ufoFormatVersion
|
|
|
|
passed during the creation of this object. If a particular format
|
|
|
|
version is desired, it can be passed with the formatVersion argument.
|
2008-01-07 17:40:34 +00:00
|
|
|
"""
|
2011-09-29 14:09:45 +00:00
|
|
|
if formatVersion is None:
|
|
|
|
if self.ufoFormatVersion >= 3:
|
|
|
|
formatVersion = 2
|
|
|
|
else:
|
|
|
|
formatVersion = 1
|
|
|
|
else:
|
|
|
|
if formatVersion not in supportedGLIFFormatVersions:
|
|
|
|
raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion)
|
|
|
|
if formatVersion == 2 and self.ufoFormatVersion < 3:
|
|
|
|
raise GlifLibError("Unsupported GLIF format version (%d) for UFO format version %d." % (formatVersion, self.ufoFormatVersion))
|
2011-06-18 17:08:25 +00:00
|
|
|
self._purgeCachedGLIF(glyphName)
|
2011-09-29 14:21:02 +00:00
|
|
|
data = writeGlyphToString(glyphName, glyphObject, drawPointsFunc, formatVersion=formatVersion)
|
2008-01-07 17:40:34 +00:00
|
|
|
fileName = self.contents.get(glyphName)
|
|
|
|
if fileName is None:
|
|
|
|
fileName = self.glyphNameToFileName(glyphName, self)
|
|
|
|
self.contents[glyphName] = fileName
|
|
|
|
if self._reverseContents is not None:
|
|
|
|
self._reverseContents[fileName.lower()] = glyphName
|
|
|
|
path = os.path.join(self.dirName, fileName)
|
|
|
|
if os.path.exists(path):
|
|
|
|
f = open(path, READ_MODE)
|
|
|
|
oldData = f.read()
|
|
|
|
f.close()
|
|
|
|
if data == oldData:
|
|
|
|
return
|
|
|
|
f = open(path, WRITE_MODE)
|
|
|
|
f.write(data)
|
|
|
|
f.close()
|
|
|
|
|
|
|
|
def deleteGlyph(self, glyphName):
|
|
|
|
"""Permanently delete the glyph from the glyph set on disk. Will
|
|
|
|
raise KeyError if the glyph is not present in the glyph set.
|
|
|
|
"""
|
2011-06-18 17:08:25 +00:00
|
|
|
self._purgeCachedGLIF(glyphName)
|
2008-01-07 17:40:34 +00:00
|
|
|
fileName = self.contents[glyphName]
|
|
|
|
os.remove(os.path.join(self.dirName, fileName))
|
|
|
|
if self._reverseContents is not None:
|
|
|
|
del self._reverseContents[self.contents[glyphName].lower()]
|
|
|
|
del self.contents[glyphName]
|
|
|
|
|
|
|
|
# dict-like support
|
|
|
|
|
|
|
|
def keys(self):
|
|
|
|
return self.contents.keys()
|
|
|
|
|
|
|
|
def has_key(self, glyphName):
|
|
|
|
return glyphName in self.contents
|
|
|
|
|
|
|
|
__contains__ = has_key
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
return len(self.contents)
|
|
|
|
|
|
|
|
def __getitem__(self, glyphName):
|
|
|
|
if glyphName not in self.contents:
|
|
|
|
raise KeyError, glyphName
|
|
|
|
return self.glyphClass(glyphName, self)
|
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# quickly fetch unicode values
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
def getUnicodes(self):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Return a dictionary that maps all glyph names to lists containing
|
2008-01-07 17:40:34 +00:00
|
|
|
the unicode value[s] for that glyph, if any. This parses the .glif
|
2011-10-11 14:08:24 +00:00
|
|
|
files partially, so it is a lot faster than parsing all files completely.
|
2008-01-07 17:40:34 +00:00
|
|
|
"""
|
|
|
|
unicodes = {}
|
2011-06-18 17:08:25 +00:00
|
|
|
for glyphName in self.contents.keys():
|
|
|
|
text = self.getGLIF(glyphName)
|
|
|
|
unicodes[glyphName] = _fetchUnicodes(text)
|
2008-01-07 17:40:34 +00:00
|
|
|
return unicodes
|
|
|
|
|
2011-10-11 14:08:24 +00:00
|
|
|
def getComponentReferences(self):
|
|
|
|
"""
|
|
|
|
Return a dictionary that maps all glyph names to lists containing the
|
|
|
|
base glyph name of components in the glyph. This parses the .glif
|
|
|
|
files partially, so it is a lot faster than parsing all files completely.
|
|
|
|
"""
|
|
|
|
components = {}
|
|
|
|
for glyphName in self.contents.keys():
|
|
|
|
text = self.getGLIF(glyphName)
|
|
|
|
components[glyphName] = _fetchComponentBases(text)
|
|
|
|
return components
|
|
|
|
|
|
|
|
def getImageReferences(self):
|
|
|
|
"""
|
|
|
|
Return a dictionary that maps all glyph names to the file name of the image
|
|
|
|
referenced by the glyph. This parses the .glif files partially, so it is a
|
|
|
|
lot faster than parsing all files completely.
|
|
|
|
"""
|
|
|
|
images = {}
|
|
|
|
for glyphName in self.contents.keys():
|
|
|
|
text = self.getGLIF(glyphName)
|
|
|
|
images[glyphName] = _fetchImageFileName(text)
|
|
|
|
return images
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
# internal methods
|
|
|
|
|
2011-09-28 15:29:10 +00:00
|
|
|
def _readPlist(self, path):
|
|
|
|
try:
|
|
|
|
data = readPlist(path)
|
|
|
|
return data
|
|
|
|
except:
|
|
|
|
raise GlifLibError("The file %s could not be read." % path)
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# -----------------------
|
|
|
|
# Glyph Name to File Name
|
|
|
|
# -----------------------
|
|
|
|
|
|
|
|
def glyphNameToFileName(glyphName, glyphSet):
|
|
|
|
"""
|
|
|
|
Wrapper around the userNameToFileName function in filenames.py
|
|
|
|
"""
|
|
|
|
existing = [name.lower() for name in glyphSet.contents.values()]
|
|
|
|
if not isinstance(glyphName, unicode):
|
|
|
|
try:
|
|
|
|
new = unicode(glyphName)
|
|
|
|
glyphName = new
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
pass
|
|
|
|
return userNameToFileName(glyphName, existing=existing, suffix=".glif")
|
|
|
|
|
|
|
|
# -----------------------
|
|
|
|
# GLIF To and From String
|
|
|
|
# -----------------------
|
|
|
|
|
2011-10-07 13:54:14 +00:00
|
|
|
def readGlyphFromString(aString, glyphObject=None, pointPen=None, formatVersions=(1, 2)):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Read .glif data from a string into a glyph object.
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
The 'glyphObject' argument can be any kind of object (even None);
|
|
|
|
the readGlyphFromString() method will attempt to set the following
|
|
|
|
attributes on it:
|
|
|
|
"width" the advance with of the glyph
|
2011-10-03 16:24:24 +00:00
|
|
|
"height" the advance height of the glyph
|
2008-01-07 17:40:34 +00:00
|
|
|
"unicodes" a list of unicode values for this glyph
|
|
|
|
"note" a string
|
|
|
|
"lib" a dictionary containing custom data
|
2011-10-03 16:24:24 +00:00
|
|
|
"image" a dictionary containing image data
|
|
|
|
"guidelines" a list of guideline data dictionaries
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
All attributes are optional, in two ways:
|
|
|
|
1) An attribute *won't* be set if the .glif file doesn't
|
|
|
|
contain data for it. 'glyphObject' will have to deal
|
|
|
|
with default values itself.
|
|
|
|
2) If setting the attribute fails with an AttributeError
|
|
|
|
(for example if the 'glyphObject' attribute is read-
|
|
|
|
only), readGlyphFromString() will not propagate that
|
|
|
|
exception, but ignore that attribute.
|
|
|
|
|
|
|
|
To retrieve outline information, you need to pass an object
|
|
|
|
conforming to the PointPen protocol as the 'pointPen' argument.
|
|
|
|
This argument may be None if you don't need the outline data.
|
2011-10-07 13:54:14 +00:00
|
|
|
|
|
|
|
The formatVersions argument defined the GLIF format versions
|
|
|
|
that are allowed to be read.
|
2008-01-07 17:40:34 +00:00
|
|
|
"""
|
|
|
|
tree = _glifTreeFromFile(StringIO(aString))
|
|
|
|
_readGlyphFromTree(tree, glyphObject, pointPen)
|
|
|
|
|
|
|
|
|
2011-09-29 14:09:45 +00:00
|
|
|
def writeGlyphToString(glyphName, glyphObject=None, drawPointsFunc=None, writer=None, formatVersion=2):
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Return .glif data for a glyph as a UTF-8 encoded string.
|
2008-01-07 17:40:34 +00:00
|
|
|
The 'glyphObject' argument can be any kind of object (even None);
|
|
|
|
the writeGlyphToString() method will attempt to get the following
|
|
|
|
attributes from it:
|
2011-10-03 16:24:24 +00:00
|
|
|
"width" the advance width of the glyph
|
|
|
|
"height" the advance height of the glyph
|
|
|
|
"unicodes" a list of unicode values for this glyph
|
|
|
|
"note" a string
|
|
|
|
"lib" a dictionary containing custom data
|
|
|
|
"image" a dictionary containing image data
|
|
|
|
"guidelines" a list of guideline data dictionaries
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
All attributes are optional: if 'glyphObject' doesn't
|
|
|
|
have the attribute, it will simply be skipped.
|
|
|
|
|
|
|
|
To write outline data to the .glif file, writeGlyphToString() needs
|
|
|
|
a function (any callable object actually) that will take one
|
|
|
|
argument: an object that conforms to the PointPen protocol.
|
|
|
|
The function will be called by writeGlyphToString(); it has to call the
|
|
|
|
proper PointPen methods to transfer the outline to the .glif file.
|
2011-09-29 14:09:45 +00:00
|
|
|
|
|
|
|
The GLIF format version can be specified with the formatVersion argument.
|
2008-01-07 17:40:34 +00:00
|
|
|
"""
|
|
|
|
if writer is None:
|
|
|
|
from xmlWriter import XMLWriter
|
|
|
|
aFile = StringIO()
|
|
|
|
writer = XMLWriter(aFile, encoding="UTF-8")
|
|
|
|
else:
|
|
|
|
aFile = None
|
2011-09-30 00:28:24 +00:00
|
|
|
identifiers = set()
|
|
|
|
# start
|
2011-09-30 15:25:47 +00:00
|
|
|
if not isinstance(glyphName, basestring):
|
|
|
|
raise GlifLibError("The glyph name is not properly formatted.")
|
2011-10-03 16:36:29 +00:00
|
|
|
if len(glyphName) == 0:
|
|
|
|
raise GlifLibError("The glyph name is empty.")
|
2011-09-29 14:09:45 +00:00
|
|
|
writer.begintag("glyph", [("name", glyphName), ("format", formatVersion)])
|
2008-01-07 17:40:34 +00:00
|
|
|
writer.newline()
|
2011-09-30 00:28:24 +00:00
|
|
|
# advance
|
|
|
|
_writeAdvance(glyphObject, writer)
|
|
|
|
# unicodes
|
|
|
|
if getattr(glyphObject, "unicodes", None):
|
|
|
|
_writeUnicodes(glyphObject, writer)
|
|
|
|
# note
|
|
|
|
if getattr(glyphObject, "note", None):
|
|
|
|
_writeNote(glyphObject, writer)
|
|
|
|
# image
|
2011-09-30 00:39:34 +00:00
|
|
|
if formatVersion >= 2 and getattr(glyphObject, "image", None):
|
2011-09-30 00:28:24 +00:00
|
|
|
_writeImage(glyphObject, writer)
|
|
|
|
# guidelines
|
2011-09-30 00:39:34 +00:00
|
|
|
if formatVersion >= 2 and getattr(glyphObject, "guidelines", None):
|
2011-09-30 00:28:24 +00:00
|
|
|
_writeGuidelines(glyphObject, writer, identifiers)
|
|
|
|
# outline
|
|
|
|
if drawPointsFunc is not None:
|
|
|
|
writer.begintag("outline")
|
|
|
|
writer.newline()
|
|
|
|
pen = GLIFPointPen(writer, identifiers=identifiers)
|
|
|
|
drawPointsFunc(pen)
|
|
|
|
writer.endtag("outline")
|
|
|
|
writer.newline()
|
|
|
|
# lib
|
|
|
|
if getattr(glyphObject, "lib", None):
|
|
|
|
_writeLib(glyphObject, writer)
|
|
|
|
# end
|
|
|
|
writer.endtag("glyph")
|
|
|
|
writer.newline()
|
|
|
|
# return the appropriate value
|
|
|
|
if aFile is not None:
|
|
|
|
return aFile.getvalue()
|
|
|
|
else:
|
|
|
|
return None
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-30 00:28:24 +00:00
|
|
|
def _writeAdvance(glyphObject, writer):
|
2008-01-07 17:40:34 +00:00
|
|
|
width = getattr(glyphObject, "width", None)
|
|
|
|
if width is not None:
|
|
|
|
if not isinstance(width, (int, float)):
|
2011-09-29 19:38:33 +00:00
|
|
|
raise GlifLibError("width attribute must be int or float")
|
2011-10-04 01:42:57 +00:00
|
|
|
if width == 0:
|
|
|
|
width = None
|
2011-09-29 14:15:40 +00:00
|
|
|
height = getattr(glyphObject, "height", None)
|
|
|
|
if height is not None:
|
|
|
|
if not isinstance(height, (int, float)):
|
2011-09-29 19:38:33 +00:00
|
|
|
raise GlifLibError("height attribute must be int or float")
|
2011-10-04 01:42:57 +00:00
|
|
|
if height == 0:
|
|
|
|
height = None
|
2011-09-29 14:15:40 +00:00
|
|
|
if width is not None and height is not None:
|
|
|
|
writer.simpletag("advance", width=str(width), height=str(height))
|
|
|
|
writer.newline()
|
|
|
|
elif width is not None:
|
2008-01-07 17:40:34 +00:00
|
|
|
writer.simpletag("advance", width=str(width))
|
|
|
|
writer.newline()
|
2011-09-29 14:15:40 +00:00
|
|
|
elif height is not None:
|
|
|
|
writer.simpletag("advance", height=str(height))
|
|
|
|
writer.newline()
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-30 00:28:24 +00:00
|
|
|
def _writeUnicodes(glyphObject, writer):
|
2008-01-07 17:40:34 +00:00
|
|
|
unicodes = getattr(glyphObject, "unicodes", None)
|
2011-09-30 00:28:24 +00:00
|
|
|
if isinstance(unicodes, int):
|
|
|
|
unicodes = [unicodes]
|
2011-09-30 15:25:47 +00:00
|
|
|
seen = set()
|
2011-09-30 00:28:24 +00:00
|
|
|
for code in unicodes:
|
|
|
|
if not isinstance(code, int):
|
|
|
|
raise GlifLibError("unicode values must be int")
|
2011-09-30 15:25:47 +00:00
|
|
|
if code in seen:
|
|
|
|
continue
|
|
|
|
seen.add(code)
|
2011-09-30 00:28:24 +00:00
|
|
|
hexCode = hex(code)[2:].upper()
|
|
|
|
if len(hexCode) < 4:
|
|
|
|
hexCode = "0" * (4 - len(hexCode)) + hexCode
|
|
|
|
writer.simpletag("unicode", hex=hexCode)
|
|
|
|
writer.newline()
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-30 00:28:24 +00:00
|
|
|
def _writeNote(glyphObject, writer):
|
2008-01-07 17:40:34 +00:00
|
|
|
note = getattr(glyphObject, "note", None)
|
2011-09-30 00:28:24 +00:00
|
|
|
if not isinstance(note, (str, unicode)):
|
|
|
|
raise GlifLibError("note attribute must be str or unicode")
|
|
|
|
note = note.encode("utf-8")
|
|
|
|
writer.begintag("note")
|
|
|
|
writer.newline()
|
|
|
|
for line in note.splitlines():
|
|
|
|
writer.write(line.strip())
|
2008-01-07 17:40:34 +00:00
|
|
|
writer.newline()
|
2011-09-30 00:28:24 +00:00
|
|
|
writer.endtag("note")
|
|
|
|
writer.newline()
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-30 00:28:24 +00:00
|
|
|
def _writeImage(glyphObject, writer):
|
|
|
|
image = getattr(glyphObject, "image", None)
|
|
|
|
if not imageValidator(image):
|
|
|
|
raise GlifLibError("image attribute must be a dict or dict-like object with the proper structure.")
|
|
|
|
attrs = [
|
|
|
|
("fileName", image["fileName"])
|
|
|
|
]
|
2011-09-30 00:39:34 +00:00
|
|
|
for attr, default in _transformationInfo:
|
|
|
|
value = image.get(attr, default)
|
2011-09-30 19:24:10 +00:00
|
|
|
if value != default:
|
|
|
|
attrs.append((attr, str(value)))
|
2011-09-30 00:28:24 +00:00
|
|
|
color = image.get("color")
|
|
|
|
if color is not None:
|
|
|
|
attrs.append(("color", color))
|
2011-09-30 00:39:34 +00:00
|
|
|
writer.simpletag("image", attrs)
|
|
|
|
writer.newline()
|
2011-09-30 00:28:24 +00:00
|
|
|
|
|
|
|
def _writeGuidelines(glyphObject, writer, identifiers):
|
|
|
|
guidelines = getattr(glyphObject, "guidelines", [])
|
2011-09-30 00:39:34 +00:00
|
|
|
if not guidelinesValidator(guidelines):
|
2011-09-30 00:28:24 +00:00
|
|
|
raise GlifLibError("guidelines attribute does not have the proper structure.")
|
|
|
|
for guideline in guidelines:
|
|
|
|
attrs = []
|
|
|
|
x = guideline.get("x")
|
|
|
|
if x is not None:
|
|
|
|
attrs.append(("x", str(x)))
|
|
|
|
y = guideline.get("y")
|
|
|
|
if y is not None:
|
|
|
|
attrs.append(("y", str(y)))
|
|
|
|
angle = guideline.get("angle")
|
|
|
|
if angle is not None:
|
|
|
|
attrs.append(("angle", str(angle)))
|
|
|
|
name = guideline.get("name")
|
|
|
|
if name is not None:
|
|
|
|
attrs.append(("name", name))
|
2011-09-30 00:39:34 +00:00
|
|
|
color = guideline.get("color")
|
2011-09-30 00:28:24 +00:00
|
|
|
if color is not None:
|
|
|
|
attrs.append(("color", color))
|
2011-09-30 00:39:34 +00:00
|
|
|
identifier = guideline.get("identifier")
|
2011-09-30 00:28:24 +00:00
|
|
|
if identifier is not None:
|
|
|
|
if identifier in identifiers:
|
|
|
|
raise GlifLibError("identifier used more than once: %s" % identifier)
|
|
|
|
attrs.append(("identifier", identifier))
|
2011-09-30 18:55:15 +00:00
|
|
|
identifiers.add(identifier)
|
2011-09-30 00:39:34 +00:00
|
|
|
writer.simpletag("guideline", attrs)
|
|
|
|
writer.newline()
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-30 00:28:24 +00:00
|
|
|
def _writeLib(glyphObject, writer):
|
|
|
|
from ufoLib.plistlib import PlistWriter
|
2008-01-07 17:40:34 +00:00
|
|
|
lib = getattr(glyphObject, "lib", None)
|
2011-10-06 19:23:48 +00:00
|
|
|
if not isDictEnough(lib):
|
2011-09-30 00:28:24 +00:00
|
|
|
raise GlifLibError("lib attribute must be a dict.")
|
2011-10-06 19:23:48 +00:00
|
|
|
if not isinstance(lib, dict):
|
|
|
|
lib = dict(lib)
|
2011-09-30 00:28:24 +00:00
|
|
|
writer.begintag("lib")
|
|
|
|
writer.newline()
|
|
|
|
plistWriter = PlistWriter(writer.file, indentLevel=writer.indentlevel,
|
|
|
|
indent=writer.indentwhite, writeHeader=False)
|
|
|
|
plistWriter.writeValue(lib)
|
|
|
|
writer.endtag("lib")
|
2008-01-07 17:40:34 +00:00
|
|
|
writer.newline()
|
|
|
|
|
2011-09-28 13:16:25 +00:00
|
|
|
# -----------------------
|
|
|
|
# layerinfo.plist Support
|
|
|
|
# -----------------------
|
|
|
|
|
2011-09-28 15:29:10 +00:00
|
|
|
layerInfoVersion3ValueData = {
|
|
|
|
"color" : dict(type=basestring, valueValidator=colorValidator),
|
|
|
|
"lib" : dict(type=dict, valueValidator=genericTypeValidator)
|
|
|
|
}
|
|
|
|
|
|
|
|
def validateLayerInfoVersion3ValueForAttribute(attr, value):
|
|
|
|
"""
|
|
|
|
This performs very basic validation of the value for attribute
|
|
|
|
following the UFO 3 fontinfo.plist specification. The results
|
|
|
|
of this should not be interpretted as *correct* for the font
|
|
|
|
that they are part of. This merely indicates that the value
|
|
|
|
is of the proper type and, where the specification defines
|
|
|
|
a set range of possible values for an attribute, that the
|
|
|
|
value is in the accepted range.
|
|
|
|
"""
|
|
|
|
if attr not in layerInfoVersion3ValueData:
|
|
|
|
return False
|
|
|
|
dataValidationDict = layerInfoVersion3ValueData[attr]
|
|
|
|
valueType = dataValidationDict.get("type")
|
|
|
|
validator = dataValidationDict.get("valueValidator")
|
|
|
|
valueOptions = dataValidationDict.get("valueOptions")
|
|
|
|
# have specific options for the validator
|
|
|
|
if valueOptions is not None:
|
|
|
|
isValidValue = validator(value, valueOptions)
|
|
|
|
# no specific options
|
|
|
|
else:
|
|
|
|
if validator == genericTypeValidator:
|
|
|
|
isValidValue = validator(value, valueType)
|
|
|
|
else:
|
|
|
|
isValidValue = validator(value)
|
|
|
|
return isValidValue
|
|
|
|
|
|
|
|
def validateLayerInfoVersion3Data(infoData):
|
|
|
|
"""
|
|
|
|
This performs very basic validation of the value for infoData
|
|
|
|
following the UFO 3 layerinfo.plist specification. The results
|
|
|
|
of this should not be interpretted as *correct* for the font
|
|
|
|
that they are part of. This merely indicates that the values
|
|
|
|
are of the proper type and, where the specification defines
|
|
|
|
a set range of possible values for an attribute, that the
|
|
|
|
value is in the accepted range.
|
|
|
|
"""
|
|
|
|
validInfoData = {}
|
|
|
|
for attr, value in infoData.items():
|
|
|
|
if attr not in layerInfoVersion3ValueData:
|
|
|
|
raise GlifLibError("Unknown attribute %s." % attr)
|
|
|
|
isValidValue = validateLayerInfoVersion3ValueForAttribute(attr, value)
|
|
|
|
if not isValidValue:
|
|
|
|
raise GlifLibError("Invalid value for attribute %s (%s)." % (attr, repr(value)))
|
|
|
|
else:
|
|
|
|
validInfoData[attr] = value
|
|
|
|
return validInfoData
|
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# -----------------
|
|
|
|
# GLIF Tree Support
|
|
|
|
# -----------------
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
def _stripGlyphXMLTree(nodes):
|
2011-09-30 15:25:47 +00:00
|
|
|
for node in nodes:
|
|
|
|
if len(node) != 3:
|
|
|
|
raise GlifLibError("Invalid GLIF structure.")
|
|
|
|
element, attrs, children = node
|
2008-01-07 17:40:34 +00:00
|
|
|
# "lib" is formatted as a plist, so we need unstripped
|
|
|
|
# character data so we can support strings with leading or
|
|
|
|
# trailing whitespace. Do strip everything else.
|
|
|
|
recursive = (element != "lib")
|
|
|
|
stripCharacterData(children, recursive=recursive)
|
|
|
|
|
|
|
|
def _glifTreeFromFile(aFile):
|
|
|
|
tree = buildTree(aFile, stripData=False)
|
|
|
|
stripCharacterData(tree[2], recursive=False)
|
2011-09-29 01:14:14 +00:00
|
|
|
if tree[0] != "glyph":
|
|
|
|
raise GlifLibError("The GLIF is not properly formatted.")
|
2008-01-07 17:40:34 +00:00
|
|
|
_stripGlyphXMLTree(tree[2])
|
|
|
|
return tree
|
|
|
|
|
2011-10-07 13:54:14 +00:00
|
|
|
def _readGlyphFromTree(tree, glyphObject=None, pointPen=None, formatVersions=(1, 2)):
|
2011-09-28 17:21:07 +00:00
|
|
|
# quick format validation
|
|
|
|
formatError = False
|
|
|
|
if len(tree) != 3:
|
|
|
|
formatError = True
|
|
|
|
else:
|
|
|
|
if tree[0] != "glyph":
|
|
|
|
formatError = True
|
|
|
|
if formatError:
|
|
|
|
raise GlifLibError("GLIF data is not properly formatted.")
|
|
|
|
# check the format version
|
2011-09-28 20:39:28 +00:00
|
|
|
formatVersion = tree[1].get("format", "undefined")
|
2011-09-28 17:21:07 +00:00
|
|
|
try:
|
|
|
|
v = int(formatVersion)
|
|
|
|
formatVersion = v
|
|
|
|
except ValueError:
|
|
|
|
pass
|
2011-10-07 13:54:14 +00:00
|
|
|
if formatVersion not in formatVersions:
|
2011-09-28 17:21:07 +00:00
|
|
|
raise GlifLibError, "Unsupported GLIF format version: %s" % formatVersion
|
|
|
|
# get the name
|
2008-01-07 17:40:34 +00:00
|
|
|
glyphName = tree[1].get("name")
|
2011-10-03 16:36:29 +00:00
|
|
|
if len(glyphName) == 0:
|
2011-09-30 15:25:47 +00:00
|
|
|
raise GlifLibError("Empty glyph name in GLIF.")
|
2008-01-07 17:40:34 +00:00
|
|
|
if glyphName and glyphObject is not None:
|
|
|
|
_relaxedSetattr(glyphObject, "name", glyphName)
|
2011-09-28 17:21:07 +00:00
|
|
|
# populate the sub elements
|
|
|
|
unicodes = []
|
2011-09-28 20:58:42 +00:00
|
|
|
guidelines = []
|
2011-09-29 14:21:02 +00:00
|
|
|
haveSeenAdvance = haveSeenImage = haveSeenOutline = haveSeenLib = haveSeenNote = False
|
2011-09-28 18:07:13 +00:00
|
|
|
identifiers = set()
|
2008-01-07 17:40:34 +00:00
|
|
|
for element, attrs, children in tree[2]:
|
|
|
|
if element == "outline":
|
2011-09-29 14:21:02 +00:00
|
|
|
if haveSeenOutline:
|
|
|
|
raise GlifLibError("The outline element occurs more than once.")
|
2011-09-30 16:00:20 +00:00
|
|
|
if attrs:
|
|
|
|
raise GlifLibError("The outline element contains unknwon attributes.")
|
2011-09-29 14:21:02 +00:00
|
|
|
haveSeenOutline = True
|
2008-01-07 17:40:34 +00:00
|
|
|
if pointPen is not None:
|
2011-09-28 18:07:13 +00:00
|
|
|
buildOutline(pointPen, children, formatVersion, identifiers)
|
2008-01-07 17:40:34 +00:00
|
|
|
elif glyphObject is None:
|
|
|
|
continue
|
|
|
|
elif element == "advance":
|
2011-09-29 14:21:02 +00:00
|
|
|
if haveSeenAdvance:
|
|
|
|
raise GlifLibError("The advance element occurs more than once.")
|
|
|
|
haveSeenAdvance = True
|
2011-09-28 17:30:41 +00:00
|
|
|
width = _number(attrs.get("width", 0))
|
2008-01-07 17:40:34 +00:00
|
|
|
_relaxedSetattr(glyphObject, "width", width)
|
2011-09-28 17:30:41 +00:00
|
|
|
height = _number(attrs.get("height", 0))
|
|
|
|
_relaxedSetattr(glyphObject, "height", height)
|
2008-01-07 17:40:34 +00:00
|
|
|
elif element == "unicode":
|
2011-09-28 20:39:28 +00:00
|
|
|
try:
|
|
|
|
v = attrs.get("hex", "undefined")
|
|
|
|
v = int(v, 16)
|
2011-09-30 15:25:47 +00:00
|
|
|
if v not in unicodes:
|
|
|
|
unicodes.append(v)
|
2011-09-28 20:39:28 +00:00
|
|
|
except ValueError:
|
|
|
|
raise GlifLibError("Illegal value for hex attribute of unicode element.")
|
2011-09-28 20:58:42 +00:00
|
|
|
elif element == "guideline":
|
|
|
|
if formatVersion == 1:
|
|
|
|
raise GlifLibError("The guideline element is not allowed in GLIF format 1.")
|
|
|
|
if len(children):
|
|
|
|
raise GlifLibError("Unknown children in guideline element.")
|
2011-09-30 00:39:34 +00:00
|
|
|
for attr in ("x", "y", "angle"):
|
|
|
|
if attr in attrs:
|
|
|
|
attrs[attr] = _number(attrs[attr])
|
2011-09-28 20:58:42 +00:00
|
|
|
guidelines.append(attrs)
|
2011-09-29 00:29:57 +00:00
|
|
|
elif element == "image":
|
|
|
|
if formatVersion == 1:
|
|
|
|
raise GlifLibError("The image element is not allowed in GLIF format 1.")
|
|
|
|
if haveSeenImage:
|
|
|
|
raise GlifLibError("The image element occurs more than once.")
|
|
|
|
if len(children):
|
|
|
|
raise GlifLibError("Unknown children in image element.")
|
|
|
|
haveSeenImage = True
|
|
|
|
imageData = attrs
|
2011-10-11 17:31:27 +00:00
|
|
|
for attr, default in _transformationInfo:
|
|
|
|
value = default
|
2011-09-30 00:39:34 +00:00
|
|
|
if attr in imageData:
|
2011-10-11 17:31:27 +00:00
|
|
|
value = imageData[attr]
|
|
|
|
imageData[attr] = _number(value)
|
2011-09-29 00:29:57 +00:00
|
|
|
if not imageValidator(imageData):
|
|
|
|
raise GlifLibError("The image element is not properly formatted.")
|
2011-09-30 00:39:34 +00:00
|
|
|
_relaxedSetattr(glyphObject, "image", imageData)
|
2008-01-07 17:40:34 +00:00
|
|
|
elif element == "note":
|
2011-09-29 14:21:02 +00:00
|
|
|
if haveSeenNote:
|
|
|
|
raise GlifLibError("The note element occurs more than once.")
|
|
|
|
haveSeenNote = True
|
2008-01-07 17:40:34 +00:00
|
|
|
rawNote = "\n".join(children)
|
|
|
|
lines = rawNote.split("\n")
|
|
|
|
lines = [line.strip() for line in lines]
|
|
|
|
note = "\n".join(lines)
|
|
|
|
_relaxedSetattr(glyphObject, "note", note)
|
|
|
|
elif element == "lib":
|
2011-09-29 14:21:02 +00:00
|
|
|
if haveSeenLib:
|
|
|
|
raise GlifLibError("The lib element occurs more than once.")
|
|
|
|
haveSeenLib = True
|
2008-01-07 17:40:34 +00:00
|
|
|
from plistFromTree import readPlistFromTree
|
|
|
|
assert len(children) == 1
|
|
|
|
lib = readPlistFromTree(children[0])
|
2011-10-03 15:56:08 +00:00
|
|
|
valid, message = libValidator(lib)
|
|
|
|
if not valid:
|
|
|
|
raise GlifLibError(message)
|
2008-01-07 17:40:34 +00:00
|
|
|
_relaxedSetattr(glyphObject, "lib", lib)
|
2011-09-28 17:21:07 +00:00
|
|
|
else:
|
|
|
|
raise GlifLibError("Unknown element in GLIF: %s" % element)
|
|
|
|
# set the collected unicodes
|
2008-01-07 17:40:34 +00:00
|
|
|
if unicodes:
|
|
|
|
_relaxedSetattr(glyphObject, "unicodes", unicodes)
|
2011-09-28 20:58:42 +00:00
|
|
|
# set the collected guidelines
|
|
|
|
if formatVersion > 1 and guidelines:
|
|
|
|
if not guidelinesValidator(guidelines, identifiers):
|
2011-09-29 00:29:57 +00:00
|
|
|
raise GlifLibError("The guidelines are improperly formatted.")
|
2011-09-28 20:58:42 +00:00
|
|
|
_relaxedSetattr(glyphObject, "guidelines", guidelines)
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# ----------------
|
|
|
|
# GLIF to PointPen
|
|
|
|
# ----------------
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 19:04:22 +00:00
|
|
|
componentAttributes = set(["base", "xScale", "xyScale", "yxScale", "yScale", "xOffset", "yOffset", "identifier"])
|
|
|
|
contourAttributes = set(["identifier"])
|
|
|
|
pointAttributes = set(["x", "y", "type", "smooth", "name", "identifier"])
|
|
|
|
pointSmoothOptions = set(("no", "yes"))
|
|
|
|
pointTypeOptions = set(["move", "line", "offcurve", "curve", "qcurve"])
|
2011-09-28 18:07:13 +00:00
|
|
|
|
|
|
|
def buildOutline(pen, xmlNodes, formatVersion, identifiers):
|
2011-09-30 16:00:20 +00:00
|
|
|
for node in xmlNodes:
|
|
|
|
if len(node) != 3:
|
|
|
|
raise GlifLibError("The outline element is not properly structured.")
|
|
|
|
element, attrs, children = node
|
2008-01-07 17:40:34 +00:00
|
|
|
if element == "contour":
|
2011-09-28 20:27:55 +00:00
|
|
|
_buildOutlineContour(pen, (attrs, children), formatVersion, identifiers)
|
2008-01-07 17:40:34 +00:00
|
|
|
elif element == "component":
|
2011-09-28 20:27:55 +00:00
|
|
|
_buildOutlineComponent(pen, (attrs, children), formatVersion, identifiers)
|
2011-09-30 16:00:20 +00:00
|
|
|
else:
|
|
|
|
raise GlifLibError("Unknown element in outline element: %s" % element)
|
2011-09-28 20:27:55 +00:00
|
|
|
|
|
|
|
def _buildOutlineContour(pen, (attrs, children), formatVersion, identifiers):
|
|
|
|
# search for unknown attributes
|
|
|
|
if set(attrs.keys()) - contourAttributes:
|
|
|
|
raise GlifLibError("Unknown attributes in contour element.")
|
|
|
|
# identifier is not required but it is not part of format 1
|
|
|
|
identifier = attrs.get("identifier")
|
|
|
|
if identifier is not None and formatVersion == 1:
|
|
|
|
raise GlifLibError("The contour identifier attribute is not allowed in GLIF format 1.")
|
|
|
|
if identifier is not None:
|
|
|
|
if identifier in identifiers:
|
|
|
|
raise GlifLibError("The identifier %s is used more than once." % identifier)
|
|
|
|
if not identifierValidator(identifier):
|
|
|
|
raise GlifLibError("The contour identifier %s is not valid." % identifier)
|
|
|
|
identifiers.add(identifier)
|
|
|
|
# try to pass the identifier attribute
|
2011-09-30 16:00:20 +00:00
|
|
|
try:
|
2011-09-30 16:58:04 +00:00
|
|
|
pen.beginPath(identifier=identifier)
|
2011-09-30 16:00:20 +00:00
|
|
|
except TypeError:
|
2011-09-28 20:27:55 +00:00
|
|
|
pen.beginPath()
|
2011-09-30 16:00:20 +00:00
|
|
|
raise DeprecationWarning("The beginPath method needs an identifier kwarg. The contour's identifier value has been discarded.")
|
2011-09-28 20:27:55 +00:00
|
|
|
# points
|
|
|
|
if children:
|
2011-09-30 16:58:04 +00:00
|
|
|
# make sure that only points are here
|
|
|
|
for e, d, c in children:
|
|
|
|
if e != "point":
|
|
|
|
raise GlifLibError("The contour element contains an unknown element: %s" % e)
|
2011-09-28 20:27:55 +00:00
|
|
|
# loop through the points very quickly to make sure that the number of off-curves is correct
|
|
|
|
_validateSegmentStructures(children)
|
|
|
|
# interpret the points
|
|
|
|
_buildOutlinePoints(pen, children, formatVersion, identifiers)
|
|
|
|
# done
|
|
|
|
pen.endPath()
|
|
|
|
|
|
|
|
def _buildOutlineComponent(pen, (attrs, children), formatVersion, identifiers):
|
|
|
|
# unknown child element of contour
|
|
|
|
if len(children):
|
|
|
|
raise GlifLibError("Unknown child elements of component element." % subElement)
|
|
|
|
# search for unknown attributes
|
|
|
|
if set(attrs.keys()) - componentAttributes:
|
|
|
|
raise GlifLibError("Unknown attributes in component element.")
|
|
|
|
# base is required
|
|
|
|
baseGlyphName = attrs.get("base")
|
|
|
|
if baseGlyphName is None:
|
|
|
|
raise GlifLibError("The base attribute is not defined in the component.")
|
|
|
|
# transformation is not required
|
|
|
|
transformation = []
|
|
|
|
for attr, default in _transformationInfo:
|
|
|
|
value = attrs.get(attr)
|
|
|
|
if value is None:
|
|
|
|
value = default
|
|
|
|
else:
|
|
|
|
value = _number(value)
|
|
|
|
transformation.append(value)
|
|
|
|
# identifier is not required but it is not part of format 1
|
|
|
|
identifier = attrs.get("identifier")
|
|
|
|
if identifier is not None and formatVersion == 1:
|
|
|
|
raise GlifLibError("The component identifier attribute is not allowed in GLIF format 1.")
|
|
|
|
if identifier is not None:
|
|
|
|
if identifier in identifiers:
|
|
|
|
raise GlifLibError("The identifier %s is used more than once." % identifier)
|
|
|
|
if not identifierValidator(identifier):
|
|
|
|
raise GlifLibError("The identifier %s is not valid." % identifier)
|
|
|
|
identifiers.add(identifier)
|
|
|
|
# try to pass the identifier attribute
|
|
|
|
try:
|
|
|
|
pen.addComponent(baseGlyphName, tuple(transformation), identifier=identifier)
|
|
|
|
except TypeError:
|
|
|
|
pen.addComponent(baseGlyphName, tuple(transformation))
|
|
|
|
raise DeprecationWarning("The addComponent method needs an identifier kwarg. The component's identifier value has been discarded.")
|
2011-09-28 19:04:22 +00:00
|
|
|
|
2011-09-28 20:27:55 +00:00
|
|
|
def _validateSegmentStructures(children):
|
|
|
|
pointTypes = [a.get("type", "offcurve") for s, a, d in children]
|
2011-10-03 16:36:29 +00:00
|
|
|
if set(pointTypes) != set(["offcurve"]):
|
2011-09-28 20:17:23 +00:00
|
|
|
while pointTypes[-1] == "offcurve":
|
|
|
|
pointTypes.insert(0, pointTypes.pop(-1))
|
|
|
|
segments = []
|
|
|
|
for pointType in reversed(pointTypes):
|
|
|
|
if pointType != "offcurve":
|
|
|
|
segments.append([pointType])
|
|
|
|
else:
|
|
|
|
segments[-1].append(pointType)
|
|
|
|
for segment in segments:
|
|
|
|
if len(segment) == 1:
|
|
|
|
continue
|
|
|
|
segmentType = segment[0]
|
|
|
|
offCurves = segment[1:]
|
|
|
|
# move and line can't be preceded by off-curves
|
|
|
|
if segmentType == "move":
|
|
|
|
raise GlifLibError("move can not have an offcurve.")
|
|
|
|
elif segmentType == "line":
|
2011-09-30 18:28:15 +00:00
|
|
|
raise GlifLibError("line can not have an offcurve.")
|
2011-09-28 20:17:23 +00:00
|
|
|
elif segmentType == "curve":
|
|
|
|
if len(offCurves) > 2:
|
|
|
|
raise GlifLibError("Too many offcurves defined for curve.")
|
|
|
|
elif segmentType == "qcurve":
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
# unknown segement type. it'll be caught later.
|
|
|
|
pass
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 20:27:55 +00:00
|
|
|
def _buildOutlinePoints(pen, children, formatVersion, identifiers):
|
|
|
|
for index, (subElement, attrs, dummy) in enumerate(children):
|
|
|
|
# unknown child element of contour
|
|
|
|
if subElement != "point":
|
|
|
|
raise GlifLibError("Unknown child element (%s) of contour element." % subElement)
|
|
|
|
# search for unknown attributes
|
|
|
|
if set(attrs.keys()) - pointAttributes:
|
|
|
|
raise GlifLibError("Unknown attributes in point element.")
|
|
|
|
# search for unknown children
|
|
|
|
if len(dummy):
|
|
|
|
raise GlifLibError("Unknown child elements in point element.")
|
|
|
|
# x and y are required
|
2011-09-28 20:39:28 +00:00
|
|
|
x = attrs.get("x", "undefined")
|
|
|
|
y = attrs.get("y", "undefined")
|
2011-09-28 20:27:55 +00:00
|
|
|
if x is None:
|
|
|
|
raise GlifLibError("Required x attribute is missing in point element.")
|
|
|
|
if y is None:
|
|
|
|
raise GlifLibError("Required y attribute is missing in point element.")
|
|
|
|
x = _number(x)
|
|
|
|
y = _number(y)
|
|
|
|
# type is not required
|
|
|
|
segmentType = attrs.get("type", "offcurve")
|
|
|
|
if segmentType not in pointTypeOptions:
|
|
|
|
raise GlifLibError("Unknown point type: %s" % segmentType)
|
|
|
|
if segmentType == "offcurve":
|
|
|
|
segmentType = None
|
|
|
|
# move can only occur as the first point
|
|
|
|
if segmentType == "move" and index != 0:
|
|
|
|
raise GlifLibError("A move point occurs after the first point in the contour.")
|
|
|
|
# smooth is not required
|
|
|
|
smooth = attrs.get("smooth", "no")
|
|
|
|
if smooth is not None:
|
|
|
|
if smooth not in pointSmoothOptions:
|
|
|
|
raise GlifLibError("Unknown point smooth value: %s" % smooth)
|
|
|
|
smooth = smooth == "yes"
|
2011-09-30 18:28:15 +00:00
|
|
|
if smooth and segmentType not in ("curve", "qcurve"):
|
2011-09-30 16:58:04 +00:00
|
|
|
raise GlifLibError("smooth attribute set in a %s point." % segmentType)
|
2011-09-28 20:27:55 +00:00
|
|
|
# name is not required
|
|
|
|
name = attrs.get("name")
|
|
|
|
# identifier is not required but it is not part of format 1
|
|
|
|
identifier = attrs.get("identifier")
|
|
|
|
if identifier is not None and formatVersion == 1:
|
|
|
|
raise GlifLibError("The point identifier attribute is not allowed in GLIF format 1.")
|
|
|
|
if identifier is not None:
|
|
|
|
if identifier in identifiers:
|
|
|
|
raise GlifLibError("The identifier %s is used more than once." % identifier)
|
|
|
|
if not identifierValidator(identifier):
|
|
|
|
raise GlifLibError("The identifier %s is not valid." % identifier)
|
|
|
|
identifiers.add(identifier)
|
|
|
|
# try to pass the identifier attribute
|
|
|
|
try:
|
|
|
|
pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name, identifier=identifier)
|
|
|
|
except TypeError:
|
|
|
|
pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name)
|
|
|
|
raise DeprecationWarning("The addPoint method needs an identifier kwarg. The point's identifier value has been discarded.")
|
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# ---------------------
|
|
|
|
# Misc Helper Functions
|
|
|
|
# ---------------------
|
|
|
|
|
|
|
|
def _relaxedSetattr(object, attr, value):
|
|
|
|
try:
|
|
|
|
setattr(object, attr, value)
|
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _number(s):
|
|
|
|
"""
|
|
|
|
Given a numeric string, return an integer or a float, whichever
|
|
|
|
the string indicates. _number("1") will return the integer 1,
|
|
|
|
_number("1.0") will return the float 1.0.
|
2011-09-28 17:27:25 +00:00
|
|
|
|
|
|
|
>>> _number("1")
|
|
|
|
1
|
|
|
|
>>> _number("1.0")
|
|
|
|
1.0
|
|
|
|
>>> _number("a")
|
|
|
|
Traceback (most recent call last):
|
|
|
|
...
|
|
|
|
GlifLibError: Could not convert a to an int or float.
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
try:
|
|
|
|
n = int(s)
|
2011-09-28 17:27:25 +00:00
|
|
|
return n
|
2011-09-28 13:13:07 +00:00
|
|
|
except ValueError:
|
2011-09-28 17:27:25 +00:00
|
|
|
pass
|
|
|
|
try:
|
2011-09-28 13:13:07 +00:00
|
|
|
n = float(s)
|
2011-09-28 17:27:25 +00:00
|
|
|
return n
|
|
|
|
except ValueError:
|
|
|
|
raise GlifLibError("Could not convert %s to an int or float." % s)
|
2011-09-28 13:13:07 +00:00
|
|
|
|
2011-10-11 14:08:24 +00:00
|
|
|
# --------------------
|
|
|
|
# Rapid Value Fetching
|
|
|
|
# --------------------
|
2011-09-28 13:13:07 +00:00
|
|
|
|
2011-10-11 14:08:24 +00:00
|
|
|
# base
|
2011-09-28 13:13:07 +00:00
|
|
|
|
2011-10-11 14:08:24 +00:00
|
|
|
class _DoneParsing(Exception): pass
|
2011-09-28 13:13:07 +00:00
|
|
|
|
2011-10-11 14:08:24 +00:00
|
|
|
class _BaseParser(object):
|
|
|
|
|
|
|
|
def __init__(self):
|
2011-09-28 13:13:07 +00:00
|
|
|
self._elementStack = []
|
2011-10-11 14:08:24 +00:00
|
|
|
|
|
|
|
def parse(self, text):
|
|
|
|
from xml.parsers.expat import ParserCreate
|
2011-09-28 13:13:07 +00:00
|
|
|
parser = ParserCreate()
|
|
|
|
parser.returns_unicode = 0 # XXX, Don't remember why. It sucks, though.
|
|
|
|
parser.StartElementHandler = self.startElementHandler
|
|
|
|
parser.EndElementHandler = self.endElementHandler
|
|
|
|
parser.Parse(text)
|
|
|
|
|
|
|
|
def startElementHandler(self, name, attrs):
|
|
|
|
self._elementStack.append(name)
|
|
|
|
|
|
|
|
def endElementHandler(self, name):
|
|
|
|
other = self._elementStack.pop(-1)
|
|
|
|
assert other == name
|
|
|
|
|
|
|
|
|
2011-10-11 14:08:24 +00:00
|
|
|
# unicodes
|
|
|
|
|
|
|
|
def _fetchUnicodes(glif):
|
|
|
|
"""
|
|
|
|
Get a list of unicodes listed in glif.
|
|
|
|
"""
|
|
|
|
parser = _FetchUnicodesParser()
|
|
|
|
parser.parse(glif)
|
|
|
|
return parser.unicodes
|
|
|
|
|
|
|
|
class _FetchUnicodesParser(_BaseParser):
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.unicodes = []
|
|
|
|
super(_FetchUnicodesParser, self).__init__()
|
|
|
|
|
|
|
|
def startElementHandler(self, name, attrs):
|
|
|
|
if name == "unicode" and self._elementStack and self._elementStack[-1] == "glyph":
|
|
|
|
value = attrs.get("hex")
|
|
|
|
if value is not None:
|
|
|
|
try:
|
|
|
|
value = int(value, 16)
|
|
|
|
if value not in self.unicodes:
|
|
|
|
self.unicodes.append(value)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
super(_FetchUnicodesParser, self).startElementHandler(name, attrs)
|
|
|
|
|
|
|
|
# image
|
|
|
|
|
|
|
|
def _fetchImageFileName(glif):
|
|
|
|
"""
|
|
|
|
The image file name (if any) from glif.
|
|
|
|
"""
|
|
|
|
parser = _FetchImageFileNameParser()
|
|
|
|
try:
|
|
|
|
parser.parse(glif)
|
|
|
|
except _DoneParsing:
|
|
|
|
pass
|
|
|
|
return parser.fileName
|
|
|
|
|
|
|
|
class _FetchImageFileNameParser(_BaseParser):
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.fileName = None
|
|
|
|
super(_FetchImageFileNameParser, self).__init__()
|
|
|
|
|
|
|
|
def startElementHandler(self, name, attrs):
|
|
|
|
if name == "image" and self._elementStack and self._elementStack[-1] == "glyph":
|
|
|
|
self.fileName = attrs.get("fileName")
|
|
|
|
raise _DoneParsing
|
|
|
|
super(_FetchImageFileNameParser, self).startElementHandler(name, attrs)
|
|
|
|
|
|
|
|
# component references
|
|
|
|
|
|
|
|
def _fetchComponentBases(glif):
|
|
|
|
"""
|
|
|
|
Get a list of component base glyphs listed in glif.
|
|
|
|
"""
|
|
|
|
parser = _FetchComponentBasesParser()
|
|
|
|
try:
|
|
|
|
parser.parse(glif)
|
|
|
|
except _DoneParsing:
|
|
|
|
pass
|
|
|
|
return list(parser.bases)
|
|
|
|
|
|
|
|
class _FetchComponentBasesParser(_BaseParser):
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.bases = []
|
|
|
|
super(_FetchComponentBasesParser, self).__init__()
|
|
|
|
|
|
|
|
def startElementHandler(self, name, attrs):
|
|
|
|
if name == "component" and self._elementStack and self._elementStack[-1] == "outline":
|
|
|
|
base = attrs.get("base")
|
|
|
|
if base is not None:
|
|
|
|
self.bases.append(base)
|
|
|
|
super(_FetchComponentBasesParser, self).startElementHandler(name, attrs)
|
|
|
|
|
|
|
|
def endElementHandler(self, name):
|
|
|
|
if name == "outline":
|
|
|
|
raise _DoneParsing
|
|
|
|
super(_FetchComponentBasesParser, self).endElementHandler(name)
|
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
# --------------
|
|
|
|
# GLIF Point Pen
|
|
|
|
# --------------
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
_transformationInfo = [
|
|
|
|
# field name, default value
|
|
|
|
("xScale", 1),
|
|
|
|
("xyScale", 0),
|
|
|
|
("yxScale", 0),
|
|
|
|
("yScale", 1),
|
|
|
|
("xOffset", 0),
|
|
|
|
("yOffset", 0),
|
|
|
|
]
|
|
|
|
|
|
|
|
class GLIFPointPen(AbstractPointPen):
|
|
|
|
|
2011-09-28 13:13:07 +00:00
|
|
|
"""
|
|
|
|
Helper class using the PointPen protocol to write the <outline>
|
2008-01-07 17:40:34 +00:00
|
|
|
part of .glif files.
|
|
|
|
"""
|
|
|
|
|
2011-09-29 20:55:20 +00:00
|
|
|
def __init__(self, xmlWriter, formatVersion=2, identifiers=None):
|
|
|
|
if identifiers is None:
|
|
|
|
identifiers = set()
|
|
|
|
self.formatVersion = formatVersion
|
|
|
|
self.identifiers = identifiers
|
2008-01-07 17:40:34 +00:00
|
|
|
self.writer = xmlWriter
|
2011-09-30 17:04:00 +00:00
|
|
|
self.prevSegmentType = None
|
2011-09-30 18:28:15 +00:00
|
|
|
self.prevOffCurveCount = 0
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 19:04:22 +00:00
|
|
|
def beginPath(self, identifier=None, **kwargs):
|
|
|
|
attrs = []
|
2011-09-30 00:28:24 +00:00
|
|
|
if identifier is not None and self.formatVersion >= 2:
|
|
|
|
if identifier in self.identifiers:
|
2011-09-29 20:55:20 +00:00
|
|
|
raise GlifLibError("identifier used more than once: %s" % identifier)
|
2011-09-29 20:58:26 +00:00
|
|
|
if not identifierValidator(identifier):
|
|
|
|
raise GlifLibError("identifier not formatted properly: %s" % identifier)
|
2011-09-28 19:04:22 +00:00
|
|
|
attrs.append(("identifier", identifier))
|
2011-09-30 18:55:15 +00:00
|
|
|
self.identifiers.add(identifier)
|
2011-09-28 19:04:22 +00:00
|
|
|
self.writer.begintag("contour", attrs)
|
2008-01-07 17:40:34 +00:00
|
|
|
self.writer.newline()
|
2011-09-30 17:04:00 +00:00
|
|
|
self.prevSegmentType = None
|
2011-09-30 18:28:15 +00:00
|
|
|
self.prevOffCurveCount = 0
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
def endPath(self):
|
|
|
|
self.writer.endtag("contour")
|
|
|
|
self.writer.newline()
|
2011-09-30 17:04:00 +00:00
|
|
|
self.prevSegmentType = None
|
2011-09-30 18:28:15 +00:00
|
|
|
self.prevOffCurveCount = 0
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 19:04:22 +00:00
|
|
|
def addPoint(self, pt, segmentType=None, smooth=None, name=None, identifier=None, **kwargs):
|
2008-01-07 17:40:34 +00:00
|
|
|
attrs = []
|
2011-09-30 17:04:00 +00:00
|
|
|
# coordinates
|
2008-01-07 17:40:34 +00:00
|
|
|
if pt is not None:
|
|
|
|
for coord in pt:
|
|
|
|
if not isinstance(coord, (int, float)):
|
2011-09-29 20:55:20 +00:00
|
|
|
raise GlifLibError("coordinates must be int or float")
|
2008-01-07 17:40:34 +00:00
|
|
|
attrs.append(("x", str(pt[0])))
|
|
|
|
attrs.append(("y", str(pt[1])))
|
2011-09-30 17:04:00 +00:00
|
|
|
# segment type
|
|
|
|
if segmentType == "move" and self.prevSegmentType is not None:
|
|
|
|
raise GlifLibError("move occurs after a point has already been added to the contour.")
|
2011-09-30 18:28:15 +00:00
|
|
|
if segmentType in ("move", "line") and self.prevSegmentType == "offcurve":
|
|
|
|
raise GlifLibError("offcurve occurs before %s point." % segmentType)
|
|
|
|
if segmentType == "curve" and self.prevOffCurveCount > 2:
|
|
|
|
raise GlifLibError("too many offcurve points before curve point.")
|
2008-01-07 17:40:34 +00:00
|
|
|
if segmentType is not None:
|
|
|
|
attrs.append(("type", segmentType))
|
2011-09-30 18:28:15 +00:00
|
|
|
else:
|
|
|
|
segmentType = "offcurve"
|
|
|
|
if segmentType == "offcurve":
|
|
|
|
self.prevOffCurveCount += 1
|
|
|
|
else:
|
|
|
|
self.prevOffCurveCount = 0
|
2011-09-30 17:04:00 +00:00
|
|
|
self.prevSegmentType = segmentType
|
|
|
|
# smooth
|
2008-01-07 17:40:34 +00:00
|
|
|
if smooth:
|
2011-09-30 18:28:15 +00:00
|
|
|
if segmentType not in ("curve", "qcurve"):
|
2011-09-30 16:58:04 +00:00
|
|
|
raise GlifLibError("can't set smooth in a %s point." % segmentType)
|
2008-01-07 17:40:34 +00:00
|
|
|
attrs.append(("smooth", "yes"))
|
2011-09-30 17:04:00 +00:00
|
|
|
# name
|
2008-01-07 17:40:34 +00:00
|
|
|
if name is not None:
|
|
|
|
attrs.append(("name", name))
|
2011-09-30 17:04:00 +00:00
|
|
|
# identifier
|
2011-09-30 00:28:24 +00:00
|
|
|
if identifier is not None and self.formatVersion >= 2:
|
|
|
|
if identifier in self.identifiers:
|
2011-09-29 20:55:20 +00:00
|
|
|
raise GlifLibError("identifier used more than once: %s" % identifier)
|
2011-09-29 20:58:26 +00:00
|
|
|
if not identifierValidator(identifier):
|
|
|
|
raise GlifLibError("identifier not formatted properly: %s" % identifier)
|
2011-09-28 19:04:22 +00:00
|
|
|
attrs.append(("identifier", identifier))
|
2011-09-30 18:55:15 +00:00
|
|
|
self.identifiers.add(identifier)
|
2008-01-07 17:40:34 +00:00
|
|
|
self.writer.simpletag("point", attrs)
|
|
|
|
self.writer.newline()
|
|
|
|
|
2011-09-28 19:04:22 +00:00
|
|
|
def addComponent(self, glyphName, transformation, identifier=None, **kwargs):
|
2008-01-07 17:40:34 +00:00
|
|
|
attrs = [("base", glyphName)]
|
|
|
|
for (attr, default), value in zip(_transformationInfo, transformation):
|
|
|
|
if not isinstance(value, (int, float)):
|
2011-09-29 20:55:20 +00:00
|
|
|
raise GlifLibError("transformation values must be int or float")
|
2008-01-07 17:40:34 +00:00
|
|
|
if value != default:
|
|
|
|
attrs.append((attr, str(value)))
|
2011-09-30 00:28:24 +00:00
|
|
|
if identifier is not None and self.formatVersion >= 2:
|
|
|
|
if identifier in self.identifiers:
|
2011-09-29 20:55:20 +00:00
|
|
|
raise GlifLibError("identifier used more than once: %s" % identifier)
|
2011-09-29 20:58:26 +00:00
|
|
|
if not identifierValidator(identifier):
|
|
|
|
raise GlifLibError("identifier not formatted properly: %s" % identifier)
|
2011-09-28 19:04:22 +00:00
|
|
|
attrs.append(("identifier", identifier))
|
2011-09-30 18:55:15 +00:00
|
|
|
self.identifiers.add(identifier)
|
2008-01-07 17:40:34 +00:00
|
|
|
self.writer.simpletag("component", attrs)
|
|
|
|
self.writer.newline()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2011-09-28 17:27:25 +00:00
|
|
|
import doctest
|
|
|
|
doctest.testmod()
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
from pprint import pprint
|
|
|
|
from robofab.pens.pointPen import PrintingPointPen
|
2011-09-28 20:58:42 +00:00
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
class TestGlyph: pass
|
2011-09-28 20:58:42 +00:00
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
gs = GlyphSet(".")
|
2011-09-28 20:58:42 +00:00
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def drawPoints(pen):
|
2011-09-28 19:04:22 +00:00
|
|
|
pen.beginPath(identifier="my contour")
|
|
|
|
pen.addPoint((100, 200), name="foo", identifier="my point")
|
2008-01-07 17:40:34 +00:00
|
|
|
pen.addPoint((200, 250), segmentType="curve", smooth=True)
|
|
|
|
pen.endPath()
|
2011-09-28 19:04:22 +00:00
|
|
|
pen.addComponent("a", (1, 0, 0, 1, 20, 30), identifier="my component")
|
2011-09-28 20:58:42 +00:00
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
glyph = TestGlyph()
|
|
|
|
glyph.width = 120
|
|
|
|
glyph.unicodes = [1, 2, 3, 43215, 66666]
|
|
|
|
glyph.lib = {"a": "b", "c": [1, 2, 3, True]}
|
|
|
|
glyph.note = " hallo! "
|
2011-09-30 00:39:34 +00:00
|
|
|
glyph.image = dict(fileName="foo.png", color="1,1,0,.1")
|
|
|
|
glyph.guidelines = [dict(x=100, y=200, angle=144.44)]
|
2011-09-28 20:58:42 +00:00
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
if 0:
|
|
|
|
gs.writeGlyph("a", glyph, drawPoints)
|
|
|
|
g2 = TestGlyph()
|
|
|
|
gs.readGlyph("a", g2, PrintingPointPen())
|
|
|
|
pprint(g2.__dict__)
|
|
|
|
else:
|
|
|
|
s = writeGlyphToString("a", glyph, drawPoints)
|
|
|
|
print s
|
|
|
|
g2 = TestGlyph()
|
|
|
|
readGlyphFromString(s, g2, PrintingPointPen())
|
|
|
|
pprint(g2.__dict__)
|
2009-03-27 20:30:18 +00:00
|
|
|
|