]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blobdiff - displayio/tilegrid.py
Add pre-commit support.
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / tilegrid.py
index 2a2222a5e9395edcb271f0453cc8723e4218577d..8532ac5ece0055ee7dfe9426d516393592938d28 100644 (file)
@@ -1,27 +1,9 @@
-# The MIT License (MIT)
+# SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
 #
 #
-# 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.
+# SPDX-License-Identifier: MIT
 
 """
 
 """
-`displayio`
+`displayio.tilegrid`
 ================================================================================
 
 displayio for Blinka
 ================================================================================
 
 displayio for Blinka
@@ -35,20 +17,23 @@ displayio for Blinka
 
 """
 
 
 """
 
+from recordclass import recordclass
 from PIL import Image
 from displayio.bitmap import Bitmap
 from displayio.colorconverter import ColorConverter
 from displayio.ondiskbitmap import OnDiskBitmap
 from displayio.shape import Shape
 from displayio.palette import Palette
 from PIL import Image
 from displayio.bitmap import Bitmap
 from displayio.colorconverter import ColorConverter
 from displayio.ondiskbitmap import OnDiskBitmap
 from displayio.shape import Shape
 from displayio.palette import Palette
-from displayio import Rectangle
-from displayio import Transform
 
 __version__ = "0.0.0-auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
 
 __version__ = "0.0.0-auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
-# pylint: disable=too-many-instance-attributes
+Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
+Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
+
+
 class TileGrid:
 class TileGrid:
