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 zipfile
import enum
from collections import OrderedDict
import collections
import fs
import fs.base
import fs.subfs
@ -93,7 +93,33 @@ LAYERINFO_FILENAME = "layerinfo.plist"
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):
@ -282,9 +308,26 @@ class UFOReader(_UFOBaseIO):
path = property(_get_path, doc="The path of the UFO (DEPRECATED).")
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):
return self._fileStructure
@ -380,9 +423,9 @@ class UFOReader(_UFOBaseIO):
return None
# 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
to the class's validate value, can be overridden.
@ -392,19 +435,41 @@ class UFOReader(_UFOBaseIO):
data = self._getPlist(METAINFO_FILENAME)
if validate and not isinstance(data, dict):
raise UFOLibError("metainfo.plist is not properly formatted.")
formatVersion = data["formatVersion"]
formatVersionMajor = data["formatVersion"]
formatVersionMinor = data.setdefault("formatVersionMinor", 0)
if validate:
if not isinstance(formatVersion, int):
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 formatVersion not in supportedUFOFormatVersions:
raise UFOLibError(
"Unsupported UFO format (%d) in '%s' on %s"
% (formatVersion, METAINFO_FILENAME, self.fs)
)
self._formatVersion = formatVersion
if (formatVersionMajor, formatVersionMinor) not in supportedUFOFormatVersions:
unsupportedMsg = (
"Unsupported UFO format (%d.%d) in '%s' on %s"
% (formatVersionMajor, formatVersionMinor, METAINFO_FILENAME, self.fs)
)
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
@ -420,7 +485,7 @@ class UFOReader(_UFOBaseIO):
if validate is None:
validate = self._validate
# handle up conversion
if self._formatVersion < 3:
if self._formatVersionMajor < 3:
self._upConvertKerning(validate)
groups = self._upConvertedKerningData["groups"]
# normal
@ -451,7 +516,7 @@ class UFOReader(_UFOBaseIO):
"""
if validate is None:
validate = self._validate
if self._formatVersion >= 3:
if self._formatVersionMajor >= 3:
return dict(side1={}, side2={})
# use the public group reader to force the load and
# conversion of the data if it hasn't happened yet.
@ -481,7 +546,7 @@ class UFOReader(_UFOBaseIO):
infoDict = self._readInfo(validate)
infoDataToSet = {}
# version 1
if self._formatVersion == 1:
if self._formatVersionMajor == 1:
for attr in fontInfoAttributesVersion1:
value = infoDict.get(attr)
if value is not None:
@ -489,7 +554,7 @@ class UFOReader(_UFOBaseIO):
infoDataToSet = _convertFontInfoDataVersion1ToVersion2(infoDataToSet)
infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet)
# version 2
elif self._formatVersion == 2:
elif self._formatVersionMajor == 2:
for attr, dataValidationDict in list(fontInfoAttributesVersion2ValueData.items()):
value = infoDict.get(attr)
if value is None:
@ -497,7 +562,7 @@ class UFOReader(_UFOBaseIO):
infoDataToSet[attr] = value
infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet)
# version 3
elif self._formatVersion == 3:
elif self._formatVersionMajor == 3:
for attr, dataValidationDict in list(fontInfoAttributesVersion3ValueData.items()):
value = infoDict.get(attr)
if value is None:
@ -532,7 +597,7 @@ class UFOReader(_UFOBaseIO):
if validate is None:
validate = self._validate
# handle up conversion
if self._formatVersion < 3:
if self._formatVersionMajor < 3:
self._upConvertKerning(validate)
kerningNested = self._upConvertedKerningData["kerning"]
# normal
@ -590,7 +655,7 @@ class UFOReader(_UFOBaseIO):
``validate`` will validate the layer contents.
"""
if self._formatVersion < 3:
if self._formatVersionMajor < 3:
return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)]
contents = self._getPlist(LAYERCONTENTS_FILENAME)
if validate:
@ -665,7 +730,7 @@ class UFOReader(_UFOBaseIO):
)
return GlyphSet(
glyphSubFS,
ufoFormatVersion=self._formatVersion,
ufoFormatVersion=self.formatVersionTuple,
validateRead=validateRead,
validateWrite=validateWrite,
)
@ -722,7 +787,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._formatVersion < 3:
if self._formatVersionMajor < 3:
return []
if validate is None:
validate = self._validate
@ -772,8 +837,10 @@ class UFOReader(_UFOBaseIO):
"""
if validate is None:
validate = self._validate
if self._formatVersion < 3:
raise UFOLibError("Reading images is not allowed in UFO %d." % self._formatVersion)
if self._formatVersionMajor < 3:
raise UFOLibError(
"Reading images is not allowed in UFO %d." % self._formatVersionMajor
)
fileName = fsdecode(fileName)
try:
try:
@ -818,13 +885,15 @@ class UFOWriter(UFOReader):
def __init__(
self,
path,
formatVersion=3,
formatVersion=LATEST_UFO_FORMAT,
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)." % formatVersion)
raise UFOLibError("Unsupported UFO format (%d.%d)." % formatVersion)
if hasattr(path, "__fspath__"): # support os.PathLike objects
path = path.__fspath__()
@ -932,7 +1001,7 @@ class UFOWriter(UFOReader):
# establish some basic stuff
self._path = fsdecode(path)
self._formatVersion = formatVersion
self._formatVersionMajor, self._formatVersionMinor = formatVersion
self._fileCreator = fileCreator
self._downConversionKerningData = None
self._validate = validate
@ -940,24 +1009,22 @@ class UFOWriter(UFOReader):
# this will be needed for up and down conversion.
previousFormatVersion = None
if self._havePreviousFile:
metaInfo = self._getPlist(METAINFO_FILENAME)
previousFormatVersion = metaInfo.get("formatVersion")
try:
previousFormatVersion = int(previousFormatVersion)
except (ValueError, TypeError):
self.fs.close()
raise UFOLibError("The existing metainfo.plist is not properly formatted.")
if previousFormatVersion not in supportedUFOFormatVersions:
self.fs.close()
raise UFOLibError("Unsupported UFO format (%d)." % 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))
metaInfo = self._readMetaInfo(validate=validate)
previousFormatVersion = FormatVersion(
metaInfo["formatVersion"], metaInfo["formatVersionMinor"]
)
# 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)
)
# handle the layer contents
self.layerContents = {}
if previousFormatVersion is not None and previousFormatVersion >= 3:
if previousFormatVersion is not None and previousFormatVersion.major >= 3:
# already exists
self.layerContents = OrderedDict(self._readLayerContents(validate))
self.layerContents = collections.OrderedDict(self._readLayerContents(validate))
else:
# previous < 3
# imply the layer contents
@ -1091,8 +1158,10 @@ class UFOWriter(UFOReader):
def _writeMetaInfo(self):
metaInfo = dict(
creator=self._fileCreator,
formatVersion=self._formatVersion
formatVersion=self._formatVersionMajor,
)
if self._formatVersionMinor != 0:
metaInfo["formatVersionMinor"] = self._formatVersionMinor
self._writePlist(METAINFO_FILENAME, metaInfo)
# groups.plist
@ -1113,7 +1182,7 @@ class UFOWriter(UFOReader):
This is the same form returned by UFOReader's
getKerningGroupConversionRenameMaps method.
"""
if self._formatVersion >= 3:
if self._formatVersionMajor >= 3:
return # XXX raise an error here
# flip the dictionaries
remap = {}
@ -1138,7 +1207,7 @@ class UFOWriter(UFOReader):
if not valid:
raise UFOLibError(message)
# 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"]
remappedGroups = {}
# there are some edge cases here that are ignored:
@ -1199,14 +1268,14 @@ class UFOWriter(UFOReader):
continue
infoData[attr] = value
# down convert data if necessary and validate
if self._formatVersion == 3:
if self._formatVersionMajor == 3:
if validate:
infoData = validateInfoVersion3Data(infoData)
elif self._formatVersion == 2:
elif self._formatVersionMajor == 2:
infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
if validate:
infoData = validateInfoVersion2Data(infoData)
elif self._formatVersion == 1:
elif self._formatVersionMajor == 1:
infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
if validate:
infoData = validateInfoVersion2Data(infoData)
@ -1249,7 +1318,7 @@ class UFOWriter(UFOReader):
if not isinstance(value, numberTypes):
raise UFOLibError(invalidFormatMessage)
# 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"]
remappedKerning = {}
for (side1, side2), value in list(kerning.items()):
@ -1299,7 +1368,7 @@ class UFOWriter(UFOReader):
"""
if validate is None:
validate = self._validate
if self._formatVersion == 1:
if self._formatVersionMajor == 1:
raise UFOLibError("features.fea is not allowed in UFO Format Version 1.")
if validate:
if not isinstance(features, str):
@ -1318,7 +1387,7 @@ class UFOWriter(UFOReader):
"""
if validate is None:
validate = self._validate
if self.formatVersion < 3:
if self._formatVersionMajor < 3:
return
if layerOrder is not None:
newOrder = []
@ -1366,8 +1435,10 @@ class UFOWriter(UFOReader):
if validateWrite is None:
validateWrite = self._validate
# only default can be written in < 3
if self._formatVersion < 3 and (not defaultLayer or layerName is not None):
raise UFOLibError("Only the default layer can be writen in UFO %d." % self.formatVersion)
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._formatVersionMajor
)
# locate a layer name when None has been given
if layerName is None and defaultLayer:
for existingLayerName, directory in self.layerContents.items():
@ -1378,14 +1449,21 @@ 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.formatVersion == 1:
if self._formatVersionMajor == 1:
return self._getGlyphSetFormatVersion1(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
elif self.formatVersion == 2:
elif self._formatVersionMajor == 2:
return self._getGlyphSetFormatVersion2(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
elif self.formatVersion == 3:
return self._getGlyphSetFormatVersion3(validateRead, validateWrite, layerName=layerName, defaultLayer=defaultLayer, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
elif self._formatVersionMajor == 3:
return self._getGlyphSetFormatVersion3(
validateRead,
validateWrite,
layerName=layerName,
defaultLayer=defaultLayer,
glyphNameToFileNameFunc=glyphNameToFileNameFunc,
formatVersionMinor=self._formatVersionMinor,
)
else:
raise AssertionError(self.formatVersion)
raise AssertionError(self._formatVersionMajor)
def _getGlyphSetFormatVersion1(self, validateRead, validateWrite, glyphNameToFileNameFunc=None):
from fontTools.ufoLib.glifLib import GlyphSet
@ -1394,7 +1472,7 @@ class UFOWriter(UFOReader):
return GlyphSet(
glyphSubFS,
glyphNameToFileNameFunc=glyphNameToFileNameFunc,
ufoFormatVersion=1,
ufoFormatVersion=UFO_FORMAT_1_0,
validateRead=validateRead,
validateWrite=validateWrite,
)
@ -1406,12 +1484,20 @@ class UFOWriter(UFOReader):
return GlyphSet(
glyphSubFS,
glyphNameToFileNameFunc=glyphNameToFileNameFunc,
ufoFormatVersion=2,
ufoFormatVersion=UFO_FORMAT_2_0,
validateRead=validateRead,
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
# if the default flag is on, make sure that the default in the file
@ -1447,7 +1533,7 @@ class UFOWriter(UFOReader):
return GlyphSet(
glyphSubFS,
glyphNameToFileNameFunc=glyphNameToFileNameFunc,
ufoFormatVersion=3,
ufoFormatVersion=FormatVersion(3, formatVersionMinor),
validateRead=validateRead,
validateWrite=validateWrite,
)
@ -1460,7 +1546,7 @@ class UFOWriter(UFOReader):
layerName, it is up to the caller to inform that object that
the directory it represents has changed.
"""
if self._formatVersion < 3:
if self._formatVersionMajor < 3:
# ignore renaming glyph sets for UFO1 UFO2
# just write the data from the default layer
return
@ -1499,7 +1585,7 @@ class UFOWriter(UFOReader):
"""
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
# just write the data from the default layer
return
@ -1529,8 +1615,10 @@ class UFOWriter(UFOReader):
"""
if validate is None:
validate = self._validate
if self._formatVersion < 3:
raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion)
if self._formatVersionMajor < 3:
raise UFOLibError(
"Images are not allowed in UFO %d." % self._formatVersionMajor
)
fileName = fsdecode(fileName)
if validate:
valid, error = pngValidator(data=data)
@ -1543,8 +1631,10 @@ class UFOWriter(UFOReader):
Remove the file named fileName from the
images directory.
"""
if self._formatVersion < 3:
raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion)
if self._formatVersionMajor < 3:
raise UFOLibError(
"Images are not allowed in UFO %d." % self._formatVersionMajor
)
self.removePath(f"{IMAGES_DIRNAME}/{fsdecode(fileName)}")
def copyImageFromReader(self, reader, sourceFileName, destFileName, validate=None):
@ -1555,8 +1645,10 @@ class UFOWriter(UFOReader):
"""
if validate is None:
validate = self._validate
if self._formatVersion < 3:
raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion)
if self._formatVersionMajor < 3:
raise UFOLibError(
"Images are not allowed in UFO %d." % self._formatVersionMajor
)
sourcePath = f"{IMAGES_DIRNAME}/{fsdecode(sourceFileName)}"
destPath = f"{IMAGES_DIRNAME}/{fsdecode(destFileName)}"
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.
"""
import logging
from warnings import warn
from collections import OrderedDict
import fs
@ -32,7 +33,13 @@ from fontTools.ufoLib.validators import (
glyphLibValidator,
)
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
@ -43,6 +50,8 @@ __all__ = [
"glyphNameToFileName"
]
logger = logging.getLogger(__name__)
# ---------
# Constants
@ -50,8 +59,16 @@ __all__ = [
CONTENTS_FILENAME = "contents.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,
path,
glyphNameToFileNameFunc=None,
ufoFormatVersion=3,
ufoFormatVersion=LATEST_UFO_FORMAT,
validateRead=True,
validateWrite=True,
):
@ -125,8 +142,10 @@ class GlyphSet(_UFOBaseIO):
``validateRead`` will validate read operations. Its default is ``True``.
``validateWrite`` will validate write operations. Its default is ``True``.
"""
if ufoFormatVersion not in supportedUFOFormatVersions:
raise GlifLibError("Unsupported UFO format version: %s" % ufoFormatVersion)
if not isinstance(ufoFormatVersion, FormatVersion):
ufoFormatVersion = FormatVersion(ufoFormatVersion)
if ufoFormatVersion not in supportedUFOFormatVersions and validateRead:
raise GlifLibError("Unsupported UFO format version: %d.%d" % ufoFormatVersion)
if isinstance(path, str):
try:
filesystem = fs.osfs.OSFS(path)
@ -157,7 +176,9 @@ class GlyphSet(_UFOBaseIO):
self.fs = filesystem
# if glyphSet contains no 'contents.plist', we consider it empty
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:
glyphNameToFileNameFunc = glyphNameToFileName
self.glyphNameToFileName = glyphNameToFileNameFunc
@ -251,8 +272,10 @@ class GlyphSet(_UFOBaseIO):
"""
if validateWrite is None:
validateWrite = self._validateWrite
if self.ufoFormatVersion < 3:
raise GlifLibError("layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersion)
if self.ufoFormatVersionTuple.major < 3:
raise GlifLibError(
"layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersionTuple.major
)
# gather data
infoData = {}
for attr in layerInfoVersion3ValueData.keys():
@ -346,10 +369,9 @@ class GlyphSet(_UFOBaseIO):
validate = self._validateRead
text = self.getGLIF(glyphName)
tree = _glifTreeFromString(text)
if self.ufoFormatVersion < 3:
formatVersions = (1,)
else:
formatVersions = (1, 2)
formatVersions = {GLIF_FORMAT_1_0}
if self.ufoFormatVersionTuple >= UFO_FORMAT_3_0:
formatVersions.add(GLIF_FORMAT_2_0)
_readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate)
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.
"""
if formatVersion is None:
if self.ufoFormatVersion >= 3:
formatVersion = 2
if self.ufoFormatVersionTuple >= UFO_FORMAT_3_0:
formatVersion = GLIF_FORMAT_2_0
else:
formatVersion = 1
formatVersion = GLIF_FORMAT_1_0
elif not isinstance(formatVersion, FormatVersion):
formatVersion = FormatVersion(formatVersion)
if formatVersion not in supportedGLIFFormatVersions:
raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion)
if formatVersion == 2 and self.ufoFormatVersion < 3:
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) for UFO format version %d."
% (formatVersion, self.ufoFormatVersion)
"Unsupported GLIF format version (%d.%d) for UFO format version %d.%d."
% (*formatVersion, *self.ufoFormatVersionTuple)
)
if validate is None:
validate = self._validateWrite
@ -527,7 +551,13 @@ def glyphNameToFileName(glyphName, existingFileNames):
# 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.
@ -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.
"""
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)
def _writeGlyphToBytes(
glyphName, glyphObject=None, drawPointsFunc=None, writer=None,
formatVersion=2, validate=True):
glyphName,
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."""
# start
if validate and not isinstance(glyphName, str):
raise GlifLibError("The glyph name is not properly formatted.")
if validate and len(glyphName) == 0:
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()
# advance
_writeAdvance(glyphObject, root, validate)
@ -585,21 +627,21 @@ def _writeGlyphToBytes(
if getattr(glyphObject, "note", None):
_writeNote(glyphObject, root, validate)
# image
if formatVersion >= 2 and getattr(glyphObject, "image", None):
if formatVersion.major >= 2 and getattr(glyphObject, "image", None):
_writeImage(glyphObject, root, validate)
# guidelines
if formatVersion >= 2 and getattr(glyphObject, "guidelines", None):
if formatVersion.major >= 2 and getattr(glyphObject, "guidelines", None):
_writeGuidelines(glyphObject, root, identifiers, validate)
# anchors
anchors = getattr(glyphObject, "anchors", None)
if formatVersion >= 2 and anchors:
if formatVersion.major >= 2 and anchors:
_writeAnchors(glyphObject, root, identifiers, validate)
# outline
if drawPointsFunc is not None:
outline = etree.SubElement(root, "outline")
pen = GLIFPointPen(outline, identifiers=identifiers, validate=validate)
drawPointsFunc(pen)
if formatVersion == 1 and anchors:
if formatVersion.major == 1 and anchors:
_writeAnchorsFormat1(pen, anchors, validate)
# prevent lxml from writing self-closing tags
if not len(outline):
@ -614,7 +656,13 @@ def _writeGlyphToBytes(
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
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.
"""
if not isinstance(formatVersion, FormatVersion):
formatVersion = FormatVersion(formatVersion)
data = _writeGlyphToBytes(
glyphName,
glyphObject=glyphObject,
@ -876,24 +926,38 @@ def _glifTreeFromString(aString):
raise GlifLibError("Invalid GLIF structure.")
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
formatVersion = tree.get("format")
if validate and formatVersion is None:
formatVersionMajor = tree.get("format")
if validate and formatVersionMajor is None:
raise GlifLibError("Unspecified format version in GLIF.")
formatVersionMinor = tree.get("formatMinor", 0)
try:
v = int(formatVersion)
formatVersion = v
formatVersion = FormatVersion(int(formatVersionMajor), int(formatVersionMinor))
except ValueError:
pass
raise GlifLibError(
"Invalid GLIF format version: (%r, %r)" % (formatVersionMajor, formatVersionMinor)
)
if validate and formatVersion not in formatVersions:
raise GlifLibError("Forbidden GLIF format version: %s" % formatVersion)
if formatVersion == 1:
_readGlyphFromTreeFormat1(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate)
elif formatVersion == 2:
_readGlyphFromTreeFormat2(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate)
else:
raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion)
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:
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):
@ -944,7 +1008,9 @@ def _readGlyphFromTreeFormat1(tree, glyphObject=None, pointPen=None, validate=No
if 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
_readName(glyphObject, tree, validate)
# populate the sub elements
@ -1030,6 +1096,13 @@ def _readGlyphFromTreeFormat2(tree, glyphObject=None, pointPen=None, validate=No
raise GlifLibError("The anchors are improperly formatted.")
_relaxedSetattr(glyphObject, "anchors", anchors)
_READ_GLYPH_FROM_TREE_FUNCS = {
GLIF_FORMAT_1_0: _readGlyphFromTreeFormat1,
GLIF_FORMAT_2_0: _readGlyphFromTreeFormat2,
}
def _readName(glyphObject, root, validate):
glyphName = root.get("name")
if validate and not glyphName:
@ -1512,9 +1585,11 @@ class GLIFPointPen(AbstractPointPen):
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:
identifiers = set()
if not isinstance(formatVersion, FormatVersion):
formatVersion = FormatVersion(formatVersion)
self.formatVersion = formatVersion
self.identifiers = identifiers
self.outline = element
@ -1525,7 +1600,7 @@ class GLIFPointPen(AbstractPointPen):
def beginPath(self, identifier=None, **kwargs):
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 identifier in self.identifiers:
raise GlifLibError("identifier used more than once: %s" % identifier)
@ -1586,7 +1661,7 @@ class GLIFPointPen(AbstractPointPen):
if name is not None:
attrs["name"] = name
# identifier
if identifier is not None and self.formatVersion >= 2:
if identifier is not None and self.formatVersion.major >= 2:
if self.validate:
if identifier in self.identifiers:
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")
if value != default:
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 identifier in self.identifiers:
raise GlifLibError("identifier used more than once: %s" % identifier)