Initial work on reading single file UFO.
This is a work in progress update of UFOReader and UFOWriter that supports UFO in its package and zipped forms. Reading works. Writing is not yet implemented. I'm building a base file system (that lives on top of fs for now and maybe in the long term) that the reader and writer then subclass. This base class implements the file system interaction so that the reader and writer can be blissfully ignorant about file systems. Additionally, I ran into a problem with the local plistlib.py creating an import error, so I've temporarily renamed it plistlibShim.py so that I can continue working. Did I mention that this is a work in progress? It's a work in progress.
This commit is contained in:
parent
185c04220d
commit
45910b6131
@ -3,11 +3,14 @@ import shutil
|
||||
from io import StringIO, BytesIO, open
|
||||
import codecs
|
||||
from copy import deepcopy
|
||||
from ufoLib.filesystem import FileSystem
|
||||
from ufoLib.glifLib import GlyphSet
|
||||
from ufoLib.validators import *
|
||||
from ufoLib.filenames import userNameToFileName
|
||||
from ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning
|
||||
from ufoLib.plistlib import readPlist, writePlist
|
||||
from ufoLib.plistlibShim import readPlist, writePlist
|
||||
from ufoLib.errors import UFOLibError
|
||||
|
||||
"""
|
||||
A library for importing .ufo files and their descendants.
|
||||
Refer to http://unifiedfontobject.com for the UFO specification.
|
||||
@ -61,7 +64,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
class UFOLibError(Exception): pass
|
||||
|
||||
|
||||
|
||||
# ----------
|
||||
@ -118,14 +121,12 @@ def _getPlist(self, fileName, default=None):
|
||||
# UFO Reader
|
||||
# ----------
|
||||
|
||||
class UFOReader(object):
|
||||
class UFOReader(FileSystem):
|
||||
|
||||
"""Read the various components of the .ufo."""
|
||||
|
||||
def __init__(self, path):
|
||||
if not os.path.exists(path):
|
||||
raise UFOLibError("The specified UFO doesn't exist.")
|
||||
self._path = path
|
||||
super(UFOReader, self).__init__(path)
|
||||
self.readMetaInfo()
|
||||
self._upConvertedKerningData = None
|
||||
|
||||
@ -182,73 +183,13 @@ class UFOReader(object):
|
||||
self._upConvertedKerningData["groups"] = groups
|
||||
self._upConvertedKerningData["groupRenameMaps"] = conversionMaps
|
||||
|
||||
# support methods
|
||||
|
||||
_checkForFile = staticmethod(os.path.exists)
|
||||
|
||||
_getPlist = _getPlist
|
||||
|
||||
def readBytesFromPath(self, path, encoding=None):
|
||||
"""
|
||||
Returns the bytes in the file at the given path.
|
||||
The path must be relative to the UFO path.
|
||||
Returns None if the file does not exist.
|
||||
An encoding may be passed if needed.
|
||||
"""
|
||||
fullPath = os.path.join(self._path, path)
|
||||
if not self._checkForFile(fullPath):
|
||||
return None
|
||||
if os.path.isdir(fullPath):
|
||||
raise UFOLibError("%s is a directory." % path)
|
||||
if encoding:
|
||||
f = open(fullPath, encoding=encoding)
|
||||
else:
|
||||
f = open(fullPath, "rb", encoding=encoding)
|
||||
data = f.read()
|
||||
f.close()
|
||||
return data
|
||||
|
||||
def getReadFileForPath(self, path, encoding=None):
|
||||
"""
|
||||
Returns a file (or file-like) object for the
|
||||
file at the given path. The path must be relative
|
||||
to the UFO path. Returns None if the file does not exist.
|
||||
An encoding may be passed if needed.
|
||||
|
||||
Note: The caller is responsible for closing the open file.
|
||||
"""
|
||||
fullPath = os.path.join(self._path, path)
|
||||
if not self._checkForFile(fullPath):
|
||||
return None
|
||||
if os.path.isdir(fullPath):
|
||||
raise UFOLibError("%s is a directory." % path)
|
||||
if encoding:
|
||||
f = open(fullPath, "rb", encoding=encoding)
|
||||
else:
|
||||
f = open(fullPath, "r")
|
||||
return f
|
||||
|
||||
def getFileModificationTime(self, path):
|
||||
"""
|
||||
Returns the modification time (as reported by os.path.getmtime)
|
||||
for the file at the given path. The path must be relative to
|
||||
the UFO path. Returns None if the file does not exist.
|
||||
"""
|
||||
fullPath = os.path.join(self._path, path)
|
||||
if not self._checkForFile(fullPath):
|
||||
return None
|
||||
return os.path.getmtime(fullPath)
|
||||
|
||||
# metainfo.plist
|
||||
|
||||
def readMetaInfo(self):
|
||||
"""
|
||||
Read metainfo.plist. Only used for internal operations.
|
||||
"""
|
||||
# should there be a blind try/except with a UFOLibError
|
||||
# raised in except here (and elsewhere)? It would be nice to
|
||||
# provide external callers with a single exception to catch.
|
||||
data = self._getPlist(METAINFO_FILENAME)
|
||||
data = self.readPlist(METAINFO_FILENAME)
|
||||
if not isinstance(data, dict):
|
||||
raise UFOLibError("metainfo.plist is not properly formatted.")
|
||||
formatVersion = data["formatVersion"]
|
||||
@ -259,7 +200,7 @@ class UFOReader(object):
|
||||
# groups.plist
|
||||
|
||||
def _readGroups(self):
|
||||
return self._getPlist(GROUPS_FILENAME, {})
|
||||
return self.readPlist(GROUPS_FILENAME, {})
|
||||
|
||||
def readGroups(self):
|
||||
"""
|
||||
@ -301,7 +242,7 @@ class UFOReader(object):
|
||||
# fontinfo.plist
|
||||
|
||||
def _readInfo(self):
|
||||
data = self._getPlist(FONTINFO_FILENAME, {})
|
||||
data = self.readPlist(FONTINFO_FILENAME, {})
|
||||
if not isinstance(data, dict):
|
||||
raise UFOLibError("fontinfo.plist is not properly formatted.")
|
||||
return data
|
||||
@ -353,7 +294,7 @@ class UFOReader(object):
|
||||
# kerning.plist
|
||||
|
||||
def _readKerning(self):
|
||||
data = self._getPlist(KERNING_FILENAME, {})
|
||||
data = self.readPlist(KERNING_FILENAME, {})
|
||||
return data
|
||||
|
||||
def readKerning(self):
|
||||
@ -384,7 +325,7 @@ class UFOReader(object):
|
||||
"""
|
||||
Read lib.plist. Returns a dict.
|
||||
"""
|
||||
data = self._getPlist(LIB_FILENAME, {})
|
||||
data = self.readPlist(LIB_FILENAME, {})
|
||||
valid, message = fontLibValidator(data)
|
||||
if not valid:
|
||||
raise UFOLibError(message)
|
||||
@ -396,10 +337,9 @@ class UFOReader(object):
|
||||
"""
|
||||
Read features.fea. Returns a string.
|
||||
"""
|
||||
path = os.path.join(self._path, FEATURES_FILENAME)
|
||||
if not self._checkForFile(path):
|
||||
if not self.exists(FEATURES_FILENAME):
|
||||
return ""
|
||||
with open(path, "r") as f:
|
||||
with self.open(FEATURES_FILENAME, "r") as f:
|
||||
text = f.read()
|
||||
return text
|
||||
|
||||
@ -413,7 +353,7 @@ class UFOReader(object):
|
||||
if self._formatVersion < 3:
|
||||
return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)]
|
||||
# read the file on disk
|
||||
contents = self._getPlist(LAYERCONTENTS_FILENAME)
|
||||
contents = self.readPlist(LAYERCONTENTS_FILENAME)
|
||||
valid, error = layerContentsValidator(contents, self._path)
|
||||
if not valid:
|
||||
raise UFOLibError(error)
|
||||
@ -456,8 +396,7 @@ class UFOReader(object):
|
||||
break
|
||||
if directory is None:
|
||||
raise UFOLibError("No glyphs directory is mapped to \"%s\"." % layerName)
|
||||
glyphsPath = os.path.join(self._path, directory)
|
||||
return GlyphSet(glyphsPath, ufoFormatVersion=self._formatVersion)
|
||||
return GlyphSet(directory, fileSystem=self, ufoFormatVersion=self._formatVersion)
|
||||
|
||||
def getCharacterMapping(self, layerName=None):
|
||||
"""
|
||||
@ -477,36 +416,18 @@ class UFOReader(object):
|
||||
|
||||
# /data
|
||||
|
||||
def getDataDirectoryListing(self, maxDepth=100):
|
||||
def getDataDirectoryListing(self):
|
||||
"""
|
||||
Returns a list of all files in the data directory.
|
||||
The returned paths will be relative to the UFO.
|
||||
This will not list directory names, only file names.
|
||||
Thus, empty directories will be skipped.
|
||||
|
||||
The maxDepth argument sets the maximum number
|
||||
of sub-directories that are allowed.
|
||||
"""
|
||||
path = os.path.join(self._path, DATA_DIRNAME)
|
||||
if not self._checkForFile(path):
|
||||
if not self.exists(DATA_DIRNAME):
|
||||
return []
|
||||
listing = self._getDirectoryListing(path, maxDepth=maxDepth)
|
||||
listing = [os.path.relpath(path, "data") for path in listing]
|
||||
listing = self.listDirectory(path, recurse=True)
|
||||
return listing
|
||||
|
||||
def _getDirectoryListing(self, path, depth=0, maxDepth=100):
|
||||
if depth > maxDepth:
|
||||
raise UFOLibError("Maximum recusion depth reached.")
|
||||
result = []
|
||||
for fileName in os.listdir(path):
|
||||
p = os.path.join(path, fileName)
|
||||
if os.path.isdir(p):
|
||||
result += self._getDirectoryListing(p, depth=depth+1, maxDepth=maxDepth)
|
||||
else:
|
||||
p = os.path.relpath(p, self._path)
|
||||
result.append(p)
|
||||
return result
|
||||
|
||||
def getImageDirectoryListing(self):
|
||||
"""
|
||||
Returns a list of all image file names in
|
||||
@ -515,15 +436,13 @@ class UFOReader(object):
|
||||
"""
|
||||
if self._formatVersion < 3:
|
||||
return []
|
||||
path = os.path.join(self._path, IMAGES_DIRNAME)
|
||||
if not os.path.exists(path):
|
||||
if not self.exists(IMAGES_DIRNAME):
|
||||
return []
|
||||
if not os.path.isdir(path):
|
||||
if not self.isDirectory(IMAGES_DIRNAME):
|
||||
raise UFOLibError("The UFO contains an \"images\" file instead of a directory.")
|
||||
result = []
|
||||
for fileName in os.listdir(path):
|
||||
p = os.path.join(path, fileName)
|
||||
if os.path.isdir(p):
|
||||
for fileName in self.listDirectory(path):
|
||||
if self.isDirectory(fileName):
|
||||
# silently skip this as version control
|
||||
# systems often have hidden directories
|
||||
continue
|
||||
@ -538,7 +457,8 @@ class UFOReader(object):
|
||||
"""
|
||||
if self._formatVersion < 3:
|
||||
raise UFOLibError("Reading images is not allowed in UFO %d." % self._formatVersion)
|
||||
data = self.readBytesFromPath(os.path.join(IMAGES_DIRNAME, fileName))
|
||||
path = self.joinPath(IMAGES_DIRNAME, fileName)
|
||||
data = self.readBytesFromPath(path)
|
||||
if data is None:
|
||||
raise UFOLibError("No image file named %s." % fileName)
|
||||
valid, error = pngValidator(data=data)
|
||||
|
1
Lib/ufoLib/errors.py
Normal file
1
Lib/ufoLib/errors.py
Normal file
@ -0,0 +1 @@
|
||||
class UFOLibError(Exception): pass
|
282
Lib/ufoLib/filesystem.py
Normal file
282
Lib/ufoLib/filesystem.py
Normal file
@ -0,0 +1,282 @@
|
||||
import os
|
||||
from io import StringIO
|
||||
from fs.osfs import OSFS
|
||||
from fs.zipfs import ZipFS, ZipOpenError
|
||||
from ufoLib.plistlibShim import readPlist, writePlist
|
||||
from ufoLib.errors import UFOLibError
|
||||
|
||||
try:
|
||||
basestring
|
||||
except NameError:
|
||||
basestring = str
|
||||
|
||||
|
||||
class FileSystem(object):
|
||||
|
||||
def __init__(self, path):
|
||||
self._path = "<data stream>"
|
||||
if isinstance(path, basestring):
|
||||
self._path = path
|
||||
if not os.path.exists(path):
|
||||
raise UFOLibError("The specified UFO doesn't exist.")
|
||||
if os.path.isdir(path):
|
||||
path = OSFS(path)
|
||||
else:
|
||||
try:
|
||||
path = ZipFS(path, mode="r", allow_zip_64=True)
|
||||
except ZipOpenError:
|
||||
raise UFOLibError("The specified UFO is not in a proper format.")
|
||||
self._fs = path
|
||||
|
||||
def close(self):
|
||||
self._fs.close()
|
||||
|
||||
# -----------------
|
||||
# Path Manipulation
|
||||
# -----------------
|
||||
|
||||
def joinPath(self, *parts):
|
||||
return os.path.join(*parts)
|
||||
|
||||
def splitPath(self, path):
|
||||
return os.path.split(path)
|
||||
|
||||
def directoryName(self, path):
|
||||
return self.splitPath(path)[0]
|
||||
|
||||
def relativePath(self, path, start):
|
||||
return os.path.relpath(path, start)
|
||||
|
||||
# ---------
|
||||
# Existence
|
||||
# ---------
|
||||
|
||||
def exists(self, path):
|
||||
return self._fs.exists(path)
|
||||
|
||||
def isDirectory(self, path):
|
||||
return self._fs.isdir(path)
|
||||
|
||||
def listDirectory(self, path, recurse=False):
|
||||
return self._listDirectory(path, recurse=recurse)
|
||||
|
||||
def _listDirectory(self, path, recurse=False, depth=0, maxDepth=100):
|
||||
if depth > maxDepth:
|
||||
raise UFOLibError("Maximum recusion depth reached.")
|
||||
result = []
|
||||
for fileName in self._fs.listdir(path):
|
||||
p = self.joinPath(path, fileName)
|
||||
if os.path.isdir(p) and recurse:
|
||||
result += self._listDirectory(p, recurse=True, depth=depth+1, maxDepth=maxDepth)
|
||||
else:
|
||||
result.append(p)
|
||||
return result
|
||||
|
||||
# -----------
|
||||
# File Opener
|
||||
# -----------
|
||||
|
||||
def open(self, path, mode="r", encoding=None):
|
||||
"""
|
||||
Returns a file (or file-like) object for the
|
||||
file at the given path. The path must be relative
|
||||
to the UFO path. Returns None if the file does
|
||||
not exist and the mode is "r" or "rb. An encoding
|
||||
may be passed if needed.
|
||||
|
||||
Note: The caller is responsible for closing the open file.
|
||||
"""
|
||||
if encoding:
|
||||
if mode == "r":
|
||||
mode = "rb"
|
||||
elif mode == "w":
|
||||
mode = "wb"
|
||||
if mode in ("r", "rb") and not self.exists(path):
|
||||
return None
|
||||
if self.exists(path) and self.isDirectory(path):
|
||||
raise UFOLibError("%s is a directory." % path)
|
||||
if mode in ("w", "wb"):
|
||||
self._buildDirectoryTree(path)
|
||||
f = self._fs.open(path, mode, encoding=encoding)
|
||||
return f
|
||||
|
||||
def _buildDirectoryTree(self, path):
|
||||
directory, fileName = self.splitPath(path)
|
||||
directoryTree = []
|
||||
while directory:
|
||||
directory, d = self.splitPath(directory)
|
||||
directoryTree.append(d)
|
||||
directoryTree.reverse()
|
||||
built = ""
|
||||
for d in directoryTree:
|
||||
d = self.joinPath(built, d)
|
||||
p = self.joinPath(self._path, d)
|
||||
if not self.exists(p):
|
||||
self._fs.mkdir(p)
|
||||
built = d
|
||||
|
||||
# ------------------
|
||||
# Modification Times
|
||||
# ------------------
|
||||
|
||||
def getFileModificationTime(self, path):
|
||||
"""
|
||||
Returns the modification time (as reported by os.path.getmtime)
|
||||
for the file at the given path. The path must be relative to
|
||||
the UFO path. Returns None if the file does not exist.
|
||||
"""
|
||||
if not self.exists(path):
|
||||
return None
|
||||
info = self._fs.getinfo(path)
|
||||
return info["modified_time"]
|
||||
|
||||
# --------------
|
||||
# Raw Read/Write
|
||||
# --------------
|
||||
|
||||
def readBytesFromPath(self, path, encoding=None):
|
||||
"""
|
||||
Returns the bytes in the file at the given path.
|
||||
The path must be relative to the UFO path.
|
||||
Returns None if the file does not exist.
|
||||
An encoding may be passed if needed.
|
||||
"""
|
||||
f = self.open(path, mode="r", encoding=encoding)
|
||||
if f is None:
|
||||
return None
|
||||
data = f.read()
|
||||
f.close()
|
||||
return data
|
||||
|
||||
def writeBytesToPath(self, path, data, encoding=None):
|
||||
"""
|
||||
Write bytes to path. If needed, the directory tree
|
||||
for the given path will be built. The path must be
|
||||
relative to the UFO. An encoding may be passed if needed.
|
||||
"""
|
||||
if encoding:
|
||||
data = StringIO(data).encode(encoding)
|
||||
self._writeFileAtomically(data, fullPath)
|
||||
|
||||
def _writeFileAtomically(self, data, path):
|
||||
"""
|
||||
Write data into a file at path. Do this sort of atomically
|
||||
making it harder to cause corrupt files. This also checks to see
|
||||
if data matches the data that is already in the file at path.
|
||||
If so, the file is not rewritten so that the modification date
|
||||
is preserved.
|
||||
"""
|
||||
assert isinstance(data, bytes)
|
||||
if self.exists(path):
|
||||
f = self.open(path, "rb")
|
||||
oldData = f.read()
|
||||
f.close()
|
||||
if data == oldData:
|
||||
return
|
||||
if data:
|
||||
f = self.open(path, "wb")
|
||||
f.write(data)
|
||||
f.close()
|
||||
|
||||
# ------------
|
||||
# File Removal
|
||||
# ------------
|
||||
|
||||
def remove(self, path):
|
||||
"""
|
||||
Remove the file (or directory) at path. The path
|
||||
must be relative to the UFO. This is only allowed
|
||||
for files in the data and image directories.
|
||||
"""
|
||||
d = path
|
||||
parts = []
|
||||
while d:
|
||||
d, p = self.splitPath(d)
|
||||
if p:
|
||||
parts.append(p)
|
||||
if parts[-1] not in ("images", "data"):
|
||||
raise UFOLibError("Removing \"%s\" is not legal." % path)
|
||||
self._removeFileForPath(path, raiseErrorIfMissing=True)
|
||||
|
||||
def _removeFileForPath(self, path, raiseErrorIfMissing=False):
|
||||
if not self.exists(path):
|
||||
if raiseErrorIfMissing:
|
||||
raise UFOLibError("The file %s does not exist." % path)
|
||||
else:
|
||||
if self.isDirectory(path):
|
||||
self._fs.removedir(path)
|
||||
else:
|
||||
self._fs.remove(path)
|
||||
directory = self.directoryName(path)
|
||||
self._removeEmptyDirectoriesForPath(directory)
|
||||
|
||||
def _removeEmptyDirectoriesForPath(self, directory):
|
||||
if not self.exists(directory):
|
||||
return
|
||||
if not len(self._fs.listdir(directory)):
|
||||
self._fs.removedir(directory)
|
||||
else:
|
||||
return
|
||||
directory = self.directoryName(directory)
|
||||
if directory:
|
||||
self._removeEmptyDirectoriesForPath(directory)
|
||||
|
||||
# --------------
|
||||
# Property Lists
|
||||
# --------------
|
||||
|
||||
def readPlist(self, path, default=None):
|
||||
"""
|
||||
Read a property list relative to the
|
||||
UFO path. If the file is missing and
|
||||
default is None a UFOLibError will be
|
||||
raised. Otherwise default is returned.
|
||||
The errors that could be raised during
|
||||
the reading of a plist are unpredictable
|
||||
and/or too large to list, so, a blind
|
||||
try: except: is done. If an exception
|
||||
occurs, a UFOLibError will be raised.
|
||||
"""
|
||||
if not self.exists(path):
|
||||
if default is not None:
|
||||
return default
|
||||
else:
|
||||
raise UFOLibError("%s is missing. This file is required" % path)
|
||||
try:
|
||||
with self.open(path, "rb") as f:
|
||||
return readPlist(f)
|
||||
except:
|
||||
raise UFOLibError("The file %s could not be read." % fileName)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from ufoLib import UFOReader
|
||||
path = os.path.dirname(__file__)
|
||||
path = os.path.dirname(path)
|
||||
path = os.path.dirname(path)
|
||||
path = os.path.join(path, "TestData", "TestFont1 (UFO2).ufo")
|
||||
|
||||
import zipfile
|
||||
|
||||
def _packDirectory(z, d, root=""):
|
||||
for i in os.listdir(d):
|
||||
p = os.path.join(d, i)
|
||||
l = os.path.join(root, i)
|
||||
if os.path.isdir(p):
|
||||
_packDirectory(z, p, root=l)
|
||||
else:
|
||||
l = os.path.join(root, i)
|
||||
z.write(p, l)
|
||||
|
||||
p = path + ".zip"
|
||||
if os.path.exists(p):
|
||||
os.remove(p)
|
||||
z = zipfile.ZipFile(p, "w")
|
||||
_packDirectory(z, path)
|
||||
z.close()
|
||||
|
||||
path += ".zip"
|
||||
|
||||
reader = UFOReader(path)
|
||||
glyphSet = reader.getGlyphSet()
|
||||
print glyphSet.getGLIF("A")
|
@ -15,8 +15,9 @@ import os
|
||||
from io import BytesIO, StringIO, open
|
||||
from warnings import warn
|
||||
from fontTools.misc.py23 import tobytes, tostr
|
||||
from ufoLib.filesystem import FileSystem
|
||||
from ufoLib.xmlTreeBuilder import buildTree, stripCharacterData
|
||||
from ufoLib.plistlib import PlistWriter, readPlist, writePlist
|
||||
from ufoLib.plistlibShim import PlistWriter, readPlist, writePlist
|
||||
from ufoLib.pointPen import AbstractPointPen, PointToSegmentPen
|
||||
from ufoLib.filenames import userNameToFileName
|
||||
from ufoLib.validators import isDictEnough, genericTypeValidator, colorValidator,\
|
||||
@ -96,7 +97,7 @@ class GlyphSet(object):
|
||||
|
||||
glyphClass = Glyph
|
||||
|
||||
def __init__(self, dirName, glyphNameToFileNameFunc=None, ufoFormatVersion=3):
|
||||
def __init__(self, dirName, fileSystem=None, glyphNameToFileNameFunc=None, ufoFormatVersion=3):
|
||||
"""
|
||||
'dirName' should be a path to an existing directory.
|
||||
|
||||
@ -109,6 +110,10 @@ class GlyphSet(object):
|
||||
self.dirName = dirName
|
||||
if ufoFormatVersion not in supportedUFOFormatVersions:
|
||||
raise GlifLibError("Unsupported UFO format version: %s" % ufoFormatVersion)
|
||||
if fileSystem is None:
|
||||
fileSystem = FileSystem(dirName)
|
||||
dirName = ""
|
||||
self._fs = fileSystem
|
||||
self.ufoFormatVersion = ufoFormatVersion
|
||||
if glyphNameToFileNameFunc is None:
|
||||
glyphNameToFileNameFunc = glyphNameToFileName
|
||||
@ -121,12 +126,12 @@ class GlyphSet(object):
|
||||
"""
|
||||
Rebuild the contents dict by loading contents.plist.
|
||||
"""
|
||||
contentsPath = os.path.join(self.dirName, "contents.plist")
|
||||
if not os.path.exists(contentsPath):
|
||||
contentsPath = self._fs.joinPath(self.dirName, "contents.plist")
|
||||
if not self._fs.exists(contentsPath):
|
||||
# missing, consider the glyphset empty.
|
||||
contents = {}
|
||||
else:
|
||||
contents = self._readPlist(contentsPath)
|
||||
contents = self._fs.readPlist(contentsPath)
|
||||
# validate the contents
|
||||
invalidFormat = False
|
||||
if not isinstance(contents, dict):
|
||||
@ -137,7 +142,7 @@ class GlyphSet(object):
|
||||
invalidFormat = True
|
||||
if not isinstance(fileName, basestring):
|
||||
invalidFormat = True
|
||||
elif not os.path.exists(os.path.join(self.dirName, fileName)):
|
||||
elif not self._fs.exists(self._fs.joinPath(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")
|
||||
@ -160,22 +165,22 @@ class GlyphSet(object):
|
||||
self._reverseContents = d
|
||||
return self._reverseContents
|
||||
|
||||
def writeContents(self):
|
||||
"""
|
||||
Write the contents.plist file out to disk. Call this method when
|
||||
you're done writing glyphs.
|
||||
"""
|
||||
contentsPath = os.path.join(self.dirName, "contents.plist")
|
||||
with open(contentsPath, "wb") as f:
|
||||
writePlist(self.contents, f)
|
||||
# def writeContents(self):
|
||||
# """
|
||||
# Write the contents.plist file out to disk. Call this method when
|
||||
# you're done writing glyphs.
|
||||
# """
|
||||
# contentsPath = os.path.join(self.dirName, "contents.plist")
|
||||
# with open(contentsPath, "wb") as f:
|
||||
# writePlist(self.contents, f)
|
||||
|
||||
# layer info
|
||||
|
||||
def readLayerInfo(self, info):
|
||||
path = os.path.join(self.dirName, LAYERINFO_FILENAME)
|
||||
if not os.path.exists(path):
|
||||
path = self._fs.joinPath(self.dirName, LAYERINFO_FILENAME)
|
||||
if not self._fs.exists(path):
|
||||
return
|
||||
infoDict = self._readPlist(path)
|
||||
infoDict = self._fs.readPlist(path)
|
||||
if not isinstance(infoDict, dict):
|
||||
raise GlifLibError("layerinfo.plist is not properly formatted.")
|
||||
infoDict = validateLayerInfoVersion3Data(infoDict)
|
||||
@ -186,28 +191,28 @@ class GlyphSet(object):
|
||||
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 list(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)
|
||||
with open(path, "wb") as f:
|
||||
writePlist(infoData, f)
|
||||
# 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 list(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)
|
||||
# with open(path, "wb") as f:
|
||||
# writePlist(infoData, f)
|
||||
|
||||
# read caching
|
||||
# # read caching
|
||||
|
||||
def getGLIF(self, glyphName):
|
||||
"""
|
||||
@ -229,18 +234,18 @@ class GlyphSet(object):
|
||||
fileName = self.contents.get(glyphName)
|
||||
path = None
|
||||
if fileName is not None:
|
||||
path = os.path.join(self.dirName, fileName)
|
||||
path = self._fs.joinPath(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]:
|
||||
elif fileName is not None and self._fs.getFileModificationTime(path) != self._glifCache[glyphName][1]:
|
||||
needRead = True
|
||||
if needRead:
|
||||
fileName = self.contents[glyphName]
|
||||
if not os.path.exists(path):
|
||||
if not self._fs.exists(path):
|
||||
raise KeyError(glyphName)
|
||||
with open(path, "rb") as f:
|
||||
with self._fs.open(path, "rb") as f:
|
||||
text = f.read()
|
||||
self._glifCache[glyphName] = (text, os.path.getmtime(path))
|
||||
self._glifCache[glyphName] = (text, self._fs.getFileModificationTime(path))
|
||||
return self._glifCache[glyphName][0]
|
||||
|
||||
def getGLIFModificationTime(self, glyphName):
|
||||
@ -297,71 +302,71 @@ class GlyphSet(object):
|
||||
formatVersions = (1, 2)
|
||||
_readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions)
|
||||
|
||||
def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=None):
|
||||
"""
|
||||
Write a .glif file for 'glyphName' to the glyph set. The
|
||||
'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
|
||||
"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
|
||||
"anchors" a list of anchor data dictionaries
|
||||
# def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=None):
|
||||
# """
|
||||
# Write a .glif file for 'glyphName' to the glyph set. The
|
||||
# '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
|
||||
# "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
|
||||
# "anchors" a list of anchor data dictionaries
|
||||
|
||||
All attributes are optional: if 'glyphObject' doesn't
|
||||
have the attribute, it will simply be skipped.
|
||||
# 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.
|
||||
# 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.
|
||||
|
||||
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.
|
||||
"""
|
||||
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))
|
||||
self._purgeCachedGLIF(glyphName)
|
||||
data = writeGlyphToString(glyphName, glyphObject, drawPointsFunc, formatVersion=formatVersion)
|
||||
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):
|
||||
with open(path, "rb") as f:
|
||||
oldData = f.read()
|
||||
if data == oldData:
|
||||
return
|
||||
with open(path, "wb") as f:
|
||||
f.write(tobytes(data, encoding="utf-8"))
|
||||
# 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.
|
||||
# """
|
||||
# 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))
|
||||
# self._purgeCachedGLIF(glyphName)
|
||||
# data = writeGlyphToString(glyphName, glyphObject, drawPointsFunc, formatVersion=formatVersion)
|
||||
# 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):
|
||||
# with open(path, "rb") as f:
|
||||
# oldData = f.read()
|
||||
# if data == oldData:
|
||||
# return
|
||||
# with open(path, "wb") as f:
|
||||
# f.write(tobytes(data, encoding="utf-8"))
|
||||
|
||||
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.
|
||||
"""
|
||||
self._purgeCachedGLIF(glyphName)
|
||||
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]
|
||||
# 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.
|
||||
# """
|
||||
# self._purgeCachedGLIF(glyphName)
|
||||
# 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
|
||||
|
||||
@ -428,16 +433,6 @@ class GlyphSet(object):
|
||||
images[glyphName] = _fetchImageFileName(text)
|
||||
return images
|
||||
|
||||
# internal methods
|
||||
|
||||
def _readPlist(self, path):
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
data = readPlist(f)
|
||||
return data
|
||||
except:
|
||||
raise GlifLibError("The file %s could not be read." % path)
|
||||
|
||||
|
||||
# -----------------------
|
||||
# Glyph Name to File Name
|
||||
|
@ -1,4 +1,4 @@
|
||||
from ufoLib.plistlib import PlistParser
|
||||
from ufoLib.plistlibShim import PlistParser
|
||||
"""
|
||||
Small helper module to parse Plist-formatted data from trees as created
|
||||
by xmlTreeBuilder.
|
||||
|
Loading…
x
Reference in New Issue
Block a user