Skip to content

Commit 5c9bc65

Browse files
authored
Merge pull request #6674 from npjg/main
Added support for reading BMP images with RLE4 compression
2 parents 509f494 + cf46156 commit 5c9bc65

File tree

3 files changed

+41
-14
lines changed

3 files changed

+41
-14
lines changed

Tests/test_file_bmp.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ def test_rle8():
176176
im.load()
177177

178178

179+
def test_rle4():
180+
with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im:
181+
assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12)
182+
183+
179184
@pytest.mark.parametrize(
180185
"file_name,length",
181186
(

docs/handbook/image-file-formats.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ BMP
4545
^^^
4646

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

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

5858
**compression**
59-
Set to ``bmp_rle`` if the file is run-length encoded.
59+
Set to 1 if the file is a 256-color run-length encoded image.
60+
Set to 2 if the file is a 16-color run-length encoded image.
6061

6162
DDS
6263
^^^

src/PIL/BmpImagePlugin.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def _bitmap(self, header=0, offset=0):
211211
elif file_info["compression"] == self.RAW:
212212
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
213213
raw_mode, self.mode = "BGRA", "RGBA"
214-
elif file_info["compression"] == self.RLE8:
214+
elif file_info["compression"] in (self.RLE8, self.RLE4):
215215
decoder_name = "bmp_rle"
216216
else:
217217
raise OSError(f"Unsupported BMP compression ({file_info['compression']})")
@@ -250,16 +250,18 @@ def _bitmap(self, header=0, offset=0):
250250

251251
# ---------------------------- Finally set the tile data for the plugin
252252
self.info["compression"] = file_info["compression"]
253+
args = [raw_mode]
254+
if decoder_name == "bmp_rle":
255+
args.append(file_info["compression"] == self.RLE4)
256+
else:
257+
args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
258+
args.append(file_info["direction"])
253259
self.tile = [
254260
(
255261
decoder_name,
256262
(0, 0, file_info["width"], file_info["height"]),
257263
offset or self.fp.tell(),
258-
(
259-
raw_mode,
260-
((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3),
261-
file_info["direction"],
262-
),
264+
tuple(args),
263265
)
264266
]
265267

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

282284
def decode(self, buffer):
285+
rle4 = self.args[1]
283286
data = bytearray()
284287
x = 0
285288
while len(data) < self.state.xsize * self.state.ysize:
@@ -293,7 +296,16 @@ def decode(self, buffer):
293296
if x + num_pixels > self.state.xsize:
294297
# Too much data for row
295298
num_pixels = max(0, self.state.xsize - x)
296-
data += byte * num_pixels
299+
if rle4:
300+
first_pixel = o8(byte[0] >> 4)
301+
second_pixel = o8(byte[0] & 0x0F)
302+
for index in range(num_pixels):
303+
if index % 2 == 0:
304+
data += first_pixel
305+
else:
306+
data += second_pixel
307+
else:
308+
data += byte * num_pixels
297309
x += num_pixels
298310
else:
299311
if byte[0] == 0:
@@ -314,9 +326,18 @@ def decode(self, buffer):
314326
x = len(data) % self.state.xsize
315327
else:
316328
# absolute mode
317-
bytes_read = self.fd.read(byte[0])
318-
data += bytes_read
319-
if len(bytes_read) < byte[0]:
329+
if rle4:
330+
# 2 pixels per byte
331+
byte_count = byte[0] // 2
332+
bytes_read = self.fd.read(byte_count)
333+
for byte_read in bytes_read:
334+
data += o8(byte_read >> 4)
335+
data += o8(byte_read & 0x0F)
336+
else:
337+
byte_count = byte[0]
338+
bytes_read = self.fd.read(byte_count)
339+
data += bytes_read
340+
if len(bytes_read) < byte_count:
320341
break
321342
x += byte[0]
322343

0 commit comments

Comments
 (0)