WIP: ufoLib: support UFO formatVersionMinor and GLIF formatMinor
This commit is contained in:
parent
cfe4eb039a
commit
c066cfc4d5
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user