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