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: