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"
 
  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
 
  38     """Stores values of a certain size in a 2D array
 
  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.
 
  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.
 
  50     `bitmaptools.arrayblit` can also be useful to move data efficiently
 
  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
 
  60         if not 1 <= value_count <= 65536:
 
  61             raise ValueError("value_count must be in the range of 1-65536")
 
  64         while (value_count - 1) >> bits:
 
  70         self._from_buffer(width, height, bits, None, False)
 
  77         data: WriteableBuffer,
 
  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
 
  86         if data is None or len(data) == 0:
 
  87             data = array("L", [0] * self._stride * height)
 
  88             self._data_alloc = True
 
  90         self._read_only = read_only
 
  91         self._bits_per_value = bits_per_value
 
  94             self._bits_per_value > 8
 
  95             and self._bits_per_value != 16
 
  96             and self._bits_per_value != 32
 
  98             raise NotImplementedError("Invalid bits per value")
 
 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.
 
 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.
 
 110         while power_of_two < 32 // bits_per_value:
 
 112             power_of_two = power_of_two << 1
 
 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)
 
 118     def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
 
 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`.
 
 123         if isinstance(index, (tuple, list)):
 
 125         elif isinstance(index, int):
 
 126             x = index % self._bmp_width
 
 127             y = index // self._bmp_width
 
 129             raise TypeError("Index is not an int, list, or tuple")
 
 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)
 
 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:
 
 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)]
 
 143                 word >> (32 - ((x & self._x_mask) + 1) * self._bits_per_value)
 
 145         row = memoryview(self._data)[row_start : row_start + self._stride]
 
 146         if bytes_per_value == 1:
 
 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]
 
 154     def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
 
 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`.
 
 160             raise RuntimeError("Read-only object")
 
 161         if isinstance(index, (tuple, list)):
 
 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)
 
 172     def _write_pixel(self, x: int, y: int, value: int) -> None:
 
 174             raise RuntimeError("Read-only")
 
 176         # Writes the color index value into a pixel position
 
 177         # Must update the dirty area separately
 
 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:
 
 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
 
 194             row = memoryview(self._data)[row_start : row_start + self._stride]
 
 195             if bytes_per_value == 1:
 
 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)
 
 202     def fill(self, value: int) -> None:
 
 203         """Fills the bitmap with the supplied palette index value."""
 
 205             raise RuntimeError("Read-only")
 
 206         self._set_dirty_area(Area(0, 0, self._bmp_width, self._bmp_height))
 
 208         # build the packed word
 
 210         for i in range(32 // self._bits_per_value):
 
 211             word |= (value & self._bitmask) << (32 - ((i + 1) * self._bits_per_value))
 
 214         for i in range(self._stride * self._bmp_height):
 
 221         source_bitmap: Bitmap,
 
 229         """Inserts the source_bitmap region defined by rectangular boundaries"""
 
 230         # pylint: disable=invalid-name
 
 232             x2 = source_bitmap.width
 
 234             y2 = source_bitmap.height
 
 236         # Rearrange so that x1 < x2 and y1 < y2
 
 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)
 
 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
 
 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[
 
 258                             y_count * source_bitmap.width
 
 259                         )  # Direct index into a bitmap array is speedier than [x,y] tuple
 
 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
 
 268                 elif y_placement > self.height:
 
 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
 
 277             y2 = self._bmp_height
 
 278         self._set_dirty_area(Area(x1, y1, x2, y2))
 
 280     def _set_dirty_area(self, dirty_area: Area) -> None:
 
 282             raise RuntimeError("Read-only")
 
 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)
 
 290     def _finish_refresh(self):
 
 293         self._dirty_area.x1 = 0
 
 294         self._dirty_area.x2 = 0
 
 296     def _get_refresh_areas(self, areas: list[Area]) -> None:
 
 297         if self._dirty_area.x1 == self._dirty_area.x2 or self._read_only:
 
 299         areas.append(self._dirty_area)
 
 302     def width(self) -> int:
 
 303         """Width of the bitmap. (read only)"""
 
 304         return self._bmp_width
 
 307     def height(self) -> int:
 
 308         """Height of the bitmap. (read only)"""
 
 309         return self._bmp_height