diff --git a/README.md b/README.md index a704972..2fcc789 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,14 @@ A simple python package for loading and saving BMP image. -It works under both CPython and MicroPython. Only uncompressed 24-bit color depth is supported. +It works under both CPython and MicroPython. BMP image of 1/2/4/8/24-bit color depth is supported. + +Loading supports compression method: +- 0(BI_RGB, no compression) +- 1(BI_RLE8, RLE 8-bit/pixel) +- 2(BI_RLE4, RLE 4-bit/pixel) + +Saving only supports compression method 0(BI_RGB, no compression). Please consider [![Paypal Donate](https://github.com/jacklinquan/images/blob/master/paypal_donate_button_200x80.png)](https://www.paypal.me/jacklinquan) to support me. @@ -11,32 +18,30 @@ Please consider [![Paypal Donate](https://github.com/jacklinquan/images/blob/mas `pip install microbmp` ## Usage -``` ->>> import microbmp ->>> # Create a 2(width) by 3(height) image. Pixel: img[x][y] = [r,g,b]. -... ->>> img=[[[255,0,0],[0,255,0],[0,0,255]],[[255,255,0],[255,0,255],[0,255,255]]] ->>> # Save the image. -... ->>> microbmp.save_bmp_file('test.bmp', img) ->>> # Load the image. -... ->>> new_img = microbmp.load_bmp_file('test.bmp') ->>> new_img -[[[255, 0, 0], [0, 255, 0], [0, 0, 255]], [[255, 255, 0], [255, 0, 255], [0, 255, 255]]] ->>> import io ->>> bytesio = io.BytesIO() ->>> # Write bmp into BytesIO. -... ->>> microbmp.write_bmp(bytesio, img) ->>> bytesio.flush() ->>> bytesio.tell() -78 ->>> bytesio.seek(0) -0 ->>> bytesio.read() -b'BMN\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x02\x00\x00\x00\x03' -b'\x00\x00\x00\x01\x00\x18\x00\x00\x00\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00' -b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\xff\xff\x00\x00' -b'\x00\x00\xff\x00\xff\x00\xff\x00\x00\x00\x00\xff\x00\xff\xff\x00\x00' +```Python +>>> from microbmp import MicroBMP +>>> img_24b = MicroBMP(2, 2, 24) # Create a 2(width) by 2(height) 24-bit image. +>>> img_24b.palette # 24-bit image has no palette. +>>> img_24b.pixels # img_24b.pixels[x][y] = [r, g, b] +[[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]] +>>> img_24b.pixels = [[[0,0,255], [255,0,0]], [[0,255,0], [255,255,255]]] +>>> img_24b.save('img_24b.bmp') +70 +>>> new_img_24b = MicroBMP().load('img_24b.bmp') +>>> new_img_24b.palette +>>> new_img_24b.pixels +[[[0, 0, 255], [255, 0, 0]], [[0, 255, 0], [255, 255, 255]]] +>>> img_1b = MicroBMP(3, 2, 1) # Create a 3(width) by 2(height) 1-bit image. +>>> img_1b.palette # img_1b.palette[index] = [r, g, b] +[[0, 0, 0], [255, 255, 255]] +>>> img_1b.pixels # img_1b.pixels[x][y] = index +[[0, 0], [0, 0], [0, 0]] +>>> img_1b.pixels = [[0, 0], [1, 1], [0, 1]] +>>> img_1b.save('img_1b.bmp') +70 +>>> new_img_1b = MicroBMP().load('img_1b.bmp') +>>> new_img_1b.palette +[[0, 0, 0], [255, 255, 255]] +>>> new_img_1b.pixels +[[0, 0], [1, 1], [0, 1]] ``` diff --git a/microbmp/__init__.py b/microbmp/__init__.py index 322511c..d5c7b5e 100644 --- a/microbmp/__init__.py +++ b/microbmp/__init__.py @@ -2,138 +2,384 @@ """A simple python package for loading and saving BMP image. It works under both CPython and MicroPython. -Only uncompressed 24-bit color depth is supported. +BMP image of 1/2/4/8/24-bit color depth is supported. +Loading supports compression method: + 0(BI_RGB, no compression) + 1(BI_RLE8, RLE 8-bit/pixel) + 2(BI_RLE4, RLE 4-bit/pixel) +Saving only supports compression method 0(BI_RGB, no compression). Author: Quan Lin License: MIT :Example: ->>> from microbmp import load_bmp_file, save_bmp_file ->>> # Create a 2(width) by 3(height) image. Pixel: img[x][y] = [r,g,b]. ->>> img=[[[255,0,0],[0,255,0],[0,0,255]],[[255,255,0],[255,0,255],[0,255,255]]] ->>> # Save the image. ->>> save_bmp_file('test.bmp', img) ->>> # Load the image. ->>> new_img = load_bmp_file('test.bmp') +>>> from microbmp import MicroBMP +>>> img_24b = MicroBMP(2, 2, 24) # Create a 2(width) by 2(height) 24-bit image. +>>> img_24b.palette # 24-bit image has no palette. +>>> img_24b.pixels # img_24b.pixels[x][y] = [r, g, b] +[[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]] +>>> img_24b.pixels = [[[0,0,255], [255,0,0]], [[0,255,0], [255,255,255]]] +>>> img_24b.save('img_24b.bmp') +70 +>>> new_img_24b = MicroBMP().load('img_24b.bmp') +>>> new_img_24b.palette +>>> new_img_24b.pixels +[[[0, 0, 255], [255, 0, 0]], [[0, 255, 0], [255, 255, 255]]] +>>> img_1b = MicroBMP(3, 2, 1) # Create a 3(width) by 2(height) 1-bit image. +>>> img_1b.palette # img_1b.palette[index] = [r, g, b] +[[0, 0, 0], [255, 255, 255]] +>>> img_1b.pixels # img_1b.pixels[x][y] = index +[[0, 0], [0, 0], [0, 0]] +>>> img_1b.pixels = [[0, 0], [1, 1], [0, 1]] +>>> img_1b.save('img_1b.bmp') +70 +>>> new_img_1b = MicroBMP().load('img_1b.bmp') +>>> new_img_1b.palette +[[0, 0, 0], [255, 255, 255]] +>>> new_img_1b.pixels +[[0, 0], [1, 1], [0, 1]] """ -# Project version -__version__ = '0.1.0' -__all__ = ['read_bmp', 'write_bmp', 'load_bmp_file', 'save_bmp_file'] +# Project Version +__version__ = '0.2.0' +__all__ = ['MicroBMP'] from struct import pack, unpack -def _get_padded_row_size(row_size): - return (row_size + 3)//4*4 - -def read_bmp(file): - """Read image from FileIO or BytesIO. +class MicroBMP(object): + """MicroBMP image class. - :param file: io file to read from. - :type file: FileIO or BytesIO. - :returns: a 3D array representing an image. - :rtype: list. + :param width: image width. + :type width: int. + :param height: image height. + :type height: int. + :param depth: image colour depth. + :type depth: int, valid values are 1/2/4/8/24. + :param palette: image palette if applicable. + :type palette: list of colours. """ - # BMP Header - data = file.read(14) - assert data[0:2] == b'BM', \ - 'Not a valid BMP file!' - file_size = unpack('> (8 - self.DIB_depth) + # One formula that suits all: 1/2/4/8-bit colour depth. + d = data[index // self.ppb] + return (d >> (8 - self.DIB_depth * ((index % self.ppb) + 1))) & mask - :param file_path: full file path. - :type file_path: str. - :param img_pixels: a 3D array representing an image. - :type img_pixels: list. - """ - with open(file_path, 'wb') as file: - write_bmp(file, img_pixels) + 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.pixels[x][y] = self._extract_from_bytes(data, i) + x += 1 + else: + b = bytes([data[1]]) + for i in range(data[0]): + self.pixels[x][y] = self._extract_from_bytes(b, i%self.ppb) + x += 1 + + def read_io(self, bf_io): + """Read image from BytesIO or FileIO. + + :param bf_io: io object to read from. + :type bf_io: BytesIO or FileIO. + :returns: self. + :rtype: MicroBMP. + """ + # 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 = [] + for i in range(self.DIB_num_in_plt): + data = bf_io.read(4) + colour = [data[2], data[1], data[0]] + self.palette.append(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 + + self.pixels = None + assert self.init(), \ + 'Failed to initialize the image!' + + # Pixels + 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) + for x in range(self.DIB_w): + if (self.DIB_depth <= 8): + self.pixels[x][y] = self._extract_from_bytes(data, x) + else: + v = x * 3 + # BMP colour is in BGR order. + self.pixels[x][y][2] = data[v] + self.pixels[x][y][1] = data[v+1] + self.pixels[x][y][0] = data[v+2] + else: + # BI_RLE8 or BI_RLE4 + self._decode_rle(bf_io) + + return self + + def write_io(self, bf_io, force_40B_DIB=False): + """Write image to BytesIO or FileIO. + + :param bf_io: io object to write to. + :type bf_io: BytesIO or FileIO. + :param force_40B_DIB: force the size of DIB to be 40 bytes or not. + :type force_40B_DIB: bool. + :returns: number of bytes written to the io. + :rtype: int. + """ + 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]&0xFF, + colour[1]&0xFF, + colour[0]&0xFF, + 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.pixels[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.pixels[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): + bf_io.write(bytes([ + self.pixels[x][y][2], + self.pixels[x][y][1], + self.pixels[x][y][0], + ])) + # 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): + """Load image from BMP file. + + :param file_path: full file path. + :type file_path: str. + :returns: self. + :rtype: MicroBMP. + """ + with open(file_path, 'rb') as file: + self.read_io(file) + return self + + def save(self, file_path, force_40B_DIB=False): + """Save image to BMP file. + + :param file_path: full file path. + :type file_path: str. + :param force_40B_DIB: force the size of DIB to be 40 bytes or not. + :type force_40B_DIB: bool. + :returns: number of bytes written to the file. + :rtype: int. + """ + with open(file_path, 'wb') as file: + num_of_bytes = self.write_io(file, force_40B_DIB) + return num_of_bytes diff --git a/setup.py b/setup.py index b455c84..a633a36 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="microbmp", - version="0.1.0", + version="0.2.0", description="A simple python package for loading and saving BMP image.", long_description="https://github.com/jacklinquan/microbmp", long_description_content_type="text/markdown",