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:
parent
f120be17e4
commit
57f4904363
@ -4,7 +4,7 @@ from os import fsdecode
|
||||
import logging
|
||||
import zipfile
|
||||
import enum
|
||||
import collections
|
||||
from collections import OrderedDict
|
||||
import fs
|
||||
import fs.base
|
||||
import fs.subfs
|
||||
@ -14,13 +14,12 @@ import fs.osfs
|
||||
import fs.zipfs
|
||||
import fs.tempfs
|
||||
import fs.tools
|
||||
from fontTools.misc.py23 import tostr
|
||||
from fontTools.misc import plistlib
|
||||
from fontTools.ufoLib.validators import *
|
||||
from fontTools.ufoLib.filenames import userNameToFileName
|
||||
from fontTools.ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning
|
||||
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.
|
||||
@ -94,32 +93,10 @@ LAYERINFO_FILENAME = "layerinfo.plist"
|
||||
DEFAULT_LAYER_NAME = "public.default"
|
||||
|
||||
|
||||
class FormatVersion(collections.namedtuple("FormatVersion", ["major", "minor"])):
|
||||
"""Convert single-digit formatVersion into (major, minor=0) namedtuple.
|
||||
|
||||
This allows to ease transition to new format versions that define a minor
|
||||
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 UFOFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum):
|
||||
FORMAT_1_0 = (1, 0)
|
||||
FORMAT_2_0 = (2, 0)
|
||||
FORMAT_3_0 = (3, 0)
|
||||
|
||||
|
||||
class UFOFileStructure(enum.Enum):
|
||||
@ -315,7 +292,7 @@ class UFOReader(_UFOBaseIO):
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self._formatVersionMajor
|
||||
return self._formatVersion.major
|
||||
|
||||
formatVersion = property(
|
||||
_get_formatVersion,
|
||||
@ -327,7 +304,7 @@ class UFOReader(_UFOBaseIO):
|
||||
"""The (major, minor) format version of the UFO.
|
||||
This is determined by reading metainfo.plist during __init__.
|
||||
"""
|
||||
return FormatVersion(self._formatVersionMajor, self._formatVersionMinor)
|
||||
return self._formatVersion
|
||||
|
||||
def _get_fileStructure(self):
|
||||
return self._fileStructure
|
||||
@ -435,29 +412,33 @@ class UFOReader(_UFOBaseIO):
|
||||
data = self._getPlist(METAINFO_FILENAME)
|
||||
if validate and not isinstance(data, dict):
|
||||
raise UFOLibError("metainfo.plist is not properly formatted.")
|
||||
try:
|
||||
formatVersionMajor = data["formatVersion"]
|
||||
formatVersionMinor = data.setdefault("formatVersionMinor", 0)
|
||||
if validate:
|
||||
if not (
|
||||
isinstance(formatVersionMajor, int) and
|
||||
isinstance(formatVersionMinor, int)
|
||||
):
|
||||
except KeyError:
|
||||
raise UFOLibError(
|
||||
"formatVersion must be specified as an integer in '%s' on %s"
|
||||
% (METAINFO_FILENAME, self.fs)
|
||||
f"Missing required formatVersion in '{METAINFO_FILENAME}' on {self.fs}"
|
||||
)
|
||||
formatVersionMinor = data.setdefault("formatVersionMinor", 0)
|
||||
|
||||
if (formatVersionMajor, formatVersionMinor) not in supportedUFOFormatVersions:
|
||||
try:
|
||||
formatVersion = UFOFormatVersion((formatVersionMajor, formatVersionMinor))
|
||||
except ValueError as e:
|
||||
unsupportedMsg = (
|
||||
"Unsupported UFO format (%d.%d) in '%s' on %s"
|
||||
% (formatVersionMajor, formatVersionMinor, METAINFO_FILENAME, self.fs)
|
||||
f"Unsupported UFO format ({formatVersionMajor}.{formatVersionMinor}) "
|
||||
f"in '{METAINFO_FILENAME}' on {self.fs}"
|
||||
)
|
||||
if validate:
|
||||
raise UFOLibError(unsupportedMsg)
|
||||
logger.warn(
|
||||
"%s. Some data may be skipped or parsed incorrectly", unsupportedMsg
|
||||
)
|
||||
from fontTools.ufoLib.errors import UnsupportedUFOFormat
|
||||
|
||||
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
|
||||
|
||||
def readMetaInfo(self, validate=None):
|
||||
@ -468,8 +449,7 @@ class UFOReader(_UFOBaseIO):
|
||||
to the class's validate value, can be overridden.
|
||||
"""
|
||||
data = self._readMetaInfo(validate=validate)
|
||||
self._formatVersionMajor = data["formatVersion"]
|
||||
self._formatVersionMinor = data["formatVersionMinor"]
|
||||
self._formatVersion = data["formatVersionTuple"]
|
||||
|
||||
# groups.plist
|
||||
|
||||
@ -485,7 +465,7 @@ class UFOReader(_UFOBaseIO):
|
||||
if validate is None:
|
||||
validate = self._validate
|
||||
# handle up conversion
|
||||
if self._formatVersionMajor < 3:
|
||||
if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
|
||||
self._upConvertKerning(validate)
|
||||
groups = self._upConvertedKerningData["groups"]
|
||||
# normal
|
||||
@ -516,7 +496,7 @@ class UFOReader(_UFOBaseIO):
|
||||
"""
|
||||
if validate is None:
|
||||
validate = self._validate
|
||||
if self._formatVersionMajor >= 3:
|
||||
if self._formatVersion >= UFOFormatVersion.FORMAT_3_0:
|
||||
return dict(side1={}, side2={})
|
||||
# use the public group reader to force the load and
|
||||
# conversion of the data if it hasn't happened yet.
|
||||
@ -546,7 +526,7 @@ class UFOReader(_UFOBaseIO):
|
||||
infoDict = self._readInfo(validate)
|
||||
infoDataToSet = {}
|
||||
# version 1
|
||||
if self._formatVersionMajor == 1:
|
||||
if self._formatVersion == UFOFormatVersion.FORMAT_1_0:
|
||||
for attr in fontInfoAttributesVersion1:
|
||||
value = infoDict.get(attr)
|
||||
if value is not None:
|
||||
@ -554,15 +534,15 @@ class UFOReader(_UFOBaseIO):
|
||||
infoDataToSet = _convertFontInfoDataVersion1ToVersion2(infoDataToSet)
|
||||
infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet)
|
||||
# version 2
|
||||
elif self._formatVersionMajor == 2:
|
||||
elif self._formatVersion == UFOFormatVersion.FORMAT_2_0:
|
||||
for attr, dataValidationDict in list(fontInfoAttributesVersion2ValueData.items()):
|
||||
value = infoDict.get(attr)
|
||||
if value is None:
|
||||
continue
|
||||
infoDataToSet[attr] = value
|
||||
infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet)
|
||||
# version 3
|
||||
elif self._formatVersionMajor == 3:
|
||||
# version 3.x
|
||||
elif self._formatVersion.major == UFOFormatVersion.FORMAT_3_0.major:
|
||||
for attr, dataValidationDict in list(fontInfoAttributesVersion3ValueData.items()):
|
||||
value = infoDict.get(attr)
|
||||
if value is None:
|
||||
@ -570,7 +550,7 @@ class UFOReader(_UFOBaseIO):
|
||||
infoDataToSet[attr] = value
|
||||
# unsupported version
|
||||
else:
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError(self._formatVersion)
|
||||
# validate data
|
||||
if validate:
|
||||
infoDataToSet = validateInfoVersion3Data(infoDataToSet)
|
||||
@ -597,7 +577,7 @@ class UFOReader(_UFOBaseIO):
|
||||
if validate is None:
|
||||
validate = self._validate
|
||||
# handle up conversion
|
||||
if self._formatVersionMajor < 3:
|
||||
if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
|
||||
self._upConvertKerning(validate)
|
||||
kerningNested = self._upConvertedKerningData["kerning"]
|
||||
# normal
|
||||
@ -655,7 +635,7 @@ class UFOReader(_UFOBaseIO):
|
||||
|
||||
``validate`` will validate the layer contents.
|
||||
"""
|
||||
if self._formatVersionMajor < 3:
|
||||
if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
|
||||
return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)]
|
||||
contents = self._getPlist(LAYERCONTENTS_FILENAME)
|
||||
if validate:
|
||||
@ -730,7 +710,7 @@ class UFOReader(_UFOBaseIO):
|
||||
)
|
||||
return GlyphSet(
|
||||
glyphSubFS,
|
||||
ufoFormatVersion=self.formatVersionTuple,
|
||||
ufoFormatVersion=self._formatVersion,
|
||||
validateRead=validateRead,
|
||||
validateWrite=validateWrite,
|
||||
)
|
||||
@ -787,7 +767,7 @@ class UFOReader(_UFOBaseIO):
|
||||
``validate`` will validate the data, by default it is set to the
|
||||
class's validate value, can be overridden.
|
||||
"""
|
||||
if self._formatVersionMajor < 3:
|
||||
if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
|
||||
return []
|
||||
if validate is None:
|
||||
validate = self._validate
|
||||
@ -837,9 +817,9 @@ class UFOReader(_UFOBaseIO):
|
||||
"""
|
||||
if validate is None:
|
||||
validate = self._validate
|
||||
if self._formatVersionMajor < 3:
|
||||
if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
|
||||
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)
|
||||
try:
|
||||
@ -880,20 +860,30 @@ class UFOWriter(UFOReader):
|
||||
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
|
||||
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__(
|
||||
self,
|
||||
path,
|
||||
formatVersion=LATEST_UFO_FORMAT,
|
||||
formatVersion=None,
|
||||
fileCreator="com.github.fonttools.ufoLib",
|
||||
structure=None,
|
||||
validate=True,
|
||||
):
|
||||
if not isinstance(formatVersion, FormatVersion):
|
||||
formatVersion = FormatVersion(formatVersion)
|
||||
if formatVersion not in supportedUFOFormatVersions:
|
||||
raise UFOLibError("Unsupported UFO format (%d.%d)." % formatVersion)
|
||||
try:
|
||||
formatVersion = UFOFormatVersion(formatVersion)
|
||||
except ValueError as e:
|
||||
from fontTools.ufoLib.errors import UnsupportedUFOFormat
|
||||
|
||||
raise UnsupportedUFOFormat(f"Unsupported UFO format: {formatVersion!r}") from e
|
||||
|
||||
if hasattr(path, "__fspath__"): # support os.PathLike objects
|
||||
path = path.__fspath__()
|
||||
@ -1001,7 +991,7 @@ class UFOWriter(UFOReader):
|
||||
|
||||
# establish some basic stuff
|
||||
self._path = fsdecode(path)
|
||||
self._formatVersionMajor, self._formatVersionMinor = formatVersion
|
||||
self._formatVersion = formatVersion
|
||||
self._fileCreator = fileCreator
|
||||
self._downConversionKerningData = None
|
||||
self._validate = validate
|
||||
@ -1010,21 +1000,21 @@ class UFOWriter(UFOReader):
|
||||
previousFormatVersion = None
|
||||
if self._havePreviousFile:
|
||||
metaInfo = self._readMetaInfo(validate=validate)
|
||||
previousFormatVersion = FormatVersion(
|
||||
metaInfo["formatVersion"], metaInfo["formatVersionMinor"]
|
||||
)
|
||||
previousFormatVersion = metaInfo["formatVersionTuple"]
|
||||
# catch down conversion
|
||||
if previousFormatVersion > formatVersion:
|
||||
raise UFOLibError(
|
||||
"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."
|
||||
% (*previousFormatVersion, *formatVersion)
|
||||
from fontTools.ufoLib.errors import UnsupportedUFOFormat
|
||||
|
||||
raise UnsupportedUFOFormat(
|
||||
"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
|
||||
self.layerContents = {}
|
||||
if previousFormatVersion is not None and previousFormatVersion.major >= 3:
|
||||
# already exists
|
||||
self.layerContents = collections.OrderedDict(self._readLayerContents(validate))
|
||||
self.layerContents = OrderedDict(self._readLayerContents(validate))
|
||||
else:
|
||||
# previous < 3
|
||||
# imply the layer contents
|
||||
@ -1158,10 +1148,10 @@ class UFOWriter(UFOReader):
|
||||
def _writeMetaInfo(self):
|
||||
metaInfo = dict(
|
||||
creator=self._fileCreator,
|
||||
formatVersion=self._formatVersionMajor,
|
||||
formatVersion=self._formatVersion.major,
|
||||
)
|
||||
if self._formatVersionMinor != 0:
|
||||
metaInfo["formatVersionMinor"] = self._formatVersionMinor
|
||||
if self._formatVersion.minor != 0:
|
||||
metaInfo["formatVersionMinor"] = self._formatVersion.minor
|
||||
self._writePlist(METAINFO_FILENAME, metaInfo)
|
||||
|
||||
# groups.plist
|
||||
@ -1182,7 +1172,7 @@ class UFOWriter(UFOReader):
|
||||
This is the same form returned by UFOReader's
|
||||
getKerningGroupConversionRenameMaps method.
|
||||
"""
|
||||
if self._formatVersionMajor >= 3:
|
||||
if self._formatVersion >= UFOFormatVersion.FORMAT_3_0:
|
||||
return # XXX raise an error here
|
||||
# flip the dictionaries
|
||||
remap = {}
|
||||
@ -1207,7 +1197,10 @@ class UFOWriter(UFOReader):
|
||||
if not valid:
|
||||
raise UFOLibError(message)
|
||||
# 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"]
|
||||
remappedGroups = {}
|
||||
# there are some edge cases here that are ignored:
|
||||
@ -1268,14 +1261,14 @@ class UFOWriter(UFOReader):
|
||||
continue
|
||||
infoData[attr] = value
|
||||
# down convert data if necessary and validate
|
||||
if self._formatVersionMajor == 3:
|
||||
if self._formatVersion == UFOFormatVersion.FORMAT_3_0:
|
||||
if validate:
|
||||
infoData = validateInfoVersion3Data(infoData)
|
||||
elif self._formatVersionMajor == 2:
|
||||
elif self._formatVersion == UFOFormatVersion.FORMAT_2_0:
|
||||
infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
|
||||
if validate:
|
||||
infoData = validateInfoVersion2Data(infoData)
|
||||
elif self._formatVersionMajor == 1:
|
||||
elif self._formatVersion == UFOFormatVersion.FORMAT_1_0:
|
||||
infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
|
||||
if validate:
|
||||
infoData = validateInfoVersion2Data(infoData)
|
||||
@ -1318,7 +1311,10 @@ class UFOWriter(UFOReader):
|
||||
if not isinstance(value, numberTypes):
|
||||
raise UFOLibError(invalidFormatMessage)
|
||||
# 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"]
|
||||
remappedKerning = {}
|
||||
for (side1, side2), value in list(kerning.items()):
|
||||
@ -1368,7 +1364,7 @@ class UFOWriter(UFOReader):
|
||||
"""
|
||||
if validate is None:
|
||||
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.")
|
||||
if validate:
|
||||
if not isinstance(features, str):
|
||||
@ -1387,7 +1383,7 @@ class UFOWriter(UFOReader):
|
||||
"""
|
||||
if validate is None:
|
||||
validate = self._validate
|
||||
if self._formatVersionMajor < 3:
|
||||
if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
|
||||
return
|
||||
if layerOrder is not None:
|
||||
newOrder = []
|
||||
@ -1435,9 +1431,12 @@ class UFOWriter(UFOReader):
|
||||
if validateWrite is None:
|
||||
validateWrite = self._validate
|
||||
# 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(
|
||||
"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
|
||||
if layerName is None and defaultLayer:
|
||||
@ -1449,42 +1448,27 @@ class UFOWriter(UFOReader):
|
||||
elif layerName is None and not defaultLayer:
|
||||
raise UFOLibError("A layer name must be provided for non-default layers.")
|
||||
# move along to format specific writing
|
||||
if self._formatVersionMajor == 1:
|
||||
return self._getGlyphSetFormatVersion1(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
|
||||
elif self._formatVersionMajor == 2:
|
||||
return self._getGlyphSetFormatVersion2(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
|
||||
elif self._formatVersionMajor == 3:
|
||||
if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
|
||||
return self._getDefaultGlyphSet(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
|
||||
elif self._formatVersion.major == UFOFormatVersion.FORMAT_3_0.major:
|
||||
return self._getGlyphSetFormatVersion3(
|
||||
validateRead,
|
||||
validateWrite,
|
||||
layerName=layerName,
|
||||
defaultLayer=defaultLayer,
|
||||
glyphNameToFileNameFunc=glyphNameToFileNameFunc,
|
||||
formatVersionMinor=self._formatVersionMinor,
|
||||
)
|
||||
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
|
||||
|
||||
glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True)
|
||||
return GlyphSet(
|
||||
glyphSubFS,
|
||||
glyphNameToFileNameFunc=glyphNameToFileNameFunc,
|
||||
ufoFormatVersion=UFO_FORMAT_1_0,
|
||||
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,
|
||||
ufoFormatVersion=self._formatVersion,
|
||||
validateRead=validateRead,
|
||||
validateWrite=validateWrite,
|
||||
)
|
||||
@ -1496,7 +1480,6 @@ class UFOWriter(UFOReader):
|
||||
layerName=None,
|
||||
defaultLayer=True,
|
||||
glyphNameToFileNameFunc=None,
|
||||
formatVersionMinor=0,
|
||||
):
|
||||
from fontTools.ufoLib.glifLib import GlyphSet
|
||||
|
||||
@ -1533,7 +1516,7 @@ class UFOWriter(UFOReader):
|
||||
return GlyphSet(
|
||||
glyphSubFS,
|
||||
glyphNameToFileNameFunc=glyphNameToFileNameFunc,
|
||||
ufoFormatVersion=FormatVersion(3, formatVersionMinor),
|
||||
ufoFormatVersion=self._formatVersion,
|
||||
validateRead=validateRead,
|
||||
validateWrite=validateWrite,
|
||||
)
|
||||
@ -1546,7 +1529,7 @@ class UFOWriter(UFOReader):
|
||||
layerName, it is up to the caller to inform that object that
|
||||
the directory it represents has changed.
|
||||
"""
|
||||
if self._formatVersionMajor < 3:
|
||||
if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
|
||||
# ignore renaming glyph sets for UFO1 UFO2
|
||||
# just write the data from the default layer
|
||||
return
|
||||
@ -1585,7 +1568,7 @@ class UFOWriter(UFOReader):
|
||||
"""
|
||||
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
|
||||
# just write the data from the default layer
|
||||
return
|
||||
@ -1615,9 +1598,9 @@ class UFOWriter(UFOReader):
|
||||
"""
|
||||
if validate is None:
|
||||
validate = self._validate
|
||||
if self._formatVersionMajor < 3:
|
||||
if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
|
||||
raise UFOLibError(
|
||||
"Images are not allowed in UFO %d." % self._formatVersionMajor
|
||||
f"Images are not allowed in UFO {self._formatVersion.major}."
|
||||
)
|
||||
fileName = fsdecode(fileName)
|
||||
if validate:
|
||||
@ -1631,9 +1614,9 @@ class UFOWriter(UFOReader):
|
||||
Remove the file named fileName from the
|
||||
images directory.
|
||||
"""
|
||||
if self._formatVersionMajor < 3:
|
||||
if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
|
||||
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)}")
|
||||
|
||||
@ -1645,9 +1628,9 @@ class UFOWriter(UFOReader):
|
||||
"""
|
||||
if validate is None:
|
||||
validate = self._validate
|
||||
if self._formatVersionMajor < 3:
|
||||
if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
|
||||
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)}"
|
||||
destPath = f"{IMAGES_DIRNAME}/{fsdecode(destFileName)}"
|
||||
|
@ -4,5 +4,13 @@ class UFOLibError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedUFOFormat(UFOLibError):
|
||||
pass
|
||||
|
||||
|
||||
class GlifLibError(UFOLibError):
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedGLIFFormat(GlifLibError):
|
||||
pass
|
||||
|
@ -11,6 +11,7 @@ glyph data. See the class doc string for details.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import enum
|
||||
from warnings import warn
|
||||
from collections import OrderedDict
|
||||
import fs
|
||||
@ -33,14 +34,8 @@ from fontTools.ufoLib.validators import (
|
||||
glyphLibValidator,
|
||||
)
|
||||
from fontTools.misc import etree
|
||||
from fontTools.ufoLib import (
|
||||
_UFOBaseIO,
|
||||
FormatVersion,
|
||||
LATEST_UFO_FORMAT,
|
||||
UFO_FORMAT_3_0,
|
||||
supportedUFOFormatVersions,
|
||||
)
|
||||
from fontTools.ufoLib.utils import numberTypes
|
||||
from fontTools.ufoLib import _UFOBaseIO, UFOFormatVersion
|
||||
from fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin
|
||||
|
||||
|
||||
__all__ = [
|
||||
@ -60,15 +55,27 @@ logger = logging.getLogger(__name__)
|
||||
CONTENTS_FILENAME = "contents.plist"
|
||||
LAYERINFO_FILENAME = "layerinfo.plist"
|
||||
|
||||
GLIF_FORMAT_1_0 = FormatVersion(1)
|
||||
GLIF_FORMAT_2_0 = FormatVersion(2)
|
||||
|
||||
supportedGLIFFormatVersions = {
|
||||
GLIF_FORMAT_1_0,
|
||||
GLIF_FORMAT_2_0,
|
||||
}
|
||||
class GLIFFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum):
|
||||
FORMAT_1_0 = (1, 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,
|
||||
path,
|
||||
glyphNameToFileNameFunc=None,
|
||||
ufoFormatVersion=LATEST_UFO_FORMAT,
|
||||
ufoFormatVersion=None,
|
||||
validateRead=True,
|
||||
validateWrite=True,
|
||||
):
|
||||
@ -142,10 +149,18 @@ class GlyphSet(_UFOBaseIO):
|
||||
``validateRead`` will validate read operations. Its default is ``True``.
|
||||
``validateWrite`` will validate write operations. Its default is ``True``.
|
||||
"""
|
||||
if not isinstance(ufoFormatVersion, FormatVersion):
|
||||
ufoFormatVersion = FormatVersion(ufoFormatVersion)
|
||||
if ufoFormatVersion not in supportedUFOFormatVersions and validateRead:
|
||||
raise GlifLibError("Unsupported UFO format version: %d.%d" % ufoFormatVersion)
|
||||
try:
|
||||
ufoFormatVersion = UFOFormatVersion(ufoFormatVersion)
|
||||
except ValueError as e:
|
||||
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):
|
||||
try:
|
||||
filesystem = fs.osfs.OSFS(path)
|
||||
@ -369,9 +384,7 @@ class GlyphSet(_UFOBaseIO):
|
||||
validate = self._validateRead
|
||||
text = self.getGLIF(glyphName)
|
||||
tree = _glifTreeFromString(text)
|
||||
formatVersions = {GLIF_FORMAT_1_0}
|
||||
if self.ufoFormatVersionTuple >= UFO_FORMAT_3_0:
|
||||
formatVersions.add(GLIF_FORMAT_2_0)
|
||||
formatVersions = GLIFFormatVersion.supported_versions(self.ufoFormatVersionTuple)
|
||||
_readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate)
|
||||
|
||||
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
|
||||
passed during the creation of this object. If a particular format
|
||||
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
|
||||
class's ``validateWrite`` value, can be overridden.
|
||||
"""
|
||||
if formatVersion is None:
|
||||
if self.ufoFormatVersionTuple >= UFO_FORMAT_3_0:
|
||||
formatVersion = GLIF_FORMAT_2_0
|
||||
formatVersion = GLIFFormatVersion.default(self.ufoFormatVersionTuple)
|
||||
else:
|
||||
formatVersion = GLIF_FORMAT_1_0
|
||||
elif not isinstance(formatVersion, FormatVersion):
|
||||
formatVersion = FormatVersion(formatVersion)
|
||||
if formatVersion not in supportedGLIFFormatVersions:
|
||||
raise GlifLibError("Unsupported GLIF format version: %d.%d" % formatVersion)
|
||||
if formatVersion.major == 2 and self.ufoFormatVersionTuple.major < 3:
|
||||
raise GlifLibError(
|
||||
"Unsupported GLIF format version (%d.%d) for UFO format version %d.%d."
|
||||
% (*formatVersion, *self.ufoFormatVersionTuple)
|
||||
try:
|
||||
formatVersion = GLIFFormatVersion(formatVersion)
|
||||
except ValueError as e:
|
||||
from fontTools.ufoLib.errors import UnsupportedGLIFFormat
|
||||
|
||||
raise UnsupportedGLIFFormat(
|
||||
f"Unsupported GLIF format version: {formatVersion!r}"
|
||||
) from e
|
||||
if formatVersion not in GLIFFormatVersion.supported_versions(
|
||||
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:
|
||||
validate = self._validateWrite
|
||||
@ -555,7 +580,7 @@ def readGlyphFromString(
|
||||
aString,
|
||||
glyphObject=None,
|
||||
pointPen=None,
|
||||
formatVersions=supportedGLIFFormatVersions,
|
||||
formatVersions=None,
|
||||
validate=True,
|
||||
):
|
||||
"""
|
||||
@ -586,17 +611,37 @@ def readGlyphFromString(
|
||||
conforming to the PointPen protocol as the 'pointPen' argument.
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
tree = _glifTreeFromString(aString)
|
||||
formatVersions = {
|
||||
FormatVersion(v) if not isinstance(v, FormatVersion) else v
|
||||
for v in formatVersions
|
||||
}
|
||||
_readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate)
|
||||
|
||||
if formatVersions is None:
|
||||
validFormatVersions = GLIFFormatVersion.supported_versions()
|
||||
else:
|
||||
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(
|
||||
@ -604,10 +649,16 @@ def _writeGlyphToBytes(
|
||||
glyphObject=None,
|
||||
drawPointsFunc=None,
|
||||
writer=None,
|
||||
formatVersion=LATEST_GLIF_FORMAT,
|
||||
formatVersion=None,
|
||||
validate=True,
|
||||
):
|
||||
"""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
|
||||
if validate and not isinstance(glyphName, str):
|
||||
raise GlifLibError("The glyph name is not properly formatted.")
|
||||
@ -660,7 +711,7 @@ def writeGlyphToString(
|
||||
glyphName,
|
||||
glyphObject=None,
|
||||
drawPointsFunc=None,
|
||||
formatVersion=LATEST_GLIF_FORMAT,
|
||||
formatVersion=None,
|
||||
validate=True,
|
||||
):
|
||||
"""
|
||||
@ -688,11 +739,14 @@ def writeGlyphToString(
|
||||
proper PointPen methods to transfer the outline to the .glif file.
|
||||
|
||||
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.
|
||||
"""
|
||||
if not isinstance(formatVersion, FormatVersion):
|
||||
formatVersion = FormatVersion(formatVersion)
|
||||
data = _writeGlyphToBytes(
|
||||
glyphName,
|
||||
glyphObject=glyphObject,
|
||||
@ -931,7 +985,7 @@ def _readGlyphFromTree(
|
||||
tree,
|
||||
glyphObject=None,
|
||||
pointPen=None,
|
||||
formatVersions=supportedGLIFFormatVersions,
|
||||
formatVersions=GLIFFormatVersion.supported_versions(),
|
||||
validate=True,
|
||||
):
|
||||
# check the format version
|
||||
@ -940,27 +994,40 @@ def _readGlyphFromTree(
|
||||
raise GlifLibError("Unspecified format version in GLIF.")
|
||||
formatVersionMinor = tree.get("formatMinor", 0)
|
||||
try:
|
||||
formatVersion = FormatVersion(int(formatVersionMajor), int(formatVersionMinor))
|
||||
except ValueError:
|
||||
raise GlifLibError(
|
||||
"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
|
||||
formatVersion = GLIFFormatVersion((int(formatVersionMajor), int(formatVersionMinor)))
|
||||
except ValueError as e:
|
||||
msg = "Unsupported GLIF format: %s.%s" % (formatVersionMajor, formatVersionMinor)
|
||||
if validate:
|
||||
raise GlifLibError(msg)
|
||||
from fontTools.ufoLib.errors import UnsupportedGLIFFormat
|
||||
|
||||
raise UnsupportedGLIFFormat(msg) from e
|
||||
# warn but continue using the latest supported format
|
||||
logger.warn("%s. Some data may be skipped or parsed incorrectly.", msg)
|
||||
readGlyphFromTree = _READ_GLYPH_FROM_TREE_FUNCS[LATEST_GLIF_FORMAT]
|
||||
formatVersion = GLIFFormatVersion.default()
|
||||
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
|
||||
_readName(glyphObject, tree, validate)
|
||||
# populate the sub elements
|
||||
@ -1098,8 +1165,8 @@ def _readGlyphFromTreeFormat2(
|
||||
|
||||
|
||||
_READ_GLYPH_FROM_TREE_FUNCS = {
|
||||
GLIF_FORMAT_1_0: _readGlyphFromTreeFormat1,
|
||||
GLIF_FORMAT_2_0: _readGlyphFromTreeFormat2,
|
||||
GLIFFormatVersion.FORMAT_1_0: _readGlyphFromTreeFormat1,
|
||||
GLIFFormatVersion.FORMAT_2_0: _readGlyphFromTreeFormat2,
|
||||
}
|
||||
|
||||
|
||||
@ -1585,12 +1652,10 @@ class GLIFPointPen(AbstractPointPen):
|
||||
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:
|
||||
identifiers = set()
|
||||
if not isinstance(formatVersion, FormatVersion):
|
||||
formatVersion = FormatVersion(formatVersion)
|
||||
self.formatVersion = formatVersion
|
||||
self.formatVersion = GLIFFormatVersion(formatVersion)
|
||||
self.identifiers = identifiers
|
||||
self.outline = element
|
||||
self.contour = None
|
||||
|
Loading…
x
Reference in New Issue
Block a user