Skip to content

Commit

Permalink
Merge pull request #6102 from radarhere/bmp_rle8
Browse files Browse the repository at this point in the history
  • Loading branch information
hugovk committed Mar 23, 2022
2 parents bda004b + 039b7ec commit a921fcb
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 2 deletions.
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
Expand Up @@ -40,6 +40,7 @@ def test_questionable():
"rgb32fakealpha.bmp",
"rgb24largepal.bmp",
"pal8os2sp.bmp",
"pal8rletrns.bmp",
"rgb32bf-xbgr.bmp",
]
for f in get_files("q"):
Expand Down
43 changes: 42 additions & 1 deletion Tests/test_file_bmp.py
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
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

0 comments on commit a921fcb

Please sign in to comment.