From 72f9e7794ad99d8899c6961b6e5f8dac4f1d2269 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Mon, 18 May 2020 12:41:25 +0100 Subject: [PATCH] SFNTReader: define __getstate__/__setstate__ to reopen external file Instead of copying to BytesIO, we can return the file name in getstate and reopen the file in setstate. This keeps the TTFont truly lazy as it avoids the extra copy --- Lib/fontTools/ttLib/sfnt.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/Lib/fontTools/ttLib/sfnt.py b/Lib/fontTools/ttLib/sfnt.py index 8ab4cecea..5996ec3d8 100644 --- a/Lib/fontTools/ttLib/sfnt.py +++ b/Lib/fontTools/ttLib/sfnt.py @@ -123,31 +123,29 @@ class SFNTReader(object): def close(self): self.file.close() - def __getstate__(self): - """Makes SFNTReader pickle/deepcopy-able even with TTFont loaded as lazy=True. + # We define custom __getstate__ and __setstate__ to make SFNTReader pickle-able + # and deepcopy-able. When a TTFont is loaded as lazy=True, SFNTReader holds a + # reference to an external file object which is not pickleable. So in __getstate__ + # we store the file name and current position, and in __setstate__ we reopen the + # same named file after unpickling. - If SFNTReader holds a reference to an external file object which is not - pickleable, we copy the file data to an in-memory bytes stream, which is - pickelable and behaves just like a file. - """ + def __getstate__(self): if isinstance(self.file, BytesIO): + # BytesIO is already pickleable, return the state unmodified return self.__dict__ + + # remove unpickleable file attribute, and only store its name and pos state = self.__dict__.copy() - state["file"] = _copy_file_to_bytes_stream(state["file"]) + del state["file"] + state["_filename"] = self.file.name + state["_filepos"] = self.file.tell() return state - -def _copy_file_to_bytes_stream(f) -> BytesIO: - # Copy bytes from file object to BytesIO stream. - # The returned stream also records the original file `name` and current position. - pos = f.tell() - f.seek(0) - buf = BytesIO(f.read()) - f.seek(pos) - buf.seek(pos) - if hasattr(f, "name"): - buf.name = f.name - return buf + def __setstate__(self, state): + if "file" not in state: + self.file = open(state.pop("_filename"), "rb") + self.file.seek(state.pop("_filepos")) + self.__dict__.update(state) # default compression level for WOFF 1.0 tables and metadata