]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/epaperdisplay.py
Merge pull request #1 from makermelissa/master
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / epaperdisplay.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.epaperdisplay`
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 digitalio
40 from recordclass import recordclass
41 from PIL import Image
42 from displayio.bitmap import Bitmap
43 from displayio.colorconverter import ColorConverter
44
45 __version__ = "0.0.0-auto.0"
46 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
47
48 # pylint: disable=unnecessary-pass, unused-argument
49
50 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
51 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
52
53
54 class EPaperDisplay:
55     """Manage updating an epaper display over a display bus
56
57     This initializes an epaper display and connects it into CircuitPython. Unlike other
58     objects in CircuitPython, EPaperDisplay objects live until
59     displayio.release_displays() is called. This is done so that CircuitPython can use
60     the display itself.
61
62     Most people should not use this class directly. Use a specific display driver instead
63     that will contain the startup and shutdown sequences at minimum.
64     """
65
66     # pylint: disable=too-many-locals
67     def __init__(
68         self,
69         display_bus,
70         start_sequence,
71         stop_sequence,
72         *,
73         width,
74         height,
75         ram_width,
76         ram_height,
77         colstart=0,
78         rowstart=0,
79         rotation=0,
80         set_column_window_command=None,
81         set_row_window_command=None,
82         single_byte_bounds=False,
83         write_black_ram_command,
84         black_bits_inverted=False,
85         write_color_ram_command=None,
86         color_bits_inverted=False,
87         highlight_color=0x000000,
88         refresh_display_command,
89         refresh_time=40,
90         busy_pin=None,
91         busy_state=True,
92         seconds_per_frame=180,
93         always_toggle_chip_select=False
94     ):
95         """
96         Create a EPaperDisplay object on the given display bus (displayio.FourWire or
97         displayio.ParallelBus).
98
99         The start_sequence and stop_sequence are bitpacked to minimize the ram impact. Every
100         command begins with a command byte followed by a byte to determine the parameter
101         count and if a delay is need after. When the top bit of the second byte is 1, the
102         next byte will be the delay time in milliseconds. The remaining 7 bits are the
103         parameter count excluding any delay byte. The third through final bytes are the
104         remaining command parameters. The next byte will begin a new command definition.
105         """
106         pass
107
108     # pylint: enable=too-many-locals
109
110     def show(self, group):
111         """Switches to displaying the given group of layers. When group is None, the default
112         CircuitPython terminal will be shown (eventually).
113         """
114         pass
115
116     def refresh(self):
117         """Refreshes the display immediately or raises an exception if too soon. Use
118         ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
119         """
120         pass
121
122     @property
123     def time_to_refresh(self):
124         """Time, in fractional seconds, until the ePaper display can be refreshed."""
125         return 0
126
127     @property
128     def width(self):
129         """Display Width"""
130         pass
131
132     @property
133     def height(self):
134         """Display Height"""
135         pass
136
137     @property
138     def bus(self):
139         """Current Display Bus"""
140         pass
141
142
143 class FourWire:
144     """Manage updating a display over SPI four wire protocol in the background while
145     Python code runs. It doesn’t handle display initialization.
146     """
147
148     def __init__(
149         self,
150         spi_bus,
151         *,
152         command,
153         chip_select,
154         reset=None,
155         baudrate=24000000,
156         polarity=0,
157         phase=0
158     ):
159         """Create a FourWire object associated with the given pins.
160
161         The SPI bus and pins are then in use by the display until
162         displayio.release_displays() is called even after a reload. (It does this so
163         CircuitPython can use the display after your code is done.)
164         So, the first time you initialize a display bus in code.py you should call
165         :py:func`displayio.release_displays` first, otherwise it will error after the
166         first code.py run.
167         """
168         self._dc = digitalio.DigitalInOut(command)
169         self._dc.switch_to_output()
170         self._chip_select = digitalio.DigitalInOut(chip_select)
171         self._chip_select.switch_to_output(value=True)
172
173         if reset is not None:
174             self._reset = digitalio.DigitalInOut(reset)
175             self._reset.switch_to_output(value=True)
176         else:
177             self._reset = None
178         self._spi = spi_bus
179         while self._spi.try_lock():
180             pass
181         self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
182         self._spi.unlock()
183
184     def _release(self):
185         self.reset()
186         self._spi.deinit()
187         self._dc.deinit()
188         self._chip_select.deinit()
189         if self._reset is not None:
190             self._reset.deinit()
191
192     def reset(self):
193         """Performs a hardware reset via the reset pin.
194         Raises an exception if called when no reset pin is available.
195         """
196         if self._reset is not None:
197             self._reset.value = False
198             time.sleep(0.001)
199             self._reset.value = True
200             time.sleep(0.001)
201         else:
202             raise RuntimeError("No reset pin defined")
203
204     def send(self, is_command, data, *, toggle_every_byte=False):
205         """Sends the given command value followed by the full set of data. Display state,
206         such as vertical scroll, set via ``send`` may or may not be reset once the code is
207         done.
208         """
209         while self._spi.try_lock():
210             pass
211         self._dc.value = not is_command
212         if toggle_every_byte:
213             for byte in data:
214                 self._spi.write(bytes([byte]))
215                 self._chip_select.value = True
216                 time.sleep(0.000001)
217                 self._chip_select.value = False
218         else:
219             self._spi.write(data)
220         self._spi.unlock()
221
222
223 class Group:
224     """Manage a group of sprites and groups and how they are inter-related."""
225
226     def __init__(self, *, max_size=4, scale=1, x=0, y=0):
227         """Create a Group of a given size and scale. Scale is in
228         one dimension. For example, scale=2 leads to a layer’s
229         pixel being 2x2 pixels when in the group.
230         """
231         if not isinstance(max_size, int) or max_size < 1:
232             raise ValueError("Max Size must be >= 1")
233         self._max_size = max_size
234         if not isinstance(scale, int) or scale < 1:
235             raise ValueError("Scale must be >= 1")
236         self._scale = scale
237         self._x = x
238         self._y = y
239         self._hidden = False
240         self._layers = []
241         self._supported_types = (TileGrid, Group)
242         self._absolute_transform = None
243         self.in_group = False
244         self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
245
246     def update_transform(self, parent_transform):
247         """Update the parent transform and child transforms"""
248         self.in_group = parent_transform is not None
249         if self.in_group:
250             x = self._x
251             y = self._y
252             if parent_transform.transpose_xy:
253                 x, y = y, x
254             self._absolute_transform.x = parent_transform.x + parent_transform.dx * x
255             self._absolute_transform.y = parent_transform.y + parent_transform.dy * y
256             self._absolute_transform.dx = parent_transform.dx * self._scale
257             self._absolute_transform.dy = parent_transform.dy * self._scale
258             self._absolute_transform.transpose_xy = parent_transform.transpose_xy
259             self._absolute_transform.mirror_x = parent_transform.mirror_x
260             self._absolute_transform.mirror_y = parent_transform.mirror_y
261             self._absolute_transform.scale = parent_transform.scale * self._scale
262         self._update_child_transforms()
263
264     def _update_child_transforms(self):
265         if self.in_group:
266             for layer in self._layers:
267                 layer.update_transform(self._absolute_transform)
268
269     def _removal_cleanup(self, index):
270         layer = self._layers[index]
271         layer.update_transform(None)
272
273     def _layer_update(self, index):
274         layer = self._layers[index]
275         layer.update_transform(self._absolute_transform)
276
277     def append(self, layer):
278         """Append a layer to the group. It will be drawn
279         above other layers.
280         """
281         self.insert(len(self._layers), layer)
282
283     def insert(self, index, layer):
284         """Insert a layer into the group."""
285         if not isinstance(layer, self._supported_types):
286             raise ValueError("Invalid Group Member")
287         if layer.in_group:
288             raise ValueError("Layer already in a group.")
289         if len(self._layers) == self._max_size:
290             raise RuntimeError("Group full")
291         self._layers.insert(index, layer)
292         self._layer_update(index)
293
294     def index(self, layer):
295         """Returns the index of the first copy of layer.
296         Raises ValueError if not found.
297         """
298         return self._layers.index(layer)
299
300     def pop(self, index=-1):
301         """Remove the ith item and return it."""
302         self._removal_cleanup(index)
303         return self._layers.pop(index)
304
305     def remove(self, layer):
306         """Remove the first copy of layer. Raises ValueError
307         if it is not present."""
308         index = self.index(layer)
309         self._layers.pop(index)
310
311     def __len__(self):
312         """Returns the number of layers in a Group"""
313         return len(self._layers)
314
315     def __getitem__(self, index):
316         """Returns the value at the given index."""
317         return self._layers[index]
318
319     def __setitem__(self, index, value):
320         """Sets the value at the given index."""
321         self._removal_cleanup(index)
322         self._layers[index] = value
323         self._layer_update(index)
324
325     def __delitem__(self, index):
326         """Deletes the value at the given index."""
327         del self._layers[index]
328
329     def _fill_area(self, buffer):
330         if self._hidden:
331             return
332
333         for layer in self._layers:
334             if isinstance(layer, (Group, TileGrid)):
335                 layer._fill_area(buffer)  # pylint: disable=protected-access
336
337     @property
338     def hidden(self):
339         """True when the Group and all of it’s layers are not visible. When False, the
340         Group’s layers are visible if they haven’t been hidden.
341         """
342         return self._hidden
343
344     @hidden.setter
345     def hidden(self, value):
346         if not isinstance(value, (bool, int)):
347             raise ValueError("Expecting a boolean or integer value")
348         self._hidden = bool(value)
349
350     @property
351     def scale(self):
352         """Scales each pixel within the Group in both directions. For example, when
353         scale=2 each pixel will be represented by 2x2 pixels.
354         """
355         return self._scale
356
357     @scale.setter
358     def scale(self, value):
359         if not isinstance(value, int) or value < 1:
360             raise ValueError("Scale must be >= 1")
361         if self._scale != value:
362             parent_scale = self._absolute_transform.scale / self._scale
363             self._absolute_transform.dx = (
364                 self._absolute_transform.dx / self._scale * value
365             )
366             self._absolute_transform.dy = (
367                 self._absolute_transform.dy / self._scale * value
368             )
369             self._absolute_transform.scale = parent_scale * value
370
371             self._scale = value
372             self._update_child_transforms()
373
374     @property
375     def x(self):
376         """X position of the Group in the parent."""
377         return self._x
378
379     @x.setter
380     def x(self, value):
381         if not isinstance(value, int):
382             raise ValueError("x must be an integer")
383         if self._x != value:
384             if self._absolute_transform.transpose_xy:
385                 dy_value = self._absolute_transform.dy / self._scale
386                 self._absolute_transform.y += dy_value * (value - self._x)
387             else:
388                 dx_value = self._absolute_transform.dx / self._scale
389                 self._absolute_transform.x += dx_value * (value - self._x)
390             self._x = value
391             self._update_child_transforms()
392
393     @property
394     def y(self):
395         """Y position of the Group in the parent."""
396         return self._y
397
398     @y.setter
399     def y(self, value):
400         if not isinstance(value, int):
401             raise ValueError("y must be an integer")
402         if self._y != value:
403             if self._absolute_transform.transpose_xy:
404                 dx_value = self._absolute_transform.dx / self._scale
405                 self._absolute_transform.x += dx_value * (value - self._y)
406             else:
407                 dy_value = self._absolute_transform.dy / self._scale
408                 self._absolute_transform.y += dy_value * (value - self._y)
409             self._y = value
410             self._update_child_transforms()
411
412
413 class I2CDisplay:
414     """Manage updating a display over I2C in the background while Python code runs.
415     It doesn’t handle display initialization.
416     """
417
418     def __init__(self, i2c_bus, *, device_address, reset=None):
419         """Create a I2CDisplay object associated with the given I2C bus and reset pin.
420
421         The I2C bus and pins are then in use by the display until displayio.release_displays() is
422         called even after a reload. (It does this so CircuitPython can use the display after your
423         code is done.) So, the first time you initialize a display bus in code.py you should call
424         :py:func`displayio.release_displays` first, otherwise it will error after the first
425         code.py run.
426         """
427         pass
428
429     def reset(self):
430         """Performs a hardware reset via the reset pin. Raises an exception if called
431         when no reset pin is available.
432         """
433         pass
434
435     def send(self, command, data):
436         """Sends the given command value followed by the full set of data. Display state,
437         such as vertical scroll, set via send may or may not be reset once the code is
438         done.
439         """
440         pass
441
442
443 class OnDiskBitmap:
444     """
445     Loads values straight from disk. This minimizes memory use but can lead to much slower
446     pixel load times. These load times may result in frame tearing where only part of the
447     image is visible."""
448
449     def __init__(self, file):
450         self._image = Image.open(file)
451
452     @property
453     def width(self):
454         """Width of the bitmap. (read only)"""
455         return self._image.width
456
457     @property
458     def height(self):
459         """Height of the bitmap. (read only)"""
460         return self._image.height
461
462
463 class Palette:
464     """Map a pixel palette_index to a full color. Colors are transformed to the display’s
465     format internally to save memory.
466     """
467
468     def __init__(self, color_count):
469         """Create a Palette object to store a set number of colors."""
470         self._needs_refresh = False
471
472         self._colors = []
473         for _ in range(color_count):
474             self._colors.append(self._make_color(0))
475
476     def _make_color(self, value, transparent=False):
477         color = {
478             "transparent": transparent,
479             "rgb888": 0,
480         }
481         if isinstance(value, (tuple, list, bytes, bytearray)):
482             value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
483         elif isinstance(value, int):
484             if not 0 <= value <= 0xFFFFFF:
485                 raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
486         else:
487             raise TypeError("Color buffer must be a buffer, tuple, list, or int")
488         color["rgb888"] = value
489         self._needs_refresh = True
490
491         return color
492
493     def __len__(self):
494         """Returns the number of colors in a Palette"""
495         return len(self._colors)
496
497     def __setitem__(self, index, value):
498         """Sets the pixel color at the given index. The index should be
499         an integer in the range 0 to color_count-1.
500
501         The value argument represents a color, and can be from 0x000000 to 0xFFFFFF
502         (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
503         4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
504         """
505         if self._colors[index]["rgb888"] != value:
506             self._colors[index] = self._make_color(value)
507
508     def __getitem__(self, index):
509         if not 0 <= index < len(self._colors):
510             raise ValueError("Palette index out of range")
511         return self._colors[index]
512
513     def make_transparent(self, palette_index):
514         """Set the palette index to be a transparent color"""
515         self._colors[palette_index]["transparent"] = True
516
517     def make_opaque(self, palette_index):
518         """Set the palette index to be an opaque color"""
519         self._colors[palette_index]["transparent"] = False
520
521
522 class ParallelBus:
523     """Manage updating a display over 8-bit parallel bus in the background while Python code
524     runs. This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
525     It doesn’t handle display initialization.
526     """
527
528     def __init__(self, i2c_bus, *, device_address, reset=None):
529         """Create a ParallelBus object associated with the given pins. The
530         bus is inferred from data0 by implying the next 7 additional pins on a given GPIO
531         port.
532
533         The parallel bus and pins are then in use by the display until
534         displayio.release_displays() is called even after a reload. (It does this so
535         CircuitPython can use the display after your code is done.) So, the first time you
536         initialize a display bus in code.py you should call
537         :py:func`displayio.release_displays` first, otherwise it will error after the first
538         code.py run.
539         """
540         pass
541
542     def reset(self):
543         """Performs a hardware reset via the reset pin. Raises an exception if called when
544         no reset pin is available.
545         """
546         pass
547
548     def send(self, command, data):
549         """Sends the given command value followed by the full set of data. Display state,
550         such as vertical scroll, set via ``send`` may or may not be reset once the code is
551         done.
552         """
553         pass
554
555
556 class Shape(Bitmap):
557     """Create a Shape object with the given fixed size. Each pixel is one bit and is stored
558     by the column boundaries of the shape on each row. Each row’s boundary defaults to the
559     full row.
560     """
561
562     def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
563         """Create a Shape object with the given fixed size. Each pixel is one bit and is
564         stored by the column boundaries of the shape on each row. Each row’s boundary
565         defaults to the full row.
566         """
567         super().__init__(width, height, 2)
568
569     def set_boundary(self, y, start_x, end_x):
570         """Loads pre-packed data into the given row."""
571         pass
572
573
574 # pylint: disable=too-many-instance-attributes
575 class TileGrid:
576     """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
577     grids can share bitmaps and pixel shaders.
578
579     A single tile grid is also known as a Sprite.
580     """
581
582     def __init__(
583         self,
584         bitmap,
585         *,
586         pixel_shader,
587         width=1,
588         height=1,
589         tile_width=None,
590         tile_height=None,
591         default_tile=0,
592         x=0,
593         y=0
594     ):
595         """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
596         used to convert the value and its location to a display native pixel color. This may
597         be a simple color palette lookup, a gradient, a pattern or a color transformer.
598
599         tile_width and tile_height match the height of the bitmap by default.
600         """
601         if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
602             raise ValueError("Unsupported Bitmap type")
603         self._bitmap = bitmap
604         bitmap_width = bitmap.width
605         bitmap_height = bitmap.height
606
607         if not isinstance(pixel_shader, (ColorConverter, Palette)):
608             raise ValueError("Unsupported Pixel Shader type")
609         self._pixel_shader = pixel_shader
610         self._hidden = False
611         self._x = x
612         self._y = y
613         self._width = width  # Number of Tiles Wide
614         self._height = height  # Number of Tiles High
615         self._transpose_xy = False
616         self._flip_x = False
617         self._flip_y = False
618         if tile_width is None:
619             tile_width = bitmap_width
620         if tile_height is None:
621             tile_height = bitmap_height
622         if bitmap_width % tile_width != 0:
623             raise ValueError("Tile width must exactly divide bitmap width")
624         self._tile_width = tile_width
625         if bitmap_height % tile_height != 0:
626             raise ValueError("Tile height must exactly divide bitmap height")
627         self._tile_height = tile_height
628         if not 0 <= default_tile <= 255:
629             raise ValueError("Default Tile is out of range")
630         self._pixel_width = width * tile_width
631         self._pixel_height = height * tile_height
632         self._tiles = (self._width * self._height) * [default_tile]
633         self.in_group = False
634         self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
635         self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
636         self._moved = False
637
638     def update_transform(self, absolute_transform):
639         """Update the parent transform and child transforms"""
640         self._absolute_transform = absolute_transform
641         if self._absolute_transform is not None:
642             self._update_current_x()
643             self._update_current_y()
644
645     def _update_current_x(self):
646         if self._transpose_xy:
647             width = self._pixel_height
648         else:
649             width = self._pixel_width
650         if self._absolute_transform.transpose_xy:
651             self._current_area.y1 = (
652                 self._absolute_transform.y + self._absolute_transform.dy * self._x
653             )
654             self._current_area.y2 = (
655                 self._absolute_transform.y
656                 + self._absolute_transform.dy * (self._x + width)
657             )
658             if self._current_area.y2 < self._current_area.y1:
659                 self._current_area.y1, self._current_area.y2 = (
660                     self._current_area.y2,
661                     self._current_area.y1,
662                 )
663         else:
664             self._current_area.x1 = (
665                 self._absolute_transform.x + self._absolute_transform.dx * self._x
666             )
667             self._current_area.x2 = (
668                 self._absolute_transform.x
669                 + self._absolute_transform.dx * (self._x + width)
670             )
671             if self._current_area.x2 < self._current_area.x1:
672                 self._current_area.x1, self._current_area.x2 = (
673                     self._current_area.x2,
674                     self._current_area.x1,
675                 )
676
677     def _update_current_y(self):
678         if self._transpose_xy:
679             height = self._pixel_width
680         else:
681             height = self._pixel_height
682         if self._absolute_transform.transpose_xy:
683             self._current_area.x1 = (
684                 self._absolute_transform.x + self._absolute_transform.dx * self._y
685             )
686             self._current_area.x2 = (
687                 self._absolute_transform.x
688                 + self._absolute_transform.dx * (self._y + height)
689             )
690             if self._current_area.x2 < self._current_area.x1:
691                 self._current_area.x1, self._current_area.x2 = (
692                     self._current_area.x2,
693                     self._current_area.x1,
694                 )
695         else:
696             self._current_area.y1 = (
697                 self._absolute_transform.y + self._absolute_transform.dy * self._y
698             )
699             self._current_area.y2 = (
700                 self._absolute_transform.y
701                 + self._absolute_transform.dy * (self._y + height)
702             )
703             if self._current_area.y2 < self._current_area.y1:
704                 self._current_area.y1, self._current_area.y2 = (
705                     self._current_area.y2,
706                     self._current_area.y1,
707                 )
708
709     # pylint: disable=too-many-locals
710     def _fill_area(self, buffer):
711         """Draw onto the image"""
712         if self._hidden:
713             return
714
715         image = Image.new(
716             "RGBA",
717             (self._width * self._tile_width, self._height * self._tile_height),
718             (0, 0, 0, 0),
719         )
720
721         tile_count_x = self._bitmap.width // self._tile_width
722         x = self._x
723         y = self._y
724
725         for tile_x in range(0, self._width):
726             for tile_y in range(0, self._height):
727                 tile_index = self._tiles[tile_y * self._width + tile_x]
728                 tile_index_x = tile_index % tile_count_x
729                 tile_index_y = tile_index // tile_count_x
730                 for pixel_x in range(self._tile_width):
731                     for pixel_y in range(self._tile_height):
732                         image_x = tile_x * self._tile_width + pixel_x
733                         image_y = tile_y * self._tile_height + pixel_y
734                         bitmap_x = tile_index_x * self._tile_width + pixel_x
735                         bitmap_y = tile_index_y * self._tile_height + pixel_y
736                         pixel_color = self._pixel_shader[
737                             self._bitmap[bitmap_x, bitmap_y]
738                         ]
739                         if not pixel_color["transparent"]:
740                             image.putpixel((image_x, image_y), pixel_color["rgb888"])
741         if self._absolute_transform is not None:
742             if self._absolute_transform.scale > 1:
743                 image = image.resize(
744                     (
745                         self._pixel_width * self._absolute_transform.scale,
746                         self._pixel_height * self._absolute_transform.scale,
747                     ),
748                     resample=Image.NEAREST,
749                 )
750             if self._absolute_transform.mirror_x:
751                 image = image.transpose(Image.FLIP_LEFT_RIGHT)
752             if self._absolute_transform.mirror_y:
753                 image = image.transpose(Image.FLIP_TOP_BOTTOM)
754             if self._absolute_transform.transpose_xy:
755                 image = image.transpose(Image.TRANSPOSE)
756             x *= self._absolute_transform.dx
757             y *= self._absolute_transform.dy
758             x += self._absolute_transform.x
759             y += self._absolute_transform.y
760         buffer.alpha_composite(image, (x, y))
761
762     # pylint: enable=too-many-locals
763
764     @property
765     def hidden(self):
766         """True when the TileGrid is hidden. This may be False even
767         when a part of a hidden Group."""
768         return self._hidden
769
770     @hidden.setter
771     def hidden(self, value):
772         if not isinstance(value, (bool, int)):
773             raise ValueError("Expecting a boolean or integer value")
774         self._hidden = bool(value)
775
776     @property
777     def x(self):
778         """X position of the left edge in the parent."""
779         return self._x
780
781     @x.setter
782     def x(self, value):
783         if not isinstance(value, int):
784             raise TypeError("X should be a integer type")
785         if self._x != value:
786             self._x = value
787             self._update_current_x()
788
789     @property
790     def y(self):
791         """Y position of the top edge in the parent."""
792         return self._y
793
794     @y.setter
795     def y(self, value):
796         if not isinstance(value, int):
797             raise TypeError("Y should be a integer type")
798         if self._y != value:
799             self._y = value
800             self._update_current_y()
801
802     @property
803     def flip_x(self):
804         """If true, the left edge rendered will be the right edge of the right-most tile."""
805         return self._flip_x
806
807     @flip_x.setter
808     def flip_x(self, value):
809         if not isinstance(value, bool):
810             raise TypeError("Flip X should be a boolean type")
811         if self._flip_x != value:
812             self._flip_x = value
813
814     @property
815     def flip_y(self):
816         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
817         return self._flip_y
818
819     @flip_y.setter
820     def flip_y(self, value):
821         if not isinstance(value, bool):
822             raise TypeError("Flip Y should be a boolean type")
823         if self._flip_y != value:
824             self._flip_y = value
825
826     @property
827     def transpose_xy(self):
828         """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
829         degree rotation can be achieved along with the corresponding mirrored version.
830         """
831         return self._transpose_xy
832
833     @transpose_xy.setter
834     def transpose_xy(self, value):
835         if not isinstance(value, bool):
836             raise TypeError("Transpose XY should be a boolean type")
837         if self._transpose_xy != value:
838             self._transpose_xy = value
839             self._update_current_x()
840             self._update_current_y()
841
842     @property
843     def pixel_shader(self):
844         """The pixel shader of the tilegrid."""
845         pass
846
847     def __getitem__(self, index):
848         """Returns the tile index at the given index. The index can either be
849         an x,y tuple or an int equal to ``y * width + x``'.
850         """
851         if isinstance(index, (tuple, list)):
852             x = index[0]
853             y = index[1]
854             index = y * self._width + x
855         elif isinstance(index, int):
856             x = index % self._width
857             y = index // self._width
858         if x > self._width or y > self._height or index >= len(self._tiles):
859             raise ValueError("Tile index out of bounds")
860         return self._tiles[index]
861
862     def __setitem__(self, index, value):
863         """Sets the tile index at the given index. The index can either be
864         an x,y tuple or an int equal to ``y * width + x``.
865         """
866         if isinstance(index, (tuple, list)):
867             x = index[0]
868             y = index[1]
869             index = y * self._width + x
870         elif isinstance(index, int):
871             x = index % self._width
872             y = index // self._width
873         if x > self._width or y > self._height or index >= len(self._tiles):
874             raise ValueError("Tile index out of bounds")
875         if not 0 <= value <= 255:
876             raise ValueError("Tile value out of bounds")
877         self._tiles[index] = value
878
879
880 # pylint: enable=too-many-instance-attributes