def _background():
"""Main thread function to loop through all displays and update them"""
for display in displays:
- display._background() # pylint: disable=protected-access
+ display.background()
def release_displays() -> None:
initialization so the display is active as long as possible.
"""
for display in displays:
- display._release() # pylint: disable=protected-access
+ display.release()
displays.clear()
for display_bus in display_buses:
- display_bus._release() # pylint: disable=protected-access
+ display_bus.deinit()
display_buses.clear()
class Area:
- # pylint: disable=invalid-name,missing-function-docstring
- """Area Class to represent an area to be updated. Currently not used."""
+ """Area Class to represent an area to be updated."""
+ # pylint: disable=invalid-name
def __init__(self, x1: int = 0, y1: int = 0, x2: int = 0, y2: int = 0):
self.x1 = x1
self.y1 = y1
self.x2 += dx
self.y2 += dy
- def _compute_overlap(self, other, overlap) -> bool:
+ def compute_overlap(self, other, overlap) -> bool:
+ """Compute the overlap between two areas. Returns True if there is an overlap."""
a = self
overlap.x1 = max(a.x1, other.x1)
overlap.x2 = min(a.x2, other.x2)
self.y1, self.y2 = self.y2, self.y1
def _union(self, other, union):
- # pylint: disable=protected-access
if self._empty():
self._copy_into(union)
return
- if other._empty():
- other._copy_into(union)
+ if other._empty(): # pylint: disable=protected-access
+ other._copy_into(union) # pylint: disable=protected-access
return
union.x1 = min(self.x1, other.x1)
union.y2 = max(self.y2, other.y2)
def width(self) -> int:
+ """Return the width of the area."""
return self.x2 - self.x1
def height(self) -> int:
+ """Return the height of the area."""
return self.y2 - self.y1
def size(self) -> int:
+ """Return the size of the area."""
return self.width() * self.height()
def __eq__(self, other):
)
@staticmethod
- def _transform_within(
+ def transform_within(
mirror_x: bool,
mirror_y: bool,
transpose_xy: bool,
whole: Area,
transformed: Area,
):
+ """Transform an area within a larger area."""
# pylint: disable=too-many-arguments
# Original and whole must be in the same coordinate space.
if mirror_x:
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
from ._colorspace import Colorspace
-from ._structs import ColorspaceStruct
+from ._structs import ColorspaceStruct, InputPixelStruct, OutputPixelStruct
+from ._helpers import clamp, bswap16
class ColorConverter:
self._cached_output_color = None
@staticmethod
- def _clamp(value, min_value, max_value):
- return max(min(max_value, value), min_value)
-
- @staticmethod
- def _bswap16(value):
- # ABCD -> 00DC
- return (value & 0xFF00) >> 8 | (value & 0x00FF) << 8
-
- def _dither_noise_1(self, noise):
+ def _dither_noise_1(noise):
noise = (noise >> 13) ^ noise
more_noise = (
noise * (noise * noise * 60493 + 19990303) + 1376312589
) & 0x7FFFFFFF
- return self._clamp(int((more_noise / (1073741824.0 * 2)) * 255), 0, 0xFFFFFFFF)
+ return clamp(int((more_noise / (1073741824.0 * 2)) * 255), 0, 0xFFFFFFFF)
- def _dither_noise_2(self, x, y):
- return self._dither_noise_1(x + y * 0xFFFF)
+ @staticmethod
+ def _dither_noise_2(x, y):
+ return ColorConverter._dither_noise_1(x + y * 0xFFFF)
@staticmethod
def _compute_rgb565(color_rgb888: int):
return hue
- def _compute_sevencolor(self, color_rgb888: int):
+ @staticmethod
+ def _compute_sevencolor(color_rgb888: int):
# pylint: disable=too-many-return-statements
- chroma = self._compute_chroma(color_rgb888)
+ chroma = ColorConverter._compute_chroma(color_rgb888)
if chroma >= 64:
- hue = self._compute_hue(color_rgb888)
+ hue = ColorConverter._compute_hue(color_rgb888)
# Red 0
if hue < 10:
return 0x4
return 0x3
# The rest is red to 255
return 0x4
- luma = self._compute_luma(color_rgb888)
+ luma = ColorConverter._compute_luma(color_rgb888)
if luma >= 128:
return 0x1 # White
return 0x0 # Black
else:
raise ValueError("Color must be an integer or 3 or 4 value tuple")
- input_pixel = {
- "pixel": color,
- "x": 0,
- "y": 0,
- "tile": 0,
- "tile_x": 0,
- "tile_y": 0,
- }
+ input_pixel = InputPixelStruct(color)
+ output_pixel = OutputPixelStruct()
- output_pixel = {"pixel": 0, "opaque": False}
+ self._convert(self._output_colorspace, input_pixel, output_pixel)
- if input_pixel["pixel"] == self._transparent_color:
- return output_pixel["pixel"]
+ return output_pixel.pixel
+
+ def _convert(
+ self,
+ colorspace: Colorspace,
+ input_pixel: InputPixelStruct,
+ output_color: OutputPixelStruct,
+ ) -> None:
+ pixel = input_pixel.pixel
+
+ if self._transparent_color == pixel:
+ output_color.opaque = False
+ return
if (
not self._dither
and self._cached_colorspace == self._output_colorspace
- and self._cached_input_pixel == input_pixel["pixel"]
+ and self._cached_input_pixel == input_pixel.pixel
):
- return self._cached_output_color
+ output_color = self._cached_output_color
+ return
rgb888_pixel = input_pixel
- rgb888_pixel["pixel"] = self._convert_pixel(
- self._input_colorspace, input_pixel["pixel"]
- )
- self._convert_color(
- self._output_colorspace, self._dither, rgb888_pixel, output_pixel
+ rgb888_pixel.pixel = self._convert_pixel(
+ self._input_colorspace, input_pixel.pixel
)
+ self._convert_color(colorspace, self._dither, rgb888_pixel, output_color)
if not self._dither:
- self._cached_colorspace = self._output_colorspace
- self._cached_input_pixel = input_pixel["pixel"]
- self._cached_output_color = output_pixel["pixel"]
-
- return output_pixel["pixel"]
+ self._cached_colorspace = colorspace
+ self._cached_input_pixel = input_pixel.pixel
+ self._cached_output_color = output_color.pixel
- def _convert_pixel(self, colorspace: Colorspace, pixel: int) -> int:
- pixel = self._clamp(pixel, 0, 0xFFFFFFFF)
+ @staticmethod
+ def _convert_pixel(colorspace: Colorspace, pixel: int) -> int:
+ pixel = clamp(pixel, 0, 0xFFFFFFFF)
if colorspace in (
Colorspace.RGB565_SWAPPED,
Colorspace.RGB555_SWAPPED,
Colorspace.BGR565_SWAPPED,
Colorspace.BGR555_SWAPPED,
):
- pixel = self._bswap16(pixel)
+ pixel = bswap16(pixel)
if colorspace in (Colorspace.RGB565, Colorspace.RGB565_SWAPPED):
red8 = (pixel >> 11) << 3
grn8 = ((pixel >> 5) << 2) & 0xFF
return (pixel & 0xFF) & 0x01010101
return pixel
+ @staticmethod
def _convert_color(
- self,
colorspace: ColorspaceStruct,
dither: bool,
- input_pixel: dict,
- output_color: dict,
+ input_pixel: InputPixelStruct,
+ output_color: OutputPixelStruct,
) -> None:
# pylint: disable=too-many-return-statements, too-many-branches, too-many-statements
- pixel = input_pixel["pixel"]
+ pixel = input_pixel.pixel
if dither:
- rand_red = self._dither_noise_2(input_pixel["x"], input_pixel["y"])
- rand_grn = self._dither_noise_2(input_pixel["x"] + 33, input_pixel["y"])
- rand_blu = self._dither_noise_2(input_pixel["x"], input_pixel["y"] + 33)
+ rand_red = ColorConverter._dither_noise_2(input_pixel.x, input_pixel.y)
+ rand_grn = ColorConverter._dither_noise_2(input_pixel.x + 33, input_pixel.y)
+ rand_blu = ColorConverter._dither_noise_2(input_pixel.x, input_pixel.y + 33)
red8 = pixel >> 16
grn8 = (pixel >> 8) & 0xFF
pixel = (red8 << 16) | (grn8 << 8) | blu8
if colorspace.depth == 16:
- packed = self._compute_rgb565(pixel)
+ packed = ColorConverter._compute_rgb565(pixel)
if colorspace.reverse_bytes_in_word:
- packed = self._bswap16(packed)
- output_color["pixel"] = packed
- output_color["opaque"] = True
+ packed = bswap16(packed)
+ output_color.pixel = packed
+ output_color.opaque = True
return
if colorspace.tricolor:
- output_color["pixel"] = self._compute_luma(pixel) >> (8 - colorspace.depth)
- if self._compute_chroma(pixel) <= 16:
+ output_color.pixel = ColorConverter._compute_luma(pixel) >> (
+ 8 - colorspace.depth
+ )
+ if ColorConverter._compute_chroma(pixel) <= 16:
if not colorspace.grayscale:
- output_color["pixel"] = 0
- output_color["opaque"] = True
+ output_color.pixel = 0
+ output_color.opaque = True
return
- pixel_hue = self._compute_hue(pixel)
- output_color["pixel"] = self._compute_tricolor(
- colorspace, pixel_hue, output_color["pixel"]
+ pixel_hue = ColorConverter._compute_hue(pixel)
+ output_color.pixel = ColorConverter._compute_tricolor(
+ colorspace, pixel_hue, output_color.pixel
)
return
if colorspace.grayscale and colorspace.depth <= 8:
bitmask = (1 << colorspace.depth) - 1
- output_color["pixel"] = (
- self._compute_luma(pixel) >> colorspace.grayscale_bit
+ output_color.pixel = (
+ ColorConverter._compute_luma(pixel) >> colorspace.grayscale_bit
) & bitmask
- output_color["opaque"] = True
+ output_color.opaque = True
return
if colorspace.depth == 32:
- output_color["pixel"] = pixel
- output_color["opaque"] = True
+ output_color.pixel = pixel
+ output_color.opaque = True
return
if colorspace.depth == 8 and colorspace.grayscale:
- packed = self._compute_rgb332(pixel)
- output_color["pixel"] = packed
- output_color["opaque"] = True
+ packed = ColorConverter._compute_rgb332(pixel)
+ output_color.pixel = packed
+ output_color.opaque = True
return
if colorspace.depth == 4:
if colorspace.sevencolor:
- packed = self._compute_sevencolor(pixel)
+ packed = ColorConverter._compute_sevencolor(pixel)
else:
- packed = self._compute_rgbd(pixel)
- output_color["pixel"] = packed
- output_color["opaque"] = True
+ packed = ColorConverter._compute_rgbd(pixel)
+ output_color.pixel = packed
+ output_color.opaque = True
return
- output_color["opaque"] = False
+ output_color.opaque = False
def make_transparent(self, color: int) -> None:
"""Set the transparent color or index for the ColorConverter. This will
import time
import struct
from typing import Optional
-from dataclasses import astuple
import digitalio
from PIL import Image
-import numpy
import microcontroller
import circuitpython_typing
from ._displaycore import _DisplayCore
from ._colorconverter import ColorConverter
from ._group import Group
from ._structs import RectangleStruct
+from ._area import Area
from ._constants import (
CHIP_SELECT_TOGGLE_EVERY_BYTE,
CHIP_SELECT_UNTOUCHED,
self._initialize(init_sequence)
self._buffer = Image.new("RGB", (width, height))
- self._subrectangles = []
- self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
self._current_group = None
self._last_refresh_call = 0
self._refresh_thread = None
time.sleep(delay_time_ms / 1000)
i += 2 + data_size
- def _send(self, command, data):
- self._core.begin_transaction()
- if self._core.data_as_commands:
- self._core.send(
- DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command]) + data
- )
- else:
- self._core.send(
- DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
- )
- self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
- self._core.end_transaction()
-
- def _send_pixels(self, data):
+ def _send_pixels(self, pixels):
if not self._core.data_as_commands:
self._core.send(
DISPLAY_COMMAND,
CHIP_SELECT_TOGGLE_EVERY_BYTE,
bytes([self._write_ram_command]),
)
- self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
+ self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels)
def show(self, group: Group) -> None:
"""Switches to displaying the given group of layers. When group is None, the
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))
# save image to buffer (or probably refresh buffer so we can compare)
self._buffer.paste(buffer)
- self._subrectangles = self._core.get_refresh_areas()
+ areas_to_refresh = self._get_refresh_areas()
- for area in self._subrectangles:
- self._refresh_display_area(area)
+ for area in areas_to_refresh:
+ self._refresh_area(area)
self._core.finish_refresh()
return True
- def _background(self):
+ def _get_refresh_areas(self) -> list[Area]:
+ """Get a list of areas to be refreshed"""
+ areas = []
+ if self._core.current_group is not None:
+ # Eventually calculate dirty rectangles here
+ areas.append(Area(0, 0, self._core.width, self._core.height))
+ return areas
+
+ def background(self):
+ """Run background refresh tasks. Do not call directly"""
if (
self._auto_refresh
and (time.monotonic() * 1000 - self._core.last_refresh)
):
self.refresh()
- def _refresh_display_area(self, rectangle):
- """Loop through dirty rectangles and redraw that area."""
- img = self._buffer.convert("RGB").crop(astuple(rectangle))
- img = img.rotate(360 - self._core.rotation, expand=True)
-
- display_rectangle = self._apply_rotation(rectangle)
- img = img.crop(astuple(self._clip(display_rectangle)))
-
- data = numpy.array(img).astype("uint16")
- color = (
- ((data[:, :, 0] & 0xF8) << 8)
- | ((data[:, :, 1] & 0xFC) << 3)
- | (data[:, :, 2] >> 3)
- )
-
- pixels = bytes(
- numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
- )
-
- self._send(
- self._core.column_command,
- self._encode_pos(
- display_rectangle.x1 + self._core.colstart,
- display_rectangle.x2 + self._core.colstart - 1,
- ),
- )
- self._send(
- self._core.row_command,
- self._encode_pos(
- display_rectangle.y1 + self._core.rowstart,
- display_rectangle.y2 + self._core.rowstart - 1,
- ),
- )
-
- self._core.begin_transaction()
- self._send_pixels(pixels)
- self._core.end_transaction()
-
- def _clip(self, rectangle):
- if self._core.rotation in (90, 270):
- width, height = self._core.height, self._core.width
- else:
- width, height = self._core.width, self._core.height
+ def _refresh_area(self, area) -> bool:
+ """Loop through dirty areas and redraw that area."""
+ # pylint: disable=too-many-locals
+ buffer_size = 128
+
+ clipped = Area()
+ if not self._core.clip_area(area, clipped):
+ return True
+
+ rows_per_buffer = clipped.height()
+ pixels_per_word = (struct.calcsize("I") * 8) // self._core.colorspace.depth
+ pixels_per_buffer = clipped.size()
+
+ subrectangles = 1
+
+ if self._core.sh1107_addressing:
+ subrectangles = rows_per_buffer // 8
+ rows_per_buffer = 8
+ elif clipped.size() > buffer_size * pixels_per_word:
+ rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
+ if rows_per_buffer == 0:
+ rows_per_buffer = 1
+ if (
+ self._core.colorspace.depth < 8
+ and self._core.colorspace.pixels_in_byte_share_row
+ ):
+ pixels_per_byte = 8 // self._core.colorspace.depth
+ if rows_per_buffer % pixels_per_byte != 0:
+ rows_per_buffer -= rows_per_buffer % pixels_per_byte
+ subrectangles = clipped.height() // rows_per_buffer
+ if clipped.height() % rows_per_buffer != 0:
+ subrectangles += 1
+ pixels_per_buffer = rows_per_buffer * clipped.width()
+ buffer_size = pixels_per_buffer // pixels_per_word
+ if pixels_per_buffer % pixels_per_word:
+ buffer_size += 1
+
+ buffer = bytearray(buffer_size)
+ mask_length = (pixels_per_buffer // 32) + 1
+ mask = bytearray(mask_length)
+ remaining_rows = clipped.height()
+
+ for subrect_index in range(subrectangles):
+ subrectangle = Area(
+ clipped.x1,
+ clipped.y1 + rows_per_buffer * subrect_index,
+ clipped.x2,
+ clipped.y1 + rows_per_buffer * (subrect_index + 1),
+ )
+ if remaining_rows < rows_per_buffer:
+ subrectangle.y2 = subrectangle.y1 + remaining_rows
+ self._core.set_region_to_update(subrectangle)
+ if self._core.colorspace.depth >= 8:
+ subrectangle_size_bytes = subrectangle.size() * (
+ self._core.colorspace.depth // 8
+ )
+ else:
+ subrectangle_size_bytes = subrectangle.size() // (
+ 8 // self._core.colorspace.depth
+ )
- rectangle.x1 = max(rectangle.x1, 0)
- rectangle.y1 = max(rectangle.y1, 0)
- rectangle.x2 = min(rectangle.x2, width)
- rectangle.y2 = min(rectangle.y2, height)
+ self._core.fill_area(subrectangle, mask, buffer)
- return rectangle
+ self._core.begin_transaction()
+ self._send_pixels(buffer[:subrectangle_size_bytes])
+ self._core.end_transaction()
+ return True
def _apply_rotation(self, rectangle):
"""Adjust the rectangle coordinates based on rotation"""
)
return rectangle
- def _encode_pos(self, x, y):
- """Encode a postion into bytes."""
- return struct.pack(self._bounds_encoding, x, y) # pylint: disable=no-member
-
def fill_row(
self, y: int, buffer: circuitpython_typing.WriteableBuffer
) -> circuitpython_typing.WriteableBuffer:
buffer[x * 2 + 1] = _rgb_565 & 0xFF
return buffer
+ def release(self) -> None:
+ """Release the display and free its resources"""
+ self.auto_refresh = False
+ self._core.release_display_core()
+
+ def reset(self) -> None:
+ """Reset the display"""
+ self.auto_refresh = True
+
@property
def auto_refresh(self) -> bool:
"""True when the display is refreshed automatically."""
@auto_refresh.setter
def auto_refresh(self, value: bool):
+ self._first_manual_refresh = not value
self._auto_refresh = value
@property
@brightness.setter
def brightness(self, value: float):
if 0 <= float(value) <= 1.0:
- self._brightness = value
- if self._backlight_type == BACKLIGHT_IN_OUT:
- self._backlight.value = round(self._brightness)
- elif self._backlight_type == BACKLIGHT_PWM:
- self._backlight.duty_cycle = self._brightness * 65535
+ if not self._backlight_on_high:
+ value = 1.0 - value
+
+ if self._backlight_type == BACKLIGHT_PWM:
+ self._backlight.duty_cycle = value * 0xFFFF
+ elif self._backlight_type == BACKLIGHT_IN_OUT:
+ self._backlight.value = value > 0.99
elif self._brightness_command is not None:
- self._send(self._brightness_command, round(value * 255))
+ self._core.begin_transaction()
+ if self._core.data_as_commands:
+ self._core.send(
+ DISPLAY_COMMAND,
+ CHIP_SELECT_TOGGLE_EVERY_BYTE,
+ bytes([self._brightness_command, 0xFF * value]),
+ )
+ else:
+ self._core.send(
+ DISPLAY_COMMAND,
+ CHIP_SELECT_TOGGLE_EVERY_BYTE,
+ bytes([self._brightness_command]),
+ )
+ self._core.send(
+ DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
+ )
+ self._core.end_transaction()
+ self._brightness = value
else:
raise ValueError("Brightness must be between 0.0 and 1.0")
import time
+import struct
import circuitpython_typing
from paralleldisplay import ParallelBus
from ._fourwire import FourWire
from ._group import Group
from ._i2cdisplay import I2CDisplay
-from ._structs import ColorspaceStruct, TransformStruct, RectangleStruct
+from ._structs import ColorspaceStruct, TransformStruct
from ._area import Area
from ._displaybus import _DisplayBus
+from ._helpers import bswap16
+from ._constants import (
+ CHIP_SELECT_UNTOUCHED,
+ CHIP_SELECT_TOGGLE_EVERY_BYTE,
+ DISPLAY_COMMAND,
+ DISPLAY_DATA,
+ NO_COMMAND,
+)
class _DisplayCore:
- # pylint: disable=too-many-arguments, too-many-instance-attributes, too-many-locals
+ # pylint: disable=too-many-arguments, too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements
def __init__(
self,
self.refresh_in_progress = False
self.last_refresh = time.monotonic() * 1000
- def get_refresh_areas(self) -> list:
- """Get a list of areas to be refreshed"""
- subrectangles = []
- if self.current_group is not None:
- # Eventually calculate dirty rectangles here
- subrectangles.append(RectangleStruct(0, 0, self.width, self.height))
- return subrectangles
-
- def release(self) -> None:
+ def release_display_core(self) -> None:
"""Release the display from the current group"""
# pylint: disable=protected-access
mask: circuitpython_typing.WriteableBuffer,
buffer: circuitpython_typing.WriteableBuffer,
) -> bool:
- # pylint: disable=protected-access
"""Call the current group's fill area function"""
+ if self.current_group is not None:
+ return self.current_group._fill_area( # pylint: disable=protected-access
+ self.colorspace, area, mask, buffer
+ )
+ return False
- return self.current_group._fill_area(self.colorspace, area, mask, buffer)
+ """
+ def _clip(self, rectangle):
+ if self._core.rotation in (90, 270):
+ width, height = self._core.height, self._core.width
+ else:
+ width, height = self._core.width, self._core.height
+
+ rectangle.x1 = max(rectangle.x1, 0)
+ rectangle.y1 = max(rectangle.y1, 0)
+ rectangle.x2 = min(rectangle.x2, width)
+ rectangle.y2 = min(rectangle.y2, height)
+
+ return rectangle
+ """
def clip_area(self, area: Area, clipped: Area) -> bool:
"""Shrink the area to the region shared by the two areas"""
- # pylint: disable=protected-access
- overlaps = self.area._compute_overlap(area, clipped)
+ overlaps = self.area.compute_overlap(area, clipped)
if not overlaps:
return False
return True
+ def set_region_to_update(self, area: Area) -> None:
+ """Set the region to update"""
+ region_x1 = area.x1 + self.colstart
+ region_x2 = area.x2 + self.colstart
+ region_y1 = area.y1 + self.rowstart
+ region_y2 = area.y2 + self.rowstart
+
+ if self.colorspace.depth < 8:
+ pixels_per_byte = 8 // self.colorspace.depth
+ if self.colorspace.pixels_in_byte_share_row:
+ region_x1 /= pixels_per_byte * self.colorspace.bytes_per_cell
+ region_x2 /= pixels_per_byte * self.colorspace.bytes_per_cell
+ else:
+ region_y1 /= pixels_per_byte * self.colorspace.bytes_per_cell
+ region_y2 /= pixels_per_byte * self.colorspace.bytes_per_cell
+
+ region_x2 -= 1
+ region_y2 -= 1
+
+ chip_select = CHIP_SELECT_UNTOUCHED
+ if self.always_toggle_chip_select or self.data_as_commands:
+ chip_select = CHIP_SELECT_TOGGLE_EVERY_BYTE
+
+ # Set column
+ self.begin_transaction()
+ data_type = DISPLAY_DATA
+ if not self.data_as_commands:
+ self.send(
+ DISPLAY_COMMAND, CHIP_SELECT_UNTOUCHED, bytes([self.column_command])
+ )
+ else:
+ data_type = DISPLAY_COMMAND
+ """
+ self._core.send(
+ DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command]) + data
+ )
+ self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
+ """
+
+ if self.ram_width < 0x100: # Single Byte Bounds
+ data = struct.pack(">BB", region_x1, region_x2)
+ else:
+ if self.address_little_endian:
+ region_x1 = bswap16(region_x1)
+ region_x2 = bswap16(region_x2)
+ data = struct.pack(">HH", region_x1, region_x2)
+
+ # Quirk for SH1107 "SH1107_addressing"
+ # Column lower command = 0x00, Column upper command = 0x10
+ if self.sh1107_addressing:
+ data = struct.pack(
+ ">BB",
+ ((region_x1 >> 4) & 0xF0) | 0x10, # 0x10 to 0x17
+ region_x1 & 0x0F, # 0x00 to 0x0F
+ )
+
+ self.send(data_type, chip_select, data)
+ self.end_transaction()
+
+ if self.set_current_column_command != NO_COMMAND:
+ self.begin_transaction()
+ self.send(
+ DISPLAY_COMMAND, chip_select, bytes([self.set_current_column_command])
+ )
+ # Only send the first half of data because it is the first coordinate.
+ self.send(DISPLAY_DATA, chip_select, data[: len(data) // 2])
+ self.end_transaction()
+
+ # Set row
+ self.begin_transaction()
+
+ if not self.data_as_commands:
+ self.send(DISPLAY_COMMAND, CHIP_SELECT_UNTOUCHED, bytes([self.row_command]))
+
+ if self.ram_width < 0x100: # Single Byte Bounds
+ data = struct.pack(">BB", region_y1, region_y2)
+ else:
+ if self.address_little_endian:
+ region_y1 = bswap16(region_y1)
+ region_y2 = bswap16(region_y2)
+ data = struct.pack(">HH", region_y1, region_y2)
+
+ # Quirk for SH1107 "SH1107_addressing"
+ # Page address command = 0xB0
+ if self.sh1107_addressing:
+ data = struct.pack(">B", 0xB0 | region_y1)
+
+ self.send(data_type, chip_select, data)
+ self.end_transaction()
+
+ if self.set_current_row_command != NO_COMMAND:
+ self.begin_transaction()
+ self.send(
+ DISPLAY_COMMAND, chip_select, bytes([self.set_current_row_command])
+ )
+ # Only send the first half of data because it is the first coordinate.
+ self.send(DISPLAY_DATA, chip_select, data[: len(data) // 2])
+ self.end_transaction()
+
+ """
+ img = self._buffer.convert("RGB").crop(astuple(area))
+ img = img.rotate(360 - self._core.rotation, expand=True)
+
+ display_area = self._apply_rotation(area)
+
+ img = img.crop(astuple(display_area))
+
+ data = numpy.array(img).astype("uint16")
+ color = (
+ ((data[:, :, 0] & 0xF8) << 8)
+ | ((data[:, :, 1] & 0xFC) << 3)
+ | (data[:, :, 2] >> 3)
+ )
+
+ pixels = bytes(
+ numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
+ )
+ """
+
def send(
self,
data_type: int,
from typing import Union, Callable
from ._structs import TransformStruct
from ._tilegrid import TileGrid
+from ._colorspace import Colorspace
+from ._area import Area
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
"""Deletes the value at the given index."""
del self._layers[index]
- def _fill_area(self, buffer):
+ def _fill_area(
+ self, colorspace: Colorspace, area: Area, mask: int, buffer: bytearray
+ ) -> bool:
if self._hidden_group:
- return
+ return False
for layer in self._layers:
if isinstance(layer, (Group, TileGrid)):
- layer._fill_area(buffer) # pylint: disable=protected-access
+ if layer._fill_area( # pylint: disable=protected-access
+ colorspace, area, mask, buffer
+ ):
+ return True
+ return False
def sort(self, key: Callable, reverse: bool) -> None:
"""Sort the members of the group."""
--- /dev/null
+# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# SPDX-License-Identifier: MIT
+
+"""
+`displayio.helpers`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+ https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+__version__ = "0.0.0+auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+def clamp(value, min_value, max_value):
+ """Clamp a value between a minimum and maximum value"""
+ return max(min(max_value, value), min_value)
+
+def bswap16(value):
+ """Swap the bytes in a 16 bit value"""
+ return (value & 0xFF00) >> 8 | (value & 0x00FF) << 8
\ No newline at end of file
from typing import Optional, Union, Tuple
import circuitpython_typing
+from ._colorconverter import ColorConverter
+from ._colorspace import Colorspace
+from ._structs import InputPixelStruct, OutputPixelStruct, ColorStruct
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
format internally to save memory.
"""
- def __init__(self, color_count: int):
+ def __init__(self, color_count: int, dither: bool = False):
"""Create a Palette object to store a set number of colors."""
self._needs_refresh = False
+ self._dither = dither
self._colors = []
for _ in range(color_count):
self._colors.append(self._make_color(0))
- self._update_rgba(len(self._colors) - 1)
-
- def _update_rgba(self, index):
- color = self._colors[index]["rgb888"]
- transparent = self._colors[index]["transparent"]
- self._colors[index]["rgba"] = (
- color >> 16,
- (color >> 8) & 0xFF,
- color & 0xFF,
- 0 if transparent else 0xFF,
- )
def _make_color(self, value, transparent=False):
- color = {
- "transparent": transparent,
- "rgb888": 0,
- "rgba": (0, 0, 0, 255),
- }
+ color = ColorStruct(transparent=transparent)
+
if isinstance(value, (tuple, list, bytes, bytearray)):
value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
elif isinstance(value, int):
raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
else:
raise TypeError("Color buffer must be a buffer, tuple, list, or int")
- color["rgb888"] = value
+ color.rgb888 = value
self._needs_refresh = True
return color
(to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
"""
- if self._colors[index]["rgb888"] != value:
+ if self._colors[index].rgb888 != value:
self._colors[index] = self._make_color(value)
- self._update_rgba(index)
def __getitem__(self, index: int) -> Optional[int]:
if not 0 <= index < len(self._colors):
raise ValueError("Palette index out of range")
- return self._colors[index]["rgb888"]
+ return self._colors[index].rgb888
def make_transparent(self, palette_index: int) -> None:
"""Set the palette index to be a transparent color"""
- self._colors[palette_index]["transparent"] = True
- self._update_rgba(palette_index)
+ self._colors[palette_index].transparent = True
def make_opaque(self, palette_index: int) -> None:
"""Set the palette index to be an opaque color"""
- self._colors[palette_index]["transparent"] = False
- self._update_rgba(palette_index)
+ self._colors[palette_index].transparent = False
def _get_palette(self):
"""Generate a palette for use with PIL"""
palette = []
for color in self._colors:
- palette += color["rgba"][0:3]
+ palette += color.rgba()[0:3]
return palette
def _get_alpha_palette(self):
palette = []
for color in self._colors:
for _ in range(3):
- palette += [0 if color["transparent"] else 255]
+ palette += [0 if color.transparent else 0xFF]
return palette
+ def _get_color(
+ self,
+ colorspace: Colorspace,
+ input_pixel: InputPixelStruct,
+ output_color: OutputPixelStruct,
+ ):
+ palette_index = input_pixel.pixel
+ if palette_index > len(self._colors) or self._colors[palette_index].transparent:
+ output_color.opaque = False
+ return
+
+ color = self._colors[palette_index]
+ if (
+ not self._dither
+ and color.cached_colorspace == colorspace
+ and color.cached_colorspace_grayscale_bit == colorspace.grayscale_bit
+ and color.cached_colorspace_grayscale == colorspace.grayscale
+ ):
+ output_color.pixel = self._colors[palette_index].cached_color
+ return
+
+ rgb888_pixel = input_pixel
+ ColorConverter._convert_color( # pylint: disable=protected-access
+ colorspace, self._dither, rgb888_pixel, output_color
+ )
+ if not self._dither:
+ color.cached_colorspace = colorspace
+ color.cached_color = output_color.pixel
+ color.cached_colorspace_grayscale = colorspace.grayscale
+ color.cached_colorspace_grayscale_bit = colorspace.grayscale_bit
+
def is_transparent(self, palette_index: int) -> bool:
"""Returns True if the palette index is transparent. Returns False if opaque."""
- return self._colors[palette_index]["transparent"]
+ return self._colors[palette_index].transparent
+
+ @property
+ def dither(self) -> bool:
+ """When true the palette dithers the output by adding
+ random noise when truncating to display bitdepth
+ """
+ return self._dither
+
+ @dither.setter
+ def dither(self, value: bool):
+ if not isinstance(value, bool):
+ raise ValueError("Value should be boolean")
+ self._dither = value
reverse_pixels_in_byte: bool = False
reverse_bytes_in_word: bool = False
dither: bool = False
+
+
+@dataclass
+class InputPixelStruct:
+ """InputPixel Struct Dataclass"""
+
+ pixel: int = 0
+ x: int = 0
+ y: int = 0
+ tile: int = 0
+ tile_x: int = 0
+ tile_y: int = 0
+
+
+@dataclass
+class OutputPixelStruct:
+ """OutputPixel Struct Dataclass"""
+
+ pixel: int = 0
+ opaque: bool = False
+
+
+@dataclass
+class ColorStruct:
+ """Color Struct Dataclass"""
+
+ rgb888: int = 0
+ cached_colorspace: ColorspaceStruct = None
+ cached_color: int = 0
+ cached_colorspace_grayscale_bit: int = 0
+ cached_colorspace_grayscale: bool = False
+ transparent: bool = False
+
+ def rgba(self) -> tuple[int, int, int, int]:
+ """Return the color as a tuple of red, green, blue, alpha"""
+ return (
+ self.rgb888 >> 16,
+ (self.rgb888 >> 8) & 0xFF,
+ self.rgb888 & 0xFF,
+ 0 if self.transparent else 0xFF,
+ )
"""
+import struct
from typing import Union, Optional, Tuple
-from PIL import Image
from ._bitmap import Bitmap
from ._colorconverter import ColorConverter
from ._ondiskbitmap import OnDiskBitmap
from ._shape import Shape
from ._palette import Palette
-from ._structs import RectangleStruct, TransformStruct
+from ._structs import TransformStruct, InputPixelStruct, OutputPixelStruct
+from ._colorspace import Colorspace
+from ._area import Area
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
self._hidden_tilegrid = False
self._x = x
self._y = y
- self._width = width # Number of Tiles Wide
- self._height = height # Number of Tiles High
+ self._width_in_tiles = width
+ self._height_in_tiles = height
self._transpose_xy = False
self._flip_x = False
self._flip_y = False
raise ValueError("Default Tile is out of range")
self._pixel_width = width * tile_width
self._pixel_height = height * tile_height
- self._tiles = (self._width * self._height) * [default_tile]
+ self._tiles = (self._width_in_tiles * self._height_in_tiles) * [default_tile]
self._in_group = False
self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
- self._current_area = RectangleStruct(
- 0, 0, self._pixel_width, self._pixel_height
- )
+ self._current_area = Area(0, 0, self._pixel_width, self._pixel_height)
self._moved = False
+ self._bitmap_width_in_tiles = bitmap_width // tile_width
+ self._tiles_in_bitmap = self._bitmap_width_in_tiles * (
+ bitmap_height // tile_height
+ )
+ self.inline_tiles = False # We have plenty of memory
def _update_transform(self, absolute_transform):
"""Update the parent transform and child transforms"""
)
image.putalpha(alpha.convert("L"))
- def _fill_area(self, buffer):
- # pylint: disable=too-many-locals,too-many-branches,too-many-statements
+ def _fill_area(
+ self, colorspace: Colorspace, area: Area, mask: bytearray, buffer: bytearray
+ ) -> bool:
"""Draw onto the image"""
- if self._hidden_tilegrid:
- return
+ # pylint: disable=too-many-locals,too-many-branches,too-many-statements
- if self._bitmap.width <= 0 or self._bitmap.height <= 0:
- return
-
- # Copy class variables to local variables in case something changes
- x = self._x
- y = self._y
- width = self._width
- height = self._height
- tile_width = self._tile_width
- tile_height = self._tile_height
- bitmap_width = self._bitmap.width
- pixel_width = self._pixel_width
- pixel_height = self._pixel_height
- tiles = self._tiles
- absolute_transform = self._absolute_transform
- pixel_shader = self._pixel_shader
- bitmap = self._bitmap
+ # If no tiles are present we have no impact
tiles = self._tiles
- tile_count_x = bitmap_width // tile_width
+ if self._hidden_tilegrid:
+ return False
- image = Image.new(
- "RGBA",
- (width * tile_width, height * tile_height),
- (0, 0, 0, 0),
- )
+ overlap = Area()
+ if not self._current_area.compute_overlap(area, overlap):
+ return False
- for tile_x in range(width):
- for tile_y in range(height):
- tile_index = tiles[tile_y * width + tile_x]
- tile_index_x = tile_index % tile_count_x
- tile_index_y = tile_index // tile_count_x
- tile_image = bitmap._image # pylint: disable=protected-access
- if isinstance(pixel_shader, Palette):
- tile_image = tile_image.copy().convert("P")
- self._apply_palette(tile_image)
- tile_image = tile_image.convert("RGBA")
- self._add_alpha(tile_image)
- elif isinstance(pixel_shader, ColorConverter):
- # This will be needed for eInks, grayscale, and monochrome displays
- pass
- image.alpha_composite(
- tile_image,
- dest=(tile_x * tile_width, tile_y * tile_height),
- source=(
- tile_index_x * tile_width,
- tile_index_y * tile_height,
- tile_index_x * tile_width + tile_width,
- tile_index_y * tile_height + tile_height,
- ),
- )
+ if self._bitmap.width <= 0 or self._bitmap.height <= 0:
+ return False
+
+ x_stride = 1
+ y_stride = area.width()
+
+ flip_x = self._flip_x
+ flip_y = self._flip_y
+ if self._transpose_xy != self._absolute_transform.transpose_xy:
+ flip_x, flip_y = flip_y, flip_x
+
+ start = 0
+ if (self._absolute_transform.dx < 0) != flip_x:
+ start += (area.width() - 1) * x_stride
+ x_stride *= -1
+ if (self._absolute_transform.dy < 0) != flip_y:
+ start += (area.height() - 1) * y_stride
+ y_stride *= -1
+
+ full_coverage = area == overlap
+
+ transformed = Area()
+ area.transform_within(
+ flip_x != (self._absolute_transform.dx < 0),
+ flip_y != (self._absolute_transform.dy < 0),
+ self.transpose_xy != self._absolute_transform.transpose_xy,
+ overlap,
+ self._current_area,
+ transformed,
+ )
- if absolute_transform is not None:
- if absolute_transform.scale > 1:
- image = image.resize(
- (
- int(pixel_width * absolute_transform.scale),
- int(
- pixel_height * absolute_transform.scale,
- ),
- ),
- resample=Image.NEAREST,
- )
- if absolute_transform.mirror_x != self._flip_x:
- image = image.transpose(Image.FLIP_LEFT_RIGHT)
- if absolute_transform.mirror_y != self._flip_y:
- image = image.transpose(Image.FLIP_TOP_BOTTOM)
- if absolute_transform.transpose_xy != self._transpose_xy:
- image = image.transpose(Image.TRANSPOSE)
- x *= absolute_transform.dx
- y *= absolute_transform.dy
- x += absolute_transform.x
- y += absolute_transform.y
-
- source_x = source_y = 0
- if x < 0:
- source_x = round(0 - x)
- x = 0
- if y < 0:
- source_y = round(0 - y)
- y = 0
-
- x = round(x)
- y = round(y)
+ start_x = transformed.x1 - self._current_area.x1
+ end_x = transformed.x2 - self._current_area.x1
+ start_y = transformed.y1 - self._current_area.y1
+ end_y = transformed.y2 - self._current_area.y1
- if (
- x <= buffer.width
- and y <= buffer.height
- and source_x <= image.width
- and source_y <= image.height
- ):
- buffer.alpha_composite(image, (x, y), source=(source_x, source_y))
+ if (self._absolute_transform.dx < 0) != flip_x:
+ x_shift = area.x2 - overlap.x2
+ else:
+ x_shift = overlap.x1 - area.x1
+ if (self._absolute_transform.dy < 0) != flip_y:
+ y_shift = area.y2 - overlap.y2
+ else:
+ y_shift = overlap.y1 - area.y1
+
+ if self._transpose_xy != self._absolute_transform.transpose_xy:
+ x_stride, y_stride = y_stride, x_stride
+ x_shift, y_shift = y_shift, x_shift
+
+ pixels_per_byte = 8 // colorspace.depth
+
+ input_pixel = InputPixelStruct()
+ output_pixel = OutputPixelStruct()
+ for input_pixel.y in range(start_y, end_y):
+ row_start = (
+ start + (input_pixel.y - start_y + y_shift) * y_stride
+ ) # In Pixels
+ local_y = input_pixel.y // self._absolute_transform.scale
+ for input_pixel.x in range(start_x, end_x):
+ offset = (
+ row_start + (input_pixel.x - start_x + x_shift) * x_stride
+ ) # In Pixels
+
+ # Check the mask first to see if the pixel has already been set
+ if mask[offset // 32] & (1 << (offset % 32)):
+ continue
+ local_x = input_pixel.x // self._absolute_transform.scale
+ tile_location = (
+ (local_y // self._tile_height + self._top_left_y)
+ % self._height_in_tiles
+ ) * self._width_in_tiles + (
+ local_x // self._tile_width + self._top_left_x
+ ) % self._width_in_tiles
+ input_pixel.tile = tiles[tile_location]
+ input_pixel.tile_x = (
+ input_pixel.tile % self._bitmap_width_in_tiles
+ ) * self._tile_width + local_x % self._tile_width
+ input_pixel.tile_y = (
+ input_pixel.tile // self._bitmap_width_in_tiles
+ ) * self._tile_height + local_y % self._tile_height
+
+ input_pixel.pixel = self.bitmap[input_pixel.tile_x, input_pixel.tile_y]
+ output_pixel.opaque = True
+
+ if self._pixel_shader is None:
+ output_pixel.pixel = input_pixel.pixel
+ elif isinstance(self._pixel_shader, Palette):
+ self._pixel_shader._get_color( # pylint: disable=protected-access
+ colorspace, input_pixel, output_pixel
+ )
+ elif isinstance(self._pixel_shader, ColorConverter):
+ self._pixel_shader._convert( # pylint: disable=protected-access
+ colorspace, input_pixel, output_pixel
+ )
+
+ if not output_pixel.opaque:
+ full_coverage = False
+ else:
+ mask[offset // 32] |= 1 << (offset % 32)
+ if colorspace.depth == 16:
+ buffer = (
+ buffer[:offset]
+ + struct.pack("H", output_pixel.pixel)
+ + buffer[offset + 2 :]
+ )
+ elif colorspace.depth == 32:
+ buffer = (
+ buffer[:offset]
+ + struct.pack("I", output_pixel.pixel)
+ + buffer[offset + 4 :]
+ )
+ elif colorspace.depth == 8:
+ buffer[offset] = output_pixel.pixel & 0xFF
+ elif colorspace.depth < 8:
+ # Reorder the offsets to pack multiple rows into
+ # a byte (meaning they share a column).
+ if not colorspace.pixels_in_byte_share_row:
+ width = area.width()
+ row = offset // width
+ col = offset % width
+ # Dividing by pixels_per_byte does truncated division
+ # even if we multiply it back out
+ offset = (
+ col * pixels_per_byte
+ + (row // pixels_per_byte) * width
+ + (row % pixels_per_byte)
+ )
+ shift = (offset % pixels_per_byte) * colorspace.depth
+ if colorspace.reverse_pixels_in_byte:
+ # Reverse the shift by subtracting it from the leftmost shift
+ shift = (pixels_per_byte - 1) * colorspace.depth - shift
+ buffer[offset // pixels_per_byte] |= output_pixel.pixel << shift
+ return full_coverage
def _finish_refresh(self):
pass
if isinstance(index, (tuple, list)):
x = index[0]
y = index[1]
- index = y * self._width + x
+ index = y * self._width_in_tiles + x
elif isinstance(index, int):
- x = index % self._width
- y = index // self._width
- if x > self._width or y > self._height or index >= len(self._tiles):
+ x = index % self._width_in_tiles
+ y = index // self._width_in_tiles
+ if (
+ x > self._width_in_tiles
+ or y > self._height_in_tiles
+ or index >= len(self._tiles)
+ ):
raise ValueError("Tile index out of bounds")
return index
@property
def width(self) -> int:
"""Width in tiles"""
- return self._width
+ return self._width_in_tiles
@property
def height(self) -> int:
"""Height in tiles"""
- return self._height
+ return self._height_in_tiles
@property
def tile_width(self) -> int: