Skip to content

Commit b4f1ab1

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 b4f1ab1

File tree

5 files changed

+142
-13
lines changed

5 files changed

+142
-13
lines changed

adafruit_imageload/bmp/__init__.py

Lines changed: 6 additions & 1 deletion
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,
68+
return indexed.load(file, width, height, data_start, colors, color_depth, compression, bitmap=bitmap,
6469
palette=palette)

adafruit_imageload/bmp/indexed.py

Lines changed: 136 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,16 @@
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, *, bitmap=None, palette=None):
3636
"""Loads indexed bitmap data into bitmap and palette objects.
3737
3838
:param file file: The open bmp file
3939
:param int width: Image width in pixels
4040
:param int height: Image height in pixels
4141
:param int data_start: Byte location where the data starts (after headers)
4242
:param int colors: Number of distinct colors in the image
43-
:param int color_depth: Number of bits used to store a value"""
43+
:param int color_depth: Number of bits used to store a value
44+
:param int compression: 0 - none, 1 - 8bit RLE, 2 - 4bit RLE"""
4445
# pylint: disable=too-many-arguments,too-many-locals
4546
if palette:
4647
palette = palette(colors)
@@ -70,7 +71,6 @@ def load(file, width, height, data_start, colors, color_depth, *, bitmap=None, p
7071
if line_size % 4 != 0:
7172
line_size += (4 - line_size % 4)
7273

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

93217
return bitmap, palette

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)