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 support for reading BMP images with RLE8 compression #6102

Merged
merged 5 commits into from
Mar 23, 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
Binary file added Tests/images/hopper_rle8.bmp
Binary file not shown.
Binary file added Tests/images/hopper_rle8_row_overflow.bmp
Binary file not shown.
1 change: 1 addition & 0 deletions Tests/test_bmp_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def test_questionable():
"rgb32fakealpha.bmp",
"rgb24largepal.bmp",
"pal8os2sp.bmp",
"pal8rletrns.bmp",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running this locally, I was told to add this by the test.

print(f"Please add {f} to the partially supported bmp specs.")

"rgb32bf-xbgr.bmp",
]
for f in get_files("q"):
Expand Down
43 changes: 42 additions & 1 deletion Tests/test_file_bmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

from PIL import BmpImagePlugin, Image

from .helper import assert_image_equal, assert_image_equal_tofile, hopper
from .helper import (
assert_image_equal,
assert_image_equal_tofile,
assert_image_similar_tofile,
hopper,
)


def test_sanity(tmp_path):
Expand Down Expand Up @@ -125,6 +130,42 @@ def test_rgba_bitfields():
assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")


def test_rle8():
with Image.open("Tests/images/hopper_rle8.bmp") as im:
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)

# This test image has been manually hexedited
# to have rows with too much data
with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im:
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)

# Signal end of bitmap before the image is finished
with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp:
data = fp.read(1063) + b"\x01"
with Image.open(io.BytesIO(data)) as im:
with pytest.raises(ValueError):
im.load()


@pytest.mark.parametrize(
"file_name,length",
(
# EOF immediately after the header
("Tests/images/hopper_rle8.bmp", 1078),
# EOF during delta
("Tests/images/bmp/q/pal8rletrns.bmp", 3670),
# EOF when reading data in absolute mode
("Tests/images/bmp/g/pal8rle.bmp", 1064),
),
)
def test_rle8_eof(file_name, length):
with open(file_name, "rb") as fp:
data = fp.read(length)
with Image.open(io.BytesIO(data)) as im:
with pytest.raises(ValueError):
im.load()


def test_offset():
# This image has been hexedited
# to exclude the palette size from the pixel data offset
Expand Down
60 changes: 59 additions & 1 deletion src/PIL/BmpImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
#


import os

from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
from ._binary import i32le as i32
Expand Down Expand Up @@ -167,6 +169,7 @@ def _bitmap(self, header=0, offset=0):
raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})")

# ---------------- Process BMP with Bitfields compression (not palette)
decoder_name = "raw"
if file_info["compression"] == self.BITFIELDS:
SUPPORTED = {
32: [
Expand Down Expand Up @@ -208,6 +211,8 @@ def _bitmap(self, header=0, offset=0):
elif file_info["compression"] == self.RAW:
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
raw_mode, self.mode = "BGRA", "RGBA"
elif file_info["compression"] == self.RLE8:
decoder_name = "bmp_rle"
else:
raise OSError(f"Unsupported BMP compression ({file_info['compression']})")

Expand Down Expand Up @@ -247,7 +252,7 @@ def _bitmap(self, header=0, offset=0):
self.info["compression"] = file_info["compression"]
self.tile = [
(
"raw",
decoder_name,
(0, 0, file_info["width"], file_info["height"]),
offset or self.fp.tell(),
(
Expand All @@ -271,6 +276,57 @@ def _open(self):
self._bitmap(offset=offset)


class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True

def decode(self, buffer):
data = bytearray()
x = 0
while len(data) < self.state.xsize * self.state.ysize:
pixels = self.fd.read(1)
byte = self.fd.read(1)
if not pixels or not byte:
break
num_pixels = pixels[0]
if num_pixels:
# encoded mode
if x + num_pixels > self.state.xsize:
# Too much data for row
num_pixels = max(0, self.state.xsize - x)
data += byte * num_pixels
x += num_pixels
else:
if byte[0] == 0:
# end of line
while len(data) % self.state.xsize != 0:
data += b"\x00"
x = 0
elif byte[0] == 1:
# end of bitmap
break
elif byte[0] == 2:
# delta
bytes_read = self.fd.read(2)
if len(bytes_read) < 2:
break
right, up = self.fd.read(2)
data += b"\x00" * (right + up * self.state.xsize)
x = len(data) % self.state.xsize
else:
# absolute mode
bytes_read = self.fd.read(byte[0])
data += bytes_read
if len(bytes_read) < byte[0]:
break
x += byte[0]

# align to 16-bit word boundary
if self.fd.tell() % 2 != 0:
self.fd.seek(1, os.SEEK_CUR)
self.set_as_raw(bytes(data), ("P", 0, self.args[-1]))
return -1, 0


# =============================================================================
# Image plugin for the DIB format (BMP alias)
# =============================================================================
Expand Down Expand Up @@ -372,6 +428,8 @@ def _save(im, fp, filename, bitmap_header=True):

Image.register_mime(BmpImageFile.format, "image/bmp")

Image.register_decoder("bmp_rle", BmpRleDecoder)

Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
Image.register_save(DibImageFile.format, _dib_save)

Expand Down