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
62 def draw_polygon(dest_bitmap: Bitmap,
63 xs: circuitpython_typing.ReadableBuffer,
64 ys: circuitpython_typing.ReadableBuffer,
65 value: int, close: bool = True):
66 if len(xs) != len(ys):
67 raise ValueError("Length of xs and ys must be equal.")
69 for i in range(len(xs) - 1):
70 cur_point = (xs[i], ys[i])
71 next_point = (xs[i + 1], ys[i + 1])
72 print(f"cur: {cur_point}, next: {next_point}")
73 draw_line(dest_bitmap=dest_bitmap,
74 x1=cur_point[0], y1=cur_point[1],
75 x2=next_point[0], y2=next_point[1],
79 print(f"close: {(xs[0], ys[0])} - {(xs[-1], ys[-1])}")
80 draw_line(dest_bitmap=dest_bitmap,
86 def blit(dest_bitmap: Bitmap, source_bitmap: Bitmap,
88 x1: int = 0, y1: int = 0,
89 x2: int | None = None, y2: int | None = None,
90 skip_source_index: int | None = None,
91 skip_dest_index: int | None = None):
92 """Inserts the source_bitmap region defined by rectangular boundaries"""
93 # pylint: disable=invalid-name
95 x2 = source_bitmap.width
97 y2 = source_bitmap.height
99 # Rearrange so that x1 < x2 and y1 < y2
105 # Ensure that x2 and y2 are within source bitmap size
106 x2 = min(x2, source_bitmap.width)
107 y2 = min(y2, source_bitmap.height)
109 for y_count in range(y2 - y1):
110 for x_count in range(x2 - x1):
111 x_placement = x + x_count
112 y_placement = y + y_count
114 if (dest_bitmap.width > x_placement >= 0) and (
115 dest_bitmap.height > y_placement >= 0
116 ): # ensure placement is within target bitmap
117 # get the palette index from the source bitmap
118 this_pixel_color = source_bitmap[
119 y1 + (y_count * source_bitmap.width) + x1 + x_count
122 if (skip_source_index is None) or (this_pixel_color != skip_source_index):
123 if (skip_dest_index is None) or (
124 dest_bitmap[y_placement * dest_bitmap.width + x_placement] != skip_dest_index):
125 dest_bitmap[ # Direct index into a bitmap array is speedier than [x,y] tuple
126 y_placement * dest_bitmap.width + x_placement
128 elif y_placement > dest_bitmap.height:
134 source_bitmap: Bitmap,
136 ox: Optional[int] = None,
137 oy: Optional[int] = None,
138 dest_clip0: Optional[Tuple[int, int]] = None,
139 dest_clip1: Optional[Tuple[int, int]] = None,
140 px: Optional[int] = None,
141 py: Optional[int] = None,
142 source_clip0: Optional[Tuple[int, int]] = None,
143 source_clip1: Optional[Tuple[int, int]] = None,
144 angle: Optional[float] = None,
145 scale: Optional[float] = None,
146 skip_index: Optional[int] = None,
150 ox = dest_bitmap.width // 2
152 oy = dest_bitmap.height // 2
154 if dest_clip0 is None:
156 if dest_clip1 is None:
157 dest_clip1 = (dest_bitmap.width,dest_bitmap.height)
160 px = source_bitmap.width // 2
162 py = source_bitmap.height // 2
164 if source_clip0 is None:
166 if source_clip1 is None:
167 source_clip1 = (source_bitmap.width,source_bitmap.height)
174 dest_clip0_x, dest_clip0_y = dest_clip0
175 dest_clip1_x, dest_clip1_y = dest_clip1
176 source_clip0_x, source_clip0_y = source_clip0
177 source_clip1_x, source_clip1_y = source_clip1
184 sin_angle = math.sin(angle)
185 cos_angle = math.cos(angle)
187 def update_bounds(dx, dy):
188 nonlocal minx, maxx, miny, maxy
198 w = source_bitmap.width
199 h = source_bitmap.height
201 dx = -cos_angle * px * scale + sin_angle * py * scale + ox
202 dy = -sin_angle * px * scale - cos_angle * py * scale + oy
203 update_bounds(dx, dy)
205 dx = cos_angle * (w - px) * scale + sin_angle * py * scale + ox
206 dy = sin_angle * (w - px) * scale - cos_angle * py * scale + oy
207 update_bounds(dx, dy)
209 dx = cos_angle * (w - px) * scale - sin_angle * (h - py) * scale + ox
210 dy = sin_angle * (w - px) * scale + cos_angle * (h - py) * scale + oy
211 update_bounds(dx, dy)
213 dx = -cos_angle * px * scale - sin_angle * (h - py) * scale + ox
214 dy = -sin_angle * px * scale + cos_angle * (h - py) * scale + oy
215 update_bounds(dx, dy)
217 # Clip to destination area
218 minx = max(minx, dest_clip0_x)
219 maxx = min(maxx, dest_clip1_x - 1)
220 miny = max(miny, dest_clip0_y)
221 maxy = min(maxy, dest_clip1_y - 1)
223 dv_col = cos_angle / scale
224 du_col = sin_angle / scale
228 startu = px - (ox * dv_col + oy * du_col)
229 startv = py - (ox * dv_row + oy * du_row)
231 rowu = startu + miny * du_col
232 rowv = startv + miny * dv_col
234 for y in range(miny, maxy + 1):
235 u = rowu + minx * du_row
236 v = rowv + minx * dv_row
237 for x in range(minx, maxx + 1):
238 if (source_clip0_x <= u < source_clip1_x) and (source_clip0_y <= v < source_clip1_y):
239 c = source_bitmap[int(u), int(v)]
240 if skip_index is None or c != skip_index:
241 dest_bitmap[x, y] = c
250 data: circuitpython_typing.ReadableBuffer,
251 x1: int = 0, y1: int = 0,
252 x2: Optional[int] = None, y2: Optional[int] = None,
253 skip_index: Optional[int] = None):
259 _value_count = 2 ** bitmap._bits_per_value
260 for y in range(y1, y2):
261 for x in range(x1, x2):
262 i = y * (x2 - x1) + x
263 value = int(data[i] % _value_count)
264 if skip_index is None or value != skip_index:
268 def readinto(bitmap: Bitmap,
271 element_size: int = 1,
272 reverse_pixels_in_element: bool = False,
273 swap_bytes: bool = False,
274 reverse_rows: bool = False):
276 height = bitmap.height
277 bits_per_value = bitmap._bits_per_value
278 mask = (1 << bits_per_value) - 1
280 elements_per_row = (width * bits_per_pixel + element_size * 8 - 1) // (element_size * 8)
281 rowsize = element_size * elements_per_row
283 for y in range(height):
284 row_bytes = file.read(rowsize)
285 if len(row_bytes) != rowsize:
288 # Convert the raw bytes into the appropriate type array for processing
289 rowdata = bytearray(row_bytes)
292 if element_size == 2:
295 struct.pack('<H', struct.unpack('>H', rowdata[i:i + 2])[0])
296 for i in range(0, len(rowdata), 2)
299 elif element_size == 4:
302 struct.pack('<I', struct.unpack('>I', rowdata[i:i + 4])[0])
303 for i in range(0, len(rowdata), 4)
307 y_draw = height - 1 - y if reverse_rows else y
309 for x in range(width):
311 if bits_per_pixel == 1:
313 bit_offset = 7 - (x % 8) if reverse_pixels_in_element else x % 8
314 value = (rowdata[byte_offset] >> bit_offset) & 0x1
315 elif bits_per_pixel == 2:
317 bit_index = 3 - (x % 4) if reverse_pixels_in_element else x % 4
318 bit_offset = 2 * bit_index
319 value = (rowdata[byte_offset] >> bit_offset) & 0x3
320 elif bits_per_pixel == 4:
322 bit_index = 1 - (x % 2) if reverse_pixels_in_element else x % 2
323 bit_offset = 4 * bit_index
324 value = (rowdata[byte_offset] >> bit_offset) & 0xF
325 elif bits_per_pixel == 8:
327 elif bits_per_pixel == 16:
328 value = struct.unpack_from('<H', rowdata, x * 2)[0]
329 elif bits_per_pixel == 24:
331 value = (rowdata[offset] << 16) | (rowdata[offset + 1] << 8) | rowdata[offset + 2]
332 elif bits_per_pixel == 32:
333 value = struct.unpack_from('<I', rowdata, x * 4)[0]
335 bitmap[x, y_draw] = value & mask
338 Normal = "bitmaptools.BlendMode.Normal"
339 Screen = "bitmaptools.BlendMode.Screen"
341 def alphablend(dest: Bitmap, source1: Bitmap, source2: Bitmap, colorspace: Colorspace,
342 factor1: float = 0.5, factor2: Optional[float] = None,
343 blendmode: BlendMode = BlendMode.Normal, skip_source1_index: Optional[int] = None,
344 skip_source2_index: Optional[int] = None):
346 colorspace should be one of: 'L8', 'RGB565', 'RGB565_SWAPPED', 'BGR565_SWAPPED'.
348 blendmode can be 'normal' (or any default) or 'screen'.
350 This assumes that all bitmaps (dest, source1, source2) support 2D access like bitmap[x, y].
352 dest.width and dest.height are used; make sure the bitmap objects have these attributes or replace them with your own logic.
354 def clamp(val, minval, maxval):
355 return max(minval, min(maxval, val))
357 ifactor1 = int(factor1 * 256)
358 ifactor2 = int(factor2 * 256)
360 width, height = dest.width, dest.height
362 if colorspace == 'L8':
363 for y in range(height):
364 for x in range(width):
367 blend_source1 = skip_source1_index is None or sp1 != skip_source1_index
368 blend_source2 = skip_source2_index is None or sp2 != skip_source2_index
370 if blend_source1 and blend_source2:
374 if blendmode == BlendMode.Screen:
375 blend = sca + sda - (sca * sda // 65536)
376 elif blendmode == BlendMode.Normal:
377 blend = sca + sda * (256 - ifactor2) // 256
379 denom = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256
380 pixel = blend // denom
382 pixel = sp1 * ifactor1 // 256
384 pixel = sp2 * ifactor2 // 256
388 dest[x, y] = clamp(pixel, 0, 255)
391 swap = colorspace in ('RGB565_SWAPPED', 'BGR565_SWAPPED')
396 for y in range(height):
397 for x in range(width):
402 sp1 = ((sp1 & 0xFF) << 8) | ((sp1 >> 8) & 0xFF)
403 sp2 = ((sp2 & 0xFF) << 8) | ((sp2 >> 8) & 0xFF)
405 blend_source1 = skip_source1_index is None or sp1 != skip_source1_index
406 blend_source2 = skip_source2_index is None or sp2 != skip_source2_index
408 if blend_source1 and blend_source2:
409 ifactor_blend = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256
411 red_dca = ((sp1 & r_mask) >> 8) * ifactor1
412 grn_dca = ((sp1 & g_mask) >> 3) * ifactor1
413 blu_dca = ((sp1 & b_mask) << 3) * ifactor1
415 red_sca = ((sp2 & r_mask) >> 8) * ifactor2
416 grn_sca = ((sp2 & g_mask) >> 3) * ifactor2
417 blu_sca = ((sp2 & b_mask) << 3) * ifactor2
419 if blendmode == BlendMode.Screen:
420 red_blend = red_sca + red_dca - (red_sca * red_dca // 65536)
421 grn_blend = grn_sca + grn_dca - (grn_sca * grn_dca // 65536)
422 blu_blend = blu_sca + blu_dca - (blu_sca * blu_dca // 65536)
423 elif blendmode == BlendMode.Normal:
424 red_blend = red_sca + red_dca * (256 - ifactor2) // 256
425 grn_blend = grn_sca + grn_dca * (256 - ifactor2) // 256
426 blu_blend = blu_sca + blu_dca * (256 - ifactor2) // 256
428 r = ((red_blend // ifactor_blend) << 8) & r_mask
429 g = ((grn_blend // ifactor_blend) << 3) & g_mask
430 b = ((blu_blend // ifactor_blend) >> 3) & b_mask
432 pixel = (r & r_mask) | (g & g_mask) | (b & b_mask)
435 pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF)
438 r = ((sp1 & r_mask) * ifactor1 // 256) & r_mask
439 g = ((sp1 & g_mask) * ifactor1 // 256) & g_mask
440 b = ((sp1 & b_mask) * ifactor1 // 256) & b_mask
443 r = ((sp2 & r_mask) * ifactor2 // 256) & r_mask
444 g = ((sp2 & g_mask) * ifactor2 // 256) & g_mask
445 b = ((sp2 & b_mask) * ifactor2 // 256) & b_mask
450 print(f"pixel hex: {hex(pixel)}")
453 class DitherAlgorithm:
454 Atkinson = "bitmaptools.DitherAlgorithm.Atkinson"
455 FloydStenberg = "bitmaptools.DitherAlgorithm.FloydStenberg"
462 {'dx': 2, 'dy': 0, 'dl': 256 // 8},
463 {'dx': -1, 'dy': 1, 'dl': 256 // 8},
464 {'dx': 0, 'dy': 1, 'dl': 256 // 8},
465 {'dx': 0, 'dy': 2, 'dl': 256 // 8},
474 {'dx': -1, 'dy': 1, 'dl': 3 * 256 // 16},
475 {'dx': 0, 'dy': 1, 'dl': 5 * 256 // 16},
476 {'dx': 1, 'dy': 1, 'dl': 1 * 256 // 16},
482 FloydStenberg: floyd_stenberg
486 def dither(dest_bitmap, source_bitmap, colorspace, algorithm=DitherAlgorithm.Atkinson):
489 height, width = dest_bitmap.width, dest_bitmap.height
490 swap_bytes = colorspace in (Colorspace.RGB565_SWAPPED, Colorspace.BGR565_SWAPPED)
491 swap_rb = colorspace in (Colorspace.BGR565, Colorspace.BGR565_SWAPPED)
492 algorithm_info = DitherAlgorithm.algorithm_map[algorithm]
493 mx = algorithm_info['mx']
494 count = algorithm_info['count']
495 terms = algorithm_info['terms']
496 dl = algorithm_info['dl']
505 print(f"swap: {swap}")
507 # Create row data arrays (3 rows with padding on both sides)
508 rowdata = [[0] * (width + 2 * mx) for _ in range(3)]
515 # Output array for one row at a time (padded to multiple of 32)
516 out = [False] * (((width + 31) // 32) * 32)
518 # Helper function to fill a row with luminance data
519 def fill_row(bitmap, swap, luminance_data, y, mx):
520 if y >= bitmap.height:
523 # Zero out padding area
525 luminance_data[-mx + i] = 0
526 luminance_data[bitmap.width + i] = 0
528 if bitmap._bits_per_value == 8:
529 for x in range(bitmap.width):
530 luminance_data[x] = bitmap[x, y]
532 for x in range(bitmap.width):
534 if swap & SWAP_BYTES:
535 # Swap bytes (equivalent to __builtin_bswap16)
536 pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF)
538 r = (pixel >> 8) & 0xF8
539 g = (pixel >> 3) & 0xFC
540 b = (pixel << 3) & 0xF8
542 if swap & SWAP_BYTES:
545 # Calculate luminance using same formula as C version
546 luminance_data[x] = (r * 78 + g * 154 + b * 29) // 256
548 # Helper function to write pixels to destination bitmap
549 def write_pixels(bitmap, y, data):
550 if bitmap._bits_per_value == 1:
551 for i in range(0, bitmap.width, 32):
552 # Pack 32 bits into an integer
554 for j in range(min(32, bitmap.width - i)):
560 for j in range(min(32, bitmap.width - i)):
561 bitmap[i + j, y] = (p >> (31 - j)) & 1
563 for i in range(bitmap.width):
564 bitmap[i, y] = 65535 if data[i] else 0
567 fill_row(source_bitmap, swap, rows[0], 0, mx)
568 fill_row(source_bitmap, swap, rows[1], 1, mx)
569 fill_row(source_bitmap, swap, rows[2], 2, mx)
573 for y in range(height):
574 # Going left to right
575 for x in range(width):
576 pixel_in = rows[0][x] + err
577 pixel_out = pixel_in >= 128
580 err = pixel_in - (255 if pixel_out else 0)
582 # Distribute error to neighboring pixels
583 for i in range(count):
584 x1 = x + terms[i]['dx']
587 rows[dy][x1] = ((terms[i]['dl'] * err) // 256) + rows[dy][x1]
589 err = (err * dl) // 256
591 write_pixels(dest_bitmap, y, out)
594 rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0]
600 # Fill the next row for future processing
601 fill_row(source_bitmap, swap, rows[2], y + 2, mx)
603 # Going right to left
604 for x in range(width - 1, -1, -1):
605 pixel_in = rows[0][x] + err
606 pixel_out = pixel_in >= 128
609 err = pixel_in - (255 if pixel_out else 0)
611 # Distribute error to neighboring pixels (in reverse direction)
612 for i in range(count):
613 x1 = x - terms[i]['dx']
616 rows[dy][x1] = ((terms[i]['dl'] * err) // 256) + rows[dy][x1]
618 err = (err * dl) // 256
620 write_pixels(dest_bitmap, y, out)
622 # Cycle the rows again
623 rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0]
625 # Fill the next row for future processing
626 fill_row(source_bitmap, swap, rows[2], y + 3, mx)
628 # Mark the entire bitmap as dirty (this would be implementation-specific)
629 # In CircuitPython, this might be something like: