]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blobdiff - displayio.py
Added proper headers, fixed pylint in actions
[hackapet/Adafruit_Blinka_Displayio.git] / displayio.py
index 3490334960f703886ed880d098d4375cb996cde1..2f7a61a54a41c501e4944b63f37d2008339ba4d7 100644 (file)
@@ -1,47 +1,63 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
 """
 `displayio`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+  https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
 """
 
 import os
 import digitalio
 import time
 import struct
+import threading
 import numpy
 from collections import namedtuple
 from PIL import Image, ImageDraw, ImagePalette
 
-"""
-import asyncio
-import signal
-import subprocess
-"""
-
-# Don't import pillow if we're running in the CI. We could mock it out but that
-# would require mocking in all reverse dependencies.
-if "GITHUB_ACTION" not in os.environ and "READTHEDOCS" not in os.environ:
-    # This will only work on Linux
-    pass
-else:
-    # this would be for Github Actions
-    utils = None  # pylint: disable=invalid-name
-
 __version__ = "0.0.0-auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
 _displays = []
-_groups = []
 
 Rectangle = namedtuple("Rectangle", "x1 y1 x2 y2")
-
-class _DisplayioSingleton:
-    def __init__(self):
-        pass
+AbsoluteTransform = namedtuple("AbsoluteTransform", "scale transposexy")
 
 
 def release_displays():
     """Releases any actively used displays so their busses and pins can be used again.
 
-    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.
+    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.
     """
     for _disp in _displays:
         _disp._release()
@@ -52,7 +68,10 @@ class Bitmap:
     """Stores values of a certain size in a 2D array"""
 
     def __init__(self, width, height, value_count):
-        """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.
+        """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.
         """
         self._width = width
         self._height = height
@@ -86,7 +105,9 @@ class Bitmap:
         an x,y tuple or an int equal to `y * width + x`.
         """
         if isinstance(index, (tuple, list)):
-            index = index[1] * self._width + index[0]
+            index = (index[1] * self._width) + index[0]
+        if index >= len(self._data):
+            raise ValueError("Index {} is out of range".format(index))
         return self._data[index]
 
     def __setitem__(self, index, value):
@@ -206,6 +227,9 @@ class ColorConverter:
         else:
             return self._compute_rgb565(color)
 
+    def _pil_palette(self):
+        return None
+
     @property
     def dither(self):
         "When true the color converter dithers the output by adding random noise when truncating to display bitdepth"
@@ -219,11 +243,20 @@ class ColorConverter:
 
 
 class Display:
-    """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.
+    """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.
 
-    Most people should not use this class directly. Use a specific display driver instead that will contain the initialization sequence at minimum.
+    Most people should not use this class directly. Use a specific display driver instead
+    that will contain the initialization sequence at minimum.
     
-    .. 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)
+    .. 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)
     
     """
 
@@ -257,7 +290,11 @@ class Display:
     ):
         """Create a Display object on the given display bus (`displayio.FourWire` or `displayio.ParallelBus`).
 
-        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:
+        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:
         .. code-block:: python
         
             init_sequence = (b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F" # Set Gamma
@@ -266,9 +303,12 @@ class Display:
             )
             display = displayio.Display(display_bus, init_sequence, width=320, height=240)
         
-        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.
+        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.
 
-        The initialization sequence should always leave the display memory access inline with the scan of the display to minimize tearing artifacts.
+        The initialization sequence should always leave the display memory access inline with the scan of
+        the display to minimize tearing artifacts.
         """
         self._bus = display_bus
         self._set_column_command = set_column_command
@@ -289,10 +329,11 @@ class Display:
         self._buffer = Image.new("RGB", (width, height))
         self._subrectangles = []
         self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
-        self._groups = []
+        self._current_group = None
         _displays.append(self)
+        self._refresh_thread = None
         if self._auto_refresh:
-            self.refresh()
+            self.auto_refresh = True
 
     def _initialize(self, init_sequence):
         i = 0
@@ -324,41 +365,40 @@ class Display:
         self._bus = None
 
     def show(self, group):
-        """Switches to displaying the given group of layers. When group is None, the default CircuitPython terminal will be shown.
+        """Switches to displaying the given group of layers. When group is None, the
+        default CircuitPython terminal will be shown.
         """
