Skip to content

Commit 1d3b373

Browse files
authored
Merge pull request #6069 from radarhere/pyencoder
2 parents 841d60c + e367746 commit 1d3b373

File tree

9 files changed

+388
-153
lines changed

9 files changed

+388
-153
lines changed

Tests/test_file_blp.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
from PIL import BlpImagePlugin, Image
44

5-
from .helper import assert_image_equal_tofile
5+
from .helper import (
6+
assert_image_equal,
7+
assert_image_equal_tofile,
8+
assert_image_similar,
9+
hopper,
10+
)
611

712

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

2732

33+
def test_save(tmp_path):
34+
f = str(tmp_path / "temp.blp")
35+
36+
for version in ("BLP1", "BLP2"):
37+
im = hopper("P")
38+
im.save(f, blp_version=version)
39+
40+
with Image.open(f) as reloaded:
41+
assert_image_equal(im.convert("RGB"), reloaded)
42+
43+
with Image.open("Tests/images/transparent.png") as im:
44+
f = str(tmp_path / "temp.blp")
45+
im.convert("P").save(f, blp_version=version)
46+
47+
with Image.open(f) as reloaded:
48+
assert_image_similar(im, reloaded, 8)
49+
50+
im = hopper()
51+
with pytest.raises(ValueError):
52+
im.save(f)
53+
54+
2855
@pytest.mark.parametrize(
2956
"test_file",
3057
[

Tests/test_imagefile.py

Lines changed: 130 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,23 @@ def test_negative_stride(self):
124124
with pytest.raises(OSError):
125125
p.close()
126126

127+
def test_no_format(self):
128+
buf = BytesIO(b"\x00" * 255)
129+
130+
class DummyImageFile(ImageFile.ImageFile):
131+
def _open(self):
132+
self.mode = "RGB"
133+
self._size = (1, 1)
134+
135+
im = DummyImageFile(buf)
136+
assert im.format is None
137+
assert im.get_format_mimetype() is None
138+
139+
def test_oserror(self):
140+
im = Image.new("RGB", (1, 1))
141+
with pytest.raises(OSError):
142+
im.save(BytesIO(), "JPEG2000", num_resolutions=2)
143+
127144
def test_truncated(self):
128145
b = BytesIO(
129146
b"BM000000000000" # head_data
@@ -179,6 +196,11 @@ def decode(self, buffer):
179196
return -1, 0
180197

181198

199+
class MockPyEncoder(ImageFile.PyEncoder):
200+
def encode(self, buffer):
201+
return 1, 1, b""
202+
203+
182204
xoff, yoff, xsize, ysize = 10, 20, 100, 100
183205

184206

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

192214

193-
class TestPyDecoder:
194-
def get_decoder(self):
195-
decoder = MockPyDecoder(None)
215+
class CodecsTest:
216+
@classmethod
217+
def setup_class(cls):
218+
cls.decoder = MockPyDecoder(None)
219+
cls.encoder = MockPyEncoder(None)
196220

197-
def closure(mode, *args):
198-
decoder.__init__(mode, *args)
199-
return decoder
221+
def decoder_closure(mode, *args):
222+
cls.decoder.__init__(mode, *args)
223+
return cls.decoder
200224

201-
Image.register_decoder("MOCK", closure)
202-
return decoder
225+
def encoder_closure(mode, *args):
226+
cls.encoder.__init__(mode, *args)
227+
return cls.encoder
203228

229+
Image.register_decoder("MOCK", decoder_closure)
230+
Image.register_encoder("MOCK", encoder_closure)
231+
232+
233+
class TestPyDecoder(CodecsTest):
204234
def test_setimage(self):
205235
buf = BytesIO(b"\x00" * 255)
206236

207237
im = MockImageFile(buf)
208-
d = self.get_decoder()
209238

210239
im.load()
211240

212-
assert d.state.xoff == xoff
213-
assert d.state.yoff == yoff
214-
assert d.state.xsize == xsize
215-
assert d.state.ysize == ysize
241+
assert self.decoder.state.xoff == xoff
242+
assert self.decoder.state.yoff == yoff
243+
assert self.decoder.state.xsize == xsize
244+
assert self.decoder.state.ysize == ysize
216245

217246
with pytest.raises(ValueError):
218-
d.set_as_raw(b"\x00")
247+
self.decoder.set_as_raw(b"\x00")
219248

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

223252
im = MockImageFile(buf)
224253
im.tile = [("MOCK", None, 32, None)]
225-
d = self.get_decoder()
226254

227255
im.load()
228256

229-
assert d.state.xoff == 0
230-
assert d.state.yoff == 0
231-
assert d.state.xsize == 200
232-
assert d.state.ysize == 200
257+
assert self.decoder.state.xoff == 0
258+
assert self.decoder.state.yoff == 0
259+
assert self.decoder.state.xsize == 200
260+
assert self.decoder.state.ysize == 200
233261

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

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

241268
with pytest.raises(ValueError):
242269
im.load()
@@ -250,7 +277,6 @@ def test_oversize(self):
250277

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

255281
with pytest.raises(ValueError):
256282
im.load()
@@ -259,14 +285,90 @@ def test_oversize(self):
259285
with pytest.raises(ValueError):
260286
im.load()
261287

262-
def test_no_format(self):
288+
def test_decode(self):
289+
decoder = ImageFile.PyDecoder(None)
290+
with pytest.raises(NotImplementedError):
291+
decoder.decode(None)
292+
293+
294+
class TestPyEncoder(CodecsTest):
295+
def test_setimage(self):
263296
buf = BytesIO(b"\x00" * 255)
264297

265298
im = MockImageFile(buf)
266-
assert im.format is None
267-
assert im.get_format_mimetype() is None
268299

269-
def test_oserror(self):
270-
im = Image.new("RGB", (1, 1))
271-
with pytest.raises(OSError):
272-
im.save(BytesIO(), "JPEG2000", num_resolutions=2)
300+
fp = BytesIO()
301+
ImageFile._save(
302+
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")]
303+
)
304+
305+
assert self.encoder.state.xoff == xoff
306+
assert self.encoder.state.yoff == yoff
307+
assert self.encoder.state.xsize == xsize
308+
assert self.encoder.state.ysize == ysize
309+
310+
def test_extents_none(self):
311+
buf = BytesIO(b"\x00" * 255)
312+
313+
im = MockImageFile(buf)
314+
im.tile = [("MOCK", None, 32, None)]
315+
316+
fp = BytesIO()
317+
ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")])
318+
319+
assert self.encoder.state.xoff == 0
320+
assert self.encoder.state.yoff == 0
321+
assert self.encoder.state.xsize == 200
322+
assert self.encoder.state.ysize == 200
323+
324+
def test_negsize(self):
325+
buf = BytesIO(b"\x00" * 255)
326+
327+
im = MockImageFile(buf)
328+
329+
fp = BytesIO()
330+
with pytest.raises(ValueError):
331+
ImageFile._save(
332+
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
333+
)
334+
335+
with pytest.raises(ValueError):
336+
ImageFile._save(
337+
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")]
338+
)
339+
340+
def test_oversize(self):
341+
buf = BytesIO(b"\x00" * 255)
342+
343+
im = MockImageFile(buf)
344+
345+
fp = BytesIO()
346+
with pytest.raises(ValueError):
347+
ImageFile._save(
348+
im,
349+
fp,
350+
[("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")],
351+
)
352+
353+
with pytest.raises(ValueError):
354+
ImageFile._save(
355+
im,
356+
fp,
357+
[("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")],
358+
)
359+
360+
def test_encode(self):
361+
encoder = ImageFile.PyEncoder(None)
362+
with pytest.raises(NotImplementedError):
363+
encoder.encode(None)
364+
365+
bytes_consumed, errcode = encoder.encode_to_pyfd()
366+
assert bytes_consumed == 0
367+
assert ImageFile.ERRORS[errcode] == "bad configuration"
368+
369+
encoder._pushes_fd = True
370+
with pytest.raises(NotImplementedError):
371+
encoder.encode_to_pyfd()
372+
373+
with pytest.raises(NotImplementedError):
374+
encoder.encode_to_file(None, None)

docs/handbook/appendices.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ Appendices
88

99
image-file-formats
1010
text-anchors
11-
writing-your-own-file-decoder
11+
writing-your-own-image-plugin

docs/handbook/image-file-formats.rst

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ Fully supported formats
2626

2727
.. contents::
2828

29+
BLP
30+
^^^
31+
32+
BLP is the Blizzard Mipmap Format, a texture format used in World of
33+
Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1``
34+
images, and all types of ``BLP2`` images.
35+
36+
Pillow supports writing BLP images. The :py:meth:`~PIL.Image.Image.save` method
37+
can take the following keyword arguments:
38+
39+
**blp_version**
40+
If present and set to "BLP1", images will be saved as BLP1. Otherwise, images
41+
will be saved as BLP2.
42+
2943
BMP
3044
^^^
3145

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

1045-
BLP
1046-
^^^
1047-
1048-
BLP is the Blizzard Mipmap Format, a texture format used in World of
1049-
Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1``
1050-
images, and all types of ``BLP2`` images.
1051-
10521059
CUR
10531060
^^^
10541061

docs/handbook/writing-your-own-file-decoder.rst renamed to docs/handbook/writing-your-own-image-plugin.rst

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ Writing Your Own Image Plugin
44
=============================
55

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

1211
.. warning:: Pillow >= 2.1.0 no longer automatically imports any file
1312
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
413412
free any allocated memory and release any resources from external
414413
libraries.
415414

416-
.. _file-decoders-py:
415+
.. _file-codecs-py:
417416

418-
Writing Your Own File Decoder in Python
419-
=======================================
417+
Writing Your Own File Codec in Python
418+
=====================================
420419

421-
Python file decoders should derive from
422-
:py:class:`PIL.ImageFile.PyDecoder` and should at least override the
423-
decode method. File decoders should be registered using
424-
:py:meth:`PIL.Image.register_decoder`. As in the C implementation of
425-
the file decoders, there are three stages in the lifetime of a
426-
Python-based file decoder:
420+
Python file decoders and encoders should derive from
421+
:py:class:`PIL.ImageFile.PyDecoder` and :py:class:`PIL.ImageFile.PyEncoder`
422+
respectively, and should at least override the decode or encode method.
423+
They should be registered using :py:meth:`PIL.Image.register_decoder` and
424+
:py:meth:`PIL.Image.register_encoder`. As in the C implementation of
425+
the file codecs, there are three stages in the lifetime of a
426+
Python-based file codec:
427427

428428
1. Setup: Pillow looks for the decoder in the registry, then
429429
instantiates the class.
430430

431-
2. Decoding: The decoder instance's ``decode`` method is repeatedly
432-
called with a buffer of data to be interpreted.
433-
434-
3. Cleanup: The decoder instance's ``cleanup`` method is called.
431+
2. Transforming: The instance's ``decode`` method is repeatedly called with
432+
a buffer of data to be interpreted, or the ``encode`` method is repeatedly
433+
called with the size of data to be output.
435434

435+
3. Cleanup: The instance's ``cleanup`` method is called.

docs/reference/ImageFile.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,16 @@ Classes
4040
.. autoclass:: PIL.ImageFile.Parser()
4141
:members:
4242

43+
.. autoclass:: PIL.ImageFile.PyCodec()
44+
:members:
45+
4346
.. autoclass:: PIL.ImageFile.PyDecoder()
4447
:members:
48+
:show-inheritance:
49+
50+
.. autoclass:: PIL.ImageFile.PyEncoder()
51+
:members:
52+
:show-inheritance:
4553

4654
.. autoclass:: PIL.ImageFile.ImageFile()
4755
:member-order: bysource

0 commit comments

Comments
 (0)