UFOReader validation turned off by default. Starting on UFOWriter

This commit is contained in:
Ben Kiel 2018-06-11 11:07:29 -05:00
parent 2a0682238e
commit b8d4b5562b

View File

@ -117,14 +117,19 @@ def _getPlist(self, fileName, default=None):
class UFOReader(object):
"""Read the various components of the .ufo."""
"""
Read the various components of the .ufo.
By default it will not validate the read data. Set ``validate`` to
``True`` to validate data.
"""
def __init__(self, path):
def __init__(self, path, validate=False):
if not os.path.exists(path):
raise UFOLibError("The specified UFO doesn't exist.")
self._path = path
self.readMetaInfo()
self._upConvertedKerningData = None
self._validate = validate
# properties
@ -135,13 +140,15 @@ class UFOReader(object):
# up conversion
def _upConvertKerning(self):
def _upConvertKerning(self, validate):
"""
Up convert kerning and groups in UFO 1 and 2.
The data will be held internally until each bit of data
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.
``validate`` will validate the data.
"""
if self._upConvertedKerningData:
testKerning = self._readKerning()
@ -152,17 +159,18 @@ class UFOReader(object):
raise UFOLibError("The data in groups.plist has been modified since it was converted to UFO 3 format.")
else:
groups = self._readGroups()
invalidFormatMessage = "groups.plist is not properly formatted."
if not isinstance(groups, dict):
raise UFOLibError(invalidFormatMessage)
for groupName, glyphList in list(groups.items()):
if not isinstance(groupName, basestring):
if validate:
invalidFormatMessage = "groups.plist is not properly formatted."
if not isinstance(groups, dict):
raise UFOLibError(invalidFormatMessage)
elif not isinstance(glyphList, list):
raise UFOLibError(invalidFormatMessage)
for glyphName in glyphList:
if not isinstance(glyphName, basestring):
for groupName, glyphList in list(groups.items()):
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)
self._upConvertedKerningData = dict(
kerning={},
originalKerning=self._readKerning(),
@ -238,22 +246,28 @@ class UFOReader(object):
# metainfo.plist
def readMetaInfo(self):
def readMetaInfo(self, validate=self._validate):
"""
Read metainfo.plist. Only used for internal operations.
``validate`` will validate the read data, by default it is set
to the class's validate value, can be overridden.
"""
# should there be a blind try/except with a UFOLibError
# raised in except here (and elsewhere)? It would be nice to
# provide external callers with a single exception to catch.
data = self._getPlist(METAINFO_FILENAME)
if not isinstance(data, dict):
raise UFOLibError("metainfo.plist is not properly formatted.")
if validate:
if not isinstance(data, dict):
raise UFOLibError("metainfo.plist is not properly formatted.")
formatVersion = data["formatVersion"]
if not isinstance(formatVersion, int):
metaplist_path = os.path.join(self._path, METAINFO_FILENAME)
raise UFOLibError("formatVersion must be specified as an integer in " + metaplist_path)
if formatVersion not in supportedUFOFormatVersions:
raise UFOLibError("Unsupported UFO format (%d) in %s." % (formatVersion, self._path))
if validate:
if not isinstance(formatVersion, int):
metaplist_path = os.path.join(self._path, METAINFO_FILENAME)
raise UFOLibError("formatVersion must be specified as an integer in " + metaplist_path)
if formatVersion not in supportedUFOFormatVersions:
raise UFOLibError("Unsupported UFO format (%d) in %s." % (formatVersion, self._path))
self._formatVersion = formatVersion
# groups.plist
@ -261,23 +275,26 @@ class UFOReader(object):
def _readGroups(self):
return self._getPlist(GROUPS_FILENAME, {})
def readGroups(self):
def readGroups(self, validate=self._validate):
"""
Read groups.plist. Returns a dict.
``validate`` will validate the read data, by default it is set to the
class's validate value, can be overridden.
"""
# handle up conversion
if self._formatVersion < 3:
self._upConvertKerning()
self._upConvertKerning(validate)
groups = self._upConvertedKerningData["groups"]
# normal
else:
groups = self._readGroups()
valid, message = groupsValidator(groups)
if not valid:
raise UFOLibError(message)
if validate:
valid, message = groupsValidator(groups)
if not valid:
raise UFOLibError(message)
return groups
def getKerningGroupConversionRenameMaps(self):
def getKerningGroupConversionRenameMaps(self, validate=self._validate):
"""
Get maps defining the renaming that was done during any
needed kerning group conversion. This method returns a
@ -290,12 +307,15 @@ class UFOReader(object):
When no conversion has been performed, the side1 and side2
dictionaries will be empty.
``validate`` will validate the groups, by default it is set to the
class's validate value, can be overridden.
"""
if self._formatVersion >= 3:
return dict(side1={}, side2={})
# use the public group reader to force the load and
# conversion of the data if it hasn't happened yet.
self.readGroups()
self.readGroups(validate=validate)
return self._upConvertedKerningData["groupRenameMaps"]
# fontinfo.plist
@ -306,12 +326,15 @@ class UFOReader(object):
raise UFOLibError("fontinfo.plist is not properly formatted.")
return data
def readInfo(self, info):
def readInfo(self, info, validate=self._validate):
"""
Read fontinfo.plist. It requires an object that allows
setting attributes with names that follow the fontinfo.plist
version 3 specification. This will write the attributes
defined in the file into the object.
``validate`` will validate the read data, by default it is set to the
class's validate value, can be overridden.
"""
infoDict = self._readInfo()
infoDataToSet = {}
@ -342,7 +365,8 @@ class UFOReader(object):
else:
raise NotImplementedError
# validate data
infoDataToSet = validateInfoVersion3Data(infoDataToSet)
if validate:
infoDataToSet = validateInfoVersion3Data(infoDataToSet)
# populate the object
for attr, value in list(infoDataToSet.items()):
try:
@ -356,20 +380,24 @@ class UFOReader(object):
data = self._getPlist(KERNING_FILENAME, {})
return data
def readKerning(self):
def readKerning(self, validate=self._validate):
"""
Read kerning.plist. Returns a dict.
``validate`` will validate the kerning data, by default it is set to the
class's validate value, can be overridden.
"""
# handle up conversion
if self._formatVersion < 3:
self._upConvertKerning()
self._upConvertKerning(validate)
kerningNested = self._upConvertedKerningData["kerning"]
# normal
else:
kerningNested = self._readKerning()
valid, message = kerningValidator(kerningNested)
if not valid:
raise UFOLibError(message)
if validate:
valid, message = kerningValidator(kerningNested)
if not valid:
raise UFOLibError(message)
# flatten
kerning = {}
for left in kerningNested:
@ -380,14 +408,18 @@ class UFOReader(object):
# lib.plist
def readLib(self):
def readLib(self, validate=self._validate):
"""
Read lib.plist. Returns a dict.
``validate`` will validate the data, by default it is set to the
class's validate value, can be overridden.
"""
data = self._getPlist(LIB_FILENAME, {})
valid, message = fontLibValidator(data)
if not valid:
raise UFOLibError(message)
if validate:
valid, message = fontLibValidator(data)
if not valid:
raise UFOLibError(message)
return data
# features.fea
@ -405,51 +437,63 @@ class UFOReader(object):
# glyph sets & layers
def _readLayerContents(self):
def _readLayerContents(self, validate):
"""
Rebuild the layer contents list by checking what glyphsets
are available on disk.
``validate`` will validate the layer contents.
"""
if self._formatVersion < 3:
return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)]
# read the file on disk
contents = self._getPlist(LAYERCONTENTS_FILENAME)
valid, error = layerContentsValidator(contents, self._path)
if not valid:
raise UFOLibError(error)
if validate:
valid, error = layerContentsValidator(contents, self._path)
if not valid:
raise UFOLibError(error)
return contents
def getLayerNames(self):
def getLayerNames(self, validate=self._validate):
"""
Get the ordered layer names from layercontents.plist.
``validate`` will validate the data, by default it is set to the
class's validate value, can be overridden.
"""
layerContents = self._readLayerContents()
layerContents = self._readLayerContents(validate)
layerNames = [layerName for layerName, directoryName in layerContents]
return layerNames
def getDefaultLayerName(self):
def getDefaultLayerName(self, validate=self._validate):
"""
Get the default layer name from layercontents.plist.
``validate`` will validate the data, by default it is set to the
class's validate value, can be overridden.
"""
layerContents = self._readLayerContents()
layerContents = self._readLayerContents(validate)
for layerName, layerDirectory in layerContents:
if layerDirectory == DEFAULT_GLYPHS_DIRNAME:
return layerName
# this will already have been raised during __init__
raise UFOLibError("The default layer is not defined in layercontents.plist.")
def getGlyphSet(self, layerName=None):
def getGlyphSet(self, layerName=None, validate=self._validate):
"""
Return the GlyphSet associated with the
glyphs directory mapped to layerName
in the UFO. If layerName is not provided,
the name retrieved with getDefaultLayerName
will be used.
``validate`` will validate the data, by default it is set to the
class's validate value, can be overridden.
"""
if layerName is None:
layerName = self.getDefaultLayerName()
layerName = self.getDefaultLayerName(validate=validate)
directory = None
layerContents = self._readLayerContents()
layerContents = self._readLayerContents(validate)
for storedLayerName, storedLayerDirectory in layerContents:
if layerName == storedLayerName:
directory = storedLayerDirectory
@ -507,11 +551,14 @@ class UFOReader(object):
result.append(p)
return result
def getImageDirectoryListing(self):
def getImageDirectoryListing(self, validate=self._validate):
"""
Returns a list of all image file names in
the images directory. Each of the images will
have been verified to have the PNG signature.
``validate`` will validate the data, by default it is set to the
class's validate value, can be overridden.
"""
if self._formatVersion < 3:
return []
@ -527,23 +574,28 @@ class UFOReader(object):
# silently skip this as version control
# systems often have hidden directories
continue
valid, error = pngValidator(path=p)
if valid:
result.append(fileName)
if validate:
valid, error = pngValidator(path=p)
if valid:
result.append(fileName)
return result
def readImage(self, fileName):
def readImage(self, fileName, validate=self._validate):
"""
Return image data for the file named fileName.
``validate`` will validate the data, by default it is set to the
class's validate value, can be overridden.
"""
if self._formatVersion < 3:
raise UFOLibError("Reading images is not allowed in UFO %d." % self._formatVersion)
data = self.readBytesFromPath(os.path.join(IMAGES_DIRNAME, fileName))
if data is None:
raise UFOLibError("No image file named %s." % fileName)
valid, error = pngValidator(data=data)
if not valid:
raise UFOLibError(error)
if validate:
valid, error = pngValidator(data=data)
if not valid:
raise UFOLibError(error)
return data
# ----------
@ -555,7 +607,7 @@ class UFOWriter(object):
"""Write the various components of the .ufo."""
def __init__(self, path, formatVersion=3, fileCreator="org.robofab.ufoLib"):
def __init__(self, path, formatVersion=3, fileCreator="org.robofab.ufoLib", validate=True):
if formatVersion not in supportedUFOFormatVersions:
raise UFOLibError("Unsupported UFO format (%d)." % formatVersion)
# establish some basic stuff
@ -563,6 +615,7 @@ class UFOWriter(object):
self._formatVersion = formatVersion
self._fileCreator = fileCreator
self._downConversionKerningData = None
self._validate = validate
# if the file already exists, get the format version.
# this will be needed for up and down conversion.
previousFormatVersion = None
@ -799,15 +852,16 @@ class UFOWriter(object):
remap[dataName] = writeName
self._downConversionKerningData = dict(groupRenameMap=remap)
def writeGroups(self, groups):
def writeGroups(self, groups, validate=self._validate):
"""
Write groups.plist. This method requires a
dict of glyph groups as an argument.
"""
# validate the data structure
valid, message = groupsValidator(groups)
if not valid:
raise UFOLibError(message)
if validate:
valid, message = groupsValidator(groups)
if not valid:
raise UFOLibError(message)
# down convert
if self._formatVersion < 3 and self._downConversionKerningData is not None:
remap = self._downConversionKerningData["groupRenameMap"]
@ -845,7 +899,7 @@ class UFOWriter(object):
# fontinfo.plist
def writeInfo(self, info):
def writeInfo(self, info, validate=self._validate):
"""
Write info.plist. This method requires an object
that supports getting attributes that follow the
@ -866,20 +920,23 @@ class UFOWriter(object):
infoData[attr] = value
# down convert data if necessary and validate
if self._formatVersion == 3:
infoData = validateInfoVersion3Data(infoData)
if validate:
infoData = validateInfoVersion3Data(infoData)
elif self._formatVersion == 2:
infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
infoData = validateInfoVersion2Data(infoData)
if validate:
infoData = validateInfoVersion2Data(infoData)
elif self._formatVersion == 1:
infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
infoData = validateInfoVersion2Data(infoData)
if validate:
infoData = validateInfoVersion2Data(infoData)
infoData = _convertFontInfoDataVersion2ToVersion1(infoData)
# write file
self._writePlist(FONTINFO_FILENAME, infoData)
# kerning.plist
def writeKerning(self, kerning):
def writeKerning(self, kerning, validate=self._validate):
"""
Write kerning.plist. This method requires a
dict of kerning pairs as an argument.
@ -890,20 +947,21 @@ class UFOWriter(object):
kerning data being passed is standards compliant.
"""
# validate the data structure
invalidFormatMessage = "The kerning is not properly formatted."
if not isDictEnough(kerning):
raise UFOLibError(invalidFormatMessage)
for pair, value in list(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)):
if validate:
invalidFormatMessage = "The kerning is not properly formatted."
if not isDictEnough(kerning):
raise UFOLibError(invalidFormatMessage)
for pair, value in list(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)
# down convert
if self._formatVersion < 3 and self._downConversionKerningData is not None:
remap = self._downConversionKerningData["groupRenameMap"]