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
28 """Converts one color format to another. Color converter based on original displayio
33 self, *, input_colorspace: Colorspace = Colorspace.RGB888, dither: bool = False
35 """Create a ColorConverter object to convert color formats.
36 Only supports rgb888 to RGB565 currently.
37 :param bool dither: Adds random noise to dither the output image
40 self._transparent_color = None
41 self._rgba = False # Todo set Output colorspace depth to 32 maybe?
42 self._input_colorspace = input_colorspace
43 self._output_colorspace = ColorspaceStruct(16)
44 self._cached_colorspace = None
45 self._cached_input_pixel = None
46 self._cached_output_color = None
49 def _clamp(value, min_value, max_value):
50 return max(min(max_value, value), min_value)
55 return (value & 0xFF00) >> 8 | (value & 0x00FF) << 8
57 def _dither_noise_1(self, noise):
58 noise = (noise >> 13) ^ noise
60 noise * (noise * noise * 60493 + 19990303) + 1376312589
62 return self._clamp(int((more_noise / (1073741824.0 * 2)) * 255), 0, 0xFFFFFFFF)
64 def _dither_noise_2(self, x, y):
65 return self._dither_noise_1(x + y * 0xFFFF)
68 def _compute_rgb565(color_rgb888: int):
69 red5 = color_rgb888 >> 19
70 grn6 = (color_rgb888 >> 10) & 0x3F
71 blu5 = (color_rgb888 >> 3) & 0x1F
72 return red5 << 11 | grn6 << 5 | blu5
75 def _compute_rgb332(color_rgb888: int):
76 red3 = color_rgb888 >> 21
77 grn2 = (color_rgb888 >> 13) & 0x7
78 blu2 = (color_rgb888 >> 6) & 0x3
79 return red3 << 5 | grn2 << 3 | blu2
82 def _compute_rgbd(color_rgb888: int):
83 red1 = (color_rgb888 >> 23) & 0x1
84 grn1 = (color_rgb888 >> 15) & 0x1
85 blu1 = (color_rgb888 >> 7) & 0x1
86 return red1 << 3 | grn1 << 2 | blu1 << 1 # | dummy
89 def _compute_luma(color_rgb888: int):
90 red8 = color_rgb888 >> 16
91 grn8 = (color_rgb888 >> 8) & 0xFF
92 blu8 = color_rgb888 & 0xFF
93 return (red8 * 19 + grn8 * 182 + blu8 + 54) / 255
96 def _compute_chroma(color_rgb888: int):
97 red8 = color_rgb888 >> 16
98 grn8 = (color_rgb888 >> 8) & 0xFF
99 blu8 = color_rgb888 & 0xFF
100 return max(red8, grn8, blu8) - min(red8, grn8, blu8)
103 def _compute_hue(color_rgb888: int):
104 red8 = color_rgb888 >> 16
105 grn8 = (color_rgb888 >> 8) & 0xFF
106 blu8 = color_rgb888 & 0xFF
107 max_color = max(red8, grn8, blu8)
108 chroma = max_color - min(red8, grn8, blu8)
112 if max_color == red8:
113 hue = (((grn8 - blu8) * 40) / chroma) % 240
114 elif max_color == grn8:
115 hue = (((blu8 - red8) + (2 * chroma)) * 40) / chroma
116 elif max_color == blu8:
117 hue = (((red8 - grn8) + (4 * chroma)) * 40) / chroma
123 def _compute_sevencolor(self, color_rgb888: int):
124 # pylint: disable=too-many-return-statements
125 chroma = self._compute_chroma(color_rgb888)
127 hue = self._compute_hue(color_rgb888)
143 # The rest is red to 255
145 luma = self._compute_luma(color_rgb888)
151 def _compute_tricolor(
152 colorspace: ColorspaceStruct, pixel_hue: int, color: int
154 hue_diff = colorspace.tricolor_hue - pixel_hue
155 if -10 <= hue_diff <= 10 or hue_diff <= -220 or hue_diff >= 220:
156 if colorspace.grayscale:
160 elif not colorspace.grayscale:
164 def convert(self, color: int) -> int:
165 "Converts the given rgb888 color to RGB565"
166 if isinstance(color, int):
167 color = ((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF, 255)
168 elif isinstance(color, tuple):
170 color = (color[0], color[1], color[2], 255)
171 elif len(color) != 4:
172 raise ValueError("Color must be a 3 or 4 value tuple")
174 raise ValueError("Color must be an integer or 3 or 4 value tuple")
185 output_pixel = {"pixel": 0, "opaque": False}
187 if input_pixel["pixel"] == self._transparent_color:
188 return output_pixel["pixel"]
192 and self._cached_colorspace == self._output_colorspace
193 and self._cached_input_pixel == input_pixel["pixel"]
195 return self._cached_output_color
197 rgb888_pixel = input_pixel
198 rgb888_pixel["pixel"] = self._convert_pixel(
199 self._input_colorspace, input_pixel["pixel"]
202 self._output_colorspace, self._dither, rgb888_pixel, output_pixel
206 self._cached_colorspace = self._output_colorspace
207 self._cached_input_pixel = input_pixel["pixel"]
208 self._cached_output_color = output_pixel["pixel"]
210 return output_pixel["pixel"]
212 def _convert_pixel(self, colorspace: Colorspace, pixel: int) -> int:
213 pixel = self._clamp(pixel, 0, 0xFFFFFFFF)
215 Colorspace.RGB565_SWAPPED,
216 Colorspace.RGB555_SWAPPED,
217 Colorspace.BGR565_SWAPPED,
218 Colorspace.BGR555_SWAPPED,
220 pixel = self._bswap16(pixel)
221 if colorspace in (Colorspace.RGB565, Colorspace.RGB565_SWAPPED):
222 red8 = (pixel >> 11) << 3
223 grn8 = ((pixel >> 5) << 2) & 0xFF
224 blu8 = (pixel << 3) & 0xFF
225 return (red8 << 16) | (grn8 << 8) | blu8
226 if colorspace in (Colorspace.RGB555, Colorspace.RGB555_SWAPPED):
227 red8 = (pixel >> 10) << 3
228 grn8 = ((pixel >> 5) << 3) & 0xFF
229 blu8 = (pixel << 3) & 0xFF
230 return (red8 << 16) | (grn8 << 8) | blu8
231 if colorspace in (Colorspace.BGR565, Colorspace.BGR565_SWAPPED):
232 blu8 = (pixel >> 11) << 3
233 grn8 = ((pixel >> 5) << 2) & 0xFF
234 red8 = (pixel << 3) & 0xFF
235 return (red8 << 16) | (grn8 << 8) | blu8
236 if colorspace in (Colorspace.BGR555, Colorspace.BGR555_SWAPPED):
237 blu8 = (pixel >> 10) << 3
238 grn8 = ((pixel >> 5) << 3) & 0xFF
239 red8 = (pixel << 3) & 0xFF
240 return (red8 << 16) | (grn8 << 8) | blu8
241 if colorspace == Colorspace.L8:
242 return (pixel & 0xFF) & 0x01010101
247 colorspace: ColorspaceStruct,
252 # pylint: disable=too-many-return-statements, too-many-branches, too-many-statements
253 pixel = input_pixel["pixel"]
255 rand_red = self._dither_noise_2(input_pixel["x"], input_pixel["y"])
256 rand_grn = self._dither_noise_2(input_pixel["x"] + 33, input_pixel["y"])
257 rand_blu = self._dither_noise_2(input_pixel["x"], input_pixel["y"] + 33)
260 grn8 = (pixel >> 8) & 0xFF
263 if colorspace.depth == 16:
264 blu8 = min(255, blu8 + (rand_blu & 0x07))
265 red8 = min(255, red8 + (rand_red & 0x07))
266 grn8 = min(255, grn8 + (rand_grn & 0x03))
268 bitmask = 0xFF >> colorspace.depth
269 blu8 = min(255, blu8 + (rand_blu & bitmask))
270 red8 = min(255, red8 + (rand_red & bitmask))
271 grn8 = min(255, grn8 + (rand_grn & bitmask))
272 pixel = (red8 << 16) | (grn8 << 8) | blu8
274 if colorspace.depth == 16:
275 packed = self._compute_rgb565(pixel)
276 if colorspace.reverse_bytes_in_word:
277 packed = self._bswap16(packed)
278 output_color["pixel"] = packed
279 output_color["opaque"] = True
281 if colorspace.tricolor:
282 output_color["pixel"] = self._compute_luma(pixel) >> (8 - colorspace.depth)
283 if self._compute_chroma(pixel) <= 16:
284 if not colorspace.grayscale:
285 output_color["pixel"] = 0
286 output_color["opaque"] = True
288 pixel_hue = self._compute_hue(pixel)
289 output_color["pixel"] = self._compute_tricolor(
290 colorspace, pixel_hue, output_color["pixel"]
293 if colorspace.grayscale and colorspace.depth <= 8:
294 bitmask = (1 << colorspace.depth) - 1
295 output_color["pixel"] = (
296 self._compute_luma(pixel) >> colorspace.grayscale_bit
298 output_color["opaque"] = True
300 if colorspace.depth == 32:
301 output_color["pixel"] = pixel
302 output_color["opaque"] = True
304 if colorspace.depth == 8 and colorspace.grayscale:
305 packed = self._compute_rgb332(pixel)
306 output_color["pixel"] = packed
307 output_color["opaque"] = True
309 if colorspace.depth == 4:
310 if colorspace.sevencolor:
311 packed = self._compute_sevencolor(pixel)
313 packed = self._compute_rgbd(pixel)
314 output_color["pixel"] = packed
315 output_color["opaque"] = True
317 output_color["opaque"] = False
319 def make_transparent(self, color: int) -> None:
320 """Set the transparent color or index for the ColorConverter. This will
321 raise an Exception if there is already a selected transparent index.
323 self._transparent_color = color
325 def make_opaque(self, _color: int) -> None:
326 """Make the ColorConverter be opaque and have no transparent pixels."""
327 self._transparent_color = None
330 def dither(self) -> bool:
331 """When true the color converter dithers the output by adding
332 random noise when truncating to display bitdepth
337 def dither(self, value: bool):
338 if not isinstance(value, bool):
339 raise ValueError("Value should be boolean")