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