Merge pull request #1967 from anthrotype/pickle
sfnt: add __getstate__ and __setstate__ to SFNTReader to make it pickelable
This commit is contained in:
commit
7ca42f6623
@ -12,7 +12,8 @@ classes, since whenever to number of tables changes or whenever
|
||||
a table's length chages you need to rewrite the whole file anyway.
|
||||
"""
|
||||
|
||||
from fontTools.misc.py23 import *
|
||||
from io import BytesIO
|
||||
from fontTools.misc.py23 import Tag
|
||||
from fontTools.misc import sstruct
|
||||
from fontTools.ttLib import TTLibError
|
||||
import struct
|
||||
@ -122,29 +123,29 @@ class SFNTReader(object):
|
||||
def close(self):
|
||||
self.file.close()
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
"""Overrides the default deepcopy of SFNTReader object, to make it work
|
||||
in the case when TTFont is loaded with lazy=True, and thus reader holds a
|
||||
reference to a file object which is not pickleable.
|
||||
We work around it by manually copying the data into a in-memory stream.
|
||||
"""
|
||||
from copy import deepcopy
|
||||
# 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.
|
||||
|
||||
cls = self.__class__
|
||||
obj = cls.__new__(cls)
|
||||
for k, v in self.__dict__.items():
|
||||
if k == "file":
|
||||
pos = v.tell()
|
||||
v.seek(0)
|
||||
buf = BytesIO(v.read())
|
||||
v.seek(pos)
|
||||
buf.seek(pos)
|
||||
if hasattr(v, "name"):
|
||||
buf.name = v.name
|
||||
obj.file = buf
|
||||
else:
|
||||
obj.__dict__[k] = deepcopy(v, memo)
|
||||
return obj
|
||||
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()
|
||||
del state["file"]
|
||||
state["_filename"] = self.file.name
|
||||
state["_filepos"] = self.file.tell()
|
||||
return state
|
||||
|
||||
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
|
||||
|
@ -1,7 +1,59 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.ttLib.sfnt import calcChecksum
|
||||
import io
|
||||
import copy
|
||||
import pickle
|
||||
from fontTools.ttLib.sfnt import calcChecksum, SFNTReader
|
||||
import pytest
|
||||
|
||||
|
||||
def test_calcChecksum():
|
||||
assert calcChecksum(b"abcd") == 1633837924
|
||||
assert calcChecksum(b"abcdxyz") == 3655064932
|
||||
|
||||
|
||||
EMPTY_SFNT = b"\x00\x01\x00\x00" + b"\x00" * 8
|
||||
|
||||
|
||||
def pickle_unpickle(obj):
|
||||
return pickle.loads(pickle.dumps(obj))
|
||||
|
||||
|
||||
class SFNTReaderTest:
|
||||
@pytest.mark.parametrize("deepcopy", [copy.deepcopy, pickle_unpickle])
|
||||
def test_pickle_protocol_FileIO(self, deepcopy, tmp_path):
|
||||
fontfile = tmp_path / "test.ttf"
|
||||
fontfile.write_bytes(EMPTY_SFNT)
|
||||
reader = SFNTReader(fontfile.open("rb"))
|
||||
|
||||
reader2 = deepcopy(reader)
|
||||
|
||||
assert reader2 is not reader
|
||||
assert reader2.file is not reader.file
|
||||
|
||||
assert isinstance(reader2.file, io.BufferedReader)
|
||||
assert isinstance(reader2.file.raw, io.FileIO)
|
||||
assert reader2.file.name == reader.file.name
|
||||
assert reader2.file.tell() == reader.file.tell()
|
||||
|
||||
for k, v in reader.__dict__.items():
|
||||
if k == "file":
|
||||
continue
|
||||
assert getattr(reader2, k) == v
|
||||
|
||||
@pytest.mark.parametrize("deepcopy", [copy.deepcopy, pickle_unpickle])
|
||||
def test_pickle_protocol_BytesIO(self, deepcopy, tmp_path):
|
||||
buf = io.BytesIO(EMPTY_SFNT)
|
||||
reader = SFNTReader(buf)
|
||||
|
||||
reader2 = deepcopy(reader)
|
||||
|
||||
assert reader2 is not reader
|
||||
assert reader2.file is not reader.file
|
||||
|
||||
assert isinstance(reader2.file, io.BytesIO)
|
||||
assert reader2.file.tell() == reader.file.tell()
|
||||
assert reader2.file.getvalue() == reader.file.getvalue()
|
||||
|
||||
for k, v in reader.__dict__.items():
|
||||
if k == "file":
|
||||
continue
|
||||
assert getattr(reader2, k) == v
|
||||
|
@ -22,6 +22,9 @@ def reload_font(font):
|
||||
"""(De)serialize to get final binary layout."""
|
||||
buf = BytesIO()
|
||||
font.save(buf)
|
||||
# Close the font to release filesystem resources so that on Windows the tearDown
|
||||
# method can successfully remove the temporary directory created during setUp.
|
||||
font.close()
|
||||
buf.seek(0)
|
||||
return TTFont(buf)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user