-        self._groups.append(group)
-
-    def _group_to_buffer(self, group):
-        """ go through any children and call this function then add group to buffer"""
-        for layer_number in range(len(group.layers)):
-            layer = group.layers[layer_number]
-            if isinstance(layer, Group):
-                self._group_to_buffer(layer)
-            elif isinstance(layer, TileGrid):
-                # Get the TileGrid Info and draw to buffer
-                pass
-            else:
-                raise TypeError("Invalid layer type found in group")
+        self._current_group = group
 
     def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
-        """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.
+        """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.
 
-        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.
+        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.
 
-        When auto refresh is on, updates the display immediately. (The display will also update without calls to this.)
+        When auto refresh is on, updates the display immediately. (The display will also update without
+        calls to this.)
         """
-        
         # Go through groups and and add each to buffer
-        #for group in self._groups:
-            
-        
+        if self._current_group is not None:
+            buffer = Image.new("RGB", (self._width, self._height))
+            # Recursively have everything draw to the image
+            self._current_group._fill_area(buffer)
+            # save image to buffer (or probably refresh buffer so we can compare)
+            self._buffer.paste(buffer)
+        print("refreshing")
+        time.sleep(1)
         # Eventually calculate dirty rectangles here
         self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
-        
+
         for area in self._subrectangles:
             self._refresh_display_area(area)
-        
-        if self._auto_refresh:
+
+    def _refresh_loop(self):
+        while self._auto_refresh:
             self.refresh()
 
     def _refresh_display_area(self, rectangle):
@@ -370,16 +410,22 @@ class Display:
             | ((data[:, :, 1] & 0xFC) << 3)
             | (data[:, :, 2] >> 3)
         )
-        
-        pixels = list(numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist())
-        
+
+        pixels = list(
+            numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
+        )
+
         self._write(
             self._set_column_command,
-            self._encode_pos(rectangle.x1 + self._colstart, rectangle.x2 + self._colstart)
+            self._encode_pos(
+                rectangle.x1 + self._colstart, rectangle.x2 + self._colstart
+            ),
         )
         self._write(
             self._set_row_command,
-            self._encode_pos(rectangle.y1 + self._rowstart, rectangle.y2 + self._rowstart)
+            self._encode_pos(
+                rectangle.y1 + self._rowstart, rectangle.y2 + self._rowstart
+            ),
         )
         self._write(self._write_ram_command, pixels)
 
@@ -397,10 +443,22 @@ class Display:
     @auto_refresh.setter
     def auto_refresh(self, value):
         self._auto_refresh = value
+        if self._refresh_thread is None:
+            self._refresh_thread = threading.Thread(
+                target=self._refresh_loop, daemon=True
+            )
+        if value and not self._refresh_thread.is_alive():
+            # Start the thread
+            self._refresh_thread.start()
+        elif not value and self._refresh_thread.is_alive():
+            # Stop the thread
+            self._refresh_thread.join()
 
     @property
     def brightness(self):
-        """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.
+        """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.
         """
         return self._brightness
 
@@ -410,7 +468,10 @@ class Display:
 
     @property
     def auto_brightness(self):
-        """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.
+        """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.
         """
         return self._auto_brightness
 
@@ -474,17 +535,24 @@ class EPaperDisplay:
         """
         Create a EPaperDisplay object on the given display bus (displayio.FourWire or displayio.ParallelBus).
 
