/ 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()