]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blobdiff - displayio/_bitmap.py
Remove more of PIL
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _bitmap.py
index 03dd75bf483c8f0e17e1910470156114fa591d9e..30009e29df5a579acf2a1962b0ab24f93a3a348c 100644 (file)
@@ -18,17 +18,39 @@ displayio for Blinka
 """
 
 from __future__ import annotations
+import struct
+from array import array
 from typing import Union, Tuple
-from PIL import Image
-from ._structs import RectangleStruct
+from circuitpython_typing import WriteableBuffer
 from ._area import Area
 
 __version__ = "0.0.0+auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
+ALIGN_BITS = 8 * struct.calcsize("I")
+
+
+def stride(width: int, bits_per_pixel: int) -> int:
+    """Return the number of bytes per row of a bitmap with the given width and bits per pixel."""
+    row_width = width * bits_per_pixel
+    return (row_width + (ALIGN_BITS - 1)) // ALIGN_BITS
+
 
 class Bitmap:
-    """Stores values of a certain size in a 2D array"""
+    """Stores values of a certain size in a 2D array
+
+    Bitmaps can be treated as read-only buffers. If the number of bits in a pixel is 8, 16,
+    or 32; and the number of bytes per row is a multiple of 4, then the resulting memoryview
+    will correspond directly with the bitmap's contents. Otherwise, the bitmap data is packed
+    into the memoryview with unspecified padding.
+
+    A Bitmap can be treated as a buffer, allowing its content to be
+    viewed and modified using e.g., with ``ulab.numpy.frombuffer``,
+    but the `displayio.Bitmap.dirty` method must be used to inform
+    displayio when a bitmap was modified through the buffer interface.
+
+    `bitmaptools.arrayblit` can also be useful to move data efficiently
+    into a Bitmap."""
 
     def __init__(self, width: int, height: int, value_count: int):
         """Create a Bitmap object with the given fixed size. Each pixel stores a value that is
@@ -36,12 +58,9 @@ class Bitmap:
         share the underlying Bitmap. value_count is used to minimize the memory used to store
         the Bitmap.
         """
-        self._bmp_width = width
-        self._bmp_height = height
-        self._read_only = False
 
-        if value_count < 0:
-            raise ValueError("value_count must be > 0")
+        if not 1 <= value_count <= 65535:
+            raise ValueError("value_count must be in the range of 1-65535")
 
         bits = 1
         while (value_count - 1) >> bits:
@@ -50,7 +69,28 @@ class Bitmap:
             else:
                 bits += 8
 
-        self._bits_per_value = bits
+        self._from_buffer(width, height, bits, None, False)
+
+    def _from_buffer(
+        self,
+        width: int,
+        height: int,
+        bits_per_value: int,
+        data: WriteableBuffer,
+        read_only: bool,
+    ) -> None:
+        # pylint: disable=too-many-arguments
+        self._bmp_width = width
+        self._bmp_height = height
+        self._stride = stride(width, bits_per_value)
+        self._data_alloc = False
+
+        if data is None or len(data) == 0:
+            data = array("L", [0] * self._stride * height)
+            self._data_alloc = True
+        self._data = data
+        self._read_only = read_only
+        self._bits_per_value = bits_per_value
 
         if (
             self._bits_per_value > 8
@@ -59,8 +99,23 @@ class Bitmap:
         ):
             raise NotImplementedError("Invalid bits per value")
 
-        self._image = Image.new("P", (width, height), 0)
-        self._dirty_area = RectangleStruct(0, 0, width, height)
+        # Division and modulus can be slow because it has to handle any integer. We know
+        # bits_per_value is a power of two. We divide and mod by bits_per_value to compute
+        # the offset into the byte array. So, we can the offset computation to simplify to
+        # a shift for division and mask for mod.
+
+        # Used to divide the index by the number of pixels per word. It's
+        # used in a shift which effectively divides by 2 ** x_shift.
+        self._x_shift = 0
+
+        power_of_two = 1
+        while power_of_two < ALIGN_BITS // bits_per_value:
+            self._x_shift += 1
+            power_of_two = power_of_two << 1
+
+        self._x_mask = (1 << self._x_shift) - 1  # UUsed as a modulus on the x value
+        self._bitmask = (1 << bits_per_value) - 1
+        self._dirty_area = Area(0, 0, width, height)
 
     def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
         """
@@ -75,12 +130,32 @@ class Bitmap:
         else:
             raise TypeError("Index is not an int, list, or tuple")
 
-        if x > self._image.width or y > self._image.height:
+        if x > self._bmp_width or x < 0 or y > self._bmp_height or y < 0:
             raise ValueError(f"Index {index} is out of range")
         return self._get_pixel(x, y)
 
     def _get_pixel(self, x: int, y: int) -> int:
