WIP: ufoLib: support UFO formatVersionMinor and GLIF formatMinor

This commit is contained in:
Cosimo Lupo 2019-11-22 19:08:42 +00:00
parent cfe4eb039a
commit c066cfc4d5
No known key found for this signature in database
GPG Key ID: 179A8F0895A02F4F
2 changed files with 285 additions and 118 deletions

View File

@ -4,7 +4,7 @@ from os import fsdecode
import logging import logging
import zipfile import zipfile
import enum import enum
from collections import OrderedDict import collections
import fs import fs
import fs.base import fs.base
import fs.subfs import fs.subfs
@ -93,7 +93,33 @@ LAYERINFO_FILENAME = "layerinfo.plist"
DEFAULT_LAYER_NAME = "public.default" DEFAULT_LAYER_NAME = "public.default"
supportedUFOFormatVersions = [1, 2, 3]
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 UFOFileStructure(enum.Enum): class UFOFileStructure(enum.Enum):
@ -282,9 +308,26 @@ class UFOReader(_UFOBaseIO):
path = property(_get_path, doc="The path of the UFO (DEPRECATED).") path = property(_get_path, doc="The path of the UFO (DEPRECATED).")
def _get_formatVersion(self): def _get_formatVersion(self):
return self._formatVersion import warnings
formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is determined by reading metainfo.plist during __init__.") warnings.warn(
"The 'formatVersion' attribute is deprecated; use the 'formatVersionTuple'",
DeprecationWarning,
stacklevel=2,
)
return self._formatVersionMajor
formatVersion = property(
_get_formatVersion,
doc="The (major) format version of the UFO. DEPRECATED: Use formatVersionTuple"
)
@property
def formatVersionTuple(self):
"""The (major, minor) format version of the UFO.
This is determined by reading metainfo.plist during __init__.
"""
return FormatVersion(self._formatVersionMajor, self._formatVersionMinor)
def _get_fileStructure(self): def _get_fileStructure(self):
return self._fileStructure return self._fileStructure
@ -380,9 +423,9 @@ class UFOReader(_UFOBaseIO):
return None return None
# metainfo.plist # metainfo.plist
def readMetaInfo(self, validate=None): def _readMetaInfo(self, validate=None):
""" """
Read metainfo.plist. Only used for internal operations. Read metainfo.plist and return raw data. Only used for internal operations.
``validate`` will validate the read data, by default it is set ``validate`` will validate the read data, by default it is set
to the class's validate value, can be overridden. to the class's validate value, can be overridden.
@ -392,19 +435,41 @@ 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.")
formatVersion = data["formatVersion"] formatVersionMajor = data["formatVersion"]
formatVersionMinor = data.setdefault("formatVersionMinor", 0)
if validate: if validate:
if not isinstance(formatVersion, int): if not (
isinstance(formatVersionMajor, int) and
isinstance(formatVersionMinor, int)
):
raise UFOLibError( raise UFOLibError(
"formatVersion must be specified as an integer in '%s' on %s" "formatVersion must be specified as an integer in '%s' on %s"
% (METAINFO_FILENAME, self.fs) % (METAINFO_FILENAME, self.fs)
) )
if formatVersion not in supportedUFOFormatVersions:
raise UFOLibError( if (formatVersionMajor, formatVersionMinor) not in supportedUFOFormatVersions:
"Unsupported UFO format (%d) in '%s' on %s" unsupportedMsg = (
% (formatVersion, METAINFO_FILENAME, self.fs) "Unsupported UFO format (%d.%d) in '%s' on %s"
) % (formatVersionMajor, formatVersionMinor, METAINFO_FILENAME, self.fs)
self._formatVersion = formatVersion )
if validate:
raise UFOLibError(unsupportedMsg)
logger.warn(
"%s. Some data may be skipped or parsed incorrectly", unsupportedMsg
)
return data
def readMetaInfo(self, validate=None):
"""
Read metainfo.plist and set formatVersion. 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.
"""
data = self._readMetaInfo(validate=validate)
self._formatVersionMajor = data["formatVersion"]
self._formatVersionMinor = data["formatVersionMinor"]
# groups.plist # groups.plist
@ -420,7 +485,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._formatVersion < 3: if self._formatVersionMajor < 3:
self._upConvertKerning(validate) self._upConvertKerning(validate)
groups = self._upConvertedKerningData["groups"] groups = self._upConvertedKerningData["groups"]
# normal # normal
@ -451,7 +516,7 @@ class UFOReader(_UFOBaseIO):
""" """
if validate is None: if validate is None:
validate = self._validate validate = self._validate
if self._formatVersion >= 3: if self._formatVersionMajor >= 3:
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.
@ -481,7 +546,7 @@ class UFOReader(_UFOBaseIO):
infoDict = self._readInfo(validate) infoDict = self._readInfo(validate)
infoDataToSet = {} infoDataToSet = {}
# version 1 # version 1
if self._formatVersion == 1: if self._formatVersionMajor == 1:
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:
@ -489,7 +554,7 @@ class UFOReader(_UFOBaseIO):
infoDataToSet = _convertFontInfoDataVersion1ToVersion2(infoDataToSet) infoDataToSet = _convertFontInfoDataVersion1ToVersion2(infoDataToSet)
infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet) infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet)
# version 2 # version 2
elif self._formatVersion == 2: elif self._formatVersionMajor == 2:
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:
@ -497,7 +562,7 @@ class UFOReader(_UFOBaseIO):
infoDataToSet[attr] = value infoDataToSet[attr] = value
infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet) infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet)
# version 3 # version 3
elif self._formatVersion == 3: elif self._formatVersionMajor == 3:
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:
@ -532,7 +597,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._formatVersion < 3: if self._formatVersionMajor < 3:
self._upConvertKerning(validate) self._upConvertKerning(validate)
kerningNested = self._upConvertedKerningData["kerning"] kerningNested = self._upConvertedKerningData["kerning"]
# normal # normal
@ -590,7 +655,7 @@ class UFOReader(_UFOBaseIO):
``validate`` will validate the layer contents. ``validate`` will validate the layer contents.
""" """
if self._formatVersion < 3: if self._formatVersionMajor < 3:
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:
@ -665,7 +730,7 @@ class UFOReader(_UFOBaseIO):
) )
return GlyphSet( return GlyphSet(
glyphSubFS, glyphSubFS,
ufoFormatVersion=self._formatVersion, ufoFormatVersion=self.formatVersionTuple,
validateRead=validateRead, validateRead=validateRead,
validateWrite=validateWrite, validateWrite=validateWrite,
) )
@ -722,7 +787,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._formatVersion < 3: if self._formatVersionMajor < 3:
return [] return []
if validate is None: if validate is None:
validate = self._validate validate = self._validate
@ -772,8 +837,10 @@ class UFOReader(_UFOBaseIO):
""" """
if validate is None: if validate is None:
validate = self._validate validate = self._validate
if self._formatVersion < 3: if self._formatVersionMajor < 3:
raise UFOLibError("Reading images is not allowed in UFO %d." % self._formatVersion) raise UFOLibError(
"Reading images is not allowed in UFO %d." % self._formatVersionMajor
)
fileName = fsdecode(fileName) fileName = fsdecode(fileName)
try: try:
try: try:
@ -818,13 +885,15 @@ class UFOWriter(UFOReader):
def __init__( def __init__(
self, self,
path, path,
formatVersion=3, formatVersion=LATEST_UFO_FORMAT,
fileCreator="com.github.fonttools.ufoLib", fileCreator="com.github.fonttools.ufoLib",
structure=None, structure=None,
validate=True, validate=True,
): ):
if not isinstance(formatVersion, FormatVersion):
formatVersion = FormatVersion(formatVersion)
if formatVersion not in supportedUFOFormatVersions: if formatVersion not in supportedUFOFormatVersions:
raise UFOLibError("Unsupported UFO format (%d)." % formatVersion) raise UFOLibError("Unsupported UFO format (%d.%d)." % formatVersion)
if hasattr(path, "__fspath__"): # support os.PathLike objects if hasattr(path, "__fspath__"): # support os.PathLike objects
path = path.__fspath__() path = path.__fspath__()
@ -932,7 +1001,7 @@ class UFOWriter(UFOReader):
# establish some basic stuff # establish some basic stuff
self._path = fsdecode(path) self._path = fsdecode(path)
self._formatVersion = formatVersion self._formatVersionMajor, self._formatVersionMinor = formatVersion
self._fileCreator = fileCreator self._fileCreator = fileCreator
self._downConversionKerningData = None self._downConversionKerningData = None
self._validate = validate self._validate = validate
@ -940,24 +1009,22 @@ class UFOWriter(UFOReader):
# this will be needed for up and down conversion. # this will be needed for up and down conversion.
previousFormatVersion = None previousFormatVersion = None
if self._havePreviousFile: if self._havePreviousFile:
metaInfo = self._getPlist(METAINFO_FILENAME) metaInfo = self._readMetaInfo(validate=validate)
previousFormatVersion = metaInfo.get("formatVersion") previousFormatVersion = FormatVersion(
try: metaInfo["formatVersion"], metaInfo["formatVersionMinor"]
previousFormatVersion = int(previousFormatVersion) )
except (ValueError, TypeError): # catch down conversion
self.fs.close() if previousFormatVersion > formatVersion:
raise UFOLibError("The existing metainfo.plist is not properly formatted.") raise UFOLibError(
if previousFormatVersion not in supportedUFOFormatVersions: "The UFO located at this path is a higher version (%d.%d) than the "
self.fs.close() "version (%d.%d) that is trying to be written. This is not supported."
raise UFOLibError("Unsupported UFO format (%d)." % formatVersion) % (*previousFormatVersion, *formatVersion)
# catch down conversion )
if previousFormatVersion is not None and previousFormatVersion > formatVersion:
raise UFOLibError("The UFO located at this path is a higher version (%d) than the version (%d) that is trying to be written. This is not supported." % (previousFormatVersion, formatVersion))
# handle the layer contents # handle the layer contents
self.layerContents = {} self.layerContents = {}
if previousFormatVersion is not None and previousFormatVersion >= 3: if previousFormatVersion is not None and previousFormatVersion.major >= 3:
# already exists # already exists
self.layerContents = OrderedDict(self._readLayerContents(validate)) self.layerContents = collections.OrderedDict(self._readLayerContents(validate))
else: else:
# previous < 3 # previous < 3
# imply the layer contents # imply the layer contents
@ -1091,8 +1158,10 @@ class UFOWriter(UFOReader):
def _writeMetaInfo(self): def _writeMetaInfo(self):
metaInfo = dict( metaInfo = dict(
creator=self._fileCreator, creator=self._fileCreator,
formatVersion=self._formatVersion formatVersion=self._formatVersionMajor,
) )
if self._formatVersionMinor != 0:
metaInfo["formatVersionMinor"] = self._formatVersionMinor
self._writePlist(METAINFO_FILENAME, metaInfo) self._writePlist(METAINFO_FILENAME, metaInfo)
# groups.plist # groups.plist
@ -1113,7 +1182,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._formatVersion >= 3: if self._formatVersionMajor >= 3:
return # XXX raise an error here return # XXX raise an error here
# flip the dictionaries # flip the dictionaries
remap = {} remap = {}
@ -1138,7 +1207,7 @@ class UFOWriter(UFOReader):
if not valid: if not valid:
raise UFOLibError(message) raise UFOLibError(message)
# down convert # down convert
if self._formatVersion < 3 and self._downConversionKerningData is not None: if self._formatVersionMajor < 3 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:
@ -1199,14 +1268,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._formatVersion == 3: if self._formatVersionMajor == 3:
if validate: if validate:
infoData = validateInfoVersion3Data(infoData) infoData = validateInfoVersion3Data(infoData)
elif self._formatVersion == 2: elif self._formatVersionMajor == 2:
infoData = _convertFontInfoDataVersion3ToVersion2(infoData) infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
if validate: if validate:
infoData = validateInfoVersion2Data(infoData) infoData = validateInfoVersion2Data(infoData)
elif self._formatVersion == 1: elif self._formatVersionMajor == 1:
infoData = _convertFontInfoDataVersion3ToVersion2(infoData) infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
if validate: if validate:
infoData = validateInfoVersion2Data(infoData) infoData = validateInfoVersion2Data(infoData)
@ -1249,7 +1318,7 @@ 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._formatVersion < 3 and self._downConversionKerningData is not None: if self._formatVersionMajor < 3 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()):
@ -1299,7 +1368,7 @@ class UFOWriter(UFOReader):
""" """
if validate is None: if validate is None:
validate = self._validate validate = self._validate
if self._formatVersion == 1: if self._formatVersionMajor == 1:
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):
@ -1318,7 +1387,7 @@ class UFOWriter(UFOReader):
""" """
if validate is None: if validate is None:
validate = self._validate validate = self._validate
if self.formatVersion < 3: if self._formatVersionMajor < 3:
return return
if layerOrder is not None: if layerOrder is not None:
newOrder = [] newOrder = []
@ -1366,8 +1435,10 @@ 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._formatVersion < 3 and (not defaultLayer or layerName is not None): if self._formatVersionMajor < 3 and (not defaultLayer or layerName is not None):
raise UFOLibError("Only the default layer can be writen in UFO %d." % self.formatVersion) raise UFOLibError(
"Only the default layer can be writen in UFO %d." % self._formatVersionMajor
)
# 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:
for existingLayerName, directory in self.layerContents.items(): for existingLayerName, directory in self.layerContents.items():
@ -1378,14 +1449,21 @@ 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.formatVersion == 1: if self._formatVersionMajor == 1:
return self._getGlyphSetFormatVersion1(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc) return self._getGlyphSetFormatVersion1(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
elif self.formatVersion == 2: elif self._formatVersionMajor == 2:
return self._getGlyphSetFormatVersion2(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc) return self._getGlyphSetFormatVersion2(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
elif self.formatVersion == 3: elif self._formatVersionMajor == 3:
return self._getGlyphSetFormatVersion3(validateRead, validateWrite, layerName=layerName, defaultLayer=defaultLayer, glyphNameToFileNameFunc=glyphNameToFileNameFunc) return self._getGlyphSetFormatVersion3(
validateRead,
validateWrite,
layerName=layerName,
defaultLayer=defaultLayer,
glyphNameToFileNameFunc=glyphNameToFileNameFunc,
formatVersionMinor=self._formatVersionMinor,
)
else: else:
raise AssertionError(self.formatVersion) raise AssertionError(self._formatVersionMajor)
def _getGlyphSetFormatVersion1(self, validateRead, validateWrite, glyphNameToFileNameFunc=None): def _getGlyphSetFormatVersion1(self, validateRead, validateWrite, glyphNameToFileNameFunc=None):
from fontTools.ufoLib.glifLib import GlyphSet from fontTools.ufoLib.glifLib import GlyphSet
@ -1394,7 +1472,7 @@ class UFOWriter(UFOReader):
return GlyphSet( return GlyphSet(
glyphSubFS, glyphSubFS,
glyphNameToFileNameFunc=glyphNameToFileNameFunc, glyphNameToFileNameFunc=glyphNameToFileNameFunc,
ufoFormatVersion=1, ufoFormatVersion=UFO_FORMAT_1_0,
validateRead=validateRead, validateRead=validateRead,
validateWrite=validateWrite, validateWrite=validateWrite,
) )
@ -1406,12 +1484,20 @@ class UFOWriter(UFOReader):
return GlyphSet( return GlyphSet(
glyphSubFS, glyphSubFS,
glyphNameToFileNameFunc=glyphNameToFileNameFunc, glyphNameToFileNameFunc=glyphNameToFileNameFunc,
ufoFormatVersion=2, ufoFormatVersion=UFO_FORMAT_2_0,
validateRead=validateRead, validateRead=validateRead,
validateWrite=validateWrite, validateWrite=validateWrite,
) )
def _getGlyphSetFormatVersion3(self, validateRead, validateWrite, layerName=None, defaultLayer=True, glyphNameToFileNameFunc=None): def _getGlyphSetFormatVersion3(
self,
validateRead,
validateWrite,
layerName=None,
defaultLayer=True,
glyphNameToFileNameFunc=None,
formatVersionMinor=0,
):
from fontTools.ufoLib.glifLib import GlyphSet from fontTools.ufoLib.glifLib import GlyphSet
# if the default flag is on, make sure that the default in the file # if the default flag is on, make sure that the default in the file
@ -1447,7 +1533,7 @@ class UFOWriter(UFOReader):
return GlyphSet( return GlyphSet(
glyphSubFS, glyphSubFS,
glyphNameToFileNameFunc=glyphNameToFileNameFunc, glyphNameToFileNameFunc=glyphNameToFileNameFunc,
ufoFormatVersion=3, ufoFormatVersion=FormatVersion(3, formatVersionMinor),
validateRead=validateRead, validateRead=validateRead,
validateWrite=validateWrite, validateWrite=validateWrite,
) )
@ -1460,7 +1546,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._formatVersion < 3: if self._formatVersionMajor < 3:
# 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
@ -1499,7 +1585,7 @@ class UFOWriter(UFOReader):
""" """
Remove the glyph set matching layerName. Remove the glyph set matching layerName.
""" """
if self._formatVersion < 3: if self._formatVersionMajor < 3:
# 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
@ -1529,8 +1615,10 @@ class UFOWriter(UFOReader):
""" """
if validate is None: if validate is None:
validate = self._validate validate = self._validate
if self._formatVersion < 3: if self._formatVersionMajor < 3:
raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) raise UFOLibError(
"Images are not allowed in UFO %d." % self._formatVersionMajor
)
fileName = fsdecode(fileName) fileName = fsdecode(fileName)
if validate: if validate:
valid, error = pngValidator(data=data) valid, error = pngValidator(data=data)
@ -1543,8 +1631,10 @@ class UFOWriter(UFOReader):
Remove the file named fileName from the Remove the file named fileName from the
images directory. images directory.
""" """
if self._formatVersion < 3: if self._formatVersionMajor < 3:
raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) raise UFOLibError(
"Images are not allowed in UFO %d." % self._formatVersionMajor
)
self.removePath(f"{IMAGES_DIRNAME}/{fsdecode(fileName)}") self.removePath(f"{IMAGES_DIRNAME}/{fsdecode(fileName)}")
def copyImageFromReader(self, reader, sourceFileName, destFileName, validate=None): def copyImageFromReader(self, reader, sourceFileName, destFileName, validate=None):
@ -1555,8 +1645,10 @@ class UFOWriter(UFOReader):
""" """
if validate is None: if validate is None:
validate = self._validate validate = self._validate
if self._formatVersion < 3: if self._formatVersionMajor < 3:
raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) raise UFOLibError(
"Images are not allowed in UFO %d." % self._formatVersionMajor
)
sourcePath = f"{IMAGES_DIRNAME}/{fsdecode(sourceFileName)}" sourcePath = f"{IMAGES_DIRNAME}/{fsdecode(sourceFileName)}"
destPath = f"{IMAGES_DIRNAME}/{fsdecode(destFileName)}" destPath = f"{IMAGES_DIRNAME}/{fsdecode(destFileName)}"
self.copyFromReader(reader, sourcePath, destPath) self.copyFromReader(reader, sourcePath, destPath)

