1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
 
   3 # SPDX-License-Identifier: MIT
 
   7 ================================================================================
 
  11 **Software and Dependencies:**
 
  14   https://github.com/adafruit/Adafruit_Blinka/releases
 
  16 * Author(s): Melissa LeBlanc-Williams
 
  20 from typing import Union, Optional, Tuple
 
  22 from ._bitmap import Bitmap
 
  23 from ._colorconverter import ColorConverter
 
  24 from ._ondiskbitmap import OnDiskBitmap
 
  25 from ._shape import Shape
 
  26 from ._palette import Palette
 
  27 from ._structs import RectangleStruct, TransformStruct
 
  29 __version__ = "0.0.0+auto.0"
 
  30 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
  34     # pylint: disable=too-many-instance-attributes
 
  35     """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
 
  36     grids can share bitmaps and pixel shaders.
 
  38     A single tile grid is also known as a Sprite.
 
  43         bitmap: Union[Bitmap, OnDiskBitmap, Shape],
 
  45         pixel_shader: Union[ColorConverter, Palette],
 
  48         tile_width: Optional[int] = None,
 
  49         tile_height: Optional[int] = None,
 
  50         default_tile: int = 0,
 
  54         """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
 
  55         used to convert the value and its location to a display native pixel color. This may
 
  56         be a simple color palette lookup, a gradient, a pattern or a color transformer.
 
  58         tile_width and tile_height match the height of the bitmap by default.
 
  60         if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
 
  61             raise ValueError("Unsupported Bitmap type")
 
  63         bitmap_width = bitmap.width
 
  64         bitmap_height = bitmap.height
 
  66         if pixel_shader is not None and not isinstance(
 
  67             pixel_shader, (ColorConverter, Palette)
 
  69             raise ValueError("Unsupported Pixel Shader type")
 
  70         self._pixel_shader = pixel_shader
 
  71         if isinstance(self._pixel_shader, ColorConverter):
 
  72             self._pixel_shader._rgba = True  # pylint: disable=protected-access
 
  73         self._hidden_tilegrid = False
 
  76         self._width = width  # Number of Tiles Wide
 
  77         self._height = height  # Number of Tiles High
 
  78         self._transpose_xy = False
 
  83         if tile_width is None or tile_width == 0:
 
  84             tile_width = bitmap_width
 
  85         if tile_height is None or tile_width == 0:
 
  86             tile_height = bitmap_height
 
  87         if tile_width < 1 or tile_height < 1:
 
  88             raise ValueError("Tile width and height must be greater than 0")
 
  89         if bitmap_width % tile_width != 0:
 
  90             raise ValueError("Tile width must exactly divide bitmap width")
 
  91         self._tile_width = tile_width
 
  92         if bitmap_height % tile_height != 0:
 
  93             raise ValueError("Tile height must exactly divide bitmap height")
 
  94         self._tile_height = tile_height
 
  95         if not 0 <= default_tile <= 255:
 
  96             raise ValueError("Default Tile is out of range")
 
  97         self._pixel_width = width * tile_width
 
  98         self._pixel_height = height * tile_height
 
  99         self._tiles = (self._width * self._height) * [default_tile]
 
 100         self._in_group = False
 
 101         self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
 
 102         self._current_area = RectangleStruct(
 
 103             0, 0, self._pixel_width, self._pixel_height
 
 107     def _update_transform(self, absolute_transform):
 
 108         """Update the parent transform and child transforms"""
 
 109         self._absolute_transform = absolute_transform
 
 110         if self._absolute_transform is not None:
 
 111             self._update_current_x()
 
 112             self._update_current_y()
 
 114     def _update_current_x(self):
 
 115         if self._transpose_xy:
 
 116             width = self._pixel_height
 
 118             width = self._pixel_width
 
 119         if self._absolute_transform.transpose_xy:
 
 120             self._current_area.y1 = (
 
 121                 self._absolute_transform.y + self._absolute_transform.dy * self._x
 
 123             self._current_area.y2 = (
 
 124                 self._absolute_transform.y
 
 125                 + self._absolute_transform.dy * (self._x + width)
 
 127             if self._current_area.y2 < self._current_area.y1:
 
 128                 self._current_area.y1, self._current_area.y2 = (
 
 129                     self._current_area.y2,
 
 130                     self._current_area.y1,
 
 133             self._current_area.x1 = (
 
 134                 self._absolute_transform.x + self._absolute_transform.dx * self._x
 
 136             self._current_area.x2 = (
 
 137                 self._absolute_transform.x
 
 138                 + self._absolute_transform.dx * (self._x + width)
 
 140             if self._current_area.x2 < self._current_area.x1:
 
 141                 self._current_area.x1, self._current_area.x2 = (
 
 142                     self._current_area.x2,
 
 143                     self._current_area.x1,
 
 146     def _update_current_y(self):
 
 147         if self._transpose_xy:
 
 148             height = self._pixel_width
 
 150             height = self._pixel_height
 
 151         if self._absolute_transform.transpose_xy:
 
 152             self._current_area.x1 = (
 
 153                 self._absolute_transform.x + self._absolute_transform.dx * self._y
 
 155             self._current_area.x2 = (
 
 156                 self._absolute_transform.x
 
 157                 + self._absolute_transform.dx * (self._y + height)
 
 159             if self._current_area.x2 < self._current_area.x1:
 
 160                 self._current_area.x1, self._current_area.x2 = (
 
 161                     self._current_area.x2,
 
 162                     self._current_area.x1,
 
 165             self._current_area.y1 = (
 
 166                 self._absolute_transform.y + self._absolute_transform.dy * self._y
 
 168             self._current_area.y2 = (
 
 169                 self._absolute_transform.y
 
 170                 + self._absolute_transform.dy * (self._y + height)
 
 172             if self._current_area.y2 < self._current_area.y1:
 
 173                 self._current_area.y1, self._current_area.y2 = (
 
 174                     self._current_area.y2,
 
 175                     self._current_area.y1,
 
 178     def _shade(self, pixel_value):
 
 179         if isinstance(self._pixel_shader, Palette):
 
 180             return self._pixel_shader[pixel_value]
 
 181         if isinstance(self._pixel_shader, ColorConverter):
 
 182             return self._pixel_shader.convert(pixel_value)
 
 185     def _apply_palette(self, image):
 
 187             self._pixel_shader._get_palette()  # pylint: disable=protected-access
 
 190     def _add_alpha(self, image):
 
 191         alpha = self._bitmap._image.copy().convert(  # pylint: disable=protected-access
 
 195             self._pixel_shader._get_alpha_palette()  # pylint: disable=protected-access
 
 197         image.putalpha(alpha.convert("L"))
 
 199     def _fill_area(self, buffer):
 
 200         # pylint: disable=too-many-locals,too-many-branches,too-many-statements
 
 201         """Draw onto the image"""
 
 202         if self._hidden_tilegrid:
 
 205         if self._bitmap.width <= 0 or self._bitmap.height <= 0:
 
 208         # Copy class variables to local variables in case something changes
 
 212         height = self._height
 
 213         tile_width = self._tile_width
 
 214         tile_height = self._tile_height
 
 215         bitmap_width = self._bitmap.width
 
 216         pixel_width = self._pixel_width
 
 217         pixel_height = self._pixel_height
 
 219         absolute_transform = self._absolute_transform
 
 220         pixel_shader = self._pixel_shader
 
 221         bitmap = self._bitmap
 
 224         tile_count_x = bitmap_width // tile_width
 
 228             (width * tile_width, height * tile_height),
 
 232         for tile_x in range(width):
 
 233             for tile_y in range(height):
 
 234                 tile_index = tiles[tile_y * width + tile_x]
 
 235                 tile_index_x = tile_index % tile_count_x
 
 236                 tile_index_y = tile_index // tile_count_x
 
 237                 tile_image = bitmap._image  # pylint: disable=protected-access
 
 238                 if isinstance(pixel_shader, Palette):
 
 239                     tile_image = tile_image.copy().convert("P")
 
 240                     self._apply_palette(tile_image)
 
 241                     tile_image = tile_image.convert("RGBA")
 
 242                     self._add_alpha(tile_image)
 
 243                 elif isinstance(pixel_shader, ColorConverter):
 
 244                     # This will be needed for eInks, grayscale, and monochrome displays
 
 246                 image.alpha_composite(
 
 248                     dest=(tile_x * tile_width, tile_y * tile_height),
 
 250                         tile_index_x * tile_width,
 
 251                         tile_index_y * tile_height,
 
 252                         tile_index_x * tile_width + tile_width,
 
 253                         tile_index_y * tile_height + tile_height,
 
 257         if absolute_transform is not None:
 
 258             if absolute_transform.scale > 1:
 
 259                 image = image.resize(
 
 261                         int(pixel_width * absolute_transform.scale),
 
 263                             pixel_height * absolute_transform.scale,
 
 266                     resample=Image.NEAREST,
 
 268             if absolute_transform.mirror_x:
 
 269                 image = image.transpose(Image.FLIP_LEFT_RIGHT)
 
 270             if absolute_transform.mirror_y:
 
 271                 image = image.transpose(Image.FLIP_TOP_BOTTOM)
 
 272             if absolute_transform.transpose_xy:
 
 273                 image = image.transpose(Image.TRANSPOSE)
 
 274             x *= absolute_transform.dx
 
 275             y *= absolute_transform.dy
 
 276             x += absolute_transform.x
 
 277             y += absolute_transform.y
 
 279         source_x = source_y = 0
 
 281             source_x = round(0 - x)
 
 284             source_y = round(0 - y)
 
 292             and y <= buffer.height
 
 293             and source_x <= image.width
 
 294             and source_y <= image.height
 
 296             buffer.alpha_composite(image, (x, y), source=(source_x, source_y))
 
 298     def _finish_refresh(self):
 
 302     def hidden(self) -> bool:
 
 303         """True when the TileGrid is hidden. This may be False even
 
 304         when a part of a hidden Group."""
 
 305         return self._hidden_tilegrid
 
 308     def hidden(self, value: bool):
 
 309         if not isinstance(value, (bool, int)):
 
 310             raise ValueError("Expecting a boolean or integer value")
 
 311         self._hidden_tilegrid = bool(value)
 
 315         """X position of the left edge in the parent."""
 
 319     def x(self, value: int):
 
 320         if not isinstance(value, int):
 
 321             raise TypeError("X should be a integer type")
 
 324             self._update_current_x()
 
 328         """Y position of the top edge in the parent."""
 
 332     def y(self, value: int):
 
 333         if not isinstance(value, int):
 
 334             raise TypeError("Y should be a integer type")
 
 337             self._update_current_y()
 
 340     def flip_x(self) -> bool:
 
 341         """If true, the left edge rendered will be the right edge of the right-most tile."""
 
 345     def flip_x(self, value: bool):
 
 346         if not isinstance(value, bool):
 
 347             raise TypeError("Flip X should be a boolean type")
 
 348         if self._flip_x != value:
 
 352     def flip_y(self) -> bool:
 
 353         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
 
 357     def flip_y(self, value: bool):
 
 358         if not isinstance(value, bool):
 
 359             raise TypeError("Flip Y should be a boolean type")
 
 360         if self._flip_y != value:
 
 364     def transpose_xy(self) -> bool:
 
 365         """If true, the TileGrid's axis will be swapped. When combined with mirroring, any 90
 
 366         degree rotation can be achieved along with the corresponding mirrored version.
 
 368         return self._transpose_xy
 
 371     def transpose_xy(self, value: bool):
 
 372         if not isinstance(value, bool):
 
 373             raise TypeError("Transpose XY should be a boolean type")
 
 374         if self._transpose_xy != value:
 
 375             self._transpose_xy = value
 
 376             self._update_current_x()
 
 377             self._update_current_y()
 
 380     def pixel_shader(self) -> Union[ColorConverter, Palette]:
 
 381         """The pixel shader of the tilegrid."""
 
 382         return self._pixel_shader
 
 385     def pixel_shader(self, new_pixel_shader: Union[ColorConverter, Palette]) -> None:
 
 386         if not isinstance(new_pixel_shader, ColorConverter) and not isinstance(
 
 387             new_pixel_shader, Palette
 
 390                 "Unsupported Type: new_pixel_shader must be ColorConverter or Palette"
 
 393         self._pixel_shader = new_pixel_shader
 
 396     def bitmap(self) -> Union[Bitmap, OnDiskBitmap, Shape]:
 
 397         """The Bitmap, OnDiskBitmap, or Shape that is assigned to this TileGrid"""
 
 401     def bitmap(self, new_bitmap: Union[Bitmap, OnDiskBitmap, Shape]) -> None:
 
 404             not isinstance(new_bitmap, Bitmap)
 
 405             and not isinstance(new_bitmap, OnDiskBitmap)
 
 406             and not isinstance(new_bitmap, Shape)
 
 409                 "Unsupported Type: new_bitmap must be Bitmap, OnDiskBitmap, or Shape"
 
 413             new_bitmap.width != self.bitmap.width
 
 414             or new_bitmap.height != self.bitmap.height
 
 416             raise ValueError("New bitmap must be same size as old bitmap")
 
 418         self._bitmap = new_bitmap
 
 420     def _extract_and_check_index(self, index):
 
 421         if isinstance(index, (tuple, list)):
 
 424             index = y * self._width + x
 
 425         elif isinstance(index, int):
 
 426             x = index % self._width
 
 427             y = index // self._width
 
 428         if x > self._width or y > self._height or index >= len(self._tiles):
 
 429             raise ValueError("Tile index out of bounds")
 
 432     def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
 
 433         """Returns the tile index at the given index. The index can either be
 
 434         an x,y tuple or an int equal to ``y * width + x``'.
 
 436         index = self._extract_and_check_index(index)
 
 437         return self._tiles[index]
 
 439     def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
 
 440         """Sets the tile index at the given index. The index can either be
 
 441         an x,y tuple or an int equal to ``y * width + x``.
 
 443         index = self._extract_and_check_index(index)
 
 444         if not 0 <= value <= 255:
 
 445             raise ValueError("Tile value out of bounds")
 
 446         self._tiles[index] = value
 
 449     def width(self) -> int:
 
 454     def height(self) -> int:
 
 455         """Height in tiles"""
 
 459     def tile_width(self) -> int:
 
 460         """Width of each tile in pixels"""
 
 461         return self._tile_width
 
 464     def tile_height(self) -> int:
 
 465         """Height of each tile in pixels"""
 
 466         return self._tile_height