]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_colorconverter.py
More displayio code updates
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _colorconverter.py
1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
2 #
3 # SPDX-License-Identifier: MIT
4
5 """
6 `displayio.colorconverter`
7 ================================================================================
8
9 displayio for Blinka
10
11 **Software and Dependencies:**
12
13 * Adafruit Blinka:
14   https://github.com/adafruit/Adafruit_Blinka/releases
15
16 * Author(s): Melissa LeBlanc-Williams
17
18 """
19
20 __version__ = "0.0.0+auto.0"
21 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
22
23 from ._colorspace import Colorspace
24 from ._structs import ColorspaceStruct, InputPixelStruct, OutputPixelStruct
25 from ._helpers import clamp, bswap16
26
27
28 class ColorConverter:
29     """Converts one color format to another. Color converter based on original displayio
30     code for consistency.
31     """
32
33     def __init__(
34         self, *, input_colorspace: Colorspace = Colorspace.RGB888, dither: bool = False
35     ):
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
39         """
40         self._dither = dither
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
49     @staticmethod
50     def _dither_noise_1(noise):
51         noise = (noise >> 13) ^ noise
52         more_noise = (
53             noise * (noise * noise * 60493 + 19990303) + 1376312589
54         ) & 0x7FFFFFFF
55         return clamp(int((more_noise / (1073741824.0 * 2)) * 255), 0, 0xFFFFFFFF)
56
57     @staticmethod
58     def _dither_noise_2(x, y):
59         return ColorConverter._dither_noise_1(x + y * 0xFFFF)
60
61     @staticmethod
62     def _compute_rgb565(color_rgb888: int):
63         red5 = color_rgb888 >> 19
64         grn6 = (color_rgb888 >> 10) & 0x3F
65         blu5 = (color_rgb888 >> 3) & 0x1F
66         return red5 << 11 | grn6 << 5 | blu5
67
68     @staticmethod
69     def _compute_rgb332(color_rgb888: int):
70         red3 = color_rgb888 >> 21
71         grn2 = (color_rgb888 >> 13) & 0x7
72         blu2 = (color_rgb888 >> 6) & 0x3
73         return red3 << 5 | grn2 << 3 | blu2
74
75     @staticmethod
76     def _compute_rgbd(color_rgb888: int):
77         red1 = (color_rgb888 >> 23) & 0x1
78         grn1 = (color_rgb888 >> 15) & 0x1
79         blu1 = (color_rgb888 >> 7) & 0x1
80         return red1 << 3 | grn1 << 2 | blu1 << 1  # | dummy
81
82     @staticmethod
83     def _compute_luma(color_rgb888: int):
84         red8 = color_rgb888 >> 16
85         grn8 = (color_rgb888 >> 8) & 0xFF
86         blu8 = color_rgb888 & 0xFF
87         return (red8 * 19 + grn8 * 182 + blu8 + 54) / 255
88
89     @staticmethod
90     def _compute_chroma(color_rgb888: int):
91         red8 = color_rgb888 >> 16
92         grn8 = (color_rgb888 >> 8) & 0xFF
93         blu8 = color_rgb888 & 0xFF
94         return max(red8, grn8, blu8) - min(red8, grn8, blu8)
95
96     @staticmethod
97     def _compute_hue(color_rgb888: int):
98         red8 = color_rgb888 >> 16
99         grn8 = (color_rgb888 >> 8) & 0xFF
100         blu8 = color_rgb888 & 0xFF
101         max_color = max(red8, grn8, blu8)
102         chroma = max_color - min(red8, grn8, blu8)
103         if chroma == 0:
104             return 0
105         hue = 0
106         if max_color == red8:
107             hue = (((grn8 - blu8) * 40) / chroma) % 240
108         elif max_color == grn8:
109             hue = (((blu8 - red8) + (2 * chroma)) * 40) / chroma
110         elif max_color == blu8:
111             hue = (((red8 - grn8) + (4 * chroma)) * 40) / chroma
112         if hue < 0:
113             hue += 240
114
115         return hue
116
117     @staticmethod
118     def _compute_sevencolor(color_rgb888: int):
119         # pylint: disable=too-many-return-statements
120         chroma = ColorConverter._compute_chroma(color_rgb888)
121         if chroma >= 64:
122             hue = ColorConverter._compute_hue(color_rgb888)
123             # Red 0
124             if hue < 10:
125                 return 0x4
126             # Orange 21
127             if hue < 21 + 10:
128                 return 0x6
129             # Yellow 42
130             if hue < 42 + 21:
131                 return 0x5
132             # Green 85
133             if hue < 85 + 42:
134                 return 0x2
135             # Blue 170
136             if hue < 170 + 42:
137                 return 0x3
138             # The rest is red to 255
139             return 0x4
140         luma = ColorConverter._compute_luma(color_rgb888)
141         if luma >= 128:
142             return 0x1  # White
143         return 0x0  # Black
144
145     @staticmethod
146     def _compute_tricolor(
147         colorspace: ColorspaceStruct, pixel_hue: int, color: int
148     ) -> int:
149         hue_diff = colorspace.tricolor_hue - pixel_hue
150         if -10 <= hue_diff <= 10 or hue_diff <= -220 or hue_diff >= 220:
151             if colorspace.grayscale:
152                 color = 0
153             else:
154                 color = 1
155         elif not colorspace.grayscale:
156             color = 0
157         return color
158
159     def convert(self, color: int) -> int:
160         "Converts the given rgb888 color to RGB565"
161         if isinstance(color, int):
162             color = ((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF, 255)
163         elif isinstance(color, tuple):
164             if len(color) == 3:
165                 color = (color[0], color[1], color[2], 255)
166             elif len(color) != 4:
167                 raise ValueError("Color must be a 3 or 4 value tuple")
168         else:
169             raise ValueError("Color must be an integer or 3 or 4 value tuple")
170
171         input_pixel = InputPixelStruct(color)
172         output_pixel = OutputPixelStruct()
173
174         self._convert(self._output_colorspace, input_pixel, output_pixel)
175
176         return output_pixel.pixel
177
178     def _convert(
179         self,
180         colorspace: Colorspace,
181         input_pixel: InputPixelStruct,
182         output_color: OutputPixelStruct,
183     ) -> None:
184         pixel = input_pixel.pixel
185
186         if self._transparent_color == pixel:
187             output_color.opaque = False
188             return
189
190         if (
191             not self._dither
192             and self._cached_colorspace == self._output_colorspace
193             and self._cached_input_pixel == input_pixel.pixel
194         ):
195             output_color = self._cached_output_color
196             return
197
198         rgb888_pixel = input_pixel
199         rgb888_pixel.pixel = self._convert_pixel(
200             self._input_colorspace, input_pixel.pixel
201         )
202         self._convert_color(colorspace, self._dither, rgb888_pixel, output_color)
203
204         if not self._dither:
205             self._cached_colorspace = colorspace
206             self._cached_input_pixel = input_pixel.pixel
207             self._cached_output_color = output_color.pixel
208
209     @staticmethod
210     def _convert_pixel(colorspace: Colorspace, pixel: int) -> int:
211         pixel = clamp(pixel, 0, 0xFFFFFFFF)
212         if colorspace in (
213             Colorspace.RGB565_SWAPPED,
214             Colorspace.RGB555_SWAPPED,
215             Colorspace.BGR565_SWAPPED,
216             Colorspace.BGR555_SWAPPED,
217         ):
218             pixel = bswap16(pixel)
219         if colorspace in (Colorspace.RGB565, Colorspace.RGB565_SWAPPED):
220             red8 = (pixel >> 11) << 3
221             grn8 = ((pixel >> 5) << 2) & 0xFF
222             blu8 = (pixel << 3) & 0xFF
223             return (red8 << 16) | (grn8 << 8) | blu8
224         if colorspace in (Colorspace.RGB555, Colorspace.RGB555_SWAPPED):
225             red8 = (pixel >> 10) << 3
226             grn8 = ((pixel >> 5) << 3) & 0xFF
227             blu8 = (pixel << 3) & 0xFF
228             return (red8 << 16) | (grn8 << 8) | blu8
229         if colorspace in (Colorspace.BGR565, Colorspace.BGR565_SWAPPED):
230             blu8 = (pixel >> 11) << 3
231             grn8 = ((pixel >> 5) << 2) & 0xFF
232             red8 = (pixel << 3) & 0xFF
233             return (red8 << 16) | (grn8 << 8) | blu8
234         if colorspace in (Colorspace.BGR555, Colorspace.BGR555_SWAPPED):
235             blu8 = (pixel >> 10) << 3
236             grn8 = ((pixel >> 5) << 3) & 0xFF
237             red8 = (pixel << 3) & 0xFF
238             return (red8 << 16) | (grn8 << 8) | blu8
239         if colorspace == Colorspace.L8:
240             return (pixel & 0xFF) & 0x01010101
241         return pixel
242
243     @staticmethod
244     def _convert_color(
245         colorspace: ColorspaceStruct,
246         dither: bool,
247         input_pixel: InputPixelStruct,
248         output_color: OutputPixelStruct,
249     ) -> None:
250         # pylint: disable=too-many-return-statements, too-many-branches, too-many-statements
251         pixel = input_pixel.pixel
252         if dither:
253             rand_red = ColorConverter._dither_noise_2(input_pixel.x, input_pixel.y)
254             rand_grn = ColorConverter._dither_noise_2(input_pixel.x + 33, input_pixel.y)
255             rand_blu = ColorConverter._dither_noise_2(input_pixel.x, input_pixel.y + 33)
256
257             red8 = pixel >> 16
258             grn8 = (pixel >> 8) & 0xFF
259             blu8 = pixel & 0xFF
260
261             if colorspace.depth == 16:
262                 blu8 = min(255, blu8 + (rand_blu & 0x07))
263                 red8 = min(255, red8 + (rand_red & 0x07))
264                 grn8 = min(255, grn8 + (rand_grn & 0x03))
265             else:
266                 bitmask = 0xFF >> colorspace.depth
267                 blu8 = min(255, blu8 + (rand_blu & bitmask))
268                 red8 = min(255, red8 + (rand_red & bitmask))
269                 grn8 = min(255, grn8 + (rand_grn & bitmask))
270             pixel = (red8 << 16) | (grn8 << 8) | blu8
271
272         if colorspace.depth == 16:
273             packed = ColorConverter._compute_rgb565(pixel)
274             if colorspace.reverse_bytes_in_word:
275                 packed = bswap16(packed)
276             output_color.pixel = packed
277             output_color.opaque = True
278             return
279         if colorspace.tricolor:
280             output_color.pixel = ColorConverter._compute_luma(pixel) >> (
281                 8 - colorspace.depth
282             )
283             if ColorConverter._compute_chroma(pixel) <= 16:
284                 if not colorspace.grayscale:
285                     output_color.pixel = 0
286                 output_color.opaque = True
287                 return
288             pixel_hue = ColorConverter._compute_hue(pixel)
289             output_color.pixel = ColorConverter._compute_tricolor(
290                 colorspace, pixel_hue, output_color.pixel
291             )
292             return
293         if colorspace.grayscale and colorspace.depth <= 8:
294             bitmask = (1 << colorspace.depth) - 1
295             output_color.pixel = (
296                 ColorConverter._compute_luma(pixel) >> colorspace.grayscale_bit
297             ) & bitmask
298             output_color.opaque = True
299             return
300         if colorspace.depth == 32:
301             output_color.pixel = pixel
302             output_color.opaque = True
303             return
304         if colorspace.depth == 8 and colorspace.grayscale:
305             packed = ColorConverter._compute_rgb332(pixel)
306             output_color.pixel = packed
307             output_color.opaque = True
308             return
309         if colorspace.depth == 4:
310             if colorspace.sevencolor:
311                 packed = ColorConverter._compute_sevencolor(pixel)
312             else:
313                 packed = ColorConverter._compute_rgbd(pixel)
314             output_color.pixel = packed
315             output_color.opaque = True
316             return
317         output_color.opaque = False
318
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.
322         """
323         self._transparent_color = color
324
325     def make_opaque(self, _color: int) -> None:
326         """Make the ColorConverter be opaque and have no transparent pixels."""
327         self._transparent_color = None
328
329     @property
330     def dither(self) -> bool:
331         """When true the color converter dithers the output by adding
332         random noise when truncating to display bitdepth
333         """
334         return self._dither
335
336     @dither.setter
337     def dither(self, value: bool):
338         if not isinstance(value, bool):
339             raise ValueError("Value should be boolean")
340         self._dither = value