]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_colorconverter.py
c3c548100b0e9aef46caa8a449b8c97bc95bf7e6
[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
25
26
27 class ColorConverter:
28     """Converts one color format to another. Color converter based on original displayio
29     code for consistency.
30     """
31
32     def __init__(
33         self, *, input_colorspace: Colorspace = Colorspace.RGB888, dither: bool = False
34     ):
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
38         """
39         self._dither = dither
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
47
48     @staticmethod
49     def _clamp(value, min_value, max_value):
50         return max(min(max_value, value), min_value)
51
52     @staticmethod
53     def _bswap16(value):
54         # ABCD -> 00DC
55         return (value & 0xFF00) >> 8 | (value & 0x00FF) << 8
56
57     def _dither_noise_1(self, noise):
58         noise = (noise >> 13) ^ noise
59         more_noise = (
60             noise * (noise * noise * 60493 + 19990303) + 1376312589
61         ) & 0x7FFFFFFF
62         return self._clamp(int((more_noise / (1073741824.0 * 2)) * 255), 0, 0xFFFFFFFF)
63
64     def _dither_noise_2(self, x, y):
65         return self._dither_noise_1(x + y * 0xFFFF)
66
67     @staticmethod
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
73
74     @staticmethod
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
80
81     @staticmethod
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
87
88     @staticmethod
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
94
95     @staticmethod
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)
101
102     @staticmethod
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)
109         if chroma == 0:
110             return 0
111         hue = 0
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
118         if hue < 0:
119             hue += 240
120
121         return hue
122
123     def _compute_sevencolor(self, color_rgb888: int):
124         # pylint: disable=too-many-return-statements
125         chroma = self._compute_chroma(color_rgb888)
126         if chroma >= 64:
127             hue = self._compute_hue(color_rgb888)
128             # Red 0
129             if hue < 10:
130                 return 0x4
131             # Orange 21
132             if hue < 21 + 10:
133                 return 0x6
134             # Yellow 42
135             if hue < 42 + 21:
136                 return 0x5
137             # Green 85
138             if hue < 85 + 42:
139                 return 0x2
140             # Blue 170
141             if hue < 170 + 42:
142                 return 0x3
143             # The rest is red to 255
144             return 0x4
145         luma = self._compute_luma(color_rgb888)
146         if luma >= 128:
147             return 0x1  # White
148         return 0x0  # Black
149
150     @staticmethod
151     def _compute_tricolor(
152         colorspace: ColorspaceStruct, pixel_hue: int, color: int
153     ) -> 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:
157                 color = 0
158             else:
159                 color = 1
160         elif not colorspace.grayscale:
161             color = 0
162         return color
163
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):
169             if len(color) == 3:
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")
173         else:
174             raise ValueError("Color must be an integer or 3 or 4 value tuple")
175
176         input_pixel = {
177             "pixel": color,
178             "x": 0,
179             "y": 0,
180             "tile": 0,
181             "tile_x": 0,
182             "tile_y": 0,
183         }
184
185         output_pixel = {"pixel": 0, "opaque": False}
186
187         if input_pixel["pixel"] == self._transparent_color:
188             return output_pixel["pixel"]
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             return self._cached_output_color
196
197         rgb888_pixel = input_pixel
198         rgb888_pixel["pixel"] = self._convert_pixel(
199             self._input_colorspace, input_pixel["pixel"]
200         )
201         self._convert_color(
202             self._output_colorspace, self._dither, rgb888_pixel, output_pixel
203         )
204
205         if not self._dither:
206             self._cached_colorspace = self._output_colorspace
207             self._cached_input_pixel = input_pixel["pixel"]
208             self._cached_output_color = output_pixel["pixel"]
209
210         return output_pixel["pixel"]
211
212     def _convert_pixel(self, colorspace: Colorspace, pixel: int) -> int:
213         pixel = self._clamp(pixel, 0, 0xFFFFFFFF)
214         if colorspace in (
215             Colorspace.RGB565_SWAPPED,
216             Colorspace.RGB555_SWAPPED,
217             Colorspace.BGR565_SWAPPED,
218             Colorspace.BGR555_SWAPPED,
219         ):
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
243         return pixel
244
245     def _convert_color(
246         self,
247         colorspace: ColorspaceStruct,
248         dither: bool,
249         input_pixel: dict,
250         output_color: dict,
251     ) -> None:
252         # pylint: disable=too-many-return-statements, too-many-branches, too-many-statements
253         pixel = input_pixel["pixel"]
254         if dither:
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)
258
259             red8 = pixel >> 16
260             grn8 = (pixel >> 8) & 0xFF
261             blu8 = pixel & 0xFF
262
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))
267             else:
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
273
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
280             return
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
287                 return
288             pixel_hue = self._compute_hue(pixel)
289             output_color["pixel"] = self._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                 self._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 = self._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 = self._compute_sevencolor(pixel)
312             else:
313                 packed = self._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