]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/commitdiff
More displayio code updates
authorMelissa LeBlanc-Williams <melissa@adafruit.com>
Fri, 22 Sep 2023 01:35:31 +0000 (18:35 -0700)
committerMelissa LeBlanc-Williams <melissa@adafruit.com>
Fri, 22 Sep 2023 01:35:31 +0000 (18:35 -0700)
displayio/__init__.py
displayio/_area.py
displayio/_colorconverter.py
displayio/_display.py
displayio/_displaycore.py
displayio/_group.py
displayio/_helpers.py [new file with mode: 0644]
displayio/_palette.py
displayio/_structs.py
displayio/_tilegrid.py

index f357dfa2959987c6273e5cad73a26083cdddbfdb..74facd99e0a288cbe35c5f53259bd4c80a6fe7f2 100644 (file)
@@ -44,7 +44,7 @@ display_buses = []
 def _background():
     """Main thread function to loop through all displays and update them"""
     for display in displays:
 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:
 
 
 def release_displays() -> None:
@@ -54,11 +54,11 @@ def release_displays() -> None:
     initialization so the display is active as long as possible.
     """
     for display in displays:
     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:
     displays.clear()
 
     for display_bus in display_buses:
-        display_bus._release()  # pylint: disable=protected-access
+        display_bus.deinit()
     display_buses.clear()
 
 
     display_buses.clear()
 
 
index 3eba700590bb7bb03249aad980ad2d9bc1f53275..5b96e9370bf4e6b72b276f0e6bae3c89f9de897d 100644 (file)
@@ -25,9 +25,9 @@ __repo__ = "https://github.com/adafruit/Adafruit_Blinka_Displayio.git"
 
 
 class Area:
 
 
 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
     def __init__(self, x1: int = 0, y1: int = 0, x2: int = 0, y2: int = 0):
         self.x1 = x1
         self.y1 = y1
@@ -56,7 +56,8 @@ class Area:
         self.x2 += dx
         self.y2 += dy
 
         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)
         a = self
         overlap.x1 = max(a.x1, other.x1)
         overlap.x2 = min(a.x2, other.x2)
@@ -79,12 +80,11 @@ class Area:
             self.y1, self.y2 = self.y2, self.y1
 
     def _union(self, other, union):
             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 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)
             return
 
         union.x1 = min(self.x1, other.x1)
@@ -93,12 +93,15 @@ class Area:
         union.y2 = max(self.y2, other.y2)
 
     def width(self) -> int:
         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 self.x2 - self.x1
 
     def height(self) -> int:
+        """Return the height of the area."""
         return self.y2 - self.y1
 
     def size(self) -> int:
         return self.y2 - self.y1
 
     def size(self) -> int:
+        """Return the size of the area."""
         return self.width() * self.height()
 
     def __eq__(self, other):
         return self.width() * self.height()
 
     def __eq__(self, other):
@@ -113,7 +116,7 @@ class Area:
         )
 
     @staticmethod
         )
 
     @staticmethod
-    def _transform_within(
+    def transform_within(
         mirror_x: bool,
         mirror_y: bool,
         transpose_xy: bool,
         mirror_x: bool,
         mirror_y: bool,
         transpose_xy: bool,
@@ -121,6 +124,7 @@ class Area:
         whole: Area,
         transformed: Area,
     ):
         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:
         # pylint: disable=too-many-arguments
         # Original and whole must be in the same coordinate space.
         if mirror_x:
index c3c548100b0e9aef46caa8a449b8c97bc95bf7e6..db57b6a9c1756923656a66ae1d7dee9dfedbb727 100644 (file)
@@ -21,7 +21,8 @@ __version__ = "0.0.0+auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
 from ._colorspace import Colorspace
 __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:
 
 
 class ColorConverter:
@@ -46,23 +47,16 @@ class ColorConverter:
         self._cached_output_color = None
 
     @staticmethod
         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
         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):
 
     @staticmethod
     def _compute_rgb565(color_rgb888: int):
@@ -120,11 +114,12 @@ class ColorConverter:
 
         return hue
 
 
         return hue
 
-    def _compute_sevencolor(self, color_rgb888: int):
+    @staticmethod
+    def _compute_sevencolor(color_rgb888: int):
         # pylint: disable=too-many-return-statements
         # pylint: disable=too-many-return-statements
-        chroma = self._compute_chroma(color_rgb888)
+        chroma = ColorConverter._compute_chroma(color_rgb888)
         if chroma >= 64:
         if chroma >= 64:
-            hue = self._compute_hue(color_rgb888)
+            hue = ColorConverter._compute_hue(color_rgb888)
             # Red 0
             if hue < 10:
                 return 0x4
             # Red 0
             if hue < 10:
                 return 0x4
@@ -142,7 +137,7 @@ class ColorConverter:
                 return 0x3
             # The rest is red to 255
             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
         if luma >= 128:
             return 0x1  # White
         return 0x0  # Black
@@ -173,51 +168,54 @@ class ColorConverter:
         else:
             raise ValueError("Color must be an integer or 3 or 4 value tuple")
 
         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
 
         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 = 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:
 
         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,
         ):
         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
         if colorspace in (Colorspace.RGB565, Colorspace.RGB565_SWAPPED):
             red8 = (pixel >> 11) << 3
             grn8 = ((pixel >> 5) << 2) & 0xFF
@@ -242,19 +240,19 @@ class ColorConverter:
             return (pixel & 0xFF) & 0x01010101
         return pixel
 
             return (pixel & 0xFF) & 0x01010101
         return pixel
 
+    @staticmethod
     def _convert_color(
     def _convert_color(
-        self,
         colorspace: ColorspaceStruct,
         dither: bool,
         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
     ) -> None:
         # pylint: disable=too-many-return-statements, too-many-branches, too-many-statements
-        pixel = input_pixel["pixel"]
+        pixel = input_pixel.pixel
         if dither:
         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
 
             red8 = pixel >> 16
             grn8 = (pixel >> 8) & 0xFF
@@ -272,49 +270,51 @@ class ColorConverter:
             pixel = (red8 << 16) | (grn8 << 8) | blu8
 
         if colorspace.depth == 16:
             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:
             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:
             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:
                 if not colorspace.grayscale:
-                    output_color["pixel"] = 0
-                output_color["opaque"] = True
+                    output_color.pixel = 0
+                output_color.opaque = True
                 return
                 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
             )
             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
             ) & bitmask
-            output_color["opaque"] = True
+            output_color.opaque = True
             return
         if colorspace.depth == 32:
             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:
             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:
             return
         if colorspace.depth == 4:
             if colorspace.sevencolor:
-                packed = self._compute_sevencolor(pixel)
+                packed = ColorConverter._compute_sevencolor(pixel)
             else:
             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
             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
 
     def make_transparent(self, color: int) -> None:
         """Set the transparent color or index for the ColorConverter. This will
