defcon/object/font.py
def save(
self,
path=None,
formatVersion=None,
removeUnreferencedImages=False,
progressBar=None,
structure=None,
):
"""
Save the font to **path**. If path is None, the path
from the last save or when the font was first opened
will be used.
The UFO will be saved using the format found at ``ufoFormatVersion``.
This value is either the format version from the exising UFO or
the format version specified in a previous save. If neither of
these is available, the UFO will be written as format version 3.
If you wish to specifiy the format version for saving, pass
the desired number as the **formatVersion** argument.
Optionally, the UFO can be purged of unreferenced images
during this operation. To do this, pass ``True`` as the
value for the removeUnreferencedImages argument.
'structure' can be either None, "zip" or "package". If it's None,
the destination UFO will use the same structure as original, provided
that is compatible with any previous UFO at the output path.
If 'structure' is "zip" the UFO will be saved as compressed archive,
else it is saved as a regular folder or "package".
"""
isNewFont = self._path is None
if path is None:
if isNewFont:
from defcon.errors import DefconError
raise DefconError("Can't save new font without a 'path'")
path = self._path
saveAs = False
else:
if not isinstance(path, basestring) and not hasattr(path, "__fspath__"):
raise TypeError(
"invalid path: expected string or os.PathLike, found %s"
% type(path).__name__
)
if isNewFont:
saveAs = True
else:
saveAs = not samepath(self._path, path)
if structure is not None:
try:
structure = UFOFileStructure(structure)
except ValueError:
from defcon.errors import DefconError
raise DefconError(
"'%s' is not a valid UFOFileStructure; choose between %s"
% (structure, tuple(e.value for e in UFOFileStructure))
)
elif self._ufoFileStructure is not None:
structure = self._ufoFileStructure
else:
structure = UFOFileStructure.PACKAGE
isExistingOSPath = os.path.exists(path)
if isExistingOSPath:
try:
with UFOReader(path, validate=True) as reader:
existingStructure = reader.fileStructure
except UFOLibError:
saveAs = True
if not saveAs and structure and structure is not existingStructure:
from defcon.errors import DefconError
raise DefconError(
"Can't save font in-place with a different structure; "
"expected %s, got %s"
% (existingStructure.value, structure.value)
)
assert self.layers.defaultLayer is not None
if self.layers.defaultLayer.name != "public.default":
assert "public.default" not in self.layers.layerOrder
if formatVersion is None and self._ufoFormatVersion is not None:
formatVersion = self._ufoFormatVersion
elif formatVersion is None:
formatVersion = 3
overwritePath = None
if ((not saveAs and formatVersion != self._ufoFormatVersion) or
(saveAs and isExistingOSPath)):
saveAs = True
overwritePath = path
path = os.path.join(tempfile.mkdtemp(), "temp.ufo")
try:
try:
writer = UFOWriter(
path,
formatVersion=formatVersion,
validate=self.ufoLibWriteValidate,
structure=structure,
)
except UFOLibError:
if overwritePath is None and isExistingOSPath:
logger.exception("Invalid ufo found '%s', the existing ufo "
"will be removed. Save will be handled as "
"save-as.", path)
saveAs = True
overwritePath = path
path = os.path.join(tempfile.mkdtemp(), "temp.ufo")
writer = UFOWriter(
path,
formatVersion=formatVersion,
validate=self.ufoLibWriteValidate,
structure=structure,
)
else:
raise
if self._ufoFormatVersion != formatVersion:
self.info.dirty = True
self.groups.dirty = True
self.kerning.dirty = True
self.lib.dirty = True
if formatVersion > 1:
self.features.dirty = True
if formatVersion < 3 and self._kerningGroupConversionRenameMaps is not None:
writer.setKerningGroupConversionRenameMaps(self._kerningGroupConversionRenameMaps)
self._saveInfo(writer=writer, saveAs=saveAs, progressBar=progressBar) <==========ここ
self._saveGroups(writer=writer, saveAs=saveAs, progressBar=progressBar)
self._saveKerning(writer=writer, saveAs=saveAs, progressBar=progressBar)
self._saveLib(writer=writer, saveAs=saveAs, progressBar=progressBar)
if formatVersion >= 2:
self._saveFeatures(writer=writer, saveAs=saveAs, progressBar=progressBar)
if formatVersion >= 3:
self.saveImages(writer=writer, removeUnreferencedImages=removeUnreferencedImages, saveAs=saveAs, progressBar=progressBar)
self.saveData(writer=writer, saveAs=saveAs, progressBar=progressBar)
self.layers.save(writer, saveAs=saveAs, progressBar=progressBar)
if writer.fileStructure is UFOFileStructure.ZIP:
writer.close()
writer.setModificationTime()
if overwritePath is not None:
if os.path.isfile(overwritePath):
os.remove(overwritePath)
elif os.path.isdir(overwritePath):
shutil.rmtree(overwritePath)
shutil.move(path, overwritePath)
finally:
if overwritePath is not None:
shutil.rmtree(os.path.dirname(path))
path = overwritePath
self._path = path
self._ufoFormatVersion = formatVersion
self._ufoFileStructure = writer.fileStructure
self.dirty = False
defcon/object/font.py
def _saveInfo(self, writer, saveAs=False, progressBar=None):
if progressBar is not None:
progressBar.update(text="Saving info...", increment=0)
self.saveInfo(writer) <=====ここ
self.info.dirty = False
self._stampInfoDataState(writer)
if progressBar is not None:
progressBar.update()
defcon/object/font.py
def saveInfo(self, writer):
"""
Save info. This method should not be called externally.
Subclasses may override this method to implement custom saving behavior.
"""
writer.writeInfo(self.info, validate=self.info.ufoLibWriteValidate) <===ここ
fontTools/ufolib/__init__.py
def writeInfo(self, info, validate=None):
"""
Write info.plist. This method requires an object
that supports getting attributes that follow the
fontinfo.plist version 2 specification. Attributes
will be taken from the given object and written
into the file.
``validate`` will validate the data, by default it is set to the
class's validate value, can be overridden.
"""
if validate is None:
validate = self._validate
infoData = {}
for attr in list(fontInfoAttributesVersion3ValueData.keys()):
if hasattr(info, attr):
try:
value = getattr(info, attr)
except AttributeError:
raise UFOLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr)
if value is None:
continue
infoData[attr] = value
if self._formatVersion == 3:
if validate:
infoData = validateInfoVersion3Data(infoData)
elif self._formatVersion == 2:
infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
if validate:
infoData = validateInfoVersion2Data(infoData)
elif self._formatVersion == 1:
infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
if validate:
infoData = validateInfoVersion2Data(infoData)
infoData = _convertFontInfoDataVersion2ToVersion1(infoData)
self._writePlist(FONTINFO_FILENAME, infoData) <=====ここ
fontTools/ufolib/__init__.py
class UFOWriter(UFOReader):
def __init__(
self,
path,
formatVersion=3,
fileCreator="com.github.fonttools.ufoLib",
structure=None,
validate=True,
):
if formatVersion not in supportedUFOFormatVersions:
raise UFOLibError("Unsupported UFO format (%d)." % formatVersion)
if hasattr(path, "__fspath__"):
path = path.__fspath__()
if isinstance(path, str):
path = os.path.normpath(path)
havePreviousFile = os.path.exists(path)
if havePreviousFile:
existingStructure = _sniffFileStructure(path)
if structure is not None:
try:
structure = UFOFileStructure(structure)
except ValueError:
raise UFOLibError(
"Invalid or unsupported structure: '%s'" % structure
)
if structure is not existingStructure:
raise UFOLibError(
"A UFO with a different structure (%s) already exists "
"at the given path: '%s'" % (existingStructure, path)
)
else:
structure = existingStructure
else:
if structure is None:
structure = UFOFileStructure.PACKAGE
dirName = os.path.dirname(path)
if dirName and not os.path.isdir(dirName):
raise UFOLibError(
"Cannot write to '%s': directory does not exist" % path
)
if structure is UFOFileStructure.ZIP:
if havePreviousFile:
parentFS = fs.tempfs.TempFS()
with fs.zipfs.ZipFS(path, encoding="utf-8") as origFS:
fs.copy.copy_fs(origFS, parentFS)
rootDirs = [
p.name for p in parentFS.scandir("/")
if p.is_dir and p.name != "__MACOSX"
]
if len(rootDirs) != 1:
raise UFOLibError(
"Expected exactly 1 root directory, found %d" % len(rootDirs)
)
else:
self.fs = parentFS.opendir(
rootDirs[0], factory=fs.subfs.ClosingSubFS
)
else:
rootDir = os.path.splitext(os.path.basename(path))[0] + ".ufo"
parentFS = fs.zipfs.ZipFS(path, write=True, encoding="utf-8")
parentFS.makedir(rootDir)
self.fs = parentFS.opendir(rootDir, factory=fs.subfs.ClosingSubFS)
else:
self.fs = fs.osfs.OSFS(path, create=True)
self._fileStructure = structure
self._havePreviousFile = havePreviousFile
self._shouldClose = True
elif isinstance(path, fs.base.FS):
filesystem = path
try:
filesystem.check()
except fs.errors.FilesystemClosed:
raise UFOLibError("the filesystem '%s' is closed" % path)
else:
self.fs = filesystem
try:
path = filesystem.getsyspath("/")
except fs.errors.NoSysPath:
path = str(filesystem)
if structure and structure is not UFOFileStructure.PACKAGE:
import warnings
warnings.warn(
"The 'structure' argument is not used when input is an FS object",
UserWarning,
stacklevel=2,
)
self._fileStructure = UFOFileStructure.PACKAGE
self._havePreviousFile = filesystem.exists(METAINFO_FILENAME)
self._shouldClose = False
else:
raise TypeError(
"Expected a path string or fs object, found %s"
% type(path).__name__
)
self._path = fsdecode(path)
self._formatVersion = formatVersion
self._fileCreator = fileCreator
self._downConversionKerningData = None
self._validate = validate
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)
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))
self.layerContents = {}
if previousFormatVersion is not None and previousFormatVersion >= 3:
self.layerContents = OrderedDict(self._readLayerContents(validate))
else:
if self.fs.exists(DEFAULT_GLYPHS_DIRNAME):
self.layerContents = {DEFAULT_LAYER_NAME : DEFAULT_GLYPHS_DIRNAME}
self._writeMetaInfo() <==============ここの中へ
fontTools/ufolib/__init__.py
def _writeMetaInfo(self):
metaInfo = dict(
creator=self._fileCreator,
formatVersion=self._formatVersion
)
self._writePlist(METAINFO_FILENAME, metaInfo)
fontTools/ufolib/__init__.py
class _UFOBaseIO:
def getFileModificationTime(self, path):
def _getPlist(self, fileName, default=None):
def _writePlist(self, fileName, obj):
"""
Write a property list to a file relative to the UFO filesystem's root.
Do this sort of atomically, making it harder to corrupt existing files,
for example when plistlib encounters an error halfway during write.
This also checks to see if text matches the text that is already in the
file at path. If so, the file is not rewritten so that the modification
date is preserved.
The errors that could be raised during the writing of a plist are
unpredictable and/or too large to list, so, a blind try: except: is done.
If an exception occurs, a UFOLibError will be raised.
"""
if self._havePreviousFile:
try:
data = plistlib.dumps(obj)
except Exception as e:
raise UFOLibError(
"'%s' could not be written on %s because "
"the data is not properly formatted: %s"
% (fileName, self.fs, e)
)
if self.fs.exists(fileName) and data == self.fs.readbytes(fileName):
return
self.fs.writebytes(fileName, data)
else:
with self.fs.openbin(fileName, mode="w") as fp:
try:
plistlib.dump(obj, fp) <========ここでエラーになる。
except Exception as e:
raise UFOLibError(
"'%s' could not be written on %s because "
"the data is not properly formatted: %s"
% (fileName, self.fs, e)
)
fontTools/misc/plistlib.py
def dump(
value,
fp,
sort_keys=True,
skipkeys=False,
use_builtin_types=None,
pretty_print=True,
):
if not hasattr(fp, "write"):
raise AttributeError(
"'%s' object has no attribute 'write'" % type(fp).__name__
)
root = etree.Element("plist", version="1.0")
el = totree(
value,
sort_keys=sort_keys,
skipkeys=skipkeys,
use_builtin_types=use_builtin_types,
pretty_print=pretty_print,
)
root.append(el)
tree = etree.ElementTree(root)
# we write the doctype ourselves instead of using the 'doctype' argument
# of 'write' method, becuse lxml will force adding a '\n' even when
# pretty_print is False.
if pretty_print:
header = b"\n".join((XML_DECLARATION, PLIST_DOCTYPE, b""))
else:
header = XML_DECLARATION + PLIST_DOCTYPE
fp.write(header)
tree.write(
fp, encoding="utf-8", pretty_print=pretty_print, xml_declaration=False
)