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
 
  21 from typing import Union, Optional, Tuple
 
  22 from circuitpython_typing import WriteableBuffer
 
  23 from ._bitmap import Bitmap
 
  24 from ._colorconverter import ColorConverter
 
  25 from ._ondiskbitmap import OnDiskBitmap
 
  26 from ._palette import Palette
 
  27 from ._structs import (
 
  32 from ._colorspace import Colorspace
 
  33 from ._area import Area
 
  35 __version__ = "0.0.0+auto.0"
 
  36 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
  40     # pylint: disable=too-many-instance-attributes, too-many-statements
 
  41     """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
 
  42     grids can share bitmaps and pixel shaders.
 
  44     A single tile grid is also known as a Sprite.
 
  49         bitmap: Union[Bitmap, OnDiskBitmap],
 
  51         pixel_shader: Union[ColorConverter, Palette],
 
  54         tile_width: Optional[int] = None,
 
  55         tile_height: Optional[int] = None,
 
  56         default_tile: int = 0,
 
  60         """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
 
  61         used to convert the value and its location to a display native pixel color. This may
 
  62         be a simple color palette lookup, a gradient, a pattern or a color transformer.
 
  64         tile_width and tile_height match the height of the bitmap by default.
 
  66         if not isinstance(bitmap, (Bitmap, OnDiskBitmap)):
 
  67             raise ValueError("Unsupported Bitmap type")
 
  69         bitmap_width = bitmap.width
 
  70         bitmap_height = bitmap.height
 
  72         if pixel_shader is not None and not isinstance(
 
  73             pixel_shader, (ColorConverter, Palette)
 
  75             raise ValueError("Unsupported Pixel Shader type")
 
  76         self._pixel_shader = pixel_shader
 
  77         if isinstance(self._pixel_shader, ColorConverter):
 
  78             self._pixel_shader._rgba = True  # pylint: disable=protected-access
 
  79         self._hidden_tilegrid = False
 
  80         self._hidden_by_parent = False
 
  81         self._rendered_hidden = False
 
  82         self._name = "Tilegrid"
 
  85         self._width_in_tiles = width
 
  86         self._height_in_tiles = height
 
  87         self._transpose_xy = False
 
  92         if tile_width is None or tile_width == 0:
 
  93             tile_width = bitmap_width
 
  94         if tile_height is None or tile_width == 0:
 
  95             tile_height = bitmap_height
 
  96         if tile_width < 1 or tile_height < 1:
 
  97             raise ValueError("Tile width and height must be greater than 0")
 
  98         if bitmap_width % tile_width != 0:
 
  99             raise ValueError("Tile width must exactly divide bitmap width")
 
 100         self._tile_width = tile_width
 
 101         if bitmap_height % tile_height != 0:
 
 102             raise ValueError("Tile height must exactly divide bitmap height")
 
 103         self._tile_height = tile_height
 
 104         if not 0 <= default_tile <= 255:
 
 105             raise ValueError("Default Tile is out of range")
 
 106         self._pixel_width = width * tile_width
 
 107         self._pixel_height = height * tile_height
 
 108         self._tiles = bytearray(
 
 109             (self._width_in_tiles * self._height_in_tiles) * [default_tile]
 
 111         self._in_group = False
 
 112         self._absolute_transform = None
 
 113         self._current_area = Area(0, 0, self._pixel_width, self._pixel_height)
 
 114         self._dirty_area = Area(0, 0, 0, 0)
 
 115         self._previous_area = Area(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF)
 
 117         self._full_change = True
 
 118         self._partial_change = True
 
 119         self._bitmap_width_in_tiles = bitmap_width // tile_width
 
 120         self._tiles_in_bitmap = self._bitmap_width_in_tiles * (
 
 121             bitmap_height // tile_height
 
 123         self._needs_refresh = True
 
 125     def _update_transform(self, absolute_transform):
 
 126         """Update the parent transform and child transforms"""
 
 127         self._in_group = absolute_transform is not None
 
 128         self._absolute_transform = absolute_transform
 
 129         if self._absolute_transform is not None:
 
 131             self._update_current_x()
 
 132             self._update_current_y()
 
 134     def _update_current_x(self):
 
 135         self._needs_refresh = True
 
 136         if self._transpose_xy:
 
 137             width = self._pixel_height
 
 139             width = self._pixel_width
 
 141         absolute_transform = (
 
 143             if self._absolute_transform is None
 
 144             else self._absolute_transform
 
 147         if absolute_transform.transpose_xy:
 
 148             self._current_area.y1 = (
 
 149                 absolute_transform.y + absolute_transform.dy * self._x
 
 151             self._current_area.y2 = absolute_transform.y + absolute_transform.dy * (
 
 154             if self._current_area.y2 < self._current_area.y1:
 
 155                 self._current_area.y1, self._current_area.y2 = (
 
 156                     self._current_area.y2,
 
 157                     self._current_area.y1,
 
 160             self._current_area.x1 = (
 
 161                 absolute_transform.x + absolute_transform.dx * self._x
 
 163             self._current_area.x2 = absolute_transform.x + absolute_transform.dx * (
 
 166             if self._current_area.x2 < self._current_area.x1:
 
 167                 self._current_area.x1, self._current_area.x2 = (
 
 168                     self._current_area.x2,
 
 169                     self._current_area.x1,
 
 172     def _update_current_y(self):
 
 173         self._needs_refresh = True
 
 174         if self._transpose_xy:
 
 175             height = self._pixel_width
 
 177             height = self._pixel_height
 
 179         absolute_transform = (
 
 181             if self._absolute_transform is None
 
 182             else self._absolute_transform
 
 185         if absolute_transform.transpose_xy:
 
 186             self._current_area.x1 = (
 
 187                 absolute_transform.x + absolute_transform.dx * self._y
 
 189             self._current_area.x2 = absolute_transform.x + absolute_transform.dx * (
 
 192             if self._current_area.x2 < self._current_area.x1:
 
 193                 self._current_area.x1, self._current_area.x2 = (
 
 194                     self._current_area.x2,
 
 195                     self._current_area.x1,
 
 198             self._current_area.y1 = (
 
 199                 absolute_transform.y + absolute_transform.dy * self._y
 
 201             self._current_area.y2 = absolute_transform.y + absolute_transform.dy * (
 
 204             if self._current_area.y2 < self._current_area.y1:
 
 205                 self._current_area.y1, self._current_area.y2 = (
 
 206                     self._current_area.y2,
 
 207                     self._current_area.y1,
 
 210     def _shade(self, pixel_value):
 
 211         if isinstance(self._pixel_shader, Palette):
 
 212             return self._pixel_shader[pixel_value]
 
 213         if isinstance(self._pixel_shader, ColorConverter):
 
 214             return self._pixel_shader.convert(pixel_value)
 
 217     def _apply_palette(self, image):
 
 219             self._pixel_shader._get_palette()  # pylint: disable=protected-access
 
 222     def _add_alpha(self, image):
 
 223         alpha = self._bitmap._image.copy().convert(  # pylint: disable=protected-access
 
 227             self._pixel_shader._get_alpha_palette()  # pylint: disable=protected-access
 
 229         image.putalpha(alpha.convert("L"))
 
 233         colorspace: Colorspace,
 
 235         mask: WriteableBuffer,
 
 236         buffer: WriteableBuffer,
 
 238         """Draw onto the image"""
 
 239         # pylint: disable=too-many-locals,too-many-branches,too-many-statements
 
 241         # If no tiles are present we have no impact
 
 244         if tiles is None or len(tiles) == 0:
 
 247         if self._hidden_tilegrid or self._hidden_by_parent:
 
 249         overlap = Area()  # area, current_area, overlap
 
 250         if not area.compute_overlap(self._current_area, overlap):
 
 253         #    print("Checking", area.x1, area.y1, area.x2, area.y2)
 
 254         #    print("Overlap", overlap.x1, overlap.y1, overlap.x2, overlap.y2)
 
 256         if self._bitmap.width <= 0 or self._bitmap.height <= 0:
 
 260         y_stride = area.width()
 
 262         flip_x = self._flip_x
 
 263         flip_y = self._flip_y
 
 264         if self._transpose_xy != self._absolute_transform.transpose_xy:
 
 265             flip_x, flip_y = flip_y, flip_x
 
 268         if (self._absolute_transform.dx < 0) != flip_x:
 
 269             start += (area.x2 - area.x1 - 1) * x_stride
 
 271         if (self._absolute_transform.dy < 0) != flip_y:
 
 272             start += (area.y2 - area.y1 - 1) * y_stride
 
 275         # Track if this layer finishes filling in the given area. We can ignore any remaining
 
 276         # layers at that point.
 
 277         full_coverage = area == overlap
 
 280         area.transform_within(
 
 281             flip_x != (self._absolute_transform.dx < 0),
 
 282             flip_y != (self._absolute_transform.dy < 0),
 
 283             self.transpose_xy != self._absolute_transform.transpose_xy,
 
 289         start_x = transformed.x1 - self._current_area.x1
 
 290         end_x = transformed.x2 - self._current_area.x1
 
 291         start_y = transformed.y1 - self._current_area.y1
 
 292         end_y = transformed.y2 - self._current_area.y1
 
 294         if (self._absolute_transform.dx < 0) != flip_x:
 
 295             x_shift = area.x2 - overlap.x2
 
 297             x_shift = overlap.x1 - area.x1
 
 298         if (self._absolute_transform.dy < 0) != flip_y:
 
 299             y_shift = area.y2 - overlap.y2
 
 301             y_shift = overlap.y1 - area.y1
 
 303         # This untransposes x and y so it aligns with bitmap rows
 
 304         if self._transpose_xy != self._absolute_transform.transpose_xy:
 
 305             x_stride, y_stride = y_stride, x_stride
 
 306             x_shift, y_shift = y_shift, x_shift
 
 308         pixels_per_byte = 8 // colorspace.depth
 
 310         input_pixel = InputPixelStruct()
 
 311         output_pixel = OutputPixelStruct()
 
 312         for input_pixel.y in range(start_y, end_y):
 
 314                 start + (input_pixel.y - start_y + y_shift) * y_stride
 
 316             local_y = input_pixel.y // self._absolute_transform.scale
 
 317             for input_pixel.x in range(start_x, end_x):
 
 318                 # Compute the destination pixel in the buffer and mask based on the transformations
 
 320                     row_start + (input_pixel.x - start_x + x_shift) * x_stride
 
 323                 # Check the mask first to see if the pixel has already been set
 
 324                 if mask[offset // 32] & (1 << (offset % 32)):
 
 326                 local_x = input_pixel.x // self._absolute_transform.scale
 
 328                     (local_y // self._tile_height + self._top_left_y)
 
 329                     % self._height_in_tiles
 
 330                 ) * self._width_in_tiles + (
 
 331                     local_x // self._tile_width + self._top_left_x
 
 332                 ) % self._width_in_tiles
 
 333                 input_pixel.tile = tiles[tile_location]
 
 334                 input_pixel.tile_x = (
 
 335                     input_pixel.tile % self._bitmap_width_in_tiles
 
 336                 ) * self._tile_width + local_x % self._tile_width
 
 337                 input_pixel.tile_y = (
 
 338                     input_pixel.tile // self._bitmap_width_in_tiles
 
 339                 ) * self._tile_height + local_y % self._tile_height
 
 341                 output_pixel.pixel = 0
 
 342                 input_pixel.pixel = 0
 
 344                 # We always want to read bitmap pixels by row first and then transpose into
 
 345                 # the destination buffer because most bitmaps are row associated.
 
 346                 if isinstance(self._bitmap, (Bitmap, OnDiskBitmap)):
 
 347                     input_pixel.pixel = (
 
 348                         self._bitmap._get_pixel(  # pylint: disable=protected-access
 
 349                             input_pixel.tile_x, input_pixel.tile_y
 
 353                 output_pixel.opaque = True
 
 354                 if self._pixel_shader is None:
 
 355                     output_pixel.pixel = input_pixel.pixel
 
 356                 elif isinstance(self._pixel_shader, Palette):
 
 357                     self._pixel_shader._get_color(  # pylint: disable=protected-access
 
 358                         colorspace, input_pixel, output_pixel
 
 360                 elif isinstance(self._pixel_shader, ColorConverter):
 
 361                     self._pixel_shader._convert(  # pylint: disable=protected-access
 
 362                         colorspace, input_pixel, output_pixel
 
 365                 if not output_pixel.opaque:
 
 366                     full_coverage = False
 
 368                     mask[offset // 32] |= 1 << (offset % 32)
 
 369                     if colorspace.depth == 16:
 
 376                     elif colorspace.depth == 32:
 
 383                     elif colorspace.depth == 8:
 
 384                         buffer.cast("B")[offset] = output_pixel.pixel & 0xFF
 
 385                     elif colorspace.depth < 8:
 
 386                         # Reorder the offsets to pack multiple rows into
 
 387                         # a byte (meaning they share a column).
 
 388                         if not colorspace.pixels_in_byte_share_row:
 
 390                             row = offset // width
 
 392                             # Dividing by pixels_per_byte does truncated division
 
 393                             # even if we multiply it back out
 
 395                                 col * pixels_per_byte
 
 396                                 + (row // pixels_per_byte) * pixels_per_byte * width
 
 397                                 + (row % pixels_per_byte)
 
 399                         shift = (offset % pixels_per_byte) * colorspace.depth
 
 400                         if colorspace.reverse_pixels_in_byte:
 
 401                             # Reverse the shift by subtracting it from the leftmost shift
 
 402                             shift = (pixels_per_byte - 1) * colorspace.depth - shift
 
 403                         buffer.cast("B")[offset // pixels_per_byte] |= (
 
 404                             output_pixel.pixel << shift
 
 409     def _finish_refresh(self):
 
 410         if not self._needs_refresh:
 
 411             first_draw = self._previous_area.x1 == self._previous_area.x2
 
 412             hidden = self._hidden_tilegrid or self._hidden_by_parent
 
 413             if not first_draw and hidden:
 
 414                 self._previous_area.x2 = self._previous_area.x1
 
 415             elif self._moved or first_draw:
 
 416                 self._current_area.copy_into(self._previous_area)
 
 419             self._full_change = False
 
 420             self._partial_change = False
 
 421             if isinstance(self._pixel_shader, (Palette, ColorConverter)):
 
 422                 self._pixel_shader._finish_refresh()  # pylint: disable=protected-access
 
 423             if isinstance(self._bitmap, Bitmap):
 
 424                 self._bitmap._finish_refresh()  # pylint: disable=protected-access
 
 426     def _get_refresh_areas(self, areas: list[Area]) -> None:
 
 427         # pylint: disable=invalid-name, too-many-branches, too-many-statements
 
 428         first_draw = self._previous_area.x1 == self._previous_area.x2
 
 429         hidden = self._hidden_tilegrid or self._hidden_by_parent
 
 431         # Check hidden first because it trumps all other changes
 
 433             self._rendered_hidden = True
 
 435                 areas.append(self._previous_area)
 
 436             self._needs_refresh = False
 
 438         if self._moved and not first_draw:
 
 439             self._previous_area.union(self._current_area, self._dirty_area)
 
 440             if self._dirty_area.size() < 2 * self._pixel_width * self._pixel_height:
 
 441                 areas.append(self._dirty_area)
 
 442                 self._needs_refresh = False
 
 444             areas.append(self._current_area)
 
 445             areas.append(self._previous_area)
 
 446             self._needs_refresh = False
 
 449         tail = areas[-1] if areas else None
 
 450         # If we have an in-memory bitmap, then check it for modifications
 
 451         if isinstance(self._bitmap, Bitmap):
 
 452             self._bitmap._get_refresh_areas(areas)  # pylint: disable=protected-access
 
 453             refresh_area = areas[-1] if areas else None
 
 454             if refresh_area != tail:
 
 455                 # Special case a TileGrid that shows a full bitmap and use its
 
 456                 # dirty area. Copy it to ours so we can transform it.
 
 457                 if self._tiles_in_bitmap == 1:
 
 458                     refresh_area.copy_into(self._dirty_area)
 
 459                     self._partial_change = True
 
 461                     self._full_change = True
 
 463         self._full_change = self._full_change or (
 
 464             isinstance(self._pixel_shader, (Palette, ColorConverter))
 
 465             and self._pixel_shader._needs_refresh  # pylint: disable=protected-access
 
 467         if self._full_change or first_draw:
 
 468             areas.append(self._current_area)
 
 469             self._needs_refresh = False
 
 472         if self._partial_change:
 
 475             if self._absolute_transform.transpose_xy:
 
 477             x1 = self._dirty_area.x1
 
 478             x2 = self._dirty_area.x2
 
 480                 x1 = self._pixel_width - x1
 
 481                 x2 = self._pixel_width - x2
 
 482             y1 = self._dirty_area.y1
 
 483             y2 = self._dirty_area.y2
 
 485                 y1 = self._pixel_height - y1
 
 486                 y2 = self._pixel_height - y2
 
 487             if self._transpose_xy != self._absolute_transform.transpose_xy:
 
 490             self._dirty_area.x1 = (
 
 491                 self._absolute_transform.x + self._absolute_transform.dx * (x + x1)
 
 493             self._dirty_area.y1 = (
 
 494                 self._absolute_transform.y + self._absolute_transform.dy * (y + y1)
 
 496             self._dirty_area.x2 = (
 
 497                 self._absolute_transform.x + self._absolute_transform.dx * (x + x2)
 
 499             self._dirty_area.y2 = (
 
 500                 self._absolute_transform.y + self._absolute_transform.dy * (y + y2)
 
 502             if self._dirty_area.y2 < self._dirty_area.y1:
 
 503                 self._dirty_area.y1, self._dirty_area.y2 = (
 
 507             if self._dirty_area.x2 < self._dirty_area.x1:
 
 508                 self._dirty_area.x1, self._dirty_area.x2 = (
 
 512             areas.append(self._dirty_area)
 
 513         self._needs_refresh = False
 
 515     def _set_hidden(self, hidden: bool) -> None:
 
 516         self._needs_refresh = True
 
 517         self._hidden_tilegrid = hidden
 
 518         self._rendered_hidden = False
 
 520             self._full_change = True
 
 522     def _set_hidden_by_parent(self, hidden: bool) -> None:
 
 523         self._needs_refresh = True
 
 524         self._hidden_by_parent = hidden
 
 525         self._rendered_hidden = False
 
 527             self._full_change = True
 
 529     def _get_rendered_hidden(self) -> bool:
 
 530         return self._rendered_hidden
 
 532     def _set_all_tiles(self, tile_index: int) -> None:
 
 533         """Set all tiles to the given tile index"""
 
 534         if tile_index >= self._tiles_in_bitmap:
 
 535             raise ValueError("Tile index out of bounds")
 
 536         self._tiles = bytearray(
 
 537             (self._width_in_tiles * self._height_in_tiles) * [tile_index]
 
 539         self._full_change = True
 
 541     def _set_tile(self, x: int, y: int, tile_index: int) -> None:
 
 542         self._needs_refresh = True
 
 543         self._tiles[y * self._width_in_tiles + x] = tile_index
 
 545         if not self._partial_change:
 
 546             tile_area = self._dirty_area
 
 548             tile_area = temp_area
 
 549         top_x = (x - self._top_left_x) % self._width_in_tiles
 
 551             top_x += self._width_in_tiles
 
 552         tile_area.x1 = top_x * self._tile_width
 
 553         tile_area.x2 = tile_area.x1 + self._tile_width
 
 554         top_y = (y - self._top_left_y) % self._height_in_tiles
 
 556             top_y += self._height_in_tiles
 
 557         tile_area.y1 = top_y * self._tile_height
 
 558         tile_area.y2 = tile_area.y1 + self._tile_height
 
 560         if self._partial_change:
 
 561             self._dirty_area.union(temp_area, self._dirty_area)
 
 563         self._partial_change = True
 
 565     def _set_top_left(self, x: int, y: int) -> None:
 
 568         self._full_change = True
 
 571     def hidden(self) -> bool:
 
 572         """True when the TileGrid is hidden. This may be False even
 
 573         when a part of a hidden Group."""
 
 574         return self._hidden_tilegrid
 
 577     def hidden(self, value: bool):
 
 578         if not isinstance(value, (bool, int)):
 
 579             raise ValueError("Expecting a boolean or integer value")
 
 581         self._set_hidden(value)
 
 585         """X position of the left edge in the parent."""
 
 589     def x(self, value: int):
 
 590         if not isinstance(value, int):
 
 591             raise TypeError("X should be a integer type")
 
 595             if self._absolute_transform is not None:
 
 596                 self._update_current_x()
 
 600         """Y position of the top edge in the parent."""
 
 604     def y(self, value: int):
 
 605         if not isinstance(value, int):
 
 606             raise TypeError("Y should be a integer type")
 
 610             if self._absolute_transform is not None:
 
 611                 self._update_current_y()
 
 614     def flip_x(self) -> bool:
 
 615         """If true, the left edge rendered will be the right edge of the right-most tile."""
 
 619     def flip_x(self, value: bool):
 
 620         if not isinstance(value, bool):
 
 621             raise TypeError("Flip X should be a boolean type")
 
 622         if self._flip_x != value:
 
 623             self._needs_refresh = True
 
 625             self._full_change = True
 
 628     def flip_y(self) -> bool:
 
 629         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
 
 633     def flip_y(self, value: bool):
 
 634         if not isinstance(value, bool):
 
 635             raise TypeError("Flip Y should be a boolean type")
 
 636         if self._flip_y != value:
 
 637             self._needs_refresh = True
 
 639             self._full_change = True
 
 642     def transpose_xy(self) -> bool:
 
 643         """If true, the TileGrid's axis will be swapped. When combined with mirroring, any 90
 
 644         degree rotation can be achieved along with the corresponding mirrored version.
 
 646         return self._transpose_xy
 
 649     def transpose_xy(self, value: bool) -> None:
 
 650         if not isinstance(value, bool):
 
 651             raise TypeError("Transpose XY should be a boolean type")
 
 652         if self._transpose_xy != value:
 
 653             self._needs_refresh = True
 
 654             self._transpose_xy = value
 
 655             if self._pixel_width == self._pixel_height:
 
 656                 self._full_change = True
 
 658             self._update_current_x()
 
 659             self._update_current_y()
 
 663     def pixel_shader(self) -> Union[ColorConverter, Palette]:
 
 664         """The pixel shader of the tilegrid."""
 
 665         return self._pixel_shader
 
 668     def pixel_shader(self, new_pixel_shader: Union[ColorConverter, Palette]) -> None:
 
 669         if not isinstance(new_pixel_shader, ColorConverter) and not isinstance(
 
 670             new_pixel_shader, Palette
 
 673                 "Unsupported Type: new_pixel_shader must be ColorConverter or Palette"
 
 676         self._pixel_shader = new_pixel_shader
 
 677         self._full_change = True
 
 678         self._needs_refresh = True
 
 681     def bitmap(self) -> Union[Bitmap, OnDiskBitmap]:
 
 682         """The Bitmap or OnDiskBitmap that is assigned to this TileGrid"""
 
 686     def bitmap(self, new_bitmap: Union[Bitmap, OnDiskBitmap]) -> None:
 
 687         if not isinstance(new_bitmap, Bitmap) and not isinstance(
 
 688             new_bitmap, OnDiskBitmap
 
 691                 "Unsupported Type: new_bitmap must be Bitmap or OnDiskBitmap"
 
 695             new_bitmap.width != self.bitmap.width
 
 696             or new_bitmap.height != self.bitmap.height
 
 698             raise ValueError("New bitmap must be same size as old bitmap")
 
 700         self._needs_refresh = True
 
 701         self._bitmap = new_bitmap
 
 702         self._full_change = True
 
 704     def _extract_and_check_index(self, index):
 
 705         if isinstance(index, (tuple, list)):
 
 708             index = y * self._width_in_tiles + x
 
 709         elif isinstance(index, int):
 
 710             x = index % self._width_in_tiles
 
 711             y = index // self._width_in_tiles
 
 713             x > self._width_in_tiles
 
 714             or y > self._height_in_tiles
 
 715             or index >= len(self._tiles)
 
 717             raise ValueError("Tile index out of bounds")
 
 720     def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
 
 721         """Returns the tile index at the given index. The index can either be
 
 722         an x,y tuple or an int equal to ``y * width + x``'.
 
 724         x, y = self._extract_and_check_index(index)
 
 725         return self._tiles[y * self._width_in_tiles + x]
 
 727     def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
 
 728         """Sets the tile index at the given index. The index can either be
 
 729         an x,y tuple or an int equal to ``y * width + x``.
 
 731         x, y = self._extract_and_check_index(index)
 
 732         if not 0 <= value <= 255:
 
 733             raise ValueError("Tile value out of bounds")
 
 734         self._set_tile(x, y, value)
 
 737     def width(self) -> int:
 
 739         return self._width_in_tiles
 
 742     def height(self) -> int:
 
 743         """Height in tiles"""
 
 744         return self._height_in_tiles
 
 747     def tile_width(self) -> int:
 
 748         """Width of each tile in pixels"""
 
 749         return self._tile_width
 
 752     def tile_height(self) -> int:
 
 753         """Height of each tile in pixels"""
 
 754         return self._tile_height