/ adafruit_framebuf.py
adafruit_framebuf.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2018 Kattni Rembor for Adafruit Industries 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_framebuf` 24 ==================================================== 25 26 CircuitPython pure-python framebuf module, based on the micropython framebuf module. 27 28 * Author(s): Kattni Rembor, Tony DiCola, original file created by Damien P. George 29 30 Implementation Notes 31 -------------------- 32 33 **Hardware:** 34 35 * `Adafruit SSD1306 OLED displays <https://www.adafruit.com/?q=ssd1306>`_ 36 37 **Software and Dependencies:** 38 39 * Adafruit CircuitPython firmware for the supported boards: 40 https://github.com/adafruit/circuitpython/releases 41 42 """ 43 44 __version__ = "0.0.0-auto.0" 45 __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_framebuf.git" 46 47 import os 48 import struct 49 50 # Framebuf format constants: 51 MVLSB = 0 # Single bit displays (like SSD1306 OLED) 52 RGB565 = 1 # 16-bit color displays 53 GS4_HMSB = 2 # Unimplemented! 54 MHMSB = 3 # Single bit displays like the Sharp Memory 55 56 57 class MHMSBFormat: 58 """MHMSBFormat""" 59 60 @staticmethod 61 def set_pixel(framebuf, x, y, color): 62 """Set a given pixel to a color.""" 63 index = (y * framebuf.stride + x) // 8 64 offset = 7 - x & 0x07 65 framebuf.buf[index] = (framebuf.buf[index] & ~(0x01 << offset)) | ( 66 (color != 0) << offset 67 ) 68 69 @staticmethod 70 def get_pixel(framebuf, x, y): 71 """Get the color of a given pixel""" 72 index = (y * framebuf.stride + x) // 8 73 offset = 7 - x & 0x07 74 return (framebuf.buf[index] >> offset) & 0x01 75 76 @staticmethod 77 def fill(framebuf, color): 78 """completely fill/clear the buffer with a color""" 79 if color: 80 fill = 0xFF 81 else: 82 fill = 0x00 83 for i in range(len(framebuf.buf)): 84 framebuf.buf[i] = fill 85 86 @staticmethod 87 def fill_rect(framebuf, x, y, width, height, color): 88 """Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws 89 both the outline and interior.""" 90 # pylint: disable=too-many-arguments 91 for _x in range(x, x + width): 92 offset = 7 - _x & 0x07 93 for _y in range(y, y + height): 94 index = (_y * framebuf.stride + _x) // 8 95 framebuf.buf[index] = (framebuf.buf[index] & ~(0x01 << offset)) | ( 96 (color != 0) << offset 97 ) 98 99 100 class MVLSBFormat: 101 """MVLSBFormat""" 102 103 @staticmethod 104 def set_pixel(framebuf, x, y, color): 105 """Set a given pixel to a color.""" 106 index = (y >> 3) * framebuf.stride + x 107 offset = y & 0x07 108 framebuf.buf[index] = (framebuf.buf[index] & ~(0x01 << offset)) | ( 109 (color != 0) << offset 110 ) 111 112 @staticmethod 113 def get_pixel(framebuf, x, y): 114 """Get the color of a given pixel""" 115 index = (y >> 3) * framebuf.stride + x 116 offset = y & 0x07 117 return (framebuf.buf[index] >> offset) & 0x01 118 119 @staticmethod 120 def fill(framebuf, color): 121 """completely fill/clear the buffer with a color""" 122 if color: 123 fill = 0xFF 124 else: 125 fill = 0x00 126 for i in range(len(framebuf.buf)): 127 framebuf.buf[i] = fill 128 129 @staticmethod 130 def fill_rect(framebuf, x, y, width, height, color): 131 """Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws 132 both the outline and interior.""" 133 # pylint: disable=too-many-arguments 134 while height > 0: 135 index = (y >> 3) * framebuf.stride + x 136 offset = y & 0x07 137 for w_w in range(width): 138 framebuf.buf[index + w_w] = ( 139 framebuf.buf[index + w_w] & ~(0x01 << offset) 140 ) | ((color != 0) << offset) 141 y += 1 142 height -= 1 143 144 145 class FrameBuffer: 146 """FrameBuffer object. 147 148 :param buf: An object with a buffer protocol which must be large enough to contain every 149 pixel defined by the width, height and format of the FrameBuffer. 150 :param width: The width of the FrameBuffer in pixel 151 :param height: The height of the FrameBuffer in pixel 152 :param buf_format: Specifies the type of pixel used in the FrameBuffer; permissible values 153 are listed under Constants below. These set the number of bits used to 154 encode a color value and the layout of these bits in ``buf``. Where a 155 color value c is passed to a method, c is a small integer with an encoding 156 that is dependent on the format of the FrameBuffer. 157 :param stride: The number of pixels between each horizontal line of pixels in the 158 FrameBuffer. This defaults to ``width`` but may need adjustments when 159 implementing a FrameBuffer within another larger FrameBuffer or screen. The 160 ``buf`` size must accommodate an increased step size. 161 162 """ 163 164 def __init__(self, buf, width, height, buf_format=MVLSB, stride=None): 165 # pylint: disable=too-many-arguments 166 self.buf = buf 167 self.width = width 168 self.height = height 169 self.stride = stride 170 self._font = None 171 if self.stride is None: 172 self.stride = width 173 if buf_format == MVLSB: 174 self.format = MVLSBFormat() 175 elif buf_format == MHMSB: 176 self.format = MHMSBFormat() 177 else: 178 raise ValueError("invalid format") 179 self._rotation = 0 180 181 @property 182 def rotation(self): 183 """The rotation setting of the display, can be one of (0, 1, 2, 3)""" 184 return self._rotation 185 186 @rotation.setter 187 def rotation(self, val): 188 if not val in (0, 1, 2, 3): 189 raise RuntimeError("Bad rotation setting") 190 self._rotation = val 191 192 def fill(self, color): 193 """Fill the entire FrameBuffer with the specified color.""" 194 self.format.fill(self, color) 195 196 def fill_rect(self, x, y, width, height, color): 197 """Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws 198 both the outline and interior.""" 199 # pylint: disable=too-many-arguments, too-many-boolean-expressions 200 self.rect(x, y, width, height, color, fill=True) 201 202 def pixel(self, x, y, color=None): 203 """If ``color`` is not given, get the color value of the specified pixel. If ``color`` is 204 given, set the specified pixel to the given color.""" 205 if self.rotation == 1: 206 x, y = y, x 207 x = self.width - x - 1 208 if self.rotation == 2: 209 x = self.width - x - 1 210 y = self.height - y - 1 211 if self.rotation == 3: 212 x, y = y, x 213 y = self.height - y - 1 214 215 if x < 0 or x >= self.width or y < 0 or y >= self.height: 216 return None 217 if color is None: 218 return self.format.get_pixel(self, x, y) 219 self.format.set_pixel(self, x, y, color) 220 return None 221 222 def hline(self, x, y, width, color): 223 """Draw a horizontal line up to a given length.""" 224 self.rect(x, y, width, 1, color, fill=True) 225 226 def vline(self, x, y, height, color): 227 """Draw a vertical line up to a given length.""" 228 self.rect(x, y, 1, height, color, fill=True) 229 230 def circle(self, center_x, center_y, radius, color): 231 """Draw a circle at the given midpoint location, radius and color. 232 The ```circle``` method draws only a 1 pixel outline.""" 233 x = radius - 1 234 y = 0 235 d_x = 1 236 d_y = 1 237 err = d_x - (radius << 1) 238 while x >= y: 239 self.pixel(center_x + x, center_y + y, color) 240 self.pixel(center_x + y, center_y + x, color) 241 self.pixel(center_x - y, center_y + x, color) 242 self.pixel(center_x - x, center_y + y, color) 243 self.pixel(center_x - x, center_y - y, color) 244 self.pixel(center_x - y, center_y - x, color) 245 self.pixel(center_x + y, center_y - x, color) 246 self.pixel(center_x + x, center_y - y, color) 247 if err <= 0: 248 y += 1 249 err += d_y 250 d_y += 2 251 if err > 0: 252 x -= 1 253 d_x += 2 254 err += d_x - (radius << 1) 255 256 def rect(self, x, y, width, height, color, *, fill=False): 257 """Draw a rectangle at the given location, size and color. The ```rect``` method draws only 258 a 1 pixel outline.""" 259 # pylint: disable=too-many-arguments 260 if self.rotation == 1: 261 x, y = y, x 262 width, height = height, width 263 x = self.width - x - width 264 if self.rotation == 2: 265 x = self.width - x - width 266 y = self.height - y - height 267 if self.rotation == 3: 268 x, y = y, x 269 width, height = height, width 270 y = self.height - y - height 271 272 # pylint: disable=too-many-boolean-expressions 273 if ( 274 width < 1 275 or height < 1 276 or (x + width) <= 0 277 or (y + height) <= 0 278 or y >= self.height 279 or x >= self.width 280 ): 281 return 282 x_end = min(self.width - 1, x + width - 1) 283 y_end = min(self.height - 1, y + height - 1) 284 x = max(x, 0) 285 y = max(y, 0) 286 if fill: 287 self.format.fill_rect(self, x, y, x_end - x + 1, y_end - y + 1, color) 288 else: 289 self.format.fill_rect(self, x, y, x_end - x + 1, 1, color) 290 self.format.fill_rect(self, x, y, 1, y_end - y + 1, color) 291 self.format.fill_rect(self, x, y_end, x_end - x + 1, 1, color) 292 self.format.fill_rect(self, x_end, y, 1, y_end - y + 1, color) 293 294 def line(self, x_0, y_0, x_1, y_1, color): 295 # pylint: disable=too-many-arguments 296 """Bresenham's line algorithm""" 297 d_x = abs(x_1 - x_0) 298 d_y = abs(y_1 - y_0) 299 x, y = x_0, y_0 300 s_x = -1 if x_0 > x_1 else 1 301 s_y = -1 if y_0 > y_1 else 1 302 if d_x > d_y: 303 err = d_x / 2.0 304 while x != x_1: 305 self.pixel(x, y, color) 306 err -= d_y 307 if err < 0: 308 y += s_y 309 err += d_x 310 x += s_x 311 else: 312 err = d_y / 2.0 313 while y != y_1: 314 self.pixel(x, y, color) 315 err -= d_x 316 if err < 0: 317 x += s_x 318 err += d_y 319 y += s_y 320 self.pixel(x, y, color) 321 322 def blit(self): 323 """blit is not yet implemented""" 324 raise NotImplementedError() 325 326 def scroll(self, delta_x, delta_y): 327 """shifts framebuf in x and y direction""" 328 if delta_x < 0: 329 shift_x = 0 330 xend = self.width + delta_x 331 dt_x = 1 332 else: 333 shift_x = self.width - 1 334 xend = delta_x - 1 335 dt_x = -1 336 if delta_y < 0: 337 y = 0 338 yend = self.height + delta_y 339 dt_y = 1 340 else: 341 y = self.height - 1 342 yend = delta_y - 1 343 dt_y = -1 344 while y != yend: 345 x = shift_x 346 while x != xend: 347 self.format.set_pixel( 348 self, x, y, self.format.get_pixel(self, x - delta_x, y - delta_y) 349 ) 350 x += dt_x 351 y += dt_y 352 353 # pylint: disable=too-many-arguments 354 def text(self, string, x, y, color, *, font_name="font5x8.bin", size=1): 355 """Place text on the screen in variables sizes. Breaks on \n to next line. 356 357 Does not break on line going off screen. 358 """ 359 for chunk in string.split("\n"): 360 if not self._font or self._font.font_name != font_name: 361 # load the font! 362 self._font = BitmapFont(font_name) 363 w = self._font.font_width 364 for i, char in enumerate(chunk): 365 self._font.draw_char( 366 char, x + (i * (w + 1)) * size, y, self, color, size=size 367 ) 368 y += self._font.font_height * size 369 370 # pylint: enable=too-many-arguments 371 372 def image(self, img): 373 """Set buffer to value of Python Imaging Library image. The image should 374 be in 1 bit mode and a size equal to the display size.""" 375 if img.mode != "1": 376 raise ValueError("Image must be in mode 1.") 377 imwidth, imheight = img.size 378 if imwidth != self.width or imheight != self.height: 379 raise ValueError( 380 "Image must be same dimensions as display ({0}x{1}).".format( 381 self.width, self.height 382 ) 383 ) 384 # Grab all the pixels from the image, faster than getpixel. 385 pixels = img.load() 386 # Clear buffer 387 for i in range(len(self.buf)): 388 self.buf[i] = 0 389 # Iterate through the pixels 390 for x in range(self.width): # yes this double loop is slow, 391 for y in range(self.height): # but these displays are small! 392 if pixels[(x, y)]: 393 self.pixel(x, y, 1) # only write if pixel is true 394 395 396 # MicroPython basic bitmap font renderer. 397 # Author: Tony DiCola 398 # License: MIT License (https://opensource.org/licenses/MIT) 399 class BitmapFont: 400 """A helper class to read binary font tiles and 'seek' through them as a 401 file to display in a framebuffer. We use file access so we dont waste 1KB 402 of RAM on a font!""" 403 404 def __init__(self, font_name="font5x8.bin"): 405 # Specify the drawing area width and height, and the pixel function to 406 # call when drawing pixels (should take an x and y param at least). 407 # Optionally specify font_name to override the font file to use (default 408 # is font5x8.bin). The font format is a binary file with the following 409 # format: 410 # - 1 unsigned byte: font character width in pixels 411 # - 1 unsigned byte: font character height in pixels 412 # - x bytes: font data, in ASCII order covering all 255 characters. 413 # Each character should have a byte for each pixel column of 414 # data (i.e. a 5x8 font has 5 bytes per character). 415 self.font_name = font_name 416 417 # Open the font file and grab the character width and height values. 418 # Note that only fonts up to 8 pixels tall are currently supported. 419 try: 420 self._font = open(self.font_name, "rb") 421 self.font_width, self.font_height = struct.unpack("BB", self._font.read(2)) 422 # simple font file validation check based on expected file size 423 if 2 + 256 * self.font_width != os.stat(font_name)[6]: 424 raise RuntimeError("Invalid font file: " + font_name) 425 except OSError: 426 print("Could not find font file", font_name) 427 raise 428 except OverflowError: 429 # os.stat can throw this on boards without long int support 430 # just hope the font file is valid and press on 431 pass 432 433 def deinit(self): 434 """Close the font file as cleanup.""" 435 self._font.close() 436 437 def __enter__(self): 438 """Initialize/open the font file""" 439 self.__init__() 440 return self 441 442 def __exit__(self, exception_type, exception_value, traceback): 443 """cleanup on exit""" 444 self.deinit() 445 446 def draw_char( 447 self, char, x, y, framebuffer, color, size=1 448 ): # pylint: disable=too-many-arguments 449 """Draw one character at position (x,y) to a framebuffer in a given color""" 450 size = max(size, 1) 451 # Don't draw the character if it will be clipped off the visible area. 452 # if x < -self.font_width or x >= framebuffer.width or \ 453 # y < -self.font_height or y >= framebuffer.height: 454 # return 455 # Go through each column of the character. 456 for char_x in range(self.font_width): 457 # Grab the byte for the current column of font data. 458 self._font.seek(2 + (ord(char) * self.font_width) + char_x) 459 try: 460 line = struct.unpack("B", self._font.read(1))[0] 461 except RuntimeError: 462 continue # maybe character isnt there? go to next 463 # Go through each row in the column byte. 464 for char_y in range(self.font_height): 465 # Draw a pixel for each bit that's flipped on. 466 if (line >> char_y) & 0x1: 467 framebuffer.fill_rect( 468 x + char_x * size, y + char_y * size, size, size, color 469 ) 470 471 def width(self, text): 472 """Return the pixel width of the specified text message.""" 473 return len(text) * (self.font_width + 1) 474 475 476 class FrameBuffer1(FrameBuffer): # pylint: disable=abstract-method 477 """FrameBuffer1 object. Inherits from FrameBuffer."""