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 .helper import assert_image_equal_tofile
from .helper import (
assert_image_equal,
assert_image_equal_tofile,
assert_image_similar,
hopper,
)
def test_load_blp1():
@ -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",
[

View File

@ -124,6 +124,23 @@ class TestImageFile:
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
@ -179,6 +196,11 @@ class MockPyDecoder(ImageFile.PyDecoder):
return -1, 0
class MockPyEncoder(ImageFile.PyEncoder):
def encode(self, buffer):
return 1, 1, b""
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)]
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()
@ -250,7 +277,6 @@ class TestPyDecoder:
im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)]
self.get_decoder()
with pytest.raises(ValueError):
im.load()
@ -259,14 +285,90 @@ class TestPyDecoder:
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)

View File

@ -8,4 +8,4 @@ Appendices
image-file-formats
text-anchors
writing-your-own-file-decoder
writing-your-own-image-plugin

View File

@ -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
^^^
@ -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
^^^

View File

@ -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
@ -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.

View File

@ -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

View File

@ -29,6 +29,7 @@ BLP files come in many different flavours:
- DXT5 compression is used if alpha_encoding == 7.
"""
import os
import struct
import warnings
from enum import IntEnum
@ -266,6 +267,10 @@ class BLPFormatError(NotImplementedError):
pass
def _accept(prefix):
return prefix[:4] in (b"BLP1", b"BLP2")
class BlpImageFile(ImageFile.ImageFile):
"""
Blizzard Mipmap Format
@ -276,51 +281,52 @@ class BlpImageFile(ImageFile.ImageFile):
def _open(self):
self.magic = self.fp.read(4)
self._read_blp_header()
if self.magic == b"BLP1":
decoder = "BLP1"
self.mode = "RGB"
elif self.magic == b"BLP2":
decoder = "BLP2"
self.mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.fp.seek(5, os.SEEK_CUR)
(self._blp_alpha_depth,) = struct.unpack("<b", self.fp.read(1))
self.fp.seek(2, os.SEEK_CUR)
self._size = struct.unpack("<II", self.fp.read(8))
if self.magic in (b"BLP1", b"BLP2"):
decoder = self.magic.decode()
else:
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))]
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):
_pulls_fd = True
def decode(self, buffer):
try:
self.fd.seek(0)
self.magic = self.fd.read(4)
self._read_blp_header()
self._load()
except struct.error as e:
raise OSError("Truncated Blp file") from e
raise OSError("Truncated BLP file") from e
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):
return ImageFile._safe_read(self.fd, length)
@ -334,23 +340,20 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
ret.append((b, g, r, a))
return ret
def _read_blp_header(self):
(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._blp_mips,) = struct.unpack("<b", self._safe_read(1))
self.size = struct.unpack("<II", self._safe_read(8))
if self.magic == b"BLP1":
# Only present for BLP1
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4))
(self._blp_subtype,) = struct.unpack("<i", self._safe_read(4))
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _read_bgra(self, palette):
data = bytearray()
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
except struct.error:
break
b, g, r, a = palette[offset]
d = (r, g, b)
if self._blp_alpha_depth:
d += (a,)
data.extend(d)
return data
class BLP1Decoder(_BLPBaseDecoder):
@ -360,17 +363,8 @@ class BLP1Decoder(_BLPBaseDecoder):
elif self._blp_compression == 1:
if self._blp_encoding in (4, 5):
data = bytearray()
palette = self._read_palette()
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
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])
data = self._read_bgra(palette)
self.set_as_raw(bytes(data))
else:
raise BLPFormatError(
@ -401,23 +395,16 @@ class BLP2Decoder(_BLPBaseDecoder):
def _load(self):
palette = self._read_palette()
data = bytearray()
self.fd.seek(self._blp_offsets[0])
if self._blp_compression == 1:
# Uncompressed or DirectX compression
if self._blp_encoding == Encoding.UNCOMPRESSED:
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
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))
data = self._read_bgra(palette)
elif self._blp_encoding == Encoding.DXT:
data = bytearray()
if self._blp_alpha_encoding == AlphaEncoding.DXT1:
linesize = (self.size[0] + 3) // 4 * 8
for yb in range((self.size[1] + 3) // 4):
@ -452,12 +439,59 @@ class BLP2Decoder(_BLPBaseDecoder):
self.set_as_raw(bytes(data))
def _accept(prefix):
return prefix[:4] in (b"BLP1", b"BLP2")
class BLPEncoder(ImageFile.PyEncoder):
_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_extension(BlpImageFile.format, ".blp")
Image.register_decoder("BLP1", BLP1Decoder)
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",
-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)
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()
@ -596,31 +591,16 @@ class PyDecoder:
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
"""
@ -628,16 +608,16 @@ class PyDecoder:
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
@ -670,6 +650,32 @@ class PyDecoder:
):
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):
"""
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")
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
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 *
_encode_to_pyfd(ImagingEncoderObject *encoder, PyObject *args) {
_encode_to_pyfd(ImagingEncoderObject *encoder) {
PyObject *result;
int status;
if (!encoder->pushes_fd) {
// UNDONE, appropriate errcode???
result = Py_BuildValue("ii", 0, IMAGING_CODEC_CONFIG);
;
return result;
}
@ -307,7 +306,7 @@ static struct PyMethodDef methods[] = {
{"encode", (PyCFunction)_encode, METH_VARARGS},
{"cleanup", (PyCFunction)_encode_cleanup, 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},
{"setfd", (PyCFunction)_setfd, METH_VARARGS},
{NULL, NULL} /* sentinel */