-        return self._image.getpixel((x, y))
+        if x >= self._bmp_width or x < 0 or y >= self._bmp_height or y < 0:
+            return 0
+        row_start = y * self._stride
+        bytes_per_value = self._bits_per_value // 8
+        if bytes_per_value < 1:
+            word = self._data[row_start + (x >> self._x_shift)]
+            return (
+                word
+                >> (
+                    struct.calcsize("I") * 8
+                    - ((x & self._x_mask) + 1) * self._bits_per_value
+                )
+            ) & self._bitmask
+        row = memoryview(self._data)[row_start : row_start + self._stride]
+        if bytes_per_value == 1:
+            return row[x]
+        if bytes_per_value == 2:
+            return struct.unpack_from("<H", row, x * 2)[0]
+        if bytes_per_value == 4:
+            return struct.unpack_from("<I", row, x * 4)[0]
+        return 0
 
     def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
         """
@@ -96,21 +171,42 @@ class Bitmap:
         elif isinstance(index, int):
             x = index % self._bmp_width
             y = index // self._bmp_width
-        self._image.putpixel((x, y), value)
-        if self._dirty_area.x1 == self._dirty_area.x2:
-            self._dirty_area.x1 = x
-            self._dirty_area.x2 = x + 1
-            self._dirty_area.y1 = y
-            self._dirty_area.y2 = y + 1
+        # update the dirty region
+        self._set_dirty_area(Area(x, y, x + 1, y + 1))
+        self._write_pixel(x, y, value)
+
+    def _write_pixel(self, x: int, y: int, value: int) -> None:
+        if self._read_only:
+            raise RuntimeError("Read-only")
+
+        # Writes the color index value into a pixel position
+        # Must update the dirty area separately
+
+        # Don't write if out of area
+        if x < 0 or x >= self._bmp_width or y < 0 or y >= self._bmp_height:
+            return
+
+        # Update one pixel of data
+        row_start = y * self._stride
+        bytes_per_value = self._bits_per_value // 8
+        if bytes_per_value < 1:
+            bit_position = (
+                struct.calcsize("I") * 8
+                - ((x & self._x_mask) + 1) * self._bits_per_value
+            )
+            index = row_start + (x >> self._x_shift)
+            word = self._data[index]
+            word &= ~(self._bitmask << bit_position)
+            word |= (value & self._bitmask) << bit_position
+            self._data[index] = word
         else:
-            if x < self._dirty_area.x1:
-                self._dirty_area.x1 = x
-            elif x >= self._dirty_area.x2:
-                self._dirty_area.x2 = x + 1
-            if y < self._dirty_area.y1:
-                self._dirty_area.y1 = y
-            elif y >= self._dirty_area.y2:
-                self._dirty_area.y2 = y + 1
+            row = memoryview(self._data)[row_start : row_start + self._stride]
+            if bytes_per_value == 1:
+                row[x] = value
+            elif bytes_per_value == 2:
+                struct.pack_into("<H", row, x * 2, value)
+            elif bytes_per_value == 4:
+                struct.pack_into("<I", row, x * 4, value)
 
     def _finish_refresh(self):
         self._dirty_area.x1 = 0
@@ -118,8 +214,18 @@ class Bitmap:
 
     def fill(self, value: int) -> None:
         """Fills the bitmap with the supplied palette index value."""
-        self._image = Image.new("P", (self._bmp_width, self._bmp_height), value)
-        self._dirty_area = RectangleStruct(0, 0, self._bmp_width, self._bmp_height)
+        if self._read_only:
+            raise RuntimeError("Read-only")
+        self._set_dirty_area(Area(0, 0, self._bmp_width, self._bmp_height))
+
+        # build the packed word
+        word = 0
+        for i in range(32 // self._bits_per_value):
+            word |= (value & self._bitmask) << (32 - ((i + 1) * self._bits_per_value))
+
+        # copy it in
+        for i in range(self._stride * self._bmp_height):
+            self._data[i] = word
 
     def blit(
         self,
@@ -182,7 +288,13 @@ class Bitmap:
             x2 = self._bmp_width
         if y2 == -1:
             y2 = self._bmp_height
-        area = Area(x1, y1, x2, y2)
+        self._set_dirty_area(Area(x1, y1, x2, y2))
+
+    def _set_dirty_area(self, dirty_area: Area) -> None:
+        if self._read_only:
+            raise RuntimeError("Read-only")
+
+        area = dirty_area
         area.canon()
         area.union(self._dirty_area, area)
         bitmap_area = Area(0, 0, self._bmp_width, self._bmp_height)