]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/commitdiff
Finish adding code to ColorConverter
authorMelissa LeBlanc-Williams <melissa@adafruit.com>
Wed, 20 Sep 2023 20:28:30 +0000 (13:28 -0700)
committerMelissa LeBlanc-Williams <melissa@adafruit.com>
Wed, 20 Sep 2023 20:28:30 +0000 (13:28 -0700)
.pylintrc
displayio/_colorconverter.py
displayio/_i2cdisplay.py
displayio/_structs.py

index 40208c3965be4d95bf88c6aa420dda7c4c856e25..f945e9204815bf2933a82c7cb973c78f3c61f9ea 100644 (file)
--- a/.pylintrc
+++ b/.pylintrc
@@ -396,4 +396,4 @@ min-public-methods=1
 
 # Exceptions that will emit a warning when being caught. Defaults to
 # "Exception"
-overgeneral-exceptions=Exception
+overgeneral-exceptions=builtins.Exception
index 01c70d67d214fadd8793b82e5022792894096c2d..c3c548100b0e9aef46caa8a449b8c97bc95bf7e6 100644 (file)
@@ -21,6 +21,7 @@ __version__ = "0.0.0+auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
 from ._colorspace import Colorspace
+from ._structs import ColorspaceStruct
 
 
 class ColorConverter:
@@ -36,62 +37,129 @@ class ColorConverter:
         :param bool dither: Adds random noise to dither the output image
         """
         self._dither = dither
-        self._depth = 16
         self._transparent_color = None
-        self._rgba = False
+        self._rgba = False  # Todo set Output colorspace depth to 32 maybe?
         self._input_colorspace = input_colorspace
+        self._output_colorspace = ColorspaceStruct(16)
+        self._cached_colorspace = None
+        self._cached_input_pixel = None
+        self._cached_output_color = None
 
-    def _compute_rgb565(self, color: int):
-        self._depth = 16
-        return (color[0] & 0xF8) << 8 | (color[1] & 0xFC) << 3 | color[2] >> 3
+    @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):
+        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)
+
+    def _dither_noise_2(self, x, y):
+        return self._dither_noise_1(x + y * 0xFFFF)
+
+    @staticmethod
+    def _compute_rgb565(color_rgb888: int):
+        red5 = color_rgb888 >> 19
+        grn6 = (color_rgb888 >> 10) & 0x3F
+        blu5 = (color_rgb888 >> 3) & 0x1F
+        return red5 << 11 | grn6 << 5 | blu5
+
+    @staticmethod
+    def _compute_rgb332(color_rgb888: int):
+        red3 = color_rgb888 >> 21
+        grn2 = (color_rgb888 >> 13) & 0x7
+        blu2 = (color_rgb888 >> 6) & 0x3
+        return red3 << 5 | grn2 << 3 | blu2
 
     @staticmethod
-    def _compute_luma(color: int):
-        red = color >> 16
-        green = (color >> 8) & 0xFF
-        blue = color & 0xFF
-        return (red * 19) / 255 + (green * 182) / 255 + (blue + 54) / 255
+    def _compute_rgbd(color_rgb888: int):
+        red1 = (color_rgb888 >> 23) & 0x1
+        grn1 = (color_rgb888 >> 15) & 0x1
+        blu1 = (color_rgb888 >> 7) & 0x1
+        return red1 << 3 | grn1 << 2 | blu1 << 1  # | dummy
 
     @staticmethod
-    def _compute_chroma(color: int):
-        red = color >> 16
-        green = (color >> 8) & 0xFF
-        blue = color & 0xFF
-        return max(red, green, blue) - min(red, green, blue)
-
-    def _compute_hue(self, color: int):
-        red = color >> 16
-        green = (color >> 8) & 0xFF
-        blue = color & 0xFF
-        max_color = max(red, green, blue)
-        chroma = self._compute_chroma(color)
+    def _compute_luma(color_rgb888: int):
+        red8 = color_rgb888 >> 16
+        grn8 = (color_rgb888 >> 8) & 0xFF
+        blu8 = color_rgb888 & 0xFF
+        return (red8 * 19 + grn8 * 182 + blu8 + 54) / 255
+
+    @staticmethod
+    def _compute_chroma(color_rgb888: int):
+        red8 = color_rgb888 >> 16
+        grn8 = (color_rgb888 >> 8) & 0xFF
+        blu8 = color_rgb888 & 0xFF
+        return max(red8, grn8, blu8) - min(red8, grn8, blu8)
+
+    @staticmethod
+    def _compute_hue(color_rgb888: int):
+        red8 = color_rgb888 >> 16
+        grn8 = (color_rgb888 >> 8) & 0xFF
+        blu8 = color_rgb888 & 0xFF
+        max_color = max(red8, grn8, blu8)
+        chroma = max_color - min(red8, grn8, blu8)
         if chroma == 0:
             return 0
         hue = 0
-        if max_color == red:
-            hue = (((green - blue) * 40) / chroma) % 240
-        elif max_color == green:
-            hue = (((blue - red) + (2 * chroma)) * 40) / chroma
-        elif max_color == blue:
-            hue = (((red - green) + (4 * chroma)) * 40) / chroma
+        if max_color == red8:
+            hue = (((grn8 - blu8) * 40) / chroma) % 240
+        elif max_color == grn8:
+            hue = (((blu8 - red8) + (2 * chroma)) * 40) / chroma
+        elif max_color == blu8:
+            hue = (((red8 - grn8) + (4 * chroma)) * 40) / chroma
         if hue < 0:
             hue += 240
 
         return hue
 
-    @staticmethod
-    def _dither_noise_1(noise):
-        noise = (noise >> 13) ^ noise
-        more_noise = (
-            noise * (noise * noise * 60493 + 19990303) + 1376312589
-        ) & 0x7FFFFFFF
-        return (more_noise / (1073741824.0 * 2)) * 255
-
-    def _dither_noise_2(self, x, y):
-        return self._dither_noise_1(x + y * 0xFFFF)
+    def _compute_sevencolor(self, color_rgb888: int):
+        # pylint: disable=too-many-return-statements
+        chroma = self._compute_chroma(color_rgb888)
+        if chroma >= 64:
+            hue = self._compute_hue(color_rgb888)
+            # Red 0
+            if hue < 10:
+                return 0x4
+            # Orange 21
+            if hue < 21 + 10:
+                return 0x6
+            # Yellow 42
+            if hue < 42 + 21:
+                return 0x5
+            # Green 85
+            if hue < 85 + 42:
+                return 0x2
+            # Blue 170
+            if hue < 170 + 42:
+                return 0x3
+            # The rest is red to 255
+            return 0x4
+        luma = self._compute_luma(color_rgb888)
+        if luma >= 128:
+            return 0x1  # White
+        return 0x0  # Black
 
-    def _compute_tricolor(self):
-        pass
+    @staticmethod
+    def _compute_tricolor(
+        colorspace: ColorspaceStruct, pixel_hue: int, color: int
+    ) -> int:
+        hue_diff = colorspace.tricolor_hue - pixel_hue
+        if -10 <= hue_diff <= 10 or hue_diff <= -220 or hue_diff >= 220:
+            if colorspace.grayscale:
+                color = 0
+            else:
+                color = 1
+        elif not colorspace.grayscale:
+            color = 0
+        return color
 
     def convert(self, color: int) -> int:
         "Converts the given rgb888 color to RGB565"
@@ -105,11 +173,148 @@ class ColorConverter:
         else:
             raise ValueError("Color must be an integer or 3 or 4 value tuple")
 
-        if self._dither:
-            return color  # To Do: return a dithered color
-        if self._rgba:
-            return color
-        return self._compute_rgb565(color)
+        input_pixel = {
+            "pixel": color,
+            "x": 0,
+            "y": 0,
+            "tile": 0,
+            "tile_x": 0,
+            "tile_y": 0,
+        }
+
+        output_pixel = {"pixel": 0, "opaque": False}
+
+        if input_pixel["pixel"] == self._transparent_color:
+            return output_pixel["pixel"]
+
+        if (
+            not self._dither
+            and self._cached_colorspace == self._output_colorspace
+            and self._cached_input_pixel == input_pixel["pixel"]
+        ):
+            return self._cached_output_color
+
+        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
+        )
+
+        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"]
+
+    def _convert_pixel(self, colorspace: Colorspace, pixel: int) -> int:
+        pixel = self._clamp(pixel, 0, 0xFFFFFFFF)
+        if colorspace in (
+            Colorspace.RGB565_SWAPPED,
+            Colorspace.RGB555_SWAPPED,
+            Colorspace.BGR565_SWAPPED,
+            Colorspace.BGR555_SWAPPED,
+        ):
+            pixel = self._bswap16(pixel)
+        if colorspace in (Colorspace.RGB565, Colorspace.RGB565_SWAPPED):
+            red8 = (pixel >> 11) << 3
+            grn8 = ((pixel >> 5) << 2) & 0xFF
+            blu8 = (pixel << 3) & 0xFF
+            return (red8 << 16) | (grn8 << 8) | blu8
+        if colorspace in (Colorspace.RGB555, Colorspace.RGB555_SWAPPED):
+            red8 = (pixel >> 10) << 3
+            grn8 = ((pixel >> 5) << 3) & 0xFF
+            blu8 = (pixel << 3) & 0xFF
+            return (red8 << 16) | (grn8 << 8) | blu8
+        if colorspace in (Colorspace.BGR565, Colorspace.BGR565_SWAPPED):
+            blu8 = (pixel >> 11) << 3
+            grn8 = ((pixel >> 5) << 2) & 0xFF
+            red8 = (pixel << 3) & 0xFF
+            return (red8 << 16) | (grn8 << 8) | blu8
+        if colorspace in (Colorspace.BGR555, Colorspace.BGR555_SWAPPED):
+            blu8 = (pixel >> 10) << 3
+            grn8 = ((pixel >> 5) << 3) & 0xFF
+            red8 = (pixel << 3) & 0xFF
+            return (red8 << 16) | (grn8 << 8) | blu8
+        if colorspace == Colorspace.L8:
+            return (pixel & 0xFF) & 0x01010101
+        return pixel
+
+    def _convert_color(
+        self,
+        colorspace: ColorspaceStruct,
+        dither: bool,
+        input_pixel: dict,
+        output_color: dict,
+    ) -> None:
+        # pylint: disable=too-many-return-statements, too-many-branches, too-many-statements
+        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)
+
+            red8 = pixel >> 16
+            grn8 = (pixel >> 8) & 0xFF
+            blu8 = pixel & 0xFF
+
+            if colorspace.depth == 16:
+                blu8 = min(255, blu8 + (rand_blu & 0x07))
+                red8 = min(255, red8 + (rand_red & 0x07))
+                grn8 = min(255, grn8 + (rand_grn & 0x03))
+            else:
+                bitmask = 0xFF >> colorspace.depth
+                blu8 = min(255, blu8 + (rand_blu & bitmask))
+                red8 = min(255, red8 + (rand_red & bitmask))
+                grn8 = min(255, grn8 + (rand_grn & bitmask))
+            pixel = (red8 << 16) | (grn8 << 8) | blu8
+
+        if colorspace.depth == 16:
+            packed = self._compute_rgb565(pixel)
+            if colorspace.reverse_bytes_in_word:
+                packed = self._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:
+                if not colorspace.grayscale:
+                    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"]
+            )
+            return
+        if colorspace.grayscale and colorspace.depth <= 8:
+            bitmask = (1 << colorspace.depth) - 1
+            output_color["pixel"] = (
+                self._compute_luma(pixel) >> colorspace.grayscale_bit
+            ) & bitmask
+            output_color["opaque"] = True
+            return
+        if colorspace.depth == 32:
+            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
+            return
+        if colorspace.depth == 4:
+            if colorspace.sevencolor:
+                packed = self._compute_sevencolor(pixel)
+            else:
+                packed = self._compute_rgbd(pixel)
+            output_color["pixel"] = packed
+            output_color["opaque"] = True
+            return
+        output_color["opaque"] = False
 
     def make_transparent(self, color: int) -> None:
         """Set the transparent color or index for the ColorConverter. This will
