3 from typing import Optional, Tuple, BinaryIO
5 from displayio import Bitmap, Colorspace
6 import circuitpython_typing
9 def fill_region(dest_bitmap: Bitmap, x1: int, y1: int, x2: int, y2: int, value: int):
10 for y in range(y1, y2):
11 for x in range(x1, x2):
12 dest_bitmap[x, y] = value
15 def draw_line(dest_bitmap: Bitmap, x1: int, y1: int, x2: int, y2: int, value: int):
17 sx = 1 if x1 < x2 else -1
19 sy = 1 if y1 < y2 else -1
23 dest_bitmap[x1, y1] = value
24 if x1 == x2 and y1 == y2:
35 def draw_circle(dest_bitmap: Bitmap, x: int, y: int, radius: int, value: int):
36 x = max(0, min(x, dest_bitmap.width - 1))
37 y = max(0, min(y, dest_bitmap.height - 1))
43 # Bresenham's circle algorithm
45 dest_bitmap[xb + x, yb + y] = value
46 dest_bitmap[-xb + x, -yb + y] = value
47 dest_bitmap[-xb + x, yb + y] = value
48 dest_bitmap[xb + x, -yb + y] = value
49 dest_bitmap[yb + x, xb + y] = value
50 dest_bitmap[-yb + x, xb + y] = value
51 dest_bitmap[-yb + x, -xb + y] = value
52 dest_bitmap[yb + x, -xb + y] = value
57 d = d + 4 * (xb - yb) + 10
64 xs: circuitpython_typing.ReadableBuffer,
65 ys: circuitpython_typing.ReadableBuffer,
69 if len(xs) != len(ys):
70 raise ValueError("Length of xs and ys must be equal.")
72 for i in range(len(xs) - 1):
73 cur_point = (xs[i], ys[i])
74 next_point = (xs[i + 1], ys[i + 1])
75 print(f"cur: {cur_point}, next: {next_point}")
77 dest_bitmap=dest_bitmap,
86 print(f"close: {(xs[0], ys[0])} - {(xs[-1], ys[-1])}")
88 dest_bitmap=dest_bitmap,
99 source_bitmap: Bitmap,
105 x2: int | None = None,
106 y2: int | None = None,
107 skip_source_index: int | None = None,
108 skip_dest_index: int | None = None,
110 """Inserts the source_bitmap region defined by rectangular boundaries"""
111 # pylint: disable=invalid-name
113 x2 = source_bitmap.width
115 y2 = source_bitmap.height
117 # Rearrange so that x1 < x2 and y1 < y2
123 # Ensure that x2 and y2 are within source bitmap size
124 x2 = min(x2, source_bitmap.width)
125 y2 = min(y2, source_bitmap.height)
127 for y_count in range(y2 - y1):
128 for x_count in range(x2 - x1):
129 x_placement = x + x_count
130 y_placement = y + y_count
132 if (dest_bitmap.width > x_placement >= 0) and (
133 dest_bitmap.height > y_placement >= 0
134 ): # ensure placement is within target bitmap
135 # get the palette index from the source bitmap
136 this_pixel_color = source_bitmap[
137 y1 + (y_count * source_bitmap.width) + x1 + x_count
140 if (skip_source_index is None) or (
141 this_pixel_color != skip_source_index
143 if (skip_dest_index is None) or (
144 dest_bitmap[y_placement * dest_bitmap.width + x_placement]
147 dest_bitmap[ # Direct index into a bitmap array is speedier than [x,y] tuple
148 y_placement * dest_bitmap.width + x_placement
150 elif y_placement > dest_bitmap.height:
156 source_bitmap: Bitmap,
158 ox: Optional[int] = None,
159 oy: Optional[int] = None,
160 dest_clip0: Optional[Tuple[int, int]] = None,
161 dest_clip1: Optional[Tuple[int, int]] = None,
162 px: Optional[int] = None,
163 py: Optional[int] = None,
164 source_clip0: Optional[Tuple[int, int]] = None,
165 source_clip1: Optional[Tuple[int, int]] = None,
166 angle: Optional[float] = None,
167 scale: Optional[float] = None,
168 skip_index: Optional[int] = None,
171 ox = dest_bitmap.width // 2
173 oy = dest_bitmap.height // 2
175 if dest_clip0 is None:
177 if dest_clip1 is None:
178 dest_clip1 = (dest_bitmap.width, dest_bitmap.height)
181 px = source_bitmap.width // 2
183 py = source_bitmap.height // 2
185 if source_clip0 is None:
186 source_clip0 = (0, 0)
187 if source_clip1 is None:
188 source_clip1 = (source_bitmap.width, source_bitmap.height)
195 dest_clip0_x, dest_clip0_y = dest_clip0
196 dest_clip1_x, dest_clip1_y = dest_clip1
197 source_clip0_x, source_clip0_y = source_clip0
198 source_clip1_x, source_clip1_y = source_clip1
205 sin_angle = math.sin(angle)
206 cos_angle = math.cos(angle)
208 def update_bounds(dx, dy):
209 nonlocal minx, maxx, miny, maxy
219 w = source_bitmap.width
220 h = source_bitmap.height
222 dx = -cos_angle * px * scale + sin_angle * py * scale + ox
223 dy = -sin_angle * px * scale - cos_angle * py * scale + oy
224 update_bounds(dx, dy)
226 dx = cos_angle * (w - px) * scale + sin_angle * py * scale + ox
227 dy = sin_angle * (w - px) * scale - cos_angle * py * scale + oy
228 update_bounds(dx, dy)
230 dx = cos_angle * (w - px) * scale - sin_angle * (h - py) * scale + ox
231 dy = sin_angle * (w - px) * scale + cos_angle * (h - py) * scale + oy
232 update_bounds(dx, dy)
234 dx = -cos_angle * px * scale - sin_angle * (h - py) * scale + ox
235 dy = -sin_angle * px * scale + cos_angle * (h - py) * scale + oy
236 update_bounds(dx, dy)
238 # Clip to destination area
239 minx = max(minx, dest_clip0_x)
240 maxx = min(maxx, dest_clip1_x - 1)
241 miny = max(miny, dest_clip0_y)
242 maxy = min(maxy, dest_clip1_y - 1)
244 dv_col = cos_angle / scale
245 du_col = sin_angle / scale
249 startu = px - (ox * dv_col + oy * du_col)
250 startv = py - (ox * dv_row + oy * du_row)
252 rowu = startu + miny * du_col
253 rowv = startv + miny * dv_col
255 for y in range(miny, maxy + 1):
256 u = rowu + minx * du_row
257 v = rowv + minx * dv_row
258 for x in range(minx, maxx + 1):
259 if (source_clip0_x <= u < source_clip1_x) and (
260 source_clip0_y <= v < source_clip1_y
262 c = source_bitmap[int(u), int(v)]
263 if skip_index is None or c != skip_index:
264 dest_bitmap[x, y] = c
273 data: circuitpython_typing.ReadableBuffer,
276 x2: Optional[int] = None,
277 y2: Optional[int] = None,
278 skip_index: Optional[int] = None,
285 _value_count = 2**bitmap._bits_per_value
286 for y in range(y1, y2):
287 for x in range(x1, x2):
288 i = y * (x2 - x1) + x
289 value = int(data[i] % _value_count)
290 if skip_index is None or value != skip_index:
298 element_size: int = 1,
299 reverse_pixels_in_element: bool = False,
300 swap_bytes: bool = False,
301 reverse_rows: bool = False,
304 height = bitmap.height
305 bits_per_value = bitmap._bits_per_value
306 mask = (1 << bits_per_value) - 1
308 elements_per_row = (width * bits_per_pixel + element_size * 8 - 1) // (
311 rowsize = element_size * elements_per_row
313 for y in range(height):
314 row_bytes = file.read(rowsize)
315 if len(row_bytes) != rowsize:
318 # Convert the raw bytes into the appropriate type array for processing
319 rowdata = bytearray(row_bytes)
322 if element_size == 2:
325 struct.pack("<H", struct.unpack(">H", rowdata[i : i + 2])[0])
326 for i in range(0, len(rowdata), 2)
329 elif element_size == 4:
332 struct.pack("<I", struct.unpack(">I", rowdata[i : i + 4])[0])
333 for i in range(0, len(rowdata), 4)
337 y_draw = height - 1 - y if reverse_rows else y
339 for x in range(width):
341 if bits_per_pixel == 1:
343 bit_offset = 7 - (x % 8) if reverse_pixels_in_element else x % 8
344 value = (rowdata[byte_offset] >> bit_offset) & 0x1
345 elif bits_per_pixel == 2:
347 bit_index = 3 - (x % 4) if reverse_pixels_in_element else x % 4
348 bit_offset = 2 * bit_index
349 value = (rowdata[byte_offset] >> bit_offset) & 0x3
350 elif bits_per_pixel == 4:
352 bit_index = 1 - (x % 2) if reverse_pixels_in_element else x % 2
353 bit_offset = 4 * bit_index
354 value = (rowdata[byte_offset] >> bit_offset) & 0xF
355 elif bits_per_pixel == 8:
357 elif bits_per_pixel == 16:
358 value = struct.unpack_from("<H", rowdata, x * 2)[0]
359 elif bits_per_pixel == 24:
362 (rowdata[offset] << 16)
363 | (rowdata[offset + 1] << 8)
364 | rowdata[offset + 2]
366 elif bits_per_pixel == 32:
367 value = struct.unpack_from("<I", rowdata, x * 4)[0]
369 bitmap[x, y_draw] = value & mask
373 Normal = "bitmaptools.BlendMode.Normal"
374 Screen = "bitmaptools.BlendMode.Screen"
381 colorspace: Colorspace,
382 factor1: float = 0.5,
383 factor2: Optional[float] = None,
384 blendmode: BlendMode = BlendMode.Normal,
385 skip_source1_index: Optional[int] = None,
386 skip_source2_index: Optional[int] = None,
389 colorspace should be one of: 'L8', 'RGB565', 'RGB565_SWAPPED', 'BGR565_SWAPPED'.
391 blendmode can be 'normal' (or any default) or 'screen'.
393 This assumes that all bitmaps (dest, source1, source2) support 2D access like bitmap[x, y].
395 dest.width and dest.height are used; make sure the bitmap objects have these attributes or replace them with your own logic.
398 def clamp(val, minval, maxval):
399 return max(minval, min(maxval, val))
401 ifactor1 = int(factor1 * 256)
402 ifactor2 = int(factor2 * 256)
404 width, height = dest.width, dest.height
406 if colorspace == "L8":
407 for y in range(height):
408 for x in range(width):
411 blend_source1 = skip_source1_index is None or sp1 != skip_source1_index
412 blend_source2 = skip_source2_index is None or sp2 != skip_source2_index
414 if blend_source1 and blend_source2:
418 if blendmode == BlendMode.Screen:
419 blend = sca + sda - (sca * sda // 65536)
420 elif blendmode == BlendMode.Normal:
421 blend = sca + sda * (256 - ifactor2) // 256
423 denom = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256
424 pixel = blend // denom
426 pixel = sp1 * ifactor1 // 256
428 pixel = sp2 * ifactor2 // 256
432 dest[x, y] = clamp(pixel, 0, 255)
435 swap = colorspace in ("RGB565_SWAPPED", "BGR565_SWAPPED")
440 for y in range(height):
441 for x in range(width):
446 sp1 = ((sp1 & 0xFF) << 8) | ((sp1 >> 8) & 0xFF)
447 sp2 = ((sp2 & 0xFF) << 8) | ((sp2 >> 8) & 0xFF)
449 blend_source1 = skip_source1_index is None or sp1 != skip_source1_index
450 blend_source2 = skip_source2_index is None or sp2 != skip_source2_index
452 if blend_source1 and blend_source2:
453 ifactor_blend = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256
455 red_dca = ((sp1 & r_mask) >> 8) * ifactor1
456 grn_dca = ((sp1 & g_mask) >> 3) * ifactor1
457 blu_dca = ((sp1 & b_mask) << 3) * ifactor1
459 red_sca = ((sp2 & r_mask) >> 8) * ifactor2
460 grn_sca = ((sp2 & g_mask) >> 3) * ifactor2
461 blu_sca = ((sp2 & b_mask) << 3) * ifactor2
463 if blendmode == BlendMode.Screen:
464 red_blend = red_sca + red_dca - (red_sca * red_dca // 65536)
465 grn_blend = grn_sca + grn_dca - (grn_sca * grn_dca // 65536)
466 blu_blend = blu_sca + blu_dca - (blu_sca * blu_dca // 65536)
467 elif blendmode == BlendMode.Normal:
468 red_blend = red_sca + red_dca * (256 - ifactor2) // 256
469 grn_blend = grn_sca + grn_dca * (256 - ifactor2) // 256
470 blu_blend = blu_sca + blu_dca * (256 - ifactor2) // 256
472 r = ((red_blend // ifactor_blend) << 8) & r_mask
473 g = ((grn_blend // ifactor_blend) << 3) & g_mask
474 b = ((blu_blend // ifactor_blend) >> 3) & b_mask
476 pixel = (r & r_mask) | (g & g_mask) | (b & b_mask)
479 pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF)
482 r = ((sp1 & r_mask) * ifactor1 // 256) & r_mask
483 g = ((sp1 & g_mask) * ifactor1 // 256) & g_mask
484 b = ((sp1 & b_mask) * ifactor1 // 256) & b_mask
487 r = ((sp2 & r_mask) * ifactor2 // 256) & r_mask
488 g = ((sp2 & g_mask) * ifactor2 // 256) & g_mask
489 b = ((sp2 & b_mask) * ifactor2 // 256) & b_mask
494 print(f"pixel hex: {hex(pixel)}")
498 class DitherAlgorithm:
499 Atkinson = "bitmaptools.DitherAlgorithm.Atkinson"
500 FloydStenberg = "bitmaptools.DitherAlgorithm.FloydStenberg"
507 {"dx": 2, "dy": 0, "dl": 256 // 8},
508 {"dx": -1, "dy": 1, "dl": 256 // 8},
509 {"dx": 0, "dy": 1, "dl": 256 // 8},
510 {"dx": 0, "dy": 2, "dl": 256 // 8},
519 {"dx": -1, "dy": 1, "dl": 3 * 256 // 16},
520 {"dx": 0, "dy": 1, "dl": 5 * 256 // 16},
521 {"dx": 1, "dy": 1, "dl": 1 * 256 // 16},
525 algorithm_map = {Atkinson: atkinson, FloydStenberg: floyd_stenberg}
528 def dither(dest_bitmap, source_bitmap, colorspace, algorithm=DitherAlgorithm.Atkinson):
531 height, width = dest_bitmap.width, dest_bitmap.height
532 swap_bytes = colorspace in (Colorspace.RGB565_SWAPPED, Colorspace.BGR565_SWAPPED)
533 swap_rb = colorspace in (Colorspace.BGR565, Colorspace.BGR565_SWAPPED)
534 algorithm_info = DitherAlgorithm.algorithm_map[algorithm]
535 mx = algorithm_info["mx"]
536 count = algorithm_info["count"]
537 terms = algorithm_info["terms"]
538 dl = algorithm_info["dl"]
547 print(f"swap: {swap}")
549 # Create row data arrays (3 rows with padding on both sides)
550 rowdata = [[0] * (width + 2 * mx) for _ in range(3)]
551 rows = [rowdata[0][mx:], rowdata[1][mx:], rowdata[2][mx:]]
553 # Output array for one row at a time (padded to multiple of 32)
554 out = [False] * (((width + 31) // 32) * 32)
556 # Helper function to fill a row with luminance data
557 def fill_row(bitmap, swap, luminance_data, y, mx):
558 if y >= bitmap.height:
561 # Zero out padding area
563 luminance_data[-mx + i] = 0
564 luminance_data[bitmap.width + i] = 0
566 if bitmap._bits_per_value == 8:
567 for x in range(bitmap.width):
568 luminance_data[x] = bitmap[x, y]
570 for x in range(bitmap.width):
572 if swap & SWAP_BYTES:
573 # Swap bytes (equivalent to __builtin_bswap16)
574 pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF)
576 r = (pixel >> 8) & 0xF8
577 g = (pixel >> 3) & 0xFC
578 b = (pixel << 3) & 0xF8
580 if swap & SWAP_BYTES:
583 # Calculate luminance using same formula as C version
584 luminance_data[x] = (r * 78 + g * 154 + b * 29) // 256
586 # Helper function to write pixels to destination bitmap
587 def write_pixels(bitmap, y, data):
588 if bitmap._bits_per_value == 1:
589 for i in range(0, bitmap.width, 32):
590 # Pack 32 bits into an integer
592 for j in range(min(32, bitmap.width - i)):
598 for j in range(min(32, bitmap.width - i)):
599 bitmap[i + j, y] = (p >> (31 - j)) & 1
601 for i in range(bitmap.width):
602 bitmap[i, y] = 65535 if data[i] else 0
605 fill_row(source_bitmap, swap, rows[0], 0, mx)
606 fill_row(source_bitmap, swap, rows[1], 1, mx)
607 fill_row(source_bitmap, swap, rows[2], 2, mx)
611 for y in range(height):
612 # Going left to right
613 for x in range(width):
614 pixel_in = rows[0][x] + err
615 pixel_out = pixel_in >= 128
618 err = pixel_in - (255 if pixel_out else 0)
620 # Distribute error to neighboring pixels
621 for i in range(count):
622 x1 = x + terms[i]["dx"]
625 rows[dy][x1] = ((terms[i]["dl"] * err) // 256) + rows[dy][x1]
627 err = (err * dl) // 256
629 write_pixels(dest_bitmap, y, out)
632 rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0]
638 # Fill the next row for future processing
639 fill_row(source_bitmap, swap, rows[2], y + 2, mx)
641 # Going right to left
642 for x in range(width - 1, -1, -1):
643 pixel_in = rows[0][x] + err
644 pixel_out = pixel_in >= 128
647 err = pixel_in - (255 if pixel_out else 0)
649 # Distribute error to neighboring pixels (in reverse direction)
650 for i in range(count):
651 x1 = x - terms[i]["dx"]
654 rows[dy][x1] = ((terms[i]["dl"] * err) // 256) + rows[dy][x1]
656 err = (err * dl) // 256
658 write_pixels(dest_bitmap, y, out)
660 # Cycle the rows again
661 rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0]
663 # Fill the next row for future processing
664 fill_row(source_bitmap, swap, rows[2], y + 3, mx)
671 fill_color_value: int,
672 replaced_color_value: Optional[int] = None,
674 if fill_color_value == replaced_color_value:
676 if replaced_color_value == -1:
677 replaced_color_value = dest_bitmap[x, y]
680 fill_points.append((x, y))
688 while len(fill_points):
689 cur_point = fill_points.pop(0)
690 seen_points.append(cur_point)
694 cur_point_color = dest_bitmap[cur_x, cur_y]
695 if replaced_color_value is not None and cur_point_color != replaced_color_value:
706 dest_bitmap[cur_x, cur_y] = fill_color_value
708 above_point = (cur_x, cur_y - 1)
709 below_point = (cur_x, cur_y + 1)
710 left_point = (cur_x - 1, cur_y)
711 right_point = (cur_x + 1, cur_y)
715 and above_point not in seen_points
716 and above_point not in fill_points
718 fill_points.append(above_point)
720 below_point[1] < dest_bitmap.height
721 and below_point not in seen_points
722 and below_point not in fill_points
724 fill_points.append(below_point)
727 and left_point not in seen_points
728 and left_point not in fill_points
730 fill_points.append(left_point)
732 right_point[0] < dest_bitmap.width
733 and right_point not in seen_points
734 and right_point not in fill_points
736 fill_points.append(right_point)