]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_ondiskbitmap.py
f9e9e8b48ba2d994e2b7a6a6efc51a10fd507988
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _ondiskbitmap.py
1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
2 # SPDX-FileCopyrightText: 2021 James Carr
3 #
4 # SPDX-License-Identifier: MIT
5
6 """
7 `displayio.ondiskbitmap`
8 ================================================================================
9
10 displayio for Blinka
11
12 **Software and Dependencies:**
13
14 * Adafruit Blinka:
15   https://github.com/adafruit/Adafruit_Blinka/releases
16
17 * Author(s): Melissa LeBlanc-Williams, James Carr
18
19 """
20
21 from typing import Union, BinaryIO
22 from ._colorconverter import ColorConverter
23 from ._palette import Palette
24
25 __version__ = "0.0.0+auto.0"
26 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
27
28
29 def _read_uint32(buffer: bytes, idx: int) -> int:
30     return (
31         buffer[idx]
32         | buffer[idx + 1] << 8
33         | buffer[idx + 2] << 16
34         | buffer[idx + 3] << 24
35     )
36
37
38 def _read_word(header: bytes, idx: int) -> int:
39     return _read_uint32(header, idx * 2)
40
41
42 class OnDiskBitmap:
43     # pylint: disable=too-many-instance-attributes
44     """
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
47     visible.
48
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>`_.
51
52     .. code-block:: Python
53
54         import board
55         import displayio
56         import time
57         import pulseio
58
59         board.DISPLAY.auto_brightness = False
60         board.DISPLAY.brightness = 0
61         splash = displayio.Group()
62         board.DISPLAY.show(splash)
63
64         odb = displayio.OnDiskBitmap(\'/sample.bmp\')
65         face = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)
66         splash.append(face)
67         # Wait for the image to load.
68         board.DISPLAY.refresh(target_frames_per_second=60)
69
70         # Fade up the backlight
71         for i in range(100):
72           board.DISPLAY.brightness = 0.01 * i
73           time.sleep(0.05)
74
75         # Wait forever
76         while True:
77           pass
78
79     """
80
81     def __init__(self, file: Union[str, BinaryIO]) -> None:
82         # pylint: disable=too-many-locals, too-many-branches, too-many-statements
83         """
84         Create an OnDiskBitmap object with the given file.
85
86         :param file file: The name of the bitmap file. For backwards compatibility, a file opened
87             in binary mode may also be passed.
88
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.
92         """
93
94         if isinstance(file, str):
95             file = open(file, "rb")  # pylint: disable=consider-using-with
96
97         if not (file.readable() and file.seekable()):
98             raise TypeError("file must be a file opened in byte mode")
99
100         self._pixel_shader_base: Union[ColorConverter, Palette, None] = None
101
102         try:
103             self._file = file
104             file.seek(0)
105             bmp_header = file.read(138)
106
107             if len(bmp_header) != 138 or bmp_header[0:2] != b"BM":
108                 raise ValueError("Invalid BMP file")
109
110             self._data_offset = _read_word(bmp_header, 5)
111
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)
116
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)
122
123             self._colorconverter = ColorConverter()
124
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)
130                 else:
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
135             elif indexed:
136                 if number_of_colors == 0:
137                     number_of_colors = 1 << bits_per_pixel
138
139                 palette = Palette(number_of_colors)
140
141                 if number_of_colors > 1:
142                     palette_size = number_of_colors * 4
143                     palette_offset = 0xE + header_size
144
145                     file.seek(palette_offset)
146
147                     palette_data = file.read(palette_size)
148                     if len(palette_data) != palette_size:
149                         raise ValueError("Unable to read color palette data")
150
151                     for i in range(number_of_colors):
152                         palette[i] = _read_uint32(palette_data, i * 4)
153                 else:
154                     palette[0] = 0x000000
155                     palette[1] = 0xFFFFFF
156                 self._palette = palette
157             elif header_size not in (12, 40, 108, 124):
158                 raise ValueError(
159                     "Only Windows format, uncompressed BMP supported: "
160                     f"given header size is {header_size}"
161                 )
162
163             if bits_per_pixel == 8 and number_of_colors == 0:
164                 raise ValueError(
165                     "Only monochrome, indexed 4bpp or 8bpp, and 16bpp or greater BMPs supported: "
166                     f"{bits_per_pixel} bpp given"
167                 )
168
169             bytes_per_pixel = (
170                 self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
171             )
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
177             else:
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
184
185     @property
186     def width(self) -> int:
187         """
188         Width of the bitmap. (read only)
189
190         :type: int
191         """
192
193         return self._width
194
195     @property
196     def height(self) -> int:
197         """
198         Height of the bitmap. (read only)
199
200         :type: int
201         """
202
203         return self._height
204
205     @property
206     def pixel_shader(self) -> Union[ColorConverter, Palette]:
207         """
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)
211
212         :type: Union[ColorConverter, Palette]
213         """
214
215         return self._pixel_shader_base
216
217     @property
218     def _colorconverter(self) -> ColorConverter:
219         return self._pixel_shader_base
220
221     @_colorconverter.setter
222     def _colorconverter(self, colorconverter: ColorConverter) -> None:
223         self._pixel_shader_base = colorconverter
224
225     @property
226     def _palette(self) -> Palette:
227         return self._pixel_shader_base
228
229     @_palette.setter
230     def _palette(self, palette: Palette) -> None:
231         self._pixel_shader_base = palette
232
233     def _get_pixel(self, x: int, y: int) -> int:
234         if not (0 <= x < self.width and 0 <= y < self.height):
235             return 0
236
237         bytes_per_pixel = (
238             self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
239         )
240         pixels_per_byte = 8 // self._bits_per_pixel
241         if pixels_per_byte == 0:
242             location = (
243                 self._data_offset
244                 + (self.height - y - 1) * self._stride
245                 + x * bytes_per_pixel
246             )
247         else:
248             location = (
249                 self._data_offset
250                 + (self.height - y - 1) * self._stride
251                 + x // pixels_per_byte
252             )
253
254         self._file.seek(location)
255
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
268                 else:  # 555
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
275
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
279             return pixel
280         return 0
281
282     def _finish_refresh(self) -> None:
283         pass