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
|
2009-02-28 15:47:24 +00:00
|
|
|
import calendar
|
2008-01-07 17:40:34 +00:00
|
|
|
from robofab.plistlib import readPlist, writePlist
|
|
|
|
from robofab.glifLib import GlyphSet, READ_MODE, WRITE_MODE
|
|
|
|
|
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-12 11:44:22 +00:00
|
|
|
supportedUFOFormatVersions = [1, 2, 3]
|
2009-02-28 15:47:24 +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)
|
|
|
|
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, GLYPHS_DIRNAME)
|
|
|
|
outGlyphs = os.path.join(outPath, 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, GLYPHS_DIRNAME)
|
|
|
|
outGlyphs = os.path.join(outPath, GLYPHS_DIRNAME)
|
|
|
|
if os.path.exists(inGlyphs):
|
|
|
|
shutil.copytree(inGlyphs, outGlyphs)
|
|
|
|
|
|
|
|
|
|
|
|
# ----------
|
|
|
|
# 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):
|
|
|
|
self._path = path
|
2009-02-28 15:47:24 +00:00
|
|
|
self.readMetaInfo()
|
|
|
|
|
|
|
|
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__.")
|
|
|
|
|
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-12 13:25:24 +00:00
|
|
|
def readDataFromPath(self, path):
|
2011-09-12 11:35:57 +00:00
|
|
|
"""
|
|
|
|
Reads the data from the file at the given path.
|
|
|
|
The path must be relative to the UFO path.
|
|
|
|
Returns None if the file does not exist.
|
|
|
|
"""
|
|
|
|
# XXX this is likely less efficient than allowing
|
|
|
|
# the caller to read files directly. However a future
|
|
|
|
# version of the UFO may not be a package with readable
|
|
|
|
# files. (ie, it could be a zip.)
|
|
|
|
path = os.path.join(self._path, path)
|
|
|
|
if not self._checkForFile(path):
|
|
|
|
return None
|
|
|
|
f = open(path, "rb")
|
|
|
|
data = f.read()
|
|
|
|
f.close()
|
|
|
|
return data
|
|
|
|
|
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.
|
|
|
|
data = readPlist(path)
|
|
|
|
formatVersion = data["formatVersion"]
|
|
|
|
if formatVersion not in supportedUFOFormatVersions:
|
|
|
|
raise UFOLibError("Unsupported UFO format (%d) in %s." % (formatVersion, self._path))
|
|
|
|
self._formatVersion = formatVersion
|
|
|
|
|
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.
|
|
|
|
"""
|
2008-01-07 17:40:34 +00:00
|
|
|
path = os.path.join(self._path, GROUPS_FILENAME)
|
|
|
|
if not self._checkForFile(path):
|
|
|
|
return {}
|
|
|
|
return readPlist(path)
|
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
|
|
|
|
version 2 specification. This will write the attributes
|
|
|
|
defined in the file into the object.
|
|
|
|
"""
|
|
|
|
# load the file and return if there is no file
|
2008-01-07 17:40:34 +00:00
|
|
|
path = os.path.join(self._path, FONTINFO_FILENAME)
|
|
|
|
if not self._checkForFile(path):
|
2009-02-28 15:47:24 +00:00
|
|
|
return
|
2008-01-07 17:40:34 +00:00
|
|
|
infoDict = readPlist(path)
|
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:
|
|
|
|
for attr, dataValidationDict in _fontInfoAttributesVersion2ValueData.items():
|
|
|
|
value = infoDict.get(attr)
|
|
|
|
if value is None:
|
|
|
|
continue
|
|
|
|
infoDataToSet[attr] = value
|
|
|
|
# unsupported version
|
|
|
|
else:
|
|
|
|
raise NotImplementedError
|
|
|
|
# validate data
|
|
|
|
infoDataToSet = _validateInfoVersion2Data(infoDataToSet)
|
|
|
|
# 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)
|
|
|
|
|
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.
|
|
|
|
"""
|
2008-01-07 17:40:34 +00:00
|
|
|
path = os.path.join(self._path, KERNING_FILENAME)
|
|
|
|
if not self._checkForFile(path):
|
|
|
|
return {}
|
|
|
|
kerningNested = readPlist(path)
|
|
|
|
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
|
|
|
|
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 {}
|
|
|
|
return readPlist(path)
|
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-11 23:47:21 +00:00
|
|
|
def _readLayerContents(self):
|
|
|
|
"""
|
|
|
|
Private utility for reading layercontents.plist.
|
|
|
|
"""
|
|
|
|
path = os.path.join(self._path, LAYERCONTENTS_FILENAME)
|
|
|
|
# XXX the spec does not address if layercontents.plist is required if
|
|
|
|
# the only layer is the default layer.
|
|
|
|
if not self._checkForFile(path):
|
|
|
|
return []
|
|
|
|
return readPlist(path)
|
|
|
|
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
# XXX the default glyphs layer name is not defined in the spec yet.
|
|
|
|
# public.foreground seems like the logical name but it needs to be discussed.
|
|
|
|
layerContents = self._readLayerContents()
|
|
|
|
for layerName, layerDirectory in layerContents:
|
|
|
|
if layerDirectory == DEFAULT_GLYPHS_DIRNAME:
|
|
|
|
return layerName
|
|
|
|
# The default layer is not defined in the UFO.
|
|
|
|
# XXX Should this error be raised when layercontents.plist is first read?
|
|
|
|
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)
|
|
|
|
|
|
|
|
def getCharacterMapping(self):
|
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.
|
|
|
|
"""
|
|
|
|
glyphsPath = os.path.join(self._path, GLYPHS_DIRNAME)
|
|
|
|
glyphSet = GlyphSet(glyphsPath)
|
|
|
|
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 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."""
|
|
|
|
|
|
|
|
def __init__(self, path, formatVersion=2, fileCreator="org.robofab.ufoLib"):
|
|
|
|
if formatVersion not in supportedUFOFormatVersions:
|
|
|
|
raise UFOLibError("Unsupported UFO format (%d)." % formatVersion)
|
2008-01-07 17:40:34 +00:00
|
|
|
self._path = path
|
2009-02-28 15:47:24 +00:00
|
|
|
self._formatVersion = formatVersion
|
|
|
|
self._fileCreator = fileCreator
|
|
|
|
self._writeMetaInfo()
|
|
|
|
# handle down conversion
|
2011-09-12 13:25:24 +00:00
|
|
|
if formatVersion < 3:
|
|
|
|
# remove all glyph sets except the default
|
|
|
|
for fileName in os.listdir(path):
|
|
|
|
if fileName.startswith("glyphs."):
|
|
|
|
p = os.path.join(path, fileName)
|
|
|
|
self._removePath(p)
|
|
|
|
# remove layercontents.plist
|
|
|
|
p = os.path.join(path, LAYERCONTENTS_FILENAME)
|
|
|
|
self._removePath(p)
|
|
|
|
# remove glyphs/layerinfo.plist
|
|
|
|
# XXX should glifLib handle this one?
|
|
|
|
p = os.path.join(path, GLYPHS_DIRNAME, LAYERINFO_FILENAME)
|
|
|
|
self._removePath(p)
|
|
|
|
# remove /images
|
|
|
|
p = os.path.join(path, IMAGES_DIRNAME)
|
|
|
|
self._removePath(p)
|
|
|
|
# remove /data
|
|
|
|
p = os.path.join(path, DATA_DIRNAME)
|
|
|
|
self._removePath(p)
|
|
|
|
if formatVersion < 2:
|
|
|
|
# remove features.fea
|
|
|
|
p = os.path.join(path, FEATURES_FILENAME)
|
|
|
|
self._removePath(p)
|
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 13:25:24 +00:00
|
|
|
def _removePath(self, path):
|
|
|
|
if os.path.exists(p):
|
|
|
|
if os.path.isdir(p):
|
|
|
|
shutil.rmtree(p)
|
|
|
|
else:
|
|
|
|
os.remove(p)
|
|
|
|
|
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
|
|
|
|
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
|
|
|
|
)
|
2008-01-07 17:40:34 +00:00
|
|
|
writePlistAtomically(metaInfo, path)
|
2009-02-28 15:47:24 +00:00
|
|
|
|
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.
|
|
|
|
"""
|
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:
|
|
|
|
writePlistAtomically(groupsNew, path)
|
|
|
|
elif os.path.exists(path):
|
|
|
|
os.remove(path)
|
2009-02-28 15:47:24 +00:00
|
|
|
|
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
|
|
|
|
fontinfo.plist version 2 secification. Attributes
|
|
|
|
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)
|
2009-02-28 15:47:24 +00:00
|
|
|
# gather version 2 data
|
|
|
|
infoData = {}
|
|
|
|
for attr in _fontInfoAttributesVersion2ValueData.keys():
|
|
|
|
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
|
|
|
|
# validate data
|
|
|
|
infoData = _validateInfoVersion2Data(infoData)
|
|
|
|
# down convert data to version 1 if necessary
|
|
|
|
if self._formatVersion == 1:
|
|
|
|
infoData = _convertFontInfoDataVersion2ToVersion1(infoData)
|
|
|
|
# write file
|
|
|
|
writePlistAtomically(infoData, path)
|
|
|
|
|
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.
|
|
|
|
"""
|
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:
|
|
|
|
writePlistAtomically(kerningDict, path)
|
|
|
|
elif os.path.exists(path):
|
|
|
|
os.remove(path)
|
2009-02-28 15:47:24 +00:00
|
|
|
|
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.
|
|
|
|
"""
|
2008-01-07 17:40:34 +00:00
|
|
|
self._makeDirectory()
|
|
|
|
path = os.path.join(self._path, LIB_FILENAME)
|
|
|
|
if libDict:
|
|
|
|
writePlistAtomically(libDict, path)
|
|
|
|
elif os.path.exists(path):
|
|
|
|
os.remove(path)
|
|
|
|
|
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.")
|
|
|
|
self._makeDirectory()
|
|
|
|
path = os.path.join(self._path, FEATURES_FILENAME)
|
|
|
|
writeFileAtomically(features, path)
|
|
|
|
|
2008-01-07 17:40:34 +00:00
|
|
|
def makeGlyphPath(self):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
|
|
|
Make the glyphs directory in the .ufo.
|
|
|
|
Returns the path of the directory created.
|
|
|
|
"""
|
2008-01-07 17:40:34 +00:00
|
|
|
glyphDir = self._makeDirectory(GLYPHS_DIRNAME)
|
|
|
|
return glyphDir
|
|
|
|
|
|
|
|
def getGlyphSet(self, glyphNameToFileNameFunc=None):
|
2009-02-28 15:47:24 +00:00
|
|
|
"""
|
|
|
|
Return the GlyphSet associated with the
|
|
|
|
glyphs directory in the .ufo.
|
|
|
|
"""
|
2008-01-07 17:40:34 +00:00
|
|
|
return GlyphSet(self.makeGlyphPath(), glyphNameToFileNameFunc)
|
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)
|
|
|
|
|
|
|
|
def writeFileAtomically(text, path):
|
|
|
|
"""Write text into a file at path. Do this sort of atomically
|
|
|
|
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
|
|
|
|
is preserved."""
|
|
|
|
if os.path.exists(path):
|
|
|
|
f = open(path, READ_MODE)
|
|
|
|
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:
|
|
|
|
f = open(path, WRITE_MODE)
|
|
|
|
f.write(text)
|
|
|
|
f.close()
|
|
|
|
|
|
|
|
# ----------------------
|
|
|
|
# fontinfo.plist Support
|
|
|
|
# ----------------------
|
|
|
|
|
|
|
|
# Version 1
|
|
|
|
|
|
|
|
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",
|
|
|
|
])
|
|
|
|
|
|
|
|
# Version 2
|
|
|
|
|
|
|
|
# Validators
|
|
|
|
|
|
|
|
def validateFontInfoVersion2ValueForAttribute(attr, value):
|
|
|
|
"""
|
|
|
|
This performs very basic validation of the value for attribute
|
|
|
|
following the UFO fontinfo.plist specification. The results
|
|
|
|
of this should not be interpretted as *correct* for the font
|
|
|
|
that they are part of. This merely indicates that the value
|
|
|
|
is of the proper type and, where the specification defines
|
|
|
|
a set range of possible values for an attribute, that the
|
|
|
|
value is in the accepted range.
|
|
|
|
"""
|
|
|
|
dataValidationDict = _fontInfoAttributesVersion2ValueData[attr]
|
|
|
|
valueType = dataValidationDict.get("type")
|
|
|
|
validator = dataValidationDict.get("valueValidator")
|
|
|
|
valueOptions = dataValidationDict.get("valueOptions")
|
|
|
|
# have specific options for the validator
|
|
|
|
if valueOptions is not None:
|
|
|
|
isValidValue = validator(value, valueOptions)
|
|
|
|
# no specific options
|
|
|
|
else:
|
|
|
|
if validator == _fontInfoTypeValidator:
|
|
|
|
isValidValue = validator(value, valueType)
|
|
|
|
else:
|
|
|
|
isValidValue = validator(value)
|
|
|
|
return isValidValue
|
|
|
|
|
|
|
|
def _validateInfoVersion2Data(infoData):
|
|
|
|
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
|
|
|
|
return infoData
|
|
|
|
|
|
|
|
def _fontInfoTypeValidator(value, typ):
|
|
|
|
return isinstance(value, typ)
|
|
|
|
|
|
|
|
def _fontInfoVersion2IntListValidator(values, validValues):
|
|
|
|
if not isinstance(values, (list, tuple)):
|
|
|
|
return False
|
|
|
|
valuesSet = set(values)
|
|
|
|
validValuesSet = set(validValues)
|
|
|
|
if len(valuesSet - validValuesSet) > 0:
|
|
|
|
return False
|
|
|
|
for value in values:
|
|
|
|
if not isinstance(value, int):
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _fontInfoVersion2StyleMapStyleNameValidator(value):
|
|
|
|
options = ["regular", "italic", "bold", "bold italic"]
|
|
|
|
return value in options
|
|
|
|
|
|
|
|
def _fontInfoVersion2OpenTypeHeadCreatedValidator(value):
|
|
|
|
# format: 0000/00/00 00:00:00
|
|
|
|
if not isinstance(value, (str, unicode)):
|
|
|
|
return False
|
|
|
|
# basic formatting
|
|
|
|
if not len(value) == 19:
|
|
|
|
return False
|
|
|
|
if value.count(" ") != 1:
|
|
|
|
return False
|
|
|
|
date, time = value.split(" ")
|
|
|
|
if date.count("/") != 2:
|
|
|
|
return False
|
|
|
|
if time.count(":") != 2:
|
|
|
|
return False
|
|
|
|
# date
|
|
|
|
year, month, day = date.split("/")
|
|
|
|
if len(year) != 4:
|
|
|
|
return False
|
|
|
|
if len(month) != 2:
|
|
|
|
return False
|
|
|
|
if len(day) != 2:
|
|
|
|
return False
|
|
|
|
try:
|
|
|
|
year = int(year)
|
|
|
|
month = int(month)
|
|
|
|
day = int(day)
|
|
|
|
except ValueError:
|
|
|
|
return False
|
|
|
|
if month < 1 or month > 12:
|
|
|
|
return False
|
|
|
|
monthMaxDay = calendar.monthrange(year, month)
|
|
|
|
if month > monthMaxDay:
|
|
|
|
return False
|
|
|
|
# time
|
|
|
|
hour, minute, second = time.split(":")
|
|
|
|
if len(hour) != 2:
|
|
|
|
return False
|
|
|
|
if len(minute) != 2:
|
|
|
|
return False
|
|
|
|
if len(second) != 2:
|
|
|
|
return False
|
|
|
|
try:
|
|
|
|
hour = int(hour)
|
|
|
|
minute = int(minute)
|
|
|
|
second = int(second)
|
|
|
|
except ValueError:
|
|
|
|
return False
|
|
|
|
if hour < 0 or hour > 23:
|
|
|
|
return False
|
|
|
|
if minute < 0 or minute > 59:
|
|
|
|
return False
|
|
|
|
if second < 0 or second > 59:
|
|
|
|
return True
|
|
|
|
# fallback
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _fontInfoVersion2OpenTypeOS2WeightClassValidator(value):
|
|
|
|
if not isinstance(value, int):
|
|
|
|
return False
|
|
|
|
if value < 0:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _fontInfoVersion2OpenTypeOS2WidthClassValidator(value):
|
|
|
|
if not isinstance(value, int):
|
|
|
|
return False
|
|
|
|
if value < 1:
|
|
|
|
return False
|
|
|
|
if value > 9:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _fontInfoVersion2OpenTypeOS2PanoseValidator(values):
|
|
|
|
if not isinstance(values, (list, tuple)):
|
|
|
|
return False
|
|
|
|
if len(values) != 10:
|
|
|
|
return False
|
|
|
|
for value in values:
|
|
|
|
if not isinstance(value, int):
|
|
|
|
return False
|
|
|
|
# XXX further validation?
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _fontInfoVersion2OpenTypeOS2FamilyClassValidator(values):
|
|
|
|
if not isinstance(values, (list, tuple)):
|
|
|
|
return False
|
|
|
|
if len(values) != 2:
|
|
|
|
return False
|
|
|
|
for value in values:
|
|
|
|
if not isinstance(value, int):
|
|
|
|
return False
|
|
|
|
classID, subclassID = values
|
|
|
|
if classID < 0 or classID > 14:
|
|
|
|
return False
|
|
|
|
if subclassID < 0 or subclassID > 15:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _fontInfoVersion2PostscriptBluesValidator(values):
|
|
|
|
if not isinstance(values, (list, tuple)):
|
|
|
|
return False
|
|
|
|
if len(values) > 14:
|
|
|
|
return False
|
|
|
|
if len(values) % 2:
|
|
|
|
return False
|
|
|
|
for value in values:
|
|
|
|
if not isinstance(value, (int, float)):
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _fontInfoVersion2PostscriptOtherBluesValidator(values):
|
|
|
|
if not isinstance(values, (list, tuple)):
|
|
|
|
return False
|
|
|
|
if len(values) > 10:
|
|
|
|
return False
|
|
|
|
if len(values) % 2:
|
|
|
|
return False
|
|
|
|
for value in values:
|
|
|
|
if not isinstance(value, (int, float)):
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _fontInfoVersion2PostscriptStemsValidator(values):
|
|
|
|
if not isinstance(values, (list, tuple)):
|
|
|
|
return False
|
|
|
|
if len(values) > 12:
|
|
|
|
return False
|
|
|
|
for value in values:
|
|
|
|
if not isinstance(value, (int, float)):
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _fontInfoVersion2PostscriptWindowsCharacterSetValidator(value):
|
|
|
|
validValues = range(1, 21)
|
|
|
|
if value not in validValues:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
# Attribute Definitions
|
|
|
|
# This defines the attributes, types and, in some
|
|
|
|
# cases the possible values, that can exist is
|
|
|
|
# fontinfo.plist.
|
|
|
|
|
|
|
|
_fontInfoVersion2OpenTypeHeadFlagsOptions = range(0, 14)
|
|
|
|
_fontInfoVersion2OpenTypeOS2SelectionOptions = [1, 2, 3, 4]
|
|
|
|
_fontInfoVersion2OpenTypeOS2UnicodeRangesOptions = range(0, 128)
|
|
|
|
_fontInfoVersion2OpenTypeOS2CodePageRangesOptions = range(0, 64)
|
|
|
|
_fontInfoVersion2OpenTypeOS2TypeOptions = [0, 1, 2, 3, 8, 9]
|
|
|
|
|
|
|
|
_fontInfoAttributesVersion2ValueData = {
|
|
|
|
"familyName" : dict(type=(str, unicode)),
|
|
|
|
"styleName" : dict(type=(str, unicode)),
|
|
|
|
"styleMapFamilyName" : dict(type=(str, unicode)),
|
|
|
|
"styleMapStyleName" : dict(type=(str, unicode), valueValidator=_fontInfoVersion2StyleMapStyleNameValidator),
|
|
|
|
"versionMajor" : dict(type=int),
|
|
|
|
"versionMinor" : dict(type=int),
|
|
|
|
"year" : dict(type=int),
|
|
|
|
"copyright" : dict(type=(str, unicode)),
|
|
|
|
"trademark" : dict(type=(str, unicode)),
|
|
|
|
"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)),
|
|
|
|
"note" : dict(type=(str, unicode)),
|
|
|
|
"openTypeHeadCreated" : dict(type=(str, unicode), valueValidator=_fontInfoVersion2OpenTypeHeadCreatedValidator),
|
|
|
|
"openTypeHeadLowestRecPPEM" : dict(type=(int, float)),
|
|
|
|
"openTypeHeadFlags" : dict(type="integerList", valueValidator=_fontInfoVersion2IntListValidator, valueOptions=_fontInfoVersion2OpenTypeHeadFlagsOptions),
|
|
|
|
"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)),
|
|
|
|
"openTypeNameDesigner" : dict(type=(str, unicode)),
|
|
|
|
"openTypeNameDesignerURL" : dict(type=(str, unicode)),
|
|
|
|
"openTypeNameManufacturer" : dict(type=(str, unicode)),
|
|
|
|
"openTypeNameManufacturerURL" : dict(type=(str, unicode)),
|
|
|
|
"openTypeNameLicense" : dict(type=(str, unicode)),
|
|
|
|
"openTypeNameLicenseURL" : dict(type=(str, unicode)),
|
|
|
|
"openTypeNameVersion" : dict(type=(str, unicode)),
|
|
|
|
"openTypeNameUniqueID" : dict(type=(str, unicode)),
|
|
|
|
"openTypeNameDescription" : dict(type=(str, unicode)),
|
|
|
|
"openTypeNamePreferredFamilyName" : dict(type=(str, unicode)),
|
|
|
|
"openTypeNamePreferredSubfamilyName" : dict(type=(str, unicode)),
|
|
|
|
"openTypeNameCompatibleFullName" : dict(type=(str, unicode)),
|
|
|
|
"openTypeNameSampleText" : dict(type=(str, unicode)),
|
|
|
|
"openTypeNameWWSFamilyName" : dict(type=(str, unicode)),
|
|
|
|
"openTypeNameWWSSubfamilyName" : dict(type=(str, unicode)),
|
|
|
|
"openTypeOS2WidthClass" : dict(type=int, valueValidator=_fontInfoVersion2OpenTypeOS2WidthClassValidator),
|
|
|
|
"openTypeOS2WeightClass" : dict(type=int, valueValidator=_fontInfoVersion2OpenTypeOS2WeightClassValidator),
|
|
|
|
"openTypeOS2Selection" : dict(type="integerList", valueValidator=_fontInfoVersion2IntListValidator, valueOptions=_fontInfoVersion2OpenTypeOS2SelectionOptions),
|
|
|
|
"openTypeOS2VendorID" : dict(type=(str, unicode)),
|
|
|
|
"openTypeOS2Panose" : dict(type="integerList", valueValidator=_fontInfoVersion2OpenTypeOS2PanoseValidator),
|
|
|
|
"openTypeOS2FamilyClass" : dict(type="integerList", valueValidator=_fontInfoVersion2OpenTypeOS2FamilyClassValidator),
|
|
|
|
"openTypeOS2UnicodeRanges" : dict(type="integerList", valueValidator=_fontInfoVersion2IntListValidator, valueOptions=_fontInfoVersion2OpenTypeOS2UnicodeRangesOptions),
|
|
|
|
"openTypeOS2CodePageRanges" : dict(type="integerList", valueValidator=_fontInfoVersion2IntListValidator, valueOptions=_fontInfoVersion2OpenTypeOS2CodePageRangesOptions),
|
|
|
|
"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)),
|
|
|
|
"openTypeOS2Type" : dict(type="integerList", valueValidator=_fontInfoVersion2IntListValidator, valueOptions=_fontInfoVersion2OpenTypeOS2TypeOptions),
|
|
|
|
"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)),
|
|
|
|
"postscriptFontName" : dict(type=(str, unicode)),
|
|
|
|
"postscriptFullName" : dict(type=(str, unicode)),
|
|
|
|
"postscriptSlantAngle" : dict(type=(float, int)),
|
|
|
|
"postscriptUniqueID" : dict(type=int),
|
|
|
|
"postscriptUnderlineThickness" : dict(type=(int, float)),
|
|
|
|
"postscriptUnderlinePosition" : dict(type=(int, float)),
|
|
|
|
"postscriptIsFixedPitch" : dict(type=bool),
|
|
|
|
"postscriptBlueValues" : dict(type="integerList", valueValidator=_fontInfoVersion2PostscriptBluesValidator),
|
|
|
|
"postscriptOtherBlues" : dict(type="integerList", valueValidator=_fontInfoVersion2PostscriptOtherBluesValidator),
|
|
|
|
"postscriptFamilyBlues" : dict(type="integerList", valueValidator=_fontInfoVersion2PostscriptBluesValidator),
|
|
|
|
"postscriptFamilyOtherBlues" : dict(type="integerList", valueValidator=_fontInfoVersion2PostscriptOtherBluesValidator),
|
|
|
|
"postscriptStemSnapH" : dict(type="integerList", valueValidator=_fontInfoVersion2PostscriptStemsValidator),
|
|
|
|
"postscriptStemSnapV" : dict(type="integerList", valueValidator=_fontInfoVersion2PostscriptStemsValidator),
|
|
|
|
"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)),
|
|
|
|
"postscriptWeightName" : dict(type=(str, unicode)),
|
|
|
|
"postscriptDefaultCharacter" : dict(type=(str, unicode)),
|
|
|
|
"postscriptWindowsCharacterSet" : dict(type=int, valueValidator=_fontInfoVersion2PostscriptWindowsCharacterSetValidator),
|
|
|
|
"macintoshFONDFamilyID" : dict(type=int),
|
|
|
|
"macintoshFONDName" : dict(type=(str, unicode)),
|
|
|
|
}
|
|
|
|
fontInfoAttributesVersion2 = set(_fontInfoAttributesVersion2ValueData.keys())
|
|
|
|
|
|
|
|
# insert the type validator for all attrs that
|
|
|
|
# have no defined validator.
|
|
|
|
for attr, dataDict in _fontInfoAttributesVersion2ValueData.items():
|
|
|
|
if "valueValidator" not in dataDict:
|
|
|
|
dataDict["valueValidator"] = _fontInfoTypeValidator
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
_fontInfoAttributesVersion1To2 = {
|
|
|
|
"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"
|
|
|
|
}
|
|
|
|
_fontInfoAttributesVersion2To1 = _flipDict(_fontInfoAttributesVersion1To2)
|
|
|
|
deprecatedFontInfoAttributesVersion2 = set(_fontInfoAttributesVersion1To2.keys())
|
|
|
|
|
|
|
|
_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
|
|
|
|
attr = _fontInfoAttributesVersion1To2.get(attr, attr)
|
|
|
|
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)
|
|
|
|
attr = _fontInfoAttributesVersion2To1.get(attr, attr)
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
import doctest
|
|
|
|
doctest.testmod()
|