ePaper display driven by a Raspberry Pi Pico
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

394 lines
14 KiB

  1. # -*- coding: utf-8 -*-
  2. """A small Python module for BMP image processing.
  3. - Author: Quan Lin
  4. - Adapted by: Dejvino
  5. - Adapted from: https://github.com/jacklinquan/micropython-microbmp
  6. - License: MIT
  7. """
  8. from struct import pack, unpack
  9. # Project Version
  10. __version__ = "0.1.0"
  11. __all__ = ["MicroBMP"]
  12. class MicroBMP(object):
  13. def __init__(self, width=None, height=None, depth=None, palette=None, header_callback=None, data_callback=None):
  14. # BMP Header
  15. self.BMP_id = b"BM"
  16. self.BMP_size = None
  17. self.BMP_reserved1 = b"\x00\x00"
  18. self.BMP_reserved2 = b"\x00\x00"
  19. self.BMP_offset = None
  20. # DIB Header
  21. self.DIB_len = 40
  22. self.DIB_w = width
  23. self.DIB_h = height
  24. self.DIB_planes_num = 1
  25. self.DIB_depth = depth
  26. self.DIB_comp = 0
  27. self.DIB_raw_size = None
  28. self.DIB_hres = 2835 # 72 DPI * 39.3701 inches/metre.
  29. self.DIB_vres = 2835
  30. self.DIB_num_in_plt = None
  31. self.DIB_extra = None
  32. self.palette = palette
  33. self.parray = None # Pixel array
  34. self.ppb = None # Number of pixels per byte for depth <= 8.
  35. self.pmask = None # Pixel Mask
  36. self.row_size = None
  37. self.padded_row_size = None
  38. self.header_callback = header_callback
  39. self.data_callback = data_callback
  40. self.initialised = False
  41. self._init()
  42. def __getitem__(self, key):
  43. assert self.initialised, "Image not initialised!"
  44. assert key[0] < self.DIB_w and key[1] < self.DIB_h, "Out of image boundary!"
  45. # Pixels are arranged in HLSB format with high bits being the leftmost
  46. pindex = key[1] * self.DIB_w + key[0] # Pixel index
  47. if self.DIB_depth <= 8:
  48. return self._extract_from_bytes(self.parray, pindex)
  49. else:
  50. pindex *= 3
  51. if (len(key) > 2) and (key[2] in (0, 1, 2)):
  52. return self.parray[pindex + key[2]]
  53. else:
  54. return (
  55. self.parray[pindex],
  56. self.parray[pindex + 1],
  57. self.parray[pindex + 2],
  58. )
  59. def __setitem__(self, key, value):
  60. assert self.initialised, "Image not initialised!"
  61. assert key[0] < self.DIB_w and key[1] < self.DIB_h, "Out of image boundary!"
  62. # Pixels are arranged in HLSB format with high bits being the leftmost
  63. pindex = key[1] * self.DIB_w + key[0] # Pixel index
  64. if self.DIB_depth <= 8:
  65. self._fill_in_bytes(self.parray, pindex, value)
  66. else:
  67. pindex *= 3
  68. if (len(key) > 2) and (key[2] in (0, 1, 2)):
  69. self.parray[pindex + key[2]] = value
  70. else:
  71. self.parray[pindex] = value[0]
  72. self.parray[pindex + 1] = value[1]
  73. self.parray[pindex + 2] = value[2]
  74. def __str__(self):
  75. if not self.initialised:
  76. return repr(self)
  77. return "BMP image, {}, {}-bit, {}x{} pixels, {} bytes".format(
  78. "indexed" if self.DIB_depth <= 8 else "RGB",
  79. self.DIB_depth,
  80. self.DIB_w,
  81. self.DIB_h,
  82. self.BMP_size,
  83. )
  84. def _init(self):
  85. if None in (self.DIB_w, self.DIB_h, self.DIB_depth):
  86. self.initialised = False
  87. return self.initialised
  88. assert self.BMP_id == b"BM", "BMP id ({}) must be b'BM'!".format(self.BMP_id)
  89. assert (
  90. len(self.BMP_reserved1) == 2 and len(self.BMP_reserved2) == 2
  91. ), "Length of BMP reserved fields ({}+{}) must be 2+2!".format(
  92. len(self.BMP_reserved1), len(self.BMP_reserved2)
  93. )
  94. assert self.DIB_planes_num == 1, "DIB planes number ({}) must be 1!".format(
  95. self.DIB_planes_num
  96. )
  97. assert self.DIB_depth in (
  98. 1,
  99. 2,
  100. 4,
  101. 8,
  102. 24,
  103. ), "Colour depth ({}) must be in (1, 2, 4, 8, 24)!".format(self.DIB_depth)
  104. assert (
  105. self.DIB_comp == 0
  106. or (self.DIB_depth == 8 and self.DIB_comp == 1)
  107. or (self.DIB_depth == 4 and self.DIB_comp == 2)
  108. ), "Colour depth + compression ({}+{}) must be X+0/8+1/4+2!".format(
  109. self.DIB_depth, self.DIB_comp
  110. )
  111. if self.DIB_depth <= 8:
  112. self.ppb = 8 // self.DIB_depth
  113. self.pmask = 0xFF >> (8 - self.DIB_depth)
  114. if self.palette is None:
  115. # Default palette is black and white or full size grey scale.
  116. self.DIB_num_in_plt = 2 ** self.DIB_depth
  117. self.palette = [None for i in range(self.DIB_num_in_plt)]
  118. for i in range(self.DIB_num_in_plt):
  119. # Assignment that suits all: 1/2/4/8-bit colour depth.
  120. s = 255 * i // (self.DIB_num_in_plt - 1)
  121. self.palette[i] = bytearray([s, s, s])
  122. else:
  123. self.DIB_num_in_plt = len(self.palette)
  124. else:
  125. self.ppb = None
  126. self.pmask = None
  127. self.DIB_num_in_plt = 0
  128. self.palette = None
  129. #if self.parray is None:
  130. # if self.DIB_depth <= 8:
  131. # div, mod = divmod(self.DIB_w * self.DIB_h, self.ppb)
  132. # self.parray = bytearray(div + (1 if mod else 0))
  133. # else:
  134. # self.parray = bytearray(self.DIB_w * self.DIB_h * 3)
  135. plt_size = self.DIB_num_in_plt * 4
  136. self.BMP_offset = 14 + self.DIB_len + plt_size
  137. self.row_size = self._size_from_width(self.DIB_w)
  138. self.padded_row_size = self._padded_size_from_size(self.row_size)
  139. if self.DIB_comp == 0:
  140. self.DIB_raw_size = self.padded_row_size * self.DIB_h
  141. self.BMP_size = self.BMP_offset + self.DIB_raw_size
  142. self.initialised = True
  143. return self.initialised
  144. def _size_from_width(self, width):
  145. return (width * self.DIB_depth + 7) // 8
  146. def _padded_size_from_size(self, size):
  147. return (size + 3) // 4 * 4
  148. def _extract_from_bytes(self, data, index):
  149. # One formula that suits all: 1/2/4/8-bit colour depth.
  150. byte_index, pos_in_byte = divmod(index, self.ppb)
  151. shift = 8 - self.DIB_depth * (pos_in_byte + 1)
  152. return (data[byte_index] >> shift) & self.pmask
  153. def _fill_in_bytes(self, data, index, value):
  154. # One formula that suits all: 1/2/4/8-bit colour depth.
  155. byte_index, pos_in_byte = divmod(index, self.ppb)
  156. shift = 8 - self.DIB_depth * (pos_in_byte + 1)
  157. value &= self.pmask
  158. data[byte_index] = (data[byte_index] & ~(self.pmask << shift)) + (
  159. value << shift
  160. )
  161. def _decode_rle(self, bf_io):
  162. # Only bottom-up bitmap can be compressed.
  163. x, y = 0, self.DIB_h - 1
  164. while True:
  165. data = bf_io.read(2)
  166. if data[0] == 0:
  167. if data[1] == 0:
  168. x, y = 0, y - 1
  169. elif data[1] == 1:
  170. return
  171. elif data[1] == 2:
  172. data = bf_io.read(2)
  173. x, y = x + data[0], y - data[1]
  174. else:
  175. num_of_pixels = data[1]
  176. num_to_read = (self._size_from_width(num_of_pixels) + 1) // 2 * 2
  177. data = bf_io.read(num_to_read)
  178. for i in range(num_of_pixels):
  179. self[x, y] = self._extract_from_bytes(data, i)
  180. x += 1
  181. else:
  182. b = bytes([data[1]])
  183. for i in range(data[0]):
  184. self[x, y] = self._extract_from_bytes(b, i % self.ppb)
  185. x += 1
  186. def read_io(self, bf_io):
  187. #print("BMP reading file")
  188. # BMP Header
  189. data = bf_io.read(14)
  190. self.BMP_id = data[0:2]
  191. self.BMP_size = unpack("<I", data[2:6])[0]
  192. self.BMP_reserved1 = data[6:8]
  193. self.BMP_reserved2 = data[8:10]
  194. self.BMP_offset = unpack("<I", data[10:14])[0]
  195. # DIB Header
  196. data = bf_io.read(4)
  197. self.DIB_len = unpack("<I", data[0:4])[0]
  198. data = bf_io.read(self.DIB_len - 4)
  199. (
  200. self.DIB_w,
  201. self.DIB_h,
  202. self.DIB_planes_num,
  203. self.DIB_depth,
  204. self.DIB_comp,
  205. self.DIB_raw_size,
  206. self.DIB_hres,
  207. self.DIB_vres,
  208. ) = unpack("<iiHHIIii", data[0:28])
  209. DIB_plt_num_info = unpack("<I", data[28:32])[0]
  210. DIB_plt_important_num_info = unpack("<I", data[32:36])[0]
  211. if self.DIB_len > 40:
  212. self.DIB_extra = data[36:]
  213. # Palette
  214. if self.DIB_depth <= 8:
  215. if DIB_plt_num_info == 0:
  216. self.DIB_num_in_plt = 2 ** self.DIB_depth
  217. else:
  218. self.DIB_num_in_plt = DIB_plt_num_info
  219. self.palette = [None for i in range(self.DIB_num_in_plt)]
  220. for i in range(self.DIB_num_in_plt):
  221. data = bf_io.read(4)
  222. colour = bytearray([data[2], data[1], data[0]])
  223. self.palette[i] = colour
  224. # In case self.DIB_h < 0 for top-down format.
  225. if self.DIB_h < 0:
  226. self.DIB_h = -self.DIB_h
  227. is_top_down = True
  228. else:
  229. is_top_down = False
  230. header = [
  231. self.DIB_w,
  232. self.DIB_h,
  233. self.DIB_planes_num,
  234. self.DIB_depth,
  235. self.DIB_comp,
  236. self.DIB_raw_size,
  237. self.DIB_hres,
  238. self.DIB_vres,
  239. self.palette,
  240. ]
  241. #print("BMP metadata: ", str(header))
  242. if self.header_callback is not None:
  243. self.header_callback(header)
  244. self.parray = None
  245. assert self._init(), "Failed to initialize the image!"
  246. # Pixels
  247. #print("BMP reading data")
  248. if self.DIB_comp == 0:
  249. # BI_RGB
  250. for h in range(self.DIB_h):
  251. y = h if is_top_down else self.DIB_h - h - 1
  252. data = bf_io.read(self.padded_row_size)
  253. #pixels_row = []
  254. for x in range(self.DIB_w):
  255. if self.DIB_depth <= 8:
  256. ##self[x, y] = self._extract_from_bytes(data, x)
  257. byte_index, pos_in_byte = divmod(x, self.ppb)
  258. shift = 8 - self.DIB_depth * (pos_in_byte + 1)
  259. pixel_data = (data[byte_index] >> shift) & self.pmask
  260. #pixel_data = self._extract_from_bytes(data, x)
  261. pixel = pixel_data if self.palette is None else self.palette[pixel_data]
  262. ##pixels_row.append(pixel)
  263. self.data_callback(x, y, pixel)
  264. else:
  265. v = x * 3
  266. # BMP colour is in BGR order.
  267. self[x, y] = (data[v + 2], data[v + 1], data[v])
  268. #self.data_callback(0, y, pixels_row)
  269. else:
  270. # BI_RLE8 or BI_RLE4
  271. self._decode_rle(bf_io)
  272. #print("BMP done")
  273. return self
  274. def write_io(self, bf_io, force_40B_DIB=False):
  275. if force_40B_DIB:
  276. self.DIB_len = 40
  277. self.DIB_extra = None
  278. # Only uncompressed image is supported to write.
  279. self.DIB_comp = 0
  280. assert self._init(), "Failed to initialize the image!"
  281. # BMP Header
  282. bf_io.write(self.BMP_id)
  283. bf_io.write(pack("<I", self.BMP_size))
  284. bf_io.write(self.BMP_reserved1)
  285. bf_io.write(self.BMP_reserved2)
  286. bf_io.write(pack("<I", self.BMP_offset))
  287. # DIB Header
  288. bf_io.write(
  289. pack(
  290. "<IiiHHIIiiII",
  291. self.DIB_len,
  292. self.DIB_w,
  293. self.DIB_h,
  294. self.DIB_planes_num,
  295. self.DIB_depth,
  296. self.DIB_comp,
  297. self.DIB_raw_size,
  298. self.DIB_hres,
  299. self.DIB_vres,
  300. self.DIB_num_in_plt,
  301. self.DIB_num_in_plt,
  302. )
  303. )
  304. if self.DIB_len > 40:
  305. bf_io.write(self.DIB_extra)
  306. # Palette
  307. if self.DIB_depth <= 8:
  308. for colour in self.palette:
  309. bf_io.write(bytes([colour[2], colour[1], colour[0], 0]))
  310. # Pixels
  311. for h in range(self.DIB_h):
  312. # BMP last row comes first.
  313. y = self.DIB_h - h - 1
  314. if self.DIB_depth <= 8:
  315. d = 0
  316. for x in range(self.DIB_w):
  317. self[x, y] %= self.DIB_num_in_plt
  318. # One formula that suits all: 1/2/4/8-bit colour depth.
  319. d = (d << (self.DIB_depth % 8)) + self[x, y]
  320. if x % self.ppb == self.ppb - 1:
  321. # Got a whole byte.
  322. bf_io.write(bytes([d]))
  323. d = 0
  324. if x % self.ppb != self.ppb - 1:
  325. # Last byte if width does not fit in whole bytes.
  326. d <<= (
  327. 8
  328. - self.DIB_depth
  329. - (x % self.ppb) * (2 ** (self.DIB_depth - 1))
  330. )
  331. bf_io.write(bytes([d]))
  332. d = 0
  333. else:
  334. for x in range(self.DIB_w):
  335. r, g, b = self[x, y]
  336. bf_io.write(bytes([b, g, r]))
  337. # Pad row to multiple of 4 bytes with 0x00.
  338. bf_io.write(b"\x00" * (self.padded_row_size - self.row_size))
  339. num_of_bytes = bf_io.tell()
  340. return num_of_bytes
  341. def load(self, file_path):
  342. with open(file_path, "rb") as file:
  343. self.read_io(file)
  344. return self
  345. def save(self, file_path, force_40B_DIB=False):
  346. with open(file_path, "wb") as file:
  347. num_of_bytes = self.write_io(file, force_40B_DIB)
  348. return num_of_bytes