2011-10-01 07:51:10 +00:00
|
|
|
"""
|
2008-01-07 17:40:34 +00:00
|
|
|
A library for importing .ufo files and their descendants.
|
2009-02-28 15:47:24 +00:00
|
|
|
Refer to http://unifiedfontobject.com for the UFO specification.
|
|
|
|
|
|
|
|
The UFOReader and UFOWriter classes support versions 1 and 2
|
|
|
|
of the specification. Up and down conversion functions are also
|
|
|
|
supplied in this library. These conversion functions are only
|
|
|
|
necessary if conversion without loading the UFO data into
|
|
|
|
a set of objects is desired. These functions are:
|
|
|
|
convertUFOFormatVersion1ToFormatVersion2
|
|
|
|
convertUFOFormatVersion2ToFormatVersion1
|
|
|
|
|
|
|
|
Two sets that list the font info attribute names for the two
|
|
|
|
fontinfo.plist formats are available for external use. These are:
|
|
|
|
fontInfoAttributesVersion1
|
|
|
|
fontInfoAttributesVersion2
|
|
|
|
|
|
|
|
A set listing the fontinfo.plist attributes that were deprecated
|
|
|
|
in version 2 is available for external use:
|
|
|
|
deprecatedFontInfoAttributesVersion2
|
|
|
|
|
|
|
|
A function, validateFontInfoVersion2ValueForAttribute, that does
|
|
|
|
some basic validation on values for a fontinfo.plist value is
|
|
|
|
available for external use.
|
|
|
|
|
|
|
|
Two value conversion functions are availble for converting
|
|
|
|
fontinfo.plist values between the possible format versions.
|
|
|
|
convertFontInfoValueForAttributeFromVersion1ToVersion2
|
|
|
|
convertFontInfoValueForAttributeFromVersion2ToVersion1
|
2008-01-07 17:40:34 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
2009-02-28 15:47:24 +00:00
|
|
|
import shutil
|
2008-01-07 17:40:34 +00:00
|
|
|
from cStringIO import StringIO
|
2011-09-12 17:49:34 +00:00
|
|
|
import codecs
|
2011-09-12 20:56:12 +00:00
|
|
|
from copy import deepcopy
|
2011-09-18 12:16:25 +00:00
|
|
|
from plistlib import readPlist, writePlist
|
|
|
|
from glifLib import GlyphSet, READ_MODE, WRITE_MODE
|
2011-09-18 12:24:29 +00:00
|
|
|
from validators import *
|
2011-09-19 01:40:21 +00:00
|
|
|
from filenames import userNameToFileName
|
2011-09-27 17:58:45 +00:00
|
|
|
from converters import convertUFO1OrUFO2KerningToUFO3Kerning
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2009-02-28 15:47:24 +00:00
|
|
|
try:
|
|
|
|
set
|
|
|
|
except NameError:
|
|
|
|
from sets import Set as set
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2009-02-28 15:47:24 +00:00
|
|
|
__all__ = [
|
|
|
|
"makeUFOPath"
|
|
|
|
"UFOLibError",
|
|
|
|
"UFOReader",
|
|
|
|
"UFOWriter",
|
|
|
|
"convertUFOFormatVersion1ToFormatVersion2",
|
|
|
|
"convertUFOFormatVersion2ToFormatVersion1",
|
|
|
|
"fontInfoAttributesVersion1",
|
|
|
|
"fontInfoAttributesVersion2",
|
|
|
|
"deprecatedFontInfoAttributesVersion2",
|
|
|
|
"validateFontInfoVersion2ValueForAttribute",
|
|
|
|
"convertFontInfoValueForAttributeFromVersion1ToVersion2",
|
|
|
|
"convertFontInfoValueForAttributeFromVersion2ToVersion1"
|
2008-01-07 17:40:34 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
2009-02-28 15:47:24 +00:00
|
|
|
class UFOLibError(Exception): pass
|
|
|
|
|
|
|
|
|
|
|
|
# ----------
|
|
|
|
# File Names
|
|
|
|
# ----------
|
|
|
|
|
2011-09-11 23:47:21 +00:00
|
|
|
DEFAULT_GLYPHS_DIRNAME = "glyphs"
|
2011-09-12 11:35:57 +00:00
|
|
|
DATA_DIRNAME = "data"
|
|
|
|
IMAGES_DIRNAME = "images"
|
2009-02-28 15:47:24 +00:00
|
|
|
METAINFO_FILENAME = "metainfo.plist"
|
|
|
|
FONTINFO_FILENAME = "fontinfo.plist"
|
|
|
|
LIB_FILENAME = "lib.plist"
|
|
|
|
GROUPS_FILENAME = "groups.plist"
|
|
|
|
KERNING_FILENAME = "kerning.plist"
|
|
|
|
FEATURES_FILENAME = "features.fea"
|
2011-09-11 23:47:21 +00:00
|
|
|
LAYERCONTENTS_FILENAME = "layercontents.plist"
|
2011-09-12 13:25:24 +00:00
|
|
|
LAYERINFO_FILENAME = "layerinfo.plist"
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-27 16:18:42 +00:00
|
|
|
DEFAULT_LAYER_NAME = "public.default"
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-19 01:40:21 +00:00
|
|
|
supportedUFOFormatVersions = [1, 2, 3]
|
2009-02-28 15:47:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
# ----------
|
|
|
|
# UFO Reader
|
|
|
|
# ----------
|
2008-01-07 17:40:34 +00:00
|
|
|
|
|
|
|
class UFOReader(object):
|
2009-02-28 15:47:24 +00:00
|
|
|
|
|
|
|
"""Read the various components of the .ufo."""
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def __init__(self, path):
|
2011-09-19 01:40:21 +00:00
|
|
|
if not os.path.exists(path):
|
|
|
|
raise UFOLibError("The specified UFO doesn't exist.")
|
2008-01-07 17:40:34 +00:00
|
|
|
self._path = path
|
2009-02-28 15:47:24 +00:00
|
|
|
self.readMetaInfo()
|
2011-09-27 17:58:45 +00:00
|
|
|
self._upConvertedKerningData = None
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# properties
|
|
|
|
|
2009-02-28 15:47:24 +00:00
|
|
|
def _get_formatVersion(self):
|
|
|
|
return self._formatVersion
|
|
|
|
|
|
|
|
formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is determined by reading metainfo.plist during __init__.")
|
|
|
|
|
2011-09-27 17:58:45 +00:00
|
|
|
# up conversion
|
|
|
|
|
|
|
|
def _upConvertKerning(self):
|
|
|
|
"""
|
2011-10-03 14:43:17 +00:00
|
|
|
Up convert kerning and groups in UFO 1 and 2.
|
2011-09-27 17:58:45 +00:00
|
|
|
The data will be held internally until each bit of data
|
2011-10-03 14:43:17 +00:00
|
|
|
has been retrieved. The conversion of both must be done
|
|
|
|
at once, so the raw data is cached and an error is raised
|
|
|
|
if one bit of data becomes obsolete before it is called.
|
2011-09-27 17:58:45 +00:00
|
|
|
"""
|
|
|
|
if self._upConvertedKerningData:
|
|
|
|
testKerning = self._readKerning()
|
|
|
|
if testKerning != self._upConvertedKerningData["originalKerning"]:
|
|
|
|
raise UFOLibError("The data in kerning.plist has been modified since it was converted to UFO 3 format.")
|
|
|
|
testGroups = self._readGroups()
|
|
|
|
if testGroups != self._upConvertedKerningData["originalGroups"]:
|
|
|
|
raise UFOLibError("The data in groups.plist has been modified since it was converted to UFO 3 format.")
|
|
|
|
else:
|
2011-10-03 15:17:31 +00:00
|
|
|
groups = self._readGroups()
|
|
|
|
invalidFormatMessage = "groups.plist is not properly formatted."
|
2011-10-04 17:14:27 +00:00
|
|
|
if not isinstance(groups, dict):
|
2011-10-03 15:17:31 +00:00
|
|
|
raise UFOLibError(invalidFormatMessage)
|
2011-10-04 17:14:27 +00:00
|
|
|
for groupName, glyphList in groups.items():
|
2011-10-03 15:17:31 +00:00
|
|
|
if not isinstance(groupName, basestring):
|
|
|
|
raise UFOLibError(invalidFormatMessage)
|
|
|
|
elif not isinstance(glyphList, list):
|
|
|
|
raise UFOLibError(invalidFormatMessage)
|
|
|
|
for glyphName in glyphList:
|
|
|
|
if not isinstance(glyphName, basestring):
|
|
|
|
raise UFOLibError(invalidFormatMessage)
|
2011-09-27 17:58:45 +00:00
|
|
|
self._upConvertedKerningData = dict(
|
|
|
|
kerning={},
|
|
|
|
originalKerning=self._readKerning(),
|
|
|
|
groups={},
|
2011-10-03 15:17:31 +00:00
|
|
|
originalGroups=groups
|
2011-09-27 17:58:45 +00:00
|
|
|
)
|
|
|
|
# convert kerning and groups
|
|
|
|
kerning, groups = convertUFO1OrUFO2KerningToUFO3Kerning(
|
|
|
|
self._upConvertedKerningData["originalKerning"],
|
2011-10-03 14:43:17 +00:00
|
|
|
deepcopy(self._upConvertedKerningData["originalGroups"])
|
2011-09-27 17:58:45 +00:00
|
|
|
)
|
|
|
|
# store
|
|
|
|
self._upConvertedKerningData["kerning"] = kerning
|
|
|
|
self._upConvertedKerningData["groups"] = groups
|
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# support methods
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def _checkForFile(self, path):
|
|
|
|
if not os.path.exists(path):
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return True
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-26 22:16:08 +00:00
|
|
|
def _readPlist(self, path):
|
|
|
|
"""
|
|
|
|
Read a property list. 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.
|
|
|
|
"""
|
|
|
|
originalPath = path
|
|
|
|
path = os.path.join(self._path, path)
|
|
|
|
try:
|
|
|
|
data = readPlist(path)
|
|
|
|
return data
|
|
|
|
except:
|
|
|
|
raise UFOLibError("The file %s could not be read." % originalPath)
|
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
def readBytesFromPath(self, path, encoding=None):
|
2011-09-12 11:35:57 +00:00
|
|
|
"""
|
2011-09-12 17:49:34 +00:00
|
|
|
Returns the bytes in the file at the given path.
|
2011-09-12 11:35:57 +00:00
|
|
|
The path must be relative to the UFO path.
|
|
|
|
Returns None if the file does not exist.
|
2011-09-12 17:49:34 +00:00
|
|
|
An encoding may be passed if needed.
|
2011-09-12 11:35:57 +00:00
|
|
|
"""
|
|
|
|
path = os.path.join(self._path, path)
|
|
|
|
if not self._checkForFile(path):
|
|
|
|
return None
|
2011-09-12 17:49:34 +00:00
|
|
|
f = codecs.open(path, READ_MODE, encoding=encoding)
|
2011-09-12 11:35:57 +00:00
|
|
|
data = f.read()
|
|
|
|
f.close()
|
|
|
|
return data
|
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
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.
|
|
|
|
"""
|
|
|
|
path = os.path.join(self._path, path)
|
|
|
|
if not self._checkForFile(path):
|
|
|
|
return None
|
|
|
|
f = codecs.open(path, READ_MODE, encoding=encoding)
|
|
|
|
return f
|
|
|
|
|
|
|
|
# metainfo.plist
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def readMetaInfo(self):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
|
|
|
Read metainfo.plist. Only used for internal operations.
|
|
|
|
"""
|
2008-01-07 17:40:34 +00:00
|
|
|
path = os.path.join(self._path, METAINFO_FILENAME)
|
|
|
|
if not self._checkForFile(path):
|
2009-02-28 15:47:24 +00:00
|
|
|
raise UFOLibError("metainfo.plist is missing in %s. This file is required." % self._path)
|
|
|
|
# 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.
|
2011-09-28 14:47:25 +00:00
|
|
|
data = self._readPlist(path)
|
|
|
|
if not isinstance(data, dict):
|
|
|
|
raise UFOLibError("maetainfo.plist is not properly formatted.")
|
2009-02-28 15:47:24 +00:00
|
|
|
formatVersion = data["formatVersion"]
|
|
|
|
if formatVersion not in supportedUFOFormatVersions:
|
|
|
|
raise UFOLibError("Unsupported UFO format (%d) in %s." % (formatVersion, self._path))
|
|
|
|
self._formatVersion = formatVersion
|
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# groups.plist
|
|
|
|
|
2011-09-27 17:58:45 +00:00
|
|
|
def _readGroups(self):
|
|
|
|
path = os.path.join(self._path, GROUPS_FILENAME)
|
|
|
|
if not self._checkForFile(path):
|
|
|
|
return {}
|
2011-09-28 14:47:25 +00:00
|
|
|
data = self._readPlist(path)
|
|
|
|
return data
|
2011-09-27 17:58:45 +00:00
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def readGroups(self):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
|
|
|
Read groups.plist. Returns a dict.
|
|
|
|
"""
|
2011-09-27 17:58:45 +00:00
|
|
|
# handle up conversion
|
|
|
|
if self._formatVersion < 3:
|
|
|
|
self._upConvertKerning()
|
2011-10-03 15:17:31 +00:00
|
|
|
groups = self._upConvertedKerningData["groups"]
|
2011-09-27 17:58:45 +00:00
|
|
|
# normal
|
|
|
|
else:
|
2011-10-03 15:17:31 +00:00
|
|
|
groups = self._readGroups()
|
2011-10-03 15:56:08 +00:00
|
|
|
valid, message = groupsValidator(groups)
|
2011-10-03 15:17:31 +00:00
|
|
|
if not valid:
|
|
|
|
raise UFOLibError(message)
|
|
|
|
return groups
|
2011-09-27 17:58:45 +00:00
|
|
|
|
|
|
|
# fontinfo.plist
|
|
|
|
|
|
|
|
def _readInfo(self):
|
|
|
|
path = os.path.join(self._path, FONTINFO_FILENAME)
|
2008-01-07 17:40:34 +00:00
|
|
|
if not self._checkForFile(path):
|
|
|
|
return {}
|
2011-09-28 14:47:25 +00:00
|
|
|
data = self._readPlist(path)
|
|
|
|
if not isinstance(data, dict):
|
|
|
|
raise UFOLibError("fontinfo.plist is not properly formatted.")
|
|
|
|
return data
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def readInfo(self, info):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
|
|
|
Read fontinfo.plist. It requires an object that allows
|
|
|
|
setting attributes with names that follow the fontinfo.plist
|
2011-09-27 17:58:45 +00:00
|
|
|
version 3 specification. This will write the attributes
|
2009-02-28 15:47:24 +00:00
|
|
|
defined in the file into the object.
|
|
|
|
"""
|
2011-10-03 14:43:17 +00:00
|
|
|
path = os.path.join(self._path, FONTINFO_FILENAME)
|
|
|
|
if not self._checkForFile(path):
|
|
|
|
return {}
|
|
|
|
infoDict = self._readPlist(path)
|
|
|
|
if not isinstance(infoDict, dict):
|
|
|
|
raise UFOLibError("fontinfo.plist is not properly formatted.")
|
2009-02-28 15:47:24 +00:00
|
|
|
infoDataToSet = {}
|
|
|
|
# version 1
|
|
|
|
if self._formatVersion == 1:
|
|
|
|
for attr in fontInfoAttributesVersion1:
|
|
|
|
value = infoDict.get(attr)
|
|
|
|
if value is not None:
|
|
|
|
infoDataToSet[attr] = value
|
|
|
|
infoDataToSet = _convertFontInfoDataVersion1ToVersion2(infoDataToSet)
|
|
|
|
# version 2
|
|
|
|
elif self._formatVersion == 2:
|
2011-09-18 12:24:29 +00:00
|
|
|
for attr, dataValidationDict in fontInfoAttributesVersion2ValueData.items():
|
2009-02-28 15:47:24 +00:00
|
|
|
value = infoDict.get(attr)
|
|
|
|
if value is None:
|
|
|
|
continue
|
|
|
|
infoDataToSet[attr] = value
|
2011-09-14 21:13:27 +00:00
|
|
|
# version 3
|
|
|
|
elif self._formatVersion == 3:
|
2011-09-18 12:24:29 +00:00
|
|
|
for attr, dataValidationDict in fontInfoAttributesVersion3ValueData.items():
|
2011-09-14 21:13:27 +00:00
|
|
|
value = infoDict.get(attr)
|
|
|
|
if value is None:
|
|
|
|
continue
|
|
|
|
infoDataToSet[attr] = value
|
2009-02-28 15:47:24 +00:00
|
|
|
# unsupported version
|
|
|
|
else:
|
|
|
|
raise NotImplementedError
|
|
|
|
# validate data
|
2011-09-14 21:13:27 +00:00
|
|
|
if self._formatVersion < 3:
|
2011-09-18 12:24:29 +00:00
|
|
|
infoDataToSet = validateInfoVersion2Data(infoDataToSet)
|
2011-09-14 21:13:27 +00:00
|
|
|
elif self._formatVersion == 3:
|
2011-09-18 12:24:29 +00:00
|
|
|
infoDataToSet = validateInfoVersion3Data(infoDataToSet)
|
2009-02-28 15:47:24 +00:00
|
|
|
# populate the object
|
|
|
|
for attr, value in infoDataToSet.items():
|
2008-01-07 17:40:34 +00:00
|
|
|
try:
|
2009-02-28 15:47:24 +00:00
|
|
|
setattr(info, attr, value)
|
2008-01-07 17:40:34 +00:00
|
|
|
except AttributeError:
|
2009-02-28 15:47:24 +00:00
|
|
|
raise UFOLibError("The supplied info object does not support setting a necessary attribute (%s)." % attr)
|
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# kerning.plist
|
|
|
|
|
2011-09-27 17:58:45 +00:00
|
|
|
def _readKerning(self):
|
|
|
|
path = os.path.join(self._path, KERNING_FILENAME)
|
|
|
|
if not self._checkForFile(path):
|
|
|
|
return {}
|
2011-09-28 14:47:25 +00:00
|
|
|
data = self._readPlist(path)
|
2011-09-29 13:31:37 +00:00
|
|
|
invalidFormatMessage = "kerning.plist is not properly formatted."
|
2011-09-28 14:47:25 +00:00
|
|
|
if not isinstance(data, dict):
|
2011-09-29 13:31:37 +00:00
|
|
|
raise UFOLibError(invalidFormatMessage)
|
|
|
|
for first, secondDict in data.items():
|
|
|
|
if not isinstance(first, basestring):
|
|
|
|
raise UFOLibError(invalidFormatMessage)
|
|
|
|
elif not isinstance(secondDict, dict):
|
|
|
|
raise UFOLibError(invalidFormatMessage)
|
|
|
|
for second, value in secondDict.items():
|
|
|
|
if not isinstance(second, basestring):
|
|
|
|
raise UFOLibError(invalidFormatMessage)
|
|
|
|
elif not isinstance(value, (int, float)):
|
|
|
|
raise UFOLibError(invalidFormatMessage)
|
2011-09-28 14:47:25 +00:00
|
|
|
return data
|
2011-09-27 17:58:45 +00:00
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def readKerning(self):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
|
|
|
Read kerning.plist. Returns a dict.
|
|
|
|
"""
|
2011-09-27 17:58:45 +00:00
|
|
|
# handle up conversion
|
|
|
|
if self._formatVersion < 3:
|
|
|
|
self._upConvertKerning()
|
|
|
|
kerningNested = self._upConvertedKerningData["kerning"]
|
|
|
|
# normal
|
|
|
|
else:
|
|
|
|
kerningNested = self._readKerning()
|
|
|
|
# flatten
|
2008-01-07 17:40:34 +00:00
|
|
|
kerning = {}
|
|
|
|
for left in kerningNested:
|
|
|
|
for right in kerningNested[left]:
|
|
|
|
value = kerningNested[left][right]
|
|
|
|
kerning[left, right] = value
|
|
|
|
return kerning
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# lib.plist
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def readLib(self):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
|
|
|
Read lib.plist. Returns a dict.
|
|
|
|
"""
|
2008-01-07 17:40:34 +00:00
|
|
|
path = os.path.join(self._path, LIB_FILENAME)
|
|
|
|
if not self._checkForFile(path):
|
|
|
|
return {}
|
2011-09-28 14:47:25 +00:00
|
|
|
data = self._readPlist(path)
|
2011-10-04 19:09:29 +00:00
|
|
|
valid, message = libValidator(data)
|
2011-10-03 15:56:08 +00:00
|
|
|
if not valid:
|
|
|
|
raise UFOLibError(message)
|
|
|
|
return data
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# features.fea
|
|
|
|
|
2009-02-28 15:47:24 +00:00
|
|
|
def readFeatures(self):
|
|
|
|
"""
|
|
|
|
Read features.fea. Returns a string.
|
|
|
|
"""
|
|
|
|
path = os.path.join(self._path, FEATURES_FILENAME)
|
|
|
|
if not self._checkForFile(path):
|
|
|
|
return ""
|
|
|
|
f = open(path, READ_MODE)
|
|
|
|
text = f.read()
|
|
|
|
f.close()
|
|
|
|
return text
|
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# glyph sets & layers
|
|
|
|
|
2011-09-26 20:12:19 +00:00
|
|
|
def _readLayerContents(self):
|
2011-09-11 23:47:21 +00:00
|
|
|
"""
|
2011-09-26 19:57:10 +00:00
|
|
|
Rebuild the layer contents list by checking what glyphsets
|
|
|
|
are available on disk.
|
2011-09-11 23:47:21 +00:00
|
|
|
"""
|
2011-09-26 19:57:10 +00:00
|
|
|
if self._formatVersion < 3:
|
2011-09-26 22:16:08 +00:00
|
|
|
return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)]
|
2011-09-26 19:57:10 +00:00
|
|
|
# read the file on disk
|
2011-09-11 23:47:21 +00:00
|
|
|
path = os.path.join(self._path, LAYERCONTENTS_FILENAME)
|
2011-09-26 19:57:10 +00:00
|
|
|
if not os.path.exists(path):
|
|
|
|
raise UFOLibError("layercontents.plist is missing.")
|
|
|
|
if os.path.exists(path):
|
2011-09-26 22:16:08 +00:00
|
|
|
contents = self._readPlist(path)
|
|
|
|
valid, error = layerContentsValidator(contents, self._path)
|
2011-09-26 19:57:10 +00:00
|
|
|
if not valid:
|
|
|
|
raise UFOLibError(error)
|
2011-09-26 22:16:08 +00:00
|
|
|
return contents
|
2011-09-11 23:47:21 +00:00
|
|
|
|
|
|
|
def getLayerNames(self):
|
|
|
|
"""
|
|
|
|
Get the ordered layer names from layercontents.plist.
|
|
|
|
"""
|
|
|
|
layerContents = self._readLayerContents()
|
|
|
|
layerNames = [layerName for layerName, directoryName in layerContents]
|
|
|
|
return layerNames
|
|
|
|
|
|
|
|
def getDefaultLayerName(self):
|
|
|
|
"""
|
|
|
|
Get the default layer name from layercontents.plist.
|
|
|
|
"""
|
|
|
|
layerContents = self._readLayerContents()
|
|
|
|
for layerName, layerDirectory in layerContents:
|
2011-09-26 22:16:08 +00:00
|
|
|
if layerDirectory == DEFAULT_GLYPHS_DIRNAME:
|
2011-09-11 23:47:21 +00:00
|
|
|
return layerName
|
2011-09-26 19:57:10 +00:00
|
|
|
# this will already have been raised during __init__
|
2011-09-11 23:47:21 +00:00
|
|
|
raise UFOLibError("The default layer is not defined in layercontents.plist.")
|
|
|
|
|
|
|
|
def getGlyphSet(self, layerName=None):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
|
|
|
Return the GlyphSet associated with the
|
2011-09-11 23:47:21 +00:00
|
|
|
glyphs directory mapped to layerName
|
|
|
|
in the UFO. If layerName is not provided,
|
|
|
|
the name retrieved with getDefaultLayerName
|
|
|
|
will be used.
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
2011-09-11 23:47:21 +00:00
|
|
|
if layerName is None:
|
|
|
|
layerName = self.getDefaultLayerName()
|
|
|
|
directory = None
|
|
|
|
layerContents = self._readLayerContents()
|
|
|
|
for storedLayerName, storedLayerDirectory in layerContents:
|
|
|
|
if layerName == storedLayerName:
|
|
|
|
directory = storedLayerDirectory
|
|
|
|
break
|
|
|
|
if directory is None:
|
|
|
|
raise UFOLibError("No glyphs directory is mapped to \"%s\"." % layerName)
|
|
|
|
glyphsPath = os.path.join(self._path, directory)
|
2008-01-07 17:40:34 +00:00
|
|
|
return GlyphSet(glyphsPath)
|
|
|
|
|
2011-09-30 20:44:21 +00:00
|
|
|
def getCharacterMapping(self, layerName=None):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
|
|
|
Return a dictionary that maps unicode values (ints) to
|
2008-01-07 17:40:34 +00:00
|
|
|
lists of glyph names.
|
|
|
|
"""
|
2011-09-30 20:44:21 +00:00
|
|
|
glyphsPath = self.getGlyphSet(layerName)
|
2008-01-07 17:40:34 +00:00
|
|
|
allUnicodes = glyphSet.getUnicodes()
|
|
|
|
cmap = {}
|
|
|
|
for glyphName, unicodes in allUnicodes.iteritems():
|
|
|
|
for code in unicodes:
|
|
|
|
if code in cmap:
|
|
|
|
cmap[code].append(glyphName)
|
|
|
|
else:
|
|
|
|
cmap[code] = [glyphName]
|
|
|
|
return cmap
|
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# /data
|
|
|
|
|
2011-09-12 11:35:57 +00:00
|
|
|
def getDataDirectoryListing(self, maxDepth=100):
|
|
|
|
"""
|
|
|
|
Returns a list of all files and directories
|
|
|
|
in the data directory. The returned paths will
|
2011-09-12 11:51:24 +00:00
|
|
|
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.
|
2011-09-12 11:35:57 +00:00
|
|
|
"""
|
|
|
|
path = os.path.join(self._path, DATA_DIRNAME)
|
|
|
|
if not self._checkForFile(path):
|
|
|
|
return []
|
|
|
|
listing = self._getDirectoryListing(path, maxDepth=maxDepth)
|
|
|
|
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
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-12 13:25:24 +00:00
|
|
|
|
2009-02-28 15:47:24 +00:00
|
|
|
# ----------
|
|
|
|
# UFO Writer
|
|
|
|
# ----------
|
|
|
|
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
class UFOWriter(object):
|
2009-02-28 15:47:24 +00:00
|
|
|
|
|
|
|
"""Write the various components of the .ufo."""
|
|
|
|
|
2011-09-14 21:13:27 +00:00
|
|
|
def __init__(self, path, formatVersion=3, fileCreator="org.robofab.ufoLib"):
|
2009-02-28 15:47:24 +00:00
|
|
|
if formatVersion not in supportedUFOFormatVersions:
|
|
|
|
raise UFOLibError("Unsupported UFO format (%d)." % formatVersion)
|
2011-09-27 15:28:42 +00:00
|
|
|
# establish some basic stuff
|
|
|
|
self._path = path
|
|
|
|
self._formatVersion = formatVersion
|
|
|
|
self._fileCreator = fileCreator
|
2011-09-26 19:57:10 +00:00
|
|
|
# if the file already exists, get the format version.
|
|
|
|
# this will be needed for up and down conversion.
|
|
|
|
previousFormatVersion = None
|
|
|
|
if os.path.exists(path):
|
|
|
|
p = os.path.join(path, METAINFO_FILENAME)
|
|
|
|
if not os.path.exists(p):
|
|
|
|
raise UFOLibError("The metainfo.plist file is not in the existing UFO.")
|
2011-09-27 15:28:42 +00:00
|
|
|
metaInfo = self._readPlist(METAINFO_FILENAME)
|
|
|
|
previousFormatVersion = metaInfo.get("formatVersion")
|
|
|
|
try:
|
|
|
|
previousFormatVersion = int(previousFormatVersion)
|
|
|
|
except:
|
|
|
|
raise UFOLibError("The existing metainfo.plist is not properly formatted.")
|
|
|
|
if previousFormatVersion not in supportedUFOFormatVersions:
|
|
|
|
raise UFOLibError("Unsupported UFO format (%d)." % formatVersion)
|
2011-09-26 19:57:10 +00:00
|
|
|
# handle the layer contents
|
2011-09-19 01:40:21 +00:00
|
|
|
self.layerContents = {}
|
2011-09-26 19:57:10 +00:00
|
|
|
if previousFormatVersion >= 3:
|
|
|
|
# already exists
|
|
|
|
self._readLayerContents()
|
|
|
|
else:
|
2011-09-19 01:40:21 +00:00
|
|
|
# previous < 3
|
|
|
|
# imply the layer contents
|
2011-09-26 19:57:10 +00:00
|
|
|
p = os.path.join(path, DEFAULT_GLYPHS_DIRNAME)
|
|
|
|
if os.path.exists(p):
|
2011-09-27 15:28:42 +00:00
|
|
|
self.layerContents = {DEFAULT_LAYER_NAME : DEFAULT_GLYPHS_DIRNAME}
|
2011-09-26 19:57:10 +00:00
|
|
|
# write the new metainfo
|
2009-02-28 15:47:24 +00:00
|
|
|
self._writeMetaInfo()
|
|
|
|
# handle down conversion
|
2011-09-26 19:57:10 +00:00
|
|
|
# >= 3 to 2
|
2011-09-27 15:28:42 +00:00
|
|
|
if formatVersion < 3 and previousFormatVersion >= 3:
|
2011-09-12 13:25:24 +00:00
|
|
|
# remove all glyph sets except the default
|
2011-09-27 15:28:42 +00:00
|
|
|
for layerName, directoryName in self.layerContents.items():
|
2011-09-26 19:57:10 +00:00
|
|
|
if directoryName != DEFAULT_GLYPHS_DIRNAME:
|
|
|
|
self._removeFileForPath(directoryName)
|
2011-09-12 13:25:24 +00:00
|
|
|
# remove layercontents.plist
|
2011-09-19 01:40:21 +00:00
|
|
|
self._removeFileForPath(LAYERCONTENTS_FILENAME)
|
2011-09-12 13:25:24 +00:00
|
|
|
# remove glyphs/layerinfo.plist
|
2011-09-19 01:40:21 +00:00
|
|
|
p = os.path.join(DEFAULT_GLYPHS_DIRNAME, LAYERINFO_FILENAME)
|
|
|
|
self._removeFileForPath(p)
|
2011-09-12 13:25:24 +00:00
|
|
|
# remove /images
|
2011-09-19 01:40:21 +00:00
|
|
|
self._removeFileForPath(IMAGES_DIRNAME)
|
2011-09-12 13:25:24 +00:00
|
|
|
# remove /data
|
2011-09-19 01:40:21 +00:00
|
|
|
self._removeFileForPath(DATA_DIRNAME)
|
2011-09-26 19:57:10 +00:00
|
|
|
# 2 to 1
|
2011-09-12 13:25:24 +00:00
|
|
|
if formatVersion < 2:
|
|
|
|
# remove features.fea
|
2011-09-19 01:40:21 +00:00
|
|
|
self._removeFileForPath(FEATURES_FILENAME)
|
2011-09-12 17:49:34 +00:00
|
|
|
|
|
|
|
# properties
|
2009-02-28 15:47:24 +00:00
|
|
|
|
|
|
|
def _get_formatVersion(self):
|
|
|
|
return self._formatVersion
|
|
|
|
|
|
|
|
formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is set into metainfo.plist during __init__.")
|
|
|
|
|
|
|
|
def _get_fileCreator(self):
|
|
|
|
return self._fileCreator
|
|
|
|
|
|
|
|
fileCreator = property(_get_fileCreator, doc="The file creator of the UFO. This is set into metainfo.plist during __init__.")
|
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# support methods
|
|
|
|
|
2011-09-19 01:40:21 +00:00
|
|
|
def _readPlist(self, path):
|
|
|
|
"""
|
|
|
|
Read a property list. 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.
|
|
|
|
"""
|
|
|
|
originalPath = path
|
|
|
|
path = os.path.join(self._path, path)
|
|
|
|
try:
|
|
|
|
data = readPlist(path)
|
|
|
|
return data
|
|
|
|
except:
|
|
|
|
raise UFOLibError("The file %s could not be read." % originalPath)
|
|
|
|
|
|
|
|
def _writePlist(self, data, path):
|
|
|
|
"""
|
|
|
|
Write a property list. The errors that
|
|
|
|
could be raised during the writing 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.
|
|
|
|
"""
|
|
|
|
originalPath = path
|
|
|
|
path = os.path.join(self._path, path)
|
|
|
|
try:
|
|
|
|
data = writePlistAtomically(data, path)
|
|
|
|
except:
|
|
|
|
raise UFOLibError("The data for the file %s could not be written because it is not properly formatted." % originalPath)
|
2011-09-12 13:25:24 +00:00
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def _makeDirectory(self, subDirectory=None):
|
|
|
|
path = self._path
|
|
|
|
if subDirectory:
|
|
|
|
path = os.path.join(self._path, subDirectory)
|
|
|
|
if not os.path.exists(path):
|
|
|
|
os.makedirs(path)
|
|
|
|
return path
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
def _buildDirectoryTree(self, path):
|
|
|
|
directory, fileName = os.path.split(path)
|
|
|
|
directoryTree = []
|
|
|
|
while directory:
|
|
|
|
directory, d = os.path.split(directory)
|
|
|
|
directoryTree.append(d)
|
|
|
|
directoryTree.reverse()
|
|
|
|
built = ""
|
|
|
|
for d in directoryTree:
|
|
|
|
d = os.path.join(built, d)
|
|
|
|
p = os.path.join(self._path, d)
|
|
|
|
if not os.path.exists(p):
|
|
|
|
os.mkdir(p)
|
|
|
|
built = d
|
|
|
|
|
2011-09-19 01:40:21 +00:00
|
|
|
def _removeFileForPath(self, path, raiseErrorIfMissing=False):
|
|
|
|
originalPath = path
|
|
|
|
path = os.path.join(self._path, path)
|
|
|
|
if not os.path.exists(path):
|
|
|
|
if raiseErrorIfMissing:
|
|
|
|
raise UFOLibError("The file %s does not exist." % path)
|
|
|
|
else:
|
|
|
|
if os.path.isdir(path):
|
|
|
|
shutil.rmtree(path)
|
|
|
|
else:
|
|
|
|
os.remove(path)
|
|
|
|
# remove any directories that are now empty
|
|
|
|
self._removeEmptyDirectoriesForPath(os.path.dirname(originalPath))
|
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
def _removeEmptyDirectoriesForPath(self, directory):
|
|
|
|
absoluteDirectory = os.path.join(self._path, directory)
|
2011-09-19 01:40:21 +00:00
|
|
|
if not os.path.exists(absoluteDirectory):
|
|
|
|
return
|
2011-09-12 17:49:34 +00:00
|
|
|
if not len(os.listdir(absoluteDirectory)):
|
|
|
|
shutil.rmtree(absoluteDirectory)
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
directory = os.path.dirname(directory)
|
|
|
|
if directory:
|
|
|
|
self._removeEmptyDirectoriesForPath(directory)
|
|
|
|
|
2011-09-26 19:57:10 +00:00
|
|
|
# file system interaction
|
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
def writeBytesToPath(self, path, bytes, 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 self._formatVersion < 2:
|
|
|
|
raise UFOLibError("The data directory is not allowed in UFO Format Version %d." % self.formatVersion)
|
|
|
|
self._buildDirectoryTree(path)
|
|
|
|
path = os.path.join(self._path, path)
|
|
|
|
writeFileAtomically(bytes, path, encoding=encoding)
|
|
|
|
|
|
|
|
def getFileObjectForPath(self, path, encoding=None):
|
|
|
|
"""
|
|
|
|
Creates a write mode file object at 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.
|
|
|
|
|
|
|
|
Note: The caller is responsible for closing the open file.
|
|
|
|
"""
|
|
|
|
if self._formatVersion < 2:
|
|
|
|
raise UFOLibError("The data directory is not allowed in UFO Format Version %d." % self.formatVersion)
|
|
|
|
self._buildDirectoryTree(path)
|
2011-09-14 21:27:49 +00:00
|
|
|
path = os.path.join(self._path, path)
|
2011-09-12 17:49:34 +00:00
|
|
|
return codecs.open(path, WRITE_MODE, encoding=encoding)
|
|
|
|
|
|
|
|
def removeFileForPath(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.
|
|
|
|
"""
|
|
|
|
# make sure that only data or images is being changed
|
|
|
|
d = path
|
|
|
|
parts = []
|
|
|
|
while d:
|
|
|
|
d, p = os.path.split(d)
|
|
|
|
if p:
|
|
|
|
parts.append(p)
|
|
|
|
if parts[-1] not in ("images", "data"):
|
|
|
|
raise UFOLibError("Removing \"%s\" is not legal." % path)
|
|
|
|
# remove the file
|
2011-09-19 01:40:21 +00:00
|
|
|
self._removeFileForPath(path, raiseErrorIfMissing=True)
|
2011-09-12 17:49:34 +00:00
|
|
|
|
|
|
|
# metainfo.plist
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def _writeMetaInfo(self):
|
2009-02-28 15:47:24 +00:00
|
|
|
self._makeDirectory()
|
2008-01-07 17:40:34 +00:00
|
|
|
path = os.path.join(self._path, METAINFO_FILENAME)
|
2009-02-28 15:47:24 +00:00
|
|
|
metaInfo = dict(
|
|
|
|
creator=self._fileCreator,
|
|
|
|
formatVersion=self._formatVersion
|
|
|
|
)
|
2011-09-19 01:40:21 +00:00
|
|
|
self._writePlist(metaInfo, path)
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# groups.plist
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def writeGroups(self, groups):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
|
|
|
Write groups.plist. This method requires a
|
|
|
|
dict of glyph groups as an argument.
|
|
|
|
"""
|
2011-10-03 15:56:08 +00:00
|
|
|
valid, message = groupsValidator(groups)
|
2011-10-03 15:17:31 +00:00
|
|
|
if not valid:
|
|
|
|
raise UFOLibError(message)
|
2008-01-07 17:40:34 +00:00
|
|
|
self._makeDirectory()
|
|
|
|
path = os.path.join(self._path, GROUPS_FILENAME)
|
|
|
|
groupsNew = {}
|
|
|
|
for key, value in groups.items():
|
|
|
|
groupsNew[key] = list(value)
|
|
|
|
if groupsNew:
|
2011-09-19 01:40:21 +00:00
|
|
|
self._writePlist(groupsNew, path)
|
2008-01-07 17:40:34 +00:00
|
|
|
elif os.path.exists(path):
|
|
|
|
os.remove(path)
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# fontinfo.plist
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def writeInfo(self, info):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
|
|
|
Write info.plist. This method requires an object
|
|
|
|
that supports getting attributes that follow the
|
2011-09-14 21:13:27 +00:00
|
|
|
fontinfo.plist version 2 specification. Attributes
|
2009-02-28 15:47:24 +00:00
|
|
|
will be taken from the given object and written
|
|
|
|
into the file.
|
|
|
|
"""
|
2008-01-07 17:40:34 +00:00
|
|
|
self._makeDirectory()
|
|
|
|
path = os.path.join(self._path, FONTINFO_FILENAME)
|
2011-09-14 21:13:27 +00:00
|
|
|
# gather version 3 data
|
2009-02-28 15:47:24 +00:00
|
|
|
infoData = {}
|
2011-09-18 12:24:29 +00:00
|
|
|
for attr in fontInfoAttributesVersion3ValueData.keys():
|
2011-09-14 21:13:27 +00:00
|
|
|
if hasattr(info, attr):
|
|
|
|
try:
|
|
|
|
value = getattr(info, attr)
|
|
|
|
except AttributeError:
|
|
|
|
raise UFOLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr)
|
|
|
|
if value is None:
|
|
|
|
continue
|
|
|
|
infoData[attr] = value
|
|
|
|
# down convert data if necessary and validate
|
|
|
|
if self._formatVersion == 3:
|
2011-09-18 12:24:29 +00:00
|
|
|
infoData = validateInfoVersion3Data(infoData)
|
2011-09-14 21:13:27 +00:00
|
|
|
elif self._formatVersion == 2:
|
|
|
|
infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
|
2011-09-18 12:24:29 +00:00
|
|
|
infoData = validateInfoVersion2Data(infoData)
|
2011-09-14 21:13:27 +00:00
|
|
|
elif self._formatVersion == 1:
|
|
|
|
infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
|
2011-09-18 12:24:29 +00:00
|
|
|
infoData = validateInfoVersion2Data(infoData)
|
2009-02-28 15:47:24 +00:00
|
|
|
infoData = _convertFontInfoDataVersion2ToVersion1(infoData)
|
|
|
|
# write file
|
2011-09-19 01:40:21 +00:00
|
|
|
self._writePlist(infoData, path)
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# kerning.plist
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def writeKerning(self, kerning):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
|
|
|
Write kerning.plist. This method requires a
|
|
|
|
dict of kerning pairs as an argument.
|
|
|
|
"""
|
2011-09-29 13:36:45 +00:00
|
|
|
invalidFormatMessage = "The kerning is not properly formatted."
|
2011-09-28 14:47:25 +00:00
|
|
|
if not isinstance(kerning, dict):
|
2011-09-29 13:36:45 +00:00
|
|
|
raise UFOLibError(invalidFormatMessage)
|
|
|
|
for pair, value in kerning.items():
|
|
|
|
if not isinstance(pair, (list, tuple)):
|
|
|
|
raise UFOLibError(invalidFormatMessage)
|
|
|
|
if not len(pair) == 2:
|
|
|
|
raise UFOLibError(invalidFormatMessage)
|
|
|
|
if not isinstance(pair[0], basestring):
|
|
|
|
raise UFOLibError(invalidFormatMessage)
|
|
|
|
if not isinstance(pair[1], basestring):
|
|
|
|
raise UFOLibError(invalidFormatMessage)
|
|
|
|
if not isinstance(value, (int, float)):
|
|
|
|
raise UFOLibError(invalidFormatMessage)
|
2008-01-07 17:40:34 +00:00
|
|
|
self._makeDirectory()
|
|
|
|
path = os.path.join(self._path, KERNING_FILENAME)
|
|
|
|
kerningDict = {}
|
|
|
|
for left, right in kerning.keys():
|
|
|
|
value = kerning[left, right]
|
|
|
|
if not left in kerningDict:
|
|
|
|
kerningDict[left] = {}
|
|
|
|
kerningDict[left][right] = value
|
|
|
|
if kerningDict:
|
2011-09-19 01:40:21 +00:00
|
|
|
self._writePlist(kerningDict, path)
|
2008-01-07 17:40:34 +00:00
|
|
|
elif os.path.exists(path):
|
|
|
|
os.remove(path)
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# lib.plist
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def writeLib(self, libDict):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
|
|
|
Write lib.plist. This method requires a
|
|
|
|
lib dict as an argument.
|
|
|
|
"""
|
2011-10-04 19:09:29 +00:00
|
|
|
valid, message = libValidator(libDict)
|
2011-10-03 15:56:08 +00:00
|
|
|
if not valid:
|
|
|
|
raise UFOLibError(message)
|
2008-01-07 17:40:34 +00:00
|
|
|
self._makeDirectory()
|
|
|
|
path = os.path.join(self._path, LIB_FILENAME)
|
|
|
|
if libDict:
|
2011-09-19 01:40:21 +00:00
|
|
|
self._writePlist(libDict, path)
|
2008-01-07 17:40:34 +00:00
|
|
|
elif os.path.exists(path):
|
|
|
|
os.remove(path)
|
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# features.fea
|
|
|
|
|
2009-02-28 15:47:24 +00:00
|
|
|
def writeFeatures(self, features):
|
|
|
|
"""
|
|
|
|
Write features.fea. This method requires a
|
|
|
|
features string as an argument.
|
|
|
|
"""
|
|
|
|
if self._formatVersion == 1:
|
|
|
|
raise UFOLibError("features.fea is not allowed in UFO Format Version 1.")
|
2011-09-29 13:36:45 +00:00
|
|
|
if not isinstance(features, basestring):
|
|
|
|
raise UFOLibError("The features are not text.")
|
2009-02-28 15:47:24 +00:00
|
|
|
self._makeDirectory()
|
|
|
|
path = os.path.join(self._path, FEATURES_FILENAME)
|
|
|
|
writeFileAtomically(features, path)
|
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
# glyph sets & layers
|
|
|
|
|
2011-09-19 01:40:21 +00:00
|
|
|
def _readLayerContents(self):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
2011-09-27 15:28:42 +00:00
|
|
|
Rebuild the layer contents list by checking what glyph sets
|
2011-09-19 01:40:21 +00:00
|
|
|
are available on disk.
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
2011-09-19 01:40:21 +00:00
|
|
|
# read the file on disk
|
|
|
|
path = os.path.join(self._path, LAYERCONTENTS_FILENAME)
|
|
|
|
if not os.path.exists(path):
|
|
|
|
raise UFOLibError("layercontents.plist is missing.")
|
|
|
|
contents = {}
|
|
|
|
if os.path.exists(path):
|
|
|
|
raw = self._readPlist(path)
|
|
|
|
valid, error = layerContentsValidator(raw, self._path)
|
|
|
|
if not valid:
|
|
|
|
raise UFOLibError(error)
|
|
|
|
for entry in raw:
|
|
|
|
layerName, directoryName = entry
|
|
|
|
contents[layerName] = directoryName
|
|
|
|
self.layerContents = contents
|
|
|
|
|
2011-09-30 20:41:24 +00:00
|
|
|
def writeLayerContents(self, layerOrder=None):
|
2011-09-30 20:51:34 +00:00
|
|
|
"""
|
|
|
|
Write the layercontents.plist file. This method *must* be called
|
|
|
|
after all glyph sets have been written.
|
|
|
|
"""
|
2011-09-30 20:41:24 +00:00
|
|
|
newOrder = []
|
|
|
|
for layerName in layerOrder:
|
|
|
|
if layerName is None:
|
|
|
|
layerName = DEFAULT_LAYER_NAME
|
|
|
|
newOrder.append(layerName)
|
|
|
|
layerOrder = newOrder
|
|
|
|
if set(layerOrder) != set(self.layerContents.keys()):
|
|
|
|
raise UFOLibError("The layer order contents does not match the glyph sets that have been created.")
|
2011-09-19 01:40:21 +00:00
|
|
|
self._makeDirectory()
|
|
|
|
path = os.path.join(self._path, LAYERCONTENTS_FILENAME)
|
2011-09-30 20:41:24 +00:00
|
|
|
layerContents = [(layerName, self.layerContents[layerName]) for layerName in layerOrder]
|
2011-09-19 01:40:21 +00:00
|
|
|
self._writePlist(layerContents, path)
|
2008-01-07 17:40:34 +00:00
|
|
|
|
2011-09-26 19:57:10 +00:00
|
|
|
def _findDirectoryForLayerName(self, layerName):
|
|
|
|
foundDirectory = None
|
|
|
|
for existingLayerName, directoryName in self.layerContents.items():
|
|
|
|
if layerName is None and directoryName == DEFAULT_GLYPHS_DIRNAME:
|
|
|
|
foundDirectory = directoryName
|
|
|
|
break
|
|
|
|
elif existingLayerName == layerName:
|
|
|
|
foundDirectory = directoryName
|
|
|
|
break
|
|
|
|
if not foundDirectory:
|
|
|
|
raise UFOLibError("Could not locate a glyph set directory for the layer named %s." % layerName)
|
|
|
|
return foundDirectory
|
|
|
|
|
2011-09-19 01:40:21 +00:00
|
|
|
def getGlyphSet(self, layerName=None, glyphNameToFileNameFunc=None):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
2011-09-26 19:57:10 +00:00
|
|
|
Return the GlyphSet object associated with the
|
2011-09-19 01:40:21 +00:00
|
|
|
appropriate glyph directory in the .ufo.
|
|
|
|
If layerName is None, the default glyph set
|
|
|
|
will be used.
|
|
|
|
"""
|
2011-09-26 20:12:19 +00:00
|
|
|
if layerName is not None and self._formatVersion < 3:
|
|
|
|
raise UFOLibError("Layer names are not supported in UFO %d." % self._formatVersion)
|
2011-09-19 01:40:21 +00:00
|
|
|
# try to find an existing directory
|
2011-09-26 20:12:19 +00:00
|
|
|
foundDirectory = None
|
2011-09-19 01:40:21 +00:00
|
|
|
if layerName is None:
|
|
|
|
for existingLayerName, directory in self.layerContents.items():
|
|
|
|
if directory == DEFAULT_GLYPHS_DIRNAME:
|
|
|
|
foundDirectory = directory
|
|
|
|
layerName = existingLayerName
|
|
|
|
else:
|
|
|
|
foundDirectory = self.layerContents.get(layerName)
|
|
|
|
directory = foundDirectory
|
|
|
|
# make a new directory name
|
|
|
|
if not directory:
|
2011-09-26 19:57:10 +00:00
|
|
|
# use the default if no name is given.
|
2011-09-19 01:40:21 +00:00
|
|
|
# this won't cause an overwrite since the
|
|
|
|
# default would have been found in the
|
2011-09-26 19:57:10 +00:00
|
|
|
# previous search.
|
2011-09-19 01:40:21 +00:00
|
|
|
if layerName is None:
|
|
|
|
layerName = DEFAULT_LAYER_NAME
|
|
|
|
directory = DEFAULT_GLYPHS_DIRNAME
|
|
|
|
else:
|
|
|
|
# not caching this could be slightly expensive,
|
|
|
|
# but caching it will be cumbersome
|
|
|
|
existing = [d.lower() for d in self.layerContents.values()]
|
|
|
|
if not isinstance(layerName, unicode):
|
|
|
|
try:
|
|
|
|
layerName = unicode(layerName)
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
raise UFOLibError("The specified layer name is not a Unicode string.")
|
2011-09-27 15:28:42 +00:00
|
|
|
directory = userNameToFileName(layerName, existing=existing, prefix="glyphs.")
|
2011-09-19 01:40:21 +00:00
|
|
|
# make the directory
|
|
|
|
path = os.path.join(self._path, directory)
|
|
|
|
if not os.path.exists(path):
|
|
|
|
self._makeDirectory(subDirectory=directory)
|
2011-09-30 20:41:24 +00:00
|
|
|
# store the mapping
|
2011-09-19 01:40:21 +00:00
|
|
|
self.layerContents[layerName] = directory
|
|
|
|
# load the glyph set
|
|
|
|
return GlyphSet(path, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
|
|
|
|
|
2011-09-26 19:57:10 +00:00
|
|
|
def renameGlyphSet(self, layerName, newLayerName):
|
|
|
|
"""
|
|
|
|
Rename a glyph set.
|
|
|
|
|
|
|
|
Note: if a GlyphSet object has already been retrieved for
|
|
|
|
layerName, it is up to the caller to inform that object that
|
|
|
|
the directory it represents has changed.
|
|
|
|
"""
|
2011-09-26 20:12:19 +00:00
|
|
|
if layerName is not None and self._formatVersion < 3:
|
|
|
|
raise UFOLibError("Renaming a glyph set is not allowed in UFO %d." % self._formatVersion)
|
2011-09-26 19:57:10 +00:00
|
|
|
# make sure the new layer name doesn't already exist
|
|
|
|
if newLayerName is None:
|
|
|
|
newLayerName = DEFAULT_LAYER_NAME
|
|
|
|
if newLayerName in self.layerContents:
|
|
|
|
raise UFOLibError("A layer named %s already exists." % newLayerName)
|
|
|
|
# get the paths
|
2011-09-27 15:28:42 +00:00
|
|
|
oldDirectory = self._findDirectoryForLayerName(layerName)
|
2011-09-28 01:02:21 +00:00
|
|
|
existing = [name.lower() for name in self.layerContents.values()]
|
|
|
|
newDirectory = userNameToFileName(newLayerName, existing=existing, prefix="glyphs.")
|
2011-09-26 19:57:10 +00:00
|
|
|
# update the internal mapping
|
|
|
|
del self.layerContents[layerName]
|
2011-09-27 15:28:42 +00:00
|
|
|
self.layerContents[newLayerName] = newDirectory
|
2011-09-26 19:57:10 +00:00
|
|
|
# do the file system copy
|
|
|
|
oldDirectory = os.path.join(self._path, oldDirectory)
|
|
|
|
newDirectory = os.path.join(self._path, newDirectory)
|
2011-09-27 15:28:42 +00:00
|
|
|
shutil.move(oldDirectory, newDirectory)
|
2011-09-26 19:57:10 +00:00
|
|
|
|
2011-09-19 01:40:21 +00:00
|
|
|
def deleteGlyphSet(self, layerName):
|
|
|
|
"""
|
|
|
|
Remove the glyph set matching layerName.
|
|
|
|
"""
|
2011-09-26 20:12:19 +00:00
|
|
|
if layerName is not None and self._formatVersion < 3:
|
|
|
|
raise UFOLibError("Deleting a glyph set is not allowed in UFO %d." % self._formatVersion)
|
2011-09-27 15:28:42 +00:00
|
|
|
foundDirectory = self._findDirectoryForLayerName(layerName)
|
2011-09-19 01:40:21 +00:00
|
|
|
self._removeFileForPath(foundDirectory)
|
|
|
|
del self.layerContents[layerName]
|
2009-02-28 15:47:24 +00:00
|
|
|
|
|
|
|
# ----------------
|
|
|
|
# Helper Functions
|
|
|
|
# ----------------
|
|
|
|
|
|
|
|
def makeUFOPath(path):
|
|
|
|
"""
|
|
|
|
Return a .ufo pathname.
|
|
|
|
|
|
|
|
>>> makeUFOPath("/directory/something.ext")
|
|
|
|
'/directory/something.ufo'
|
|
|
|
>>> makeUFOPath("/directory/something.another.thing.ext")
|
|
|
|
'/directory/something.another.thing.ufo'
|
|
|
|
"""
|
|
|
|
dir, name = os.path.split(path)
|
|
|
|
name = ".".join([".".join(name.split(".")[:-1]), "ufo"])
|
|
|
|
return os.path.join(dir, name)
|
|
|
|
|
|
|
|
def writePlistAtomically(obj, path):
|
|
|
|
"""
|
|
|
|
Write a plist for "obj" to "path". Do this sort of atomically,
|
|
|
|
making it harder to cause corrupt files, for example when writePlist
|
|
|
|
encounters an error halfway during write. This also checks to see
|
|
|
|
if text matches the text that is already in the file at path.
|
|
|
|
If so, the file is not rewritten so that the modification date
|
|
|
|
is preserved.
|
|
|
|
"""
|
|
|
|
f = StringIO()
|
|
|
|
writePlist(obj, f)
|
|
|
|
data = f.getvalue()
|
|
|
|
writeFileAtomically(data, path)
|
|
|
|
|
2011-09-12 17:49:34 +00:00
|
|
|
def writeFileAtomically(text, path, encoding=None):
|
|
|
|
"""
|
|
|
|
Write text into a file at path. Do this sort of atomically
|
2009-02-28 15:47:24 +00:00
|
|
|
making it harder to cause corrupt files. This also checks to see
|
|
|
|
if text matches the text that is already in the file at path.
|
|
|
|
If so, the file is not rewritten so that the modification date
|
2011-09-12 17:49:34 +00:00
|
|
|
is preserved. An encoding may be passed if needed.
|
|
|
|
"""
|
2009-02-28 15:47:24 +00:00
|
|
|
if os.path.exists(path):
|
2011-09-12 17:49:34 +00:00
|
|
|
f = codecs.open(path, READ_MODE, encoding=encoding)
|
2009-02-28 15:47:24 +00:00
|
|
|
oldText = f.read()
|
|
|
|
f.close()
|
|
|
|
if text == oldText:
|
|
|
|
return
|
|
|
|
# if the text is empty, remove the existing file
|
|
|
|
if not text:
|
|
|
|
os.remove(path)
|
|
|
|
if text:
|
2011-09-12 17:49:34 +00:00
|
|
|
f = codecs.open(path, WRITE_MODE, encoding=encoding)
|
2009-02-28 15:47:24 +00:00
|
|
|
f.write(text)
|
|
|
|
f.close()
|
|
|
|
|
2011-09-19 01:40:21 +00:00
|
|
|
# ---------------------------
|
|
|
|
# Format Conversion Functions
|
|
|
|
# ---------------------------
|
|
|
|
|
|
|
|
def convertUFOFormatVersion1ToFormatVersion2(inPath, outPath=None):
|
|
|
|
"""
|
|
|
|
Function for converting a version format 1 UFO
|
|
|
|
to version format 2. inPath should be a path
|
|
|
|
to a UFO. outPath is the path where the new UFO
|
|
|
|
should be written. If outPath is not given, the
|
|
|
|
inPath will be used and, therefore, the UFO will
|
|
|
|
be converted in place. Otherwise, if outPath is
|
|
|
|
specified, nothing must exist at that path.
|
|
|
|
"""
|
|
|
|
if outPath is None:
|
|
|
|
outPath = inPath
|
|
|
|
if inPath != outPath and os.path.exists(outPath):
|
|
|
|
raise UFOLibError("A file already exists at %s." % outPath)
|
|
|
|
# use a reader for loading most of the data
|
|
|
|
reader = UFOReader(inPath)
|
|
|
|
if reader.formatVersion == 2:
|
|
|
|
raise UFOLibError("The UFO at %s is already format version 2." % inPath)
|
|
|
|
groups = reader.readGroups()
|
|
|
|
kerning = reader.readKerning()
|
|
|
|
libData = reader.readLib()
|
|
|
|
# read the info data manually and convert
|
|
|
|
infoPath = os.path.join(inPath, FONTINFO_FILENAME)
|
|
|
|
if not os.path.exists(infoPath):
|
|
|
|
infoData = {}
|
|
|
|
else:
|
|
|
|
infoData = readPlist(infoPath)
|
|
|
|
infoData = _convertFontInfoDataVersion1ToVersion2(infoData)
|
|
|
|
# if the paths are the same, only need to change the
|
|
|
|
# fontinfo and meta info files.
|
|
|
|
infoPath = os.path.join(outPath, FONTINFO_FILENAME)
|
|
|
|
if inPath == outPath:
|
|
|
|
metaInfoPath = os.path.join(inPath, METAINFO_FILENAME)
|
|
|
|
metaInfo = dict(
|
|
|
|
creator="org.robofab.ufoLib",
|
|
|
|
formatVersion=2
|
|
|
|
)
|
|
|
|
writePlistAtomically(metaInfo, metaInfoPath)
|
|
|
|
writePlistAtomically(infoData, infoPath)
|
|
|
|
# otherwise write everything.
|
|
|
|
else:
|
|
|
|
writer = UFOWriter(outPath, formatVersion=2)
|
|
|
|
writer.writeGroups(groups)
|
|
|
|
writer.writeKerning(kerning)
|
|
|
|
writer.writeLib(libData)
|
|
|
|
# write the info manually
|
|
|
|
writePlistAtomically(infoData, infoPath)
|
|
|
|
# copy the glyph tree
|
|
|
|
inGlyphs = os.path.join(inPath, DEFAULT_GLYPHS_DIRNAME)
|
|
|
|
outGlyphs = os.path.join(outPath, DEFAULT_GLYPHS_DIRNAME)
|
|
|
|
if os.path.exists(inGlyphs):
|
|
|
|
shutil.copytree(inGlyphs, outGlyphs)
|
|
|
|
|
|
|
|
def convertUFOFormatVersion2ToFormatVersion1(inPath, outPath=None):
|
|
|
|
"""
|
|
|
|
Function for converting a version format 2 UFO
|
|
|
|
to version format 1. inPath should be a path
|
|
|
|
to a UFO. outPath is the path where the new UFO
|
|
|
|
should be written. If outPath is not given, the
|
|
|
|
inPath will be used and, therefore, the UFO will
|
|
|
|
be converted in place. Otherwise, if outPath is
|
|
|
|
specified, nothing must exist at that path.
|
|
|
|
"""
|
|
|
|
if outPath is None:
|
|
|
|
outPath = inPath
|
|
|
|
if inPath != outPath and os.path.exists(outPath):
|
|
|
|
raise UFOLibError("A file already exists at %s." % outPath)
|
|
|
|
# use a reader for loading most of the data
|
|
|
|
reader = UFOReader(inPath)
|
|
|
|
if reader.formatVersion == 1:
|
|
|
|
raise UFOLibError("The UFO at %s is already format version 1." % inPath)
|
|
|
|
groups = reader.readGroups()
|
|
|
|
kerning = reader.readKerning()
|
|
|
|
libData = reader.readLib()
|
|
|
|
# read the info data manually and convert
|
|
|
|
infoPath = os.path.join(inPath, FONTINFO_FILENAME)
|
|
|
|
if not os.path.exists(infoPath):
|
|
|
|
infoData = {}
|
|
|
|
else:
|
|
|
|
infoData = readPlist(infoPath)
|
|
|
|
infoData = _convertFontInfoDataVersion2ToVersion1(infoData)
|
|
|
|
# if the paths are the same, only need to change the
|
|
|
|
# fontinfo, metainfo and feature files.
|
|
|
|
infoPath = os.path.join(outPath, FONTINFO_FILENAME)
|
|
|
|
if inPath == outPath:
|
|
|
|
metaInfoPath = os.path.join(inPath, METAINFO_FILENAME)
|
|
|
|
metaInfo = dict(
|
|
|
|
creator="org.robofab.ufoLib",
|
|
|
|
formatVersion=1
|
|
|
|
)
|
|
|
|
writePlistAtomically(metaInfo, metaInfoPath)
|
|
|
|
writePlistAtomically(infoData, infoPath)
|
|
|
|
featuresPath = os.path.join(inPath, FEATURES_FILENAME)
|
|
|
|
if os.path.exists(featuresPath):
|
|
|
|
os.remove(featuresPath)
|
|
|
|
# otherwise write everything.
|
|
|
|
else:
|
|
|
|
writer = UFOWriter(outPath, formatVersion=1)
|
|
|
|
writer.writeGroups(groups)
|
|
|
|
writer.writeKerning(kerning)
|
|
|
|
writer.writeLib(libData)
|
|
|
|
# write the info manually
|
|
|
|
writePlistAtomically(infoData, infoPath)
|
|
|
|
# copy the glyph tree
|
|
|
|
inGlyphs = os.path.join(inPath, DEFAULT_GLYPHS_DIRNAME)
|
|
|
|
outGlyphs = os.path.join(outPath, DEFAULT_GLYPHS_DIRNAME)
|
|
|
|
if os.path.exists(inGlyphs):
|
|
|
|
shutil.copytree(inGlyphs, outGlyphs)
|
|
|
|
|
2009-02-28 15:47:24 +00:00
|
|
|
# ----------------------
|
|
|
|
# fontinfo.plist Support
|
|
|
|
# ----------------------
|
|
|
|
|
2011-09-12 20:56:12 +00:00
|
|
|
# Version Validators
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-12 20:56:12 +00:00
|
|
|
# There is no version 1 validator and there shouldn't be.
|
|
|
|
# The version 1 spec was very loose and there were numerous
|
|
|
|
# cases of invalid values.
|
2009-02-28 15:47:24 +00:00
|
|
|
|
|
|
|
def validateFontInfoVersion2ValueForAttribute(attr, value):
|
|
|
|
"""
|
|
|
|
This performs very basic validation of the value for attribute
|
2011-09-14 21:13:27 +00:00
|
|
|
following the UFO 2 fontinfo.plist specification. The results
|
2009-02-28 15:47:24 +00:00
|
|
|
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.
|
|
|
|
"""
|
2011-09-18 12:24:29 +00:00
|
|
|
dataValidationDict = fontInfoAttributesVersion2ValueData[attr]
|
2009-02-28 15:47:24 +00:00
|
|
|
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:
|
2011-09-28 13:42:09 +00:00
|
|
|
if validator == genericTypeValidator:
|
2009-02-28 15:47:24 +00:00
|
|
|
isValidValue = validator(value, valueType)
|
|
|
|
else:
|
|
|
|
isValidValue = validator(value)
|
|
|
|
return isValidValue
|
|
|
|
|
2011-09-18 12:24:29 +00:00
|
|
|
def validateInfoVersion2Data(infoData):
|
|
|
|
"""
|
|
|
|
This performs very basic validation of the value for infoData
|
|
|
|
following the UFO 2 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 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.
|
|
|
|
"""
|
2009-02-28 15:47:24 +00:00
|
|
|
validInfoData = {}
|
|
|
|
for attr, value in infoData.items():
|
|
|
|
isValidValue = validateFontInfoVersion2ValueForAttribute(attr, value)
|
|
|
|
if not isValidValue:
|
|
|
|
raise UFOLibError("Invalid value for attribute %s (%s)." % (attr, repr(value)))
|
|
|
|
else:
|
|
|
|
validInfoData[attr] = value
|
2011-09-28 13:59:47 +00:00
|
|
|
return validInfoData
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-14 21:13:27 +00:00
|
|
|
def validateFontInfoVersion3ValueForAttribute(attr, value):
|
|
|
|
"""
|
|
|
|
This performs very basic validation of the value for attribute
|
2011-09-18 12:24:29 +00:00
|
|
|
following the UFO 3 fontinfo.plist specification. The results
|
2011-09-14 21:13:27 +00:00
|
|
|
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.
|
|
|
|
"""
|
2011-09-18 12:24:29 +00:00
|
|
|
dataValidationDict = fontInfoAttributesVersion3ValueData[attr]
|
2011-09-14 21:13:27 +00:00
|
|
|
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:
|
2011-09-28 13:42:09 +00:00
|
|
|
if validator == genericTypeValidator:
|
2011-09-14 21:13:27 +00:00
|
|
|
isValidValue = validator(value, valueType)
|
|
|
|
else:
|
|
|
|
isValidValue = validator(value)
|
|
|
|
return isValidValue
|
|
|
|
|
2011-09-18 12:24:29 +00:00
|
|
|
def validateInfoVersion3Data(infoData):
|
|
|
|
"""
|
|
|
|
This performs very basic validation of the value for infoData
|
|
|
|
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 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.
|
|
|
|
"""
|
2011-09-14 21:13:27 +00:00
|
|
|
validInfoData = {}
|
|
|
|
for attr, value in infoData.items():
|
|
|
|
isValidValue = validateFontInfoVersion3ValueForAttribute(attr, value)
|
|
|
|
if not isValidValue:
|
|
|
|
raise UFOLibError("Invalid value for attribute %s (%s)." % (attr, repr(value)))
|
|
|
|
else:
|
|
|
|
validInfoData[attr] = value
|
2011-09-28 13:59:47 +00:00
|
|
|
return validInfoData
|
2011-09-14 21:13:27 +00:00
|
|
|
|
2011-09-12 20:56:12 +00:00
|
|
|
# Value Options
|
|
|
|
|
2011-09-18 12:24:29 +00:00
|
|
|
fontInfoOpenTypeHeadFlagsOptions = range(0, 14)
|
|
|
|
fontInfoOpenTypeOS2SelectionOptions = [1, 2, 3, 4]
|
|
|
|
fontInfoOpenTypeOS2UnicodeRangesOptions = range(0, 128)
|
|
|
|
fontInfoOpenTypeOS2CodePageRangesOptions = range(0, 64)
|
|
|
|
fontInfoOpenTypeOS2TypeOptions = [0, 1, 2, 3, 8, 9]
|
2011-09-12 20:56:12 +00:00
|
|
|
|
|
|
|
# Version Attribute Definitions
|
2009-02-28 15:47:24 +00:00
|
|
|
# This defines the attributes, types and, in some
|
|
|
|
# cases the possible values, that can exist is
|
|
|
|
# fontinfo.plist.
|
|
|
|
|
2011-09-12 20:56:12 +00:00
|
|
|
fontInfoAttributesVersion1 = set([
|
|
|
|
"familyName",
|
|
|
|
"styleName",
|
|
|
|
"fullName",
|
|
|
|
"fontName",
|
|
|
|
"menuName",
|
|
|
|
"fontStyle",
|
|
|
|
"note",
|
|
|
|
"versionMajor",
|
|
|
|
"versionMinor",
|
|
|
|
"year",
|
|
|
|
"copyright",
|
|
|
|
"notice",
|
|
|
|
"trademark",
|
|
|
|
"license",
|
|
|
|
"licenseURL",
|
|
|
|
"createdBy",
|
|
|
|
"designer",
|
|
|
|
"designerURL",
|
|
|
|
"vendorURL",
|
|
|
|
"unitsPerEm",
|
|
|
|
"ascender",
|
|
|
|
"descender",
|
|
|
|
"capHeight",
|
|
|
|
"xHeight",
|
|
|
|
"defaultWidth",
|
|
|
|
"slantAngle",
|
|
|
|
"italicAngle",
|
|
|
|
"widthName",
|
|
|
|
"weightName",
|
|
|
|
"weightValue",
|
|
|
|
"fondName",
|
|
|
|
"otFamilyName",
|
|
|
|
"otStyleName",
|
|
|
|
"otMacName",
|
|
|
|
"msCharSet",
|
|
|
|
"fondID",
|
|
|
|
"uniqueID",
|
|
|
|
"ttVendor",
|
|
|
|
"ttUniqueID",
|
|
|
|
"ttVersion",
|
|
|
|
])
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-18 12:24:29 +00:00
|
|
|
fontInfoAttributesVersion2ValueData = {
|
2011-09-16 11:23:44 +00:00
|
|
|
"familyName" : dict(type=basestring),
|
|
|
|
"styleName" : dict(type=basestring),
|
|
|
|
"styleMapFamilyName" : dict(type=basestring),
|
2011-09-18 12:24:29 +00:00
|
|
|
"styleMapStyleName" : dict(type=basestring, valueValidator=fontInfoStyleMapStyleNameValidator),
|
2009-02-28 15:47:24 +00:00
|
|
|
"versionMajor" : dict(type=int),
|
|
|
|
"versionMinor" : dict(type=int),
|
|
|
|
"year" : dict(type=int),
|
2011-09-16 11:23:44 +00:00
|
|
|
"copyright" : dict(type=basestring),
|
|
|
|
"trademark" : dict(type=basestring),
|
2009-02-28 15:47:24 +00:00
|
|
|
"unitsPerEm" : dict(type=(int, float)),
|
|
|
|
"descender" : dict(type=(int, float)),
|
|
|
|
"xHeight" : dict(type=(int, float)),
|
|
|
|
"capHeight" : dict(type=(int, float)),
|
|
|
|
"ascender" : dict(type=(int, float)),
|
|
|
|
"italicAngle" : dict(type=(float, int)),
|
2011-09-16 11:23:44 +00:00
|
|
|
"note" : dict(type=basestring),
|
2011-09-18 12:24:29 +00:00
|
|
|
"openTypeHeadCreated" : dict(type=basestring, valueValidator=fontInfoOpenTypeHeadCreatedValidator),
|
2009-02-28 15:47:24 +00:00
|
|
|
"openTypeHeadLowestRecPPEM" : dict(type=(int, float)),
|
2011-09-28 13:42:09 +00:00
|
|
|
"openTypeHeadFlags" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeHeadFlagsOptions),
|
2009-02-28 15:47:24 +00:00
|
|
|
"openTypeHheaAscender" : dict(type=(int, float)),
|
|
|
|
"openTypeHheaDescender" : dict(type=(int, float)),
|
|
|
|
"openTypeHheaLineGap" : dict(type=(int, float)),
|
|
|
|
"openTypeHheaCaretSlopeRise" : dict(type=int),
|
|
|
|
"openTypeHheaCaretSlopeRun" : dict(type=int),
|
|
|
|
"openTypeHheaCaretOffset" : dict(type=(int, float)),
|
2011-09-16 11:23:44 +00:00
|
|
|
"openTypeNameDesigner" : dict(type=basestring),
|
|
|
|
"openTypeNameDesignerURL" : dict(type=basestring),
|
|
|
|
"openTypeNameManufacturer" : dict(type=basestring),
|
|
|
|
"openTypeNameManufacturerURL" : dict(type=basestring),
|
|
|
|
"openTypeNameLicense" : dict(type=basestring),
|
|
|
|
"openTypeNameLicenseURL" : dict(type=basestring),
|
|
|
|
"openTypeNameVersion" : dict(type=basestring),
|
|
|
|
"openTypeNameUniqueID" : dict(type=basestring),
|
|
|
|
"openTypeNameDescription" : dict(type=basestring),
|
|
|
|
"openTypeNamePreferredFamilyName" : dict(type=basestring),
|
|
|
|
"openTypeNamePreferredSubfamilyName" : dict(type=basestring),
|
|
|
|
"openTypeNameCompatibleFullName" : dict(type=basestring),
|
|
|
|
"openTypeNameSampleText" : dict(type=basestring),
|
|
|
|
"openTypeNameWWSFamilyName" : dict(type=basestring),
|
|
|
|
"openTypeNameWWSSubfamilyName" : dict(type=basestring),
|
2011-09-18 12:24:29 +00:00
|
|
|
"openTypeOS2WidthClass" : dict(type=int, valueValidator=fontInfoOpenTypeOS2WidthClassValidator),
|
|
|
|
"openTypeOS2WeightClass" : dict(type=int, valueValidator=fontInfoOpenTypeOS2WeightClassValidator),
|
2011-09-28 13:42:09 +00:00
|
|
|
"openTypeOS2Selection" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2SelectionOptions),
|
2011-09-16 11:23:44 +00:00
|
|
|
"openTypeOS2VendorID" : dict(type=basestring),
|
2011-09-18 12:24:29 +00:00
|
|
|
"openTypeOS2Panose" : dict(type="integerList", valueValidator=fontInfoVersion2OpenTypeOS2PanoseValidator),
|
|
|
|
"openTypeOS2FamilyClass" : dict(type="integerList", valueValidator=fontInfoOpenTypeOS2FamilyClassValidator),
|
2011-09-28 13:42:09 +00:00
|
|
|
"openTypeOS2UnicodeRanges" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2UnicodeRangesOptions),
|
|
|
|
"openTypeOS2CodePageRanges" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2CodePageRangesOptions),
|
2009-02-28 15:47:24 +00:00
|
|
|
"openTypeOS2TypoAscender" : dict(type=(int, float)),
|
|
|
|
"openTypeOS2TypoDescender" : dict(type=(int, float)),
|
|
|
|
"openTypeOS2TypoLineGap" : dict(type=(int, float)),
|
|
|
|
"openTypeOS2WinAscent" : dict(type=(int, float)),
|
|
|
|
"openTypeOS2WinDescent" : dict(type=(int, float)),
|
2011-09-28 13:42:09 +00:00
|
|
|
"openTypeOS2Type" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2TypeOptions),
|
2009-02-28 15:47:24 +00:00
|
|
|
"openTypeOS2SubscriptXSize" : dict(type=(int, float)),
|
|
|
|
"openTypeOS2SubscriptYSize" : dict(type=(int, float)),
|
|
|
|
"openTypeOS2SubscriptXOffset" : dict(type=(int, float)),
|
|
|
|
"openTypeOS2SubscriptYOffset" : dict(type=(int, float)),
|
|
|
|
"openTypeOS2SuperscriptXSize" : dict(type=(int, float)),
|
|
|
|
"openTypeOS2SuperscriptYSize" : dict(type=(int, float)),
|
|
|
|
"openTypeOS2SuperscriptXOffset" : dict(type=(int, float)),
|
|
|
|
"openTypeOS2SuperscriptYOffset" : dict(type=(int, float)),
|
|
|
|
"openTypeOS2StrikeoutSize" : dict(type=(int, float)),
|
|
|
|
"openTypeOS2StrikeoutPosition" : dict(type=(int, float)),
|
|
|
|
"openTypeVheaVertTypoAscender" : dict(type=(int, float)),
|
|
|
|
"openTypeVheaVertTypoDescender" : dict(type=(int, float)),
|
|
|
|
"openTypeVheaVertTypoLineGap" : dict(type=(int, float)),
|
|
|
|
"openTypeVheaCaretSlopeRise" : dict(type=int),
|
|
|
|
"openTypeVheaCaretSlopeRun" : dict(type=int),
|
|
|
|
"openTypeVheaCaretOffset" : dict(type=(int, float)),
|
2011-09-16 11:23:44 +00:00
|
|
|
"postscriptFontName" : dict(type=basestring),
|
|
|
|
"postscriptFullName" : dict(type=basestring),
|
2009-02-28 15:47:24 +00:00
|
|
|
"postscriptSlantAngle" : dict(type=(float, int)),
|
|
|
|
"postscriptUniqueID" : dict(type=int),
|
|
|
|
"postscriptUnderlineThickness" : dict(type=(int, float)),
|
|
|
|
"postscriptUnderlinePosition" : dict(type=(int, float)),
|
|
|
|
"postscriptIsFixedPitch" : dict(type=bool),
|
2011-09-18 12:24:29 +00:00
|
|
|
"postscriptBlueValues" : dict(type="integerList", valueValidator=fontInfoPostscriptBluesValidator),
|
|
|
|
"postscriptOtherBlues" : dict(type="integerList", valueValidator=fontInfoPostscriptOtherBluesValidator),
|
|
|
|
"postscriptFamilyBlues" : dict(type="integerList", valueValidator=fontInfoPostscriptBluesValidator),
|
|
|
|
"postscriptFamilyOtherBlues" : dict(type="integerList", valueValidator=fontInfoPostscriptOtherBluesValidator),
|
|
|
|
"postscriptStemSnapH" : dict(type="integerList", valueValidator=fontInfoPostscriptStemsValidator),
|
|
|
|
"postscriptStemSnapV" : dict(type="integerList", valueValidator=fontInfoPostscriptStemsValidator),
|
2009-02-28 15:47:24 +00:00
|
|
|
"postscriptBlueFuzz" : dict(type=(int, float)),
|
|
|
|
"postscriptBlueShift" : dict(type=(int, float)),
|
|
|
|
"postscriptBlueScale" : dict(type=(float, int)),
|
|
|
|
"postscriptForceBold" : dict(type=bool),
|
|
|
|
"postscriptDefaultWidthX" : dict(type=(int, float)),
|
|
|
|
"postscriptNominalWidthX" : dict(type=(int, float)),
|
2011-09-16 11:23:44 +00:00
|
|
|
"postscriptWeightName" : dict(type=basestring),
|
|
|
|
"postscriptDefaultCharacter" : dict(type=basestring),
|
2011-09-18 12:24:29 +00:00
|
|
|
"postscriptWindowsCharacterSet" : dict(type=int, valueValidator=fontInfoPostscriptWindowsCharacterSetValidator),
|
2009-02-28 15:47:24 +00:00
|
|
|
"macintoshFONDFamilyID" : dict(type=int),
|
2011-09-16 11:23:44 +00:00
|
|
|
"macintoshFONDName" : dict(type=basestring),
|
2009-02-28 15:47:24 +00:00
|
|
|
}
|
2011-09-18 12:24:29 +00:00
|
|
|
fontInfoAttributesVersion2 = set(fontInfoAttributesVersion2ValueData.keys())
|
|
|
|
|
|
|
|
fontInfoAttributesVersion3ValueData = deepcopy(fontInfoAttributesVersion2ValueData)
|
|
|
|
fontInfoAttributesVersion3ValueData.update({
|
2011-09-28 13:42:09 +00:00
|
|
|
"versionMinor" : dict(type=int, valueValidator=genericNonNegativeIntValidator),
|
|
|
|
"unitsPerEm" : dict(type=(int, float), valueValidator=genericNonNegativeNumberValidator),
|
|
|
|
"openTypeHeadLowestRecPPEM" : dict(type=(int, float), valueValidator=genericNonNegativeNumberValidator),
|
2011-09-18 12:24:29 +00:00
|
|
|
"openTypeOS2Panose" : dict(type="integerList", valueValidator=fontInfoVersion3OpenTypeOS2PanoseValidator),
|
2011-09-28 13:42:09 +00:00
|
|
|
"openTypeOS2WinAscent" : dict(type=(int, float), valueValidator=genericNonNegativeNumberValidator),
|
|
|
|
"openTypeOS2WinDescent" : dict(type=(int, float), valueValidator=genericNonNegativeNumberValidator),
|
2011-09-18 12:24:29 +00:00
|
|
|
"openTypeGaspRangeRecords" : dict(type="dictList", valueValidator=fontInfoOpenTypeGaspRangeRecordsValidator),
|
|
|
|
"openTypeNameRecords" : dict(type="dictList", valueValidator=fontInfoOpenTypeNameRecordsValidator),
|
2011-09-28 13:42:09 +00:00
|
|
|
"woffMajorVersion" : dict(type=int, valueValidator=genericNonNegativeIntValidator),
|
|
|
|
"woffMinorVersion" : dict(type=int, valueValidator=genericNonNegativeIntValidator),
|
2011-09-18 12:24:29 +00:00
|
|
|
"woffMetadataUniqueID" : dict(type=dict, valueValidator=fontInfoWOFFMetadataUniqueIDValidator),
|
|
|
|
"woffMetadataVendor" : dict(type=dict, valueValidator=fontInfoWOFFMetadataVendorValidator),
|
|
|
|
"woffMetadataCredits" : dict(type=dict, valueValidator=fontInfoWOFFMetadataCreditsValidator),
|
|
|
|
"woffMetadataDescription" : dict(type=dict, valueValidator=fontInfoWOFFMetadataDescriptionValidator),
|
|
|
|
"woffMetadataLicense" : dict(type=dict, valueValidator=fontInfoWOFFMetadataLicenseValidator),
|
|
|
|
"woffMetadataCopyright" : dict(type=dict, valueValidator=fontInfoWOFFMetadataCopyrightValidator),
|
|
|
|
"woffMetadataTrademark" : dict(type=dict, valueValidator=fontInfoWOFFMetadataTrademarkValidator),
|
|
|
|
"woffMetadataLicensee" : dict(type=dict, valueValidator=fontInfoWOFFMetadataLicenseeValidator),
|
|
|
|
"woffMetadataExtensions" : dict(type=list, valueValidator=fontInfoWOFFMetadataExtensionsValidator),
|
2011-09-28 13:51:52 +00:00
|
|
|
"guidelines" : dict(type=list, valueValidator=guidelinesValidator)
|
2011-09-12 22:15:15 +00:00
|
|
|
})
|
2011-10-04 01:07:22 +00:00
|
|
|
fontInfoAttributesVersion3 = set(fontInfoAttributesVersion3ValueData.keys())
|
2011-09-12 22:15:15 +00:00
|
|
|
|
2009-02-28 15:47:24 +00:00
|
|
|
# insert the type validator for all attrs that
|
|
|
|
# have no defined validator.
|
2011-09-18 12:24:29 +00:00
|
|
|
for attr, dataDict in fontInfoAttributesVersion2ValueData.items():
|
2009-02-28 15:47:24 +00:00
|
|
|
if "valueValidator" not in dataDict:
|
2011-09-28 13:42:09 +00:00
|
|
|
dataDict["valueValidator"] = genericTypeValidator
|
2009-02-28 15:47:24 +00:00
|
|
|
|
2011-09-18 12:24:29 +00:00
|
|
|
for attr, dataDict in fontInfoAttributesVersion3ValueData.items():
|
2011-09-14 21:13:27 +00:00
|
|
|
if "valueValidator" not in dataDict:
|
2011-09-28 13:42:09 +00:00
|
|
|
dataDict["valueValidator"] = genericTypeValidator
|
2011-09-14 21:13:27 +00:00
|
|
|
|
2009-02-28 15:47:24 +00:00
|
|
|
# Version Conversion Support
|
|
|
|
# These are used from converting from version 1
|
|
|
|
# to version 2 or vice-versa.
|
|
|
|
|
|
|
|
def _flipDict(d):
|
|
|
|
flipped = {}
|
|
|
|
for key, value in d.items():
|
|
|
|
flipped[value] = key
|
|
|
|
return flipped
|
|
|
|
|
2011-09-18 12:24:29 +00:00
|
|
|
fontInfoAttributesVersion1To2 = {
|
2009-02-28 15:47:24 +00:00
|
|
|
"menuName" : "styleMapFamilyName",
|
|
|
|
"designer" : "openTypeNameDesigner",
|
|
|
|
"designerURL" : "openTypeNameDesignerURL",
|
|
|
|
"createdBy" : "openTypeNameManufacturer",
|
|
|
|
"vendorURL" : "openTypeNameManufacturerURL",
|
|
|
|
"license" : "openTypeNameLicense",
|
|
|
|
"licenseURL" : "openTypeNameLicenseURL",
|
|
|
|
"ttVersion" : "openTypeNameVersion",
|
|
|
|
"ttUniqueID" : "openTypeNameUniqueID",
|
|
|
|
"notice" : "openTypeNameDescription",
|
|
|
|
"otFamilyName" : "openTypeNamePreferredFamilyName",
|
|
|
|
"otStyleName" : "openTypeNamePreferredSubfamilyName",
|
|
|
|
"otMacName" : "openTypeNameCompatibleFullName",
|
|
|
|
"weightName" : "postscriptWeightName",
|
|
|
|
"weightValue" : "openTypeOS2WeightClass",
|
|
|
|
"ttVendor" : "openTypeOS2VendorID",
|
|
|
|
"uniqueID" : "postscriptUniqueID",
|
|
|
|
"fontName" : "postscriptFontName",
|
|
|
|
"fondID" : "macintoshFONDFamilyID",
|
|
|
|
"fondName" : "macintoshFONDName",
|
|
|
|
"defaultWidth" : "postscriptDefaultWidthX",
|
|
|
|
"slantAngle" : "postscriptSlantAngle",
|
|
|
|
"fullName" : "postscriptFullName",
|
|
|
|
# require special value conversion
|
|
|
|
"fontStyle" : "styleMapStyleName",
|
|
|
|
"widthName" : "openTypeOS2WidthClass",
|
|
|
|
"msCharSet" : "postscriptWindowsCharacterSet"
|
|
|
|
}
|
2011-09-18 12:24:29 +00:00
|
|
|
fontInfoAttributesVersion2To1 = _flipDict(fontInfoAttributesVersion1To2)
|
|
|
|
deprecatedFontInfoAttributesVersion2 = set(fontInfoAttributesVersion1To2.keys())
|
2009-02-28 15:47:24 +00:00
|
|
|
|
|
|
|
_fontStyle1To2 = {
|
|
|
|
64 : "regular",
|
|
|
|
1 : "italic",
|
|
|
|
32 : "bold",
|
|
|
|
33 : "bold italic"
|
|
|
|
}
|
|
|
|
_fontStyle2To1 = _flipDict(_fontStyle1To2)
|
|
|
|
# Some UFO 1 files have 0
|
|
|
|
_fontStyle1To2[0] = "regular"
|
|
|
|
|
|
|
|
_widthName1To2 = {
|
|
|
|
"Ultra-condensed" : 1,
|
|
|
|
"Extra-condensed" : 2,
|
|
|
|
"Condensed" : 3,
|
|
|
|
"Semi-condensed" : 4,
|
|
|
|
"Medium (normal)" : 5,
|
|
|
|
"Semi-expanded" : 6,
|
|
|
|
"Expanded" : 7,
|
|
|
|
"Extra-expanded" : 8,
|
|
|
|
"Ultra-expanded" : 9
|
|
|
|
}
|
|
|
|
_widthName2To1 = _flipDict(_widthName1To2)
|
|
|
|
# FontLab's default width value is "Normal".
|
|
|
|
# Many format version 1 UFOs will have this.
|
|
|
|
_widthName1To2["Normal"] = 5
|
|
|
|
# FontLab has an "All" width value. In UFO 1
|
|
|
|
# move this up to "Normal".
|
|
|
|
_widthName1To2["All"] = 5
|
|
|
|
# "medium" appears in a lot of UFO 1 files.
|
|
|
|
_widthName1To2["medium"] = 5
|
2009-12-03 14:44:41 +00:00
|
|
|
# "Medium" appears in a lot of UFO 1 files.
|
|
|
|
_widthName1To2["Medium"] = 5
|
2009-02-28 15:47:24 +00:00
|
|
|
|
|
|
|
_msCharSet1To2 = {
|
|
|
|
0 : 1,
|
|
|
|
1 : 2,
|
|
|
|
2 : 3,
|
|
|
|
77 : 4,
|
|
|
|
128 : 5,
|
|
|
|
129 : 6,
|
|
|
|
130 : 7,
|
|
|
|
134 : 8,
|
|
|
|
136 : 9,
|
|
|
|
161 : 10,
|
|
|
|
162 : 11,
|
|
|
|
163 : 12,
|
|
|
|
177 : 13,
|
|
|
|
178 : 14,
|
|
|
|
186 : 15,
|
|
|
|
200 : 16,
|
|
|
|
204 : 17,
|
|
|
|
222 : 18,
|
|
|
|
238 : 19,
|
|
|
|
255 : 20
|
|
|
|
}
|
|
|
|
_msCharSet2To1 = _flipDict(_msCharSet1To2)
|
|
|
|
|
|
|
|
def convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value):
|
|
|
|
"""
|
|
|
|
Convert value from version 1 to version 2 format.
|
|
|
|
Returns the new attribute name and the converted value.
|
|
|
|
If the value is None, None will be returned for the new value.
|
|
|
|
"""
|
|
|
|
# convert floats to ints if possible
|
|
|
|
if isinstance(value, float):
|
|
|
|
if int(value) == value:
|
|
|
|
value = int(value)
|
|
|
|
if value is not None:
|
|
|
|
if attr == "fontStyle":
|
|
|
|
v = _fontStyle1To2.get(value)
|
|
|
|
if v is None:
|
|
|
|
raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), attr))
|
|
|
|
value = v
|
|
|
|
elif attr == "widthName":
|
|
|
|
v = _widthName1To2.get(value)
|
|
|
|
if v is None:
|
|
|
|
raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), attr))
|
|
|
|
value = v
|
|
|
|
elif attr == "msCharSet":
|
|
|
|
v = _msCharSet1To2.get(value)
|
|
|
|
if v is None:
|
|
|
|
raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), attr))
|
|
|
|
value = v
|
2011-09-18 12:24:29 +00:00
|
|
|
attr = fontInfoAttributesVersion1To2.get(attr, attr)
|
2009-02-28 15:47:24 +00:00
|
|
|
return attr, value
|
|
|
|
|
|
|
|
def convertFontInfoValueForAttributeFromVersion2ToVersion1(attr, value):
|
|
|
|
"""
|
|
|
|
Convert value from version 2 to version 1 format.
|
|
|
|
Returns the new attribute name and the converted value.
|
|
|
|
If the value is None, None will be returned for the new value.
|
|
|
|
"""
|
|
|
|
if value is not None:
|
|
|
|
if attr == "styleMapStyleName":
|
|
|
|
value = _fontStyle2To1.get(value)
|
|
|
|
elif attr == "openTypeOS2WidthClass":
|
|
|
|
value = _widthName2To1.get(value)
|
|
|
|
elif attr == "postscriptWindowsCharacterSet":
|
|
|
|
value = _msCharSet2To1.get(value)
|
2011-09-18 12:24:29 +00:00
|
|
|
attr = fontInfoAttributesVersion2To1.get(attr, attr)
|
2009-02-28 15:47:24 +00:00
|
|
|
return attr, value
|
|
|
|
|
|
|
|
def _convertFontInfoDataVersion1ToVersion2(data):
|
|
|
|
converted = {}
|
|
|
|
for attr, value in data.items():
|
|
|
|
# FontLab gives -1 for the weightValue
|
|
|
|
# for fonts wil no defined value. Many
|
|
|
|
# format version 1 UFOs will have this.
|
|
|
|
if attr == "weightValue" and value == -1:
|
|
|
|
continue
|
|
|
|
newAttr, newValue = convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value)
|
|
|
|
# skip if the attribute is not part of version 2
|
|
|
|
if newAttr not in fontInfoAttributesVersion2:
|
|
|
|
continue
|
|
|
|
# catch values that can't be converted
|
|
|
|
if value is None:
|
|
|
|
raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), newAttr))
|
|
|
|
# store
|
|
|
|
converted[newAttr] = newValue
|
|
|
|
return converted
|
|
|
|
|
|
|
|
def _convertFontInfoDataVersion2ToVersion1(data):
|
|
|
|
converted = {}
|
|
|
|
for attr, value in data.items():
|
|
|
|
newAttr, newValue = convertFontInfoValueForAttributeFromVersion2ToVersion1(attr, value)
|
|
|
|
# only take attributes that are registered for version 1
|
|
|
|
if newAttr not in fontInfoAttributesVersion1:
|
|
|
|
continue
|
|
|
|
# catch values that can't be converted
|
|
|
|
if value is None:
|
|
|
|
raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), newAttr))
|
|
|
|
# store
|
|
|
|
converted[newAttr] = newValue
|
|
|
|
return converted
|
|
|
|
|
2011-09-14 21:13:27 +00:00
|
|
|
def _convertFontInfoDataVersion3ToVersion2(data):
|
|
|
|
converted = {}
|
|
|
|
for attr, value in data.items():
|
|
|
|
# only take attributes that are registered for version 2
|
|
|
|
if attr not in fontInfoAttributesVersion2:
|
|
|
|
continue
|
|
|
|
# store
|
|
|
|
converted[attr] = value
|
|
|
|
return converted
|
2009-02-28 15:47:24 +00:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
import doctest
|
|
|
|
doctest.testmod()
|