/ CircuitPython_BLEThermalPrinter / seekablebitmap.py
seekablebitmap.py
1 # SPDX-FileCopyrightText: 2021 Jeff Epler for Adafruit Industries 2 # 3 # SPDX-License-Identifier: MIT 4 5 import struct 6 7 class SeekableBitmap: 8 """Allow random access to an uncompressed bitmap file on disk""" 9 def __init__( 10 self, 11 image_file, 12 width, 13 height, 14 bits_per_pixel, 15 *, 16 bytes_per_row=None, 17 data_start=None, 18 stride=None, 19 palette=None, 20 ): 21 """Construct a SeekableBitmap""" 22 self.image_file = image_file 23 self.width = width 24 self.height = height 25 self.bits_per_pixel = bits_per_pixel 26 self.bytes_per_row = ( 27 bytes_per_row if bytes_per_row else (bits_per_pixel * width + 7) // 8 28 ) 29 self.stride = stride if stride else self.bytes_per_row 30 self.palette = palette 31 self.data_start = data_start if data_start else image_file.tell() 32 33 def get_row(self, row): 34 self.image_file.seek(self.data_start + row * self.stride) 35 return self.image_file.read(self.bytes_per_row) 36 37 38 def _pnmopen(filename): 39 """ 40 Scan for netpbm format info, skip over comments, and read header data. 41 42 Return the format, header, and the opened file positioned at the start of 43 the bitmap data. 44 """ 45 # pylint: disable=too-many-branches 46 image_file = open(filename, "rb") 47 magic_number = image_file.read(2) 48 image_file.seek(2) 49 pnm_header = [] 50 next_value = bytearray() 51 while True: 52 # We have all we need at length 3 for formats P2, P3, P5, P6 53 if len(pnm_header) == 3: 54 return image_file, magic_number, pnm_header 55 56 if len(pnm_header) == 2 and magic_number in [b"P1", b"P4"]: 57 return image_file, magic_number, pnm_header 58 59 next_byte = image_file.read(1) 60 if next_byte == b"": 61 raise RuntimeError("Unsupported image format {}".format(magic_number)) 62 if next_byte == b"#": # comment found, seek until a newline or EOF is found 63 while image_file.read(1) not in [b"", b"\n"]: # EOF or NL 64 pass 65 elif not next_byte.isdigit(): # boundary found in header data 66 if next_value: 67 # pull values until space is found 68 pnm_header.append(int("".join(["%c" % char for char in next_value]))) 69 next_value = bytearray() # reset the byte array 70 else: 71 next_value += next_byte # push the digit into the byte array 72 73 74 def pnmopen(filename): 75 """ 76 Interpret netpbm format info and construct a SeekableBitmap 77 """ 78 image_file, magic_number, pnm_header = _pnmopen(filename) 79 if magic_number == b"P4": 80 return SeekableBitmap( 81 image_file, 82 pnm_header[0], 83 pnm_header[1], 84 1, 85 palette=b"\xff\xff\xff\x00\x00\x00\x00\x00", 86 ) 87 if magic_number == b"P5": 88 return SeekableBitmap( 89 image_file, pnm_header[0], pnm_header[1], pnm_header[2].bit_length() 90 ) 91 if magic_number == b"P6": 92 return SeekableBitmap( 93 image_file, pnm_header[0], pnm_header[1], 3 * pnm_header[2].bit_length() 94 ) 95 raise ValueError(f"Unknown or unsupported magic number {magic_number}") 96 97 98 def bmpopen(filename): 99 """ 100 Interpret bmp format info and construct a SeekableBitmap 101 """ 102 image_file = open(filename, "rb") 103 104 header = image_file.read(34) 105 106 data_start, header_size, width, height, _, bits_per_pixel, _ = struct.unpack( 107 "<10x4l2hl", header 108 ) 109 110 bits_per_pixel = bits_per_pixel if bits_per_pixel != 0 else 1 111 112 palette_start = header_size + 14 113 image_file.seek(palette_start) 114 palette = image_file.read(4 << bits_per_pixel) 115 116 stride = (bits_per_pixel * width + 31) // 32 * 4 117 if height < 0: 118 height = -height 119 else: 120 data_start = data_start + stride * (height - 1) 121 stride = -stride 122 123 return SeekableBitmap( 124 image_file, 125 width, 126 height, 127 bits_per_pixel, 128 data_start=data_start, 129 stride=stride, 130 palette=palette, 131 ) 132 133 134 def imageopen(filename): 135 """ 136 Open a bmp or pnm file as a seekable bitmap 137 """ 138 if filename.lower().endswith(".bmp"): 139 return bmpopen(filename) 140 return pnmopen(filename)