-
Notifications
You must be signed in to change notification settings - Fork 448
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1967 from anthrotype/pickle
sfnt: add __getstate__ and __setstate__ to SFNTReader to make it pickelable
- Loading branch information
Showing
3 changed files
with
82 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,8 @@ | |
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 | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
anthrotype
Author
Member
|
||
from fontTools.misc import sstruct | ||
from fontTools.ttLib import TTLibError | ||
import struct | ||
|
@@ -122,29 +123,29 @@ def __delitem__(self, tag): | |
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 | ||
|
||
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 | ||
# 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. | ||
|
||
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@anthrotype Need to import SimpleNamespace because it's used below! This killed a bunch of stuff.