diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index f6860a9a43d..5f6d523558a 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -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) + + @pytest.mark.parametrize( "file_name,length", ( diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index dc629666c30..1e79db68bcb 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -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 ~~~~~~~ @@ -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 ^^^ diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 1041ab763d2..bdf51aa5cf1 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -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']})") @@ -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), ) ] @@ -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: @@ -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: @@ -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]