From: Melissa LeBlanc-Williams Date: Fri, 13 Jun 2025 16:41:19 +0000 (-0700) Subject: Merge pull request #156 from janvolck/main X-Git-Tag: 2.3.1~1 X-Git-Url: https://git.ayoreis.com/hackapet/Adafruit_Blinka_Displayio.git/commitdiff_plain/2a71c94d562fb8c5ec12a1697d26107ed6a0e4fd?hp=00aadca31c09318973ead0e7de3b6b77049abf2b Merge pull request #156 from janvolck/main relax displayio._background thread --- diff --git a/README.rst b/README.rst index 8007209..4338cae 100644 --- a/README.rst +++ b/README.rst @@ -58,6 +58,8 @@ Contributions are welcome! Please read our `Code of Conduct `_ before contributing to help this project stay welcoming. +To make sure your pull request will pass the commit checks, use pre-commit to check your code formatting. Please see the guide page here for more details: https://learn.adafruit.com/contribute-to-circuitpython-with-git-and-github/check-your-code + Documentation ============= diff --git a/bitmaptools/__init__.py b/bitmaptools/__init__.py new file mode 100644 index 0000000..ca5b27e --- /dev/null +++ b/bitmaptools/__init__.py @@ -0,0 +1,941 @@ +# SPDX-FileCopyrightText: 2025 Tim Cocks +# +# SPDX-License-Identifier: MIT +""" +Collection of bitmap manipulation tools +""" + +import math +import struct +from typing import Optional, Tuple, BinaryIO +import circuitpython_typing +from displayio import Bitmap, Colorspace + + +# pylint: disable=invalid-name, too-many-arguments, too-many-locals, too-many-branches, too-many-statements +def fill_region(dest_bitmap: Bitmap, x1: int, y1: int, x2: int, y2: int, value: int): + """Draws the color value into the destination bitmap within the + rectangular region bounded by (x1,y1) and (x2,y2), exclusive. + + :param bitmap dest_bitmap: Destination bitmap that will be written into + :param int x1: x-pixel position of the first corner of the rectangular fill region + :param int y1: y-pixel position of the first corner of the rectangular fill region + :param int x2: x-pixel position of the second corner of the rectangular fill region (exclusive) + :param int y2: y-pixel position of the second corner of the rectangular fill region (exclusive) + :param int value: Bitmap palette index that will be written into the rectangular + fill region in the destination bitmap""" + + for y in range(y1, y2): + for x in range(x1, x2): + dest_bitmap[x, y] = value + + +def draw_line(dest_bitmap: Bitmap, x1: int, y1: int, x2: int, y2: int, value: int): + """Draws a line into a bitmap specified two endpoints (x1,y1) and (x2,y2). + + :param bitmap dest_bitmap: Destination bitmap that will be written into + :param int x1: x-pixel position of the line's first endpoint + :param int y1: y-pixel position of the line's first endpoint + :param int x2: x-pixel position of the line's second endpoint + :param int y2: y-pixel position of the line's second endpoint + :param int value: Bitmap palette index that will be written into the + line in the destination bitmap""" + dx = abs(x2 - x1) + sx = 1 if x1 < x2 else -1 + dy = -abs(y2 - y1) + sy = 1 if y1 < y2 else -1 + error = dx + dy + + while True: + dest_bitmap[x1, y1] = value + if x1 == x2 and y1 == y2: + break + e2 = 2 * error + if e2 >= dy: + error += dy + x1 += sx + if e2 <= dx: + error += dx + y1 += sy + + +def draw_circle(dest_bitmap: Bitmap, x: int, y: int, radius: int, value: int): + """Draws a circle into a bitmap specified using a center (x0,y0) and radius r. + + :param bitmap dest_bitmap: Destination bitmap that will be written into + :param int x: x-pixel position of the circle's center + :param int y: y-pixel position of the circle's center + :param int radius: circle's radius + :param int value: Bitmap palette index that will be written into the + circle in the destination bitmap""" + + x = max(0, min(x, dest_bitmap.width - 1)) + y = max(0, min(y, dest_bitmap.height - 1)) + + xb = 0 + yb = radius + d = 3 - 2 * radius + + # Bresenham's circle algorithm + while xb <= yb: + dest_bitmap[xb + x, yb + y] = value + dest_bitmap[-xb + x, -yb + y] = value + dest_bitmap[-xb + x, yb + y] = value + dest_bitmap[xb + x, -yb + y] = value + dest_bitmap[yb + x, xb + y] = value + dest_bitmap[-yb + x, xb + y] = value + dest_bitmap[-yb + x, -xb + y] = value + dest_bitmap[yb + x, -xb + y] = value + + if d <= 0: + d = d + (4 * xb) + 6 + else: + d = d + 4 * (xb - yb) + 10 + yb = yb - 1 + xb += 1 + + +def draw_polygon( + dest_bitmap: Bitmap, + xs: circuitpython_typing.ReadableBuffer, + ys: circuitpython_typing.ReadableBuffer, + value: int, + close: bool = True, +): + """Draw a polygon connecting points on provided bitmap with provided value + + :param bitmap dest_bitmap: Destination bitmap that will be written into + :param ReadableBuffer xs: x-pixel position of the polygon's vertices + :param ReadableBuffer ys: y-pixel position of the polygon's vertices + :param int value: Bitmap palette index that will be written into the + line in the destination bitmap + :param bool close: (Optional) Whether to connect first and last point. (True) + """ + if len(xs) != len(ys): + raise ValueError("Length of xs and ys must be equal.") + + for i in range(len(xs) - 1): + cur_point = (xs[i], ys[i]) + next_point = (xs[i + 1], ys[i + 1]) + print(f"cur: {cur_point}, next: {next_point}") + draw_line( + dest_bitmap=dest_bitmap, + x1=cur_point[0], + y1=cur_point[1], + x2=next_point[0], + y2=next_point[1], + value=value, + ) + + if close: + print(f"close: {(xs[0], ys[0])} - {(xs[-1], ys[-1])}") + draw_line( + dest_bitmap=dest_bitmap, + x1=xs[0], + y1=ys[0], + x2=xs[-1], + y2=ys[-1], + value=value, + ) + + +def blit( + dest_bitmap: Bitmap, + source_bitmap: Bitmap, + x: int, + y: int, + *, + x1: int = 0, + y1: int = 0, + x2: int | None = None, + y2: int | None = None, + skip_source_index: int | None = None, + skip_dest_index: int | None = None, +): + """Inserts the source_bitmap region defined by rectangular boundaries + (x1,y1) and (x2,y2) into the bitmap at the specified (x,y) location. + + :param bitmap dest_bitmap: Destination bitmap that the area will + be copied into. + :param bitmap source_bitmap: Source bitmap that contains the graphical + region to be copied + :param int x: Horizontal pixel location in bitmap where source_bitmap + upper-left corner will be placed + :param int y: Vertical pixel location in bitmap where source_bitmap upper-left + corner will be placed + :param int x1: Minimum x-value for rectangular bounding box to be + copied from the source bitmap + :param int y1: Minimum y-value for rectangular bounding box to be + copied from the source bitmap + :param int x2: Maximum x-value (exclusive) for rectangular + bounding box to be copied from the source bitmap. + If unspecified or `None`, the source bitmap width is used. + :param int y2: Maximum y-value (exclusive) for rectangular + bounding box to be copied from the source bitmap. + If unspecified or `None`, the source bitmap height is used. + :param int skip_source_index: bitmap palette index in the source that + will not be copied, set to None to copy all pixels + :param int skip_dest_index: bitmap palette index in the + destination bitmap that will not get overwritten by the + pixels from the source""" + + # pylint: disable=invalid-name + if x2 is None: + x2 = source_bitmap.width + if y2 is None: + y2 = source_bitmap.height + + # Rearrange so that x1 < x2 and y1 < y2 + if x1 > x2: + x1, x2 = x2, x1 + if y1 > y2: + y1, y2 = y2, y1 + + # Ensure that x2 and y2 are within source bitmap size + x2 = min(x2, source_bitmap.width) + y2 = min(y2, source_bitmap.height) + + for y_count in range(y2 - y1): + for x_count in range(x2 - x1): + x_placement = x + x_count + y_placement = y + y_count + + if (dest_bitmap.width > x_placement >= 0) and ( + dest_bitmap.height > y_placement >= 0 + ): # ensure placement is within target bitmap + # get the palette index from the source bitmap + this_pixel_color = source_bitmap[ + y1 + (y_count * source_bitmap.width) + x1 + x_count + ] + + if (skip_source_index is None) or ( + this_pixel_color != skip_source_index + ): + if (skip_dest_index is None) or ( + dest_bitmap[y_placement * dest_bitmap.width + x_placement] + != skip_dest_index + ): + dest_bitmap[ + y_placement * dest_bitmap.width + x_placement + ] = this_pixel_color + elif y_placement > dest_bitmap.height: + break + + +def rotozoom( + dest_bitmap: Bitmap, + source_bitmap: Bitmap, + *, + ox: Optional[int] = None, + oy: Optional[int] = None, + dest_clip0: Optional[Tuple[int, int]] = None, + dest_clip1: Optional[Tuple[int, int]] = None, + px: Optional[int] = None, + py: Optional[int] = None, + source_clip0: Optional[Tuple[int, int]] = None, + source_clip1: Optional[Tuple[int, int]] = None, + angle: Optional[float] = None, + scale: Optional[float] = None, + skip_index: Optional[int] = None, +): + """Inserts the source bitmap region into the destination bitmap with rotation + (angle), scale and clipping (both on source and destination bitmaps). + + :param bitmap dest_bitmap: Destination bitmap that will be copied into + :param bitmap source_bitmap: Source bitmap that contains the graphical region to be copied + :param int ox: Horizontal pixel location in destination bitmap where source bitmap + point (px,py) is placed. Defaults to None which causes it to use the horizontal + midway point of the destination bitmap. + :param int oy: Vertical pixel location in destination bitmap where source bitmap + point (px,py) is placed. Defaults to None which causes it to use the vertical + midway point of the destination bitmap. + :param Tuple[int,int] dest_clip0: First corner of rectangular destination clipping + region that constrains region of writing into destination bitmap + :param Tuple[int,int] dest_clip1: Second corner of rectangular destination clipping + region that constrains region of writing into destination bitmap + :param int px: Horizontal pixel location in source bitmap that is placed into the + destination bitmap at (ox,oy). Defaults to None which causes it to use the + horizontal midway point in the source bitmap. + :param int py: Vertical pixel location in source bitmap that is placed into the + destination bitmap at (ox,oy). Defaults to None which causes it to use the + vertical midway point in the source bitmap. + :param Tuple[int,int] source_clip0: First corner of rectangular source clipping + region that constrains region of reading from the source bitmap + :param Tuple[int,int] source_clip1: Second corner of rectangular source clipping + region that constrains region of reading from the source bitmap + :param float angle: Angle of rotation, in radians (positive is clockwise direction). + Defaults to None which gets treated as 0.0 radians or no rotation. + :param float scale: Scaling factor. Defaults to None which gets treated as 1.0 or same + as original source size. + :param int skip_index: Bitmap palette index in the source that will not be copied, + set to None to copy all pixels""" + if ox is None: + ox = dest_bitmap.width // 2 + if oy is None: + oy = dest_bitmap.height // 2 + + if dest_clip0 is None: + dest_clip0 = (0, 0) + if dest_clip1 is None: + dest_clip1 = (dest_bitmap.width, dest_bitmap.height) + + if px is None: + px = source_bitmap.width // 2 + if py is None: + py = source_bitmap.height // 2 + + if source_clip0 is None: + source_clip0 = (0, 0) + if source_clip1 is None: + source_clip1 = (source_bitmap.width, source_bitmap.height) + + if angle is None: + angle = 0.0 + if scale is None: + scale = 1.0 + + dest_clip0_x, dest_clip0_y = dest_clip0 + dest_clip1_x, dest_clip1_y = dest_clip1 + source_clip0_x, source_clip0_y = source_clip0 + source_clip1_x, source_clip1_y = source_clip1 + + minx = dest_clip1_x + miny = dest_clip1_y + maxx = dest_clip0_x + maxy = dest_clip0_y + + sin_angle = math.sin(angle) + cos_angle = math.cos(angle) + + def update_bounds(dx, dy): + nonlocal minx, maxx, miny, maxy + if dx < minx: + minx = int(dx) + if dx > maxx: + maxx = int(dx) + if dy < miny: + miny = int(dy) + if dy > maxy: + maxy = int(dy) + + w = source_bitmap.width + h = source_bitmap.height + + dx = -cos_angle * px * scale + sin_angle * py * scale + ox + dy = -sin_angle * px * scale - cos_angle * py * scale + oy + update_bounds(dx, dy) + + dx = cos_angle * (w - px) * scale + sin_angle * py * scale + ox + dy = sin_angle * (w - px) * scale - cos_angle * py * scale + oy + update_bounds(dx, dy) + + dx = cos_angle * (w - px) * scale - sin_angle * (h - py) * scale + ox + dy = sin_angle * (w - px) * scale + cos_angle * (h - py) * scale + oy + update_bounds(dx, dy) + + dx = -cos_angle * px * scale - sin_angle * (h - py) * scale + ox + dy = -sin_angle * px * scale + cos_angle * (h - py) * scale + oy + update_bounds(dx, dy) + + # Clip to destination area + minx = max(minx, dest_clip0_x) + maxx = min(maxx, dest_clip1_x - 1) + miny = max(miny, dest_clip0_y) + maxy = min(maxy, dest_clip1_y - 1) + + dv_col = cos_angle / scale + du_col = sin_angle / scale + du_row = dv_col + dv_row = -du_col + + startu = px - (ox * dv_col + oy * du_col) + startv = py - (ox * dv_row + oy * du_row) + + rowu = startu + miny * du_col + rowv = startv + miny * dv_col + + for y in range(miny, maxy + 1): + u = rowu + minx * du_row + v = rowv + minx * dv_row + for x in range(minx, maxx + 1): + if (source_clip0_x <= u < source_clip1_x) and ( + source_clip0_y <= v < source_clip1_y + ): + c = source_bitmap[int(u), int(v)] + if skip_index is None or c != skip_index: + dest_bitmap[x, y] = c + u += du_row + v += dv_row + rowu += du_col + rowv += dv_col + + +def arrayblit( + bitmap: Bitmap, + data: circuitpython_typing.ReadableBuffer, + x1: int = 0, + y1: int = 0, + x2: Optional[int] = None, + y2: Optional[int] = None, + skip_index: Optional[int] = None, +): + """Inserts pixels from ``data`` into the rectangle + of width×height pixels with the upper left corner at ``(x,y)`` + + The values from ``data`` are taken modulo the number of color values + available in the destination bitmap. + + If x1 or y1 are not specified, they are taken as 0. If x2 or y2 + are not specified, or are given as None, they are taken as the width + and height of the image. + + The coordinates affected by the blit are + ``x1 <= x < x2`` and ``y1 <= y < y2``. + + ``data`` must contain at least as many elements as required. If it + contains excess elements, they are ignored. + + The blit takes place by rows, so the first elements of ``data`` go + to the first row, the next elements to the next row, and so on. + + :param displayio.Bitmap bitmap: A writable bitmap + :param ReadableBuffer data: Buffer containing the source pixel values + :param int x1: The left corner of the area to blit into (inclusive) + :param int y1: The top corner of the area to blit into (inclusive) + :param int x2: The right of the area to blit into (exclusive) + :param int y2: The bottom corner of the area to blit into (exclusive) + :param int skip_index: Bitmap palette index in the source + that will not be copied, set to None to copy all pixels + + """ + if x2 is None: + x2 = bitmap.width + if y2 is None: + y2 = bitmap.height + + _value_count = 2**bitmap._bits_per_value # pylint: disable=protected-access + for y in range(y1, y2): + for x in range(x1, x2): + i = y * (x2 - x1) + x + value = int(data[i] % _value_count) + if skip_index is None or value != skip_index: + bitmap[x, y] = value + + +def readinto( + bitmap: Bitmap, + file: BinaryIO, + bits_per_pixel: int, + element_size: int = 1, + reverse_pixels_in_element: bool = False, + swap_bytes: bool = False, + reverse_rows: bool = False, +): + """Reads from a binary file into a bitmap. + + The file must be positioned so that it consists of ``bitmap.height`` + rows of pixel data, where each row is the smallest multiple + of ``element_size`` bytes that can hold ``bitmap.width`` pixels. + + The bytes in an element can be optionally swapped, and the pixels + in an element can be reversed. Also, therow loading direction can + be reversed, which may be requires for loading certain bitmap files. + + This function doesn't parse image headers, but is useful to + speed up loading of uncompressed image formats such as PCF glyph data. + + :param displayio.Bitmap bitmap: A writable bitmap + :param typing.BinaryIO file: A file opened in binary mode + :param int bits_per_pixel: Number of bits per pixel. + Values 1, 2, 4, 8, 16, 24, and 32 are supported; + :param int element_size: Number of bytes per element. + Values of 1, 2, and 4 are supported, except that 24 + ``bits_per_pixel`` requires 1 byte per element. + :param bool reverse_pixels_in_element: If set, the first pixel in a + word is taken from the Most Significant Bits; otherwise, + it is taken from the Least Significant Bits. + :param bool swap_bytes_in_element: If the ``element_size`` is not 1, + then reverse the byte order of each element read. + :param bool reverse_rows: Reverse the direction of the row loading + (required for some bitmap images). + """ + width = bitmap.width + height = bitmap.height + bits_per_value = bitmap._bits_per_value # pylint: disable=protected-access + mask = (1 << bits_per_value) - 1 + + elements_per_row = (width * bits_per_pixel + element_size * 8 - 1) // ( + element_size * 8 + ) + rowsize = element_size * elements_per_row + + for y in range(height): + row_bytes = file.read(rowsize) + if len(row_bytes) != rowsize: + raise EOFError() + + # Convert the raw bytes into the appropriate type array for processing + rowdata = bytearray(row_bytes) + + if swap_bytes: + if element_size == 2: + rowdata = bytearray( + b"".join( + struct.pack("H", rowdata[i : i + 2])[0]) + for i in range(0, len(rowdata), 2) + ) + ) + elif element_size == 4: + rowdata = bytearray( + b"".join( + struct.pack("I", rowdata[i : i + 4])[0]) + for i in range(0, len(rowdata), 4) + ) + ) + + y_draw = height - 1 - y if reverse_rows else y + + for x in range(width): + value = 0 + if bits_per_pixel == 1: + byte_offset = x // 8 + bit_offset = 7 - (x % 8) if reverse_pixels_in_element else x % 8 + value = (rowdata[byte_offset] >> bit_offset) & 0x1 + elif bits_per_pixel == 2: + byte_offset = x // 4 + bit_index = 3 - (x % 4) if reverse_pixels_in_element else x % 4 + bit_offset = 2 * bit_index + value = (rowdata[byte_offset] >> bit_offset) & 0x3 + elif bits_per_pixel == 4: + byte_offset = x // 2 + bit_index = 1 - (x % 2) if reverse_pixels_in_element else x % 2 + bit_offset = 4 * bit_index + value = (rowdata[byte_offset] >> bit_offset) & 0xF + elif bits_per_pixel == 8: + value = rowdata[x] + elif bits_per_pixel == 16: + value = struct.unpack_from("> 8) & 0xFF) + sp2 = ((sp2 & 0xFF) << 8) | ((sp2 >> 8) & 0xFF) + + blend_source1 = skip_source1_index is None or sp1 != skip_source1_index + blend_source2 = skip_source2_index is None or sp2 != skip_source2_index + + if blend_source1 and blend_source2: + ifactor_blend = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256 + + red_dca = ((sp1 & r_mask) >> 8) * ifactor1 + grn_dca = ((sp1 & g_mask) >> 3) * ifactor1 + blu_dca = ((sp1 & b_mask) << 3) * ifactor1 + + red_sca = ((sp2 & r_mask) >> 8) * ifactor2 + grn_sca = ((sp2 & g_mask) >> 3) * ifactor2 + blu_sca = ((sp2 & b_mask) << 3) * ifactor2 + + if blendmode == BlendMode.Screen: + red_blend = red_sca + red_dca - (red_sca * red_dca // 65536) + grn_blend = grn_sca + grn_dca - (grn_sca * grn_dca // 65536) + blu_blend = blu_sca + blu_dca - (blu_sca * blu_dca // 65536) + elif blendmode == BlendMode.Normal: + red_blend = red_sca + red_dca * (256 - ifactor2) // 256 + grn_blend = grn_sca + grn_dca * (256 - ifactor2) // 256 + blu_blend = blu_sca + blu_dca * (256 - ifactor2) // 256 + + r = ((red_blend // ifactor_blend) << 8) & r_mask + g = ((grn_blend // ifactor_blend) << 3) & g_mask + b = ((blu_blend // ifactor_blend) >> 3) & b_mask + + pixel = (r & r_mask) | (g & g_mask) | (b & b_mask) + + if swap: + pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF) + + elif blend_source1: + r = ((sp1 & r_mask) * ifactor1 // 256) & r_mask + g = ((sp1 & g_mask) * ifactor1 // 256) & g_mask + b = ((sp1 & b_mask) * ifactor1 // 256) & b_mask + pixel = r | g | b + elif blend_source2: + r = ((sp2 & r_mask) * ifactor2 // 256) & r_mask + g = ((sp2 & g_mask) * ifactor2 // 256) & g_mask + b = ((sp2 & b_mask) * ifactor2 // 256) & b_mask + pixel = r | g | b + else: + pixel = dest[x, y] + + print(f"pixel hex: {hex(pixel)}") + dest[x, y] = pixel + + +class DitherAlgorithm: + """ + Options for algorithm to use by dither() function. + """ + + # pylint: disable=too-few-public-methods + + Atkinson = "bitmaptools.DitherAlgorithm.Atkinson" + FloydStenberg = "bitmaptools.DitherAlgorithm.FloydStenberg" + + atkinson = { + "count": 4, + "mx": 2, + "dl": 256 // 8, + "terms": [ + {"dx": 2, "dy": 0, "dl": 256 // 8}, + {"dx": -1, "dy": 1, "dl": 256 // 8}, + {"dx": 0, "dy": 1, "dl": 256 // 8}, + {"dx": 0, "dy": 2, "dl": 256 // 8}, + ], + } + + floyd_stenberg = { + "count": 3, + "mx": 1, + "dl": 7 * 256 // 16, + "terms": [ + {"dx": -1, "dy": 1, "dl": 3 * 256 // 16}, + {"dx": 0, "dy": 1, "dl": 5 * 256 // 16}, + {"dx": 1, "dy": 1, "dl": 1 * 256 // 16}, + ], + } + + algorithm_map = {Atkinson: atkinson, FloydStenberg: floyd_stenberg} + + +def dither(dest_bitmap, source_bitmap, colorspace, algorithm=DitherAlgorithm.Atkinson): + """Convert the input image into a 2-level output image using the given dither algorithm. + + :param bitmap dest_bitmap: Destination bitmap. It must have + a value_count of 2 or 65536. The stored values are 0 and the maximum pixel value. + :param bitmap source_bitmap: Source bitmap that contains the + graphical region to be dithered. It must have a value_count of 65536. + :param colorspace: The colorspace of the image. The supported colorspaces + are ``RGB565``, ``BGR565``, ``RGB565_SWAPPED``, and ``BGR565_SWAPPED`` + :param algorithm: The dither algorithm to use, one of the `DitherAlgorithm` values. + """ + SWAP_BYTES = 1 << 0 + SWAP_RB = 1 << 1 + height, width = dest_bitmap.width, dest_bitmap.height + swap_bytes = colorspace in (Colorspace.RGB565_SWAPPED, Colorspace.BGR565_SWAPPED) + swap_rb = colorspace in (Colorspace.BGR565, Colorspace.BGR565_SWAPPED) + algorithm_info = DitherAlgorithm.algorithm_map[algorithm] + mx = algorithm_info["mx"] + count = algorithm_info["count"] + terms = algorithm_info["terms"] + dl = algorithm_info["dl"] + + swap = 0 + if swap_bytes: + swap |= SWAP_BYTES + + if swap_rb: + swap |= SWAP_RB + + print(f"swap: {swap}") + + # Create row data arrays (3 rows with padding on both sides) + rowdata = [[0] * (width + 2 * mx) for _ in range(3)] + rows = [rowdata[0][mx:], rowdata[1][mx:], rowdata[2][mx:]] + + # Output array for one row at a time (padded to multiple of 32) + out = [False] * (((width + 31) // 32) * 32) + + # Helper function to fill a row with luminance data + def fill_row(bitmap, swap, luminance_data, y, mx): + if y >= bitmap.height: + return + + # Zero out padding area + for i in range(mx): + luminance_data[-mx + i] = 0 + luminance_data[bitmap.width + i] = 0 + + if bitmap._bits_per_value == 8: # pylint: disable=protected-access + for x in range(bitmap.width): + luminance_data[x] = bitmap[x, y] + else: + for x in range(bitmap.width): + pixel = bitmap[x, y] + if swap & SWAP_BYTES: + # Swap bytes (equivalent to __builtin_bswap16) + pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF) + + r = (pixel >> 8) & 0xF8 + g = (pixel >> 3) & 0xFC + b = (pixel << 3) & 0xF8 + + if swap & SWAP_BYTES: + r, b = b, r + + # Calculate luminance using same formula as C version + luminance_data[x] = (r * 78 + g * 154 + b * 29) // 256 + + # Helper function to write pixels to destination bitmap + def write_pixels(bitmap, y, data): + if bitmap._bits_per_value == 1: # pylint: disable=protected-access + for i in range(0, bitmap.width, 32): + # Pack 32 bits into an integer + p = 0 + for j in range(min(32, bitmap.width - i)): + p = p << 1 + if data[i + j]: + p |= 1 + + # Write packed value + for j in range(min(32, bitmap.width - i)): + bitmap[i + j, y] = (p >> (31 - j)) & 1 + else: + for i in range(bitmap.width): + bitmap[i, y] = 65535 if data[i] else 0 + + # Fill initial rows + fill_row(source_bitmap, swap, rows[0], 0, mx) + fill_row(source_bitmap, swap, rows[1], 1, mx) + fill_row(source_bitmap, swap, rows[2], 2, mx) + + err = 0 + + for y in range(height): + # Going left to right + for x in range(width): + pixel_in = rows[0][x] + err + pixel_out = pixel_in >= 128 + out[x] = pixel_out + + err = pixel_in - (255 if pixel_out else 0) + + # Distribute error to neighboring pixels + for i in range(count): + x1 = x + terms[i]["dx"] + dy = terms[i]["dy"] + + rows[dy][x1] = ((terms[i]["dl"] * err) // 256) + rows[dy][x1] + + err = (err * dl) // 256 + + write_pixels(dest_bitmap, y, out) + + # Cycle the rows + rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0] + + y += 1 + if y == height: + break + + # Fill the next row for future processing + fill_row(source_bitmap, swap, rows[2], y + 2, mx) + + # Going right to left + for x in range(width - 1, -1, -1): + pixel_in = rows[0][x] + err + pixel_out = pixel_in >= 128 + out[x] = pixel_out + + err = pixel_in - (255 if pixel_out else 0) + + # Distribute error to neighboring pixels (in reverse direction) + for i in range(count): + x1 = x - terms[i]["dx"] + dy = terms[i]["dy"] + + rows[dy][x1] = ((terms[i]["dl"] * err) // 256) + rows[dy][x1] + + err = (err * dl) // 256 + + write_pixels(dest_bitmap, y, out) + + # Cycle the rows again + rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0] + + # Fill the next row for future processing + fill_row(source_bitmap, swap, rows[2], y + 3, mx) + + +def boundary_fill( + dest_bitmap: Bitmap, + x: int, + y: int, + fill_color_value: int, + replaced_color_value: Optional[int] = None, +): + """Draws the color value into the destination bitmap enclosed + area of pixels of the background_value color. Like "Paint Bucket" + fill tool. + + :param bitmap dest_bitmap: Destination bitmap that will be written into + :param int x: x-pixel position of the first pixel to check and fill if needed + :param int y: y-pixel position of the first pixel to check and fill if needed + :param int fill_color_value: Bitmap palette index that will be written into the + enclosed area in the destination bitmap + :param int replaced_color_value: Bitmap palette index that will filled with the + value color in the enclosed area in the destination bitmap""" + if fill_color_value == replaced_color_value: + return + if replaced_color_value == -1: + replaced_color_value = dest_bitmap[x, y] + + fill_points = [] + fill_points.append((x, y)) + + seen_points = [] + minx = x + miny = y + maxx = x + maxy = y + + while len(fill_points) > 0: + cur_point = fill_points.pop(0) + seen_points.append(cur_point) + cur_x = cur_point[0] + cur_y = cur_point[1] + + cur_point_color = dest_bitmap[cur_x, cur_y] + if replaced_color_value is not None and cur_point_color != replaced_color_value: + continue + if cur_x < minx: + minx = cur_x + if cur_y < miny: + miny = cur_y + if cur_x > maxx: + maxx = cur_x + if cur_y > maxy: + maxy = cur_y + + dest_bitmap[cur_x, cur_y] = fill_color_value + + above_point = (cur_x, cur_y - 1) + below_point = (cur_x, cur_y + 1) + left_point = (cur_x - 1, cur_y) + right_point = (cur_x + 1, cur_y) + + if ( + above_point[1] >= 0 + and above_point not in seen_points + and above_point not in fill_points + ): + fill_points.append(above_point) + if ( + below_point[1] < dest_bitmap.height + and below_point not in seen_points + and below_point not in fill_points + ): + fill_points.append(below_point) + if ( + left_point[0] >= 0 + and left_point not in seen_points + and left_point not in fill_points + ): + fill_points.append(left_point) + if ( + right_point[0] < dest_bitmap.width + and right_point not in seen_points + and right_point not in fill_points + ): + fill_points.append(right_point) diff --git a/displayio/__init__.py b/displayio/__init__.py index b22ce94..e42d24a 100644 --- a/displayio/__init__.py +++ b/displayio/__init__.py @@ -20,11 +20,6 @@ import threading import time from typing import Union -import fourwire -import i2cdisplaybus -from busdisplay import BusDisplay -from busdisplay._displaybus import _DisplayBus -from epaperdisplay import EPaperDisplay from ._bitmap import Bitmap from ._colorspace import Colorspace from ._colorconverter import ColorConverter @@ -34,11 +29,6 @@ from ._palette import Palette from ._tilegrid import TileGrid from ._constants import CIRCUITPY_DISPLAY_LIMIT -# 8.x Backwards compatibility, remove at 10.x or -# when compatibility is removed from core displayio -Display = BusDisplay -FourWire = fourwire.FourWire -I2CDisplay = i2cdisplaybus.I2CDisplayBus __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" @@ -74,14 +64,16 @@ def release_displays() -> None: display_buses.clear() -def allocate_display(new_display: Union[BusDisplay, EPaperDisplay]) -> None: +def allocate_display( + new_display: Union["busdisplay.BusDisplay", "epaperdisplay.EPaperDisplay"] +) -> None: """Add a display to the displays pool and return the new display""" if len(displays) >= CIRCUITPY_DISPLAY_LIMIT: raise RuntimeError("Too many displays") displays.append(new_display) -def allocate_display_bus(new_display_bus: _DisplayBus) -> None: +def allocate_display_bus(new_display_bus: "busdisplay._displaybus._DisplayBus") -> None: """Add a display bus to the display_buses pool and return the new display bus""" if len(display_buses) >= CIRCUITPY_DISPLAY_LIMIT: raise RuntimeError( diff --git a/displayio/_area.py b/displayio/_area.py index f7272dd..0b3dd80 100644 --- a/displayio/_area.py +++ b/displayio/_area.py @@ -87,10 +87,10 @@ class Area: def union(self, other, union): """Combine this area along with another into union""" if self.empty(): - self.copy_into(union) + other.copy_into(union) return if other.empty(): - other.copy_into(union) + self.copy_into(union) return union.x1 = min(self.x1, other.x1) diff --git a/displayio/_bitmap.py b/displayio/_bitmap.py index 7f92006..13dc0d8 100644 --- a/displayio/_bitmap.py +++ b/displayio/_bitmap.py @@ -214,60 +214,6 @@ class Bitmap: for i in range(self._stride * self._bmp_height): self._data[i] = word - def blit( - self, - x: int, - y: int, - source_bitmap: Bitmap, - *, - x1: int, - y1: int, - x2: int, - y2: int, - skip_index: int, - ) -> None: - """Inserts the source_bitmap region defined by rectangular boundaries""" - # pylint: disable=invalid-name - if x2 is None: - x2 = source_bitmap.width - if y2 is None: - y2 = source_bitmap.height - - # Rearrange so that x1 < x2 and y1 < y2 - if x1 > x2: - x1, x2 = x2, x1 - if y1 > y2: - y1, y2 = y2, y1 - - # Ensure that x2 and y2 are within source bitmap size - x2 = min(x2, source_bitmap.width) - y2 = min(y2, source_bitmap.height) - - for y_count in range(y2 - y1): - for x_count in range(x2 - x1): - x_placement = x + x_count - y_placement = y + y_count - - if (self.width > x_placement >= 0) and ( - self.height > y_placement >= 0 - ): # ensure placement is within target bitmap - # get the palette index from the source bitmap - this_pixel_color = source_bitmap[ - y1 - + ( - y_count * source_bitmap.width - ) # Direct index into a bitmap array is speedier than [x,y] tuple - + x1 - + x_count - ] - - if (skip_index is None) or (this_pixel_color != skip_index): - self[ # Direct index into a bitmap array is speedier than [x,y] tuple - y_placement * self.width + x_placement - ] = this_pixel_color - elif y_placement > self.height: - break - def dirty(self, x1: int = 0, y1: int = 0, x2: int = -1, y2: int = -1) -> None: """Inform displayio of bitmap updates done via the buffer protocol.""" # pylint: disable=invalid-name diff --git a/pyproject.toml b/pyproject.toml index 3f697d2..8452572 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,8 @@ dynamic = ["dependencies"] [tool.setuptools] py-modules = ["fontio", "terminalio"] -packages = ["displayio", "vectorio", "paralleldisplaybus", "i2cdisplaybus", "fourwire", "busdisplay", "epaperdisplay"] +packages = ["displayio", "vectorio", "paralleldisplaybus", "i2cdisplaybus", "fourwire", "busdisplay", "epaperdisplay", + "bitmaptools"] [tool.setuptools.dynamic] dependencies = {file = ["requirements.txt"]} diff --git a/vectorio/_circle.py b/vectorio/_circle.py index cc87351..339b154 100644 --- a/vectorio/_circle.py +++ b/vectorio/_circle.py @@ -61,7 +61,7 @@ class Circle(_VectorShape): def radius(self, value: int) -> None: if value < 1: raise ValueError("radius must be >= 1") - self._radius = abs(value) + self._radius = int(value) # other code assumes radius is an integer self._shape_set_dirty() @property diff --git a/vectorio/_polygon.py b/vectorio/_polygon.py index 4e6e444..7f2af58 100644 --- a/vectorio/_polygon.py +++ b/vectorio/_polygon.py @@ -17,7 +17,7 @@ vectorio Polygon for Blinka """ -from typing import Union, Tuple +from typing import Union, Tuple, List from displayio._colorconverter import ColorConverter from displayio._palette import Palette from displayio._area import Area @@ -34,7 +34,7 @@ class Polygon(_VectorShape): self, *, pixel_shader: Union[ColorConverter, Palette], - points: list | Tuple[int, int], + points: List[Tuple[int, int]], x: int, y: int, ): @@ -54,12 +54,12 @@ class Polygon(_VectorShape): self.points = points @property - def points(self) -> list | Tuple[int, int]: + def points(self) -> List[Tuple[int, int]]: """The points of the polygon in pixels""" return self._points @points.setter - def points(self, value: list | Tuple[int, int]) -> None: + def points(self, value: List[Tuple[int, int]]) -> None: if len(value) < 3: raise ValueError("Polygon needs at least 3 points") self._points = value diff --git a/vectorio/_rectangle.py b/vectorio/_rectangle.py index e4c5952..15880c6 100644 --- a/vectorio/_rectangle.py +++ b/vectorio/_rectangle.py @@ -38,6 +38,7 @@ class Rectangle(_VectorShape): height: int, x: int, y: int, + color_index: int = 0, ): """Represents a rectangle by defining its bounds @@ -51,7 +52,7 @@ class Rectangle(_VectorShape): """ self._width = 1 self._height = 1 - self._color_index = 1 + self._color_index = color_index + 1 super().__init__(pixel_shader, x, y) self.width = width diff --git a/vectorio/_vectorshape.py b/vectorio/_vectorshape.py index d3d1ddb..735e5ae 100644 --- a/vectorio/_vectorshape.py +++ b/vectorio/_vectorshape.py @@ -122,9 +122,12 @@ class _VectorShape: self._get_screen_area(current_area) moved = current_area != self._current_area if moved: + # This will add _current_area (the old position) to dirty area self._current_area.union( self._ephemeral_dirty_area, self._ephemeral_dirty_area ) + # This will add the new position to the dirty area + current_area.union(self._ephemeral_dirty_area, self._ephemeral_dirty_area) # Dirty area tracks the shape's footprint between draws. It's reset on refresh finish. current_area.copy_into(self._current_area) self._current_area_dirty = True