-        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.
+        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.
         """
         pass
 
     def show(self, group):
-        """Switches to displaying the given group of layers. When group is None, the default CircuitPython terminal will be shown.
+        """Switches to displaying the given group of layers. When group is None, the default CircuitPython
+        terminal will be shown.
         """
         pass
 
     def refresh(self):
-        """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.
+        """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.
         """
         pass
 
@@ -524,7 +592,10 @@ class FourWire:
     ):
         """Create a FourWire object associated with the given pins.
 
-        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.
+        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.
         """
         self._dc = digitalio.DigitalInOut(command)
         self._dc.switch_to_output()
@@ -641,6 +712,14 @@ class Group:
         """Deletes the value at the given index."""
         del self._layers[index]
 
+    def _fill_area(self, buffer):
+        if self._hidden:
+            return
+
+        for layer in self._layers:
+            if isinstance(layer, (Group, TileGrid)):
+                layer._fill_area(buffer)
+
     @property
     def hidden(self):
         return self._hidden
@@ -683,13 +762,18 @@ class Group:
 
 
 class I2CDisplay:
-    """Manage updating a display over I2C in the background while Python code runs. It doesn’t handle display initialization.
+    """Manage updating a display over I2C in the background while Python code runs.
+    It doesn’t handle display initialization.
     """
 
     def __init__(self, i2c_bus, *, device_address, reset=None):
         """Create a I2CDisplay object associated with the given I2C bus and reset pin.
 
-        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.
+        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.
         """
         pass
 
@@ -700,7 +784,7 @@ class I2CDisplay:
         pass
 
 
-class OnDisplayBitmap:
+class OnDiskBitmap:
     """
     Loads values straight from disk. This minimizes memory use but can lead to much slower pixel load times.
     These load times may result in frame tearing where only part of the image is visible."""
@@ -720,12 +804,14 @@ class OnDisplayBitmap:
 
 
 class Palette:
-    """Map a pixel palette_index to a full color. Colors are transformed to the display’s format internally to save memory."""
+    """Map a pixel palette_index to a full color. Colors are transformed to the display’s
+    format internally to save memory.
+    """
 
     def __init__(self, color_count):
         """Create a Palette object to store a set number of colors."""
         self._needs_refresh = False
-        
+
         self._colors = []
         for _ in range(color_count):
             self._colors.append(self._make_color(0))
@@ -753,30 +839,26 @@ class Palette:
         return len(self._colors)
 
     def __setitem__(self, index, value):
-        """Sets the pixel color at the given index. The index should be an integer in the range 0 to color_count-1.
+        """Sets the pixel color at the given index. The index should be
+        an integer in the range 0 to color_count-1.
 
-        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.
+        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.
         """
         if self._colors[index]["rgb888"] != value:
             self._colors[index] = self._make_color(value)
 
     def __getitem__(self, index):
-        pass
+        if not 0 <= index < len(self._colors):
+            raise ValueError("Palette index out of range")
+        return self._colors[index]
 
     def make_transparent(self, palette_index):
-        self._colors[palette_index].transparent = True
+        self._colors[palette_index]["transparent"] = True
 
     def make_opaque(self, palette_index):
-        self._colors[palette_index].transparent = False
-
-    def _pil_palette(self):
-        "Generate a Pillow ImagePalette and return it"
-        palette = []
-        for channel in range(3):
-            for color in self._colors:
-                palette.append(color >> (8 * (2 - channel)) & 0xFF)
-            
-        return ImagePalette(mode='RGB', palette=palette, size=self._color_count)
+        self._colors[palette_index]["transparent"] = False
 
 
 class ParallelBus:
@@ -786,14 +868,20 @@ class ParallelBus:
     """
 
     def __init__(self, i2c_bus, *, device_address, reset=None):
-        """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.
-
-        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.
+        """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.
+
+        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.
         """
         pass
 
     def reset(self):
-        """Performs a hardware reset via the reset pin. Raises an exception if called when no reset pin is available.
+        """Performs a hardware reset via the reset pin. Raises an exception if called when
+        no reset pin is available.
         """
         pass
 
