Skip to content

Commit 03c47bf

Browse files
committed
Adds support for RLE compression to BMPs
Fixes #20 RLE compression is allowed on 4-bit and 8-bit BMP images. This commit adds handling for compression levels 1 and 2. It adds 2 tiny test files for the 4-bit and 8-bit depths, since the algorithm differs slightly between them. It also includes a version of the color_wheel.bmp encoded with 8-bit RLE. I cut the image in half horizontally so that it would load on the PyGamer, which couldn't allocate the full Bitmap (presumably due to heap fragmentation).
1 parent 8e07b50 commit 03c47bf

File tree

5 files changed

+159
-14
lines changed

5 files changed

+159
-14
lines changed

adafruit_imageload/bmp/__init__.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,19 @@ def load(file, *, bitmap=None, palette=None):
5151
height = int.from_bytes(file.read(4), 'little')
5252
file.seek(0x1c) # Number of bits per pixel
5353
color_depth = int.from_bytes(file.read(2), 'little')
54+
file.seek(0x1e) # Compression type
55+
compression = int.from_bytes(file.read(2), 'little')
5456
file.seek(0x2e) # Number of colors in the color palette
5557
colors = int.from_bytes(file.read(4), 'little')
5658

5759
if colors == 0 and color_depth >= 16:
5860
raise NotImplementedError("True color BMP unsupported")
5961

62+
if compression > 2:
63+
raise NotImplementedError("bitmask compression unsupported")
64+
6065
if colors == 0:
6166
colors = 2 ** color_depth
6267
from . import indexed
63-
return indexed.load(file, width, height, data_start, colors, color_depth, bitmap=bitmap,
64-
palette=palette)
68+
return indexed.load(file, width, height, data_start, colors, color_depth,
69+
compression, bitmap=bitmap, palette=palette)

adafruit_imageload/bmp/indexed.py

+152-12
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,18 @@
3232
__version__ = "0.0.0-auto.0"
3333
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ImageLoad.git"
3434

