Merge pull request #6069 from radarhere/pyencoder

This commit is contained in:
Hugo van Kemenade 2022-02-27 18:34:09 +02:00 committed by GitHub
commit 1d3b373160
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 390 additions and 155 deletions

View File

@ -2,7 +2,12 @@ import pytest
from PIL import BlpImagePlugin, Image 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(): def test_load_blp1():
@ -25,6 +30,28 @@ def test_load_blp2_dxt1a():
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") 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( @pytest.mark.parametrize(
"test_file", "test_file",
[ [

View File

@ -124,6 +124,23 @@ class TestImageFile:
with pytest.raises(OSError): with pytest.raises(OSError):
p.close() 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): def test_truncated(self):
b = BytesIO( b = BytesIO(
b"BM000000000000" # head_data b"BM000000000000" # head_data
@ -179,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
@ -190,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()
@ -250,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()
@ -259,14 +285,90 @@ class TestPyDecoder:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() 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) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
assert im.format is None
assert im.get_format_mimetype() is None
def test_oserror(self): fp = BytesIO()
im = Image.new("RGB", (1, 1)) ImageFile._save(
with pytest.raises(OSError): im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")]
im.save(BytesIO(), "JPEG2000", num_resolutions=2) )
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

@ -26,6 +26,20 @@ Fully supported formats
.. contents:: .. 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 BMP
^^^ ^^^
@ -1042,13 +1056,6 @@ Pillow reads and writes X bitmap files (mode ``1``).
Read-only formats 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 CUR
^^^ ^^^

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

@ -29,6 +29,7 @@ BLP files come in many different flavours:
- DXT5 compression is used if alpha_encoding == 7. - DXT5 compression is used if alpha_encoding == 7.
""" """
import os
import struct import struct
import warnings import warnings
from enum import IntEnum from enum import IntEnum
@ -266,6 +267,10 @@ class BLPFormatError(NotImplementedError):
pass pass
def _accept(prefix):
return prefix[:4] in (b"BLP1", b"BLP2")
class BlpImageFile(ImageFile.ImageFile): class BlpImageFile(ImageFile.ImageFile):
""" """
Blizzard Mipmap Format Blizzard Mipmap Format
@ -276,51 +281,52 @@ class BlpImageFile(ImageFile.ImageFile):
def _open(self): def _open(self):
self.magic = self.fp.read(4) self.magic = self.fp.read(4)
self._read_blp_header()
if self.magic == b"BLP1": self.fp.seek(5, os.SEEK_CUR)
decoder = "BLP1" (self._blp_alpha_depth,) = struct.unpack("<b", self.fp.read(1))
self.mode = "RGB"
elif self.magic == b"BLP2": self.fp.seek(2, os.SEEK_CUR)
decoder = "BLP2" self._size = struct.unpack("<II", self.fp.read(8))
self.mode = "RGBA" if self._blp_alpha_depth else "RGB"
if self.magic in (b"BLP1", b"BLP2"):
decoder = self.magic.decode()
else: else:
raise BLPFormatError(f"Bad BLP magic {repr(self.magic)}") raise BLPFormatError(f"Bad BLP magic {repr(self.magic)}")
self.mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
def _read_blp_header(self):
(self._blp_compression,) = struct.unpack("<i", self.fp.read(4))
(self._blp_encoding,) = struct.unpack("<b", self.fp.read(1))
(self._blp_alpha_depth,) = struct.unpack("<b", self.fp.read(1))
(self._blp_alpha_encoding,) = struct.unpack("<b", self.fp.read(1))
(self._blp_mips,) = struct.unpack("<b", self.fp.read(1))
self._size = struct.unpack("<II", self.fp.read(8))
if self.magic == b"BLP1":
# Only present for BLP1
(self._blp_encoding,) = struct.unpack("<i", self.fp.read(4))
(self._blp_subtype,) = struct.unpack("<i", self.fp.read(4))
self._blp_offsets = struct.unpack("<16I", self.fp.read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self.fp.read(16 * 4))
class _BLPBaseDecoder(ImageFile.PyDecoder): class _BLPBaseDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer):
try: try:
self.fd.seek(0)
self.magic = self.fd.read(4)
self._read_blp_header() self._read_blp_header()
self._load() self._load()
except struct.error as e: except struct.error as e:
raise OSError("Truncated Blp file") from e raise OSError("Truncated BLP file") from e
return 0, 0 return 0, 0
def _read_blp_header(self):
self.fd.seek(4)
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
(self._blp_encoding,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_depth,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_encoding,) = struct.unpack("<b", self._safe_read(1))
self.fd.seek(1, os.SEEK_CUR) # mips
self.size = struct.unpack("<II", self._safe_read(8))
if isinstance(self, BLP1Decoder):
# Only present for BLP1
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4))
self.fd.seek(4, os.SEEK_CUR) # subtype
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _safe_read(self, length): def _safe_read(self, length):
return ImageFile._safe_read(self.fd, length) return ImageFile._safe_read(self.fd, length)
@ -334,23 +340,20 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
ret.append((b, g, r, a)) ret.append((b, g, r, a))
return ret return ret
def _read_blp_header(self): def _read_bgra(self, palette):
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4)) data = bytearray()
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
(self._blp_encoding,) = struct.unpack("<b", self._safe_read(1)) while True:
(self._blp_alpha_depth,) = struct.unpack("<b", self._safe_read(1)) try:
(self._blp_alpha_encoding,) = struct.unpack("<b", self._safe_read(1)) (offset,) = struct.unpack("<B", _data.read(1))
(self._blp_mips,) = struct.unpack("<b", self._safe_read(1)) except struct.error:
break
self.size = struct.unpack("<II", self._safe_read(8)) b, g, r, a = palette[offset]
d = (r, g, b)
if self.magic == b"BLP1": if self._blp_alpha_depth:
# Only present for BLP1 d += (a,)
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4)) data.extend(d)
(self._blp_subtype,) = struct.unpack("<i", self._safe_read(4)) return data
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
class BLP1Decoder(_BLPBaseDecoder): class BLP1Decoder(_BLPBaseDecoder):
@ -360,17 +363,8 @@ class BLP1Decoder(_BLPBaseDecoder):
elif self._blp_compression == 1: elif self._blp_compression == 1:
if self._blp_encoding in (4, 5): if self._blp_encoding in (4, 5):
data = bytearray()
palette = self._read_palette() palette = self._read_palette()
_data = BytesIO(self._safe_read(self._blp_lengths[0])) data = self._read_bgra(palette)
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
except struct.error:
break
b, g, r, a = palette[offset]
data.extend([r, g, b])
self.set_as_raw(bytes(data)) self.set_as_raw(bytes(data))
else: else:
raise BLPFormatError( raise BLPFormatError(
@ -401,23 +395,16 @@ class BLP2Decoder(_BLPBaseDecoder):
def _load(self): def _load(self):
palette = self._read_palette() palette = self._read_palette()
data = bytearray()
self.fd.seek(self._blp_offsets[0]) self.fd.seek(self._blp_offsets[0])
if self._blp_compression == 1: if self._blp_compression == 1:
# Uncompressed or DirectX compression # Uncompressed or DirectX compression
if self._blp_encoding == Encoding.UNCOMPRESSED: if self._blp_encoding == Encoding.UNCOMPRESSED:
_data = BytesIO(self._safe_read(self._blp_lengths[0])) data = self._read_bgra(palette)
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
except struct.error:
break
b, g, r, a = palette[offset]
data.extend((r, g, b))
elif self._blp_encoding == Encoding.DXT: elif self._blp_encoding == Encoding.DXT:
data = bytearray()
if self._blp_alpha_encoding == AlphaEncoding.DXT1: if self._blp_alpha_encoding == AlphaEncoding.DXT1:
linesize = (self.size[0] + 3) // 4 * 8 linesize = (self.size[0] + 3) // 4 * 8
for yb in range((self.size[1] + 3) // 4): for yb in range((self.size[1] + 3) // 4):
@ -452,12 +439,59 @@ class BLP2Decoder(_BLPBaseDecoder):
self.set_as_raw(bytes(data)) self.set_as_raw(bytes(data))
def _accept(prefix): class BLPEncoder(ImageFile.PyEncoder):
return prefix[:4] in (b"BLP1", b"BLP2") _pushes_fd = True
def _write_palette(self):
data = b""
palette = self.im.getpalette("RGBA", "RGBA")
for i in range(256):
r, g, b, a = palette[i * 4 : (i + 1) * 4]
data += struct.pack("<4B", b, g, r, a)
return data
def encode(self, bufsize):
palette_data = self._write_palette()
offset = 20 + 16 * 4 * 2 + len(palette_data)
data = struct.pack("<16I", offset, *((0,) * 15))
w, h = self.im.size
data += struct.pack("<16I", w * h, *((0,) * 15))
data += palette_data
for y in range(h):
for x in range(w):
data += struct.pack("<B", self.im.getpixel((x, y)))
return len(data), 0, data
def _save(im, fp, filename, save_all=False):
if im.mode != "P":
raise ValueError("Unsupported BLP image mode")
magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2"
fp.write(magic)
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
fp.write(struct.pack("<b", 1 if im.palette.mode == "RGBA" else 0))
fp.write(struct.pack("<b", 0)) # alpha encoding
fp.write(struct.pack("<b", 0)) # mips
fp.write(struct.pack("<II", *im.size))
if magic == b"BLP1":
fp.write(struct.pack("<i", 5))
fp.write(struct.pack("<i", 0))
ImageFile._save(im, fp, [("BLP", (0, 0) + im.size, 0, im.mode)])
Image.register_open(BlpImageFile.format, BlpImageFile, _accept) Image.register_open(BlpImageFile.format, BlpImageFile, _accept)
Image.register_extension(BlpImageFile.format, ".blp") Image.register_extension(BlpImageFile.format, ".blp")
Image.register_decoder("BLP1", BLP1Decoder) Image.register_decoder("BLP1", BLP1Decoder)
Image.register_decoder("BLP2", BLP2Decoder) Image.register_decoder("BLP2", BLP2Decoder)
Image.register_save(BlpImageFile.format, _save)
Image.register_encoder("BLP", BLPEncoder)

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

View File

@ -149,14 +149,13 @@ _encode(ImagingEncoderObject *encoder, PyObject *args) {
} }
static PyObject * static PyObject *
_encode_to_pyfd(ImagingEncoderObject *encoder, PyObject *args) { _encode_to_pyfd(ImagingEncoderObject *encoder) {
PyObject *result; PyObject *result;
int status; int status;
if (!encoder->pushes_fd) { if (!encoder->pushes_fd) {
// UNDONE, appropriate errcode??? // UNDONE, appropriate errcode???
result = Py_BuildValue("ii", 0, IMAGING_CODEC_CONFIG); result = Py_BuildValue("ii", 0, IMAGING_CODEC_CONFIG);
;
return result; return result;
} }
@ -307,7 +306,7 @@ static struct PyMethodDef methods[] = {
{"encode", (PyCFunction)_encode, METH_VARARGS}, {"encode", (PyCFunction)_encode, METH_VARARGS},
{"cleanup", (PyCFunction)_encode_cleanup, METH_VARARGS}, {"cleanup", (PyCFunction)_encode_cleanup, METH_VARARGS},
{"encode_to_file", (PyCFunction)_encode_to_file, METH_VARARGS}, {"encode_to_file", (PyCFunction)_encode_to_file, METH_VARARGS},
{"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, METH_VARARGS}, {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, METH_NOARGS},
{"setimage", (PyCFunction)_setimage, METH_VARARGS}, {"setimage", (PyCFunction)_setimage, METH_VARARGS},
{"setfd", (PyCFunction)_setfd, METH_VARARGS}, {"setfd", (PyCFunction)_setfd, METH_VARARGS},
{NULL, NULL} /* sentinel */ {NULL, NULL} /* sentinel */