]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_ondiskbitmap.py
30598ef185936afe352f6a3ee0deadd4354d3833
[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 ._helpers import read_word
23 from ._colorconverter import ColorConverter
24 from ._palette import Palette
25
26 __version__ = "0.0.0+auto.0"
27 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
28
29
30 class OnDiskBitmap:
31     # pylint: disable=too-many-instance-attributes
32     """
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
35     visible.
36
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>`_.
39
40     .. code-block:: Python
41
42         import board
43         import displayio
44         import time
45         import pulseio
46
47         board.DISPLAY.auto_brightness = False
48         board.DISPLAY.brightness = 0
49         splash = displayio.Group()
50         board.DISPLAY.show(splash)
51
52         odb = displayio.OnDiskBitmap(\'/sample.bmp\')
53         face = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)
54         splash.append(face)
55         # Wait for the image to load.
56         board.DISPLAY.refresh(target_frames_per_second=60)
57
58         # Fade up the backlight
59         for i in range(100):
60           board.DISPLAY.brightness = 0.01 * i
61           time.sleep(0.05)
62
63         # Wait forever
64         while True:
65           pass
66
67     """
68
69     def __init__(self, file: Union[str, BinaryIO]) -> None:
70         # pylint: disable=too-many-locals, too-many-branches, too-many-statements
71         """
72         Create an OnDiskBitmap object with the given file.
73
74         :param file file: The name of the bitmap file. For backwards compatibility, a file opened
75             in binary mode may also be passed.
76
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.
80         """
81
82         if isinstance(file, str):
83             file = open(file, "rb")  # pylint: disable=consider-using-with
84
85         if not (file.readable() and file.seekable()):
86             raise TypeError("file must be a file opened in byte mode")
87
88         self._pixel_shader_base: Union[ColorConverter, Palette, None] = None
89
90         try:
91             self._file = file
92             file.seek(0)
93             bmp_header = memoryview(file.read(138)).cast(
94                 "H"
95             )  # cast as unsigned 16-bit int
96
97             if len(bmp_header.tobytes()) != 138 or bmp_header.tobytes()[0:2] != b"BM":
98                 raise ValueError("Invalid BMP file")
99
100             self._data_offset = read_word(bmp_header, 5)
101
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)
106
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)
112
113             self._colorconverter = ColorConverter()
114
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)
120                 else:
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
125             elif indexed:
126                 if number_of_colors == 0:
127                     number_of_colors = 1 << bits_per_pixel
128
129                 palette = Palette(number_of_colors)
130
131                 if number_of_colors > 1:
132                     palette_size = number_of_colors * 4
133                     palette_offset = 0xE + header_size
134
135                     file.seek(palette_offset)
136
137                     palette_data = memoryview(file.read(palette_size)).cast(
138                         "I"
139                     )  # cast as unsigned 32-bit int
140                     if len(palette_data.tobytes()) != palette_size:
141                         raise ValueError("Unable to read color palette data")
142
143                     for i in range(number_of_colors):
144                         palette[i] = palette_data[i]
145                 else:
146                     palette[0] = 0x000000
147                     palette[1] = 0xFFFFFF
148                 self._palette = palette
149             elif header_size not in (12, 40, 108, 124):
150                 raise ValueError(
151                     "Only Windows format, uncompressed BMP supported: "
152                     f"given header size is {header_size}"
153                 )
154
155             if bits_per_pixel == 8 and number_of_colors == 0:
156                 raise ValueError(
157                     "Only monochrome, indexed 4bpp or 8bpp, and 16bpp or greater BMPs supported: "
158                     f"{bits_per_pixel} bpp given"
159                 )
160
161             bytes_per_pixel = (
162                 self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
163             )
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
169             else:
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
176
177     @property
178     def width(self) -> int:
179         """
180         Width of the bitmap. (read only)
181
182         :type: int
183         """
184
185         return self._width
186
187     @property
188     def height(self) -> int:
189         """
190         Height of the bitmap. (read only)
191
192         :type: int
193         """
194
195         return self._height
196
197     @property
198     def pixel_shader(self) -> Union[ColorConverter, Palette]:
199         """
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)
203
204         :type: Union[ColorConverter, Palette]
205         """
206
207         return self._pixel_shader_base
208
209     @property
210     def _colorconverter(self) -> ColorConverter:
211         return self._pixel_shader_base
212
213     @_colorconverter.setter
214     def _colorconverter(self, colorconverter: ColorConverter) -> None:
215         self._pixel_shader_base = colorconverter
216
217     @property
218     def _palette(self) -> Palette:
219         return self._pixel_shader_base
220
221     @_palette.setter
222     def _palette(self, palette: Palette) -> None:
223         self._pixel_shader_base = palette
224
225     def _get_pixel(self, x: int, y: int) -> int:
226         if not (0 <= x < self.width and 0 <= y < self.height):
227             return 0
228
229         bytes_per_pixel = (
230             self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
231         )
232         pixels_per_byte = 8 // self._bits_per_pixel
233         if pixels_per_byte == 0:
234             location = (
235                 self._data_offset
236                 + (self.height - y - 1) * self._stride
237                 + x * bytes_per_pixel
238             )
239         else:
240             location = (
241                 self._data_offset
242                 + (self.height - y - 1) * self._stride
243                 + x // pixels_per_byte
244             )
245
246         self._file.seek(location)
247
248         pixel_data = memoryview(self._file.read(4)).cast(
249             "I"
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
261             else:  # 555
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
268         return pixel_data
269
270     def _finish_refresh(self) -> None:
271         pass