/ tests / displayio_shared_bindings.py
displayio_shared_bindings.py
  1  # The MIT License (MIT)
  2  #
  3  # Copyright (c) 2019 Matt Land
  4  #
  5  # Permission is hereby granted, free of charge, to any person obtaining a copy
  6  # of this software and associated documentation files (the "Software"), to deal
  7  # in the Software without restriction, including without limitation the rights
  8  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9  # copies of the Software, and to permit persons to whom the Software is
 10  # furnished to do so, subject to the following conditions:
 11  #
 12  # The above copyright notice and this permission notice shall be included in
 13  # all copies or substantial portions of the Software.
 14  #
 15  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 21  # THE SOFTWARE.
 22  """
 23  `adafruit_imageload.tests.displayio_shared_bindings`
 24  ====================================================
 25  
 26  The classes in this file are designed to emulate Circuitpython's displayio classes
 27  for Bitmap and Palette. These mimic classes should have the same methods and interface as the real interface,
 28  but with extra validation checks, warnings, and messages to facilitate debugging.
 29  
 30  Code that can be run successfully against these classes will have a good chance of
 31   working correctly on hardware running Circuitpython, but without needing to upload code to a board
 32   after each attempt.
 33  
 34  * Author(s):  Matt Land
 35  
 36  """
 37  from typing import Union
 38  
 39  
 40  class Bitmap_C_Interface(object):
 41      """
 42      A class to simulate the displayio.Bitmap class for testing, based on
 43      https://circuitpython.readthedocs.io/en/latest/shared-bindings/displayio/Bitmap.html
 44      In case of discrepancy, the C implementation takes precedence.
 45      """
 46  
 47      def __init__(self, width: int, height: int, colors: int) -> None:
 48          self.width = width
 49          self.height = height
 50          self.colors = colors
 51          self.data = {}
 52  
 53      def _abs_pos(self, width: int, height: int) -> int:
 54          if height >= self.height:
 55              raise ValueError("height > max")
 56          if width >= self.width:
 57              raise ValueError("width > max")
 58          return width + (height * self.width)
 59  
 60      def _decode(self, position: int) -> tuple:
 61          return position % self.width, position // self.width
 62  
 63      def __setitem__(self, key: Union[tuple, int], value: int) -> None:
 64          """
 65          Set using x, y coordinates, or absolution position
 66          bitmap[0] = 1
 67          bitmap[2,1] = 5
 68          """
 69          if isinstance(key, tuple):
 70              # order is X, Y from the docs
 71              # https://github.com/adafruit/circuitpython/blob/master/shared-bindings/displayio/Bitmap.c
 72              self.__setitem__(self._abs_pos(key[0], key[1]), value)
 73              return
 74          if not isinstance(value, (int)):
 75              raise RuntimeError(f"set value as int, not {type(value)}")
 76          if value > 255:
 77              raise ValueError(f"pixel value {value} too large")
 78          if self.data.get(key):
 79              raise ValueError(
 80                  f"pixel {self._decode(key)}/{key} already set, cannot set again"
 81              )
 82          self.data[key] = value
 83  
 84      def __getitem__(self, item: Union[tuple, int]) -> bytearray:
 85          if isinstance(item, tuple):
 86              return self.__getitem__(self._abs_pos(item[0], item[1]))
 87          if item > self.height * self.width:
 88              raise RuntimeError(f"get position out of range {item}")
 89          try:
 90              return self.data[item]
 91          except KeyError:
 92              raise RuntimeError("no data at {} [{}]".format(self._decode(item), item))
 93  
 94      def validate(self, detect_empty_image=True) -> None:
 95          """
 96          method to to make sure all pixels allocated in the Bitmap
 97          were set with a value
 98          """
 99          seen_colors = set()
100          if not self.data:
101              raise ValueError("no rows were set / no data in memory")
102          for y in range(self.height):
103              for x in range(self.width):
104                  try:
105                      seen_colors.add(self[x, y])
106                  except KeyError:
107                      raise ValueError(f"missing data at {x},{y}")
108          if detect_empty_image and len(seen_colors) < 2:
109              raise ValueError(
110                  "image detected as only one color. set detect_empty_image=False to ignore"
111              )
112  
113      def __str__(self) -> str:
114          """
115          method to dump the contents of the Bitmap to a terminal,
116          for debugging purposes
117  
118          Example:
119          --------
120  
121          bitmap = Bitmap(5, 4, 4)
122          ...  # assign bitmap values
123          print(str(bitmap))
124          """
125          out = "\n"
126          for y in range(self.height):
127              for x in range(self.width):
128                  data = self[x, y]
129                  out += f"{data:>4}"
130              out += "\n"
131          return out
132  
133  
134  class Palette_C_Interface(object):
135      """
136      A class to simulates the displayio.Palette class for testing, based on
137      https://circuitpython.readthedocs.io/en/latest/shared-bindings/displayio/Palette.html
138      In case of discrepancy, the C implementation takes precedence.
139      """
140  
141      def __init__(self, num_colors: int) -> None:
142          self.num_colors = num_colors
143          self.colors = {}
144  
145      def __setitem__(self, key: int, value: Union[bytes, int, bytearray]) -> None:
146          """
147          Set using zero indexed color value
148          palette = Palette(1)
149          palette[0] = 0xFFFFFF
150  
151          """
152          if key >= self.num_colors:
153              raise ValueError(
154                  f"palette index {key} is greater than allowed by num_colors {self.num_colors}"
155              )
156          if not isinstance(value, (bytes, int, bytearray)):
157              raise ValueError(f"palette color should be bytes, not {type(value)}")
158          if isinstance(value, int) and value > 0xFFFFFF:
159              raise ValueError(f"palette color int {value} is too large")
160          if self.colors.get(key):
161              raise ValueError(
162                  f"palette color {key} was already set, should not reassign"
163              )
164          self.colors[key] = value
165  
166      def __getitem__(self, item: int) -> Union[bytes, int, bytearray]:
167          """
168          Warning: this method is not supported in the actual C interface.
169          It is provided here for debugging purposes.
170          """
171          if item >= self.num_colors:
172              raise ValueError(
173                  f"palette index {item} should be less than {self.num_colors}"
174              )
175          if not self.colors.get(item):
176              raise ValueError(f"palette index {item} is not set")
177          return self.colors[item]
178  
179      def validate(self):
180          """
181          method to make sure all colors allocated in Palette were set to a value
182          """
183          if not self.colors:
184              raise IndexError("no palette colors were set")
185          if len(self.colors) != self.num_colors:
186              raise IndexError(
187                  "palette was initialized for {} colors, but only {} were inserted".format(
188                      self.num_colors, len(self.colors)
189                  )
190              )
191          for i in range(self.num_colors):
192              try:
193                  self.colors
194              except IndexError:
195                  raise ValueError("missing color `{}` in palette color list".format(i))
196  
197      def __str__(self):
198          """
199          method to dump the contents of the Palette to a terminal,
200          for debugging purposes
201  
202          Example:
203          --------
204  
205          palette = Palette(1)
206          palette[0] = 0xFFFFFF
207          print(str(palette))
208          """
209          out = "\nPalette:\n"
210          for y in range(len(self.colors)):
211              out += f" [{y}] {self.colors[y]}\n"
212          return out