Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added PyEncoder and support BLP saving #6069

Merged
merged 7 commits into from Feb 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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