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.
|
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.misc import sstruct
|
||||||
from fontTools.ttLib import TTLibError
|
from fontTools.ttLib import TTLibError
|
||||||
import struct
|
import struct
|
||||||
@ -122,29 +123,29 @@ class SFNTReader(object):
|
|||||||
def close(self):
|
def close(self):
|
||||||
self.file.close()
|
self.file.close()
|
||||||
|
|
||||||
def __deepcopy__(self, memo):
|
# We define custom __getstate__ and __setstate__ to make SFNTReader pickle-able
|
||||||
"""Overrides the default deepcopy of SFNTReader object, to make it work
|
# and deepcopy-able. When a TTFont is loaded as lazy=True, SFNTReader holds a
|
||||||
in the case when TTFont is loaded with lazy=True, and thus reader holds a
|
# reference to an external file object which is not pickleable. So in __getstate__
|
||||||
reference to a file object which is not pickleable.
|
# we store the file name and current position, and in __setstate__ we reopen the
|
||||||
We work around it by manually copying the data into a in-memory stream.
|
# same named file after unpickling.
|
||||||
"""
|
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
cls = self.__class__
|
def __getstate__(self):
|
||||||
obj = cls.__new__(cls)
|
if isinstance(self.file, BytesIO):
|
||||||
for k, v in self.__dict__.items():
|
# BytesIO is already pickleable, return the state unmodified
|
||||||
if k == "file":
|
return self.__dict__
|
||||||
pos = v.tell()
|
|
||||||
v.seek(0)
|
# remove unpickleable file attribute, and only store its name and pos
|
||||||
buf = BytesIO(v.read())
|
state = self.__dict__.copy()
|
||||||
v.seek(pos)
|
del state["file"]
|
||||||
buf.seek(pos)
|
state["_filename"] = self.file.name
|
||||||
if hasattr(v, "name"):
|
state["_filepos"] = self.file.tell()
|
||||||
buf.name = v.name
|
return state
|
||||||
obj.file = buf
|
|
||||||
else:
|
def __setstate__(self, state):
|
||||||
obj.__dict__[k] = deepcopy(v, memo)
|
if "file" not in state:
|
||||||
return obj
|
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
|
# default compression level for WOFF 1.0 tables and metadata
|
||||||
|
@ -1,7 +1,59 @@
|
|||||||
from fontTools.misc.py23 import *
|
import io
|
||||||
from fontTools.ttLib.sfnt import calcChecksum
|
import copy
|
||||||
|
import pickle
|
||||||
|
from fontTools.ttLib.sfnt import calcChecksum, SFNTReader
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def test_calcChecksum():
|
def test_calcChecksum():
|
||||||
assert calcChecksum(b"abcd") == 1633837924
|
assert calcChecksum(b"abcd") == 1633837924
|
||||||
assert calcChecksum(b"abcdxyz") == 3655064932
|
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."""
|
"""(De)serialize to get final binary layout."""
|
||||||
buf = BytesIO()
|
buf = BytesIO()
|
||||||
font.save(buf)
|
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)
|
buf.seek(0)
|
||||||
return TTFont(buf)
|
return TTFont(buf)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user