@@ -821,7 +909,8 @@ class Shape(Bitmap):
 
 
 class TileGrid:
-    """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple grids can share bitmaps and pixel shaders.
+    """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
+    grids can share bitmaps and pixel shaders.
 
     A single tile grid is also known as a Sprite.
     """
@@ -839,7 +928,9 @@ class TileGrid:
         x=0,
         y=0
     ):
-        """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.
+        """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.
 
         tile_width and tile_height match the height of the bitmap by default.
         """
@@ -848,15 +939,15 @@ class TileGrid:
         self._bitmap = bitmap
         bitmap_width = bitmap.width
         bitmap_height = bitmap.height
-        
+
         if not isinstance(pixel_shader, (ColorConverter, Palette)):
             raise ValueError("Unsupported Pixel Shader type")
         self._pixel_shader = pixel_shader
-        self_hidden = False
+        self._hidden = False
         self._x = x
         self._y = y
-        self._width = width # Number of Tiles Wide
-        self._height = height # Number of Tiles High
+        self._width = width  # Number of Tiles Wide
+        self._height = height  # Number of Tiles High
         if tile_width is None:
             tile_width = bitmap_width
         if tile_height is None:
@@ -867,17 +958,54 @@ class TileGrid:
         if bitmap_height % tile_height != 0:
             raise ValueError("Tile height must exactly divide bitmap height")
         self._tile_height = tile_height
+        if not 0 <= default_tile <= 255:
+            raise ValueError("Default Tile is out of range")
         self._tiles = (self._width * self._height) * [default_tile]
 
+    def _fill_area(self, buffer):
+        """Draw onto the image"""
+        if self._hidden:
+            return
+
+        image = Image.new(
+            "RGB", (self._width * self._tile_width, self._height * self._tile_height)
+        )
+
+        tile_count_x = self._bitmap.width // self._tile_width
+        tile_count_y = self._bitmap.height // self._tile_height
+
+        for tile_x in range(0, self._width):
+            for tile_y in range(0, self._height):
+                tile_index = self._tiles[tile_y * self._width + tile_x]
+                tile_index_x = tile_index % tile_count_x
+                tile_index_y = tile_index // tile_count_x
+                for pixel_x in range(self._tile_width):
+                    for pixel_y in range(self._tile_height):
+                        image_x = tile_x * self._tile_width + pixel_x
+                        image_y = tile_y * self._tile_height + pixel_y
+                        bitmap_x = tile_index_x * self._tile_width + pixel_x
+                        bitmap_y = tile_index_y * self._tile_height + pixel_y
+                        pixel_color = self._pixel_shader[
+                            self._bitmap[bitmap_x, bitmap_y]
+                        ]
+                        if not pixel_color["transparent"]:
+                            image.putpixel((image_x, image_y), pixel_color["rgb888"])
+
+        # Apply transforms or mirrors or whatever here
+        if self._tile_width == 6:
+            print("Putting at {}".format((self._x, self._y)))
+        buffer.paste(image, (self._x, self._y))
 
     @property
     def hidden(self):
         """True when the TileGrid is hidden. This may be False even when a part of a hidden Group."""
-        return self_hidden
+        return self._hidden
 
     @hidden.setter
     def hidden(self, value):
-        self._hidden = value
+        if not isinstance(value, (bool, int)):
+            raise ValueError("Expecting a boolean or integer value")
+        self._hidden = bool(value)
 
     @property
     def x(self):
@@ -955,7 +1083,7 @@ class TileGrid:
         elif ininstance(index, int):
             x = index % self._width
             y = index // self._width
-        if x > width or y > self._height or index > len(self._tiles):
+        if x > width or y > self._height or index >= len(self._tiles):
             raise ValueError("Tile index out of bounds")
         if not 0 <= value <= 255:
             raise ValueError("Tile value out of bounds")