ufoLib/glifLib: use TupleEnum instead of namedtuple for {UFO,GLIF}FormatVersion

Also add dedicated Unsupported{UFO,GLIF}Format exceptions in
fontTools.ufoLib.errors module
This commit is contained in:
Cosimo Lupo 2020-05-05 18:24:59 +01:00
parent f120be17e4
commit 57f4904363
No known key found for this signature in database
GPG Key ID: 179A8F0895A02F4F
3 changed files with 246 additions and 190 deletions

View File

@ -4,7 +4,7 @@ from os import fsdecode
import logging import logging
import zipfile import zipfile
import enum import enum
import collections from collections import OrderedDict
import fs import fs
import fs.base import fs.base
import fs.subfs import fs.subfs
@ -14,13 +14,12 @@ import fs.osfs
import fs.zipfs import fs.zipfs
import fs.tempfs import fs.tempfs
import fs.tools import fs.tools
from fontTools.misc.py23 import tostr
from fontTools.misc import plistlib from fontTools.misc import plistlib
from fontTools.ufoLib.validators import * from fontTools.ufoLib.validators import *
from fontTools.ufoLib.filenames import userNameToFileName from fontTools.ufoLib.filenames import userNameToFileName
from fontTools.ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning from fontTools.ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning
from fontTools.ufoLib.errors import UFOLibError from fontTools.ufoLib.errors import UFOLibError
from fontTools.ufoLib.utils import numberTypes from fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin
""" """
A library for importing .ufo files and their descendants. A library for importing .ufo files and their descendants.
@ -94,32 +93,10 @@ LAYERINFO_FILENAME = "layerinfo.plist"
DEFAULT_LAYER_NAME = "public.default" DEFAULT_LAYER_NAME = "public.default"
class FormatVersion(collections.namedtuple("FormatVersion", ["major", "minor"])): class UFOFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum):
"""Convert single-digit formatVersion into (major, minor=0) namedtuple. FORMAT_1_0 = (1, 0)
FORMAT_2_0 = (2, 0)
This allows to ease transition to new format versions that define a minor FORMAT_3_0 = (3, 0)
component, without breaking API for old formats where only major was defined.
"""
def __new__(cls, *args, **kwargs):
if len(args) == 1 and isinstance(args[0], collections.abc.Iterable):
args = tuple(args[0])
if "minor" not in kwargs and len(args) < 2:
kwargs["minor"] = 0
return super().__new__(cls, *args, **kwargs)
UFO_FORMAT_1_0 = FormatVersion(1)
UFO_FORMAT_2_0 = FormatVersion(2)
UFO_FORMAT_3_0 = FormatVersion(3)
supportedUFOFormatVersions = [
UFO_FORMAT_1_0,
UFO_FORMAT_2_0,
UFO_FORMAT_3_0,
]
LATEST_UFO_FORMAT = sorted(supportedUFOFormatVersions)[-1]
class UFOFileStructure(enum.Enum): class UFOFileStructure(enum.Enum):
@ -315,7 +292,7 @@ class UFOReader(_UFOBaseIO):
DeprecationWarning, DeprecationWarning,
stacklevel=2, stacklevel=2,
) )
return self._formatVersionMajor return self._formatVersion.major
formatVersion = property( formatVersion = property(
_get_formatVersion, _get_formatVersion,
@ -327,7 +304,7 @@ class UFOReader(_UFOBaseIO):
"""The (major, minor) format version of the UFO. """The (major, minor) format version of the UFO.
This is determined by reading metainfo.plist during __init__. This is determined by reading metainfo.plist during __init__.
""" """
return FormatVersion(self._formatVersionMajor, self._formatVersionMinor) return self._formatVersion
def _get_fileStructure(self): def _get_fileStructure(self):
return self._fileStructure return self._fileStructure
@ -435,29 +412,33 @@ class UFOReader(_UFOBaseIO):
data = self._getPlist(METAINFO_FILENAME) data = self._getPlist(METAINFO_FILENAME)
if validate and not isinstance(data, dict): if validate and not isinstance(data, dict):
raise UFOLibError("metainfo.plist is not properly formatted.") raise UFOLibError("metainfo.plist is not properly formatted.")
formatVersionMajor = data["formatVersion"] try:
formatVersionMajor = data["formatVersion"]
except KeyError:
raise UFOLibError(
f"Missing required formatVersion in '{METAINFO_FILENAME}' on {self.fs}"
)
formatVersionMinor = data.setdefault("formatVersionMinor", 0) formatVersionMinor = data.setdefault("formatVersionMinor", 0)
if validate:
if not (
isinstance(formatVersionMajor, int) and
isinstance(formatVersionMinor, int)
):
raise UFOLibError(
"formatVersion must be specified as an integer in '%s' on %s"
% (METAINFO_FILENAME, self.fs)
)
if (formatVersionMajor, formatVersionMinor) not in supportedUFOFormatVersions: try:
formatVersion = UFOFormatVersion((formatVersionMajor, formatVersionMinor))
except ValueError as e:
unsupportedMsg = ( unsupportedMsg = (
"Unsupported UFO format (%d.%d) in '%s' on %s" f"Unsupported UFO format ({formatVersionMajor}.{formatVersionMinor}) "
% (formatVersionMajor, formatVersionMinor, METAINFO_FILENAME, self.fs) f"in '{METAINFO_FILENAME}' on {self.fs}"
) )
if validate: if validate:
raise UFOLibError(unsupportedMsg) from fontTools.ufoLib.errors import UnsupportedUFOFormat
logger.warn(
"%s. Some data may be skipped or parsed incorrectly", unsupportedMsg
)
raise UnsupportedUFOFormat(unsupportedMsg) from e
formatVersion = UFOFormatVersion.default()
logger.warning(
"%s. Assuming the latest supported version (%s). "
"Some data may be skipped or parsed incorrectly",
unsupportedMsg, formatVersion
)
data["formatVersionTuple"] = formatVersion
return data return data
def readMetaInfo(self, validate=None): def readMetaInfo(self, validate=None):
@ -468,8 +449,7 @@ class UFOReader(_UFOBaseIO):
to the class's validate value, can be overridden. to the class's validate value, can be overridden.
""" """
data = self._readMetaInfo(validate=validate) data = self._readMetaInfo(validate=validate)
self._formatVersionMajor = data["formatVersion"] self._formatVersion = data["formatVersionTuple"]
self._formatVersionMinor = data["formatVersionMinor"]
# groups.plist # groups.plist
@ -485,7 +465,7 @@ class UFOReader(_UFOBaseIO):
if validate is None: if validate is None:
validate = self._validate validate = self._validate
# handle up conversion # handle up conversion
if self._formatVersionMajor < 3: if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
self._upConvertKerning(validate) self._upConvertKerning(validate)
groups = self._upConvertedKerningData["groups"] groups = self._upConvertedKerningData["groups"]
# normal # normal
@ -516,7 +496,7 @@ class UFOReader(_UFOBaseIO):
""" """
if validate is None: if validate is None:
validate = self._validate validate = self._validate
if self._formatVersionMajor >= 3: if self._formatVersion >= UFOFormatVersion.FORMAT_3_0:
return dict(side1={}, side2={}) return dict(side1={}, side2={})
# use the public group reader to force the load and # use the public group reader to force the load and
# conversion of the data if it hasn't happened yet. # conversion of the data if it hasn't happened yet.
@ -546,7 +526,7 @@ class UFOReader(_UFOBaseIO):
infoDict = self._readInfo(validate) infoDict = self._readInfo(validate)
infoDataToSet = {} infoDataToSet = {}
# version 1 # version 1
if self._formatVersionMajor == 1: if self._formatVersion == UFOFormatVersion.FORMAT_1_0:
for attr in fontInfoAttributesVersion1: for attr in fontInfoAttributesVersion1:
value = infoDict.get(attr) value = infoDict.get(attr)
if value is not None: if value is not None:
@ -554,15 +534,15 @@ class UFOReader(_UFOBaseIO):
infoDataToSet = _convertFontInfoDataVersion1ToVersion2(infoDataToSet) infoDataToSet = _convertFontInfoDataVersion1ToVersion2(infoDataToSet)
infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet) infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet)
# version 2 # version 2
elif self._formatVersionMajor == 2: elif self._formatVersion == UFOFormatVersion.FORMAT_2_0:
for attr, dataValidationDict in list(fontInfoAttributesVersion2ValueData.items()): for attr, dataValidationDict in list(fontInfoAttributesVersion2ValueData.items()):
value = infoDict.get(attr) value = infoDict.get(attr)
if value is None: if value is None:
continue continue
infoDataToSet[attr] = value infoDataToSet[attr] = value
infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet) infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet)
# version 3 # version 3.x
elif self._formatVersionMajor == 3: elif self._formatVersion.major == UFOFormatVersion.FORMAT_3_0.major:
for attr, dataValidationDict in list(fontInfoAttributesVersion3ValueData.items()): for attr, dataValidationDict in list(fontInfoAttributesVersion3ValueData.items()):
value = infoDict.get(attr) value = infoDict.get(attr)
if value is None: if value is None:
@ -570,7 +550,7 @@ class UFOReader(_UFOBaseIO):
infoDataToSet[attr] = value infoDataToSet[attr] = value
# unsupported version # unsupported version
else: else:
raise NotImplementedError raise NotImplementedError(self._formatVersion)
# validate data # validate data
if validate: if validate:
infoDataToSet = validateInfoVersion3Data(infoDataToSet) infoDataToSet = validateInfoVersion3Data(infoDataToSet)
@ -597,7 +577,7 @@ class UFOReader(_UFOBaseIO):
if validate is None: if validate is None:
validate = self._validate validate = self._validate
# handle up conversion # handle up conversion
if self._formatVersionMajor < 3: if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
self._upConvertKerning(validate) self._upConvertKerning(validate)
kerningNested = self._upConvertedKerningData["kerning"] kerningNested = self._upConvertedKerningData["kerning"]
# normal # normal
@ -655,7 +635,7 @@ class UFOReader(_UFOBaseIO):
``validate`` will validate the layer contents. ``validate`` will validate the layer contents.
""" """
if self._formatVersionMajor < 3: if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)] return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)]
contents = self._getPlist(LAYERCONTENTS_FILENAME) contents = self._getPlist(LAYERCONTENTS_FILENAME)
if validate: if validate:
@ -730,7 +710,7 @@ class UFOReader(_UFOBaseIO):
) )
return GlyphSet( return GlyphSet(
glyphSubFS, glyphSubFS,
ufoFormatVersion=self.formatVersionTuple, ufoFormatVersion=self._formatVersion,
validateRead=validateRead, validateRead=validateRead,
validateWrite=validateWrite, validateWrite=validateWrite,
) )
@ -787,7 +767,7 @@ class UFOReader(_UFOBaseIO):
``validate`` will validate the data, by default it is set to the ``validate`` will validate the data, by default it is set to the
class's validate value, can be overridden. class's validate value, can be overridden.
""" """
if self._formatVersionMajor < 3: if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
return [] return []
if validate is None: if validate is None:
validate = self._validate validate = self._validate
@ -837,9 +817,9 @@ class UFOReader(_UFOBaseIO):
""" """
if validate is None: if validate is None:
validate = self._validate validate = self._validate
if self._formatVersionMajor < 3: if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
raise UFOLibError( raise UFOLibError(
"Reading images is not allowed in UFO %d." % self._formatVersionMajor f"Reading images is not allowed in UFO {self._formatVersion.major}."
) )
fileName = fsdecode(fileName) fileName = fsdecode(fileName)
try: try:
@ -880,20 +860,30 @@ class UFOWriter(UFOReader):
By default, the written data will be validated before writing. Set ``validate`` to By default, the written data will be validated before writing. Set ``validate`` to
``False`` if you do not want to validate the data. Validation can also be overriden ``False`` if you do not want to validate the data. Validation can also be overriden
on a per method level if desired. on a per method level if desired.
The ``formatVersion`` argument allows to specify the UFO format version as a tuple
of integers (major, minor), or as a single integer for the major digit only (minor
is implied as 0). By default the latest formatVersion will be used; currently it's
3.0, which is equivalent to formatVersion=(3, 0).
An UnsupportedUFOFormat exception is raised if the requested UFO formatVersion is
not supported.
""" """
def __init__( def __init__(
self, self,
path, path,
formatVersion=LATEST_UFO_FORMAT, formatVersion=None,
fileCreator="com.github.fonttools.ufoLib", fileCreator="com.github.fonttools.ufoLib",
structure=None, structure=None,
validate=True, validate=True,
): ):
if not isinstance(formatVersion, FormatVersion): try:
formatVersion = FormatVersion(formatVersion) formatVersion = UFOFormatVersion(formatVersion)
if formatVersion not in supportedUFOFormatVersions: except ValueError as e:
raise UFOLibError("Unsupported UFO format (%d.%d)." % formatVersion) from fontTools.ufoLib.errors import UnsupportedUFOFormat
raise UnsupportedUFOFormat(f"Unsupported UFO format: {formatVersion!r}") from e
if hasattr(path, "__fspath__"): # support os.PathLike objects if hasattr(path, "__fspath__"): # support os.PathLike objects
path = path.__fspath__() path = path.__fspath__()
@ -1001,7 +991,7 @@ class UFOWriter(UFOReader):
# establish some basic stuff # establish some basic stuff
self._path = fsdecode(path) self._path = fsdecode(path)
self._formatVersionMajor, self._formatVersionMinor = formatVersion self._formatVersion = formatVersion
self._fileCreator = fileCreator self._fileCreator = fileCreator
self._downConversionKerningData = None self._downConversionKerningData = None
self._validate = validate self._validate = validate
@ -1010,21 +1000,21 @@ class UFOWriter(UFOReader):
previousFormatVersion = None previousFormatVersion = None
if self._havePreviousFile: if self._havePreviousFile:
metaInfo = self._readMetaInfo(validate=validate) metaInfo = self._readMetaInfo(validate=validate)
previousFormatVersion = FormatVersion( previousFormatVersion = metaInfo["formatVersionTuple"]
metaInfo["formatVersion"], metaInfo["formatVersionMinor"]
)
# catch down conversion # catch down conversion
if previousFormatVersion > formatVersion: if previousFormatVersion > formatVersion:
raise UFOLibError( from fontTools.ufoLib.errors import UnsupportedUFOFormat
"The UFO located at this path is a higher version (%d.%d) than the "
"version (%d.%d) that is trying to be written. This is not supported." raise UnsupportedUFOFormat(
% (*previousFormatVersion, *formatVersion) "The UFO located at this path is a higher version "
f"({previousFormatVersion}) than the version ({formatVersion}) "
"that is trying to be written. This is not supported."
) )
# handle the layer contents # handle the layer contents
self.layerContents = {} self.layerContents = {}
if previousFormatVersion is not None and previousFormatVersion.major >= 3: if previousFormatVersion is not None and previousFormatVersion.major >= 3:
# already exists # already exists
self.layerContents = collections.OrderedDict(self._readLayerContents(validate)) self.layerContents = OrderedDict(self._readLayerContents(validate))
else: else:
# previous < 3 # previous < 3
# imply the layer contents # imply the layer contents
@ -1158,10 +1148,10 @@ class UFOWriter(UFOReader):
def _writeMetaInfo(self): def _writeMetaInfo(self):
metaInfo = dict( metaInfo = dict(
creator=self._fileCreator, creator=self._fileCreator,
formatVersion=self._formatVersionMajor, formatVersion=self._formatVersion.major,
) )
if self._formatVersionMinor != 0: if self._formatVersion.minor != 0:
metaInfo["formatVersionMinor"] = self._formatVersionMinor metaInfo["formatVersionMinor"] = self._formatVersion.minor
self._writePlist(METAINFO_FILENAME, metaInfo) self._writePlist(METAINFO_FILENAME, metaInfo)
# groups.plist # groups.plist
@ -1182,7 +1172,7 @@ class UFOWriter(UFOReader):
This is the same form returned by UFOReader's This is the same form returned by UFOReader's
getKerningGroupConversionRenameMaps method. getKerningGroupConversionRenameMaps method.
""" """
if self._formatVersionMajor >= 3: if self._formatVersion >= UFOFormatVersion.FORMAT_3_0:
return # XXX raise an error here return # XXX raise an error here
# flip the dictionaries # flip the dictionaries
remap = {} remap = {}
@ -1207,7 +1197,10 @@ class UFOWriter(UFOReader):
if not valid: if not valid:
raise UFOLibError(message) raise UFOLibError(message)
# down convert # down convert
if self._formatVersionMajor < 3 and self._downConversionKerningData is not None: if (
self._formatVersion < UFOFormatVersion.FORMAT_3_0
and self._downConversionKerningData is not None
):
remap = self._downConversionKerningData["groupRenameMap"] remap = self._downConversionKerningData["groupRenameMap"]
remappedGroups = {} remappedGroups = {}
# there are some edge cases here that are ignored: # there are some edge cases here that are ignored:
@ -1268,14 +1261,14 @@ class UFOWriter(UFOReader):
continue continue
infoData[attr] = value infoData[attr] = value
# down convert data if necessary and validate # down convert data if necessary and validate
if self._formatVersionMajor == 3: if self._formatVersion == UFOFormatVersion.FORMAT_3_0:
if validate: if validate:
infoData = validateInfoVersion3Data(infoData) infoData = validateInfoVersion3Data(infoData)
elif self._formatVersionMajor == 2: elif self._formatVersion == UFOFormatVersion.FORMAT_2_0:
infoData = _convertFontInfoDataVersion3ToVersion2(infoData) infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
if validate: if validate:
infoData = validateInfoVersion2Data(infoData) infoData = validateInfoVersion2Data(infoData)
elif self._formatVersionMajor == 1: elif self._formatVersion == UFOFormatVersion.FORMAT_1_0:
infoData = _convertFontInfoDataVersion3ToVersion2(infoData) infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
if validate: if validate:
infoData = validateInfoVersion2Data(infoData) infoData = validateInfoVersion2Data(infoData)
@ -1318,7 +1311,10 @@ class UFOWriter(UFOReader):
if not isinstance(value, numberTypes): if not isinstance(value, numberTypes):
raise UFOLibError(invalidFormatMessage) raise UFOLibError(invalidFormatMessage)
# down convert # down convert
if self._formatVersionMajor < 3 and self._downConversionKerningData is not None: if (
self._formatVersion < UFOFormatVersion.FORMAT_3_0
and self._downConversionKerningData is not None
):
remap = self._downConversionKerningData["groupRenameMap"] remap = self._downConversionKerningData["groupRenameMap"]
remappedKerning = {} remappedKerning = {}
for (side1, side2), value in list(kerning.items()): for (side1, side2), value in list(kerning.items()):
@ -1368,7 +1364,7 @@ class UFOWriter(UFOReader):
""" """
if validate is None: if validate is None:
validate = self._validate validate = self._validate
if self._formatVersionMajor == 1: if self._formatVersion == UFOFormatVersion.FORMAT_1_0:
raise UFOLibError("features.fea is not allowed in UFO Format Version 1.") raise UFOLibError("features.fea is not allowed in UFO Format Version 1.")
if validate: if validate:
if not isinstance(features, str): if not isinstance(features, str):
@ -1387,7 +1383,7 @@ class UFOWriter(UFOReader):
""" """
if validate is None: if validate is None:
validate = self._validate validate = self._validate
if self._formatVersionMajor < 3: if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
return return
if layerOrder is not None: if layerOrder is not None:
newOrder = [] newOrder = []
@ -1435,9 +1431,12 @@ class UFOWriter(UFOReader):
if validateWrite is None: if validateWrite is None:
validateWrite = self._validate validateWrite = self._validate
# only default can be written in < 3 # only default can be written in < 3
if self._formatVersionMajor < 3 and (not defaultLayer or layerName is not None): if (
self._formatVersion < UFOFormatVersion.FORMAT_3_0
and (not defaultLayer or layerName is not None)
):
raise UFOLibError( raise UFOLibError(
"Only the default layer can be writen in UFO %d." % self._formatVersionMajor f"Only the default layer can be writen in UFO {self._formatVersion.major}."
) )
# locate a layer name when None has been given # locate a layer name when None has been given
if layerName is None and defaultLayer: if layerName is None and defaultLayer:
@ -1449,42 +1448,27 @@ class UFOWriter(UFOReader):
elif layerName is None and not defaultLayer: elif layerName is None and not defaultLayer:
raise UFOLibError("A layer name must be provided for non-default layers.") raise UFOLibError("A layer name must be provided for non-default layers.")
# move along to format specific writing # move along to format specific writing
if self._formatVersionMajor == 1: if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
return self._getGlyphSetFormatVersion1(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc) return self._getDefaultGlyphSet(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
elif self._formatVersionMajor == 2: elif self._formatVersion.major == UFOFormatVersion.FORMAT_3_0.major:
return self._getGlyphSetFormatVersion2(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
elif self._formatVersionMajor == 3:
return self._getGlyphSetFormatVersion3( return self._getGlyphSetFormatVersion3(
validateRead, validateRead,
validateWrite, validateWrite,
layerName=layerName, layerName=layerName,
defaultLayer=defaultLayer, defaultLayer=defaultLayer,
glyphNameToFileNameFunc=glyphNameToFileNameFunc, glyphNameToFileNameFunc=glyphNameToFileNameFunc,
formatVersionMinor=self._formatVersionMinor,
) )
else: else:
raise AssertionError(self._formatVersionMajor) raise NotImplementedError(self._formatVersion)
def _getGlyphSetFormatVersion1(self, validateRead, validateWrite, glyphNameToFileNameFunc=None): def _getDefaultGlyphSet(self, validateRead, validateWrite, glyphNameToFileNameFunc=None):
from fontTools.ufoLib.glifLib import GlyphSet from fontTools.ufoLib.glifLib import GlyphSet
glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True) glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True)
return GlyphSet( return GlyphSet(
glyphSubFS, glyphSubFS,
glyphNameToFileNameFunc=glyphNameToFileNameFunc, glyphNameToFileNameFunc=glyphNameToFileNameFunc,
ufoFormatVersion=UFO_FORMAT_1_0, ufoFormatVersion=self._formatVersion,
validateRead=validateRead,
validateWrite=validateWrite,
)
def _getGlyphSetFormatVersion2(self, validateRead, validateWrite, glyphNameToFileNameFunc=None):
from fontTools.ufoLib.glifLib import GlyphSet
glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True)
return GlyphSet(
glyphSubFS,
glyphNameToFileNameFunc=glyphNameToFileNameFunc,
ufoFormatVersion=UFO_FORMAT_2_0,
validateRead=validateRead, validateRead=validateRead,
validateWrite=validateWrite, validateWrite=validateWrite,
) )
@ -1496,7 +1480,6 @@ class UFOWriter(UFOReader):
layerName=None, layerName=None,
defaultLayer=True, defaultLayer=True,
glyphNameToFileNameFunc=None, glyphNameToFileNameFunc=None,
formatVersionMinor=0,
): ):
from fontTools.ufoLib.glifLib import GlyphSet from fontTools.ufoLib.glifLib import GlyphSet
@ -1533,7 +1516,7 @@ class UFOWriter(UFOReader):
return GlyphSet( return GlyphSet(
glyphSubFS, glyphSubFS,
glyphNameToFileNameFunc=glyphNameToFileNameFunc, glyphNameToFileNameFunc=glyphNameToFileNameFunc,
ufoFormatVersion=FormatVersion(3, formatVersionMinor), ufoFormatVersion=self._formatVersion,
validateRead=validateRead, validateRead=validateRead,
validateWrite=validateWrite, validateWrite=validateWrite,
) )
@ -1546,7 +1529,7 @@ class UFOWriter(UFOReader):
layerName, it is up to the caller to inform that object that layerName, it is up to the caller to inform that object that
the directory it represents has changed. the directory it represents has changed.
""" """
if self._formatVersionMajor < 3: if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
# ignore renaming glyph sets for UFO1 UFO2 # ignore renaming glyph sets for UFO1 UFO2
# just write the data from the default layer # just write the data from the default layer
return return
@ -1585,7 +1568,7 @@ class UFOWriter(UFOReader):
""" """
Remove the glyph set matching layerName. Remove the glyph set matching layerName.
""" """
if self._formatVersionMajor < 3: if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
# ignore deleting glyph sets for UFO1 UFO2 as there are no layers # ignore deleting glyph sets for UFO1 UFO2 as there are no layers
# just write the data from the default layer # just write the data from the default layer
return return
@ -1615,9 +1598,9 @@ class UFOWriter(UFOReader):
""" """
if validate is None: if validate is None:
validate = self._validate validate = self._validate
if self._formatVersionMajor < 3: if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
raise UFOLibError( raise UFOLibError(
"Images are not allowed in UFO %d." % self._formatVersionMajor f"Images are not allowed in UFO {self._formatVersion.major}."
) )
fileName = fsdecode(fileName) fileName = fsdecode(fileName)
if validate: if validate:
@ -1631,9 +1614,9 @@ class UFOWriter(UFOReader):
Remove the file named fileName from the Remove the file named fileName from the
images directory. images directory.
""" """
if self._formatVersionMajor < 3: if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
raise UFOLibError( raise UFOLibError(
"Images are not allowed in UFO %d." % self._formatVersionMajor f"Images are not allowed in UFO {self._formatVersion.major}."
) )
self.removePath(f"{IMAGES_DIRNAME}/{fsdecode(fileName)}") self.removePath(f"{IMAGES_DIRNAME}/{fsdecode(fileName)}")
@ -1645,9 +1628,9 @@ class UFOWriter(UFOReader):
""" """
if validate is None: if validate is None:
validate = self._validate validate = self._validate
if self._formatVersionMajor < 3: if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
raise UFOLibError( raise UFOLibError(
"Images are not allowed in UFO %d." % self._formatVersionMajor f"Images are not allowed in UFO {self._formatVersion.major}."
) )
sourcePath = f"{IMAGES_DIRNAME}/{fsdecode(sourceFileName)}" sourcePath = f"{IMAGES_DIRNAME}/{fsdecode(sourceFileName)}"
destPath = f"{IMAGES_DIRNAME}/{fsdecode(destFileName)}" destPath = f"{IMAGES_DIRNAME}/{fsdecode(destFileName)}"

