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