diff --git a/adafruit_imageload/bmp/__init__.py b/adafruit_imageload/bmp/__init__.py index 26dcca3..0c77f07 100644 --- a/adafruit_imageload/bmp/__init__.py +++ b/adafruit_imageload/bmp/__init__.py @@ -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) diff --git a/adafruit_imageload/bmp/indexed.py b/adafruit_imageload/bmp/indexed.py index 5242dde..e25c89b 100644 --- a/adafruit_imageload/bmp/indexed.py +++ b/adafruit_imageload/bmp/indexed.py @@ -32,7 +32,8 @@ __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 @@ -40,8 +41,9 @@ def load(file, width, height, data_start, colors, color_depth, *, bitmap=None, p :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) @@ -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 @@ -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 diff --git a/examples/images/4bit_rle.bmp b/examples/images/4bit_rle.bmp new file mode 100644 index 0000000..427d565 Binary files /dev/null and b/examples/images/4bit_rle.bmp differ diff --git a/examples/images/8bit_rle.bmp b/examples/images/8bit_rle.bmp new file mode 100644 index 0000000..0d5cc8b Binary files /dev/null and b/examples/images/8bit_rle.bmp differ diff --git a/examples/images/color_wheel_rle.bmp b/examples/images/color_wheel_rle.bmp new file mode 100644 index 0000000..656c3b6 Binary files /dev/null and b/examples/images/color_wheel_rle.bmp differ