Skip to content

Commit

Permalink
Added PyEncoder
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere committed Feb 18, 2022
1 parent 747b23b commit fb7f2eb
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 52 deletions.
118 changes: 97 additions & 21 deletions Tests/test_imagefile.py
Expand Up @@ -196,6 +196,11 @@ def decode(self, buffer):
return -1, 0


class MockPyEncoder(ImageFile.PyEncoder):
def encode(self, buffer):
return 1, 1, b""


xoff, yoff, xsize, ysize = 10, 20, 100, 100


Expand All @@ -207,53 +212,58 @@ def _open(self):
self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)]


class TestPyDecoder:
def get_decoder(self):
decoder = MockPyDecoder(None)
class CodecsTest:
@classmethod
def setup_class(cls):
cls.decoder = MockPyDecoder(None)
cls.encoder = MockPyEncoder(None)

def decoder_closure(mode, *args):
cls.decoder.__init__(mode, *args)
return cls.decoder

def closure(mode, *args):
decoder.__init__(mode, *args)
return decoder
def encoder_closure(mode, *args):
cls.encoder.__init__(mode, *args)
return cls.encoder

Image.register_decoder("MOCK", closure)
return decoder
Image.register_decoder("MOCK", decoder_closure)
Image.register_encoder("MOCK", encoder_closure)


class TestPyDecoder(CodecsTest):
def test_setimage(self):
buf = BytesIO(b"\x00" * 255)

im = MockImageFile(buf)
d = self.get_decoder()

im.load()

assert d.state.xoff == xoff
assert d.state.yoff == yoff
assert d.state.xsize == xsize
assert d.state.ysize == ysize
assert self.decoder.state.xoff == xoff
assert self.decoder.state.yoff == yoff
assert self.decoder.state.xsize == xsize
assert self.decoder.state.ysize == ysize

with pytest.raises(ValueError):
d.set_as_raw(b"\x00")
self.decoder.set_as_raw(b"\x00")

def test_extents_none(self):
buf = BytesIO(b"\x00" * 255)

im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)]
d = self.get_decoder()

im.load()

assert d.state.xoff == 0
assert d.state.yoff == 0
assert d.state.xsize == 200
assert d.state.ysize == 200
assert self.decoder.state.xoff == 0
assert self.decoder.state.yoff == 0
assert self.decoder.state.xsize == 200
assert self.decoder.state.ysize == 200

def test_negsize(self):
buf = BytesIO(b"\x00" * 255)

im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)]
self.get_decoder()

with pytest.raises(ValueError):
im.load()
Expand All @@ -267,11 +277,77 @@ def test_oversize(self):

im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)]
self.get_decoder()

with pytest.raises(ValueError):
im.load()

im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)]
with pytest.raises(ValueError):
im.load()


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")],
)
8 changes: 8 additions & 0 deletions docs/reference/ImageFile.rst
Expand Up @@ -40,8 +40,16 @@ Classes
.. autoclass:: PIL.ImageFile.Parser()
:members:

.. autoclass:: PIL.ImageFile.PyCodec()
:members:

.. autoclass:: PIL.ImageFile.PyDecoder()
:members:
:show-inheritance:

.. autoclass:: PIL.ImageFile.PyEncoder()
:members:
:show-inheritance:

.. autoclass:: PIL.ImageFile.ImageFile()
:member-order: bysource
Expand Down
117 changes: 86 additions & 31 deletions src/PIL/ImageFile.py
Expand Up @@ -49,7 +49,11 @@
-8: "bad configuration",
-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`.
"""


#
Expand Down Expand Up @@ -577,16 +581,7 @@ def extents(self):
return (self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize)


class PyDecoder:
"""
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

class PyCodec:
def __init__(self, mode, *args):
self.im = None
self.state = PyCodecState()
Expand All @@ -596,48 +591,33 @@ def __init__(self, mode, *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
:returns: None
"""
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):
"""
Override to perform decoder specific cleanup
Override to perform codec specific cleanup
:returns: None
"""
pass

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
"""
self.fd = fd

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 extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle
Expand Down Expand Up @@ -670,6 +650,32 @@ def setimage(self, im, extents=None):
):
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 Decoder in Python<file-decoders-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):
"""
Convenience method to set the internal image from a stream of raw data
Expand All @@ -690,3 +696,52 @@ def set_as_raw(self, data, rawmode=None):
raise ValueError("not enough image data")
if s[1] != 0:
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
return self.encode(0)[:2]

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

0 comments on commit fb7f2eb

Please sign in to comment.