X-Git-Url: https://git.ayoreis.com/hackapet/Adafruit_Blinka_Displayio.git/blobdiff_plain/30ae2c3b1efc86d6dbe32b2359c215e79d898e43..50c817f6739915e81a9000124cf75218d2c826a9:/bitmaptools/__init__.py diff --git a/bitmaptools/__init__.py b/bitmaptools/__init__.py index 78802b8..cc83549 100644 --- a/bitmaptools/__init__.py +++ b/bitmaptools/__init__.py @@ -1,8 +1,8 @@ import math import struct from typing import Optional, Tuple, BinaryIO - -from displayio import Bitmap +import numpy as np +from displayio import Bitmap, Colorspace import circuitpython_typing @@ -59,10 +59,13 @@ 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 | None = True): +def draw_polygon( + dest_bitmap: Bitmap, + xs: circuitpython_typing.ReadableBuffer, + ys: circuitpython_typing.ReadableBuffer, + value: int, + close: bool = True, +): if len(xs) != len(ys): raise ValueError("Length of xs and ys must be equal.") @@ -70,25 +73,40 @@ 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): + 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""" # pylint: disable=invalid-name if x2 is None: @@ -112,39 +130,68 @@ 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[y_placement * dest_bitmap.width + x_placement] + != skip_dest_index + ): dest_bitmap[ # Direct index into a bitmap array is speedier than [x,y] tuple y_placement * dest_bitmap.width + x_placement - ] = this_pixel_color + ] = this_pixel_color elif y_placement > dest_bitmap.height: break def rotozoom( - dest_bitmap, - source_bitmap, - *, - ox: int, - oy: int, - dest_clip0: Tuple[int, int], - dest_clip1: Tuple[int, int], - px: int, - py: int, - source_clip0: Tuple[int, int], - source_clip1: Tuple[int, int], - angle: float, - scale: float, - skip_index: int, + 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, ): + if ox is None: + ox = dest_bitmap.width // 2 + if oy in 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 in 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 @@ -209,7 +256,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 @@ -220,17 +269,20 @@ def rotozoom( def arrayblit( - bitmap: Bitmap, - data: circuitpython_typing.ReadableBuffer, - x1: int = 0, y1: int = 0, - x2: int | None = None, y2: int | None = None, - skip_index: int | None = 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, +): 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 for y in range(y1, y2): for x in range(x1, x2): i = y * (x2 - x1) + x @@ -239,20 +291,23 @@ 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, +): width = bitmap.width height = bitmap.height bits_per_value = bitmap._bits_per_value 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): @@ -266,15 +321,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) ) ) @@ -300,11 +355,382 @@ def readinto(bitmap: Bitmap, 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: + 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): + 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: + 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: + 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, +): + 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): + 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)