View File

@ -10,6 +10,7 @@ in a folder. It offers two ways to read glyph data, and one way to write
glyph data. See the class doc string for details. glyph data. See the class doc string for details.
""" """
import logging
from warnings import warn from warnings import warn
from collections import OrderedDict from collections import OrderedDict
import fs import fs
@ -32,7 +33,13 @@ from fontTools.ufoLib.validators import (
glyphLibValidator, glyphLibValidator,
) )
from fontTools.misc import etree from fontTools.misc import etree
from fontTools.ufoLib import _UFOBaseIO from fontTools.ufoLib import (
_UFOBaseIO,
FormatVersion,
LATEST_UFO_FORMAT,
UFO_FORMAT_3_0,
supportedUFOFormatVersions,
)
from fontTools.ufoLib.utils import numberTypes from fontTools.ufoLib.utils import numberTypes
@ -43,6 +50,8 @@ __all__ = [
"glyphNameToFileName" "glyphNameToFileName"
] ]
logger = logging.getLogger(__name__)
# --------- # ---------
# Constants # Constants
@ -50,8 +59,16 @@ __all__ = [
CONTENTS_FILENAME = "contents.plist" CONTENTS_FILENAME = "contents.plist"
LAYERINFO_FILENAME = "layerinfo.plist" LAYERINFO_FILENAME = "layerinfo.plist"
supportedUFOFormatVersions = [1, 2, 3]
supportedGLIFFormatVersions = [1, 2] GLIF_FORMAT_1_0 = FormatVersion(1)
GLIF_FORMAT_2_0 = FormatVersion(2)
supportedGLIFFormatVersions = {
GLIF_FORMAT_1_0,
GLIF_FORMAT_2_0,
}
LATEST_GLIF_FORMAT = sorted(supportedGLIFFormatVersions)[-1]
# ------------ # ------------
@ -108,7 +125,7 @@ class GlyphSet(_UFOBaseIO):
self, self,
path, path,
glyphNameToFileNameFunc=None, glyphNameToFileNameFunc=None,
ufoFormatVersion=3, ufoFormatVersion=LATEST_UFO_FORMAT,
validateRead=True, validateRead=True,
validateWrite=True, validateWrite=True,
): ):
@ -125,8 +142,10 @@ 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 ufoFormatVersion not in supportedUFOFormatVersions: if not isinstance(ufoFormatVersion, FormatVersion):
raise GlifLibError("Unsupported UFO format version: %s" % ufoFormatVersion) ufoFormatVersion = FormatVersion(ufoFormatVersion)
if ufoFormatVersion not in supportedUFOFormatVersions and validateRead:
raise GlifLibError("Unsupported UFO format version: %d.%d" % ufoFormatVersion)
if isinstance(path, str): if isinstance(path, str):
try: try:
filesystem = fs.osfs.OSFS(path) filesystem = fs.osfs.OSFS(path)
@ -157,7 +176,9 @@ class GlyphSet(_UFOBaseIO):
self.fs = filesystem self.fs = filesystem
# if glyphSet contains no 'contents.plist', we consider it empty # if glyphSet contains no 'contents.plist', we consider it empty
self._havePreviousFile = filesystem.exists(CONTENTS_FILENAME) self._havePreviousFile = filesystem.exists(CONTENTS_FILENAME)
self.ufoFormatVersion = ufoFormatVersion # attribute kept for backward compatibility
self.ufoFormatVersion = ufoFormatVersion.major
self.ufoFormatVersionTuple = ufoFormatVersion
if glyphNameToFileNameFunc is None: if glyphNameToFileNameFunc is None:
glyphNameToFileNameFunc = glyphNameToFileName glyphNameToFileNameFunc = glyphNameToFileName
self.glyphNameToFileName = glyphNameToFileNameFunc self.glyphNameToFileName = glyphNameToFileNameFunc
@ -251,8 +272,10 @@ class GlyphSet(_UFOBaseIO):
""" """
if validateWrite is None: if validateWrite is None:
validateWrite = self._validateWrite validateWrite = self._validateWrite
if self.ufoFormatVersion < 3: if self.ufoFormatVersionTuple.major < 3:
raise GlifLibError("layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersion) raise GlifLibError(
"layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersionTuple.major
)
# gather data # gather data
infoData = {} infoData = {}
for attr in layerInfoVersion3ValueData.keys(): for attr in layerInfoVersion3ValueData.keys():
@ -346,10 +369,9 @@ class GlyphSet(_UFOBaseIO):
validate = self._validateRead validate = self._validateRead
text = self.getGLIF(glyphName) text = self.getGLIF(glyphName)
tree = _glifTreeFromString(text) tree = _glifTreeFromString(text)
if self.ufoFormatVersion < 3: formatVersions = {GLIF_FORMAT_1_0}
formatVersions = (1,) if self.ufoFormatVersionTuple >= UFO_FORMAT_3_0:
else: formatVersions.add(GLIF_FORMAT_2_0)
formatVersions = (1, 2)
_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):
@ -384,16 +406,18 @@ class GlyphSet(_UFOBaseIO):
class's ``validateWrite`` value, can be overridden. class's ``validateWrite`` value, can be overridden.
""" """
if formatVersion is None: if formatVersion is None:
if self.ufoFormatVersion >= 3: if self.ufoFormatVersionTuple >= UFO_FORMAT_3_0:
formatVersion = 2 formatVersion = GLIF_FORMAT_2_0
else: else:
formatVersion = 1 formatVersion = GLIF_FORMAT_1_0
elif not isinstance(formatVersion, FormatVersion):
formatVersion = FormatVersion(formatVersion)
if formatVersion not in supportedGLIFFormatVersions: if formatVersion not in supportedGLIFFormatVersions:
raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion) raise GlifLibError("Unsupported GLIF format version: %d.%d" % formatVersion)
if formatVersion == 2 and self.ufoFormatVersion < 3: if formatVersion.major == 2 and self.ufoFormatVersionTuple.major < 3:
raise GlifLibError( raise GlifLibError(
"Unsupported GLIF format version (%d) for UFO format version %d." "Unsupported GLIF format version (%d.%d) for UFO format version %d.%d."
% (formatVersion, self.ufoFormatVersion) % (*formatVersion, *self.ufoFormatVersionTuple)
) )
if validate is None: if validate is None:
validate = self._validateWrite validate = self._validateWrite
@ -527,7 +551,13 @@ def glyphNameToFileName(glyphName, existingFileNames):
# GLIF To and From String # GLIF To and From String
# ----------------------- # -----------------------
def readGlyphFromString(aString, glyphObject=None, pointPen=None, formatVersions=(1, 2), validate=True): def readGlyphFromString(
aString,
glyphObject=None,
pointPen=None,
formatVersions=supportedGLIFFormatVersions,
validate=True,
):
""" """
Read .glif data from a string into a glyph object. Read .glif data from a string into a glyph object.
@ -562,19 +592,31 @@ def readGlyphFromString(aString, glyphObject=None, pointPen=None, formatVersions
``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
for v in formatVersions
}
_readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate) _readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate)
def _writeGlyphToBytes( def _writeGlyphToBytes(
glyphName, glyphObject=None, drawPointsFunc=None, writer=None, glyphName,
formatVersion=2, validate=True): glyphObject=None,
drawPointsFunc=None,
writer=None,
formatVersion=LATEST_GLIF_FORMAT,
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."""
# 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.")
if validate and len(glyphName) == 0: if validate and len(glyphName) == 0:
raise GlifLibError("The glyph name is empty.") raise GlifLibError("The glyph name is empty.")
root = etree.Element("glyph", OrderedDict([("name", glyphName), ("format", repr(formatVersion))])) glyphAttrs = OrderedDict([("name", glyphName), ("format", repr(formatVersion.major))])
if formatVersion.minor != 0:
glyphAttrs["formatMinor"] = repr(formatVersion.minor)
root = etree.Element("glyph", glyphAttrs)
identifiers = set() identifiers = set()
# advance # advance
_writeAdvance(glyphObject, root, validate) _writeAdvance(glyphObject, root, validate)
@ -585,21 +627,21 @@ def _writeGlyphToBytes(
if getattr(glyphObject, "note", None): if getattr(glyphObject, "note", None):
_writeNote(glyphObject, root, validate) _writeNote(glyphObject, root, validate)
# image # image
if formatVersion >= 2 and getattr(glyphObject, "image", None): if formatVersion.major >= 2 and getattr(glyphObject, "image", None):
_writeImage(glyphObject, root, validate) _writeImage(glyphObject, root, validate)
# guidelines # guidelines
if formatVersion >= 2 and getattr(glyphObject, "guidelines", None): if formatVersion.major >= 2 and getattr(glyphObject, "guidelines", None):
_writeGuidelines(glyphObject, root, identifiers, validate) _writeGuidelines(glyphObject, root, identifiers, validate)
# anchors # anchors
anchors = getattr(glyphObject, "anchors", None) anchors = getattr(glyphObject, "anchors", None)
if formatVersion >= 2 and anchors: if formatVersion.major >= 2 and anchors:
_writeAnchors(glyphObject, root, identifiers, validate) _writeAnchors(glyphObject, root, identifiers, validate)
# outline # outline
if drawPointsFunc is not None: if drawPointsFunc is not None:
outline = etree.SubElement(root, "outline") outline = etree.SubElement(root, "outline")
pen = GLIFPointPen(outline, identifiers=identifiers, validate=validate) pen = GLIFPointPen(outline, identifiers=identifiers, validate=validate)
drawPointsFunc(pen) drawPointsFunc(pen)
if formatVersion == 1 and anchors: if formatVersion.major == 1 and anchors:
_writeAnchorsFormat1(pen, anchors, validate) _writeAnchorsFormat1(pen, anchors, validate)
# prevent lxml from writing self-closing tags # prevent lxml from writing self-closing tags
if not len(outline): if not len(outline):
@ -614,7 +656,13 @@ def _writeGlyphToBytes(
return data return data
def writeGlyphToString(glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=2, validate=True): def writeGlyphToString(
glyphName,
glyphObject=None,
drawPointsFunc=None,
formatVersion=LATEST_GLIF_FORMAT,
validate=True,
):
""" """
Return .glif data for a glyph as a string. The XML declaration's Return .glif data for a glyph as a string. The XML declaration's
encoding is always set to "UTF-8". encoding is always set to "UTF-8".
@ -643,6 +691,8 @@ def writeGlyphToString(glyphName, glyphObject=None, drawPointsFunc=None, formatV
``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,
@ -876,24 +926,38 @@ def _glifTreeFromString(aString):
raise GlifLibError("Invalid GLIF structure.") raise GlifLibError("Invalid GLIF structure.")
return root return root
def _readGlyphFromTree(tree, glyphObject=None, pointPen=None, formatVersions=(1, 2), validate=True):
def _readGlyphFromTree(
tree,
glyphObject=None,
pointPen=None,
formatVersions=supportedGLIFFormatVersions,
validate=True,
):
# check the format version # check the format version
formatVersion = tree.get("format") formatVersionMajor = tree.get("format")
if validate and formatVersion is None: if validate and formatVersionMajor is None:
raise GlifLibError("Unspecified format version in GLIF.") raise GlifLibError("Unspecified format version in GLIF.")
formatVersionMinor = tree.get("formatMinor", 0)
try: try:
v = int(formatVersion) formatVersion = FormatVersion(int(formatVersionMajor), int(formatVersionMinor))
formatVersion = v
except ValueError: except ValueError:
pass raise GlifLibError(
"Invalid GLIF format version: (%r, %r)" % (formatVersionMajor, formatVersionMinor)
)
if validate and formatVersion not in formatVersions: if validate and formatVersion not in formatVersions:
raise GlifLibError("Forbidden GLIF format version: %s" % formatVersion) raise GlifLibError("Forbidden GLIF format version: %s.%s" % formatVersion)
if formatVersion == 1:
_readGlyphFromTreeFormat1(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate) readGlyphFromTree = _READ_GLYPH_FROM_TREE_FUNCS.get(formatVersion)
elif formatVersion == 2: if not readGlyphFromTree:
_readGlyphFromTreeFormat2(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate) msg = "Unsupported GLIF format version: %s.%s" % formatVersion
else: if validate:
raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion) raise GlifLibError(msg)
# 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]
readGlyphFromTree(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate)
def _readGlyphFromTreeFormat1(tree, glyphObject=None, pointPen=None, validate=None): def _readGlyphFromTreeFormat1(tree, glyphObject=None, pointPen=None, validate=None):
@ -944,7 +1008,9 @@ def _readGlyphFromTreeFormat1(tree, glyphObject=None, pointPen=None, validate=No
if unicodes: if unicodes:
_relaxedSetattr(glyphObject, "unicodes", unicodes) _relaxedSetattr(glyphObject, "unicodes", unicodes)
def _readGlyphFromTreeFormat2(tree, glyphObject=None, pointPen=None, validate=None): def _readGlyphFromTreeFormat2(
tree, glyphObject=None, pointPen=None, validate=None, formatMinor=0
):
# get the name # get the name
_readName(glyphObject, tree, validate) _readName(glyphObject, tree, validate)
# populate the sub elements # populate the sub elements
@ -1030,6 +1096,13 @@ def _readGlyphFromTreeFormat2(tree, glyphObject=None, pointPen=None, validate=No
raise GlifLibError("The anchors are improperly formatted.") raise GlifLibError("The anchors are improperly formatted.")
_relaxedSetattr(glyphObject, "anchors", anchors) _relaxedSetattr(glyphObject, "anchors", anchors)
_READ_GLYPH_FROM_TREE_FUNCS = {
GLIF_FORMAT_1_0: _readGlyphFromTreeFormat1,
GLIF_FORMAT_2_0: _readGlyphFromTreeFormat2,
}
def _readName(glyphObject, root, validate): def _readName(glyphObject, root, validate):
glyphName = root.get("name") glyphName = root.get("name")
if validate and not glyphName: if validate and not glyphName:
@ -1512,9 +1585,11 @@ class GLIFPointPen(AbstractPointPen):
part of .glif files. part of .glif files.
""" """
def __init__(self, element, formatVersion=2, identifiers=None, validate=True): def __init__(self, element, formatVersion=LATEST_GLIF_FORMAT, identifiers=None, validate=True):
if identifiers is None: if identifiers is None:
identifiers = set() identifiers = set()
if not isinstance(formatVersion, FormatVersion):
formatVersion = FormatVersion(formatVersion)
self.formatVersion = formatVersion self.formatVersion = formatVersion
self.identifiers = identifiers self.identifiers = identifiers
self.outline = element self.outline = element
@ -1525,7 +1600,7 @@ class GLIFPointPen(AbstractPointPen):
def beginPath(self, identifier=None, **kwargs): def beginPath(self, identifier=None, **kwargs):
attrs = OrderedDict() attrs = OrderedDict()
if identifier is not None and self.formatVersion >= 2: if identifier is not None and self.formatVersion.major >= 2:
if self.validate: if self.validate:
if identifier in self.identifiers: if identifier in self.identifiers:
raise GlifLibError("identifier used more than once: %s" % identifier) raise GlifLibError("identifier used more than once: %s" % identifier)
@ -1586,7 +1661,7 @@ class GLIFPointPen(AbstractPointPen):
if name is not None: if name is not None:
attrs["name"] = name attrs["name"] = name
# identifier # identifier
if identifier is not None and self.formatVersion >= 2: if identifier is not None and self.formatVersion.major >= 2:
if self.validate: if self.validate:
if identifier in self.identifiers: if identifier in self.identifiers:
raise GlifLibError("identifier used more than once: %s" % identifier) raise GlifLibError("identifier used more than once: %s" % identifier)
@ -1603,7 +1678,7 @@ class GLIFPointPen(AbstractPointPen):
raise GlifLibError("transformation values must be int or float") raise GlifLibError("transformation values must be int or float")
if value != default: if value != default:
attrs[attr] = repr(value) attrs[attr] = repr(value)
if identifier is not None and self.formatVersion >= 2: if identifier is not None and self.formatVersion.major >= 2:
if self.validate: if self.validate:
if identifier in self.identifiers: if identifier in self.identifiers:
raise GlifLibError("identifier used more than once: %s" % identifier) raise GlifLibError("identifier used more than once: %s" % identifier)