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

Combined BMP RLE decoders #1

Merged
merged 2 commits into from Oct 24, 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
4 changes: 2 additions & 2 deletions docs/handbook/image-file-formats.rst
Expand Up @@ -56,8 +56,8 @@ The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:

**compression**
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.
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
113 changes: 34 additions & 79 deletions src/PIL/BmpImagePlugin.py
Expand Up @@ -211,10 +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_rle8"
elif file_info["compression"] == self.RLE4:
decoder_name = "bmp_rle4"
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 @@ -252,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 @@ -278,10 +278,11 @@ def _open(self):
self._bitmap(offset=offset)


class BmpRle8Decoder(ImageFile.PyDecoder):
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 @@ -295,67 +296,19 @@ 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
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
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:
# 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)
rawmode = "L" if self.mode == "L" else "P"
self.set_as_raw(bytes(data), (rawmode, 0, self.args[-1]))
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
data += byte * num_pixels
x += num_pixels
else:
# absolute mode
if byte[0] == 0:
# end of line
while len(data) % self.state.xsize != 0:
Expand All @@ -373,15 +326,18 @@ def decode(self, buffer):
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
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:
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
Comment on lines +329 to +339
Copy link

Choose a reason for hiding this comment

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

The first two lines of code in each branch here are the same (other than the comment), so they could be pulled out of the if..else.

Copy link
Author

Choose a reason for hiding this comment

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

The first line of the first branch is byte_count = byte[0] // 2 and the first line of the second branch is byte_count = byte[0]. As I noted elsewhere, // 2 isn't a comment, it's division.

if len(bytes_read) < byte_count:
break
x += byte[0]

Expand Down Expand Up @@ -498,8 +454,7 @@ def _save(im, fp, filename, bitmap_header=True):

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

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

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