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