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-09-28 15:29:10 +00:00
|
|
|
from validators import genericTypeValidator, colorValidator, guidelinesValidator
|
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-28 17:21:07 +00:00
|
|
|
# ----------
|
|
|
|
# Connstants
|
|
|
|
# ----------
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 15:29:10 +00:00
|
|
|
LAYERINFO_FILENAME = "layerinfo.plist"
|
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-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
|
|
|
|
self.contents = self._findContents()
|
|
|
|
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
|
|
|
"""
|
|
|
|
Rebuild the contents dict by checking what glyphs are available
|
2008-01-07 17:40:34 +00:00
|
|
|
on disk.
|
|
|
|
"""
|
|
|
|
self.contents = self._findContents(forceRebuild=True)
|
|
|
|
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:
|
|
|
|
"width" the advance with of the glyph
|
|
|
|
"unicodes" a list of unicode values for this glyph
|
|
|
|
"note" a string
|
|
|
|
"lib" a dictionary containing custom data
|
|
|
|
|
|
|
|
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))
|
2008-01-07 17:40:34 +00:00
|
|
|
_readGlyphFromTree(tree, glyphObject, pointPen)
|
|
|
|
|
|
|
|
def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=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:
|
|
|
|
"width" the advance with of the glyph
|
|
|
|
"unicodes" a list of unicode values for this glyph
|
|
|
|
"note" a string
|
|
|
|
"lib" a dictionary containing custom data
|
|
|
|
|
|
|
|
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-06-18 17:08:25 +00:00
|
|
|
self._purgeCachedGLIF(glyphName)
|
2008-01-07 17:40:34 +00:00
|
|
|
data = writeGlyphToString(glyphName, glyphObject, drawPointsFunc)
|
|
|
|
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
|
|
|
|
files partially, so is a lot faster than parsing all files completely.
|
|
|
|
"""
|
|
|
|
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
|
|
|
|
|
|
|
|
# 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
|
|
|
def _findContents(self, forceRebuild=False):
|
|
|
|
contentsPath = os.path.join(self.dirName, "contents.plist")
|
|
|
|
if forceRebuild or not os.path.exists(contentsPath):
|
|
|
|
fileNames = os.listdir(self.dirName)
|
|
|
|
fileNames = [n for n in fileNames if n.endswith(".glif")]
|
|
|
|
contents = {}
|
|
|
|
for n in fileNames:
|
|
|
|
glyphPath = os.path.join(self.dirName, n)
|
|
|
|
contents[_fetchGlyphName(glyphPath)] = n
|
|
|
|
else:
|
|
|
|
contents = readPlist(contentsPath)
|
|
|
|
return contents
|
|
|
|
|
|
|
|
|
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
|
|
|
|
# -----------------------
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def readGlyphFromString(aString, glyphObject=None, pointPen=None):
|
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
|
|
|
|
"unicodes" a list of unicode values for this glyph
|
|
|
|
"note" a string
|
|
|
|
"lib" a dictionary containing custom data
|
|
|
|
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
tree = _glifTreeFromFile(StringIO(aString))
|
|
|
|
_readGlyphFromTree(tree, glyphObject, pointPen)
|
|
|
|
|
|
|
|
|
|
|
|
def writeGlyphToString(glyphName, glyphObject=None, drawPointsFunc=None, writer=None):
|
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:
|
|
|
|
"width" the advance with of the glyph
|
|
|
|
"unicodes" a list of unicode values for this glyph
|
|
|
|
"note" a string
|
|
|
|
"lib" a dictionary containing custom data
|
|
|
|
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
if writer is None:
|
|
|
|
from xmlWriter import XMLWriter
|
|
|
|
aFile = StringIO()
|
|
|
|
writer = XMLWriter(aFile, encoding="UTF-8")
|
|
|
|
else:
|
|
|
|
aFile = None
|
|
|
|
writer.begintag("glyph", [("name", glyphName), ("format", "1")])
|
|
|
|
writer.newline()
|
|
|
|
|
|
|
|
width = getattr(glyphObject, "width", None)
|
|
|
|
if width is not None:
|
|
|
|
if not isinstance(width, (int, float)):
|
|
|
|
raise GlifLibError, "width attribute must be int or float"
|
|
|
|
writer.simpletag("advance", width=str(width))
|
|
|
|
writer.newline()
|
|
|
|
|
|
|
|
unicodes = getattr(glyphObject, "unicodes", None)
|
|
|
|
if unicodes:
|
|
|
|
if isinstance(unicodes, int):
|
|
|
|
unicodes = [unicodes]
|
|
|
|
for code in unicodes:
|
|
|
|
if not isinstance(code, int):
|
|
|
|
raise GlifLibError, "unicode values must be int"
|
|
|
|
hexCode = hex(code)[2:].upper()
|
|
|
|
if len(hexCode) < 4:
|
|
|
|
hexCode = "0" * (4 - len(hexCode)) + hexCode
|
|
|
|
writer.simpletag("unicode", hex=hexCode)
|
|
|
|
writer.newline()
|
|
|
|
|
|
|
|
note = getattr(glyphObject, "note", None)
|
|
|
|
if note is not None:
|
|
|
|
if not isinstance(note, (str, unicode)):
|
|
|
|
raise GlifLibError, "note attribute must be str or unicode"
|
2008-02-02 12:37:51 +00:00
|
|
|
note = note.encode('utf-8')
|
2008-01-07 17:40:34 +00:00
|
|
|
writer.begintag("note")
|
|
|
|
writer.newline()
|
|
|
|
for line in note.splitlines():
|
|
|
|
writer.write(line.strip())
|
|
|
|
writer.newline()
|
|
|
|
writer.endtag("note")
|
|
|
|
writer.newline()
|
|
|
|
|
|
|
|
if drawPointsFunc is not None:
|
|
|
|
writer.begintag("outline")
|
|
|
|
writer.newline()
|
|
|
|
pen = GLIFPointPen(writer)
|
|
|
|
drawPointsFunc(pen)
|
|
|
|
writer.endtag("outline")
|
|
|
|
writer.newline()
|
|
|
|
|
|
|
|
lib = getattr(glyphObject, "lib", None)
|
|
|
|
if lib:
|
2011-09-28 00:46:51 +00:00
|
|
|
from ufoLib.plistlib import PlistWriter
|
2008-01-07 17:40:34 +00:00
|
|
|
if not isinstance(lib, dict):
|
|
|
|
lib = dict(lib)
|
|
|
|
writer.begintag("lib")
|
|
|
|
writer.newline()
|
|
|
|
plistWriter = PlistWriter(writer.file, indentLevel=writer.indentlevel,
|
|
|
|
indent=writer.indentwhite, writeHeader=False)
|
|
|
|
plistWriter.writeValue(lib)
|
|
|
|
writer.endtag("lib")
|
|
|
|
writer.newline()
|
|
|
|
|
|
|
|
writer.endtag("glyph")
|
|
|
|
writer.newline()
|
|
|
|
if aFile is not None:
|
|
|
|
return aFile.getvalue()
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
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),
|
|
|
|
"guidelines" : dict(type=list, valueValidator=guidelinesValidator),
|
|
|
|
"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):
|
|
|
|
for element, attrs, children in nodes:
|
|
|
|
# "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)
|
|
|
|
assert tree[0] == "glyph"
|
|
|
|
_stripGlyphXMLTree(tree[2])
|
|
|
|
return tree
|
|
|
|
|
|
|
|
def _readGlyphFromTree(tree, glyphObject=None, pointPen=None):
|
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
|
|
|
|
formatVersion = tree[1].get("format", None)
|
|
|
|
try:
|
|
|
|
v = int(formatVersion)
|
|
|
|
formatVersion = v
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
if formatVersion not in supportedGLIFFormatVersions:
|
|
|
|
raise GlifLibError, "Unsupported GLIF format version: %s" % formatVersion
|
|
|
|
# get the name
|
2008-01-07 17:40:34 +00:00
|
|
|
glyphName = tree[1].get("name")
|
|
|
|
if glyphName and glyphObject is not None:
|
|
|
|
_relaxedSetattr(glyphObject, "name", glyphName)
|
2011-09-28 17:21:07 +00:00
|
|
|
# populate the sub elements
|
|
|
|
unicodes = []
|
2008-01-07 17:40:34 +00:00
|
|
|
for element, attrs, children in tree[2]:
|
|
|
|
if element == "outline":
|
|
|
|
if pointPen is not None:
|
2011-09-28 17:21:07 +00:00
|
|
|
buildOutline(pointPen, children, formatVersion)
|
2008-01-07 17:40:34 +00:00
|
|
|
elif glyphObject is None:
|
|
|
|
continue
|
|
|
|
elif element == "advance":
|
|
|
|
width = _number(attrs["width"])
|
|
|
|
_relaxedSetattr(glyphObject, "width", width)
|
|
|
|
elif element == "unicode":
|
|
|
|
unicodes.append(int(attrs["hex"], 16))
|
|
|
|
elif element == "note":
|
|
|
|
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":
|
|
|
|
from plistFromTree import readPlistFromTree
|
|
|
|
assert len(children) == 1
|
|
|
|
lib = readPlistFromTree(children[0])
|
|
|
|
_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 13:13:07 +00:00
|
|
|
# ----------------
|
|
|
|
# GLIF to PointPen
|
|
|
|
# ----------------
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-28 17:21:07 +00:00
|
|
|
def buildOutline(pen, xmlNodes, formatVersion):
|
2008-01-07 17:40:34 +00:00
|
|
|
for element, attrs, children in xmlNodes:
|
|
|
|
if element == "contour":
|
|
|
|
pen.beginPath()
|
|
|
|
for subElement, attrs, dummy in children:
|
|
|
|
if subElement != "point":
|
|
|
|
continue
|
|
|
|
x = _number(attrs["x"])
|
|
|
|
y = _number(attrs["y"])
|
|
|
|
segmentType = attrs.get("type", "offcurve")
|
|
|
|
if segmentType == "offcurve":
|
|
|
|
segmentType = None
|
|
|
|
smooth = attrs.get("smooth") == "yes"
|
|
|
|
name = attrs.get("name")
|
|
|
|
pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name)
|
|
|
|
pen.endPath()
|
|
|
|
elif element == "component":
|
|
|
|
baseGlyphName = attrs["base"]
|
|
|
|
transformation = []
|
|
|
|
for attr, default in _transformationInfo:
|
|
|
|
value = attrs.get(attr)
|
|
|
|
if value is None:
|
|
|
|
value = default
|
|
|
|
else:
|
|
|
|
value = _number(value)
|
|
|
|
transformation.append(value)
|
|
|
|
pen.addComponent(baseGlyphName, tuple(transformation))
|
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
n = int(s)
|
|
|
|
except ValueError:
|
|
|
|
n = float(s)
|
|
|
|
return n
|
|
|
|
|
|
|
|
# -------------------
|
|
|
|
# Glyph Name Fetching
|
|
|
|
# -------------------
|
|
|
|
|
|
|
|
class _DoneParsing(Exception): pass
|
|
|
|
|
|
|
|
def _startElementHandler(tagName, attrs):
|
|
|
|
if tagName != "glyph":
|
|
|
|
# the top level element of any .glif file must be <glyph>
|
|
|
|
raise _DoneParsing(None)
|
|
|
|
glyphName = attrs["name"]
|
|
|
|
raise _DoneParsing(glyphName)
|
|
|
|
|
|
|
|
def _fetchGlyphName(glyphPath):
|
|
|
|
# Given a path to an existing .glif file, get the glyph name
|
|
|
|
# from the XML data.
|
|
|
|
from xml.parsers.expat import ParserCreate
|
|
|
|
|
|
|
|
p = ParserCreate()
|
|
|
|
p.StartElementHandler = _startElementHandler
|
|
|
|
p.returns_unicode = True
|
|
|
|
f = open(glyphPath)
|
|
|
|
try:
|
|
|
|
p.ParseFile(f)
|
|
|
|
except _DoneParsing, why:
|
|
|
|
glyphName = why.args[0]
|
|
|
|
if glyphName is None:
|
|
|
|
raise ValueError, (".glif file doen't have a <glyph> top-level "
|
|
|
|
"element: %r" % glyphPath)
|
|
|
|
else:
|
|
|
|
assert 0, "it's not expected that parsing the file ends normally"
|
|
|
|
return glyphName
|
|
|
|
|
|
|
|
# ----------------
|
|
|
|
# Unicode Fetching
|
|
|
|
# ----------------
|
|
|
|
|
|
|
|
def _fetchUnicodes(text):
|
|
|
|
# Given GLIF text, get a list of all unicode values from the XML data.
|
|
|
|
parser = _FetchUnicodesParser(text)
|
|
|
|
return parser.unicodes
|
|
|
|
|
|
|
|
class _FetchUnicodesParser(object):
|
|
|
|
|
|
|
|
def __init__(self, text):
|
|
|
|
from xml.parsers.expat import ParserCreate
|
|
|
|
self.unicodes = []
|
|
|
|
self._elementStack = []
|
|
|
|
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):
|
|
|
|
if name == "unicode" and len(self._elementStack) == 1 and self._elementStack[0] == "glyph":
|
|
|
|
value = attrs.get("hex")
|
|
|
|
value = int(value, 16)
|
|
|
|
self.unicodes.append(value)
|
|
|
|
self._elementStack.append(name)
|
|
|
|
|
|
|
|
def endElementHandler(self, name):
|
|
|
|
other = self._elementStack.pop(-1)
|
|
|
|
assert other == name
|
|
|
|
|
|
|
|
|
|
|
|
# --------------
|
|
|
|
# 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.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, xmlWriter):
|
|
|
|
self.writer = xmlWriter
|
|
|
|
|
|
|
|
def beginPath(self):
|
|
|
|
self.writer.begintag("contour")
|
|
|
|
self.writer.newline()
|
|
|
|
|
|
|
|
def endPath(self):
|
|
|
|
self.writer.endtag("contour")
|
|
|
|
self.writer.newline()
|
|
|
|
|
|
|
|
def addPoint(self, pt, segmentType=None, smooth=None, name=None, **kwargs):
|
|
|
|
attrs = []
|
|
|
|
if pt is not None:
|
|
|
|
for coord in pt:
|
|
|
|
if not isinstance(coord, (int, float)):
|
|
|
|
raise GlifLibError, "coordinates must be int or float"
|
|
|
|
attrs.append(("x", str(pt[0])))
|
|
|
|
attrs.append(("y", str(pt[1])))
|
|
|
|
if segmentType is not None:
|
|
|
|
attrs.append(("type", segmentType))
|
|
|
|
if smooth:
|
|
|
|
attrs.append(("smooth", "yes"))
|
|
|
|
if name is not None:
|
|
|
|
attrs.append(("name", name))
|
|
|
|
self.writer.simpletag("point", attrs)
|
|
|
|
self.writer.newline()
|
|
|
|
|
|
|
|
def addComponent(self, glyphName, transformation):
|
|
|
|
attrs = [("base", glyphName)]
|
|
|
|
for (attr, default), value in zip(_transformationInfo, transformation):
|
|
|
|
if not isinstance(value, (int, float)):
|
|
|
|
raise GlifLibError, "transformation values must be int or float"
|
|
|
|
if value != default:
|
|
|
|
attrs.append((attr, str(value)))
|
|
|
|
self.writer.simpletag("component", attrs)
|
|
|
|
self.writer.newline()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
from pprint import pprint
|
|
|
|
from robofab.pens.pointPen import PrintingPointPen
|
|
|
|
class TestGlyph: pass
|
|
|
|
gs = GlyphSet(".")
|
|
|
|
def drawPoints(pen):
|
|
|
|
pen.beginPath()
|
|
|
|
pen.addPoint((100, 200), name="foo")
|
|
|
|
pen.addPoint((200, 250), segmentType="curve", smooth=True)
|
|
|
|
pen.endPath()
|
|
|
|
pen.addComponent("a", (1, 0, 0, 1, 20, 30))
|
|
|
|
glyph = TestGlyph()
|
|
|
|
glyph.width = 120
|
|
|
|
glyph.unicodes = [1, 2, 3, 43215, 66666]
|
|
|
|
glyph.lib = {"a": "b", "c": [1, 2, 3, True]}
|
|
|
|
glyph.note = " hallo! "
|
|
|
|
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
|
|
|
|