]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio.py
Scaling, Mirroring, etc. now working + linted
[hackapet/Adafruit_Blinka_Displayio.git] / displayio.py
1 # The MIT License (MIT)
2 #
3 # Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
4 #
5 # Permission is hereby granted, free of charge, to any person obtaining a copy
6 # of this software and associated documentation files (the "Software"), to deal
7 # in the Software without restriction, including without limitation the rights
8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 # copies of the Software, and to permit persons to whom the Software is
10 # furnished to do so, subject to the following conditions:
11 #
12 # The above copyright notice and this permission notice shall be included in
13 # all copies or substantial portions of the Software.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 # THE SOFTWARE.
22
23 """
24 `displayio`
25 ================================================================================
26
27 displayio for Blinka
28
29 **Software and Dependencies:**
30
31 * Adafruit Blinka:
32   https://github.com/adafruit/Adafruit_Blinka/releases
33
34 * Author(s): Melissa LeBlanc-Williams
35
36 """
37
38 import time
39 import struct
40 import threading
41 import numpy
42 import digitalio
43 from PIL import Image
44 from recordclass import recordclass
45
46 __version__ = "0.0.0-auto.0"
47 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
48
49 # pylint: disable=unnecessary-pass, unused-argument, too-many-lines
50
51 _displays = []
52
53 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
54 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
55
56
57 def release_displays():
58     """Releases any actively used displays so their busses and pins can be used again.
59
60     Use this once in your code.py if you initialize a display. Place it right before the
61     initialization so the display is active as long as possible.
62     """
63     for _disp in _displays:
64         _disp._release()  # pylint: disable=protected-access
65     _displays.clear()
66
67
68 class Bitmap:
69     """Stores values of a certain size in a 2D array"""
70
71     def __init__(self, width, height, value_count):
72         """Create a Bitmap object with the given fixed size. Each pixel stores a value that is
73         used to index into a corresponding palette. This enables differently colored sprites to
74         share the underlying Bitmap. value_count is used to minimize the memory used to store
75         the Bitmap.
76         """
77         self._width = width
78         self._height = height
79         self._read_only = False
80
81         if value_count < 0:
82             raise ValueError("value_count must be > 0")
83
84         bits = 1
85         while (value_count - 1) >> bits:
86             if bits < 8:
87                 bits = bits << 1
88             else:
89                 bits += 8
90
91         self._bits_per_value = bits
92
93         if (
94             self._bits_per_value > 8
95             and self._bits_per_value != 16
96             and self._bits_per_value != 32
97         ):
98             raise NotImplementedError("Invalid bits per value")
99
100         self._data = (width * height) * [0]
101         self._dirty_area = Rectangle(0, 0, width, height)
102
103     def __getitem__(self, index):
104         """
105         Returns the value at the given index. The index can either be
106         an x,y tuple or an int equal to `y * width + x`.
107         """
108         if isinstance(index, (tuple, list)):
109             index = (index[1] * self._width) + index[0]
110         if index >= len(self._data):
111             raise ValueError("Index {} is out of range".format(index))
112         return self._data[index]
113
114     def __setitem__(self, index, value):
115         """
116         Sets the value at the given index. The index can either be
117         an x,y tuple or an int equal to `y * width + x`.
118         """
119         if self._read_only:
120             raise RuntimeError("Read-only object")
121         if isinstance(index, (tuple, list)):
122             x = index[0]
123             y = index[1]
124             index = y * self._width + x
125         elif isinstance(index, int):
126             x = index % self._width
127             y = index // self._width
128         self._data[index] = value
129         if self._dirty_area.x1 == self._dirty_area.x2:
130             self._dirty_area.x1 = x
131             self._dirty_area.x2 = x + 1
132             self._dirty_area.y1 = y
133             self._dirty_area.y2 = y + 1
134         else:
135             if x < self._dirty_area.x1:
136                 self._dirty_area.x1 = x
137             elif x >= self._dirty_area.x2:
138                 self._dirty_area.x2 = x + 1
139             if y < self._dirty_area.y1:
140                 self._dirty_area.y1 = y
141             elif y >= self._dirty_area.y2:
142                 self._dirty_area.y2 = y + 1
143
144     def _finish_refresh(self):
145         self._dirty_area.x1 = 0
146         self._dirty_area.x2 = 0
147
148     def fill(self, value):
149         """Fills the bitmap with the supplied palette index value."""
150         self._data = (self._width * self._height) * [value]
151         self._dirty_area = Rectangle(0, 0, self._width, self._height)
152
153     @property
154     def width(self):
155         """Width of the bitmap. (read only)"""
156         return self._width
157
158     @property
159     def height(self):
160         """Height of the bitmap. (read only)"""
161         return self._height
162
163
164 class ColorConverter:
165     """Converts one color format to another. Color converter based on original displayio
166     code for consistency.
167     """
168
169     def __init__(self, *, dither=False):
170         """Create a ColorConverter object to convert color formats.
171         Only supports rgb888 to RGB565 currently.
172         :param bool dither: Adds random noise to dither the output image
173         """
174         self._dither = dither
175         self._depth = 16
176
177     # pylint: disable=no-self-use
178     def _compute_rgb565(self, color):
179         self._depth = 16
180         return (color >> 19) << 11 | ((color >> 10) & 0x3F) << 5 | (color >> 3) & 0x1F
181
182     def _compute_luma(self, color):
183         red = color >> 16
184         green = (color >> 8) & 0xFF
185         blue = color & 0xFF
186         return (red * 19) / 255 + (green * 182) / 255 + (blue + 54) / 255
187
188     def _compute_chroma(self, color):
189         red = color >> 16
190         green = (color >> 8) & 0xFF
191         blue = color & 0xFF
192         return max(red, green, blue) - min(red, green, blue)
193
194     def _compute_hue(self, color):
195         red = color >> 16
196         green = (color >> 8) & 0xFF
197         blue = color & 0xFF
198         max_color = max(red, green, blue)
199         chroma = self._compute_chroma(color)
200         if chroma == 0:
201             return 0
202         hue = 0
203         if max_color == red:
204             hue = (((green - blue) * 40) / chroma) % 240
205         elif max_color == green:
206             hue = (((blue - red) + (2 * chroma)) * 40) / chroma
207         elif max_color == blue:
208             hue = (((red - green) + (4 * chroma)) * 40) / chroma
209         if hue < 0:
210             hue += 240
211
212         return hue
213
214     def _dither_noise_1(self, noise):
215         noise = (noise >> 13) ^ noise
216         more_noise = (
217             noise * (noise * noise * 60493 + 19990303) + 1376312589
218         ) & 0x7FFFFFFF
219         return (more_noise / (1073741824.0 * 2)) * 255
220
221     def _dither_noise_2(self, x, y):
222         return self._dither_noise_1(x + y * 0xFFFF)
223
224     def _compute_tricolor(self):
225         pass
226
227     def convert(self, color):
228         "Converts the given rgb888 color to RGB565"
229         if self._dither:
230             return color  # To Do: return a dithered color
231         return self._compute_rgb565(color)
232
233     # pylint: enable=no-self-use
234
235     @property
236     def dither(self):
237         """When true the color converter dithers the output by adding
238         random noise when truncating to display bitdepth
239         """
240         return self._dither
241
242     @dither.setter
243     def dither(self, value):
244         if not isinstance(value, bool):
245             raise ValueError("Value should be boolean")
246         self._dither = value
247
248
249 # pylint: disable=too-many-instance-attributes
250 class Display:
251     """This initializes a display and connects it into CircuitPython. Unlike other objects
252     in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
253     This is done so that CircuitPython can use the display itself.
254
255     Most people should not use this class directly. Use a specific display driver instead
256     that will contain the initialization sequence at minimum.
257
258     .. class::
259         Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0,
260         color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1,
261         reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b,
262         write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None,
263         brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False,
264         auto_refresh=True, native_frames_per_second=60)
265     """
266
267     # pylint: disable=too-many-locals
268     def __init__(
269         self,
270         display_bus,
271         init_sequence,
272         *,
273         width,
274         height,
275         colstart=0,
276         rowstart=0,
277         rotation=0,
278         color_depth=16,
279         grayscale=False,
280         pixels_in_byte_share_row=True,
281         bytes_per_cell=1,
282         reverse_pixels_in_byte=False,
283         set_column_command=0x2A,
284         set_row_command=0x2B,
285         write_ram_command=0x2C,
286         set_vertical_scroll=0,
287         backlight_pin=None,
288         brightness_command=None,
289         brightness=1.0,
290         auto_brightness=False,
291         single_byte_bounds=False,
292         data_as_commands=False,
293         auto_refresh=True,
294         native_frames_per_second=60
295     ):
296         """Create a Display object on the given display bus (`displayio.FourWire` or
297         `displayio.ParallelBus`).
298
299         The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
300         with a command byte followed by a byte to determine the parameter count and if a
301         delay is need after. When the top bit of the second byte is 1, the next byte will be
302         the delay time in milliseconds. The remaining 7 bits are the parameter count
303         excluding any delay byte. The third through final bytes are the remaining command
304         parameters. The next byte will begin a new command definition. Here is a portion of
305         ILI9341 init code:
306         .. code-block:: python
307
308             init_sequence = (
309                 b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F"
310                 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
311                 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
312             )
313             display = displayio.Display(display_bus, init_sequence, width=320, height=240)
314
315         The first command is 0xe1 with 15 (0xf) parameters following. The second and third
316         are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
317         Multiple byte literals (b”“) are merged together on load. The parens are needed to
318         allow byte literals on subsequent lines.
319
320         The initialization sequence should always leave the display memory access inline with
321         the scan of the display to minimize tearing artifacts.
322         """
323         self._bus = display_bus
324         self._set_column_command = set_column_command
325         self._set_row_command = set_row_command
326         self._write_ram_command = write_ram_command
327         self._brightness_command = brightness_command
328         self._data_as_commands = data_as_commands
329         self._single_byte_bounds = single_byte_bounds
330         self._width = width
331         self._height = height
332         self._colstart = colstart
333         self._rowstart = rowstart
334         self._rotation = rotation
335         self._auto_brightness = auto_brightness
336         self._brightness = brightness
337         self._auto_refresh = auto_refresh
338         self._initialize(init_sequence)
339         self._buffer = Image.new("RGBA", (width, height))
340         self._subrectangles = []
341         self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
342         self._current_group = None
343         _displays.append(self)
344         self._refresh_thread = None
345         if self._auto_refresh:
346             self.auto_refresh = True
347
348     # pylint: enable=too-many-locals
349
350     def _initialize(self, init_sequence):
351         i = 0
352         while i < len(init_sequence):
353             command = init_sequence[i]
354             data_size = init_sequence[i + 1]
355             delay = (data_size & 0x80) > 0
356             data_size &= ~0x80
357             self._write(command, init_sequence[i + 2 : i + 2 + data_size])
358             delay_time_ms = 10
359             if delay:
360                 data_size += 1
361                 delay_time_ms = init_sequence[i + 1 + data_size]
362                 if delay_time_ms == 255:
363                     delay_time_ms = 500
364             time.sleep(delay_time_ms / 1000)
365             i += 2 + data_size
366
367     def _write(self, command, data):
368         if self._single_byte_bounds:
369             self._bus.send(True, bytes([command]) + data, toggle_every_byte=True)
370         else:
371             self._bus.send(True, bytes([command]), toggle_every_byte=True)
372             self._bus.send(False, data)
373
374     def _release(self):
375         self._bus.release()
376         self._bus = None
377
378     def show(self, group):
379         """Switches to displaying the given group of layers. When group is None, the
380         default CircuitPython terminal will be shown.
381         """
382         self._current_group = group
383
384     def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
385         """When auto refresh is off, waits for the target frame rate and then refreshes the
386         display, returning True. If the call has taken too long since the last refresh call
387         for the given target frame rate, then the refresh returns False immediately without
388         updating the screen to hopefully help getting caught up.
389
390         If the time since the last successful refresh is below the minimum frame rate, then
391         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
392
393         When auto refresh is on, updates the display immediately. (The display will also
394         update without calls to this.)
395         """
396         # Go through groups and and add each to buffer
397         if self._current_group is not None:
398             buffer = Image.new("RGBA", (self._width, self._height))
399             # Recursively have everything draw to the image
400             self._current_group._fill_area(buffer)  # pylint: disable=protected-access
401             # save image to buffer (or probably refresh buffer so we can compare)
402             self._buffer.paste(buffer)
403         time.sleep(1)
404         # Eventually calculate dirty rectangles here
405         self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
406
407         for area in self._subrectangles:
408             self._refresh_display_area(area)
409
410     def _refresh_loop(self):
411         while self._auto_refresh:
412             self.refresh()
413
414     def _refresh_display_area(self, rectangle):
415         """Loop through dirty rectangles and redraw that area."""
416         data = numpy.array(self._buffer.crop(rectangle).convert("RGB")).astype("uint16")
417         color = (
418             ((data[:, :, 0] & 0xF8) << 8)
419             | ((data[:, :, 1] & 0xFC) << 3)
420             | (data[:, :, 2] >> 3)
421         )
422
423         pixels = list(
424             numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
425         )
426
427         self._write(
428             self._set_column_command,
429             self._encode_pos(
430                 rectangle.x1 + self._colstart, rectangle.x2 + self._colstart
431             ),
432         )
433         self._write(
434             self._set_row_command,
435             self._encode_pos(
436                 rectangle.y1 + self._rowstart, rectangle.y2 + self._rowstart
437             ),
438         )
439         self._write(self._write_ram_command, pixels)
440
441     def _encode_pos(self, x, y):
442         """Encode a postion into bytes."""
443         return struct.pack(self._bounds_encoding, x, y)
444
445     def fill_row(self, y, buffer):
446         """Extract the pixels from a single row"""
447         pass
448
449     @property
450     def auto_refresh(self):
451         """True when the display is refreshed automatically."""
452         return self._auto_refresh
453
454     @auto_refresh.setter
455     def auto_refresh(self, value):
456         self._auto_refresh = value
457         if self._refresh_thread is None:
458             self._refresh_thread = threading.Thread(
459                 target=self._refresh_loop, daemon=True
460             )
461         if value and not self._refresh_thread.is_alive():
462             # Start the thread
463             self._refresh_thread.start()
464         elif not value and self._refresh_thread.is_alive():
465             # Stop the thread
466             self._refresh_thread.join()
467
468     @property
469     def brightness(self):
470         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
471         When `auto_brightness` is True, the value of `brightness` will change automatically.
472         If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
473         """
474         return self._brightness
475
476     @brightness.setter
477     def brightness(self, value):
478         self._brightness = value
479
480     @property
481     def auto_brightness(self):
482         """True when the display brightness is adjusted automatically, based on an ambient
483         light sensor or other method. Note that some displays may have this set to True by
484         default, but not actually implement automatic brightness adjustment.
485         `auto_brightness` is set to False if `brightness` is set manually.
486         """
487         return self._auto_brightness
488
489     @auto_brightness.setter
490     def auto_brightness(self, value):
491         self._auto_brightness = value
492
493     @property
494     def width(self):
495         """Display Width"""
496         return self._width
497
498     @property
499     def height(self):
500         """Display Height"""
501         return self._height
502
503     @property
504     def rotation(self):
505         """The rotation of the display as an int in degrees."""
506         return self._rotation
507
508     @rotation.setter
509     def rotation(self, value):
510         if value not in (0, 90, 180, 270):
511             raise ValueError("Rotation must be 0/90/180/270")
512         self._rotation = value
513
514     @property
515     def bus(self):
516         """Current Display Bus"""
517         return self._bus
518
519
520 # pylint: enable=too-many-instance-attributes
521
522
523 class EPaperDisplay:
524     """Manage updating an epaper display over a display bus
525
526     This initializes an epaper display and connects it into CircuitPython. Unlike other
527     objects in CircuitPython, EPaperDisplay objects live until
528     displayio.release_displays() is called. This is done so that CircuitPython can use
529     the display itself.
530
531     Most people should not use this class directly. Use a specific display driver instead
532     that will contain the startup and shutdown sequences at minimum.
533     """
534
535     # pylint: disable=too-many-locals
536     def __init__(
537         self,
538         display_bus,
539         start_sequence,
540         stop_sequence,
541         *,
542         width,
543         height,
544         ram_width,
545         ram_height,
546         colstart=0,
547         rowstart=0,
548         rotation=0,
549         set_column_window_command=None,
550         set_row_window_command=None,
551         single_byte_bounds=False,
552         write_black_ram_command,
553         black_bits_inverted=False,
554         write_color_ram_command=None,
555         color_bits_inverted=False,
556         highlight_color=0x000000,
557         refresh_display_command,
558         refresh_time=40,
559         busy_pin=None,
560         busy_state=True,
561         seconds_per_frame=180,
562         always_toggle_chip_select=False
563     ):
564         """
565         Create a EPaperDisplay object on the given display bus (displayio.FourWire or
566         displayio.ParallelBus).
567
568         The start_sequence and stop_sequence are bitpacked to minimize the ram impact. Every
569         command begins with a command byte followed by a byte to determine the parameter
570         count and if a delay is need after. When the top bit of the second byte is 1, the
571         next byte will be the delay time in milliseconds. The remaining 7 bits are the
572         parameter count excluding any delay byte. The third through final bytes are the
573         remaining command parameters. The next byte will begin a new command definition.
574         """
575         pass
576
577     # pylint: enable=too-many-locals
578
579     def show(self, group):
580         """Switches to displaying the given group of layers. When group is None, the default
581         CircuitPython terminal will be shown (eventually).
582         """
583         pass
584
585     def refresh(self):
586         """Refreshes the display immediately or raises an exception if too soon. Use
587         ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
588         """
589         pass
590
591     @property
592     def time_to_refresh(self):
593         """Time, in fractional seconds, until the ePaper display can be refreshed."""
594         return 0
595
596     @property
597     def width(self):
598         """Display Width"""
599         pass
600
601     @property
602     def height(self):
603         """Display Height"""
604         pass
605
606     @property
607     def bus(self):
608         """Current Display Bus"""
609         pass
610
611
612 class FourWire:
613     """Manage updating a display over SPI four wire protocol in the background while
614     Python code runs. It doesn’t handle display initialization.
615     """
616
617     def __init__(
618         self,
619         spi_bus,
620         *,
621         command,
622         chip_select,
623         reset=None,
624         baudrate=24000000,
625         polarity=0,
626         phase=0
627     ):
628         """Create a FourWire object associated with the given pins.
629
630         The SPI bus and pins are then in use by the display until
631         displayio.release_displays() is called even after a reload. (It does this so
632         CircuitPython can use the display after your code is done.)
633         So, the first time you initialize a display bus in code.py you should call
634         :py:func`displayio.release_displays` first, otherwise it will error after the
635         first code.py run.
636         """
637         self._dc = digitalio.DigitalInOut(command)
638         self._dc.switch_to_output()
639         self._chip_select = digitalio.DigitalInOut(chip_select)
640         self._chip_select.switch_to_output(value=True)
641
642         if reset is not None:
643             self._reset = digitalio.DigitalInOut(reset)
644             self._reset.switch_to_output(value=True)
645         else:
646             self._reset = None
647         self._spi = spi_bus
648         while self._spi.try_lock():
649             pass
650         self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
651         self._spi.unlock()
652
653     def _release(self):
654         self.reset()
655         self._spi.deinit()
656         self._dc.deinit()
657         self._chip_select.deinit()
658         if self._reset is not None:
659             self._reset.deinit()
660
661     def reset(self):
662         """Performs a hardware reset via the reset pin.
663         Raises an exception if called when no reset pin is available.
664         """
665         if self._reset is not None:
666             self._reset.value = False
667             time.sleep(0.001)
668             self._reset.value = True
669             time.sleep(0.001)
670         else:
671             raise RuntimeError("No reset pin defined")
672
673     def send(self, is_command, data, *, toggle_every_byte=False):
674         """Sends the given command value followed by the full set of data. Display state,
675         such as vertical scroll, set via ``send`` may or may not be reset once the code is
676         done.
677         """
678         while self._spi.try_lock():
679             pass
680         self._dc.value = not is_command
681         if toggle_every_byte:
682             for byte in data:
683                 self._spi.write(bytes([byte]))
684                 self._chip_select.value = True
685                 time.sleep(0.000001)
686                 self._chip_select.value = False
687         else:
688             self._spi.write(data)
689         self._spi.unlock()
690
691
692 class Group:
693     """Manage a group of sprites and groups and how they are inter-related."""
694
695     def __init__(self, *, max_size=4, scale=1, x=0, y=0):
696         """Create a Group of a given size and scale. Scale is in
697         one dimension. For example, scale=2 leads to a layer’s
698         pixel being 2x2 pixels when in the group.
699         """
700         if not isinstance(max_size, int) or max_size < 1:
701             raise ValueError("Max Size must be >= 1")
702         self._max_size = max_size
703         if not isinstance(scale, int) or scale < 1:
704             raise ValueError("Scale must be >= 1")
705         self._scale = scale
706         self._x = x
707         self._y = y
708         self._hidden = False
709         self._layers = []
710         self._supported_types = (TileGrid, Group)
711         self._absolute_transform = None
712         self.in_group = False
713         self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
714
715     def update_transform(self, parent_transform):
716         """Update the parent transform and child transforms"""
717         self.in_group = parent_transform is not None
718         if self.in_group:
719             x = self._x
720             y = self._y
721             if parent_transform.transpose_xy:
722                 x, y = y, x
723             self._absolute_transform.x = parent_transform.x + parent_transform.dx * x
724             self._absolute_transform.y = parent_transform.y + parent_transform.dy * y
725             self._absolute_transform.dx = parent_transform.dx * self._scale
726             self._absolute_transform.dy = parent_transform.dy * self._scale
727             self._absolute_transform.transpose_xy = parent_transform.transpose_xy
728             self._absolute_transform.mirror_x = parent_transform.mirror_x
729             self._absolute_transform.mirror_y = parent_transform.mirror_y
730             self._absolute_transform.scale = parent_transform.scale * self._scale
731         self._update_child_transforms()
732
733     def _update_child_transforms(self):
734         if self.in_group:
735             for layer in self._layers:
736                 layer.update_transform(self._absolute_transform)
737
738     def _removal_cleanup(self, index):
739         layer = self._layers[index]
740         layer.update_transform(None)
741
742     def _layer_update(self, index):
743         layer = self._layers[index]
744         layer.update_transform(self._absolute_transform)
745
746     def append(self, layer):
747         """Append a layer to the group. It will be drawn
748         above other layers.
749         """
750         self.insert(len(self._layers), layer)
751
752     def insert(self, index, layer):
753         """Insert a layer into the group."""
754         if not isinstance(layer, self._supported_types):
755             raise ValueError("Invalid Group Member")
756         if layer.in_group:
757             raise ValueError("Layer already in a group.")
758         if len(self._layers) == self._max_size:
759             raise RuntimeError("Group full")
760         self._layers.insert(index, layer)
761         self._layer_update(index)
762
763     def index(self, layer):
764         """Returns the index of the first copy of layer.
765         Raises ValueError if not found.
766         """
767         return self._layers.index(layer)
768
769     def pop(self, index=-1):
770         """Remove the ith item and return it."""
771         self._removal_cleanup(index)
772         return self._layers.pop(index)
773
774     def remove(self, layer):
775         """Remove the first copy of layer. Raises ValueError
776         if it is not present."""
777         index = self.index(layer)
778         self._layers.pop(index)
779
780     def __len__(self):
781         """Returns the number of layers in a Group"""
782         return len(self._layers)
783
784     def __getitem__(self, index):
785         """Returns the value at the given index."""
786         return self._layers[index]
787
788     def __setitem__(self, index, value):
789         """Sets the value at the given index."""
790         self._removal_cleanup(index)
791         self._layers[index] = value
792         self._layer_update(index)
793
794     def __delitem__(self, index):
795         """Deletes the value at the given index."""
796         del self._layers[index]
797
798     def _fill_area(self, buffer):
799         if self._hidden:
800             return
801
802         for layer in self._layers:
803             if isinstance(layer, (Group, TileGrid)):
804                 layer._fill_area(buffer)  # pylint: disable=protected-access
805
806     @property
807     def hidden(self):
808         """True when the Group and all of it’s layers are not visible. When False, the
809         Group’s layers are visible if they haven’t been hidden.
810         """
811         return self._hidden
812
813     @hidden.setter
814     def hidden(self, value):
815         if not isinstance(value, (bool, int)):
816             raise ValueError("Expecting a boolean or integer value")
817         self._hidden = bool(value)
818
819     @property
820     def scale(self):
821         """Scales each pixel within the Group in both directions. For example, when
822         scale=2 each pixel will be represented by 2x2 pixels.
823         """
824         return self._scale
825
826     @scale.setter
827     def scale(self, value):
828         if not isinstance(value, int) or value < 1:
829             raise ValueError("Scale must be >= 1")
830         if self._scale != value:
831             parent_scale = self._absolute_transform.scale / self._scale
832             self._absolute_transform.dx = (
833                 self._absolute_transform.dx / self._scale * value
834             )
835             self._absolute_transform.dy = (
836                 self._absolute_transform.dy / self._scale * value
837             )
838             self._absolute_transform.scale = parent_scale * value
839
840             self._scale = value
841             self._update_child_transforms()
842
843     @property
844     def x(self):
845         """X position of the Group in the parent."""
846         return self._x
847
848     @x.setter
849     def x(self, value):
850         if not isinstance(value, int):
851             raise ValueError("x must be an integer")
852         if self._x != value:
853             if self._absolute_transform.transpose_xy:
854                 dy_value = self._absolute_transform.dy / self._scale
855                 self._absolute_transform.y += dy_value * (value - self._x)
856             else:
857                 dx_value = self._absolute_transform.dx / self._scale
858                 self._absolute_transform.x += dx_value * (value - self._x)
859             self._x = value
860             self._update_child_transforms()
861
862     @property
863     def y(self):
864         """Y position of the Group in the parent."""
865         return self._y
866
867     @y.setter
868     def y(self, value):
869         if not isinstance(value, int):
870             raise ValueError("y must be an integer")
871         if self._y != value:
872             if self._absolute_transform.transpose_xy:
873                 dx_value = self._absolute_transform.dx / self._scale
874                 self._absolute_transform.x += dx_value * (value - self._y)
875             else:
876                 dy_value = self._absolute_transform.dy / self._scale
877                 self._absolute_transform.y += dy_value * (value - self._y)
878             self._y = value
879             self._update_child_transforms()
880
881
882 class I2CDisplay:
883     """Manage updating a display over I2C in the background while Python code runs.
884     It doesn’t handle display initialization.
885     """
886
887     def __init__(self, i2c_bus, *, device_address, reset=None):
888         """Create a I2CDisplay object associated with the given I2C bus and reset pin.
889
890         The I2C bus and pins are then in use by the display until displayio.release_displays() is
891         called even after a reload. (It does this so CircuitPython can use the display after your
892         code is done.) So, the first time you initialize a display bus in code.py you should call
893         :py:func`displayio.release_displays` first, otherwise it will error after the first
894         code.py run.
895         """
896         pass
897
898     def reset(self):
899         """Performs a hardware reset via the reset pin. Raises an exception if called
900         when no reset pin is available.
901         """
902         pass
903
904     def send(self, command, data):
905         """Sends the given command value followed by the full set of data. Display state,
906         such as vertical scroll, set via send may or may not be reset once the code is
907         done.
908         """
909         pass
910
911
912 class OnDiskBitmap:
913     """
914     Loads values straight from disk. This minimizes memory use but can lead to much slower
915     pixel load times. These load times may result in frame tearing where only part of the
916     image is visible."""
917
918     def __init__(self, file):
919         self._image = Image.open(file)
920
921     @property
922     def width(self):
923         """Width of the bitmap. (read only)"""
924         return self._image.width
925
926     @property
927     def height(self):
928         """Height of the bitmap. (read only)"""
929         return self._image.height
930
931
932 class Palette:
933     """Map a pixel palette_index to a full color. Colors are transformed to the display’s
934     format internally to save memory.
935     """
936
937     def __init__(self, color_count):
938         """Create a Palette object to store a set number of colors."""
939         self._needs_refresh = False
940
941         self._colors = []
942         for _ in range(color_count):
943             self._colors.append(self._make_color(0))
944             self._update_rgba(len(self._colors) - 1)
945
946     def _update_rgba(self, index):
947         color = self._colors[index]["rgb888"]
948         transparent = self._colors[index]["transparent"]
949         self._colors[index]["rgba"] = (
950             color >> 16,
951             (color >> 8) & 0xFF,
952             color & 0xFF,
953             0 if transparent else 0xFF,
954         )
955
956     def _make_color(self, value, transparent=False):
957         color = {
958             "transparent": transparent,
959             "rgb888": 0,
960             "rgba": (0, 0, 0, 255),
961         }
962         if isinstance(value, (tuple, list, bytes, bytearray)):
963             value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
964         elif isinstance(value, int):
965             if not 0 <= value <= 0xFFFFFF:
966                 raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
967         else:
968             raise TypeError("Color buffer must be a buffer, tuple, list, or int")
969         color["rgb888"] = value
970         self._needs_refresh = True
971
972         return color
973
974     def __len__(self):
975         """Returns the number of colors in a Palette"""
976         return len(self._colors)
977
978     def __setitem__(self, index, value):
979         """Sets the pixel color at the given index. The index should be
980         an integer in the range 0 to color_count-1.
981
982         The value argument represents a color, and can be from 0x000000 to 0xFFFFFF
983         (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
984         4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
985         """
986         if self._colors[index]["rgb888"] != value:
987             self._colors[index] = self._make_color(value)
988             self._update_rgba(index)
989
990     def __getitem__(self, index):
991         if not 0 <= index < len(self._colors):
992             raise ValueError("Palette index out of range")
993         return self._colors[index]
994
995     def make_transparent(self, palette_index):
996         """Set the palette index to be a transparent color"""
997         self._colors[palette_index]["transparent"] = True
998         self._update_rgba(palette_index)
999
1000     def make_opaque(self, palette_index):
1001         """Set the palette index to be an opaque color"""
1002         self._colors[palette_index]["transparent"] = False
1003         self._update_rgba(palette_index)
1004
1005
1006 class ParallelBus:
1007     """Manage updating a display over 8-bit parallel bus in the background while Python code
1008     runs. This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
1009     It doesn’t handle display initialization.
1010     """
1011
1012     def __init__(self, i2c_bus, *, device_address, reset=None):
1013         """Create a ParallelBus object associated with the given pins. The
1014         bus is inferred from data0 by implying the next 7 additional pins on a given GPIO
1015         port.
1016
1017         The parallel bus and pins are then in use by the display until
1018         displayio.release_displays() is called even after a reload. (It does this so
1019         CircuitPython can use the display after your code is done.) So, the first time you
1020         initialize a display bus in code.py you should call
1021         :py:func`displayio.release_displays` first, otherwise it will error after the first
1022         code.py run.
1023         """
1024         pass
1025
1026     def reset(self):
1027         """Performs a hardware reset via the reset pin. Raises an exception if called when
1028         no reset pin is available.
1029         """
1030         pass
1031
1032     def send(self, command, data):
1033         """Sends the given command value followed by the full set of data. Display state,
1034         such as vertical scroll, set via ``send`` may or may not be reset once the code is
1035         done.
1036         """
1037         pass
1038
1039
1040 class Shape(Bitmap):
1041     """Create a Shape object with the given fixed size. Each pixel is one bit and is stored
1042     by the column boundaries of the shape on each row. Each row’s boundary defaults to the
1043     full row.
1044     """
1045
1046     def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
1047         """Create a Shape object with the given fixed size. Each pixel is one bit and is
1048         stored by the column boundaries of the shape on each row. Each row’s boundary
1049         defaults to the full row.
1050         """
1051         super().__init__(width, height, 2)
1052
1053     def set_boundary(self, y, start_x, end_x):
1054         """Loads pre-packed data into the given row."""
1055         pass
1056
1057
1058 # pylint: disable=too-many-instance-attributes
1059 class TileGrid:
1060     """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
1061     grids can share bitmaps and pixel shaders.
1062
1063     A single tile grid is also known as a Sprite.
1064     """
1065
1066     def __init__(
1067         self,
1068         bitmap,
1069         *,
1070         pixel_shader,
1071         width=1,
1072         height=1,
1073         tile_width=None,
1074         tile_height=None,
1075         default_tile=0,
1076         x=0,
1077         y=0
1078     ):
1079         """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
1080         used to convert the value and its location to a display native pixel color. This may
1081         be a simple color palette lookup, a gradient, a pattern or a color transformer.
1082
1083         tile_width and tile_height match the height of the bitmap by default.
1084         """
1085         if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
1086             raise ValueError("Unsupported Bitmap type")
1087         self._bitmap = bitmap
1088         bitmap_width = bitmap.width
1089         bitmap_height = bitmap.height
1090
1091         if not isinstance(pixel_shader, (ColorConverter, Palette)):
1092             raise ValueError("Unsupported Pixel Shader type")
1093         self._pixel_shader = pixel_shader
1094         self._hidden = False
1095         self._x = x
1096         self._y = y
1097         self._width = width  # Number of Tiles Wide
1098         self._height = height  # Number of Tiles High
1099         self._transpose_xy = False
1100         self._flip_x = False
1101         self._flip_y = False
1102         if tile_width is None:
1103             tile_width = bitmap_width
1104         if tile_height is None:
1105             tile_height = bitmap_height
1106         if bitmap_width % tile_width != 0:
1107             raise ValueError("Tile width must exactly divide bitmap width")
1108         self._tile_width = tile_width
1109         if bitmap_height % tile_height != 0:
1110             raise ValueError("Tile height must exactly divide bitmap height")
1111         self._tile_height = tile_height
1112         if not 0 <= default_tile <= 255:
1113             raise ValueError("Default Tile is out of range")
1114         self._pixel_width = width * tile_width
1115         self._pixel_height = height * tile_height
1116         self._tiles = (self._width * self._height) * [default_tile]
1117         self.in_group = False
1118         self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
1119         self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
1120         self._moved = False
1121
1122     def update_transform(self, absolute_transform):
1123         """Update the parent transform and child transforms"""
1124         self._absolute_transform = absolute_transform
1125         if self._absolute_transform is not None:
1126             self._update_current_x()
1127             self._update_current_y()
1128
1129     def _update_current_x(self):
1130         if self._transpose_xy:
1131             width = self._pixel_height
1132         else:
1133             width = self._pixel_width
1134         if self._absolute_transform.transpose_xy:
1135             self._current_area.y1 = (
1136                 self._absolute_transform.y + self._absolute_transform.dy * self._x
1137             )
1138             self._current_area.y2 = (
1139                 self._absolute_transform.y
1140                 + self._absolute_transform.dy * (self._x + width)
1141             )
1142             if self._current_area.y2 < self._current_area.y1:
1143                 self._current_area.y1, self._current_area.y2 = (
1144                     self._current_area.y2,
1145                     self._current_area.y1,
1146                 )
1147         else:
1148             self._current_area.x1 = (
1149                 self._absolute_transform.x + self._absolute_transform.dx * self._x
1150             )
1151             self._current_area.x2 = (
1152                 self._absolute_transform.x
1153                 + self._absolute_transform.dx * (self._x + width)
1154             )
1155             if self._current_area.x2 < self._current_area.x1:
1156                 self._current_area.x1, self._current_area.x2 = (
1157                     self._current_area.x2,
1158                     self._current_area.x1,
1159                 )
1160
1161     def _update_current_y(self):
1162         if self._transpose_xy:
1163             height = self._pixel_width
1164         else:
1165             height = self._pixel_height
1166         if self._absolute_transform.transpose_xy:
1167             self._current_area.x1 = (
1168                 self._absolute_transform.x + self._absolute_transform.dx * self._y
1169             )
1170             self._current_area.x2 = (
1171                 self._absolute_transform.x
1172                 + self._absolute_transform.dx * (self._y + height)
1173             )
1174             if self._current_area.x2 < self._current_area.x1:
1175                 self._current_area.x1, self._current_area.x2 = (
1176                     self._current_area.x2,
1177                     self._current_area.x1,
1178                 )
1179         else:
1180             self._current_area.y1 = (
1181                 self._absolute_transform.y + self._absolute_transform.dy * self._y
1182             )
1183             self._current_area.y2 = (
1184                 self._absolute_transform.y
1185                 + self._absolute_transform.dy * (self._y + height)
1186             )
1187             if self._current_area.y2 < self._current_area.y1:
1188                 self._current_area.y1, self._current_area.y2 = (
1189                     self._current_area.y2,
1190                     self._current_area.y1,
1191                 )
1192
1193     # pylint: disable=too-many-locals
1194     def _fill_area(self, buffer):
1195         """Draw onto the image"""
1196         if self._hidden:
1197             return
1198
1199         image = Image.new(
1200             "RGBA", (self._width * self._tile_width, self._height * self._tile_height)
1201         )
1202
1203         tile_count_x = self._bitmap.width // self._tile_width
1204         x = self._x
1205         y = self._y
1206
1207         # TODO: Fix transparency
1208
1209         for tile_x in range(0, self._width):
1210             for tile_y in range(0, self._height):
1211                 tile_index = self._tiles[tile_y * self._width + tile_x]
1212                 tile_index_x = tile_index % tile_count_x
1213                 tile_index_y = tile_index // tile_count_x
1214                 for pixel_x in range(self._tile_width):
1215                     for pixel_y in range(self._tile_height):
1216                         image_x = tile_x * self._tile_width + pixel_x
1217                         image_y = tile_y * self._tile_height + pixel_y
1218                         bitmap_x = tile_index_x * self._tile_width + pixel_x
1219                         bitmap_y = tile_index_y * self._tile_height + pixel_y
1220                         pixel_color = self._pixel_shader[
1221                             self._bitmap[bitmap_x, bitmap_y]
1222                         ]
1223                         image.putpixel((image_x, image_y), pixel_color["rgba"])
1224
1225         if self._absolute_transform is not None:
1226             if self._absolute_transform.scale > 1:
1227                 image = image.resize(
1228                     (
1229                         self._pixel_width * self._absolute_transform.scale,
1230                         self._pixel_height * self._absolute_transform.scale,
1231                     ),
1232                     resample=Image.NEAREST,
1233                 )
1234             if self._absolute_transform.mirror_x:
1235                 image = image.transpose(Image.FLIP_LEFT_RIGHT)
1236             if self._absolute_transform.mirror_y:
1237                 image = image.transpose(Image.FLIP_TOP_BOTTOM)
1238             if self._absolute_transform.transpose_xy:
1239                 image = image.transpose(Image.TRANSPOSE)
1240             x *= self._absolute_transform.dx
1241             y *= self._absolute_transform.dy
1242             x += self._absolute_transform.x
1243             y += self._absolute_transform.y
1244         buffer.paste(image, (x, y))
1245
1246     # pylint: enable=too-many-locals
1247
1248     @property
1249     def hidden(self):
1250         """True when the TileGrid is hidden. This may be False even
1251         when a part of a hidden Group."""
1252         return self._hidden
1253
1254     @hidden.setter
1255     def hidden(self, value):
1256         if not isinstance(value, (bool, int)):
1257             raise ValueError("Expecting a boolean or integer value")
1258         self._hidden = bool(value)
1259
1260     @property
1261     def x(self):
1262         """X position of the left edge in the parent."""
1263         return self._x
1264
1265     @x.setter
1266     def x(self, value):
1267         if not isinstance(value, int):
1268             raise TypeError("X should be a integer type")
1269         if self._x != value:
1270             self._x = value
1271             self._update_current_x()
1272
1273     @property
1274     def y(self):
1275         """Y position of the top edge in the parent."""
1276         return self._y
1277
1278     @y.setter
1279     def y(self, value):
1280         if not isinstance(value, int):
1281             raise TypeError("Y should be a integer type")
1282         if self._y != value:
1283             self._y = value
1284             self._update_current_y()
1285
1286     @property
1287     def flip_x(self):
1288         """If true, the left edge rendered will be the right edge of the right-most tile."""
1289         return self._flip_x
1290
1291     @flip_x.setter
1292     def flip_x(self, value):
1293         if not isinstance(value, bool):
1294             raise TypeError("Flip X should be a boolean type")
1295         if self._flip_x != value:
1296             self._flip_x = value
1297
1298     @property
1299     def flip_y(self):
1300         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
1301         return self._flip_y
1302
1303     @flip_y.setter
1304     def flip_y(self, value):
1305         if not isinstance(value, bool):
1306             raise TypeError("Flip Y should be a boolean type")
1307         if self._flip_y != value:
1308             self._flip_y = value
1309
1310     @property
1311     def transpose_xy(self):
1312         """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
1313         degree rotation can be achieved along with the corresponding mirrored version.
1314         """
1315         return self._transpose_xy
1316
1317     @transpose_xy.setter
1318     def transpose_xy(self, value):
1319         if not isinstance(value, bool):
1320             raise TypeError("Transpose XY should be a boolean type")
1321         if self._transpose_xy != value:
1322             self._transpose_xy = value
1323             self._update_current_x()
1324             self._update_current_y()
1325
1326     @property
1327     def pixel_shader(self):
1328         """The pixel shader of the tilegrid."""
1329         pass
1330
1331     def __getitem__(self, index):
1332         """Returns the tile index at the given index. The index can either be
1333         an x,y tuple or an int equal to ``y * width + x``'.
1334         """
1335         if isinstance(index, (tuple, list)):
1336             x = index[0]
1337             y = index[1]
1338             index = y * self._width + x
1339         elif isinstance(index, int):
1340             x = index % self._width
1341             y = index // self._width
1342         if x > self._width or y > self._height or index >= len(self._tiles):
1343             raise ValueError("Tile index out of bounds")
1344         return self._tiles[index]
1345
1346     def __setitem__(self, index, value):
1347         """Sets the tile index at the given index. The index can either be
1348         an x,y tuple or an int equal to ``y * width + x``.
1349         """
1350         if isinstance(index, (tuple, list)):
1351             x = index[0]
1352             y = index[1]
1353             index = y * self._width + x
1354         elif isinstance(index, int):
1355             x = index % self._width
1356             y = index // self._width
1357         if x > self._width or y > self._height or index >= len(self._tiles):
1358             raise ValueError("Tile index out of bounds")
1359         if not 0 <= value <= 255:
1360             raise ValueError("Tile value out of bounds")
1361         self._tiles[index] = value
1362
1363
1364 # pylint: enable=too-many-instance-attributes