X-Git-Url: https://git.ayoreis.com/hackapet/Adafruit_Blinka_Displayio.git/blobdiff_plain/86839449b3dff6550f8bcdb33f669b73990e6abb..HEAD:/bitmaptools/__init__.py diff --git a/bitmaptools/__init__.py b/bitmaptools/__init__.py index e9ca3bb..69be2d6 100644 --- a/bitmaptools/__init__.py +++ b/bitmaptools/__init__.py @@ -1,18 +1,45 @@ +# 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 numpy as np -from displayio import Bitmap, Colorspace 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) @@ -33,6 +60,15 @@ def draw_line(dest_bitmap: Bitmap, x1: int, y1: int, x2: int, y2: int, value: in 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)) @@ -59,10 +95,22 @@ def draw_circle(dest_bitmap: Bitmap, x: int, y: int, radius: int, value: int): xb += 1 -def draw_polygon(dest_bitmap: Bitmap, - xs: circuitpython_typing.ReadableBuffer, - ys: circuitpython_typing.ReadableBuffer, - value: int, close: bool = True): +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.") @@ -70,26 +118,67 @@ def draw_polygon(dest_bitmap: Bitmap, 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) + 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""" + 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: Optional[int] = None, + y2: Optional[int] = None, + skip_source_index: Optional[int] = None, + skip_dest_index: Optional[int] = 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 @@ -112,59 +201,93 @@ def blit(dest_bitmap: Bitmap, source_bitmap: Bitmap, y_placement = y + y_count if (dest_bitmap.width > x_placement >= 0) and ( - dest_bitmap.height > y_placement >= 0 + 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_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[ # Direct index into a bitmap array is speedier than [x,y] tuple + dest_bitmap[y_placement * dest_bitmap.width + x_placement] + != skip_dest_index + ): + dest_bitmap[ y_placement * dest_bitmap.width + x_placement - ] = this_pixel_color + ] = 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, + 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 in None: + if oy is None: oy = dest_bitmap.height // 2 if dest_clip0 is None: - dest_clip0 = (0,0) + dest_clip0 = (0, 0) if dest_clip1 is None: - dest_clip1 = (dest_bitmap.width,dest_bitmap.height) + dest_clip1 = (dest_bitmap.width, dest_bitmap.height) if px is None: px = source_bitmap.width // 2 - if py in None: + if py is None: py = source_bitmap.height // 2 if source_clip0 is None: - source_clip0 = (0,0) + source_clip0 = (0, 0) if source_clip1 is None: - source_clip1 = (source_bitmap.width,source_bitmap.height) + source_clip1 = (source_bitmap.width, source_bitmap.height) if angle is None: angle = 0.0 @@ -235,7 +358,9 @@ def rotozoom( 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): + 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 @@ -246,17 +371,49 @@ def rotozoom( 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): + 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 + _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 @@ -265,19 +422,51 @@ def arrayblit( 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): +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 + 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) + 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): @@ -291,15 +480,15 @@ def readinto(bitmap: Bitmap, if swap_bytes: if element_size == 2: rowdata = bytearray( - b''.join( - struct.pack('H', rowdata[i:i + 2])[0]) + 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]) + b"".join( + struct.pack("I", rowdata[i : i + 4])[0]) for i in range(0, len(rowdata), 4) ) ) @@ -325,32 +514,65 @@ def readinto(bitmap: Bitmap, elif bits_per_pixel == 8: value = rowdata[x] elif bits_per_pixel == 16: - value = struct.unpack_from(' 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)