]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_bitmap.py
Fewer bugs, more code, shape done
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _bitmap.py
1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
2 #
3 # SPDX-License-Identifier: MIT
4
5 """
6 `displayio.bitmap`
7 ================================================================================
8
9 displayio for Blinka
10
11 **Software and Dependencies:**
12
13 * Adafruit Blinka:
14   https://github.com/adafruit/Adafruit_Blinka/releases
15
16 * Author(s): Melissa LeBlanc-Williams
17
18 """
19
20 from __future__ import annotations
21 from typing import Union, Tuple
22 from PIL import Image
23 from ._structs import RectangleStruct
24 from ._area import Area
25
26 __version__ = "0.0.0+auto.0"
27 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
28
29
30 class Bitmap:
31     """Stores values of a certain size in a 2D array"""
32
33     def __init__(self, width: int, height: int, value_count: int):
34         """Create a Bitmap object with the given fixed size. Each pixel stores a value that is
35         used to index into a corresponding palette. This enables differently colored sprites to
36         share the underlying Bitmap. value_count is used to minimize the memory used to store
37         the Bitmap.
38         """
39         self._bmp_width = width
40         self._bmp_height = height
41         self._read_only = False
42
43         if value_count < 0:
44             raise ValueError("value_count must be > 0")
45
46         bits = 1
47         while (value_count - 1) >> bits:
48             if bits < 8:
49                 bits = bits << 1
50             else:
51                 bits += 8
52
53         self._bits_per_value = bits
54
55         if (
56             self._bits_per_value > 8
57             and self._bits_per_value != 16
58             and self._bits_per_value != 32
59         ):
60             raise NotImplementedError("Invalid bits per value")
61
62         self._image = Image.new("P", (width, height), 0)
63         self._dirty_area = RectangleStruct(0, 0, width, height)
64
65     def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
66         """
67         Returns the value at the given index. The index can either be
68         an x,y tuple or an int equal to `y * width + x`.
69         """
70         if isinstance(index, (tuple, list)):
71             x, y = index
72         elif isinstance(index, int):
73             x = index % self._bmp_width
74             y = index // self._bmp_width
75         else:
76             raise TypeError("Index is not an int, list, or tuple")
77
78         if x > self._image.width or y > self._image.height:
79             raise ValueError(f"Index {index} is out of range")
80         return self._get_pixel(x, y)
81
82     def _get_pixel(self, x: int, y: int) -> int:
83         return self._image.getpixel((x, y))
84
85     def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
86         """
87         Sets the value at the given index. The index can either be
88         an x,y tuple or an int equal to `y * width + x`.
89         """
90         if self._read_only:
91             raise RuntimeError("Read-only object")
92         if isinstance(index, (tuple, list)):
93             x = index[0]
94             y = index[1]
95             index = y * self._bmp_width + x
96         elif isinstance(index, int):
97             x = index % self._bmp_width
98             y = index // self._bmp_width
99         self._image.putpixel((x, y), value)
100         if self._dirty_area.x1 == self._dirty_area.x2:
101             self._dirty_area.x1 = x
102             self._dirty_area.x2 = x + 1
103             self._dirty_area.y1 = y
104             self._dirty_area.y2 = y + 1
105         else:
106             if x < self._dirty_area.x1:
107                 self._dirty_area.x1 = x
108             elif x >= self._dirty_area.x2:
109                 self._dirty_area.x2 = x + 1
110             if y < self._dirty_area.y1:
111                 self._dirty_area.y1 = y
112             elif y >= self._dirty_area.y2:
113                 self._dirty_area.y2 = y + 1
114
115     def _finish_refresh(self):
116         self._dirty_area.x1 = 0
117         self._dirty_area.x2 = 0
118
119     def fill(self, value: int) -> None:
120         """Fills the bitmap with the supplied palette index value."""
121         self._image = Image.new("P", (self._bmp_width, self._bmp_height), value)
122         self._dirty_area = RectangleStruct(0, 0, self._bmp_width, self._bmp_height)
123
124     def blit(
125         self,
126         x: int,
127         y: int,
128         source_bitmap: Bitmap,
129         *,
130         x1: int,
131         y1: int,
132         x2: int,
133         y2: int,
134         skip_index: int,
135     ) -> None:
136         """Inserts the source_bitmap region defined by rectangular boundaries"""
137         # pylint: disable=invalid-name
138         if x2 is None:
139             x2 = source_bitmap.width
140         if y2 is None:
141             y2 = source_bitmap.height
142
143         # Rearrange so that x1 < x2 and y1 < y2
144         if x1 > x2:
145             x1, x2 = x2, x1
146         if y1 > y2:
147             y1, y2 = y2, y1
148
149         # Ensure that x2 and y2 are within source bitmap size
150         x2 = min(x2, source_bitmap.width)
151         y2 = min(y2, source_bitmap.height)
152
153         for y_count in range(y2 - y1):
154             for x_count in range(x2 - x1):
155                 x_placement = x + x_count
156                 y_placement = y + y_count
157
158                 if (self.width > x_placement >= 0) and (
159                     self.height > y_placement >= 0
160                 ):  # ensure placement is within target bitmap
161                     # get the palette index from the source bitmap
162                     this_pixel_color = source_bitmap[
163                         y1
164                         + (
165                             y_count * source_bitmap.width
166                         )  # Direct index into a bitmap array is speedier than [x,y] tuple
167                         + x1
168                         + x_count
169                     ]
170
171                     if (skip_index is None) or (this_pixel_color != skip_index):
172                         self[  # Direct index into a bitmap array is speedier than [x,y] tuple
173                             y_placement * self.width + x_placement
174                         ] = this_pixel_color
175                 elif y_placement > self.height:
176                     break
177
178     def dirty(self, x1: int = 0, y1: int = 0, x2: int = -1, y2: int = -1) -> None:
179         """Inform displayio of bitmap updates done via the buffer protocol."""
180         # pylint: disable=invalid-name
181         if x2 == -1:
182             x2 = self._bmp_width
183         if y2 == -1:
184             y2 = self._bmp_height
185         area = Area(x1, y1, x2, y2)
186         area.canon()
187         area.union(self._dirty_area, area)
188         bitmap_area = Area(0, 0, self._bmp_width, self._bmp_height)
189         area.compute_overlap(bitmap_area, self._dirty_area)
190
191     def _finish_refresh(self):
192         if self._read_only:
193             return
194         self._dirty_area.x1 = 0
195         self._dirty_area.x2 = 0
196
197     def _get_refresh_areas(self, areas: list[Area]) -> None:
198         if self._dirty_area.x1 == self._dirty_area.x2 or self._read_only:
199             return
200         areas.append(self._dirty_area)
201
202     @property
203     def width(self) -> int:
204         """Width of the bitmap. (read only)"""
205         return self._bmp_width
206
207     @property
208     def height(self) -> int:
209         """Height of the bitmap. (read only)"""
210         return self._bmp_height