View File

@ -4,5 +4,13 @@ class UFOLibError(Exception):
pass pass
class UnsupportedUFOFormat(UFOLibError):
pass
class GlifLibError(UFOLibError): class GlifLibError(UFOLibError):
pass pass
class UnsupportedGLIFFormat(GlifLibError):
pass

View File

@ -11,6 +11,7 @@ glyph data. See the class doc string for details.
""" """
import logging import logging
import enum
from warnings import warn from warnings import warn
from collections import OrderedDict from collections import OrderedDict
import fs import fs
@ -33,14 +34,8 @@ from fontTools.ufoLib.validators import (
glyphLibValidator, glyphLibValidator,
) )
from fontTools.misc import etree from fontTools.misc import etree
from fontTools.ufoLib import ( from fontTools.ufoLib import _UFOBaseIO, UFOFormatVersion
_UFOBaseIO, from fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin
FormatVersion,
LATEST_UFO_FORMAT,
UFO_FORMAT_3_0,
supportedUFOFormatVersions,
)
from fontTools.ufoLib.utils import numberTypes
__all__ = [ __all__ = [
@ -60,15 +55,27 @@ logger = logging.getLogger(__name__)
CONTENTS_FILENAME = "contents.plist" CONTENTS_FILENAME = "contents.plist"
LAYERINFO_FILENAME = "layerinfo.plist" LAYERINFO_FILENAME = "layerinfo.plist"
GLIF_FORMAT_1_0 = FormatVersion(1)
GLIF_FORMAT_2_0 = FormatVersion(2)
supportedGLIFFormatVersions = { class GLIFFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum):
GLIF_FORMAT_1_0, FORMAT_1_0 = (1, 0)
GLIF_FORMAT_2_0, FORMAT_2_0 = (2, 0)
}
LATEST_GLIF_FORMAT = sorted(supportedGLIFFormatVersions)[-1] @classmethod
def default(cls, ufoFormatVersion=None):
if ufoFormatVersion is not None:
return max(cls.supported_versions(ufoFormatVersion))
return super().default()
@classmethod
def supported_versions(cls, ufoFormatVersion=None):
if ufoFormatVersion is None:
# if ufo format unspecified, return all the supported GLIF formats
return super().supported_versions()
# else only return the GLIF formats supported by the given UFO format
versions = {cls.FORMAT_1_0}
if ufoFormatVersion >= UFOFormatVersion.FORMAT_3_0:
versions.add(cls.FORMAT_2_0)
return frozenset(versions)
# ------------ # ------------
@ -125,7 +132,7 @@ class GlyphSet(_UFOBaseIO):
self, self,
path, path,
glyphNameToFileNameFunc=None, glyphNameToFileNameFunc=None,
ufoFormatVersion=LATEST_UFO_FORMAT, ufoFormatVersion=None,
validateRead=True, validateRead=True,
validateWrite=True, validateWrite=True,
): ):
@ -142,10 +149,18 @@ class GlyphSet(_UFOBaseIO):
``validateRead`` will validate read operations. Its default is ``True``. ``validateRead`` will validate read operations. Its default is ``True``.
``validateWrite`` will validate write operations. Its default is ``True``. ``validateWrite`` will validate write operations. Its default is ``True``.
""" """
if not isinstance(ufoFormatVersion, FormatVersion): try:
ufoFormatVersion = FormatVersion(ufoFormatVersion) ufoFormatVersion = UFOFormatVersion(ufoFormatVersion)
if ufoFormatVersion not in supportedUFOFormatVersions and validateRead: except ValueError as e:
raise GlifLibError("Unsupported UFO format version: %d.%d" % ufoFormatVersion) from fontTools.ufoLib.errors import UnsupportedUFOFormat
raise UnsupportedUFOFormat(
f"Unsupported UFO format: {ufoFormatVersion!r}"
) from e
if hasattr(path, "__fspath__"): # support os.PathLike objects
path = path.__fspath__()
if isinstance(path, str): if isinstance(path, str):
try: try:
filesystem = fs.osfs.OSFS(path) filesystem = fs.osfs.OSFS(path)
@ -369,9 +384,7 @@ class GlyphSet(_UFOBaseIO):
validate = self._validateRead validate = self._validateRead
text = self.getGLIF(glyphName) text = self.getGLIF(glyphName)
tree = _glifTreeFromString(text) tree = _glifTreeFromString(text)
formatVersions = {GLIF_FORMAT_1_0} formatVersions = GLIFFormatVersion.supported_versions(self.ufoFormatVersionTuple)
if self.ufoFormatVersionTuple >= UFO_FORMAT_3_0:
formatVersions.add(GLIF_FORMAT_2_0)
_readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate) _readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate)
def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=None, validate=None): def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=None, validate=None):
@ -401,23 +414,35 @@ class GlyphSet(_UFOBaseIO):
The GLIF format version will be chosen based on the ufoFormatVersion The GLIF format version will be chosen based on the ufoFormatVersion
passed during the creation of this object. If a particular format passed during the creation of this object. If a particular format
version is desired, it can be passed with the formatVersion argument. version is desired, it can be passed with the formatVersion argument.
The formatVersion argument accepts either a tuple of integers for
(major, minor), or a single integer for the major digit only (with
minor digit implied as 0).
An UnsupportedGLIFFormat exception is raised if the requested GLIF
formatVersion is not supported.
``validate`` will validate the data, by default it is set to the ``validate`` will validate the data, by default it is set to the
class's ``validateWrite`` value, can be overridden. class's ``validateWrite`` value, can be overridden.
""" """
if formatVersion is None: if formatVersion is None:
if self.ufoFormatVersionTuple >= UFO_FORMAT_3_0: formatVersion = GLIFFormatVersion.default(self.ufoFormatVersionTuple)
formatVersion = GLIF_FORMAT_2_0 else:
else: try:
formatVersion = GLIF_FORMAT_1_0 formatVersion = GLIFFormatVersion(formatVersion)
elif not isinstance(formatVersion, FormatVersion): except ValueError as e:
formatVersion = FormatVersion(formatVersion) from fontTools.ufoLib.errors import UnsupportedGLIFFormat
if formatVersion not in supportedGLIFFormatVersions:
raise GlifLibError("Unsupported GLIF format version: %d.%d" % formatVersion) raise UnsupportedGLIFFormat(
if formatVersion.major == 2 and self.ufoFormatVersionTuple.major < 3: f"Unsupported GLIF format version: {formatVersion!r}"
raise GlifLibError( ) from e
"Unsupported GLIF format version (%d.%d) for UFO format version %d.%d." if formatVersion not in GLIFFormatVersion.supported_versions(
% (*formatVersion, *self.ufoFormatVersionTuple) self.ufoFormatVersionTuple
):
from fontTools.ufoLib.errors import UnsupportedGLIFFormat
raise UnsupportedGLIFFormat(
f"Unsupported GLIF format version ({formatVersion!s}) "
f"for UFO format version {self.ufoFormatVersionTuple!s}."
) )
if validate is None: if validate is None:
validate = self._validateWrite validate = self._validateWrite
@ -555,7 +580,7 @@ def readGlyphFromString(
aString, aString,
glyphObject=None, glyphObject=None,
pointPen=None, pointPen=None,
formatVersions=supportedGLIFFormatVersions, formatVersions=None,
validate=True, validate=True,
): ):
""" """
@ -586,17 +611,37 @@ def readGlyphFromString(
conforming to the PointPen protocol as the 'pointPen' argument. conforming to the PointPen protocol as the 'pointPen' argument.
This argument may be None if you don't need the outline data. This argument may be None if you don't need the outline data.
The formatVersions argument defined the GLIF format versions The formatVersions optional argument defines the GLIF format versions
that are allowed to be read. that are allowed to be read.
The type is Optional[Iterable[Tuple[int, int], int]]. It can contain
either integers (for the major versions to be allowed, with minor
digits defaulting to 0), or tuples of integers to specify both
(major, minor) versions.
``validate`` will validate the read data. It is set to ``True`` by default. ``validate`` will validate the read data. It is set to ``True`` by default.
""" """
tree = _glifTreeFromString(aString) tree = _glifTreeFromString(aString)
formatVersions = {
FormatVersion(v) if not isinstance(v, FormatVersion) else v if formatVersions is None:
for v in formatVersions validFormatVersions = GLIFFormatVersion.supported_versions()
} else:
_readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate) validFormatVersions, invalidFormatVersions = set(), set()
for v in formatVersions:
try:
formatVersion = GLIFFormatVersion(v)
except ValueError:
invalidFormatVersions.add(v)
else:
validFormatVersions.add(formatVersion)
if not validFormatVersions:
raise ValueError(
"None of the requested GLIF formatVersions are supported: "
f"{formatVersions!r}"
)
_readGlyphFromTree(
tree, glyphObject, pointPen, formatVersions=validFormatVersions, validate=validate
)
def _writeGlyphToBytes( def _writeGlyphToBytes(
@ -604,10 +649,16 @@ def _writeGlyphToBytes(
glyphObject=None, glyphObject=None,
drawPointsFunc=None, drawPointsFunc=None,
writer=None, writer=None,
formatVersion=LATEST_GLIF_FORMAT, formatVersion=None,
validate=True, validate=True,
): ):
"""Return .glif data for a glyph as a UTF-8 encoded bytes string.""" """Return .glif data for a glyph as a UTF-8 encoded bytes string."""
try:
formatVersion = GLIFFormatVersion(formatVersion)
except ValueError:
from fontTools.ufoLib.errors import UnsupportedGLIFFormat
raise UnsupportedGLIFFormat("Unsupported GLIF format version: {formatVersion!r}")
# start # start
if validate and not isinstance(glyphName, str): if validate and not isinstance(glyphName, str):
raise GlifLibError("The glyph name is not properly formatted.") raise GlifLibError("The glyph name is not properly formatted.")
@ -660,7 +711,7 @@ def writeGlyphToString(
glyphName, glyphName,
glyphObject=None, glyphObject=None,
drawPointsFunc=None, drawPointsFunc=None,
formatVersion=LATEST_GLIF_FORMAT, formatVersion=None,
validate=True, validate=True,
): ):
""" """
@ -688,11 +739,14 @@ def writeGlyphToString(
proper PointPen methods to transfer the outline to the .glif file. proper PointPen methods to transfer the outline to the .glif file.
The GLIF format version can be specified with the formatVersion argument. The GLIF format version can be specified with the formatVersion argument.
This accepts either a tuple of integers for (major, minor), or a single
integer for the major digit only (with minor digit implied as 0).
An UnsupportedGLIFFormat exception is raised if the requested UFO
formatVersion is not supported.
``validate`` will validate the written data. It is set to ``True`` by default. ``validate`` will validate the written data. It is set to ``True`` by default.
""" """
if not isinstance(formatVersion, FormatVersion):
formatVersion = FormatVersion(formatVersion)
data = _writeGlyphToBytes( data = _writeGlyphToBytes(
glyphName, glyphName,
glyphObject=glyphObject, glyphObject=glyphObject,
@ -931,7 +985,7 @@ def _readGlyphFromTree(
tree, tree,
glyphObject=None, glyphObject=None,
pointPen=None, pointPen=None,
formatVersions=supportedGLIFFormatVersions, formatVersions=GLIFFormatVersion.supported_versions(),
validate=True, validate=True,
): ):
# check the format version # check the format version
@ -940,27 +994,40 @@ def _readGlyphFromTree(
raise GlifLibError("Unspecified format version in GLIF.") raise GlifLibError("Unspecified format version in GLIF.")
formatVersionMinor = tree.get("formatMinor", 0) formatVersionMinor = tree.get("formatMinor", 0)
try: try:
formatVersion = FormatVersion(int(formatVersionMajor), int(formatVersionMinor)) formatVersion = GLIFFormatVersion((int(formatVersionMajor), int(formatVersionMinor)))
except ValueError: except ValueError as e:
raise GlifLibError( msg = "Unsupported GLIF format: %s.%s" % (formatVersionMajor, formatVersionMinor)
"Invalid GLIF format version: (%r, %r)" % (formatVersionMajor, formatVersionMinor)
)
if validate and formatVersion not in formatVersions:
raise GlifLibError("Forbidden GLIF format version: %s.%s" % formatVersion)
readGlyphFromTree = _READ_GLYPH_FROM_TREE_FUNCS.get(formatVersion)
if not readGlyphFromTree:
msg = "Unsupported GLIF format version: %s.%s" % formatVersion
if validate: if validate:
raise GlifLibError(msg) from fontTools.ufoLib.errors import UnsupportedGLIFFormat
raise UnsupportedGLIFFormat(msg) from e
# warn but continue using the latest supported format # warn but continue using the latest supported format
logger.warn("%s. Some data may be skipped or parsed incorrectly.", msg) formatVersion = GLIFFormatVersion.default()
readGlyphFromTree = _READ_GLYPH_FROM_TREE_FUNCS[LATEST_GLIF_FORMAT] logger.warning(
"%s. Assuming the latest supported version (%s). "
"Some data may be skipped or parsed incorrectly.",
msg,
formatVersion,
)
readGlyphFromTree(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate) if validate and formatVersion not in formatVersions:
raise GlifLibError(f"Forbidden GLIF format version: {formatVersion!s}")
try:
readGlyphFromTree = _READ_GLYPH_FROM_TREE_FUNCS[formatVersion]
except KeyError:
raise NotImplementedError(formatVersion)
readGlyphFromTree(
tree=tree,
glyphObject=glyphObject,
pointPen=pointPen,
validate=validate,
formatMinor=formatVersion.minor,
)
def _readGlyphFromTreeFormat1(tree, glyphObject=None, pointPen=None, validate=None): def _readGlyphFromTreeFormat1(tree, glyphObject=None, pointPen=None, validate=None, **kwargs):
# get the name # get the name
_readName(glyphObject, tree, validate) _readName(glyphObject, tree, validate)
# populate the sub elements # populate the sub elements
@ -1098,8 +1165,8 @@ def _readGlyphFromTreeFormat2(
_READ_GLYPH_FROM_TREE_FUNCS = { _READ_GLYPH_FROM_TREE_FUNCS = {
GLIF_FORMAT_1_0: _readGlyphFromTreeFormat1, GLIFFormatVersion.FORMAT_1_0: _readGlyphFromTreeFormat1,
GLIF_FORMAT_2_0: _readGlyphFromTreeFormat2, GLIFFormatVersion.FORMAT_2_0: _readGlyphFromTreeFormat2,
} }
@ -1585,12 +1652,10 @@ class GLIFPointPen(AbstractPointPen):
part of .glif files. part of .glif files.
""" """
def __init__(self, element, formatVersion=LATEST_GLIF_FORMAT, identifiers=None, validate=True): def __init__(self, element, formatVersion=None, identifiers=None, validate=True):
if identifiers is None: if identifiers is None:
identifiers = set() identifiers = set()
if not isinstance(formatVersion, FormatVersion): self.formatVersion = GLIFFormatVersion(formatVersion)
formatVersion = FormatVersion(formatVersion)
self.formatVersion = formatVersion
self.identifiers = identifiers self.identifiers = identifiers
self.outline = element self.outline = element
self.contour = None self.contour = None