__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
from ._colorspace import Colorspace
+from ._structs import ColorspaceStruct
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"
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
"""
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