]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio.py
Added proper headers, fixed pylint in actions
[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 os
39 import digitalio
40 import time
41 import struct
42 import threading
43 import numpy
44 from collections import namedtuple
45 from PIL import Image, ImageDraw, ImagePalette
46
47 __version__ = "0.0.0-auto.0"
48 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
49
50 _displays = []
51
52 Rectangle = namedtuple("Rectangle", "x1 y1 x2 y2")
53 AbsoluteTransform = namedtuple("AbsoluteTransform", "scale transposexy")
54
55
56 def release_displays():
57     """Releases any actively used displays so their busses and pins can be used again.
58
59     Use this once in your code.py if you initialize a display. Place it right before the
60     initialization so the display is active as long as possible.
61     """
62     for _disp in _displays:
63         _disp._release()
64     _displays.clear()
65
66
67 class Bitmap:
68     """Stores values of a certain size in a 2D array"""
69
70     def __init__(self, width, height, value_count):
71         """Create a Bitmap object with the given fixed size. Each pixel stores a value that is
72         used to index into a corresponding palette. This enables differently colored sprites to
73         share the underlying Bitmap. value_count is used to minimize the memory used to store
74         the Bitmap.
75         """
76         self._width = width
77         self._height = height
78         self._read_only = False
79
80         if value_count < 0:
81             raise ValueError("value_count must be > 0")
82
83         bits = 1
84         while (value_count - 1) >> bits:
85             if bits < 8:
86                 bits = bits << 1
87             else:
88                 bits += 8
89
90         self._bits_per_value = bits
91
92         if (
93             self._bits_per_value > 8
94             and self._bits_per_value != 16
95             and self._bits_per_value != 32
96         ):
97             raise NotImplementedError("Invalid bits per value")
98
99         self._data = (width * height) * [0]
100         self._dirty_area = Rectangle(0, 0, width, height)
101
102     def __getitem__(self, index):
103         """
104         Returns the value at the given index. The index can either be
105         an x,y tuple or an int equal to `y * width + x`.
106         """
107         if isinstance(index, (tuple, list)):
108             index = (index[1] * self._width) + index[0]
109         if index >= len(self._data):
110             raise ValueError("Index {} is out of range".format(index))
111         return self._data[index]
112
113     def __setitem__(self, index, value):
114         """
115         Sets the value at the given index. The index can either be
116         an x,y tuple or an int equal to `y * width + x`.
117         """
118         if self._read_only:
119             raise RuntimeError("Read-only object")
120         if isinstance(index, (tuple, list)):
121             x = index[0]
122             y = index[1]
123             index = y * self._width + x
124         elif ininstance(index, int):
125             x = index % self._width
126             y = index // self._width
127         self._data[index] = value
128         if self._dirty_area.x1 == self._dirty_area.x2:
129             self._dirty_area.x1 = x
130             self._dirty_area.x2 = x + 1
131             self._dirty_area.y1 = y
132             self._dirty_area.y2 = y + 1
133         else:
134             if x < self._dirty_area.x1:
135                 self._dirty_area.x1 = x
136             elif x >= self._dirty_area.x2:
137                 self._dirty_area.x2 = x + 1
138             if y < self._dirty_area.y1:
139                 self._dirty_area.y1 = y
140             elif y >= self._dirty_area.y2:
141                 self._dirty_area.y2 = y + 1
142
143     def _finish_refresh(self):
144         self._dirty_area.x1 = 0
145         self._dirty_area.x2 = 0
146
147     def fill(self, value):
148         """Fills the bitmap with the supplied palette index value."""
149         self._data = (self._width * self._height) * [value]
150         self._dirty_area = Rectangle(0, 0, self._width, self._height)
151
152     @property
153     def width(self):
154         """Width of the bitmap. (read only)"""
155         return self._width
156
157     @property
158     def height(self):
159         """Height of the bitmap. (read only)"""
160         return self._height
161
162
163 class ColorConverter:
164     """Converts one color format to another. Color converter based on original displayio
165     code for consistency.
166     """
167
168     def __init__(self, *, dither=False):
169         """Create a ColorConverter object to convert color formats.
170         Only supports RGB888 to RGB565 currently.
171         :param bool dither: Adds random noise to dither the output image
172         """
173         self._dither = dither
174         self._depth = 16
175
176     def _compute_rgb565(self, color):
177         self._depth = 16
178         return (color >> 19) << 11 | ((color >> 10) & 0x3F) << 5 | (color >> 3) & 0x1F
179
180     def _compute_luma(self, color):
181         r8 = color >> 16
182         g8 = (color >> 8) & 0xFF
183         b8 = color & 0xFF
184         return (r8 * 19) / 255 + (g8 * 182) / 255 + (b8 + 54) / 255
185
186     def _compute_chroma(self, color):
187         r8 = color >> 16
188         g8 = (color >> 8) & 0xFF
189         b8 = color & 0xFF
190         return max(r8, g8, b8) - min(r8, g8, b8)
191
192     def _compute_hue(self, color):
193         r8 = color >> 16
194         g8 = (color >> 8) & 0xFF
195         b8 = color & 0xFF
196         max_color = max(r8, g8, b8)
197         chroma = self._compute_chroma(color)
198         if chroma == 0:
199             return 0
200         hue = 0
201         if max_color == r8:
202             hue = (((g8 - b8) * 40) / chroma) % 240
203         elif max_color == g8:
204             hue = (((b8 - r8) + (2 * chroma)) * 40) / chroma
205         elif max_color == b8:
206             hue = (((r8 - g8) + (4 * chroma)) * 40) / chroma
207         if hue < 0:
208             hue += 240
209
210         return hue
211
212     def _dither_noise_1(self, noise):
213         n = (n >> 13) ^ n
214         nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7FFFFFFF
215         return (nn / (1073741824.0 * 2)) * 255
216
217     def _dither_noise_2(self, x, y):
218         return self._dither_noise_1(x + y * 0xFFFF)
219
220     def _compute_tricolor(self):
221         pass
222
223     def convert(self, color):
224         "Converts the given RGB888 color to RGB565"
225         if self._dither:
226             return color  # To Do: return a dithered color
227         else:
228             return self._compute_rgb565(color)
229
230     def _pil_palette(self):
231         return None
232
233     @property
234     def dither(self):
235         "When true the color converter dithers the output by adding random noise when truncating to display bitdepth"
236         return self._dither
237
238     @dither.setter
239     def dither(self, value):
240         if not isinstance(value, bool):
241             raise ValueError("Value should be boolean")
242         self._dither = value
243
244
245 class Display:
246     """This initializes a display and connects it into CircuitPython. Unlike other objects
247     in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
248     This is done so that CircuitPython can use the display itself.
249
250     Most people should not use this class directly. Use a specific display driver instead
251     that will contain the initialization sequence at minimum.
252     
253     .. class::
254         Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0,
255         color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1,
256         reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b,
257         write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None,
258         brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False,
259         auto_refresh=True, native_frames_per_second=60)
260     
261     """
262
263     def __init__(
264         self,
265         display_bus,
266         init_sequence,
267         *,
268         width,
269         height,
270         colstart=0,
271         rowstart=0,
272         rotation=0,
273         color_depth=16,
274         grayscale=False,
275         pixels_in_byte_share_row=True,
276         bytes_per_cell=1,
277         reverse_pixels_in_byte=False,
278         set_column_command=0x2A,
279         set_row_command=0x2B,
280         write_ram_command=0x2C,
281         set_vertical_scroll=0,
282         backlight_pin=None,
283         brightness_command=None,
284         brightness=1.0,
285         auto_brightness=False,
286         single_byte_bounds=False,
287         data_as_commands=False,
288         auto_refresh=True,
289         native_frames_per_second=60
290     ):
291         """Create a Display object on the given display bus (`displayio.FourWire` or `displayio.ParallelBus`).
292
293         The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins with a command byte
294         followed by a byte to determine the parameter count and if a delay is need after. When the top bit of the
295         second byte is 1, the next byte will be the delay time in milliseconds. The remaining 7 bits are the
296         parameter count excluding any delay byte. The third through final bytes are the remaining command
297         parameters. The next byte will begin a new command definition. Here is a portion of ILI9341 init code:
298         .. code-block:: python
299         
300             init_sequence = (b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F" # Set Gamma
301                 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
302                 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
303             )
304             display = displayio.Display(display_bus, init_sequence, width=320, height=240)
305         
306         The first command is 0xe1 with 15 (0xf) parameters following. The second and third are 0x11 and 0x29
307         respectively with delays (0x80) of 120ms (0x78) and no parameters. Multiple byte literals (b”“) are
308         merged together on load. The parens are needed to allow byte literals on subsequent lines.
309
310         The initialization sequence should always leave the display memory access inline with the scan of
311         the display to minimize tearing artifacts.
312         """
313         self._bus = display_bus
314         self._set_column_command = set_column_command
315         self._set_row_command = set_row_command
316         self._write_ram_command = write_ram_command
317         self._brightness_command = brightness_command
318         self._data_as_commands = data_as_commands
319         self._single_byte_bounds = single_byte_bounds
320         self._width = width
321         self._height = height
322         self._colstart = colstart
323         self._rowstart = rowstart
324         self._rotation = rotation
325         self._auto_brightness = auto_brightness
326         self._brightness = brightness
327         self._auto_refresh = auto_refresh
328         self._initialize(init_sequence)
329         self._buffer = Image.new("RGB", (width, height))
330         self._subrectangles = []
331         self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
332         self._current_group = None
333         _displays.append(self)
334         self._refresh_thread = None
335         if self._auto_refresh:
336             self.auto_refresh = True
337
338     def _initialize(self, init_sequence):
339         i = 0
340         while i < len(init_sequence):
341             command = init_sequence[i]
342             data_size = init_sequence[i + 1]
343             delay = (data_size & 0x80) > 0
344             data_size &= ~0x80
345             data_byte = init_sequence[i + 2]
346             self._write(command, init_sequence[i + 2 : i + 2 + data_size])
347             delay_time_ms = 10
348             if delay:
349                 data_size += 1
350                 delay_time_ms = init_sequence[i + 1 + data_size]
351                 if delay_time_ms == 255:
352                     delay_time_ms = 500
353             time.sleep(delay_time_ms / 1000)
354             i += 2 + data_size
355
356     def _write(self, command, data):
357         if self._single_byte_bounds:
358             self._bus.send(True, bytes([command]) + data, toggle_every_byte=True)
359         else:
360             self._bus.send(True, bytes([command]), toggle_every_byte=True)
361             self._bus.send(False, data)
362
363     def _release(self):
364         self._bus.release()
365         self._bus = None
366
367     def show(self, group):
368         """Switches to displaying the given group of layers. When group is None, the
369         default CircuitPython terminal will be shown.
370         """
371         self._current_group = group
372
373     def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
374         """When auto refresh is off, waits for the target frame rate and then refreshes the display,
375         returning True. If the call has taken too long since the last refresh call for the given target
376         frame rate, then the refresh returns False immediately without updating the screen to hopefully
377         help getting caught up.
378
379         If the time since the last successful refresh is below the minimum frame rate, then an exception
380         will be raised. Set minimum_frames_per_second to 0 to disable.
381
382         When auto refresh is on, updates the display immediately. (The display will also update without
383         calls to this.)
384         """
385         # Go through groups and and add each to buffer
386         if self._current_group is not None:
387             buffer = Image.new("RGB", (self._width, self._height))
388             # Recursively have everything draw to the image
389             self._current_group._fill_area(buffer)
390             # save image to buffer (or probably refresh buffer so we can compare)
391             self._buffer.paste(buffer)
392         print("refreshing")
393         time.sleep(1)
394         # Eventually calculate dirty rectangles here
395         self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
396
397         for area in self._subrectangles:
398             self._refresh_display_area(area)
399
400     def _refresh_loop(self):
401         while self._auto_refresh:
402             self.refresh()
403
404     def _refresh_display_area(self, rectangle):
405         """Loop through dirty rectangles and redraw that area."""
406         """Read or write a block of data."""
407         data = numpy.array(self._buffer.crop(rectangle).convert("RGB")).astype("uint16")
408         color = (
409             ((data[:, :, 0] & 0xF8) << 8)
410             | ((data[:, :, 1] & 0xFC) << 3)
411             | (data[:, :, 2] >> 3)
412         )
413
414         pixels = list(
415             numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
416         )
417
418         self._write(
419             self._set_column_command,
420             self._encode_pos(
421                 rectangle.x1 + self._colstart, rectangle.x2 + self._colstart
422             ),
423         )
424         self._write(
425             self._set_row_command,
426             self._encode_pos(
427                 rectangle.y1 + self._rowstart, rectangle.y2 + self._rowstart
428             ),
429         )
430         self._write(self._write_ram_command, pixels)
431
432     def _encode_pos(self, x, y):
433         """Encode a postion into bytes."""
434         return struct.pack(self._bounds_encoding, x, y)
435
436     def fill_row(self, y, buffer):
437         pass
438
439     @property
440     def auto_refresh(self):
441         return self._auto_refresh
442
443     @auto_refresh.setter
444     def auto_refresh(self, value):
445         self._auto_refresh = value
446         if self._refresh_thread is None:
447             self._refresh_thread = threading.Thread(
448                 target=self._refresh_loop, daemon=True
449             )
450         if value and not self._refresh_thread.is_alive():
451             # Start the thread
452             self._refresh_thread.start()
453         elif not value and self._refresh_thread.is_alive():
454             # Stop the thread
455             self._refresh_thread.join()
456
457     @property
458     def brightness(self):
459         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`. When
460         `auto_brightness` is True, the value of `brightness` will change automatically. If `brightness`
461         is set, `auto_brightness` will be disabled and will be set to False.
462         """
463         return self._brightness
464
465     @brightness.setter
466     def brightness(self, value):
467         self._brightness = value
468
469     @property
470     def auto_brightness(self):
471         """True when the display brightness is adjusted automatically, based on an ambient light sensor
472         or other method. Note that some displays may have this set to True by default, but not actually
473         implement automatic brightness adjustment. `auto_brightness` is set to False if `brightness`
474         is set manually.
475         """
476         return self._auto_brightness
477
478     @auto_brightness.setter
479     def auto_brightness(self, value):
480         self._auto_brightness = value
481
482     @property
483     def width(self):
484         return self._width
485
486     @property
487     def height(self):
488         return self._height
489
490     @property
491     def rotation(self):
492         """The rotation of the display as an int in degrees."""
493         return self._rotation
494
495     @rotation.setter
496     def rotation(self, value):
497         if value not in (0, 90, 180, 270):
498             raise ValueError("Rotation must be 0/90/180/270")
499         self._rotation = value
500
501     @property
502     def bus(self):
503         return self._bus
504
505
506 class EPaperDisplay:
507     def __init__(
508         self,
509         display_bus,
510         start_sequence,
511         stop_sequence,
512         *,
513         width,
514         height,
515         ram_width,
516         ram_height,
517         colstart=0,
518         rowstart=0,
519         rotation=0,
520         set_column_window_command=None,
521         set_row_window_command=None,
522         single_byte_bounds=False,
523         write_black_ram_command,
524         black_bits_inverted=False,
525         write_color_ram_command=None,
526         color_bits_inverted=False,
527         highlight_color=0x000000,
528         refresh_display_command,
529         refresh_time=40,
530         busy_pin=None,
531         busy_state=True,
532         seconds_per_frame=180,
533         always_toggle_chip_select=False
534     ):
535         """
536         Create a EPaperDisplay object on the given display bus (displayio.FourWire or displayio.ParallelBus).
537
538         The start_sequence and stop_sequence are bitpacked to minimize the ram impact. Every command
539         begins with a command byte followed by a byte to determine the parameter count and if a delay
540         is need after. When the top bit of the second byte is 1, the next byte will be the delay time
541         in milliseconds. The remaining 7 bits are the parameter count excluding any delay byte. The
542         third through final bytes are the remaining command parameters. The next byte will begin a
543         new command definition.
544         """
545         pass
546
547     def show(self, group):
548         """Switches to displaying the given group of layers. When group is None, the default CircuitPython
549         terminal will be shown.
550         """
551         pass
552
553     def refresh(self):
554         """Refreshes the display immediately or raises an exception if too soon. Use
555         ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
556         """
557         pass
558
559     @property
560     def time_to_refresh(self):
561         """Time, in fractional seconds, until the ePaper display can be refreshed."""
562         return 0
563
564     @property
565     def width(self):
566         pass
567
568     @property
569     def height(self):
570         pass
571
572     @property
573     def bus(self):
574         pass
575
576
577 class FourWire:
578     """Manage updating a display over SPI four wire protocol in the background while
579     Python code runs. It doesn’t handle display initialization.
580     """
581
582     def __init__(
583         self,
584         spi_bus,
585         *,
586         command,
587         chip_select,
588         reset=None,
589         baudrate=24000000,
590         polarity=0,
591         phase=0
592     ):
593         """Create a FourWire object associated with the given pins.
594
595         The SPI bus and pins are then in use by the display until displayio.release_displays() is called
596         even after a reload. (It does this so CircuitPython can use the display after your code is done.)
597         So, the first time you initialize a display bus in code.py you should call
598         :py:func`displayio.release_displays` first, otherwise it will error after the first code.py run.
599         """
600         self._dc = digitalio.DigitalInOut(command)
601         self._dc.switch_to_output()
602         self._chip_select = digitalio.DigitalInOut(chip_select)
603         self._chip_select.switch_to_output(value=True)
604
605         if reset is not None:
606             self._reset = digitalio.DigitalInOut(reset)
607             self._reset.switch_to_output(value=True)
608         else:
609             self._reset = None
610         self._spi = spi_bus
611         while self._spi.try_lock():
612             pass
613         self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
614         self._spi.unlock()
615
616     def _release(self):
617         self.reset()
618         self._spi.deinit()
619         self._dc.deinit()
620         self._chip_select.deinit()
621         if self._reset is not None:
622             self._reset.deinit()
623
624     def reset(self):
625         if self._reset is not None:
626             self._reset.value = False
627             time.sleep(0.001)
628             self._reset.value = True
629             time.sleep(0.001)
630
631     def send(self, is_command, data, *, toggle_every_byte=False):
632         while self._spi.try_lock():
633             pass
634         self._dc.value = not is_command
635         if toggle_every_byte:
636             for byte in data:
637                 self._spi.write(bytes([byte]))
638                 self._chip_select.value = True
639                 time.sleep(0.000001)
640                 self._chip_select.value = False
641         else:
642             self._spi.write(data)
643         self._spi.unlock()
644
645
646 class Group:
647     """Manage a group of sprites and groups and how they are inter-related."""
648
649     def __init__(self, *, max_size=4, scale=1, x=0, y=0):
650         """Create a Group of a given size and scale. Scale is in
651         one dimension. For example, scale=2 leads to a layer’s
652         pixel being 2x2 pixels when in the group.
653         """
654         if not isinstance(max_size, int) or max_size < 1:
655             raise ValueError("Max Size must be an integer and >= 1")
656         self._max_size = max_size
657         if not isinstance(scale, int) or scale < 1:
658             raise ValueError("Scale must be an integer and >= 1")
659         self._scale = scale
660         self._x = x
661         self._y = y
662         self._hidden = False
663         self._layers = []
664         self._supported_types = (TileGrid, Group)
665
666     def append(self, layer):
667         """Append a layer to the group. It will be drawn
668         above other layers.
669         """
670         if not isinstance(layer, self._supported_types):
671             raise ValueError("Invalid Group Memeber")
672         if len(self._layers) == self._max_size:
673             raise RuntimeError("Group full")
674         self._layers.append(layer)
675
676     def insert(self, index, layer):
677         """Insert a layer into the group."""
678         if not isinstance(layer, self._supported_types):
679             raise ValueError("Invalid Group Memeber")
680         if len(self._layers) == self._max_size:
681             raise RuntimeError("Group full")
682         self._layers.insert(index, layer)
683
684     def index(self, layer):
685         """Returns the index of the first copy of layer.
686         Raises ValueError if not found.
687         """
688         pass
689
690     def pop(self, index=-1):
691         """Remove the ith item and return it."""
692         return self._layers.pop(index)
693
694     def remove(self, layer):
695         """Remove the first copy of layer. Raises ValueError
696         if it is not present."""
697         pass
698
699     def __len__(self):
700         """Returns the number of layers in a Group"""
701         return len(self._layers)
702
703     def __getitem__(self, index):
704         """Returns the value at the given index."""
705         return self._layers[index]
706
707     def __setitem__(self, index, value):
708         """Sets the value at the given index."""
709         self._layers[index] = value
710
711     def __delitem__(self, index):
712         """Deletes the value at the given index."""
713         del self._layers[index]
714
715     def _fill_area(self, buffer):
716         if self._hidden:
717             return
718
719         for layer in self._layers:
720             if isinstance(layer, (Group, TileGrid)):
721                 layer._fill_area(buffer)
722
723     @property
724     def hidden(self):
725         return self._hidden
726
727     @hidden.setter
728     def hidden(self, value):
729         if not isinstance(value, (bool, int)):
730             raise ValueError("Expecting a boolean or integer value")
731         self._hidden = bool(value)
732
733     @property
734     def scale(self):
735         return self._scale
736
737     @scale.setter
738     def scale(self, value):
739         if not isinstance(value, int) or value < 1:
740             raise ValueError("Scale must be an integer and at least 1")
741         self._scale = value
742
743     @property
744     def x(self):
745         return self._x
746
747     @x.setter
748     def x(self, value):
749         if not isinstance(value, int):
750             raise ValueError("x must be an integer")
751         self._x = value
752
753     @property
754     def y(self):
755         return self._y
756
757     @y.setter
758     def y(self, value):
759         if not isinstance(value, int):
760             raise ValueError("y must be an integer")
761         self._y = value
762
763
764 class I2CDisplay:
765     """Manage updating a display over I2C in the background while Python code runs.
766     It doesn’t handle display initialization.
767     """
768
769     def __init__(self, i2c_bus, *, device_address, reset=None):
770         """Create a I2CDisplay object associated with the given I2C bus and reset pin.
771
772         The I2C bus and pins are then in use by the display until displayio.release_displays() is
773         called even after a reload. (It does this so CircuitPython can use the display after your
774         code is done.) So, the first time you initialize a display bus in code.py you should call
775         :py:func`displayio.release_displays` first, otherwise it will error after the first
776         code.py run.
777         """
778         pass
779
780     def reset(self):
781         pass
782
783     def send(self, command, data):
784         pass
785
786
787 class OnDiskBitmap:
788     """
789     Loads values straight from disk. This minimizes memory use but can lead to much slower pixel load times.
790     These load times may result in frame tearing where only part of the image is visible."""
791
792     def __init__(self, file):
793         self._image = Image.open(file)
794
795     @property
796     def width(self):
797         """Width of the bitmap. (read only)"""
798         return self._image.width
799
800     @property
801     def height(self):
802         """Height of the bitmap. (read only)"""
803         return self._image.height
804
805
806 class Palette:
807     """Map a pixel palette_index to a full color. Colors are transformed to the display’s
808     format internally to save memory.
809     """
810
811     def __init__(self, color_count):
812         """Create a Palette object to store a set number of colors."""
813         self._needs_refresh = False
814
815         self._colors = []
816         for _ in range(color_count):
817             self._colors.append(self._make_color(0))
818
819     def _make_color(self, value):
820         color = {
821             "transparent": False,
822             "rgb888": 0,
823         }
824         color_converter = ColorConverter()
825         if isinstance(value, (tuple, list, bytes, bytearray)):
826             value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
827         elif isinstance(value, int):
828             if not 0 <= value <= 0xFFFFFF:
829                 raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
830         else:
831             raise TypeError("Color buffer must be a buffer, tuple, list, or int")
832         color["rgb888"] = value
833         self._needs_refresh = True
834
835         return color
836
837     def __len__(self):
838         """Returns the number of colors in a Palette"""
839         return len(self._colors)
840
841     def __setitem__(self, index, value):
842         """Sets the pixel color at the given index. The index should be
843         an integer in the range 0 to color_count-1.
844
845         The value argument represents a color, and can be from 0x000000 to 0xFFFFFF
846         (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
847         4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
848         """
849         if self._colors[index]["rgb888"] != value:
850             self._colors[index] = self._make_color(value)
851
852     def __getitem__(self, index):
853         if not 0 <= index < len(self._colors):
854             raise ValueError("Palette index out of range")
855         return self._colors[index]
856
857     def make_transparent(self, palette_index):
858         self._colors[palette_index]["transparent"] = True
859
860     def make_opaque(self, palette_index):
861         self._colors[palette_index]["transparent"] = False
862
863
864 class ParallelBus:
865     """Manage updating a display over 8-bit parallel bus in the background while Python code runs.
866     This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
867     It doesn’t handle display initialization.
868     """
869
870     def __init__(self, i2c_bus, *, device_address, reset=None):
871         """Create a ParallelBus object associated with the given pins. The
872         bus is inferred from data0 by implying the next 7 additional pins on a given GPIO port.
873
874         The parallel bus and pins are then in use by the display until displayio.release_displays()
875         is called even after a reload. (It does this so CircuitPython can use the display after your
876         code is done.) So, the first time you initialize a display bus in code.py you should call
877         :py:func`displayio.release_displays` first, otherwise it will error after the first
878         code.py run.
879         """
880         pass
881
882     def reset(self):
883         """Performs a hardware reset via the reset pin. Raises an exception if called when
884         no reset pin is available.
885         """
886         pass
887
888     def send(self, command, data):
889         """Sends the given command value followed by the full set of data. Display state, such as
890         vertical scroll, set via ``send`` may or may not be reset once the code is done.
891         """
892         pass
893
894
895 class Shape(Bitmap):
896     """Create a Shape object with the given fixed size. Each pixel is one bit and is stored by the column
897     boundaries of the shape on each row. Each row’s boundary defaults to the full row.
898     """
899
900     def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
901         """Create a Shape object with the given fixed size. Each pixel is one bit and is stored by the
902         column boundaries of the shape on each row. Each row’s boundary defaults to the full row.
903         """
904         pass
905
906     def set_boundary(self, y, start_x, end_x):
907         """Loads pre-packed data into the given row."""
908         pass
909
910
911 class TileGrid:
912     """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
913     grids can share bitmaps and pixel shaders.
914
915     A single tile grid is also known as a Sprite.
916     """
917
918     def __init__(
919         self,
920         bitmap,
921         *,
922         pixel_shader,
923         width=1,
924         height=1,
925         tile_width=None,
926         tile_height=None,
927         default_tile=0,
928         x=0,
929         y=0
930     ):
931         """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is used to convert
932         the value and its location to a display native pixel color. This may be a simple color palette lookup,
933         a gradient, a pattern or a color transformer.
934
935         tile_width and tile_height match the height of the bitmap by default.
936         """
937         if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
938             raise ValueError("Unsupported Bitmap type")
939         self._bitmap = bitmap
940         bitmap_width = bitmap.width
941         bitmap_height = bitmap.height
942
943         if not isinstance(pixel_shader, (ColorConverter, Palette)):
944             raise ValueError("Unsupported Pixel Shader type")
945         self._pixel_shader = pixel_shader
946         self._hidden = False
947         self._x = x
948         self._y = y
949         self._width = width  # Number of Tiles Wide
950         self._height = height  # Number of Tiles High
951         if tile_width is None:
952             tile_width = bitmap_width
953         if tile_height is None:
954             tile_height = bitmap_height
955         if bitmap_width % tile_width != 0:
956             raise ValueError("Tile width must exactly divide bitmap width")
957         self._tile_width = tile_width
958         if bitmap_height % tile_height != 0:
959             raise ValueError("Tile height must exactly divide bitmap height")
960         self._tile_height = tile_height
961         if not 0 <= default_tile <= 255:
962             raise ValueError("Default Tile is out of range")
963         self._tiles = (self._width * self._height) * [default_tile]
964
965     def _fill_area(self, buffer):
966         """Draw onto the image"""
967         if self._hidden:
968             return
969
970         image = Image.new(
971             "RGB", (self._width * self._tile_width, self._height * self._tile_height)
972         )
973
974         tile_count_x = self._bitmap.width // self._tile_width
975         tile_count_y = self._bitmap.height // self._tile_height
976
977         for tile_x in range(0, self._width):
978             for tile_y in range(0, self._height):
979                 tile_index = self._tiles[tile_y * self._width + tile_x]
980                 tile_index_x = tile_index % tile_count_x
981                 tile_index_y = tile_index // tile_count_x
982                 for pixel_x in range(self._tile_width):
983                     for pixel_y in range(self._tile_height):
984                         image_x = tile_x * self._tile_width + pixel_x
985                         image_y = tile_y * self._tile_height + pixel_y
986                         bitmap_x = tile_index_x * self._tile_width + pixel_x
987                         bitmap_y = tile_index_y * self._tile_height + pixel_y
988                         pixel_color = self._pixel_shader[
989                             self._bitmap[bitmap_x, bitmap_y]
990                         ]
991                         if not pixel_color["transparent"]:
992                             image.putpixel((image_x, image_y), pixel_color["rgb888"])
993
994         # Apply transforms or mirrors or whatever here
995         if self._tile_width == 6:
996             print("Putting at {}".format((self._x, self._y)))
997         buffer.paste(image, (self._x, self._y))
998
999     @property
1000     def hidden(self):
1001         """True when the TileGrid is hidden. This may be False even when a part of a hidden Group."""
1002         return self._hidden
1003
1004     @hidden.setter
1005     def hidden(self, value):
1006         if not isinstance(value, (bool, int)):
1007             raise ValueError("Expecting a boolean or integer value")
1008         self._hidden = bool(value)
1009
1010     @property
1011     def x(self):
1012         """X position of the left edge in the parent."""
1013         return self._x
1014
1015     @property
1016     def y(self):
1017         """Y position of the top edge in the parent."""
1018         return self._y
1019
1020     @property
1021     def flip_x(self):
1022         """If true, the left edge rendered will be the right edge of the right-most tile."""
1023         return self._flip_x
1024
1025     @flip_x.setter
1026     def flip_x(self, value):
1027         if not isinstance(value, bool):
1028             raise TypeError("Flip X should be a boolean type")
1029         self._flip_x = value
1030
1031     @property
1032     def flip_y(self):
1033         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
1034         return self._flip_y
1035
1036     @flip_y.setter
1037     def flip_y(self, value):
1038         if not isinstance(value, bool):
1039             raise TypeError("Flip Y should be a boolean type")
1040         self._flip_y = value
1041
1042     @property
1043     def transpose_xy(self):
1044         """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90 degree
1045         rotation can be achieved along with the corresponding mirrored version.
1046         """
1047         return self._transpose_xy
1048
1049     @transpose_xy.setter
1050     def transpose_xy(self, value):
1051         if not isinstance(value, bool):
1052             raise TypeError("Transpose XY should be a boolean type")
1053         self._transpose_xy = value
1054
1055     @property
1056     def pixel_shader(self):
1057         """The pixel shader of the tilegrid."""
1058         pass
1059
1060     def __getitem__(self, index):
1061         """Returns the tile index at the given index. The index can either be
1062         an x,y tuple or an int equal to ``y * width + x``'.
1063         """
1064         if isinstance(index, (tuple, list)):
1065             x = index[0]
1066             y = index[1]
1067             index = y * self._width + x
1068         elif ininstance(index, int):
1069             x = index % self._width
1070             y = index // self._width
1071         if x > self._width or y > self._height:
1072             raise ValueError("Tile index out of bounds")
1073         return self._tiles[index]
1074
1075     def __setitem__(self, index, value):
1076         """Sets the tile index at the given index. The index can either be
1077         an x,y tuple or an int equal to ``y * width + x``.
1078         """
1079         if isinstance(index, (tuple, list)):
1080             x = index[0]
1081             y = index[1]
1082             index = y * self._width + x
1083         elif ininstance(index, int):
1084             x = index % self._width
1085             y = index // self._width
1086         if x > width or y > self._height or index >= len(self._tiles):
1087             raise ValueError("Tile index out of bounds")
1088         if not 0 <= value <= 255:
1089             raise ValueError("Tile value out of bounds")
1090         self._tiles[index] = value