From 33accffa7278997a55e237473c33880813def934 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Fri, 22 Sep 2023 22:36:30 -0700 Subject: [PATCH] Remove more of PIL --- displayio/_bitmap.py | 172 +++++++++++++++++++++++++++++------- displayio/_display.py | 90 ++++--------------- displayio/_displaycore.py | 8 +- displayio/_epaperdisplay.py | 10 +-- displayio/_fourwire.py | 6 +- displayio/_group.py | 6 +- displayio/_i2cdisplay.py | 6 +- displayio/_palette.py | 4 +- displayio/_structs.py | 10 --- displayio/_tilegrid.py | 24 ++--- 10 files changed, 194 insertions(+), 142 deletions(-) diff --git a/displayio/_bitmap.py b/displayio/_bitmap.py index 03dd75b..30009e2 100644 --- a/displayio/_bitmap.py +++ b/displayio/_bitmap.py @@ -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(" 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(" 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) diff --git a/displayio/_display.py b/displayio/_display.py index ac71981..e001577 100644 --- a/displayio/_display.py +++ b/displayio/_display.py @@ -22,14 +22,12 @@ import struct from array import array from typing import Optional import digitalio -from PIL import Image import microcontroller -import circuitpython_typing +from circuitpython_typing import WriteableBuffer, ReadableBuffer from ._displaycore import _DisplayCore from ._displaybus import _DisplayBus from ._colorconverter import ColorConverter from ._group import Group -from ._structs import RectangleStruct from ._area import Area from ._constants import ( CHIP_SELECT_TOGGLE_EVERY_BYTE, @@ -58,7 +56,7 @@ class Display: def __init__( self, display_bus: _DisplayBus, - init_sequence: circuitpython_typing.ReadableBuffer, + init_sequence: ReadableBuffer, *, width: int, height: int, @@ -77,16 +75,14 @@ class Display: backlight_pin: Optional[microcontroller.Pin] = None, brightness_command: Optional[int] = None, brightness: float = 1.0, - auto_brightness: bool = False, single_byte_bounds: bool = False, data_as_commands: bool = False, auto_refresh: bool = True, native_frames_per_second: int = 60, backlight_on_high: bool = True, SH1107_addressing: bool = False, - set_vertical_scroll: int = 0, ): - # pylint: disable=unused-argument,too-many-locals,invalid-name + # pylint: disable=too-many-locals,invalid-name """Create a Display object on the given display bus (`displayio.FourWire` or `paralleldisplay.ParallelBus`). @@ -157,12 +153,10 @@ class Display: self._native_frames_per_second = native_frames_per_second self._native_ms_per_frame = 1000 // native_frames_per_second - self._auto_brightness = auto_brightness self._brightness = brightness self._auto_refresh = auto_refresh self._initialize(init_sequence) - self._buffer = Image.new("RGB", (width, height)) self._current_group = None self._last_refresh_call = 0 self._refresh_thread = None @@ -295,20 +289,7 @@ class Display: if not self._core.start_refresh(): return False - # TODO: Likely move this to _refresh_area() - # Go through groups and and add each to buffer - """ - if self._core.current_group is not None: - buffer = Image.new("RGBA", (self._core.width, self._core.height)) - # Recursively have everything draw to the image - self._core.current_group._fill_area( - buffer - ) # pylint: disable=protected-access - # save image to buffer (or probably refresh buffer so we can compare) - self._buffer.paste(buffer) - """ areas_to_refresh = self._get_refresh_areas() - for area in areas_to_refresh: self._refresh_area(area) @@ -404,39 +385,22 @@ class Display: self._core.end_transaction() return True - def _apply_rotation(self, rectangle): - """Adjust the rectangle coordinates based on rotation""" - if self._core.rotation == 90: - return RectangleStruct( - self._core.height - rectangle.y2, - rectangle.x1, - self._core.height - rectangle.y1, - rectangle.x2, - ) - if self._core.rotation == 180: - return RectangleStruct( - self._core.width - rectangle.x2, - self._core.height - rectangle.y2, - self._core.width - rectangle.x1, - self._core.height - rectangle.y1, - ) - if self._core.rotation == 270: - return RectangleStruct( - rectangle.y1, - self._core.width - rectangle.x2, - rectangle.y2, - self._core.width - rectangle.x1, - ) - return rectangle - - def fill_row( - self, y: int, buffer: circuitpython_typing.WriteableBuffer - ) -> circuitpython_typing.WriteableBuffer: + def fill_row(self, y: int, buffer: WriteableBuffer) -> WriteableBuffer: """Extract the pixels from a single row""" - for x in range(0, self._core.width): - _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y))) - buffer[x * 2] = (_rgb_565 >> 8) & 0xFF - buffer[x * 2 + 1] = _rgb_565 & 0xFF + if self._core.colorspace.depth != 16: + raise ValueError("Display must have a 16 bit colorspace.") + + area = Area(0, y, self._core.width, y + 1) + pixels_per_word = (struct.calcsize("I") * 8) // self._core.colorspace.depth + buffer_size = self._core.width // pixels_per_word + pixels_per_buffer = area.size() + if pixels_per_buffer % pixels_per_word: + buffer_size += 1 + + buffer = bytearray([0] * (buffer_size * struct.calcsize("I"))) + mask_length = (pixels_per_buffer // 32) + 1 + mask = array("L", [0x00000000] * mask_length) + self._core.fill_area(area, mask, buffer) return buffer def release(self) -> None: @@ -460,10 +424,7 @@ class Display: @property def brightness(self) -> float: - """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`. - When `auto_brightness` is True, the value of `brightness` will change automatically. - If `brightness` is set, `auto_brightness` will be disabled and will be set to False. - """ + """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.""" return self._brightness @brightness.setter @@ -498,19 +459,6 @@ class Display: else: raise ValueError("Brightness must be between 0.0 and 1.0") - @property - def auto_brightness(self) -> bool: - """True when the display brightness is adjusted automatically, based on an ambient - light sensor or other method. Note that some displays may have this set to True by - default, but not actually implement automatic brightness adjustment. - `auto_brightness` is set to False if `brightness` is set manually. - """ - return self._auto_brightness - - @auto_brightness.setter - def auto_brightness(self, value: bool): - self._auto_brightness = value - @property def width(self) -> int: """Display Width""" diff --git a/displayio/_displaycore.py b/displayio/_displaycore.py index 8db49e0..9bbb665 100644 --- a/displayio/_displaycore.py +++ b/displayio/_displaycore.py @@ -24,7 +24,7 @@ __repo__ = "https://github.com/adafruit/Adafruit_Blinka_Displayio.git" import time import struct -import circuitpython_typing +from circuitpython_typing import WriteableBuffer, ReadableBuffer from paralleldisplay import ParallelBus from ._fourwire import FourWire from ._group import Group @@ -248,8 +248,8 @@ class _DisplayCore: def fill_area( self, area: Area, - mask: circuitpython_typing.WriteableBuffer, - buffer: circuitpython_typing.WriteableBuffer, + mask: WriteableBuffer, + buffer: WriteableBuffer, ) -> bool: """Call the current group's fill area function""" if self.current_group is not None: @@ -400,7 +400,7 @@ class _DisplayCore: self, data_type: int, chip_select: int, - data: circuitpython_typing.ReadableBuffer, + data: ReadableBuffer, ) -> None: """ Send the data to the current bus diff --git a/displayio/_epaperdisplay.py b/displayio/_epaperdisplay.py index 686f8bb..114506c 100644 --- a/displayio/_epaperdisplay.py +++ b/displayio/_epaperdisplay.py @@ -19,7 +19,7 @@ displayio for Blinka from typing import Optional import microcontroller -import circuitpython_typing +from circuitpython_typing import ReadableBuffer from ._group import Group from ._displaybus import _DisplayBus @@ -42,8 +42,8 @@ class EPaperDisplay: def __init__( self, display_bus: _DisplayBus, - start_sequence: circuitpython_typing.ReadableBuffer, - stop_sequence: circuitpython_typing.ReadableBuffer, + start_sequence: ReadableBuffer, + stop_sequence: ReadableBuffer, *, width: int, height: int, @@ -84,9 +84,9 @@ class EPaperDisplay: :param display_bus: The bus that the display is connected to :type _DisplayBus: displayio.FourWire or displayio.ParallelBus - :param ~circuitpython_typing.ReadableBuffer start_sequence: Byte-packed + :param ~ReadableBuffer start_sequence: Byte-packed initialization sequence. - :param ~circuitpython_typing.ReadableBuffer stop_sequence: Byte-packed + :param ~ReadableBuffer stop_sequence: Byte-packed initialization sequence. :param int width: Width in pixels :param int height: Height in pixels diff --git a/displayio/_fourwire.py b/displayio/_fourwire.py index 31b1be5..142e308 100644 --- a/displayio/_fourwire.py +++ b/displayio/_fourwire.py @@ -22,7 +22,7 @@ from typing import Optional import digitalio import busio import microcontroller -import circuitpython_typing +from circuitpython_typing import ReadableBuffer from ._constants import ( CHIP_SELECT_TOGGLE_EVERY_BYTE, CHIP_SELECT_UNTOUCHED, @@ -95,7 +95,7 @@ class FourWire: def send( self, command, - data: circuitpython_typing.ReadableBuffer, + data: ReadableBuffer, *, toggle_every_byte: bool = False, ) -> None: @@ -120,7 +120,7 @@ class FourWire: self, data_type: int, chip_select: int, - data: circuitpython_typing.ReadableBuffer, + data: ReadableBuffer, ): self._dc.value = data_type == DISPLAY_DATA if chip_select == CHIP_SELECT_TOGGLE_EVERY_BYTE: diff --git a/displayio/_group.py b/displayio/_group.py index ed90f8e..833b95c 100644 --- a/displayio/_group.py +++ b/displayio/_group.py @@ -19,7 +19,7 @@ displayio for Blinka from __future__ import annotations from typing import Union, Callable -import circuitpython_typing +from circuitpython_typing import WriteableBuffer from ._structs import TransformStruct from ._tilegrid import TileGrid from ._colorspace import Colorspace @@ -149,8 +149,8 @@ class Group: self, colorspace: Colorspace, area: Area, - mask: circuitpython_typing.WriteableBuffer, - buffer: circuitpython_typing.WriteableBuffer, + mask: WriteableBuffer, + buffer: WriteableBuffer, ) -> bool: if self._hidden_group: return False diff --git a/displayio/_i2cdisplay.py b/displayio/_i2cdisplay.py index 67022f5..e37eb8a 100644 --- a/displayio/_i2cdisplay.py +++ b/displayio/_i2cdisplay.py @@ -23,7 +23,7 @@ displayio for Blinka import time import busio import digitalio -import circuitpython_typing +from circuitpython_typing import ReadableBuffer from ._constants import CHIP_SELECT_UNTOUCHED, DISPLAY_COMMAND __version__ = "0.0.0+auto.0" @@ -85,7 +85,7 @@ class I2CDisplay: while not self._i2c.try_lock(): pass - def send(self, command: int, data: circuitpython_typing.ReadableBuffer) -> None: + def send(self, command: int, data: ReadableBuffer) -> None: """ Sends the given command value followed by the full set of data. Display state, such as vertical scroll, set via ``send`` may or may not be reset once the code is @@ -99,7 +99,7 @@ class I2CDisplay: self, data_type: int, _chip_select: int, # Chip select behavior - data: circuitpython_typing.ReadableBuffer, + data: ReadableBuffer, ): if data_type == DISPLAY_COMMAND: n = len(data) diff --git a/displayio/_palette.py b/displayio/_palette.py index af72dcb..63d98ee 100644 --- a/displayio/_palette.py +++ b/displayio/_palette.py @@ -18,7 +18,7 @@ displayio for Blinka """ from typing import Optional, Union, Tuple -import circuitpython_typing +from circuitpython_typing import ReadableBuffer from ._colorconverter import ColorConverter from ._colorspace import Colorspace from ._structs import InputPixelStruct, OutputPixelStruct, ColorStruct @@ -63,7 +63,7 @@ class Palette: def __setitem__( self, index: int, - value: Union[int, circuitpython_typing.ReadableBuffer, Tuple[int, int, int]], + value: Union[int, ReadableBuffer, Tuple[int, int, int]], ) -> None: """Sets the pixel color at the given index. The index should be an integer in the range 0 to color_count-1. diff --git a/displayio/_structs.py b/displayio/_structs.py index a46fb48..3f0c2e1 100644 --- a/displayio/_structs.py +++ b/displayio/_structs.py @@ -23,16 +23,6 @@ __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_Blinka_Displayio.git" -@dataclass -class RectangleStruct: - # pylint: disable=invalid-name - """Rectangle Struct Dataclass. To eventually be replaced by Area.""" - x1: int - y1: int - x2: int - y2: int - - @dataclass class TransformStruct: # pylint: disable=invalid-name diff --git a/displayio/_tilegrid.py b/displayio/_tilegrid.py index ec5e45f..cd27662 100644 --- a/displayio/_tilegrid.py +++ b/displayio/_tilegrid.py @@ -19,7 +19,7 @@ displayio for Blinka import struct from typing import Union, Optional, Tuple -import circuitpython_typing +from circuitpython_typing import WriteableBuffer from ._bitmap import Bitmap from ._colorconverter import ColorConverter from ._ondiskbitmap import OnDiskBitmap @@ -215,8 +215,8 @@ class TileGrid: self, colorspace: Colorspace, area: Area, - mask: circuitpython_typing.WriteableBuffer, - buffer: circuitpython_typing.WriteableBuffer, + mask: WriteableBuffer, + buffer: WriteableBuffer, ) -> bool: """Draw onto the image""" # pylint: disable=too-many-locals,too-many-branches,too-many-statements @@ -335,16 +335,18 @@ class TileGrid: else: mask[offset // 32] |= 1 << (offset % 32) if colorspace.depth == 16: - buffer = ( - buffer[:offset] - + struct.pack("H", output_pixel.pixel) - + buffer[offset + 2 :] + struct.pack_into( + "H", + buffer, + offset, + output_pixel.pixel, ) elif colorspace.depth == 32: - buffer = ( - buffer[:offset] - + struct.pack("I", output_pixel.pixel) - + buffer[offset + 4 :] + struct.pack_into( + "I", + buffer, + offset, + output_pixel.pixel, ) elif colorspace.depth == 8: buffer[offset] = output_pixel.pixel & 0xFF -- 2.49.0