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