1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
 
   2 # SPDX-FileCopyrightText: 2021 James Carr
 
   4 # SPDX-License-Identifier: MIT
 
   7 `displayio.ondiskbitmap`
 
   8 ================================================================================
 
  12 **Software and Dependencies:**
 
  15   https://github.com/adafruit/Adafruit_Blinka/releases
 
  17 * Author(s): Melissa LeBlanc-Williams, James Carr
 
  21 from typing import Union, BinaryIO
 
  22 from ._helpers import read_word
 
  23 from ._colorconverter import ColorConverter
 
  24 from ._colorspace import Colorspace
 
  25 from ._palette import Palette
 
  27 __version__ = "0.0.0+auto.0"
 
  28 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
  32     # pylint: disable=too-many-instance-attributes
 
  34     Loads values straight from disk. This minimizes memory use but can lead to much slower pixel
 
  35     load times. These load times may result in frame tearing where only part of the image is
 
  38     .. code-block:: Python
 
  45         board.DISPLAY.auto_brightness = False
 
  46         board.DISPLAY.brightness = 0
 
  47         splash = displayio.Group()
 
  48         board.DISPLAY.root_group = splash
 
  50         odb = displayio.OnDiskBitmap(\'/sample.bmp\')
 
  51         face = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)
 
  53         # Wait for the image to load.
 
  54         board.DISPLAY.refresh(target_frames_per_second=60)
 
  56         # Fade up the backlight
 
  58           board.DISPLAY.brightness = 0.01 * i
 
  67     def __init__(self, file: Union[str, BinaryIO]) -> None:
 
  68         # pylint: disable=too-many-locals, too-many-branches, too-many-statements
 
  70         Create an OnDiskBitmap object with the given file.
 
  72         :param file file: The name of the bitmap file. For backwards compatibility, a file opened
 
  73             in binary mode may also be passed.
 
  75         Older versions of CircuitPython required a file opened in binary mode. CircuitPython 7.0
 
  76         modified OnDiskBitmap so that it takes a filename instead, and opens the file internally.
 
  77         A future version of CircuitPython will remove the ability to pass in an opened file.
 
  80         if isinstance(file, str):
 
  81             file = open(file, "rb")  # pylint: disable=consider-using-with
 
  83         if not (file.readable() and file.seekable()):
 
  84             raise TypeError("file must be a file opened in byte mode")
 
  86         self._pixel_shader_base: Union[ColorConverter, Palette, None] = None
 
  91             bmp_header = memoryview(file.read(138)).cast(
 
  93             )  # cast as unsigned 16-bit int
 
  95             if len(bmp_header.tobytes()) != 138 or bmp_header.tobytes()[0:2] != b"BM":
 
  96                 raise ValueError("Invalid BMP file")
 
  98             self._data_offset = read_word(bmp_header, 5)
 
 100             header_size = read_word(bmp_header, 7)
 
 101             bits_per_pixel = bmp_header[14]
 
 102             compression = read_word(bmp_header, 15)
 
 103             number_of_colors = read_word(bmp_header, 23)
 
 105             indexed = bits_per_pixel <= 8
 
 106             self._bitfield_compressed = compression == 3
 
 107             self._bits_per_pixel = bits_per_pixel
 
 108             self._width = read_word(bmp_header, 9)
 
 109             self._height = read_word(bmp_header, 11)
 
 111             self._pixel_shader_base = ColorConverter(
 
 112                 input_colorspace=Colorspace.RGB888, dither=False
 
 115             if bits_per_pixel == 16:
 
 116                 if header_size >= 56 or self._bitfield_compressed:
 
 117                     self._r_bitmask = read_word(bmp_header, 27)
 
 118                     self._g_bitmask = read_word(bmp_header, 29)
 
 119                     self._b_bitmask = read_word(bmp_header, 31)
 
 121                     # No compression or short header mean 5:5:5
 
 122                     self._r_bitmask = 0x7C00
 
 123                     self._g_bitmask = 0x03E0
 
 124                     self._b_bitmask = 0x001F
 
 126                 if number_of_colors == 0:
 
 127                     number_of_colors = 1 << bits_per_pixel
 
 129                 palette = Palette(number_of_colors, dither=False)
 
 131                 if number_of_colors > 1:
 
 132                     palette_size = number_of_colors * 4
 
 133                     palette_offset = 0xE + header_size
 
 135                     file.seek(palette_offset)
 
 137                     palette_data = memoryview(file.read(palette_size)).cast(
 
 139                     )  # cast as unsigned 32-bit int
 
 140                     if len(palette_data.tobytes()) != palette_size:
 
 141                         raise ValueError("Unable to read color palette data")
 
 143                     for i in range(number_of_colors):
 
 146                         )  # pylint: disable=protected-access
 
 148                     palette._set_color(0x000000, 0)  # pylint: disable=protected-access
 
 149                     palette._set_color(0xFFFFFF, 1)  # pylint: disable=protected-access
 
 150                 self._pixel_shader_base = palette
 
 151             elif header_size not in (12, 40, 108, 124):
 
 153                     "Only Windows format, uncompressed BMP supported: "
 
 154                     f"given header size is {header_size}"
 
 157             if bits_per_pixel == 8 and number_of_colors == 0:
 
 159                     "Only monochrome, indexed 4bpp or 8bpp, and 16bpp or greater BMPs supported: "
 
 160                     f"{bits_per_pixel} bpp given"
 
 164                 self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
 
 166             pixels_per_byte = 8 // self._bits_per_pixel
 
 167             if pixels_per_byte == 0:
 
 168                 self._stride = self._width * bytes_per_pixel
 
 169                 if self._stride % 4 != 0:
 
 170                     self._stride += 4 - self._stride % 4
 
 172                 bit_stride = self._width * self._bits_per_pixel
 
 173                 if bit_stride % 32 != 0:
 
 174                     bit_stride += 32 - bit_stride % 32
 
 175                 self._stride = bit_stride // 8
 
 176         except IOError as error:
 
 177             raise OSError from error
 
 180     def width(self) -> int:
 
 182         Width of the bitmap. (read only)
 
 190     def height(self) -> int:
 
 192         Height of the bitmap. (read only)
 
 200     def pixel_shader(self) -> Union[ColorConverter, Palette]:
 
 202         The image's pixel_shader. The type depends on the underlying `Bitmap`'s structure. The
 
 203         pixel shader can be modified (e.g., to set the transparent pixel or, for paletted images,
 
 204         to update the palette)
 
 206         :type: Union[ColorConverter, Palette]
 
 209         return self._pixel_shader_base
 
 211     def _get_pixel(self, x: int, y: int) -> int:
 
 212         if not (0 <= x < self.width and 0 <= y < self.height):
 
 216             self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
 
 218         pixels_per_byte = 8 // self._bits_per_pixel
 
 219         if pixels_per_byte == 0:
 
 222                 + (self.height - y - 1) * self._stride
 
 223                 + x * bytes_per_pixel
 
 228                 + (self.height - y - 1) * self._stride
 
 229                 + x // pixels_per_byte
 
 232         self._file.seek(location)
 
 234         pixel_data = self._file.read(bytes_per_pixel)
 
 235         if len(pixel_data) == bytes_per_pixel:
 
 236             if bytes_per_pixel == 1:
 
 237                 offset = (x % pixels_per_byte) * self._bits_per_pixel
 
 238                 mask = (1 << self._bits_per_pixel) - 1
 
 239                 return (pixel_data[0] >> ((8 - self._bits_per_pixel) - offset)) & mask
 
 240             if bytes_per_pixel == 2:
 
 241                 pixel_data = pixel_data[0] | pixel_data[1] << 8
 
 242                 if self._g_bitmask == 0x07E0:  # 565
 
 243                     red = (pixel_data & self._r_bitmask) >> 11
 
 244                     green = (pixel_data & self._g_bitmask) >> 5
 
 245                     blue = pixel_data & self._b_bitmask
 
 247                     red = (pixel_data & self._r_bitmask) >> 10
 
 248                     green = (pixel_data & self._g_bitmask) >> 4
 
 249                     blue = pixel_data & self._b_bitmask
 
 250                 return red << 19 | green << 10 | blue << 3
 
 251             if bytes_per_pixel == 4 and self._bitfield_compressed:
 
 252                 return pixel_data[0] | pixel_data[1] << 8 | pixel_data[2] << 16
 
 253             pixel = pixel_data[0] | pixel_data[1] << 8 | pixel_data[2] << 16
 
 254             if bytes_per_pixel == 4:
 
 255                 pixel |= pixel_data[3] << 24
 
 259     def _finish_refresh(self) -> None: