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 It's easiest to use on a board with a built in display such as the `Hallowing M0 Express
39 <https://www.adafruit.com/product/3900>`_.
41 .. code-block:: Python
48 board.DISPLAY.auto_brightness = False
49 board.DISPLAY.brightness = 0
50 splash = displayio.Group()
51 board.DISPLAY.show(splash)
53 odb = displayio.OnDiskBitmap(\'/sample.bmp\')
54 face = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)
56 # Wait for the image to load.
57 board.DISPLAY.refresh(target_frames_per_second=60)
59 # Fade up the backlight
61 board.DISPLAY.brightness = 0.01 * i
70 def __init__(self, file: Union[str, BinaryIO]) -> None:
71 # pylint: disable=too-many-locals, too-many-branches, too-many-statements
73 Create an OnDiskBitmap object with the given file.
75 :param file file: The name of the bitmap file. For backwards compatibility, a file opened
76 in binary mode may also be passed.
78 Older versions of CircuitPython required a file opened in binary mode. CircuitPython 7.0
79 modified OnDiskBitmap so that it takes a filename instead, and opens the file internally.
80 A future version of CircuitPython will remove the ability to pass in an opened file.
83 if isinstance(file, str):
84 file = open(file, "rb") # pylint: disable=consider-using-with
86 if not (file.readable() and file.seekable()):
87 raise TypeError("file must be a file opened in byte mode")
89 self._pixel_shader_base: Union[ColorConverter, Palette, None] = None
94 bmp_header = memoryview(file.read(138)).cast(
96 ) # cast as unsigned 16-bit int
98 if len(bmp_header.tobytes()) != 138 or bmp_header.tobytes()[0:2] != b"BM":
99 raise ValueError("Invalid BMP file")
101 self._data_offset = read_word(bmp_header, 5)
103 header_size = read_word(bmp_header, 7)
104 bits_per_pixel = bmp_header[14]
105 compression = read_word(bmp_header, 15)
106 number_of_colors = read_word(bmp_header, 23)
108 indexed = bits_per_pixel <= 8
109 self._bitfield_compressed = compression == 3
110 self._bits_per_pixel = bits_per_pixel
111 self._width = read_word(bmp_header, 9)
112 self._height = read_word(bmp_header, 11)
114 self._pixel_shader_base = ColorConverter(
115 input_colorspace=Colorspace.RGB888, dither=False
118 if bits_per_pixel == 16:
119 if header_size >= 56 or self._bitfield_compressed:
120 self._r_bitmask = read_word(bmp_header, 27)
121 self._g_bitmask = read_word(bmp_header, 29)
122 self._b_bitmask = read_word(bmp_header, 31)
124 # No compression or short header mean 5:5:5
125 self._r_bitmask = 0x7C00
126 self._g_bitmask = 0x03E0
127 self._b_bitmask = 0x001F
129 if number_of_colors == 0:
130 number_of_colors = 1 << bits_per_pixel
132 palette = Palette(number_of_colors, dither=False)
134 if number_of_colors > 1:
135 palette_size = number_of_colors * 4
136 palette_offset = 0xE + header_size
138 file.seek(palette_offset)
140 palette_data = memoryview(file.read(palette_size)).cast(
142 ) # cast as unsigned 32-bit int
143 if len(palette_data.tobytes()) != palette_size:
144 raise ValueError("Unable to read color palette data")
146 for i in range(number_of_colors):
149 ) # pylint: disable=protected-access
151 palette._set_color(0x000000, 0) # pylint: disable=protected-access
152 palette._set_color(0xFFFFFF, 1) # pylint: disable=protected-access
153 self._pixel_shader_base = palette
154 elif header_size not in (12, 40, 108, 124):
156 "Only Windows format, uncompressed BMP supported: "
157 f"given header size is {header_size}"
160 if bits_per_pixel == 8 and number_of_colors == 0:
162 "Only monochrome, indexed 4bpp or 8bpp, and 16bpp or greater BMPs supported: "
163 f"{bits_per_pixel} bpp given"
167 self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
169 pixels_per_byte = 8 // self._bits_per_pixel
170 if pixels_per_byte == 0:
171 self._stride = self._width * bytes_per_pixel
172 if self._stride % 4 != 0:
173 self._stride += 4 - self._stride % 4
175 bit_stride = self._width * self._bits_per_pixel
176 if bit_stride % 32 != 0:
177 bit_stride += 32 - bit_stride % 32
178 self._stride = bit_stride // 8
179 except IOError as error:
180 raise OSError from error
183 def width(self) -> int:
185 Width of the bitmap. (read only)
193 def height(self) -> int:
195 Height of the bitmap. (read only)
203 def pixel_shader(self) -> Union[ColorConverter, Palette]:
205 The image's pixel_shader. The type depends on the underlying `Bitmap`'s structure. The
206 pixel shader can be modified (e.g., to set the transparent pixel or, for paletted images,
207 to update the palette)
209 :type: Union[ColorConverter, Palette]
212 return self._pixel_shader_base
214 def _get_pixel(self, x: int, y: int) -> int:
215 if not (0 <= x < self.width and 0 <= y < self.height):
219 self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
221 pixels_per_byte = 8 // self._bits_per_pixel
222 if pixels_per_byte == 0:
225 + (self.height - y - 1) * self._stride
226 + x * bytes_per_pixel
231 + (self.height - y - 1) * self._stride
232 + x // pixels_per_byte
235 self._file.seek(location)
237 pixel_data = self._file.read(bytes_per_pixel)
238 if len(pixel_data) == bytes_per_pixel:
239 if bytes_per_pixel == 1:
240 offset = (x % pixels_per_byte) * self._bits_per_pixel
241 mask = (1 << self._bits_per_pixel) - 1
242 return (pixel_data[0] >> ((8 - self._bits_per_pixel) - offset)) & mask
243 if bytes_per_pixel == 2:
244 pixel_data = pixel_data[0] | pixel_data[1] << 8
245 if self._g_bitmask == 0x07E0: # 565
246 red = (pixel_data & self._r_bitmask) >> 11
247 green = (pixel_data & self._g_bitmask) >> 5
248 blue = pixel_data & self._b_bitmask
250 red = (pixel_data & self._r_bitmask) >> 10
251 green = (pixel_data & self._g_bitmask) >> 4
252 blue = pixel_data & self._b_bitmask
253 return red << 19 | green << 10 | blue << 3
254 if bytes_per_pixel == 4 and self._bitfield_compressed:
255 return pixel_data[0] | pixel_data[1] << 8 | pixel_data[2] << 16
256 pixel = pixel_data[0] | pixel_data[1] << 8 | pixel_data[2] << 16
257 if bytes_per_pixel == 4:
258 pixel |= pixel_data[3] << 24
262 def _finish_refresh(self) -> None: