1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
 
   3 # SPDX-License-Identifier: MIT
 
   6 `displayio.colorconverter`
 
   7 ================================================================================
 
  11 **Software and Dependencies:**
 
  14   https://github.com/adafruit/Adafruit_Blinka/releases
 
  16 * Author(s): Melissa LeBlanc-Williams
 
  20 __version__ = "0.0.0+auto.0"
 
  21 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
  23 from ._colorspace import Colorspace
 
  24 from ._structs import ColorspaceStruct, InputPixelStruct, OutputPixelStruct
 
  25 from ._helpers import clamp, bswap16
 
  29     """Converts one color format to another. Color converter based on original displayio
 
  34         self, *, input_colorspace: Colorspace = Colorspace.RGB888, dither: bool = False
 
  36         """Create a ColorConverter object to convert color formats.
 
  37         Only supports rgb888 to RGB565 currently.
 
  38         :param bool dither: Adds random noise to dither the output image
 
  41         self._transparent_color = None
 
  42         self._rgba = False  # Todo set Output colorspace depth to 32 maybe?
 
  43         self._input_colorspace = input_colorspace
 
  44         self._output_colorspace = ColorspaceStruct(16)
 
  45         self._cached_colorspace = None
 
  46         self._cached_input_pixel = None
 
  47         self._cached_output_color = None
 
  48         self._needs_refresh = False
 
  51     def _dither_noise_1(noise):
 
  52         noise = (noise >> 13) ^ noise
 
  54             noise * (noise * noise * 60493 + 19990303) + 1376312589
 
  56         return clamp(int((more_noise / (1073741824.0 * 2)) * 255), 0, 0xFFFFFFFF)
 
  59     def _dither_noise_2(x, y):
 
  60         return ColorConverter._dither_noise_1(x + y * 0xFFFF)
 
  63     def _compute_rgb565(color_rgb888: int):
 
  64         red5 = color_rgb888 >> 19
 
  65         grn6 = (color_rgb888 >> 10) & 0x3F
 
  66         blu5 = (color_rgb888 >> 3) & 0x1F
 
  67         return red5 << 11 | grn6 << 5 | blu5
 
  70     def _compute_rgb332(color_rgb888: int):
 
  71         red3 = color_rgb888 >> 21
 
  72         grn2 = (color_rgb888 >> 13) & 0x7
 
  73         blu2 = (color_rgb888 >> 6) & 0x3
 
  74         return red3 << 5 | grn2 << 3 | blu2
 
  77     def _compute_rgbd(color_rgb888: int):
 
  78         red1 = (color_rgb888 >> 23) & 0x1
 
  79         grn1 = (color_rgb888 >> 15) & 0x1
 
  80         blu1 = (color_rgb888 >> 7) & 0x1
 
  81         return red1 << 3 | grn1 << 2 | blu1 << 1  # | dummy
 
  84     def _compute_luma(color_rgb888: int):
 
  85         red8 = color_rgb888 >> 16
 
  86         grn8 = (color_rgb888 >> 8) & 0xFF
 
  87         blu8 = color_rgb888 & 0xFF
 
  88         return (red8 * 19 + grn8 * 182 + blu8 + 54) // 255
 
  91     def _compute_chroma(color_rgb888: int):
 
  92         red8 = color_rgb888 >> 16
 
  93         grn8 = (color_rgb888 >> 8) & 0xFF
 
  94         blu8 = color_rgb888 & 0xFF
 
  95         return max(red8, grn8, blu8) - min(red8, grn8, blu8)
 
  98     def _compute_hue(color_rgb888: int):
 
  99         red8 = color_rgb888 >> 16
 
 100         grn8 = (color_rgb888 >> 8) & 0xFF
 
 101         blu8 = color_rgb888 & 0xFF
 
 102         max_color = max(red8, grn8, blu8)
 
 103         chroma = max_color - min(red8, grn8, blu8)
 
 107         if max_color == red8:
 
 108             hue = (((grn8 - blu8) * 40) // chroma) % 240
 
 109         elif max_color == grn8:
 
 110             hue = (((blu8 - red8) + (2 * chroma)) * 40) // chroma
 
 111         elif max_color == blu8:
 
 112             hue = (((red8 - grn8) + (4 * chroma)) * 40) // chroma
 
 119     def _compute_sevencolor(color_rgb888: int):
 
 120         # pylint: disable=too-many-return-statements
 
 121         chroma = ColorConverter._compute_chroma(color_rgb888)
 
 123             hue = ColorConverter._compute_hue(color_rgb888)
 
 139             # The rest is red to 255
 
 141         luma = ColorConverter._compute_luma(color_rgb888)
 
 147     def _compute_tricolor(
 
 148         colorspace: ColorspaceStruct, pixel_hue: int, color: int
 
 150         hue_diff = colorspace.tricolor_hue - pixel_hue
 
 151         if -10 <= hue_diff <= 10 or hue_diff <= -220 or hue_diff >= 220:
 
 152             if colorspace.grayscale:
 
 156         elif not colorspace.grayscale:
 
 160     def convert(self, color: int) -> int:
 
 161         "Converts the given rgb888 color to RGB565"
 
 162         if isinstance(color, int):
 
 163             color = ((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF, 255)
 
 164         elif isinstance(color, tuple):
 
 166                 color = (color[0], color[1], color[2], 255)
 
 167             elif len(color) != 4:
 
 168                 raise ValueError("Color must be a 3 or 4 value tuple")
 
 170             raise ValueError("Color must be an integer or 3 or 4 value tuple")
 
 172         input_pixel = InputPixelStruct(color)
 
 173         output_pixel = OutputPixelStruct()
 
 175         self._convert(self._output_colorspace, input_pixel, output_pixel)
 
 177         return output_pixel.pixel
 
 181         colorspace: Colorspace,
 
 182         input_pixel: InputPixelStruct,
 
 183         output_color: OutputPixelStruct,
 
 185         pixel = input_pixel.pixel
 
 187         if self._transparent_color == pixel:
 
 188             output_color.opaque = False
 
 193             and self._cached_colorspace == colorspace
 
 194             and self._cached_input_pixel == input_pixel.pixel
 
 196             output_color = self._cached_output_color
 
 199         rgb888_pixel = input_pixel
 
 200         rgb888_pixel.pixel = self._convert_pixel(
 
 201             self._input_colorspace, input_pixel.pixel
 
 203         self._convert_color(colorspace, self._dither, rgb888_pixel, output_color)
 
 206             self._cached_colorspace = colorspace
 
 207             self._cached_input_pixel = input_pixel.pixel
 
 208             self._cached_output_color = output_color.pixel
 
 211     def _convert_pixel(colorspace: Colorspace, pixel: int) -> int:
 
 212         pixel = clamp(pixel, 0, 0xFFFFFFFF)
 
 214             Colorspace.RGB565_SWAPPED,
 
 215             Colorspace.RGB555_SWAPPED,
 
 216             Colorspace.BGR565_SWAPPED,
 
 217             Colorspace.BGR555_SWAPPED,
 
 219             pixel = bswap16(pixel)
 
 220         if colorspace in (Colorspace.RGB565, Colorspace.RGB565_SWAPPED):
 
 221             red8 = (pixel >> 11) << 3
 
 222             grn8 = ((pixel >> 5) << 2) & 0xFF
 
 223             blu8 = (pixel << 3) & 0xFF
 
 224             return (red8 << 16) | (grn8 << 8) | blu8
 
 225         if colorspace in (Colorspace.RGB555, Colorspace.RGB555_SWAPPED):
 
 226             red8 = (pixel >> 10) << 3
 
 227             grn8 = ((pixel >> 5) << 3) & 0xFF
 
 228             blu8 = (pixel << 3) & 0xFF
 
 229             return (red8 << 16) | (grn8 << 8) | blu8
 
 230         if colorspace in (Colorspace.BGR565, Colorspace.BGR565_SWAPPED):
 
 231             blu8 = (pixel >> 11) << 3
 
 232             grn8 = ((pixel >> 5) << 2) & 0xFF
 
 233             red8 = (pixel << 3) & 0xFF
 
 234             return (red8 << 16) | (grn8 << 8) | blu8
 
 235         if colorspace in (Colorspace.BGR555, Colorspace.BGR555_SWAPPED):
 
 236             blu8 = (pixel >> 10) << 3
 
 237             grn8 = ((pixel >> 5) << 3) & 0xFF
 
 238             red8 = (pixel << 3) & 0xFF
 
 239             return (red8 << 16) | (grn8 << 8) | blu8
 
 240         if colorspace == Colorspace.L8:
 
 241             return (pixel & 0xFF) & 0x01010101
 
 246         colorspace: ColorspaceStruct,
 
 248         input_pixel: InputPixelStruct,
 
 249         output_color: OutputPixelStruct,
 
 251         # pylint: disable=too-many-return-statements, too-many-branches, too-many-statements
 
 252         pixel = input_pixel.pixel
 
 254             rand_red = ColorConverter._dither_noise_2(input_pixel.x, input_pixel.y)
 
 255             rand_grn = ColorConverter._dither_noise_2(input_pixel.x + 33, input_pixel.y)
 
 256             rand_blu = ColorConverter._dither_noise_2(input_pixel.x, input_pixel.y + 33)
 
 259             grn8 = (pixel >> 8) & 0xFF
 
 262             if colorspace.depth == 16:
 
 263                 blu8 = min(255, blu8 + (rand_blu & 0x07))
 
 264                 red8 = min(255, red8 + (rand_red & 0x07))
 
 265                 grn8 = min(255, grn8 + (rand_grn & 0x03))
 
 267                 bitmask = 0xFF >> colorspace.depth
 
 268                 blu8 = min(255, blu8 + (rand_blu & bitmask))
 
 269                 red8 = min(255, red8 + (rand_red & bitmask))
 
 270                 grn8 = min(255, grn8 + (rand_grn & bitmask))
 
 271             pixel = (red8 << 16) | (grn8 << 8) | blu8
 
 273         if colorspace.depth == 16:
 
 274             packed = ColorConverter._compute_rgb565(pixel)
 
 275             if colorspace.reverse_bytes_in_word:
 
 276                 packed = bswap16(packed)
 
 277             output_color.pixel = packed
 
 278             output_color.opaque = True
 
 280         if colorspace.tricolor:
 
 281             output_color.pixel = ColorConverter._compute_luma(pixel) >> (
 
 284             if ColorConverter._compute_chroma(pixel) <= 16:
 
 285                 if not colorspace.grayscale:
 
 286                     output_color.pixel = 0
 
 287                 output_color.opaque = True
 
 289             pixel_hue = ColorConverter._compute_hue(pixel)
 
 290             output_color.pixel = ColorConverter._compute_tricolor(
 
 291                 colorspace, pixel_hue, output_color.pixel
 
 294         if colorspace.grayscale and colorspace.depth <= 8:
 
 295             bitmask = (1 << colorspace.depth) - 1
 
 296             output_color.pixel = (
 
 297                 ColorConverter._compute_luma(pixel) >> colorspace.grayscale_bit
 
 299             output_color.opaque = True
 
 301         if colorspace.depth == 32:
 
 302             output_color.pixel = pixel
 
 303             output_color.opaque = True
 
 305         if colorspace.depth == 8 and colorspace.grayscale:
 
 306             packed = ColorConverter._compute_rgb332(pixel)
 
 307             output_color.pixel = packed
 
 308             output_color.opaque = True
 
 310         if colorspace.depth == 4:
 
 311             if colorspace.sevencolor:
 
 312                 packed = ColorConverter._compute_sevencolor(pixel)
 
 314                 packed = ColorConverter._compute_rgbd(pixel)
 
 315             output_color.pixel = packed
 
 316             output_color.opaque = True
 
 318         output_color.opaque = False
 
 320     def make_transparent(self, color: int) -> None:
 
 321         """Set the transparent color or index for the ColorConverter. This will
 
 322         raise an Exception if there is already a selected transparent index.
 
 324         self._transparent_color = color
 
 326     def make_opaque(self, _color: int) -> None:
 
 327         """Make the ColorConverter be opaque and have no transparent pixels."""
 
 328         self._transparent_color = None
 
 331     def dither(self) -> bool:
 
 332         """When true the color converter dithers the output by adding
 
 333         random noise when truncating to display bitdepth
 
 338     def dither(self, value: bool):
 
 339         if not isinstance(value, bool):
 
 340             raise ValueError("Value should be boolean")