Skip to content

Commit

Permalink
Merge pull request #6069 from radarhere/pyencoder
Browse files Browse the repository at this point in the history
  • Loading branch information
hugovk committed Feb 27, 2022
2 parents 841d60c + e367746 commit 1d3b373
Show file tree
Hide file tree
Showing 9 changed files with 388 additions and 153 deletions.
29 changes: 28 additions & 1 deletion Tests/test_file_blp.py
Expand Up @@ -2,7 +2,12 @@

from PIL import BlpImagePlugin, Image

from .helper import assert_image_equal_tofile
from .helper import (
assert_image_equal,
assert_image_equal_tofile,
assert_image_similar,
hopper,
)


def test_load_blp1():
Expand All @@ -25,6 +30,28 @@ def test_load_blp2_dxt1a():
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")


def test_save(tmp_path):
f = str(tmp_path / "temp.blp")

for version in ("BLP1", "BLP2"):
im = hopper("P")
im.save(f, blp_version=version)

with Image.open(f) as reloaded:
assert_image_equal(im.convert("RGB"), reloaded)

with Image.open("Tests/images/transparent.png") as im:
f = str(tmp_path / "temp.blp")
im.convert("P").save(f, blp_version=version)

with Image.open(f) as reloaded:
assert_image_similar(im, reloaded, 8)

im = hopper()
with pytest.raises(ValueError):
im.save(f)


@pytest.mark.parametrize(
"test_file",
[
Expand Down
158 changes: 130 additions & 28 deletions Tests/test_imagefile.py
Expand Up @@ -124,6 +124,23 @@ def test_negative_stride(self):
with pytest.raises(OSError):
p.close()

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

class DummyImageFile(ImageFile.ImageFile):
def _open(self):
self.mode = "RGB"
self._size = (1, 1)

im = DummyImageFile(buf)
assert im.format is None
assert im.get_format_mimetype() is None

def test_oserror(self):
im = Image.new("RGB", (1, 1))
with pytest.raises(OSError):
im.save(BytesIO(), "JPEG2000", num_resolutions=2)

def test_truncated(self):
b = BytesIO(
b"BM000000000000" # head_data
Expand Down Expand Up @@ -179,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 @@ -190,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 closure(mode, *args):
decoder.__init__(mode, *args)
return decoder
def decoder_closure(mode, *args):
cls.decoder.__init__(mode, *args)
return cls.decoder

Image.register_decoder("MOCK", closure)
return decoder
def encoder_closure(mode, *args):
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):
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 @@ -250,7 +277,6 @@ 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()
Expand All @@ -259,14 +285,90 @@ def test_oversize(self):
with pytest.raises(ValueError):
im.load()

def test_no_format(self):
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)
assert im.format is None
assert im.get_format_mimetype() is None

def test_oserror(self):
im = Image.new("RGB", (1, 1))
with pytest.raises(OSError):
im.save(BytesIO(), "JPEG2000", num_resolutions=2)
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)
2 changes: 1 addition & 1 deletion docs/handbook/appendices.rst
Expand Up @@ -8,4 +8,4 @@ Appendices

image-file-formats
text-anchors
writing-your-own-file-decoder
writing-your-own-image-plugin
21 changes: 14 additions & 7 deletions docs/handbook/image-file-formats.rst
Expand Up @@ -26,6 +26,20 @@ Fully supported formats

.. contents::

BLP
^^^

BLP is the Blizzard Mipmap Format, a texture format used in World of
Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1``
images, and all types of ``BLP2`` images.

Pillow supports writing BLP images. The :py:meth:`~PIL.Image.Image.save` method
can take the following keyword arguments:

**blp_version**
If present and set to "BLP1", images will be saved as BLP1. Otherwise, images
will be saved as BLP2.

BMP
^^^

Expand Down Expand Up @@ -1042,13 +1056,6 @@ Pillow reads and writes X bitmap files (mode ``1``).
Read-only formats
-----------------

BLP
^^^

BLP is the Blizzard Mipmap Format, a texture format used in World of
Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1``
images, and all types of ``BLP2`` images.

CUR
^^^

Expand Down
Expand Up @@ -4,10 +4,9 @@ Writing Your Own Image Plugin
=============================

Pillow uses a plugin model which allows you to add your own
decoders to the library, without any changes to the library
itself. Such plugins usually have names like
:file:`XxxImagePlugin.py`, where ``Xxx`` is a unique format name
(usually an abbreviation).
decoders and encoders to the library, without any changes to the library
itself. Such plugins usually have names like :file:`XxxImagePlugin.py`,
where ``Xxx`` is a unique format name (usually an abbreviation).

.. warning:: Pillow >= 2.1.0 no longer automatically imports any file
in the Python path with a name ending in
Expand Down Expand Up @@ -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
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
:py:class:`PIL.ImageFile.PyDecoder` and should at least override the
decode method. File decoders should be registered using
:py:meth:`PIL.Image.register_decoder`. As in the C implementation of
the file decoders, there are three stages in the lifetime of a
Python-based file decoder:
Python file decoders and encoders should derive from
:py:class:`PIL.ImageFile.PyDecoder` and :py:class:`PIL.ImageFile.PyEncoder`
respectively, and should at least override the decode or encode method.
They should be registered using :py:meth:`PIL.Image.register_decoder` and
:py:meth:`PIL.Image.register_encoder`. As in the C implementation of
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
instantiates the class.

2. Decoding: The decoder instance's ``decode`` method is repeatedly
called with a buffer of data to be interpreted.

3. Cleanup: The decoder instance's ``cleanup`` method is called.
2. Transforming: The instance's ``decode`` method is repeatedly called with
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 instance's ``cleanup`` method is called.
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

0 comments on commit 1d3b373

Please sign in to comment.