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