mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-01-25 00:34:14 +03:00
Merge pull request #6069 from radarhere/pyencoder
This commit is contained in:
commit
1d3b373160
|
@ -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",
|
||||
[
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -8,4 +8,4 @@ Appendices
|
|||
|
||||
image-file-formats
|
||||
text-anchors
|
||||
writing-your-own-file-decoder
|
||||
writing-your-own-image-plugin
|
||||
|
|
|
@ -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
|
||||
^^^
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 */
|
||||
|
|
Loading…
Reference in New Issue
Block a user