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 ._palette import Palette
26 __version__ = "0.0.0+auto.0"
27 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
31 # pylint: disable=too-many-instance-attributes
33 Loads values straight from disk. This minimizes memory use but can lead to much slower pixel
34 load times. These load times may result in frame tearing where only part of the image is
37 It's easiest to use on a board with a built in display such as the `Hallowing M0 Express
38 <https://www.adafruit.com/product/3900>`_.
40 .. code-block:: Python
47 board.DISPLAY.auto_brightness = False
48 board.DISPLAY.brightness = 0
49 splash = displayio.Group()
50 board.DISPLAY.show(splash)
52 odb = displayio.OnDiskBitmap(\'/sample.bmp\')
53 face = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)
55 # Wait for the image to load.
56 board.DISPLAY.refresh(target_frames_per_second=60)
58 # Fade up the backlight
60 board.DISPLAY.brightness = 0.01 * i
69 def __init__(self, file: Union[str, BinaryIO]) -> None:
70 # pylint: disable=too-many-locals, too-many-branches, too-many-statements
72 Create an OnDiskBitmap object with the given file.
74 :param file file: The name of the bitmap file. For backwards compatibility, a file opened
75 in binary mode may also be passed.
77 Older versions of CircuitPython required a file opened in binary mode. CircuitPython 7.0
78 modified OnDiskBitmap so that it takes a filename instead, and opens the file internally.
79 A future version of CircuitPython will remove the ability to pass in an opened file.
82 if isinstance(file, str):
83 file = open(file, "rb") # pylint: disable=consider-using-with
85 if not (file.readable() and file.seekable()):
86 raise TypeError("file must be a file opened in byte mode")
88 self._pixel_shader_base: Union[ColorConverter, Palette, None] = None
93 bmp_header = memoryview(file.read(138)).cast(
95 ) # cast as unsigned 16-bit int
97 if len(bmp_header.tobytes()) != 138 or bmp_header.tobytes()[0:2] != b"BM":
98 raise ValueError("Invalid BMP file")
100 self._data_offset = read_word(bmp_header, 5)
102 header_size = read_word(bmp_header, 7)
103 bits_per_pixel = bmp_header[14]
104 compression = read_word(bmp_header, 15)
105 number_of_colors = read_word(bmp_header, 23)
107 indexed = bits_per_pixel <= 8
108 self._bitfield_compressed = compression == 3
109 self._bits_per_pixel = bits_per_pixel
110 self._width = read_word(bmp_header, 9)
111 self._height = read_word(bmp_header, 11)
113 self._colorconverter = ColorConverter()
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)
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):
144 palette[i] = palette_data[i]
146 palette[0] = 0x000000
147 palette[1] = 0xFFFFFF
148 self._palette = palette
149 elif header_size not in (12, 40, 108, 124):
151 "Only Windows format, uncompressed BMP supported: "
152 f"given header size is {header_size}"
155 if bits_per_pixel == 8 and number_of_colors == 0:
157 "Only monochrome, indexed 4bpp or 8bpp, and 16bpp or greater BMPs supported: "
158 f"{bits_per_pixel} bpp given"
162 self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
164 pixels_per_byte = 8 // self._bits_per_pixel
165 if pixels_per_byte == 0:
166 self._stride = self._width * bytes_per_pixel
167 if self._stride % 4 != 0:
168 self._stride += 4 - self._stride % 4
170 bit_stride = self._width * self._bits_per_pixel
171 if bit_stride % 32 != 0:
172 bit_stride += 32 - bit_stride % 32
173 self._stride = bit_stride // 8
174 except IOError as error:
175 raise OSError from error
178 def width(self) -> int:
180 Width of the bitmap. (read only)
188 def height(self) -> int:
190 Height of the bitmap. (read only)
198 def pixel_shader(self) -> Union[ColorConverter, Palette]:
200 The image's pixel_shader. The type depends on the underlying `Bitmap`'s structure. The
201 pixel shader can be modified (e.g., to set the transparent pixel or, for paletted images,
202 to update the palette)
204 :type: Union[ColorConverter, Palette]
207 return self._pixel_shader_base
210 def _colorconverter(self) -> ColorConverter:
211 return self._pixel_shader_base
213 @_colorconverter.setter
214 def _colorconverter(self, colorconverter: ColorConverter) -> None:
215 self._pixel_shader_base = colorconverter
218 def _palette(self) -> Palette:
219 return self._pixel_shader_base
222 def _palette(self, palette: Palette) -> None:
223 self._pixel_shader_base = palette
225 def _get_pixel(self, x: int, y: int) -> int:
226 if not (0 <= x < self.width and 0 <= y < self.height):
230 self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
232 pixels_per_byte = 8 // self._bits_per_pixel
233 if pixels_per_byte == 0:
236 + (self.height - y - 1) * self._stride
237 + x * bytes_per_pixel
242 + (self.height - y - 1) * self._stride
243 + x // pixels_per_byte
246 self._file.seek(location)
248 pixel_data = memoryview(self._file.read(4)).cast(
250 ) # cast as unsigned 32-bit int
251 pixel_data = pixel_data[0] # We only need a single 32-bit uint
252 if bytes_per_pixel == 1:
253 offset = (x % pixels_per_byte) * self._bits_per_pixel
254 mask = (1 << self._bits_per_pixel) - 1
255 return (pixel_data >> ((8 - self._bits_per_pixel) - offset)) & mask
256 if bytes_per_pixel == 2:
257 if self._g_bitmask == 0x07E0: # 565
258 red = (pixel_data & self._r_bitmask) >> 11
259 green = (pixel_data & self._g_bitmask) >> 5
260 blue = pixel_data & self._b_bitmask
262 red = (pixel_data & self._r_bitmask) >> 10
263 green = (pixel_data & self._g_bitmask) >> 4
264 blue = pixel_data & self._b_bitmask
265 return red << 19 | green << 10 | blue << 3
266 if bytes_per_pixel == 4 and self._bitfield_compressed:
267 return pixel_data & 0x00FFFFFF
270 def _finish_refresh(self) -> None: