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(colorspace: ColorspaceStruct, pixel_hue: int) -> int:
148 hue_diff = colorspace.tricolor_hue - pixel_hue
149 if -10 <= hue_diff <= 10 or hue_diff <= -220 or hue_diff >= 220:
150 if colorspace.grayscale:
154 elif not colorspace.grayscale:
158 def convert(self, color: int) -> int:
159 "Converts the given rgb888 color to RGB565"
160 if isinstance(color, int):
161 color = ((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF, 255)
162 elif isinstance(color, tuple):
164 color = (color[0], color[1], color[2], 255)
165 elif len(color) != 4:
166 raise ValueError("Color must be a 3 or 4 value tuple")
168 raise ValueError("Color must be an integer or 3 or 4 value tuple")
170 input_pixel = InputPixelStruct(pixel=color)
171 output_pixel = OutputPixelStruct()
173 self._convert(self._output_colorspace, input_pixel, output_pixel)
175 return output_pixel.pixel
179 colorspace: Colorspace,
180 input_pixel: InputPixelStruct,
181 output_color: OutputPixelStruct,
183 pixel = input_pixel.pixel
185 if self._transparent_color == pixel:
186 output_color.opaque = False
191 and self._cached_colorspace == colorspace
192 and self._cached_input_pixel == input_pixel.pixel
194 output_color.pixel = self._cached_output_color
197 rgb888_pixel = input_pixel
198 rgb888_pixel.pixel = self._convert_pixel(
199 self._input_colorspace, input_pixel.pixel
201 self._convert_color(colorspace, self._dither, rgb888_pixel, output_color)
204 self._cached_colorspace = colorspace
205 self._cached_input_pixel = input_pixel.pixel
206 self._cached_output_color = output_color.pixel
209 def _rgbtuple_to_hex(color_tuple):
210 """Convert rgb tuple with 0-255 values to hex color value"""
211 return color_tuple[0] << 16 | color_tuple[1] << 8 | color_tuple[2]
214 def _convert_pixel(colorspace: Colorspace, pixel: int) -> int:
215 if isinstance(pixel, tuple):
216 pixel = ColorConverter._rgbtuple_to_hex(pixel)
217 pixel = clamp(pixel, 0, 0xFFFFFFFF)
219 Colorspace.RGB565_SWAPPED,
220 Colorspace.RGB555_SWAPPED,
221 Colorspace.BGR565_SWAPPED,
222 Colorspace.BGR555_SWAPPED,
224 pixel = bswap16(pixel)
225 if colorspace in (Colorspace.RGB565, Colorspace.RGB565_SWAPPED):
226 red8 = (pixel >> 11) << 3
227 grn8 = ((pixel >> 5) << 2) & 0xFF
228 blu8 = (pixel << 3) & 0xFF
229 return (red8 << 16) | (grn8 << 8) | blu8
230 if colorspace in (Colorspace.RGB555, Colorspace.RGB555_SWAPPED):
231 red8 = (pixel >> 10) << 3
232 grn8 = ((pixel >> 5) << 3) & 0xFF
233 blu8 = (pixel << 3) & 0xFF
234 return (red8 << 16) | (grn8 << 8) | blu8
235 if colorspace in (Colorspace.BGR565, Colorspace.BGR565_SWAPPED):
236 blu8 = (pixel >> 11) << 3
237 grn8 = ((pixel >> 5) << 2) & 0xFF
238 red8 = (pixel << 3) & 0xFF
239 return (red8 << 16) | (grn8 << 8) | blu8
240 if colorspace in (Colorspace.BGR555, Colorspace.BGR555_SWAPPED):
241 blu8 = (pixel >> 10) << 3
242 grn8 = ((pixel >> 5) << 3) & 0xFF
243 red8 = (pixel << 3) & 0xFF
244 return (red8 << 16) | (grn8 << 8) | blu8
245 if colorspace == Colorspace.L8:
246 return (pixel & 0xFF) & 0x01010101
251 colorspace: ColorspaceStruct,
253 input_pixel: InputPixelStruct,
254 output_color: OutputPixelStruct,
256 # pylint: disable=too-many-return-statements, too-many-branches, too-many-statements
257 pixel = input_pixel.pixel
259 rand_red = ColorConverter._dither_noise_2(
260 input_pixel.tile_x, input_pixel.tile_y
262 rand_grn = ColorConverter._dither_noise_2(
263 input_pixel.tile_x + 33, input_pixel.tile_y
265 rand_blu = ColorConverter._dither_noise_2(
266 input_pixel.tile_x, input_pixel.tile_y + 33
270 grn8 = (pixel >> 8) & 0xFF
273 if colorspace.depth == 16:
274 blu8 = min(255, blu8 + (rand_blu & 0x07))
275 red8 = min(255, red8 + (rand_red & 0x07))
276 grn8 = min(255, grn8 + (rand_grn & 0x03))
278 bitmask = 0xFF >> colorspace.depth
279 blu8 = min(255, blu8 + (rand_blu & bitmask))
280 red8 = min(255, red8 + (rand_red & bitmask))
281 grn8 = min(255, grn8 + (rand_grn & bitmask))
282 pixel = (red8 << 16) | (grn8 << 8) | blu8
284 if colorspace.depth == 16:
285 packed = ColorConverter._compute_rgb565(pixel)
286 if colorspace.reverse_bytes_in_word:
287 packed = bswap16(packed)
288 output_color.pixel = packed
289 output_color.opaque = True
291 if colorspace.tricolor:
292 output_color.pixel = ColorConverter._compute_luma(pixel) >> (
295 if ColorConverter._compute_chroma(pixel) <= 16:
296 if not colorspace.grayscale:
297 output_color.pixel = 0
298 output_color.opaque = True
300 pixel_hue = ColorConverter._compute_hue(pixel)
301 output_color.pixel = ColorConverter._compute_tricolor(colorspace, pixel_hue)
303 if colorspace.grayscale and colorspace.depth <= 8:
304 bitmask = (1 << colorspace.depth) - 1
305 output_color.pixel = (
306 ColorConverter._compute_luma(pixel) >> colorspace.grayscale_bit
308 output_color.opaque = True
310 if colorspace.depth == 32:
311 output_color.pixel = pixel
312 output_color.opaque = True
314 if colorspace.depth == 8 and colorspace.grayscale:
315 packed = ColorConverter._compute_rgb332(pixel)
316 output_color.pixel = packed
317 output_color.opaque = True
319 if colorspace.depth == 4:
320 if colorspace.sevencolor:
321 packed = ColorConverter._compute_sevencolor(pixel)
323 packed = ColorConverter._compute_rgbd(pixel)
324 output_color.pixel = packed
325 output_color.opaque = True
327 output_color.opaque = False
329 def make_transparent(self, color: int) -> None:
330 """Set the transparent color or index for the ColorConverter. This will
331 raise an Exception if there is already a selected transparent index.
333 self._transparent_color = color
335 def make_opaque(self, _color: int) -> None:
336 """Make the ColorConverter be opaque and have no transparent pixels."""
337 self._transparent_color = None
339 def _finish_refresh(self) -> None:
343 def dither(self) -> bool:
344 """When true the color converter dithers the output by adding
345 random noise when truncating to display bitdepth
350 def dither(self, value: bool):
351 if not isinstance(value, bool):
352 raise ValueError("Value should be boolean")