index eb3655d9ed2405598182f389c42a188d41cefc23..8e33803a7293df2df6e27eac7650ac30c0638080 100644 (file)
@@ -20,10 +20,8 @@ displayio for Blinka
 import time
 import struct
 from typing import Optional
 import time
 import struct
 from typing import Optional
-from dataclasses import astuple
 import digitalio
 from PIL import Image
 import digitalio
 from PIL import Image
-import numpy
 import microcontroller
 import circuitpython_typing
 from ._displaycore import _DisplayCore
 import microcontroller
 import circuitpython_typing
 from ._displaycore import _DisplayCore
@@ -31,6 +29,7 @@ from ._displaybus import _DisplayBus
 from ._colorconverter import ColorConverter
 from ._group import Group
 from ._structs import RectangleStruct
 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,
 from ._constants import (
     CHIP_SELECT_TOGGLE_EVERY_BYTE,
     CHIP_SELECT_UNTOUCHED,
@@ -163,8 +162,6 @@ class Display:
 
         self._initialize(init_sequence)
         self._buffer = Image.new("RGB", (width, height))
 
         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
         self._current_group = None
         self._last_refresh_call = 0
         self._refresh_thread = None
@@ -229,27 +226,14 @@ class Display:
             time.sleep(delay_time_ms / 1000)
             i += 2 + data_size
 
             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]),
             )
         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
 
     def show(self, group: Group) -> None:
         """Switches to displaying the given group of layers. When group is None, the
@@ -310,6 +294,7 @@ class Display:
         if not self._core.start_refresh():
             return False
 
         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))
         # 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))
@@ -320,16 +305,25 @@ class Display:
             # save image to buffer (or probably refresh buffer so we can compare)
             self._buffer.paste(buffer)
 
             # 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
 
 
         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)
         if (
             self._auto_refresh
             and (time.monotonic() * 1000 - self._core.last_refresh)
@@ -337,56 +331,73 @@ class Display:
         ):
             self.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"""
 
     def _apply_rotation(self, rectangle):
         """Adjust the rectangle coordinates based on rotation"""
