Skip to content

Adds support for RLE compression to BMPs #21

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

Merged
merged 1 commit into from
Aug 22, 2019
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions adafruit_imageload/bmp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,19 @@ def load(file, *, bitmap=None, palette=None):
height = int.from_bytes(file.read(4), 'little')
file.seek(0x1c) # Number of bits per pixel
color_depth = int.from_bytes(file.read(2), 'little')
file.seek(0x1e) # Compression type
compression = int.from_bytes(file.read(2), 'little')
file.seek(0x2e) # Number of colors in the color palette
colors = int.from_bytes(file.read(4), 'little')

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

if compression > 2:
raise NotImplementedError("bitmask compression unsupported")

if colors == 0:
colors = 2 ** color_depth
from . import indexed
return indexed.load(file, width, height, data_start, colors, color_depth, bitmap=bitmap,
palette=palette)
return indexed.load(file, width, height, data_start, colors, color_depth,
compression, bitmap=bitmap, palette=palette)
164 changes: 152 additions & 12 deletions adafruit_imageload/bmp/indexed.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,18 @@
__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ImageLoad.git"

def load(file, width, height, data_start, colors, color_depth, *, bitmap=None, palette=None):
def load(file, width, height, data_start, colors, color_depth, compression, *,
bitmap=None, palette=None):
"""Loads indexed bitmap data into bitmap and palette objects.

:param file file: The open bmp file
:param int width: Image width in pixels
:param int height: Image height in pixels
:param int data_start: Byte location where the data starts (after headers)
:param int colors: Number of distinct colors in the image
:param int color_depth: Number of bits used to store a value"""
# pylint: disable=too-many-arguments,too-many-locals
:param int color_depth: Number of bits used to store a value
:param int compression: 0 - none, 1 - 8bit RLE, 2 - 4bit RLE"""
# pylint: disable=too-many-arguments,too-many-locals,too-many-branches
if palette:
palette = palette(colors)

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

chunk = bytearray(line_size)
mask = (1 << minimum_color_depth) - 1
if height > 0:
range1 = height - 1
Expand All @@ -80,14 +81,153 @@ def load(file, width, height, data_start, colors, color_depth, *, bitmap=None, p
range1 = 0
range2 = abs(height)
range3 = 1
for y in range(range1, range2, range3):
file.readinto(chunk)
pixels_per_byte = 8 // color_depth
offset = y * width

for x in range(width):
i = x // pixels_per_byte
pixel = (chunk[i] >> (8 - color_depth*(x % pixels_per_byte + 1))) & mask
bitmap[offset + x] = pixel
if compression == 0:
chunk = bytearray(line_size)
for y in range(range1, range2, range3):
file.readinto(chunk)
pixels_per_byte = 8 // color_depth
offset = y * width

for x in range(width):
i = x // pixels_per_byte
pixel = (chunk[i] >> (8 - color_depth*(x % pixels_per_byte + 1))) & mask
bitmap[offset + x] = pixel
elif compression in (1, 2):
decode_rle(
bitmap=bitmap,
file=file,
compression=compression,
y_range=(range1, range2, range3),
width=width)

return bitmap, palette

def decode_rle(bitmap, file, compression, y_range, width):
"""Helper to decode RLE images"""
# pylint: disable=too-many-locals,too-many-nested-blocks,too-many-branches

# RLE algorithm, either 8-bit (1) or 4-bit (2)
#
# Ref: http://www.fileformat.info/format/bmp/egff.htm

is_4bit = compression == 2

# This will store the 2-byte run commands, which are either an
# amount to repeat and a value to repeat, or a 0x00 and command
# marker.
run_buf = bytearray(2)

# We need to be prepared to load up to 256 pixels of literal image
# data. (0xFF is max literal length, but odd literal runs are padded
# up to an even byte count, so we need space for 256 in the case of
# 8-bit.) 4-bit images can get away with half that.
literal_buf = bytearray(128 if is_4bit else 256)

# We iterate with numbers rather than a range because the "delta"
# command can cause us to jump forward arbitrarily in the output
# image.
#
# In theory RLE images are only stored in bottom-up scan line order,
# but we support either.
(range1, range2, range3) = y_range
y = range1
x = 0

while y * range3 < range2 * range3:
offset = y * width + x

# We keep track of how much space is left in our row so that we
# can avoid writing extra data outside of the Bitmap. While the
# reference above seems to say that the "end run" command is
# optional and that image data should wrap from one scan line to
# the next, in practice (looking at the output of ImageMagick
# and GIMP, and what Preview renders) the bitmap part of the
# image can contain data that goes beyond the image’s stated
# width that should just be ignored. For example, the 8bit RLE
# file is 15px wide but has data for 16px.
width_remaining = width - x

file.readinto(run_buf)

if run_buf[0] == 0:
# A repeat length of "0" is a special command. The next byte
# tells us what needs to happen.
if run_buf[1] == 0:
# end of the current scan line
y = y + range3
x = 0
elif run_buf[1] == 1:
# end of image
break
elif run_buf[1] == 2:
# delta command jumps us ahead in the bitmap output by
# the x, y amounts stored in the next 2 bytes.
file.readinto(run_buf)

x = x + run_buf[0]
y = y + run_buf[1] * range3
else:
# command values of 3 or more indicate that many pixels
# of literal (uncompressed) image data. For 8-bit mode,
# this is raw bytes, but 4-bit mode counts in nibbles.
literal_length_px = run_buf[1]

# Inverting the value here to get round-up integer division
if is_4bit:
read_length_bytes = -(-literal_length_px // 2)
else:
read_length_bytes = literal_length_px

# If the run has an odd length then there’s a 1-byte padding
# we need to consume but not write into the output
if read_length_bytes % 2 == 1:
read_length_bytes += 1

# We use memoryview to artificially limit the length of
# literal_buf so that readinto only reads the amount
# that we want.
literal_buf_mem = memoryview(literal_buf)
file.readinto(literal_buf_mem[0:read_length_bytes])

if is_4bit:
for i in range(0, min(literal_length_px, width_remaining)):
# Expanding the two nibbles of the 4-bit data
# into two bytes for our output bitmap.
if i % 2 == 0:
bitmap[offset + i] = literal_buf[i // 2] >> 4
else:
bitmap[offset + i] = literal_buf[i // 2] & 0x0F
else:
# 8-bit values are just a raw copy (limited by
# what’s left in the row so we don’t overflow out of
# the buffer)
for i in range(0, min(literal_length_px, width_remaining)):
bitmap[offset + i] = literal_buf[i]

x = x + literal_length_px
else:
# first byte was not 0, which means it tells us how much to
# repeat the next byte into the output
run_length_px = run_buf[0]

if is_4bit:
# In 4 bit mode, we repeat the *two* values that are
# packed into the next byte. The repeat amount is based
# on pixels, not bytes, though, so if we were to repeat
# 0xab 3 times, the output pixel values would be: 0x0a
# 0x0b 0x0a (notice how it ends at 0x0a) rather than
# 0x0a 0x0b 0x0a 0x0b 0x0a 0x0b
run_values = [
run_buf[1] >> 4,
run_buf[1] & 0x0F
]
for i in range(0, min(run_length_px, width_remaining)):
bitmap[offset + i] = run_values[i % 2]
else:
run_value = run_buf[1]
for i in range(0, min(run_length_px, width_remaining)):
bitmap[offset + i] = run_value


x = x + run_length_px
Binary file added examples/images/4bit_rle.bmp
Binary file not shown.
Binary file added examples/images/8bit_rle.bmp
Binary file not shown.
Binary file added examples/images/color_wheel_rle.bmp
Binary file not shown.