]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_ondiskbitmap.py
update docs build reqs
[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     .. code-block:: Python
39
40         import board
41         import displayio
42         import time
43         import pulseio
44
45         board.DISPLAY.auto_brightness = False
46         board.DISPLAY.brightness = 0
47         splash = displayio.Group()
48         board.DISPLAY.root_group = splash
49
50         odb = displayio.OnDiskBitmap(\'/sample.bmp\')
51         face = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)
52         splash.append(face)
53         # Wait for the image to load.
54         board.DISPLAY.refresh(target_frames_per_second=60)
55
56         # Fade up the backlight
57         for i in range(100):
58           board.DISPLAY.brightness = 0.01 * i
59           time.sleep(0.05)
60
61         # Wait forever
62         while True:
63           pass
64
65     """
66
67     def __init__(self, file: Union[str, BinaryIO]) -> None:
68         # pylint: disable=too-many-locals, too-many-branches, too-many-statements
69         """
70         Create an OnDiskBitmap object with the given file.
71
72         :param file file: The name of the bitmap file. For backwards compatibility, a file opened
73             in binary mode may also be passed.
74
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.
78         """
79
80         if isinstance(file, str):
81             file = open(file, "rb")  # pylint: disable=consider-using-with
82
83         if not (file.readable() and file.seekable()):
84             raise TypeError("file must be a file opened in byte mode")
85
86         self._pixel_shader_base: Union[ColorConverter, Palette, None] = None
87
88         try:
89             self._file = file
90             file.seek(0)
91             bmp_header = memoryview(file.read(138)).cast(
92                 "H"
93             )  # cast as unsigned 16-bit int
94
95             if len(bmp_header.tobytes()) != 138 or bmp_header.tobytes()[0:2] != b"BM":
96                 raise ValueError("Invalid BMP file")
97
98             self._data_offset = read_word(bmp_header, 5)
99
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)
104
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)
110
111             self._pixel_shader_base = ColorConverter(
112                 input_colorspace=Colorspace.RGB888, dither=False
113             )
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, dither=False)
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._set_color(
145                             palette_data[i], i
146                         )  # pylint: disable=protected-access
147                 else:
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):
152                 raise ValueError(
153                     "Only Windows format, uncompressed BMP supported: "
154                     f"given header size is {header_size}"
155                 )
156
157             if bits_per_pixel == 8 and number_of_colors == 0:
158                 raise ValueError(
159                     "Only monochrome, indexed 4bpp or 8bpp, and 16bpp or greater BMPs supported: "
160                     f"{bits_per_pixel} bpp given"
161                 )
162
163             bytes_per_pixel = (
164                 self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
165             )
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
171             else:
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
178
179     @property
180     def width(self) -> int:
181         """
182         Width of the bitmap. (read only)
183
184         :type: int
185         """
186
187         return self._width
188
189     @property
190     def height(self) -> int:
191         """
192         Height of the bitmap. (read only)
193
194         :type: int
195         """
196
197         return self._height
198
199     @property
200     def pixel_shader(self) -> Union[ColorConverter, Palette]:
201         """
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)
205
206         :type: Union[ColorConverter, Palette]
207         """
208
209         return self._pixel_shader_base
210
211     def _get_pixel(self, x: int, y: int) -> int:
212         if not (0 <= x < self.width and 0 <= y < self.height):
213             return 0
214
215         bytes_per_pixel = (
216             self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
217         )
218         pixels_per_byte = 8 // self._bits_per_pixel
219         if pixels_per_byte == 0:
220             location = (
221                 self._data_offset
222                 + (self.height - y - 1) * self._stride
223                 + x * bytes_per_pixel
224             )
225         else:
226             location = (
227                 self._data_offset
228                 + (self.height - y - 1) * self._stride
229                 + x // pixels_per_byte
230             )
231
232         self._file.seek(location)
233
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
246                 else:  # 555
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
256             return pixel
257         return 0
258
259     def _finish_refresh(self) -> None:
260         pass