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
5 changes: 5 additions & 0 deletions Tests/test_file_bmp.py
Expand Up @@ -176,6 +176,11 @@ def test_rle8():
im.load()


def test_rle4():
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 1 if the file is a 256-color run-length encoded image.
Set to 2 if the file is a 16-color run-length encoded image.

DDS
^^^
Expand Down
41 changes: 31 additions & 10 deletions src/PIL/BmpImagePlugin.py
Expand Up @@ -211,7 +211,7 @@ 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:
elif file_info["compression"] in (self.RLE8, self.RLE4):
decoder_name = "bmp_rle"
else:
raise OSError(f"Unsupported BMP compression ({file_info['compression']})")
Expand Down Expand Up @@ -250,16 +250,18 @@ def _bitmap(self, header=0, offset=0):

# ---------------------------- Finally set the tile data for the plugin
self.info["compression"] = file_info["compression"]
args = [raw_mode]
if decoder_name == "bmp_rle":
args.append(file_info["compression"] == self.RLE4)
else:
args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
args.append(file_info["direction"])
self.tile = [
(
decoder_name,
(0, 0, file_info["width"], file_info["height"]),
offset or self.fp.tell(),
(
raw_mode,
((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3),
file_info["direction"],
),
tuple(args),
)
]

Expand All @@ -280,6 +282,7 @@ class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True

def decode(self, buffer):
rle4 = self.args[1]
data = bytearray()
x = 0
while len(data) < self.state.xsize * self.state.ysize:
Expand All @@ -293,7 +296,16 @@ def decode(self, buffer):
if x + num_pixels > self.state.xsize:
# Too much data for row
num_pixels = max(0, self.state.xsize - x)
data += byte * num_pixels
if rle4:
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
else:
data += byte * num_pixels
x += num_pixels
else:
if byte[0] == 0:
Expand All @@ -314,9 +326,18 @@ def decode(self, buffer):
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]:
if rle4:
# 2 pixels per byte
byte_count = byte[0] // 2
bytes_read = self.fd.read(byte_count)
for byte_read in bytes_read:
data += o8(byte_read >> 4)
data += o8(byte_read & 0x0F)
else:
byte_count = byte[0]
bytes_read = self.fd.read(byte_count)
data += bytes_read
if len(bytes_read) < byte_count:
break
x += byte[0]

Expand Down