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