@@ -117,8 +322,7 @@ class ColorConverter:
         """
         self._transparent_color = color
 
-    def make_opaque(self, color: int) -> None:
-        # pylint: disable=unused-argument
+    def make_opaque(self, _color: int) -> None:
         """Make the ColorConverter be opaque and have no transparent pixels."""
         self._transparent_color = None
 
index 26476599fa61acc0379b3d3b27c5b54163feb86d..42f85ecac625e38f15885d392293c537db8e2acc 100644 (file)
@@ -89,10 +89,9 @@ class I2CDisplay:
     def _send(
         self,
         data_type: int,
-        chip_select: int,
+        _chip_select: int,  # Chip select behavior
         data: circuitpython_typing.ReadableBuffer,
     ):
-        # pylint: disable=unused-argument
         if data_type == DISPLAY_COMMAND:
             n = len(data)
             if n > 0:
@@ -101,12 +100,12 @@ class I2CDisplay:
                     command_bytes[2 * i] = 0x80
                     command_bytes[2 * i + 1] = data[i]
 
-                self._i2c.writeto(self._dev_addr, buffer=command_bytes, stop=True)
+                self._i2c.writeto(self._dev_addr, buffer=command_bytes)
         else:
             data_bytes = bytearray(len(data) + 1)
             data_bytes[0] = 0x40
             data_bytes[1:] = data
-            self._i2c.writeto(self._dev_addr, buffer=data_bytes, stop=True)
+            self._i2c.writeto(self._dev_addr, buffer=data_bytes)
 
     def _end_transaction(self) -> None:
         """Release the bus after sending data."""
index 955b9b4a1a8c803ba66d890bed013b4500379a84..9a2017e20475fff56e32c761645e279a9c72bdf3 100644 (file)
@@ -49,7 +49,7 @@ class TransformStruct:
 
 @dataclass
 class ColorspaceStruct:
-    # pylint: disable=invalid-name
+    # pylint: disable=invalid-name, too-many-instance-attributes
     """Colorspace Struct Dataclass"""
     depth: int
     bytes_per_cell: int = 0
@@ -58,6 +58,7 @@ class ColorspaceStruct:
     grayscale_bit: int = 0
     grayscale: bool = False
     tricolor: bool = False
+    sevencolor: bool = False  # Acep e-ink screens.
     pixels_in_byte_share_row: bool = False
     reverse_pixels_in_byte: bool = False
     reverse_bytes_in_word: bool = False