]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_bitmap.py
324475f4662dc7fc86c543867e858593aaca7791
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _bitmap.py
1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
2 #
3 # SPDX-License-Identifier: MIT
4
5 """
6 `displayio.bitmap`
7 ================================================================================
8
9 displayio for Blinka
10
11 **Software and Dependencies:**
12
13 * Adafruit Blinka:
14   https://github.com/adafruit/Adafruit_Blinka/releases
15
16 * Author(s): Melissa LeBlanc-Williams
17
18 """
19
20 from __future__ import annotations
21 import struct
22 from array import array
23 from typing import Union, Tuple
24 from circuitpython_typing import WriteableBuffer
25 from ._area import Area
26
27 __version__ = "0.0.0+auto.0"
28 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
29
30 ALIGN_BITS = 8 * struct.calcsize("I")
31
32
33 def stride(width: int, bits_per_pixel: int) -> int:
34     """Return the number of bytes per row of a bitmap with the given width and bits per pixel."""
35     row_width = width * bits_per_pixel
36     return (row_width + (ALIGN_BITS - 1)) // ALIGN_BITS
37
38
39 class Bitmap:
40     """Stores values of a certain size in a 2D array
41
42     Bitmaps can be treated as read-only buffers. If the number of bits in a pixel is 8, 16,
43     or 32; and the number of bytes per row is a multiple of 4, then the resulting memoryview
44     will correspond directly with the bitmap's contents. Otherwise, the bitmap data is packed
45     into the memoryview with unspecified padding.
46
47     A Bitmap can be treated as a buffer, allowing its content to be
48     viewed and modified using e.g., with ``ulab.numpy.frombuffer``,
49     but the `displayio.Bitmap.dirty` method must be used to inform
50     displayio when a bitmap was modified through the buffer interface.
51
52     `bitmaptools.arrayblit` can also be useful to move data efficiently
53     into a Bitmap."""
54
55     def __init__(self, width: int, height: int, value_count: int):
56         """Create a Bitmap object with the given fixed size. Each pixel stores a value that is
57         used to index into a corresponding palette. This enables differently colored sprites to
58         share the underlying Bitmap. value_count is used to minimize the memory used to store
59         the Bitmap.
60         """
61
62         if not 1 <= value_count <= 65535:
63             raise ValueError("value_count must be in the range of 1-65535")
64
65         bits = 1
66         while (value_count - 1) >> bits:
67             if bits < 8:
68                 bits = bits << 1
69             else:
70                 bits += 8
71
72         self._from_buffer(width, height, bits, None, False)
73
74     def _from_buffer(
75         self,
76         width: int,
77         height: int,
78         bits_per_value: int,
79         data: WriteableBuffer,
80         read_only: bool,
81     ) -> None:
82         # pylint: disable=too-many-arguments
83         self._bmp_width = width
84         self._bmp_height = height
85         self._stride = stride(width, bits_per_value)
86         self._data_alloc = False
87
88         if data is None or len(data) == 0:
89             data = array("L", [0] * self._stride * height)
90             self._data_alloc = True
91         self._data = data
92         self._read_only = read_only
93         self._bits_per_value = bits_per_value
94
95         if (
96             self._bits_per_value > 8
97             and self._bits_per_value != 16
98             and self._bits_per_value != 32
99         ):
100             raise NotImplementedError("Invalid bits per value")
101
102         # Division and modulus can be slow because it has to handle any integer. We know
103         # bits_per_value is a power of two. We divide and mod by bits_per_value to compute
104         # the offset into the byte array. So, we can the offset computation to simplify to
105         # a shift for division and mask for mod.
106
107         # Used to divide the index by the number of pixels per word. It's
108         # used in a shift which effectively divides by 2 ** x_shift.
109         self._x_shift = 0
110
111         power_of_two = 1
112         while power_of_two < ALIGN_BITS // bits_per_value:
113             self._x_shift += 1
114             power_of_two = power_of_two << 1
115
116         self._x_mask = (1 << self._x_shift) - 1  # Used as a modulus on the x value
117         self._bitmask = (1 << bits_per_value) - 1
118         self._dirty_area = Area(0, 0, width, height)
119
120     def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
121         """
122         Returns the value at the given index. The index can either be
123         an x,y tuple or an int equal to `y * width + x`.
124         """
125         if isinstance(index, (tuple, list)):
126             x, y = index
127         elif isinstance(index, int):
128             x = index % self._bmp_width
129             y = index // self._bmp_width
130         else:
131             raise TypeError("Index is not an int, list, or tuple")
132
133         if x > self._bmp_width or x < 0 or y > self._bmp_height or y < 0:
134             raise ValueError(f"Index {index} is out of range")
135         return self._get_pixel(x, y)
136
137     def _get_pixel(self, x: int, y: int) -> int:
138         if x >= self._bmp_width or x < 0 or y >= self._bmp_height or y < 0:
139             return 0
140         row_start = y * self._stride
141         bytes_per_value = self._bits_per_value // 8
142         if bytes_per_value < 1:
143             word = self._data[row_start + (x >> self._x_shift)]
144             return (
145                 word
146                 >> (
147                     struct.calcsize("I") * 8
148                     - ((x & self._x_mask) + 1) * self._bits_per_value
149                 )
150             ) & self._bitmask
151         row = memoryview(self._data)[row_start : row_start + self._stride]
152         if bytes_per_value == 1:
153             return row[x]
154         if bytes_per_value == 2:
155             return struct.unpack_from("<H", row, x * 2)[0]
156         if bytes_per_value == 4:
157             return struct.unpack_from("<I", row, x * 4)[0]
158         return 0
159
160     def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
161         """
162         Sets the value at the given index. The index can either be
163         an x,y tuple or an int equal to `y * width + x`.
164         """
165         if self._read_only:
166             raise RuntimeError("Read-only object")
167         if isinstance(index, (tuple, list)):
168             x = index[0]
169             y = index[1]
170             index = y * self._bmp_width + x
171         elif isinstance(index, int):
172             x = index % self._bmp_width
173             y = index // self._bmp_width
174         # update the dirty region
175         self._set_dirty_area(Area(x, y, x + 1, y + 1))
176         self._write_pixel(x, y, value)
177
178     def _write_pixel(self, x: int, y: int, value: int) -> None:
179         if self._read_only:
180             raise RuntimeError("Read-only")
181
182         # Writes the color index value into a pixel position
183         # Must update the dirty area separately
184
185         # Don't write if out of area
186         if x < 0 or x >= self._bmp_width or y < 0 or y >= self._bmp_height:
187             return
188
189         # Update one pixel of data
190         row_start = y * self._stride
191         bytes_per_value = self._bits_per_value // 8
192         if bytes_per_value < 1:
193             bit_position = (
194                 struct.calcsize("I") * 8
195                 - ((x & self._x_mask) + 1) * self._bits_per_value
196             )
197             index = row_start + (x >> self._x_shift)
198             word = self._data[index]
199             word &= ~(self._bitmask << bit_position)
200             word |= (value & self._bitmask) << bit_position
201             self._data[index] = word
202         else:
203             row = memoryview(self._data)[row_start : row_start + self._stride]
204             if bytes_per_value == 1:
205                 row[x] = value
206             elif bytes_per_value == 2:
207                 struct.pack_into("<H", row, x * 2, value)
208             elif bytes_per_value == 4:
209                 struct.pack_into("<I", row, x * 4, value)
210
211     def _finish_refresh(self):
212         self._dirty_area.x1 = 0
213         self._dirty_area.x2 = 0
214
215     def fill(self, value: int) -> None:
216         """Fills the bitmap with the supplied palette index value."""
217         if self._read_only:
218             raise RuntimeError("Read-only")
219         self._set_dirty_area(Area(0, 0, self._bmp_width, self._bmp_height))
220
221         # build the packed word
222         word = 0
223         for i in range(32 // self._bits_per_value):
224             word |= (value & self._bitmask) << (32 - ((i + 1) * self._bits_per_value))
225
226         # copy it in
227         for i in range(self._stride * self._bmp_height):
228             self._data[i] = word
229
230     def blit(
231         self,
232         x: int,
233         y: int,
234         source_bitmap: Bitmap,
235         *,
236         x1: int,
237         y1: int,
238         x2: int,
239         y2: int,
240         skip_index: int,
241     ) -> None:
242         """Inserts the source_bitmap region defined by rectangular boundaries"""
243         # pylint: disable=invalid-name
244         if x2 is None:
245             x2 = source_bitmap.width
246         if y2 is None:
247             y2 = source_bitmap.height
248
249         # Rearrange so that x1 < x2 and y1 < y2
250         if x1 > x2:
251             x1, x2 = x2, x1
252         if y1 > y2:
253             y1, y2 = y2, y1
254
255         # Ensure that x2 and y2 are within source bitmap size
256         x2 = min(x2, source_bitmap.width)
257         y2 = min(y2, source_bitmap.height)
258
259         for y_count in range(y2 - y1):
260             for x_count in range(x2 - x1):
261                 x_placement = x + x_count
262                 y_placement = y + y_count
263
264                 if (self.width > x_placement >= 0) and (
265                     self.height > y_placement >= 0
266                 ):  # ensure placement is within target bitmap
267                     # get the palette index from the source bitmap
268                     this_pixel_color = source_bitmap[
269                         y1
270                         + (
271                             y_count * source_bitmap.width
272                         )  # Direct index into a bitmap array is speedier than [x,y] tuple
273                         + x1
274                         + x_count
275                     ]
276
277                     if (skip_index is None) or (this_pixel_color != skip_index):
278                         self[  # Direct index into a bitmap array is speedier than [x,y] tuple
279                             y_placement * self.width + x_placement
280                         ] = this_pixel_color
281                 elif y_placement > self.height:
282                     break
283
284     def dirty(self, x1: int = 0, y1: int = 0, x2: int = -1, y2: int = -1) -> None:
285         """Inform displayio of bitmap updates done via the buffer protocol."""
286         # pylint: disable=invalid-name
287         if x2 == -1:
288             x2 = self._bmp_width
289         if y2 == -1:
290             y2 = self._bmp_height
291         self._set_dirty_area(Area(x1, y1, x2, y2))
292
293     def _set_dirty_area(self, dirty_area: Area) -> None:
294         if self._read_only:
295             raise RuntimeError("Read-only")
296
297         area = dirty_area
298         area.canon()
299         area.union(self._dirty_area, area)
300         bitmap_area = Area(0, 0, self._bmp_width, self._bmp_height)
301         area.compute_overlap(bitmap_area, self._dirty_area)
302
303     def _finish_refresh(self):
304         if self._read_only:
305             return
306         self._dirty_area.x1 = 0
307         self._dirty_area.x2 = 0
308
309     def _get_refresh_areas(self, areas: list[Area]) -> None:
310         if self._dirty_area.x1 == self._dirty_area.x2 or self._read_only:
311             return
312         areas.append(self._dirty_area)
313
314     @property
315     def width(self) -> int:
316         """Width of the bitmap. (read only)"""
317         return self._bmp_width
318
319     @property
320     def height(self) -> int:
321         """Height of the bitmap. (read only)"""
322         return self._bmp_height