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 RLE4 compression #6674

Merged
merged 10 commits into from Oct 24, 2022
Binary file added Tests/images/hopper_4bit.bmp
Binary file not shown.
Binary file added Tests/images/hopper_rle4.bmp
Binary file not shown.
8 changes: 8 additions & 0 deletions Tests/test_file_bmp.py
Expand Up @@ -176,6 +176,14 @@ def test_rle8():
im.load()


def test_rle4():
with Image.open("Tests/images/hopper_rle4.bmp") as im:
assert_image_similar_tofile(im, "Tests/images/hopper_4bit.bmp", 12)

with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im:
assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12)
Copy link
Member

Choose a reason for hiding this comment

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

Is there any significant difference between hopper_rle4.bmp and pal4rle.bmp?

Copy link
Member

Choose a reason for hiding this comment

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

I've removed hopper_rle4.bmp, since the other test image seems sufficient.



@pytest.mark.parametrize(
"file_name,length",
(
Expand Down
9 changes: 5 additions & 4 deletions docs/handbook/image-file-formats.rst
Expand Up @@ -45,9 +45,9 @@ BMP
^^^

Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``,
or ``RGB`` data. 16-colour images are read as ``P`` images. 4-bit run-length encoding
is not supported. Support for reading 8-bit run-length encoding was added in Pillow
9.1.0.
or ``RGB`` data. 16-colour images are read as ``P`` images.
Support for reading 8-bit run-length encoding was added in Pillow 9.1.0.
Support for reading 4-bit run-length encoding was added in Pillow 9.3.0.

Opening
~~~~~~~
Expand All @@ -56,7 +56,8 @@ The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:

**compression**
Set to ``bmp_rle`` if the file is run-length encoded.
Set to ``bmp_rle8`` if the file is a 256-color run-length encoded image.
Set to ``bmp_rle4`` if the file is a 16-color run-length encoded image.

DDS
^^^
Expand Down
72 changes: 69 additions & 3 deletions src/PIL/BmpImagePlugin.py
Expand Up @@ -212,7 +212,9 @@ def _bitmap(self, header=0, offset=0):
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"
decoder_name = "bmp_rle8"
elif file_info["compression"] == self.RLE4:
decoder_name = "bmp_rle4"
else:
raise OSError(f"Unsupported BMP compression ({file_info['compression']})")

Expand Down Expand Up @@ -276,7 +278,7 @@ def _open(self):
self._bitmap(offset=offset)


class BmpRleDecoder(ImageFile.PyDecoder):
class BmpRle8Decoder(ImageFile.PyDecoder):
_pulls_fd = True

def decode(self, buffer):
Expand Down Expand Up @@ -328,6 +330,69 @@ def decode(self, buffer):
return -1, 0


class BmpRle4Decoder(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)
first_pixel = o8(byte[0] >> 4)
second_pixel = o8(byte[0] & 0x0F)
for index in range(num_pixels):
if index % 2 == 0:
data += first_pixel
else:
data += second_pixel
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 (2 pixels per byte)
total_bytes_to_read = byte[0] // 2
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this "2" comment mean? If it means this value is always 2, that won't work, because this is already checked for on line 367.

Copy link
Member

Choose a reason for hiding this comment

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

It's not a comment. Commenting in Python is #. This is floor division.

Copy link
Contributor

Choose a reason for hiding this comment

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

ah, right. There's even a real comment right above that and I didn't notice...

bytes_read = self.fd.read(total_bytes_to_read)
for byte_read in bytes_read:
first_pixel = o8(byte_read >> 4)
data += first_pixel
second_pixel = o8(byte_read & 0x0F)
data += second_pixel
if len(bytes_read) < total_bytes_to_read:
break
x += byte[0]

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


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

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

Image.register_decoder("bmp_rle", BmpRleDecoder)
Image.register_decoder("bmp_rle8", BmpRle8Decoder)
Image.register_decoder("bmp_rle4", BmpRle4Decoder)

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