]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_colorconverter.py
update docs build reqs
[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         self._needs_refresh = False
49
50     @staticmethod
51     def _dither_noise_1(noise):
52         noise = (noise >> 13) ^ noise
53         more_noise = (
54             noise * (noise * noise * 60493 + 19990303) + 1376312589
55         ) & 0x7FFFFFFF
56         return clamp(int((more_noise / (1073741824.0 * 2)) * 255), 0, 0xFFFFFFFF)
57
58     @staticmethod
59     def _dither_noise_2(x, y):
60         return ColorConverter._dither_noise_1(x + y * 0xFFFF)
61
62     @staticmethod
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
68
69     @staticmethod
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
75
76     @staticmethod
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
82
83     @staticmethod
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
89
90     @staticmethod
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)
96
97     @staticmethod
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)
104         if chroma == 0:
105             return 0
106         hue = 0
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
113         if hue < 0:
114             hue += 240
115
116         return hue
117
118     @staticmethod
119     def _compute_sevencolor(color_rgb888: int):
120         # pylint: disable=too-many-return-statements
121         chroma = ColorConverter._compute_chroma(color_rgb888)
122         if chroma >= 64:
123             hue = ColorConverter._compute_hue(color_rgb888)
124             # Red 0
125             if hue < 10:
126                 return 0x4
127             # Orange 21
128             if hue < 21 + 10:
129                 return 0x6
130             # Yellow 42
131             if hue < 42 + 21:
132                 return 0x5
133             # Green 85
134             if hue < 85 + 42:
135                 return 0x2
136             # Blue 170
137             if hue < 170 + 42:
138                 return 0x3
139             # The rest is red to 255
140             return 0x4
141         luma = ColorConverter._compute_luma(color_rgb888)
142         if luma >= 128:
143             return 0x1  # White
144         return 0x0  # Black
145
146     @staticmethod
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:
151                 color = 0
152             else:
153                 color = 1
154         elif not colorspace.grayscale:
155             color = 0
156         return color
157
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):
163             if len(color) == 3:
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")
167         else:
168             raise ValueError("Color must be an integer or 3 or 4 value tuple")
169
170         input_pixel = InputPixelStruct(pixel=color)
171         output_pixel = OutputPixelStruct()
172
173         self._convert(self._output_colorspace, input_pixel, output_pixel)
174
175         return output_pixel.pixel
176
177     def _convert(
178         self,
179         colorspace: Colorspace,
180         input_pixel: InputPixelStruct,
181         output_color: OutputPixelStruct,
182     ) -> None:
183         pixel = input_pixel.pixel
184
185         if self._transparent_color == pixel:
186             output_color.opaque = False
187             return
188
189         if (
190             not self._dither
191             and self._cached_colorspace == colorspace
192             and self._cached_input_pixel == input_pixel.pixel
193         ):
194             output_color.pixel = self._cached_output_color
195             return
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(colorspace, self._dither, rgb888_pixel, output_color)
202
203         if not self._dither:
204             self._cached_colorspace = colorspace
205             self._cached_input_pixel = input_pixel.pixel
206             self._cached_output_color = output_color.pixel
207
208     @staticmethod
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]
212
213     @staticmethod
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)
218         if colorspace in (
219             Colorspace.RGB565_SWAPPED,
220             Colorspace.RGB555_SWAPPED,
221             Colorspace.BGR565_SWAPPED,
222             Colorspace.BGR555_SWAPPED,
223         ):
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
247         return pixel
248
249     @staticmethod
250     def _convert_color(
251         colorspace: ColorspaceStruct,
252         dither: bool,
253         input_pixel: InputPixelStruct,
254         output_color: OutputPixelStruct,
255     ) -> None:
256         # pylint: disable=too-many-return-statements, too-many-branches, too-many-statements
257         pixel = input_pixel.pixel
258         if dither:
259             rand_red = ColorConverter._dither_noise_2(
260                 input_pixel.tile_x, input_pixel.tile_y
261             )
262             rand_grn = ColorConverter._dither_noise_2(
263                 input_pixel.tile_x + 33, input_pixel.tile_y
264             )
265             rand_blu = ColorConverter._dither_noise_2(
266                 input_pixel.tile_x, input_pixel.tile_y + 33
267             )
268
269             red8 = pixel >> 16
270             grn8 = (pixel >> 8) & 0xFF
271             blu8 = pixel & 0xFF
272
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))
277             else:
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
283
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
290             return
291         if colorspace.tricolor:
292             output_color.pixel = ColorConverter._compute_luma(pixel) >> (
293                 8 - colorspace.depth
294             )
295             if ColorConverter._compute_chroma(pixel) <= 16:
296                 if not colorspace.grayscale:
297                     output_color.pixel = 0
298                 output_color.opaque = True
299                 return
300             pixel_hue = ColorConverter._compute_hue(pixel)
301             output_color.pixel = ColorConverter._compute_tricolor(colorspace, pixel_hue)
302             return
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
307             ) & bitmask
308             output_color.opaque = True
309             return
310         if colorspace.depth == 32:
311             output_color.pixel = pixel
312             output_color.opaque = True
313             return
314         if colorspace.depth == 8 and colorspace.grayscale:
315             packed = ColorConverter._compute_rgb332(pixel)
316             output_color.pixel = packed
317             output_color.opaque = True
318             return
319         if colorspace.depth == 4:
320             if colorspace.sevencolor:
321                 packed = ColorConverter._compute_sevencolor(pixel)
322             else:
323                 packed = ColorConverter._compute_rgbd(pixel)
324             output_color.pixel = packed
325             output_color.opaque = True
326             return
327         output_color.opaque = False
328
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.
332         """
333         self._transparent_color = color
334
335     def make_opaque(self, _color: int) -> None:
336         """Make the ColorConverter be opaque and have no transparent pixels."""
337         self._transparent_color = None
338
339     def _finish_refresh(self) -> None:
340         pass
341
342     @property
343     def dither(self) -> bool:
344         """When true the color converter dithers the output by adding
345         random noise when truncating to display bitdepth
346         """
347         return self._dither
348
349     @dither.setter
350     def dither(self, value: bool):
351         if not isinstance(value, bool):
352             raise ValueError("Value should be boolean")
353         self._dither = value