@@ -413,10 +424,6 @@ class Display:
             )
         return rectangle
 
             )
         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:
     def fill_row(
         self, y: int, buffer: circuitpython_typing.WriteableBuffer
     ) -> circuitpython_typing.WriteableBuffer:
@@ -427,6 +434,15 @@ class Display:
             buffer[x * 2 + 1] = _rgb_565 & 0xFF
         return buffer
 
             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."""
     @property
     def auto_refresh(self) -> bool:
         """True when the display is refreshed automatically."""
@@ -434,6 +450,7 @@ class Display:
 
     @auto_refresh.setter
     def auto_refresh(self, value: bool):
 
     @auto_refresh.setter
     def auto_refresh(self, value: bool):
+        self._first_manual_refresh = not value
         self._auto_refresh = value
 
     @property
         self._auto_refresh = value
 
     @property
@@ -447,13 +464,32 @@ class Display:
     @brightness.setter
     def brightness(self, value: float):
         if 0 <= float(value) <= 1.0:
     @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:
             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")
 
         else:
             raise ValueError("Brightness must be between 0.0 and 1.0")
 
index ccda898e66a6c268f748921560817244307ac51b..3126b8732da7df9a27a42ed36371513ad5ed7266 100644 (file)
@@ -23,18 +23,27 @@ __repo__ = "https://github.com/adafruit/Adafruit_Blinka_Displayio.git"
 
 
 import time
 
 
 import time
+import struct
 import circuitpython_typing
 from paralleldisplay import ParallelBus
 from ._fourwire import FourWire
 from ._group import Group
 from ._i2cdisplay import I2CDisplay
 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 ._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:
 
 
 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,
 
     def __init__(
         self,
@@ -229,15 +238,7 @@ class _DisplayCore:
         self.refresh_in_progress = False
         self.last_refresh = time.monotonic() * 1000
 
         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
 
         """Release the display from the current group"""
         # pylint: disable=protected-access
 
@@ -250,16 +251,32 @@ class _DisplayCore:
         mask: circuitpython_typing.WriteableBuffer,
         buffer: circuitpython_typing.WriteableBuffer,
     ) -> bool:
         mask: circuitpython_typing.WriteableBuffer,
         buffer: circuitpython_typing.WriteableBuffer,
     ) -> bool:
-        # pylint: disable=protected-access
         """Call the current group's fill area function"""
         """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"""
 
     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
 
         if not overlaps:
             return False
 
