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 ._colorconverter import ColorConverter
23 from ._palette import Palette
25 __version__ = "0.0.0+auto.0"
26 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
29 def _read_uint32(buffer: bytes, idx: int) -> int:
32 | buffer[idx + 1] << 8
33 | buffer[idx + 2] << 16
34 | buffer[idx + 3] << 24
38 def _read_word(header: bytes, idx: int) -> int:
39 return _read_uint32(header, idx * 2)
43 # pylint: disable=too-many-instance-attributes
45 Loads values straight from disk. This minimizes memory use but can lead to much slower pixel
46 load times. These load times may result in frame tearing where only part of the image is
49 It's easiest to use on a board with a built in display such as the `Hallowing M0 Express
50 <https://www.adafruit.com/product/3900>`_.
52 .. code-block:: Python
59 board.DISPLAY.auto_brightness = False
60 board.DISPLAY.brightness = 0
61 splash = displayio.Group()
62 board.DISPLAY.show(splash)
64 odb = displayio.OnDiskBitmap(\'/sample.bmp\')
65 face = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)
67 # Wait for the image to load.
68 board.DISPLAY.refresh(target_frames_per_second=60)
70 # Fade up the backlight
72 board.DISPLAY.brightness = 0.01 * i
81 def __init__(self, file: Union[str, BinaryIO]) -> None:
82 # pylint: disable=too-many-locals, too-many-branches, too-many-statements
84 Create an OnDiskBitmap object with the given file.
86 :param file file: The name of the bitmap file. For backwards compatibility, a file opened
87 in binary mode may also be passed.
89 Older versions of CircuitPython required a file opened in binary mode. CircuitPython 7.0
90 modified OnDiskBitmap so that it takes a filename instead, and opens the file internally.
91 A future version of CircuitPython will remove the ability to pass in an opened file.
94 if isinstance(file, str):
95 file = open(file, "rb") # pylint: disable=consider-using-with
97 if not (file.readable() and file.seekable()):
98 raise TypeError("file must be a file opened in byte mode")
100 self._pixel_shader_base: Union[ColorConverter, Palette, None] = None
105 bmp_header = file.read(138)
107 if len(bmp_header) != 138 or bmp_header[0:2] != b"BM":
108 raise ValueError("Invalid BMP file")
110 self._data_offset = _read_word(bmp_header, 5)
112 header_size = _read_word(bmp_header, 7)
113 bits_per_pixel = bmp_header[14 * 2] | bmp_header[14 * 2 + 1] << 8
114 compression = _read_word(bmp_header, 15)
115 number_of_colors = _read_word(bmp_header, 23)
117 indexed = bits_per_pixel <= 8
118 self._bitfield_compressed = compression == 3
119 self._bits_per_pixel = bits_per_pixel
120 self._width = _read_word(bmp_header, 9)
121 self._height = _read_word(bmp_header, 11)
123 self._colorconverter = ColorConverter()
125 if bits_per_pixel == 16:
126 if header_size >= 56 or self._bitfield_compressed:
127 self._r_bitmask = _read_word(bmp_header, 27)
128 self._g_bitmask = _read_word(bmp_header, 29)
129 self._b_bitmask = _read_word(bmp_header, 31)
131 # No compression or short header mean 5:5:5
132 self._r_bitmask = 0x7C00
133 self._g_bitmask = 0x03E0
134 self._b_bitmask = 0x001F
136 if number_of_colors == 0:
137 number_of_colors = 1 << bits_per_pixel
139 palette = Palette(number_of_colors)
141 if number_of_colors > 1:
142 palette_size = number_of_colors * 4
143 palette_offset = 0xE + header_size
145 file.seek(palette_offset)
147 palette_data = file.read(palette_size)
148 if len(palette_data) != palette_size:
149 raise ValueError("Unable to read color palette data")
151 for i in range(number_of_colors):
152 palette[i] = _read_uint32(palette_data, i * 4)
154 palette[0] = 0x000000
155 palette[1] = 0xFFFFFF
156 self._palette = palette
157 elif header_size not in (12, 40, 108, 124):
159 "Only Windows format, uncompressed BMP supported: "
160 f"given header size is {header_size}"
163 if bits_per_pixel == 8 and number_of_colors == 0:
165 "Only monochrome, indexed 4bpp or 8bpp, and 16bpp or greater BMPs supported: "
166 f"{bits_per_pixel} bpp given"
170 self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
172 pixels_per_byte = 8 // self._bits_per_pixel
173 if pixels_per_byte == 0:
174 self._stride = self._width * bytes_per_pixel
175 if self._stride % 4 != 0:
176 self._stride += 4 - self._stride % 4
178 bit_stride = self._width * self._bits_per_pixel
179 if bit_stride % 32 != 0:
180 bit_stride += 32 - bit_stride % 32
181 self._stride = bit_stride // 8
182 except IOError as error:
183 raise OSError from error
186 def width(self) -> int:
188 Width of the bitmap. (read only)
196 def height(self) -> int:
198 Height of the bitmap. (read only)
206 def pixel_shader(self) -> Union[ColorConverter, Palette]:
208 The image's pixel_shader. The type depends on the underlying `Bitmap`'s structure. The
209 pixel shader can be modified (e.g., to set the transparent pixel or, for paletted images,
210 to update the palette)
212 :type: Union[ColorConverter, Palette]
215 return self._pixel_shader_base
218 def _colorconverter(self) -> ColorConverter:
219 return self._pixel_shader_base
221 @_colorconverter.setter
222 def _colorconverter(self, colorconverter: ColorConverter) -> None:
223 self._pixel_shader_base = colorconverter
226 def _palette(self) -> Palette:
227 return self._pixel_shader_base
230 def _palette(self, palette: Palette) -> None:
231 self._pixel_shader_base = palette
233 def _get_pixel(self, x: int, y: int) -> int:
234 if not (0 <= x < self.width and 0 <= y < self.height):
238 self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
240 pixels_per_byte = 8 // self._bits_per_pixel
241 if pixels_per_byte == 0:
244 + (self.height - y - 1) * self._stride
245 + x * bytes_per_pixel
250 + (self.height - y - 1) * self._stride
251 + x // pixels_per_byte
254 self._file.seek(location)
256 pixel_data = self._file.read(bytes_per_pixel)
257 if len(pixel_data) == bytes_per_pixel:
258 if bytes_per_pixel == 1:
259 offset = (x % pixels_per_byte) * self._bits_per_pixel
260 mask = (1 << self._bits_per_pixel) - 1
261 return (pixel_data[0] >> ((8 - self._bits_per_pixel) - offset)) & mask
262 if bytes_per_pixel == 2:
263 pixel_data = pixel_data[0] | pixel_data[1] << 8
264 if self._g_bitmask == 0x07E0: # 565
265 red = (pixel_data & self._r_bitmask) >> 11
266 green = (pixel_data & self._g_bitmask) >> 5
267 blue = pixel_data & self._b_bitmask
269 red = (pixel_data & self._r_bitmask) >> 10
270 green = (pixel_data & self._g_bitmask) >> 4
271 blue = pixel_data & self._b_bitmask
272 return red << 19 | green << 10 | blue << 3
273 if bytes_per_pixel == 4 and self._bitfield_compressed:
274 return pixel_data[0] | pixel_data[1] << 8 | pixel_data[2] << 16
276 pixel = pixel_data[0] | pixel_data[1] << 8 | pixel_data[2] << 16
277 if bytes_per_pixel == 4:
278 pixel |= pixel_data[3] << 24
282 def _finish_refresh(self) -> None: