summaryrefslogtreecommitdiff
path: root/misc/pylib/robofab/ufoLib.py
diff options
context:
space:
mode:
Diffstat (limited to 'misc/pylib/robofab/ufoLib.py')
-rwxr-xr-xmisc/pylib/robofab/ufoLib.py1084
1 files changed, 1084 insertions, 0 deletions
diff --git a/misc/pylib/robofab/ufoLib.py b/misc/pylib/robofab/ufoLib.py
new file mode 100755
index 000000000..b329fd929
--- /dev/null
+++ b/misc/pylib/robofab/ufoLib.py
@@ -0,0 +1,1084 @@
+""""
+A library for importing .ufo files and their descendants.
+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
+"""
+
+
+import os
+import shutil
+from cStringIO import StringIO
+import calendar
+from robofab.plistlib import readPlist, writePlist
+from robofab.glifLib import GlyphSet, READ_MODE, WRITE_MODE
+
+try:
+ set
+except NameError:
+ from sets import Set as set
+
+__all__ = [
+ "makeUFOPath"
+ "UFOLibError",
+ "UFOReader",
+ "UFOWriter",
+ "convertUFOFormatVersion1ToFormatVersion2",
+ "convertUFOFormatVersion2ToFormatVersion1",
+ "fontInfoAttributesVersion1",
+ "fontInfoAttributesVersion2",
+ "deprecatedFontInfoAttributesVersion2",
+ "validateFontInfoVersion2ValueForAttribute",
+ "convertFontInfoValueForAttributeFromVersion1ToVersion2",
+ "convertFontInfoValueForAttributeFromVersion2ToVersion1"
+]
+
+
+class UFOLibError(Exception): pass
+
+
+# ----------
+# File Names
+# ----------
+
+GLYPHS_DIRNAME = "glyphs"
+METAINFO_FILENAME = "metainfo.plist"
+FONTINFO_FILENAME = "fontinfo.plist"
+LIB_FILENAME = "lib.plist"
+GROUPS_FILENAME = "groups.plist"
+KERNING_FILENAME = "kerning.plist"
+FEATURES_FILENAME = "features.fea"
+
+supportedUFOFormatVersions = [1, 2]
+
+
+# ---------------------------
+# 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
+# ----------
+
+
+class UFOReader(object):
+
+ """Read the various components of the .ufo."""
+
+ def __init__(self, path):
+ self._path = path
+ 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__.")
+
+ def _checkForFile(self, path):
+ if not os.path.exists(path):
+ return False
+ else:
+ return True
+
+ def readMetaInfo(self):
+ """
+ Read metainfo.plist. Only used for internal operations.
+ """
+ path = os.path.join(self._path, METAINFO_FILENAME)
+ if not self._checkForFile(path):
+ 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
+
+ def readGroups(self):
+ """
+ Read groups.plist. Returns a dict.
+ """
+ path = os.path.join(self._path, GROUPS_FILENAME)
+ if not self._checkForFile(path):
+ return {}
+ return readPlist(path)
+
+ def readInfo(self, info):
+ """
+ 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
+ path = os.path.join(self._path, FONTINFO_FILENAME)
+ if not self._checkForFile(path):
+ return
+ infoDict = readPlist(path)
+ 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():
+ try:
+ setattr(info, attr, value)
+ except AttributeError:
+ raise UFOLibError("The supplied info object does not support setting a necessary attribute (%s)." % attr)
+
+ def readKerning(self):
+ """
+ Read kerning.plist. Returns a dict.
+ """
+ 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
+
+ def readLib(self):
+ """
+ Read lib.plist. Returns a dict.
+ """
+ path = os.path.join(self._path, LIB_FILENAME)
+ if not self._checkForFile(path):
+ return {}
+ return readPlist(path)
+
+ 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
+
+ def getGlyphSet(self):
+ """
+ Return the GlyphSet associated with the
+ glyphs directory in the .ufo.
+ """
+ glyphsPath = os.path.join(self._path, GLYPHS_DIRNAME)
+ return GlyphSet(glyphsPath)
+
+ def getCharacterMapping(self):
+ """
+ Return a dictionary that maps unicode values (ints) to
+ 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
+
+
+# ----------
+# UFO Writer
+# ----------
+
+
+class UFOWriter(object):
+
+ """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)
+ self._path = path
+ self._formatVersion = formatVersion
+ self._fileCreator = fileCreator
+ self._writeMetaInfo()
+ # handle down conversion
+ if formatVersion == 1:
+ ## remove existing features.fea
+ featuresPath = os.path.join(path, FEATURES_FILENAME)
+ if os.path.exists(featuresPath):
+ os.remove(featuresPath)
+
+ 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__.")
+
+ 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
+
+ def _writeMetaInfo(self):
+ self._makeDirectory()
+ path = os.path.join(self._path, METAINFO_FILENAME)
+ metaInfo = dict(
+ creator=self._fileCreator,
+ formatVersion=self._formatVersion
+ )
+ writePlistAtomically(metaInfo, path)
+
+ def writeGroups(self, groups):
+ """
+ Write groups.plist. This method requires a
+ dict of glyph groups as an argument.
+ """
+ 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)
+
+ def writeInfo(self, info):
+ """
+ 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.
+ """
+ self._makeDirectory()
+ path = os.path.join(self._path, FONTINFO_FILENAME)
+ # 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)
+
+ def writeKerning(self, kerning):
+ """
+ Write kerning.plist. This method requires a
+ dict of kerning pairs as an argument.
+ """
+ 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)
+
+ def writeLib(self, libDict):
+ """
+ Write lib.plist. This method requires a
+ lib dict as an argument.
+ """
+ self._makeDirectory()
+ path = os.path.join(self._path, LIB_FILENAME)
+ if libDict:
+ writePlistAtomically(libDict, path)
+ elif os.path.exists(path):
+ os.remove(path)
+
+ 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)
+
+ def makeGlyphPath(self):
+ """
+ Make the glyphs directory in the .ufo.
+ Returns the path of the directory created.
+ """
+ glyphDir = self._makeDirectory(GLYPHS_DIRNAME)
+ return glyphDir
+
+ def getGlyphSet(self, glyphNameToFileNameFunc=None):
+ """
+ Return the GlyphSet associated with the
+ glyphs directory in the .ufo.
+ """
+ return GlyphSet(self.makeGlyphPath(), glyphNameToFileNameFunc)
+
+# ----------------
+# 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, 7, 8, 9]
+_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
+# "Medium" appears in a lot of UFO 1 files.
+_widthName1To2["Medium"] = 5
+
+_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()