@@ -281,6 +298,125 @@ class _DisplayCore:
 
         return True
 
 
         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,
     def send(
         self,
         data_type: int,
index 505a3bffb19c4f598c4a2b4e4e732e1dfb5d7efa..a23ae4e4600973ffa979aacfbcd26aa5a55f92d9 100644 (file)
@@ -21,6 +21,8 @@ from __future__ import annotations
 from typing import Union, Callable
 from ._structs import TransformStruct
 from ._tilegrid import TileGrid
 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"
 
 __version__ = "0.0.0+auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
@@ -141,13 +143,19 @@ class Group:
         """Deletes the value at the given index."""
         del self._layers[index]
 
         """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:
         if self._hidden_group:
-            return
+            return False
 
         for layer in self._layers:
             if isinstance(layer, (Group, TileGrid)):
 
         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."""
 
     def sort(self, key: Callable, reverse: bool) -> None:
         """Sort the members of the group."""
diff --git a/displayio/_helpers.py b/displayio/_helpers.py
new file mode 100644 (file)
index 0000000..cbff9f4
--- /dev/null
@@ -0,0 +1,29 @@
+# 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
index 969c769b9ae6738cbce64e806c184d8805110976..2424db1b9839ed48f7bc1b249c2c6c79bb9fc509 100644 (file)
@@ -19,6 +19,9 @@ displayio for Blinka
 
 from typing import Optional, Union, Tuple
 import circuitpython_typing
 
 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"
 
 __version__ = "0.0.0+auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
@@ -29,31 +32,18 @@ class Palette:
     format internally to save memory.
     """
 
     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
         """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._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):
 
     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):
         if isinstance(value, (tuple, list, bytes, bytearray)):
             value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
         elif isinstance(value, int):
@@ -61,7 +51,7 @@ class Palette:
                 raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
         else:
             raise TypeError("Color buffer must be a buffer, tuple, list, or 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
         self._needs_refresh = True
 
         return color
@@ -82,30 +72,27 @@ class Palette:
         (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.
         """
         (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._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")
 
     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"""
 
     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"""
 
     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:
 
     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):
         return palette
 
     def _get_alpha_palette(self):
@@ -114,9 +101,53 @@ class Palette:
         palette = []
         for color in self._colors:
             for _ in range(3):
         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
 
         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."""
     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
index 9a2017e20475fff56e32c761645e279a9c72bdf3..a46fb48204529403ee4e8f8273fdf3396f86ce8a 100644 (file)
@@ -63,3 +63,44 @@ class ColorspaceStruct:
     reverse_pixels_in_byte: bool = False
     reverse_bytes_in_word: bool = False
     dither: bool = False
     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,
+        )
index 41ff504058eda9746dd708946aea07cc7c742592..0cfc6d3d304bbe47dfb724f460406cf098e12535 100644 (file)
@@ -17,14 +17,16 @@ displayio for Blinka
 
 """
 
 
 """
 
+import struct
 from typing import Union, Optional, Tuple
 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 ._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"
 
 __version__ = "0.0.0+auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
@@ -73,8 +75,8 @@ class TileGrid:
         self._hidden_tilegrid = False
         self._x = x
         self._y = y
         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
         self._transpose_xy = False
         self._flip_x = False
         self._flip_y = False
@@ -96,13 +98,16 @@ class TileGrid:
             raise ValueError("Default Tile is out of range")
         self._pixel_width = width * tile_width
         self._pixel_height = height * tile_height
             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._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._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"""
 
     def _update_transform(self, absolute_transform):
         """Update the parent transform and child transforms"""
@@ -196,104 +201,155 @@ class TileGrid:
         )
         image.putalpha(alpha.convert("L"))
 
         )
         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"""
         """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
 
         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
 
     def _finish_refresh(self):
         pass
@@ -420,11 +476,15 @@ class TileGrid:
         if isinstance(index, (tuple, list)):
             x = index[0]
             y = index[1]
         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):
         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
 
             raise ValueError("Tile index out of bounds")
         return index
 
@@ -447,12 +507,12 @@ class TileGrid:
     @property
     def width(self) -> int:
         """Width in tiles"""
     @property
     def width(self) -> int:
         """Width in tiles"""
-        return self._width
+        return self._width_in_tiles
 
     @property
     def height(self) -> int:
         """Height in tiles"""
 
     @property
     def height(self) -> int:
         """Height in tiles"""
-        return self._height
+        return self._height_in_tiles
 
     @property
     def tile_width(self) -> int:
 
     @property
     def tile_width(self) -> int: