"""
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
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:
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
):
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:
"""
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:
"""
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
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,
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)
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,
def __init__(
self,
display_bus: _DisplayBus,
- init_sequence: circuitpython_typing.ReadableBuffer,
+ init_sequence: ReadableBuffer,
*,
width: int,
height: int,
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`).
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
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)
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:
@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
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"""
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
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:
self,
data_type: int,
chip_select: int,
- data: circuitpython_typing.ReadableBuffer,
+ data: ReadableBuffer,
) -> None:
"""
Send the data to the current bus
from typing import Optional
import microcontroller
-import circuitpython_typing
+from circuitpython_typing import ReadableBuffer
from ._group import Group
from ._displaybus import _DisplayBus
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,
: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
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,
def send(
self,
command,
- data: circuitpython_typing.ReadableBuffer,
+ data: ReadableBuffer,
*,
toggle_every_byte: bool = False,
) -> None:
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:
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
self,
colorspace: Colorspace,
area: Area,
- mask: circuitpython_typing.WriteableBuffer,
- buffer: circuitpython_typing.WriteableBuffer,
+ mask: WriteableBuffer,
+ buffer: WriteableBuffer,
) -> bool:
if self._hidden_group:
return False
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"
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
self,
data_type: int,
_chip_select: int, # Chip select behavior
- data: circuitpython_typing.ReadableBuffer,
+ data: ReadableBuffer,
):
if data_type == DISPLAY_COMMAND:
n = len(data)
"""
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
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.
__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
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
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
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