diff --git a/cricket.bmp b/cricket.bmp new file mode 100644 index 0000000..7a504bd Binary files /dev/null and b/cricket.bmp differ diff --git a/seekablebitmap.py b/seekablebitmap.py new file mode 100644 index 0000000..272b0ce --- /dev/null +++ b/seekablebitmap.py @@ -0,0 +1,140 @@ +# SPDX-FileCopyrightText: 2021 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +import struct + +class SeekableBitmap: + """Allow random access to an uncompressed bitmap file on disk""" + def __init__( + self, + image_file, + width, + height, + bits_per_pixel, + *, + bytes_per_row=None, + data_start=None, + stride=None, + palette=None, + ): + """Construct a SeekableBitmap""" + self.image_file = image_file + self.width = width + self.height = height + self.bits_per_pixel = bits_per_pixel + self.bytes_per_row = ( + bytes_per_row if bytes_per_row else (bits_per_pixel * width + 7) // 8 + ) + self.stride = stride if stride else self.bytes_per_row + self.palette = palette + self.data_start = data_start if data_start else image_file.tell() + + def get_row(self, row): + self.image_file.seek(self.data_start + row * self.stride) + return self.image_file.read(self.bytes_per_row) + + +def _pnmopen(filename): + """ + Scan for netpbm format info, skip over comments, and read header data. + + Return the format, header, and the opened file positioned at the start of + the bitmap data. + """ + # pylint: disable=too-many-branches + image_file = open(filename, "rb") + magic_number = image_file.read(2) + image_file.seek(2) + pnm_header = [] + next_value = bytearray() + while True: + # We have all we need at length 3 for formats P2, P3, P5, P6 + if len(pnm_header) == 3: + return image_file, magic_number, pnm_header + + if len(pnm_header) == 2 and magic_number in [b"P1", b"P4"]: + return image_file, magic_number, pnm_header + + next_byte = image_file.read(1) + if next_byte == b"": + raise RuntimeError("Unsupported image format {}".format(magic_number)) + if next_byte == b"#": # comment found, seek until a newline or EOF is found + while image_file.read(1) not in [b"", b"\n"]: # EOF or NL + pass + elif not next_byte.isdigit(): # boundary found in header data + if next_value: + # pull values until space is found + pnm_header.append(int("".join(["%c" % char for char in next_value]))) + next_value = bytearray() # reset the byte array + else: + next_value += next_byte # push the digit into the byte array + + +def pnmopen(filename): + """ + Interpret netpbm format info and construct a SeekableBitmap + """ + image_file, magic_number, pnm_header = _pnmopen(filename) + if magic_number == b"P4": + return SeekableBitmap( + image_file, + pnm_header[0], + pnm_header[1], + 1, + palette=b"\xff\xff\xff\x00\x00\x00\x00\x00", + ) + if magic_number == b"P5": + return SeekableBitmap( + image_file, pnm_header[0], pnm_header[1], pnm_header[2].bit_length() + ) + if magic_number == b"P6": + return SeekableBitmap( + image_file, pnm_header[0], pnm_header[1], 3 * pnm_header[2].bit_length() + ) + raise ValueError(f"Unknown or unsupported magic number {magic_number}") + + +def bmpopen(filename): + """ + Interpret bmp format info and construct a SeekableBitmap + """ + image_file = open(filename, "rb") + + header = image_file.read(34) + + data_start, header_size, width, height, _, bits_per_pixel, _ = struct.unpack( + "<10x4l2hl", header + ) + + bits_per_pixel = bits_per_pixel if bits_per_pixel != 0 else 1 + + palette_start = header_size + 14 + image_file.seek(palette_start) + palette = image_file.read(4 << bits_per_pixel) + + stride = (bits_per_pixel * width + 31) // 32 * 4 + if height < 0: + height = -height + else: + data_start = data_start + stride * (height - 1) + stride = -stride + + return SeekableBitmap( + image_file, + width, + height, + bits_per_pixel, + data_start=data_start, + stride=stride, + palette=palette, + ) + + +def imageopen(filename): + """ + Open a bmp or pnm file as a seekable bitmap + """ + if filename.lower().endswith(".bmp"): + return bmpopen(filename) + return pnmopen(filename)