35-
def load(file, width, height, data_start, colors, color_depth, *, bitmap=None, palette=None):
35+
def load(file, width, height, data_start, colors, color_depth, compression, *,
36+
bitmap=None, palette=None):
3637
"""Loads indexed bitmap data into bitmap and palette objects.
3738
3839
:param file file: The open bmp file
3940
:param int width: Image width in pixels
4041
:param int height: Image height in pixels
4142
:param int data_start: Byte location where the data starts (after headers)
4243
:param int colors: Number of distinct colors in the image
43-
:param int color_depth: Number of bits used to store a value"""
44-
# pylint: disable=too-many-arguments,too-many-locals
44+
:param int color_depth: Number of bits used to store a value
45+
:param int compression: 0 - none, 1 - 8bit RLE, 2 - 4bit RLE"""
46+
# pylint: disable=too-many-arguments,too-many-locals,too-many-branches
4547
if palette:
4648
palette = palette(colors)
4749

@@ -70,7 +72,6 @@ def load(file, width, height, data_start, colors, color_depth, *, bitmap=None, p
7072
if line_size % 4 != 0:
7173
line_size += (4 - line_size % 4)
7274

73-
chunk = bytearray(line_size)
7475
mask = (1 << minimum_color_depth) - 1
7576
if height > 0:
7677
range1 = height - 1
@@ -80,14 +81,153 @@ def load(file, width, height, data_start, colors, color_depth, *, bitmap=None, p
8081
range1 = 0
8182
range2 = abs(height)
8283
range3 = 1
83-
for y in range(range1, range2, range3):
84-
file.readinto(chunk)
85-
pixels_per_byte = 8 // color_depth
86-
offset = y * width
8784

88-
for x in range(width):
89-
i = x // pixels_per_byte
90-
pixel = (chunk[i] >> (8 - color_depth*(x % pixels_per_byte + 1))) & mask
91-
bitmap[offset + x] = pixel
85+
if compression == 0:
86+
chunk = bytearray(line_size)
87+
for y in range(range1, range2, range3):
88+
file.readinto(chunk)
89+
pixels_per_byte = 8 // color_depth
90+
offset = y * width
91+
92+
for x in range(width):
93+
i = x // pixels_per_byte
94+
pixel = (chunk[i] >> (8 - color_depth*(x % pixels_per_byte + 1))) & mask
95+
bitmap[offset + x] = pixel
96+
elif compression in (1, 2):
97+
decode_rle(
98+
bitmap=bitmap,
99+
file=file,
100+
compression=compression,
101+
y_range=(range1, range2, range3),
102+
width=width)
92103

93104
return bitmap, palette
105+
106+
def decode_rle(bitmap, file, compression, y_range, width):
107+
"""Helper to decode RLE images"""
108+
# pylint: disable=too-many-locals,too-many-nested-blocks,too-many-branches
109+
110+
# RLE algorithm, either 8-bit (1) or 4-bit (2)
111+
#
112+
# Ref: http://www.fileformat.info/format/bmp/egff.htm
113+
114+
is_4bit = compression == 2
115+
116+
# This will store the 2-byte run commands, which are either an
117+
# amount to repeat and a value to repeat, or a 0x00 and command
118+
# marker.
119+
run_buf = bytearray(2)
120+
121+
# We need to be prepared to load up to 256 pixels of literal image
122+
# data. (0xFF is max literal length, but odd literal runs are padded
123+
# up to an even byte count, so we need space for 256 in the case of
124+
# 8-bit.) 4-bit images can get away with half that.
125+
literal_buf = bytearray(128 if is_4bit else 256)
126+
127+
# We iterate with numbers rather than a range because the "delta"
128+
# command can cause us to jump forward arbitrarily in the output
129+
# image.
130+
#
131+
# In theory RLE images are only stored in bottom-up scan line order,
132+
# but we support either.
133+
(range1, range2, range3) = y_range
134+
y = range1
135+
x = 0
136+
137+
while y * range3 < range2 * range3:
138+
offset = y * width + x
139+
140+
# We keep track of how much space is left in our row so that we
141+
# can avoid writing extra data outside of the Bitmap. While the
142+
# reference above seems to say that the "end run" command is
143+
# optional and that image data should wrap from one scan line to
144+
# the next, in practice (looking at the output of ImageMagick
145+
# and GIMP, and what Preview renders) the bitmap part of the
146+
# image can contain data that goes beyond the image’s stated
147+
# width that should just be ignored. For example, the 8bit RLE
148+
# file is 15px wide but has data for 16px.
149+
width_remaining = width - x
150+
151+
file.readinto(run_buf)
152+
153+
if run_buf[0] == 0:
154+
# A repeat length of "0" is a special command. The next byte
155+
# tells us what needs to happen.
156+
if run_buf[1] == 0:
157+
# end of the current scan line
158+
y = y + range3
159+
x = 0
160+
elif run_buf[1] == 1:
161+
# end of image
162+
break
163+
elif run_buf[1] == 2:
164+
# delta command jumps us ahead in the bitmap output by
165+
# the x, y amounts stored in the next 2 bytes.
166+
file.readinto(run_buf)
167+
168+
x = x + run_buf[0]
169+
y = y + run_buf[1] * range3
170+
else:
171+
# command values of 3 or more indicate that many pixels
172+
# of literal (uncompressed) image data. For 8-bit mode,
173+
# this is raw bytes, but 4-bit mode counts in nibbles.
174+
literal_length_px = run_buf[1]
175+
176+
# Inverting the value here to get round-up integer division
177+
if is_4bit:
178+
read_length_bytes = -(-literal_length_px // 2)
179+
else:
180+
read_length_bytes = literal_length_px
181+
182+
# If the run has an odd length then there’s a 1-byte padding
183+
# we need to consume but not write into the output
184+
if read_length_bytes % 2 == 1:
185+
read_length_bytes += 1
186+
187+
# We use memoryview to artificially limit the length of
188+
# literal_buf so that readinto only reads the amount
189+
# that we want.
190+
literal_buf_mem = memoryview(literal_buf)
191+
file.readinto(literal_buf_mem[0:read_length_bytes])
192+
193+
if is_4bit:
194+
for i in range(0, min(literal_length_px, width_remaining)):
195+
# Expanding the two nibbles of the 4-bit data
196+
# into two bytes for our output bitmap.
197+
if i % 2 == 0:
198+
bitmap[offset + i] = literal_buf[i // 2] >> 4
199+
else:
200+
bitmap[offset + i] = literal_buf[i // 2] & 0x0F
201+
else:
202+
# 8-bit values are just a raw copy (limited by
203+
# what’s left in the row so we don’t overflow out of
204+
# the buffer)
205+
for i in range(0, min(literal_length_px, width_remaining)):
206+
bitmap[offset + i] = literal_buf[i]
207+
208+
x = x + literal_length_px
209+
else:
210+
# first byte was not 0, which means it tells us how much to
211+
# repeat the next byte into the output
212+
run_length_px = run_buf[0]
213+
214+
if is_4bit:
215+
# In 4 bit mode, we repeat the *two* values that are
216+
# packed into the next byte. The repeat amount is based
217+
# on pixels, not bytes, though, so if we were to repeat
218+
# 0xab 3 times, the output pixel values would be: 0x0a
219+
# 0x0b 0x0a (notice how it ends at 0x0a) rather than
220+
# 0x0a 0x0b 0x0a 0x0b 0x0a 0x0b
221+
run_values = [
222+
run_buf[1] >> 4,
223+
run_buf[1] & 0x0F
224+
]
225+
for i in range(0, min(run_length_px, width_remaining)):
226+
bitmap[offset + i] = run_values[i % 2]
227+
else:
228+
run_value = run_buf[1]
229+
for i in range(0, min(run_length_px, width_remaining)):
230+
bitmap[offset + i] = run_value
231+
232+
233+
x = x + run_length_px

examples/images/4bit_rle.bmp

298 Bytes
Binary file not shown.

examples/images/8bit_rle.bmp

1.22 KB
Binary file not shown.

examples/images/color_wheel_rle.bmp

16.6 KB
Binary file not shown.

0 commit comments

Comments
 (0)