10 from collections import namedtuple
11 from PIL import Image, ImageDraw, ImagePalette
19 # Don't import pillow if we're running in the CI. We could mock it out but that
20 # would require mocking in all reverse dependencies.
21 if "GITHUB_ACTION" not in os.environ and "READTHEDOCS" not in os.environ:
22 # This will only work on Linux
25 # this would be for Github Actions
26 utils = None # pylint: disable=invalid-name
28 __version__ = "0.0.0-auto.0"
29 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
34 Rectangle = namedtuple("Rectangle", "x1 y1 x2 y2")
36 class _DisplayioSingleton:
41 def release_displays():
42 """Releases any actively used displays so their busses and pins can be used again.
44 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.
46 for _disp in _displays:
52 """Stores values of a certain size in a 2D array"""
54 def __init__(self, width, height, value_count):
55 """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.
59 self._read_only = False
62 raise ValueError("value_count must be > 0")
65 while (value_count - 1) >> bits:
71 self._bits_per_value = bits
74 self._bits_per_value > 8
75 and self._bits_per_value != 16
76 and self._bits_per_value != 32
78 raise NotImplementedError("Invalid bits per value")
80 self._data = (width * height) * [0]
81 self._dirty_area = Rectangle(0, 0, width, height)
83 def __getitem__(self, index):
85 Returns the value at the given index. The index can either be
86 an x,y tuple or an int equal to `y * width + x`.
88 if isinstance(index, (tuple, list)):
89 index = index[1] * self._width + index[0]
90 return self._data[index]
92 def __setitem__(self, index, value):
94 Sets the value at the given index. The index can either be
95 an x,y tuple or an int equal to `y * width + x`.
98 raise RuntimeError("Read-only object")
99 if isinstance(index, (tuple, list)):
102 index = y * self._width + x
103 elif ininstance(index, int):
104 x = index % self._width
105 y = index // self._width
106 self._data[index] = value
107 if self._dirty_area.x1 == self._dirty_area.x2:
108 self._dirty_area.x1 = x
109 self._dirty_area.x2 = x + 1
110 self._dirty_area.y1 = y
111 self._dirty_area.y2 = y + 1
113 if x < self._dirty_area.x1:
114 self._dirty_area.x1 = x
115 elif x >= self._dirty_area.x2:
116 self._dirty_area.x2 = x + 1
117 if y < self._dirty_area.y1:
118 self._dirty_area.y1 = y
119 elif y >= self._dirty_area.y2:
120 self._dirty_area.y2 = y + 1
122 def _finish_refresh(self):
123 self._dirty_area.x1 = 0
124 self._dirty_area.x2 = 0
126 def fill(self, value):
127 """Fills the bitmap with the supplied palette index value."""
128 self._data = (self._width * self._height) * [value]
129 self._dirty_area = Rectangle(0, 0, self._width, self._height)
133 """Width of the bitmap. (read only)"""
138 """Height of the bitmap. (read only)"""
142 class ColorConverter:
143 """Converts one color format to another. Color converter based on original displayio
144 code for consistency.
147 def __init__(self, *, dither=False):
148 """Create a ColorConverter object to convert color formats.
149 Only supports RGB888 to RGB565 currently.
150 :param bool dither: Adds random noise to dither the output image
152 self._dither = dither
155 def _compute_rgb565(self, color):
157 return (color >> 19) << 11 | ((color >> 10) & 0x3F) << 5 | (color >> 3) & 0x1F
159 def _compute_luma(self, color):
161 g8 = (color >> 8) & 0xFF
163 return (r8 * 19) / 255 + (g8 * 182) / 255 + (b8 + 54) / 255
165 def _compute_chroma(self, color):
167 g8 = (color >> 8) & 0xFF
169 return max(r8, g8, b8) - min(r8, g8, b8)
171 def _compute_hue(self, color):
173 g8 = (color >> 8) & 0xFF
175 max_color = max(r8, g8, b8)
176 chroma = self._compute_chroma(color)
181 hue = (((g8 - b8) * 40) / chroma) % 240
182 elif max_color == g8:
183 hue = (((b8 - r8) + (2 * chroma)) * 40) / chroma
184 elif max_color == b8:
185 hue = (((r8 - g8) + (4 * chroma)) * 40) / chroma
191 def _dither_noise_1(self, noise):
193 nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7FFFFFFF
194 return (nn / (1073741824.0 * 2)) * 255
196 def _dither_noise_2(self, x, y):
197 return self._dither_noise_1(x + y * 0xFFFF)
199 def _compute_tricolor(self):
202 def convert(self, color):
203 "Converts the given RGB888 color to RGB565"
205 return color # To Do: return a dithered color
207 return self._compute_rgb565(color)
211 "When true the color converter dithers the output by adding random noise when truncating to display bitdepth"
215 def dither(self, value):
216 if not isinstance(value, bool):
217 raise ValueError("Value should be boolean")
222 """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.
224 Most people should not use this class directly. Use a specific display driver instead that will contain the initialization sequence at minimum.
226 .. 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)
242 pixels_in_byte_share_row=True,
244 reverse_pixels_in_byte=False,
245 set_column_command=0x2A,
246 set_row_command=0x2B,
247 write_ram_command=0x2C,
248 set_vertical_scroll=0,
250 brightness_command=None,
252 auto_brightness=False,
253 single_byte_bounds=False,
254 data_as_commands=False,
256 native_frames_per_second=60
258 """Create a Display object on the given display bus (`displayio.FourWire` or `displayio.ParallelBus`).
260 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:
261 .. code-block:: python
263 init_sequence = (b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F" # Set Gamma
264 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
265 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
267 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
269 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.
271 The initialization sequence should always leave the display memory access inline with the scan of the display to minimize tearing artifacts.
273 self._bus = display_bus
274 self._set_column_command = set_column_command
275 self._set_row_command = set_row_command
276 self._write_ram_command = write_ram_command
277 self._brightness_command = brightness_command
278 self._data_as_commands = data_as_commands
279 self._single_byte_bounds = single_byte_bounds
281 self._height = height
282 self._colstart = colstart
283 self._rowstart = rowstart
284 self._rotation = rotation
285 self._auto_brightness = auto_brightness
286 self._brightness = brightness
287 self._auto_refresh = auto_refresh
288 self._initialize(init_sequence)
289 self._buffer = Image.new("RGB", (width, height))
290 self._subrectangles = []
291 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
293 _displays.append(self)
294 if self._auto_refresh:
297 def _initialize(self, init_sequence):
299 while i < len(init_sequence):
300 command = init_sequence[i]
301 data_size = init_sequence[i + 1]
302 delay = (data_size & 0x80) > 0
304 data_byte = init_sequence[i + 2]
305 self._write(command, init_sequence[i + 2 : i + 2 + data_size])
309 delay_time_ms = init_sequence[i + 1 + data_size]
310 if delay_time_ms == 255:
312 time.sleep(delay_time_ms / 1000)
315 def _write(self, command, data):
316 if self._single_byte_bounds:
317 self._bus.send(True, bytes([command]) + data, toggle_every_byte=True)
319 self._bus.send(True, bytes([command]), toggle_every_byte=True)
320 self._bus.send(False, data)
326 def show(self, group):
327 """Switches to displaying the given group of layers. When group is None, the default CircuitPython terminal will be shown.
329 self._groups.append(group)
331 def _group_to_buffer(self, group):
332 """ go through any children and call this function then add group to buffer"""
333 for layer_number in range(len(group.layers)):
334 layer = group.layers[layer_number]
335 if isinstance(layer, Group):
336 self._group_to_buffer(layer)
337 elif isinstance(layer, TileGrid):
338 # Get the TileGrid Info and draw to buffer
341 raise TypeError("Invalid layer type found in group")
343 def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
344 """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.
346 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.
348 When auto refresh is on, updates the display immediately. (The display will also update without calls to this.)
351 # Go through groups and and add each to buffer
352 #for group in self._groups:
355 # Eventually calculate dirty rectangles here
356 self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
358 for area in self._subrectangles:
359 self._refresh_display_area(area)
361 if self._auto_refresh:
364 def _refresh_display_area(self, rectangle):
365 """Loop through dirty rectangles and redraw that area."""
366 """Read or write a block of data."""
367 data = numpy.array(self._buffer.crop(rectangle).convert("RGB")).astype("uint16")
369 ((data[:, :, 0] & 0xF8) << 8)
370 | ((data[:, :, 1] & 0xFC) << 3)
371 | (data[:, :, 2] >> 3)
374 pixels = list(numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist())
377 self._set_column_command,
378 self._encode_pos(rectangle.x1 + self._colstart, rectangle.x2 + self._colstart)
381 self._set_row_command,
382 self._encode_pos(rectangle.y1 + self._rowstart, rectangle.y2 + self._rowstart)
384 self._write(self._write_ram_command, pixels)
386 def _encode_pos(self, x, y):
387 """Encode a postion into bytes."""
388 return struct.pack(self._bounds_encoding, x, y)
390 def fill_row(self, y, buffer):
394 def auto_refresh(self):
395 return self._auto_refresh
398 def auto_refresh(self, value):
399 self._auto_refresh = value
402 def brightness(self):
403 """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.
405 return self._brightness
408 def brightness(self, value):
409 self._brightness = value
412 def auto_brightness(self):
413 """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.
415 return self._auto_brightness
417 @auto_brightness.setter
418 def auto_brightness(self, value):
419 self._auto_brightness = value
431 """The rotation of the display as an int in degrees."""
432 return self._rotation
435 def rotation(self, value):
436 if value not in (0, 90, 180, 270):
437 raise ValueError("Rotation must be 0/90/180/270")
438 self._rotation = value
459 set_column_window_command=None,
460 set_row_window_command=None,
461 single_byte_bounds=False,
462 write_black_ram_command,
463 black_bits_inverted=False,
464 write_color_ram_command=None,
465 color_bits_inverted=False,
466 highlight_color=0x000000,
467 refresh_display_command,
471 seconds_per_frame=180,
472 always_toggle_chip_select=False
475 Create a EPaperDisplay object on the given display bus (displayio.FourWire or displayio.ParallelBus).
477 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.
481 def show(self, group):
482 """Switches to displaying the given group of layers. When group is None, the default CircuitPython terminal will be shown.
487 """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.
492 def time_to_refresh(self):
493 """Time, in fractional seconds, until the ePaper display can be refreshed."""
510 """Manage updating a display over SPI four wire protocol in the background while
511 Python code runs. It doesn’t handle display initialization.
525 """Create a FourWire object associated with the given pins.
527 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.
529 self._dc = digitalio.DigitalInOut(command)
530 self._dc.switch_to_output()
531 self._chip_select = digitalio.DigitalInOut(chip_select)
532 self._chip_select.switch_to_output(value=True)
534 if reset is not None:
535 self._reset = digitalio.DigitalInOut(reset)
536 self._reset.switch_to_output(value=True)
540 while self._spi.try_lock():
542 self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
549 self._chip_select.deinit()
550 if self._reset is not None:
554 if self._reset is not None:
555 self._reset.value = False
557 self._reset.value = True
560 def send(self, is_command, data, *, toggle_every_byte=False):
561 while self._spi.try_lock():
563 self._dc.value = not is_command
564 if toggle_every_byte:
566 self._spi.write(bytes([byte]))
567 self._chip_select.value = True
569 self._chip_select.value = False
571 self._spi.write(data)
576 """Manage a group of sprites and groups and how they are inter-related."""
578 def __init__(self, *, max_size=4, scale=1, x=0, y=0):
579 """Create a Group of a given size and scale. Scale is in
580 one dimension. For example, scale=2 leads to a layer’s
581 pixel being 2x2 pixels when in the group.
583 if not isinstance(max_size, int) or max_size < 1:
584 raise ValueError("Max Size must be an integer and >= 1")
585 self._max_size = max_size
586 if not isinstance(scale, int) or scale < 1:
587 raise ValueError("Scale must be an integer and >= 1")
593 self._supported_types = (TileGrid, Group)
595 def append(self, layer):
596 """Append a layer to the group. It will be drawn
599 if not isinstance(layer, self._supported_types):
600 raise ValueError("Invalid Group Memeber")
601 if len(self._layers) == self._max_size:
602 raise RuntimeError("Group full")
603 self._layers.append(layer)
605 def insert(self, index, layer):
606 """Insert a layer into the group."""
607 if not isinstance(layer, self._supported_types):
608 raise ValueError("Invalid Group Memeber")
609 if len(self._layers) == self._max_size:
610 raise RuntimeError("Group full")
611 self._layers.insert(index, layer)
613 def index(self, layer):
614 """Returns the index of the first copy of layer.
615 Raises ValueError if not found.
619 def pop(self, index=-1):
620 """Remove the ith item and return it."""
621 return self._layers.pop(index)
623 def remove(self, layer):
624 """Remove the first copy of layer. Raises ValueError
625 if it is not present."""
629 """Returns the number of layers in a Group"""
630 return len(self._layers)
632 def __getitem__(self, index):
633 """Returns the value at the given index."""
634 return self._layers[index]
636 def __setitem__(self, index, value):
637 """Sets the value at the given index."""
638 self._layers[index] = value
640 def __delitem__(self, index):
641 """Deletes the value at the given index."""
642 del self._layers[index]
649 def hidden(self, value):
650 if not isinstance(value, (bool, int)):
651 raise ValueError("Expecting a boolean or integer value")
652 self._hidden = bool(value)
659 def scale(self, value):
660 if not isinstance(value, int) or value < 1:
661 raise ValueError("Scale must be an integer and at least 1")
670 if not isinstance(value, int):
671 raise ValueError("x must be an integer")
680 if not isinstance(value, int):
681 raise ValueError("y must be an integer")
686 """Manage updating a display over I2C in the background while Python code runs. It doesn’t handle display initialization.
689 def __init__(self, i2c_bus, *, device_address, reset=None):
690 """Create a I2CDisplay object associated with the given I2C bus and reset pin.
692 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.
699 def send(self, command, data):
703 class OnDisplayBitmap:
705 Loads values straight from disk. This minimizes memory use but can lead to much slower pixel load times.
706 These load times may result in frame tearing where only part of the image is visible."""
708 def __init__(self, file):
709 self._image = Image.open(file)
713 """Width of the bitmap. (read only)"""
714 return self._image.width
718 """Height of the bitmap. (read only)"""
719 return self._image.height
723 """Map a pixel palette_index to a full color. Colors are transformed to the display’s format internally to save memory."""
725 def __init__(self, color_count):
726 """Create a Palette object to store a set number of colors."""
727 self._needs_refresh = False
730 for _ in range(color_count):
731 self._colors.append(self._make_color(0))
733 def _make_color(self, value):
735 "transparent": False,
738 color_converter = ColorConverter()
739 if isinstance(value, (tuple, list, bytes, bytearray)):
740 value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
741 elif isinstance(value, int):
742 if not 0 <= value <= 0xFFFFFF:
743 raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
745 raise TypeError("Color buffer must be a buffer, tuple, list, or int")
746 color["rgb888"] = value
747 self._needs_refresh = True
752 """Returns the number of colors in a Palette"""
753 return len(self._colors)
755 def __setitem__(self, index, value):
756 """Sets the pixel color at the given index. The index should be an integer in the range 0 to color_count-1.
758 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.
760 if self._colors[index]["rgb888"] != value:
761 self._colors[index] = self._make_color(value)
763 def __getitem__(self, index):
766 def make_transparent(self, palette_index):
767 self._colors[palette_index].transparent = True
769 def make_opaque(self, palette_index):
770 self._colors[palette_index].transparent = False
772 def _pil_palette(self):
773 "Generate a Pillow ImagePalette and return it"
775 for channel in range(3):
776 for color in self._colors:
777 palette.append(color >> (8 * (2 - channel)) & 0xFF)
779 return ImagePalette(mode='RGB', palette=palette, size=self._color_count)
783 """Manage updating a display over 8-bit parallel bus in the background while Python code runs.
784 This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
785 It doesn’t handle display initialization.
788 def __init__(self, i2c_bus, *, device_address, reset=None):
789 """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.
791 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.
796 """Performs a hardware reset via the reset pin. Raises an exception if called when no reset pin is available.
800 def send(self, command, data):
801 """Sends the given command value followed by the full set of data. Display state, such as
802 vertical scroll, set via ``send`` may or may not be reset once the code is done.
808 """Create a Shape object with the given fixed size. Each pixel is one bit and is stored by the column
809 boundaries of the shape on each row. Each row’s boundary defaults to the full row.
812 def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
813 """Create a Shape object with the given fixed size. Each pixel is one bit and is stored by the
814 column boundaries of the shape on each row. Each row’s boundary defaults to the full row.
818 def set_boundary(self, y, start_x, end_x):
819 """Loads pre-packed data into the given row."""
824 """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple grids can share bitmaps and pixel shaders.
826 A single tile grid is also known as a Sprite.
842 """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.
844 tile_width and tile_height match the height of the bitmap by default.
846 if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
847 raise ValueError("Unsupported Bitmap type")
848 self._bitmap = bitmap
849 bitmap_width = bitmap.width
850 bitmap_height = bitmap.height
852 if not isinstance(pixel_shader, (ColorConverter, Palette)):
853 raise ValueError("Unsupported Pixel Shader type")
854 self._pixel_shader = pixel_shader
858 self._width = width # Number of Tiles Wide
859 self._height = height # Number of Tiles High
860 if tile_width is None:
861 tile_width = bitmap_width
862 if tile_height is None:
863 tile_height = bitmap_height
864 if bitmap_width % tile_width != 0:
865 raise ValueError("Tile width must exactly divide bitmap width")
866 self._tile_width = tile_width
867 if bitmap_height % tile_height != 0:
868 raise ValueError("Tile height must exactly divide bitmap height")
869 self._tile_height = tile_height
870 self._tiles = (self._width * self._height) * [default_tile]
875 """True when the TileGrid is hidden. This may be False even when a part of a hidden Group."""
879 def hidden(self, value):
884 """X position of the left edge in the parent."""
889 """Y position of the top edge in the parent."""
894 """If true, the left edge rendered will be the right edge of the right-most tile."""
898 def flip_x(self, value):
899 if not isinstance(value, bool):
900 raise TypeError("Flip X should be a boolean type")
905 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
909 def flip_y(self, value):
910 if not isinstance(value, bool):
911 raise TypeError("Flip Y should be a boolean type")
915 def transpose_xy(self):
916 """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90 degree
917 rotation can be achieved along with the corresponding mirrored version.
919 return self._transpose_xy
922 def transpose_xy(self, value):
923 if not isinstance(value, bool):
924 raise TypeError("Transpose XY should be a boolean type")
925 self._transpose_xy = value
928 def pixel_shader(self):
929 """The pixel shader of the tilegrid."""
932 def __getitem__(self, index):
933 """Returns the tile index at the given index. The index can either be
934 an x,y tuple or an int equal to ``y * width + x``'.
936 if isinstance(index, (tuple, list)):
939 index = y * self._width + x
940 elif ininstance(index, int):
941 x = index % self._width
942 y = index // self._width
943 if x > self._width or y > self._height:
944 raise ValueError("Tile index out of bounds")
945 return self._tiles[index]
947 def __setitem__(self, index, value):
948 """Sets the tile index at the given index. The index can either be
949 an x,y tuple or an int equal to ``y * width + x``.
951 if isinstance(index, (tuple, list)):
954 index = y * self._width + x
955 elif ininstance(index, int):
956 x = index % self._width
957 y = index // self._width
958 if x > width or y > self._height or index > len(self._tiles):
959 raise ValueError("Tile index out of bounds")
960 if not 0 <= value <= 255:
961 raise ValueError("Tile value out of bounds")
962 self._tiles[index] = value