Added PyEncoder

This commit is contained in:
Andrew Murray 2022-02-25 16:07:01 +11:00
parent afb7728b8c
commit a0e1fde1ed
5 changed files with 233 additions and 70 deletions

View File

@ -196,6 +196,11 @@ class MockPyDecoder(ImageFile.PyDecoder):
return -1, 0 return -1, 0
class MockPyEncoder(ImageFile.PyEncoder):
def encode(self, buffer):
return 1, 1, b""
xoff, yoff, xsize, ysize = 10, 20, 100, 100 xoff, yoff, xsize, ysize = 10, 20, 100, 100
@ -207,53 +212,58 @@ class MockImageFile(ImageFile.ImageFile):
self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)]
class TestPyDecoder: class CodecsTest:
def get_decoder(self): @classmethod
decoder = MockPyDecoder(None) def setup_class(cls):
cls.decoder = MockPyDecoder(None)
cls.encoder = MockPyEncoder(None)
def closure(mode, *args): def decoder_closure(mode, *args):
decoder.__init__(mode, *args) cls.decoder.__init__(mode, *args)
return decoder return cls.decoder
Image.register_decoder("MOCK", closure) def encoder_closure(mode, *args):
return decoder cls.encoder.__init__(mode, *args)
return cls.encoder
Image.register_decoder("MOCK", decoder_closure)
Image.register_encoder("MOCK", encoder_closure)
class TestPyDecoder(CodecsTest):
def test_setimage(self): def test_setimage(self):
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
d = self.get_decoder()
im.load() im.load()
assert d.state.xoff == xoff assert self.decoder.state.xoff == xoff
assert d.state.yoff == yoff assert self.decoder.state.yoff == yoff
assert d.state.xsize == xsize assert self.decoder.state.xsize == xsize
assert d.state.ysize == ysize assert self.decoder.state.ysize == ysize
with pytest.raises(ValueError): with pytest.raises(ValueError):
d.set_as_raw(b"\x00") self.decoder.set_as_raw(b"\x00")
def test_extents_none(self): def test_extents_none(self):
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)] im.tile = [("MOCK", None, 32, None)]
d = self.get_decoder()
im.load() im.load()
assert d.state.xoff == 0 assert self.decoder.state.xoff == 0
assert d.state.yoff == 0 assert self.decoder.state.yoff == 0
assert d.state.xsize == 200 assert self.decoder.state.xsize == 200
assert d.state.ysize == 200 assert self.decoder.state.ysize == 200
def test_negsize(self): def test_negsize(self):
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)]
self.get_decoder()
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() im.load()
@ -267,7 +277,6 @@ class TestPyDecoder:
im = MockImageFile(buf) im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)] im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)]
self.get_decoder()
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() im.load()
@ -275,3 +284,91 @@ class TestPyDecoder:
im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)] im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)]
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() im.load()
def test_decode(self):
decoder = ImageFile.PyDecoder(None)
with pytest.raises(NotImplementedError):
decoder.decode(None)
class TestPyEncoder(CodecsTest):
def test_setimage(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
fp = BytesIO()
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")]
)
assert self.encoder.state.xoff == xoff
assert self.encoder.state.yoff == yoff
assert self.encoder.state.xsize == xsize
assert self.encoder.state.ysize == ysize
def test_extents_none(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)]
fp = BytesIO()
ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")])
assert self.encoder.state.xoff == 0
assert self.encoder.state.yoff == 0
assert self.encoder.state.xsize == 200
assert self.encoder.state.ysize == 200
def test_negsize(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
fp = BytesIO()
with pytest.raises(ValueError):
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
)
with pytest.raises(ValueError):
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")]
)
def test_oversize(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
fp = BytesIO()
with pytest.raises(ValueError):
ImageFile._save(
im,
fp,
[("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")],
)
with pytest.raises(ValueError):
ImageFile._save(
im,
fp,
[("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")],
)
def test_encode(self):
encoder = ImageFile.PyEncoder(None)
with pytest.raises(NotImplementedError):
encoder.encode(None)
bytes_consumed, errcode = encoder.encode_to_pyfd()
assert bytes_consumed == 0
assert ImageFile.ERRORS[errcode] == "bad configuration"
encoder._pushes_fd = True
with pytest.raises(NotImplementedError):
encoder.encode_to_pyfd()
with pytest.raises(NotImplementedError):
encoder.encode_to_file(None, None)

View File

@ -8,4 +8,4 @@ Appendices
image-file-formats image-file-formats
text-anchors text-anchors
writing-your-own-file-decoder writing-your-own-image-plugin

View File

@ -4,10 +4,9 @@ Writing Your Own Image Plugin
============================= =============================
Pillow uses a plugin model which allows you to add your own Pillow uses a plugin model which allows you to add your own
decoders to the library, without any changes to the library decoders and encoders to the library, without any changes to the library
itself. Such plugins usually have names like itself. Such plugins usually have names like :file:`XxxImagePlugin.py`,
:file:`XxxImagePlugin.py`, where ``Xxx`` is a unique format name where ``Xxx`` is a unique format name (usually an abbreviation).
(usually an abbreviation).
.. warning:: Pillow >= 2.1.0 no longer automatically imports any file .. warning:: Pillow >= 2.1.0 no longer automatically imports any file
in the Python path with a name ending in in the Python path with a name ending in
@ -413,23 +412,24 @@ value, or if there is a read error from the file. This function should
free any allocated memory and release any resources from external free any allocated memory and release any resources from external
libraries. libraries.
.. _file-decoders-py: .. _file-codecs-py:
Writing Your Own File Decoder in Python Writing Your Own File Codec in Python
======================================= =====================================
Python file decoders should derive from Python file decoders and encoders should derive from
:py:class:`PIL.ImageFile.PyDecoder` and should at least override the :py:class:`PIL.ImageFile.PyDecoder` and :py:class:`PIL.ImageFile.PyEncoder`
decode method. File decoders should be registered using respectively, and should at least override the decode or encode method.
:py:meth:`PIL.Image.register_decoder`. As in the C implementation of They should be registered using :py:meth:`PIL.Image.register_decoder` and
the file decoders, there are three stages in the lifetime of a :py:meth:`PIL.Image.register_encoder`. As in the C implementation of
Python-based file decoder: the file codecs, there are three stages in the lifetime of a
Python-based file codec:
1. Setup: Pillow looks for the decoder in the registry, then 1. Setup: Pillow looks for the decoder in the registry, then
instantiates the class. instantiates the class.
2. Decoding: The decoder instance's ``decode`` method is repeatedly 2. Transforming: The instance's ``decode`` method is repeatedly called with
called with a buffer of data to be interpreted. a buffer of data to be interpreted, or the ``encode`` method is repeatedly
called with the size of data to be output.
3. Cleanup: The decoder instance's ``cleanup`` method is called.
3. Cleanup: The instance's ``cleanup`` method is called.

View File

@ -40,8 +40,16 @@ Classes
.. autoclass:: PIL.ImageFile.Parser() .. autoclass:: PIL.ImageFile.Parser()
:members: :members:
.. autoclass:: PIL.ImageFile.PyCodec()
:members:
.. autoclass:: PIL.ImageFile.PyDecoder() .. autoclass:: PIL.ImageFile.PyDecoder()
:members: :members:
:show-inheritance:
.. autoclass:: PIL.ImageFile.PyEncoder()
:members:
:show-inheritance:
.. autoclass:: PIL.ImageFile.ImageFile() .. autoclass:: PIL.ImageFile.ImageFile()
:member-order: bysource :member-order: bysource

View File

@ -49,7 +49,11 @@ ERRORS = {
-8: "bad configuration", -8: "bad configuration",
-9: "out of memory error", -9: "out of memory error",
} }
"""Dict of known error codes returned from :meth:`.PyDecoder.decode`.""" """
Dict of known error codes returned from :meth:`.PyDecoder.decode`,
:meth:`.PyEncoder.encode` :meth:`.PyEncoder.encode_to_pyfd` and
:meth:`.PyEncoder.encode_to_file`.
"""
# #
@ -577,16 +581,7 @@ class PyCodecState:
return (self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize) return (self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize)
class PyDecoder: class PyCodec:
"""
Python implementation of a format decoder. Override this class and
add the decoding logic in the :meth:`decode` method.
See :ref:`Writing Your Own File Decoder in Python<file-decoders-py>`
"""
_pulls_fd = False
def __init__(self, mode, *args): def __init__(self, mode, *args):
self.im = None self.im = None
self.state = PyCodecState() self.state = PyCodecState()
@ -596,31 +591,16 @@ class PyDecoder:
def init(self, args): def init(self, args):
""" """
Override to perform decoder specific initialization Override to perform codec specific initialization
:param args: Array of args items from the tile entry :param args: Array of args items from the tile entry
:returns: None :returns: None
""" """
self.args = args self.args = args
@property
def pulls_fd(self):
return self._pulls_fd
def decode(self, buffer):
"""
Override to perform the decoding process.
:param buffer: A bytes object with the data to be decoded.
:returns: A tuple of ``(bytes consumed, errcode)``.
If finished with decoding return <0 for the bytes consumed.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
raise NotImplementedError()
def cleanup(self): def cleanup(self):
""" """
Override to perform decoder specific cleanup Override to perform codec specific cleanup
:returns: None :returns: None
""" """
@ -628,16 +608,16 @@ class PyDecoder:
def setfd(self, fd): def setfd(self, fd):
""" """
Called from ImageFile to set the python file-like object Called from ImageFile to set the Python file-like object
:param fd: A python file-like object :param fd: A Python file-like object
:returns: None :returns: None
""" """
self.fd = fd self.fd = fd
def setimage(self, im, extents=None): def setimage(self, im, extents=None):
""" """
Called from ImageFile to set the core output image for the decoder Called from ImageFile to set the core output image for the codec
:param im: A core image object :param im: A core image object
:param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle :param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle
@ -670,6 +650,32 @@ class PyDecoder:
): ):
raise ValueError("Tile cannot extend outside image") raise ValueError("Tile cannot extend outside image")
class PyDecoder(PyCodec):
"""
Python implementation of a format decoder. Override this class and
add the decoding logic in the :meth:`decode` method.
See :ref:`Writing Your Own File Codec in Python<file-codecs-py>`
"""
_pulls_fd = False
@property
def pulls_fd(self):
return self._pulls_fd
def decode(self, buffer):
"""
Override to perform the decoding process.
:param buffer: A bytes object with the data to be decoded.
:returns: A tuple of ``(bytes consumed, errcode)``.
If finished with decoding return 0 for the bytes consumed.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
raise NotImplementedError()
def set_as_raw(self, data, rawmode=None): def set_as_raw(self, data, rawmode=None):
""" """
Convenience method to set the internal image from a stream of raw data Convenience method to set the internal image from a stream of raw data
@ -690,3 +696,55 @@ class PyDecoder:
raise ValueError("not enough image data") raise ValueError("not enough image data")
if s[1] != 0: if s[1] != 0:
raise ValueError("cannot decode image data") raise ValueError("cannot decode image data")
class PyEncoder(PyCodec):
"""
Python implementation of a format encoder. Override this class and
add the decoding logic in the :meth:`encode` method.
"""
_pushes_fd = False
@property
def pushes_fd(self):
return self._pushes_fd
def encode(self, bufsize):
"""
Override to perform the encoding process.
:param bufsize: Buffer size.
:returns: A tuple of ``(bytes encoded, errcode, bytes)``.
If finished with encoding return 1 for the error code.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
raise NotImplementedError()
def encode_to_pyfd(self):
"""
:returns: A tuple of ``(bytes consumed, errcode)``.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
if not self.pushes_fd:
return 0, -8 # bad configuration
bytes_consumed, errcode, data = self.encode(0)
if data:
self.fd.write(data)
return bytes_consumed, errcode
def encode_to_file(self, fh, bufsize):
"""
:param fh: File handle.
:param bufsize: Buffer size.
:returns: If finished successfully, return 0.
Otherwise, return an error code. Err codes are from
:data:`.ImageFile.ERRORS`.
"""
errcode = 0
while errcode == 0:
status, errcode, buf = self.encode(bufsize)
if status > 0:
fh.write(buf[status:])
return errcode