+    # pylint: disable=too-many-instance-attributes
     """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.
 
@@ -80,10 +65,14 @@ class TileGrid:
         bitmap_width = bitmap.width
         bitmap_height = bitmap.height
 
         bitmap_width = bitmap.width
         bitmap_height = bitmap.height
 
-        if not isinstance(pixel_shader, (ColorConverter, Palette)):
+        if pixel_shader is not None and not isinstance(
+            pixel_shader, (ColorConverter, Palette)
+        ):
             raise ValueError("Unsupported Pixel Shader type")
         self._pixel_shader = pixel_shader
             raise ValueError("Unsupported Pixel Shader type")
         self._pixel_shader = pixel_shader
-        self._hidden = False
+        if isinstance(self._pixel_shader, ColorConverter):
+            self._pixel_shader._rgba = True  # pylint: disable=protected-access
+        self._hidden_tilegrid = False
         self._x = x
         self._y = y
         self._width = width  # Number of Tiles Wide
         self._x = x
         self._y = y
         self._width = width  # Number of Tiles Wide
@@ -91,10 +80,16 @@ class TileGrid:
         self._transpose_xy = False
         self._flip_x = False
         self._flip_y = False
         self._transpose_xy = False
         self._flip_x = False
         self._flip_y = False
-        if tile_width is None:
+        self._top_left_x = 0
+        self._top_left_y = 0
+        if tile_width is None or tile_width == 0:
             tile_width = bitmap_width
             tile_width = bitmap_width
-        if tile_height is None:
+        if tile_height is None or tile_width == 0:
             tile_height = bitmap_height
             tile_height = bitmap_height
+        if tile_width < 1:
+            tile_width = 1
+        if tile_height < 1:
+            tile_height = 1
         if bitmap_width % tile_width != 0:
             raise ValueError("Tile width must exactly divide bitmap width")
         self._tile_width = tile_width
         if bitmap_width % tile_width != 0:
             raise ValueError("Tile width must exactly divide bitmap width")
         self._tile_width = tile_width
@@ -182,72 +177,137 @@ class TileGrid:
                     self._current_area.y1,
                 )
 
                     self._current_area.y1,
                 )
 
-    # pylint: disable=too-many-locals
+    def _shade(self, pixel_value):
+        if isinstance(self._pixel_shader, Palette):
+            return self._pixel_shader[pixel_value]["rgba"]
+        if isinstance(self._pixel_shader, ColorConverter):
+            return self._pixel_shader.convert(pixel_value)
+        return pixel_value
+
+    def _apply_palette(self, image):
+        image.putpalette(
+            self._pixel_shader._get_palette()  # pylint: disable=protected-access
+        )
+
+    def _add_alpha(self, image):
+        alpha = self._bitmap._image.copy().convert(  # pylint: disable=protected-access
+            "P"
+        )
+        alpha.putpalette(
+            self._pixel_shader._get_alpha_palette()  # pylint: disable=protected-access
+        )
+        image.putalpha(alpha.convert("L"))
+
     def _fill_area(self, buffer):
     def _fill_area(self, buffer):
+        # pylint: disable=too-many-locals,too-many-branches,too-many-statements
         """Draw onto the image"""
         """Draw onto the image"""
-        if self._hidden:
+        if self._hidden_tilegrid:
             return
 
             return
 
+        if self._bitmap.width <= 0 or self._bitmap.height <= 0:
+            return
+
+        # Copy class variables to local variables in case something changes
+        x = self._x
+        y = self._y
+        width = self._width
+        height = self._height
+        tile_width = self._tile_width
+        tile_height = self._tile_height
+        bitmap_width = self._bitmap.width
+        pixel_width = self._pixel_width
+        pixel_height = self._pixel_height
+        tiles = self._tiles
+        absolute_transform = self._absolute_transform
+        pixel_shader = self._pixel_shader
+        bitmap = self._bitmap
+        tiles = self._tiles
+
+        tile_count_x = bitmap_width // tile_width
+
         image = Image.new(
             "RGBA",
         image = Image.new(
             "RGBA",
-            (self._width * self._tile_width, self._height * self._tile_height),
+            (width * tile_width, height * tile_height),
             (0, 0, 0, 0),
         )
 
             (0, 0, 0, 0),
         )
 
-        tile_count_x = self._bitmap.width // self._tile_width
-        x = self._x
-        y = self._y
-
-        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]
+        for tile_x in range(width):
+            for tile_y in range(height):
+                tile_index = tiles[tile_y * width + tile_x]
                 tile_index_x = tile_index % tile_count_x
                 tile_index_y = tile_index // tile_count_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"])
-        if self._absolute_transform is not None:
-            if self._absolute_transform.scale > 1:
+                tile_image = bitmap._image  # pylint: disable=protected-access
+                if isinstance(pixel_shader, Palette):
+                    tile_image = tile_image.copy().convert("P")
+                    self._apply_palette(tile_image)
+                    tile_image = tile_image.convert("RGBA")
+                    self._add_alpha(tile_image)
+                elif isinstance(pixel_shader, ColorConverter):
+                    # This will be needed for eInks, grayscale, and monochrome displays
+                    pass
+                image.alpha_composite(
+                    tile_image,
+                    dest=(tile_x * tile_width, tile_y * tile_height),
+                    source=(
+                        tile_index_x * tile_width,
+                        tile_index_y * tile_height,
+                        tile_index_x * tile_width + tile_width,
+                        tile_index_y * tile_height + tile_height,
+                    ),
+                )
+
+        if absolute_transform is not None:
+            if absolute_transform.scale > 1:
                 image = image.resize(
                     (
                 image = image.resize(
                     (
-                        self._pixel_width * self._absolute_transform.scale,
-                        self._pixel_height * self._absolute_transform.scale,
+                        int(pixel_width * absolute_transform.scale),
+                        int(
+                            pixel_height * absolute_transform.scale,
+                        ),
                     ),
                     resample=Image.NEAREST,
                 )
                     ),
                     resample=Image.NEAREST,
                 )
-            if self._absolute_transform.mirror_x:
+            if absolute_transform.mirror_x:
                 image = image.transpose(Image.FLIP_LEFT_RIGHT)
                 image = image.transpose(Image.FLIP_LEFT_RIGHT)
-            if self._absolute_transform.mirror_y:
+            if absolute_transform.mirror_y:
                 image = image.transpose(Image.FLIP_TOP_BOTTOM)
                 image = image.transpose(Image.FLIP_TOP_BOTTOM)
-            if self._absolute_transform.transpose_xy:
+            if absolute_transform.transpose_xy:
                 image = image.transpose(Image.TRANSPOSE)
                 image = image.transpose(Image.TRANSPOSE)
-            x *= self._absolute_transform.dx
-            y *= self._absolute_transform.dy
-            x += self._absolute_transform.x
-            y += self._absolute_transform.y
-        buffer.alpha_composite(image, (x, y))
-
-    # pylint: enable=too-many-locals
+            x *= absolute_transform.dx
+            y *= absolute_transform.dy
+            x += absolute_transform.x
+            y += absolute_transform.y
+
+        source_x = source_y = 0
+        if x < 0:
+            source_x = round(0 - x)
+            x = 0
+        if y < 0:
+            source_y = round(0 - y)
+            y = 0
+
+        x = round(x)
+        y = round(y)
+
+        if (
+            x <= buffer.width
+            and y <= buffer.height
+            and source_x <= image.width
+            and source_y <= image.height
+        ):
+            buffer.alpha_composite(image, (x, y), source=(source_x, source_y))
 
     @property
     def hidden(self):
         """True when the TileGrid is hidden. This may be False even
         when a part of a hidden Group."""
 
     @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_tilegrid
 
     @hidden.setter
     def hidden(self, value):
         if not isinstance(value, (bool, int)):
             raise ValueError("Expecting a boolean or integer value")
 
     @hidden.setter
     def hidden(self, value):
         if not isinstance(value, (bool, int)):
             raise ValueError("Expecting a boolean or integer value")
-        self._hidden = bool(value)
+        self._hidden_tilegrid = bool(value)
 
     @property
     def x(self):
 
     @property
     def x(self):
@@ -320,10 +380,7 @@ class TileGrid:
         """The pixel shader of the tilegrid."""
         return self._pixel_shader
 
         """The pixel shader of the tilegrid."""
         return self._pixel_shader
 
-    def __getitem__(self, index):
-        """Returns the tile index at the given index. The index can either be
-        an x,y tuple or an int equal to ``y * width + x``'.
-        """
+    def _extract_and_check_index(self, index):
         if isinstance(index, (tuple, list)):
             x = index[0]
             y = index[1]
         if isinstance(index, (tuple, list)):
             x = index[0]
             y = index[1]
@@ -333,24 +390,20 @@ class TileGrid:
             y = index // self._width
         if x > self._width or y > self._height or index >= len(self._tiles):
             raise ValueError("Tile index out of bounds")
             y = index // self._width
         if x > self._width or y > self._height or index >= len(self._tiles):
             raise ValueError("Tile index out of bounds")
+        return index
+
+    def __getitem__(self, index):
+        """Returns the tile index at the given index. The index can either be
+        an x,y tuple or an int equal to ``y * width + x``'.
+        """
+        index = self._extract_and_check_index(index)
         return self._tiles[index]
 
     def __setitem__(self, index, value):
         """Sets the tile index at the given index. The index can either be
         an x,y tuple or an int equal to ``y * width + x``.
         """
         return self._tiles[index]
 
     def __setitem__(self, index, value):
         """Sets the tile index at the given index. The index can either be
         an x,y tuple or an int equal to ``y * width + x``.
         """
-        if isinstance(index, (tuple, list)):
-            x = index[0]
-            y = index[1]
-            index = y * self._width + x
-        elif isinstance(index, int):
-            x = index % self._width
-            y = index // self._width
-        if x > self._width or y > self._height or index >= len(self._tiles):
-            raise ValueError("Tile index out of bounds")
+        index = self._extract_and_check_index(index)
         if not 0 <= value <= 255:
             raise ValueError("Tile value out of bounds")
         self._tiles[index] = value
         if not 0 <= value <= 255:
             raise ValueError("Tile value out of bounds")
         self._tiles[index] = value
-
-
-# pylint: enable=too-many-instance-attributes