/ adafruit_bitmapsaver.py
adafruit_bitmapsaver.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2019 Dave Astels for Adafruit Industries 4 # 5 # Permission is hereby granted, free of charge, to any person obtaining a copy 6 # of this software and associated documentation files (the "Software"), to deal 7 # in the Software without restriction, including without limitation the rights 8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 # copies of the Software, and to permit persons to whom the Software is 10 # furnished to do so, subject to the following conditions: 11 # 12 # The above copyright notice and this permission notice shall be included in 13 # all copies or substantial portions of the Software. 14 # 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 # THE SOFTWARE. 22 """ 23 `adafruit_bitmapsaver` 24 ================================================================================ 25 26 Save a displayio.Bitmap (and associated displayio.Palette) in a BMP file. 27 Make a screenshot (the contents of a displayio.Display) and save in a BMP file. 28 29 30 * Author(s): Dave Astels 31 32 Implementation Notes 33 -------------------- 34 35 **Hardware:** 36 37 38 **Software and Dependencies:** 39 40 * Adafruit CircuitPython firmware for the supported boards: 41 https://github.com/adafruit/circuitpython/releases 42 43 """ 44 45 # imports 46 47 import gc 48 import struct 49 import board 50 from displayio import Bitmap, Palette, Display 51 52 __version__ = "0.0.0-auto.0" 53 __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BitmapSaver.git" 54 55 56 def _write_bmp_header(output_file, filesize): 57 output_file.write(bytes("BM", "ascii")) 58 output_file.write(struct.pack("<I", filesize)) 59 output_file.write(b"\00\x00") 60 output_file.write(b"\00\x00") 61 output_file.write(struct.pack("<I", 54)) 62 63 64 def _write_dib_header(output_file, width, height): 65 output_file.write(struct.pack("<I", 40)) 66 output_file.write(struct.pack("<I", width)) 67 output_file.write(struct.pack("<I", height)) 68 output_file.write(struct.pack("<H", 1)) 69 output_file.write(struct.pack("<H", 24)) 70 for _ in range(24): 71 output_file.write(b"\x00") 72 73 74 def _bytes_per_row(source_width): 75 pixel_bytes = 3 * source_width 76 padding_bytes = (4 - (pixel_bytes % 4)) % 4 77 return pixel_bytes + padding_bytes 78 79 80 def _rotated_height_and_width(pixel_source): 81 # flip axis if the display is rotated 82 if isinstance(pixel_source, Display) and (pixel_source.rotation % 180 != 0): 83 return (pixel_source.height, pixel_source.width) 84 return (pixel_source.width, pixel_source.height) 85 86 87 def _rgb565_to_bgr_tuple(color): 88 blue = (color << 3) & 0x00F8 # extract each of the RGB tripple into it's own byte 89 green = (color >> 3) & 0x00FC 90 red = (color >> 8) & 0x00F8 91 return (blue, green, red) 92 93 94 # pylint:disable=too-many-locals 95 def _write_pixels(output_file, pixel_source, palette): 96 saving_bitmap = isinstance(pixel_source, Bitmap) 97 width, height = _rotated_height_and_width(pixel_source) 98 row_buffer = bytearray(_bytes_per_row(width)) 99 result_buffer = bytearray(2048) 100 for y in range(height, 0, -1): 101 buffer_index = 0 102 if saving_bitmap: 103 for x in range(width): 104 pixel = pixel_source[x, y - 1] 105 color = palette[pixel] 106 for _ in range(3): 107 row_buffer[buffer_index] = color & 0xFF 108 color >>= 8 109 buffer_index += 1 110 else: 111 data = pixel_source.fill_row(y - 1, result_buffer) 112 for i in range(width): 113 pixel565 = (data[i * 2] << 8) + data[i * 2 + 1] 114 for b in _rgb565_to_bgr_tuple(pixel565): 115 row_buffer[buffer_index] = b & 0xFF 116 buffer_index += 1 117 output_file.write(row_buffer) 118 gc.collect() 119 120 121 # pylint:enable=too-many-locals 122 123 124 def save_pixels(file_or_filename, pixel_source=None, palette=None): 125 """Save pixels to a 24 bit per pixel BMP file. 126 If pixel_source if a displayio.Bitmap, save it's pixels through palette. 127 If it's a displayio.Display, a palette isn't required. 128 129 :param file_or_filename: either the file to save to, or it's absolute name 130 :param pixel_source: the Bitmap or Display to save 131 :param palette: the Palette to use for looking up colors in the bitmap 132 """ 133 if not pixel_source: 134 if "DISPLAY" in dir(board): 135 pixel_source = board.DISPLAY 136 else: 137 raise ValueError("Second argument must be a Bitmap or Display") 138 if isinstance(pixel_source, Bitmap): 139 if not isinstance(palette, Palette): 140 raise ValueError("Third argument must be a Palette for a Bitmap save") 141 elif not isinstance(pixel_source, Display): 142 raise ValueError("Second argument must be a Bitmap or Display") 143 try: 144 if isinstance(file_or_filename, str): 145 output_file = open(file_or_filename, "wb") 146 else: 147 output_file = file_or_filename 148 149 width, height = _rotated_height_and_width(pixel_source) 150 filesize = 54 + height * _bytes_per_row(width) 151 _write_bmp_header(output_file, filesize) 152 _write_dib_header(output_file, width, height) 153 _write_pixels(output_file, pixel_source, palette) 154 except Exception as ex: 155 raise ex 156 else: 157 output_file.close()