1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
3 # SPDX-License-Identifier: MIT
7 ================================================================================
11 **Software and Dependencies:**
14 https://github.com/adafruit/Adafruit_Blinka/releases
16 * Author(s): Melissa LeBlanc-Williams
20 from __future__ import annotations
22 from array import array
23 from typing import Union, Tuple
24 from circuitpython_typing import WriteableBuffer
25 from ._area import Area
27 __version__ = "0.0.0+auto.0"
28 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
30 ALIGN_BITS = 8 * struct.calcsize("I")
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
40 """Stores values of a certain size in a 2D array
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.
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.
52 `bitmaptools.arrayblit` can also be useful to move data efficiently
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
62 if not 1 <= value_count <= 65535:
63 raise ValueError("value_count must be in the range of 1-65535")
66 while (value_count - 1) >> bits:
72 self._from_buffer(width, height, bits, None, False)
79 data: WriteableBuffer,
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
88 if data is None or len(data) == 0:
89 data = array("L", [0] * self._stride * height)
90 self._data_alloc = True
92 self._read_only = read_only
93 self._bits_per_value = bits_per_value
96 self._bits_per_value > 8
97 and self._bits_per_value != 16
98 and self._bits_per_value != 32
100 raise NotImplementedError("Invalid bits per value")
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.
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.
112 while power_of_two < ALIGN_BITS // bits_per_value:
114 power_of_two = power_of_two << 1
116 self._x_mask = (1 << self._x_shift) - 1 # UUsed as a modulus on the x value
117 self._bitmask = (1 << bits_per_value) - 1
118 self._dirty_area = Area(0, 0, width, height)
120 def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
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`.
125 if isinstance(index, (tuple, list)):
127 elif isinstance(index, int):
128 x = index % self._bmp_width
129 y = index // self._bmp_width
131 raise TypeError("Index is not an int, list, or tuple")
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)
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:
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)]
147 struct.calcsize("I") * 8
148 - ((x & self._x_mask) + 1) * self._bits_per_value
151 row = memoryview(self._data)[row_start : row_start + self._stride]
152 if bytes_per_value == 1:
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]
160 def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
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`.
166 raise RuntimeError("Read-only object")
167 if isinstance(index, (tuple, list)):
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)
178 def _write_pixel(self, x: int, y: int, value: int) -> None:
180 raise RuntimeError("Read-only")
182 # Writes the color index value into a pixel position
183 # Must update the dirty area separately
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:
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:
194 struct.calcsize("I") * 8
195 - ((x & self._x_mask) + 1) * self._bits_per_value
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
203 row = memoryview(self._data)[row_start : row_start + self._stride]
204 if bytes_per_value == 1:
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)
211 def _finish_refresh(self):
212 self._dirty_area.x1 = 0
213 self._dirty_area.x2 = 0
215 def fill(self, value: int) -> None:
216 """Fills the bitmap with the supplied palette index value."""
218 raise RuntimeError("Read-only")
219 self._set_dirty_area(Area(0, 0, self._bmp_width, self._bmp_height))
221 # build the packed word
223 for i in range(32 // self._bits_per_value):
224 word |= (value & self._bitmask) << (32 - ((i + 1) * self._bits_per_value))
227 for i in range(self._stride * self._bmp_height):
234 source_bitmap: Bitmap,
242 """Inserts the source_bitmap region defined by rectangular boundaries"""
243 # pylint: disable=invalid-name
245 x2 = source_bitmap.width
247 y2 = source_bitmap.height
249 # Rearrange so that x1 < x2 and y1 < y2
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)
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
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[
271 y_count * source_bitmap.width
272 ) # Direct index into a bitmap array is speedier than [x,y] tuple
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
281 elif y_placement > self.height:
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
290 y2 = self._bmp_height
291 self._set_dirty_area(Area(x1, y1, x2, y2))
293 def _set_dirty_area(self, dirty_area: Area) -> None:
295 raise RuntimeError("Read-only")
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)
303 def _finish_refresh(self):
306 self._dirty_area.x1 = 0
307 self._dirty_area.x2 = 0
309 def _get_refresh_areas(self, areas: list[Area]) -> None:
310 if self._dirty_area.x1 == self._dirty_area.x2 or self._read_only:
312 areas.append(self._dirty_area)
315 def width(self) -> int:
316 """Width of the bitmap. (read only)"""
317 return self._bmp_width
320 def height(self) -> int:
321 """Height of the bitmap. (read only)"""
322 return self._bmp_height