# -*- coding: utf-8 -*- """A small Python module for BMP image processing. - Author: Quan Lin - Adapted by: Dejvino - Adapted from: https://github.com/jacklinquan/micropython-microbmp - License: MIT """ from struct import pack, unpack # Project Version __version__ = "0.1.0" __all__ = ["MicroBMP"] class MicroBMP(object): def __init__(self, width=None, height=None, depth=None, palette=None, header_callback=None, data_callback=None): # BMP Header self.BMP_id = b"BM" self.BMP_size = None self.BMP_reserved1 = b"\x00\x00" self.BMP_reserved2 = b"\x00\x00" self.BMP_offset = None # DIB Header self.DIB_len = 40 self.DIB_w = width self.DIB_h = height self.DIB_planes_num = 1 self.DIB_depth = depth self.DIB_comp = 0 self.DIB_raw_size = None self.DIB_hres = 2835 # 72 DPI * 39.3701 inches/metre. self.DIB_vres = 2835 self.DIB_num_in_plt = None self.DIB_extra = None self.palette = palette self.parray = None # Pixel array self.ppb = None # Number of pixels per byte for depth <= 8. self.pmask = None # Pixel Mask self.row_size = None self.padded_row_size = None self.header_callback = header_callback self.data_callback = data_callback self.initialised = False self._init() def __getitem__(self, key): assert self.initialised, "Image not initialised!" assert key[0] < self.DIB_w and key[1] < self.DIB_h, "Out of image boundary!" # Pixels are arranged in HLSB format with high bits being the leftmost pindex = key[1] * self.DIB_w + key[0] # Pixel index if self.DIB_depth <= 8: return self._extract_from_bytes(self.parray, pindex) else: pindex *= 3 if (len(key) > 2) and (key[2] in (0, 1, 2)): return self.parray[pindex + key[2]] else: return ( self.parray[pindex], self.parray[pindex + 1], self.parray[pindex + 2], ) def __setitem__(self, key, value): assert self.initialised, "Image not initialised!" assert key[0] < self.DIB_w and key[1] < self.DIB_h, "Out of image boundary!" # Pixels are arranged in HLSB format with high bits being the leftmost pindex = key[1] * self.DIB_w + key[0] # Pixel index if self.DIB_depth <= 8: self._fill_in_bytes(self.parray, pindex, value) else: pindex *= 3 if (len(key) > 2) and (key[2] in (0, 1, 2)): self.parray[pindex + key[2]] = value else: self.parray[pindex] = value[0] self.parray[pindex + 1] = value[1] self.parray[pindex + 2] = value[2] def __str__(self): if not self.initialised: return repr(self) return "BMP image, {}, {}-bit, {}x{} pixels, {} bytes".format( "indexed" if self.DIB_depth <= 8 else "RGB", self.DIB_depth, self.DIB_w, self.DIB_h, self.BMP_size, ) def _init(self): if None in (self.DIB_w, self.DIB_h, self.DIB_depth): self.initialised = False return self.initialised assert self.BMP_id == b"BM", "BMP id ({}) must be b'BM'!".format(self.BMP_id) assert ( len(self.BMP_reserved1) == 2 and len(self.BMP_reserved2) == 2 ), "Length of BMP reserved fields ({}+{}) must be 2+2!".format( len(self.BMP_reserved1), len(self.BMP_reserved2) ) assert self.DIB_planes_num == 1, "DIB planes number ({}) must be 1!".format( self.DIB_planes_num ) assert self.DIB_depth in ( 1, 2, 4, 8, 24, ), "Colour depth ({}) must be in (1, 2, 4, 8, 24)!".format(self.DIB_depth) assert ( self.DIB_comp == 0 or (self.DIB_depth == 8 and self.DIB_comp == 1) or (self.DIB_depth == 4 and self.DIB_comp == 2) ), "Colour depth + compression ({}+{}) must be X+0/8+1/4+2!".format( self.DIB_depth, self.DIB_comp ) if self.DIB_depth <= 8: self.ppb = 8 // self.DIB_depth self.pmask = 0xFF >> (8 - self.DIB_depth) if self.palette is None: # Default palette is black and white or full size grey scale. self.DIB_num_in_plt = 2 ** self.DIB_depth self.palette = [None for i in range(self.DIB_num_in_plt)] for i in range(self.DIB_num_in_plt): # Assignment that suits all: 1/2/4/8-bit colour depth. s = 255 * i // (self.DIB_num_in_plt - 1) self.palette[i] = bytearray([s, s, s]) else: self.DIB_num_in_plt = len(self.palette) else: self.ppb = None self.pmask = None self.DIB_num_in_plt = 0 self.palette = None #if self.parray is None: # if self.DIB_depth <= 8: # div, mod = divmod(self.DIB_w * self.DIB_h, self.ppb) # self.parray = bytearray(div + (1 if mod else 0)) # else: # self.parray = bytearray(self.DIB_w * self.DIB_h * 3) plt_size = self.DIB_num_in_plt * 4 self.BMP_offset = 14 + self.DIB_len + plt_size self.row_size = self._size_from_width(self.DIB_w) self.padded_row_size = self._padded_size_from_size(self.row_size) if self.DIB_comp == 0: self.DIB_raw_size = self.padded_row_size * self.DIB_h self.BMP_size = self.BMP_offset + self.DIB_raw_size self.initialised = True return self.initialised def _size_from_width(self, width): return (width * self.DIB_depth + 7) // 8 def _padded_size_from_size(self, size): return (size + 3) // 4 * 4 def _extract_from_bytes(self, data, index): # One formula that suits all: 1/2/4/8-bit colour depth. byte_index, pos_in_byte = divmod(index, self.ppb) shift = 8 - self.DIB_depth * (pos_in_byte + 1) return (data[byte_index] >> shift) & self.pmask def _fill_in_bytes(self, data, index, value): # One formula that suits all: 1/2/4/8-bit colour depth. byte_index, pos_in_byte = divmod(index, self.ppb) shift = 8 - self.DIB_depth * (pos_in_byte + 1) value &= self.pmask data[byte_index] = (data[byte_index] & ~(self.pmask << shift)) + ( value << shift ) def _decode_rle(self, bf_io): # Only bottom-up bitmap can be compressed. x, y = 0, self.DIB_h - 1 while True: data = bf_io.read(2) if data[0] == 0: if data[1] == 0: x, y = 0, y - 1 elif data[1] == 1: return elif data[1] == 2: data = bf_io.read(2) x, y = x + data[0], y - data[1] else: num_of_pixels = data[1] num_to_read = (self._size_from_width(num_of_pixels) + 1) // 2 * 2 data = bf_io.read(num_to_read) for i in range(num_of_pixels): self[x, y] = self._extract_from_bytes(data, i) x += 1 else: b = bytes([data[1]]) for i in range(data[0]): self[x, y] = self._extract_from_bytes(b, i % self.ppb) x += 1 def read_io(self, bf_io): #print("BMP reading file") # BMP Header data = bf_io.read(14) self.BMP_id = data[0:2] self.BMP_size = unpack(" 40: self.DIB_extra = data[36:] # Palette if self.DIB_depth <= 8: if DIB_plt_num_info == 0: self.DIB_num_in_plt = 2 ** self.DIB_depth else: self.DIB_num_in_plt = DIB_plt_num_info self.palette = [None for i in range(self.DIB_num_in_plt)] for i in range(self.DIB_num_in_plt): data = bf_io.read(4) colour = bytearray([data[2], data[1], data[0]]) self.palette[i] = colour # In case self.DIB_h < 0 for top-down format. if self.DIB_h < 0: self.DIB_h = -self.DIB_h is_top_down = True else: is_top_down = False header = [ self.DIB_w, self.DIB_h, self.DIB_planes_num, self.DIB_depth, self.DIB_comp, self.DIB_raw_size, self.DIB_hres, self.DIB_vres, self.palette, ] #print("BMP metadata: ", str(header)) if self.header_callback is not None: self.header_callback(header) self.parray = None assert self._init(), "Failed to initialize the image!" # Pixels #print("BMP reading data") if self.DIB_comp == 0: # BI_RGB for h in range(self.DIB_h): y = h if is_top_down else self.DIB_h - h - 1 data = bf_io.read(self.padded_row_size) #pixels_row = [] for x in range(self.DIB_w): if self.DIB_depth <= 8: ##self[x, y] = self._extract_from_bytes(data, x) byte_index, pos_in_byte = divmod(x, self.ppb) shift = 8 - self.DIB_depth * (pos_in_byte + 1) pixel_data = (data[byte_index] >> shift) & self.pmask #pixel_data = self._extract_from_bytes(data, x) pixel = pixel_data if self.palette is None else self.palette[pixel_data] ##pixels_row.append(pixel) self.data_callback(x, y, pixel) else: v = x * 3 # BMP colour is in BGR order. self[x, y] = (data[v + 2], data[v + 1], data[v]) #self.data_callback(0, y, pixels_row) else: # BI_RLE8 or BI_RLE4 self._decode_rle(bf_io) #print("BMP done") return self def write_io(self, bf_io, force_40B_DIB=False): if force_40B_DIB: self.DIB_len = 40 self.DIB_extra = None # Only uncompressed image is supported to write. self.DIB_comp = 0 assert self._init(), "Failed to initialize the image!" # BMP Header bf_io.write(self.BMP_id) bf_io.write(pack(" 40: bf_io.write(self.DIB_extra) # Palette if self.DIB_depth <= 8: for colour in self.palette: bf_io.write(bytes([colour[2], colour[1], colour[0], 0])) # Pixels for h in range(self.DIB_h): # BMP last row comes first. y = self.DIB_h - h - 1 if self.DIB_depth <= 8: d = 0 for x in range(self.DIB_w): self[x, y] %= self.DIB_num_in_plt # One formula that suits all: 1/2/4/8-bit colour depth. d = (d << (self.DIB_depth % 8)) + self[x, y] if x % self.ppb == self.ppb - 1: # Got a whole byte. bf_io.write(bytes([d])) d = 0 if x % self.ppb != self.ppb - 1: # Last byte if width does not fit in whole bytes. d <<= ( 8 - self.DIB_depth - (x % self.ppb) * (2 ** (self.DIB_depth - 1)) ) bf_io.write(bytes([d])) d = 0 else: for x in range(self.DIB_w): r, g, b = self[x, y] bf_io.write(bytes([b, g, r])) # Pad row to multiple of 4 bytes with 0x00. bf_io.write(b"\x00" * (self.padded_row_size - self.row_size)) num_of_bytes = bf_io.tell() return num_of_bytes def load(self, file_path): with open(file_path, "rb") as file: self.read_io(file) return self def save(self, file_path, force_40B_DIB=False): with open(file_path, "wb") as file: num_of_bytes = self.write_io(file, force_40B_DIB) return num_of_bytes