]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - bitmaptools/__init__.py
adding types, fix default args behavior, dither implementation, BlendMode enum class
[hackapet/Adafruit_Blinka_Displayio.git] / bitmaptools / __init__.py
1 import math
2 import struct
3 from typing import Optional, Tuple, BinaryIO
4 import numpy as np
5 from displayio import Bitmap, Colorspace
6 import circuitpython_typing
7
8
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
13
14
15 def draw_line(dest_bitmap: Bitmap, x1: int, y1: int, x2: int, y2: int, value: int):
16     dx = abs(x2 - x1)
17     sx = 1 if x1 < x2 else -1
18     dy = -abs(y2 - y1)
19     sy = 1 if y1 < y2 else -1
20     error = dx + dy
21
22     while True:
23         dest_bitmap[x1, y1] = value
24         if x1 == x2 and y1 == y2:
25             break
26         e2 = 2 * error
27         if e2 >= dy:
28             error += dy
29             x1 += sx
30         if e2 <= dx:
31             error += dx
32             y1 += sy
33
34
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))
38
39     xb = 0
40     yb = radius
41     d = 3 - 2 * radius
42
43     # Bresenham's circle algorithm
44     while xb <= yb:
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
53
54         if d <= 0:
55             d = d + (4 * xb) + 6
56         else:
57             d = d + 4 * (xb - yb) + 10
58             yb = yb - 1
59         xb += 1
60
61
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.")
68
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],
76                   value=value)
77
78     if close:
79         print(f"close: {(xs[0], ys[0])} - {(xs[-1], ys[-1])}")
80         draw_line(dest_bitmap=dest_bitmap,
81                   x1=xs[0], y1=ys[0],
82                   x2=xs[-1], y2=ys[-1],
83                   value=value)
84
85
86 def blit(dest_bitmap: Bitmap, source_bitmap: Bitmap,
87          x: int, y: int, *,
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
94     if x2 is None:
95         x2 = source_bitmap.width
96     if y2 is None:
97         y2 = source_bitmap.height
98
99     # Rearrange so that x1 < x2 and y1 < y2
100     if x1 > x2:
101         x1, x2 = x2, x1
102     if y1 > y2:
103         y1, y2 = y2, y1
104
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)
108
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
113
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
120                     ]
121
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
127                             ] = this_pixel_color
128             elif y_placement > dest_bitmap.height:
129                 break
130
131
132 def rotozoom(
133         dest_bitmap: Bitmap,
134         source_bitmap: Bitmap,
135         *,
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,
147 ):
148
149     if ox is None:
150         ox = dest_bitmap.width // 2
151     if oy in None:
152         oy = dest_bitmap.height // 2
153
154     if dest_clip0 is None:
155         dest_clip0 = (0,0)
156     if dest_clip1 is None:
157         dest_clip1 = (dest_bitmap.width,dest_bitmap.height)
158
159     if px is None:
160         px = source_bitmap.width // 2
161     if py in None:
162         py = source_bitmap.height // 2
163
164     if source_clip0 is None:
165         source_clip0 = (0,0)
166     if source_clip1 is None:
167         source_clip1 = (source_bitmap.width,source_bitmap.height)
168
169     if angle is None:
170         angle = 0.0
171     if scale is None:
172         scale = 1.0
173
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
178
179     minx = dest_clip1_x
180     miny = dest_clip1_y
181     maxx = dest_clip0_x
182     maxy = dest_clip0_y
183
184     sin_angle = math.sin(angle)
185     cos_angle = math.cos(angle)
186
187     def update_bounds(dx, dy):
188         nonlocal minx, maxx, miny, maxy
189         if dx < minx:
190             minx = int(dx)
191         if dx > maxx:
192             maxx = int(dx)
193         if dy < miny:
194             miny = int(dy)
195         if dy > maxy:
196             maxy = int(dy)
197
198     w = source_bitmap.width
199     h = source_bitmap.height
200
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)
204
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)
208
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)
212
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)
216
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)
222
223     dv_col = cos_angle / scale
224     du_col = sin_angle / scale
225     du_row = dv_col
226     dv_row = -du_col
227
228     startu = px - (ox * dv_col + oy * du_col)
229     startv = py - (ox * dv_row + oy * du_row)
230
231     rowu = startu + miny * du_col
232     rowv = startv + miny * dv_col
233
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
242             u += du_row
243             v += dv_row
244         rowu += du_col
245         rowv += dv_col
246
247
248 def arrayblit(
249         bitmap: Bitmap,
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):
254     if x2 is None:
255         x2 = bitmap.width
256     if y2 is None:
257         y2 = bitmap.height
258
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:
265                 bitmap[x, y] = value
266
267
268 def readinto(bitmap: Bitmap,
269              file: BinaryIO,
270              bits_per_pixel: int,
271              element_size: int = 1,
272              reverse_pixels_in_element: bool = False,
273              swap_bytes: bool = False,
274              reverse_rows: bool = False):
275     width = bitmap.width
276     height = bitmap.height
277     bits_per_value = bitmap._bits_per_value
278     mask = (1 << bits_per_value) - 1
279
280     elements_per_row = (width * bits_per_pixel + element_size * 8 - 1) // (element_size * 8)
281     rowsize = element_size * elements_per_row
282
283     for y in range(height):
284         row_bytes = file.read(rowsize)
285         if len(row_bytes) != rowsize:
286             raise EOFError()
287
288         # Convert the raw bytes into the appropriate type array for processing
289         rowdata = bytearray(row_bytes)
290
291         if swap_bytes:
292             if element_size == 2:
293                 rowdata = bytearray(
294                     b''.join(
295                         struct.pack('<H', struct.unpack('>H', rowdata[i:i + 2])[0])
296                         for i in range(0, len(rowdata), 2)
297                     )
298                 )
299             elif element_size == 4:
300                 rowdata = bytearray(
301                     b''.join(
302                         struct.pack('<I', struct.unpack('>I', rowdata[i:i + 4])[0])
303                         for i in range(0, len(rowdata), 4)
304                     )
305                 )
306
307         y_draw = height - 1 - y if reverse_rows else y
308
309         for x in range(width):
310             value = 0
311             if bits_per_pixel == 1:
312                 byte_offset = x // 8
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:
316                 byte_offset = x // 4
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:
321                 byte_offset = x // 2
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:
326                 value = rowdata[x]
327             elif bits_per_pixel == 16:
328                 value = struct.unpack_from('<H', rowdata, x * 2)[0]
329             elif bits_per_pixel == 24:
330                 offset = x * 3
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]
334
335             bitmap[x, y_draw] = value & mask
336
337 class BlendMode:
338     Normal = "bitmaptools.BlendMode.Normal"
339     Screen = "bitmaptools.BlendMode.Screen"
340
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):
345     """
346     colorspace should be one of: 'L8', 'RGB565', 'RGB565_SWAPPED', 'BGR565_SWAPPED'.
347
348 blendmode can be 'normal' (or any default) or 'screen'.
349
350 This assumes that all bitmaps (dest, source1, source2) support 2D access like bitmap[x, y].
351
352 dest.width and dest.height are used; make sure the bitmap objects have these attributes or replace them with your own logic.
353     """
354     def clamp(val, minval, maxval):
355         return max(minval, min(maxval, val))
356
357     ifactor1 = int(factor1 * 256)
358     ifactor2 = int(factor2 * 256)
359
360     width, height = dest.width, dest.height
361
362     if colorspace == 'L8':
363         for y in range(height):
364             for x in range(width):
365                 sp1 = source1[x, y]
366                 sp2 = source2[x, y]
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
369
370                 if blend_source1 and blend_source2:
371                     sda = sp1 * ifactor1
372                     sca = sp2 * ifactor2
373
374                     if blendmode == BlendMode.Screen:
375                         blend = sca + sda - (sca * sda // 65536)
376                     elif blendmode == BlendMode.Normal:
377                         blend = sca + sda * (256 - ifactor2) // 256
378
379                     denom = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256
380                     pixel = blend // denom
381                 elif blend_source1:
382                     pixel = sp1 * ifactor1 // 256
383                 elif blend_source2:
384                     pixel = sp2 * ifactor2 // 256
385                 else:
386                     pixel = dest[x, y]
387
388                 dest[x, y] = clamp(pixel, 0, 255)
389
390     else:
391         swap = colorspace in ('RGB565_SWAPPED', 'BGR565_SWAPPED')
392         r_mask = 0xf800
393         g_mask = 0x07e0
394         b_mask = 0x001f
395
396         for y in range(height):
397             for x in range(width):
398                 sp1 = source1[x, y]
399                 sp2 = source2[x, y]
400
401                 if swap:
402                     sp1 = ((sp1 & 0xFF) << 8) | ((sp1 >> 8) & 0xFF)
403                     sp2 = ((sp2 & 0xFF) << 8) | ((sp2 >> 8) & 0xFF)
404
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
407
408                 if blend_source1 and blend_source2:
409                     ifactor_blend = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256
410
411                     red_dca = ((sp1 & r_mask) >> 8) * ifactor1
412                     grn_dca = ((sp1 & g_mask) >> 3) * ifactor1
413                     blu_dca = ((sp1 & b_mask) << 3) * ifactor1
414
415                     red_sca = ((sp2 & r_mask) >> 8) * ifactor2
416                     grn_sca = ((sp2 & g_mask) >> 3) * ifactor2
417                     blu_sca = ((sp2 & b_mask) << 3) * ifactor2
418
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
427
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
431
432                     pixel = (r & r_mask) | (g & g_mask) | (b & b_mask)
433
434                     if swap:
435                         pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF)
436
437                 elif blend_source1:
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
441                     pixel = r | g | b
442                 elif blend_source2:
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
446                     pixel = r | g | b
447                 else:
448                     pixel = dest[x, y]
449
450                 print(f"pixel hex: {hex(pixel)}")
451                 dest[x, y] = pixel
452
453 class DitherAlgorithm:
454     Atkinson = "bitmaptools.DitherAlgorithm.Atkinson"
455     FloydStenberg = "bitmaptools.DitherAlgorithm.FloydStenberg"
456
457     atkinson = {
458         'count': 4,
459         'mx': 2,
460         'dl': 256 // 8,
461         'terms': [
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},
466         ]
467     }
468
469     floyd_stenberg = {
470         'count': 3,
471         'mx': 1,
472         'dl': 7 * 256 // 16,
473         'terms': [
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},
477         ]
478     }
479
480     algorithm_map = {
481         Atkinson: atkinson,
482         FloydStenberg: floyd_stenberg
483     }
484
485
486 def dither(dest_bitmap, source_bitmap, colorspace, algorithm=DitherAlgorithm.Atkinson):
487     SWAP_BYTES = 1 << 0
488     SWAP_RB = 1 << 1
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']
497
498     swap = 0
499     if swap_bytes:
500         swap |= SWAP_BYTES
501
502     if swap_rb:
503         swap |= SWAP_RB
504
505     print(f"swap: {swap}")
506
507     # Create row data arrays (3 rows with padding on both sides)
508     rowdata = [[0] * (width + 2 * mx) for _ in range(3)]
509     rows = [
510         rowdata[0][mx:],
511         rowdata[1][mx:],
512         rowdata[2][mx:]
513     ]
514
515     # Output array for one row at a time (padded to multiple of 32)
516     out = [False] * (((width + 31) // 32) * 32)
517
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:
521             return
522
523         # Zero out padding area
524         for i in range(mx):
525             luminance_data[-mx + i] = 0
526             luminance_data[bitmap.width + i] = 0
527
528         if bitmap._bits_per_value == 8:
529             for x in range(bitmap.width):
530                 luminance_data[x] = bitmap[x, y]
531         else:
532             for x in range(bitmap.width):
533                 pixel = bitmap[x, y]
534                 if swap & SWAP_BYTES:
535                     # Swap bytes (equivalent to __builtin_bswap16)
536                     pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF)
537
538                 r = (pixel >> 8) & 0xF8
539                 g = (pixel >> 3) & 0xFC
540                 b = (pixel << 3) & 0xF8
541
542                 if swap & SWAP_BYTES:
543                     r, b = b, r
544
545                 # Calculate luminance using same formula as C version
546                 luminance_data[x] = (r * 78 + g * 154 + b * 29) // 256
547
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
553                 p = 0
554                 for j in range(min(32, bitmap.width - i)):
555                     p = (p << 1)
556                     if data[i + j]:
557                         p |= 1
558
559                 # Write packed value
560                 for j in range(min(32, bitmap.width - i)):
561                     bitmap[i + j, y] = (p >> (31 - j)) & 1
562         else:
563             for i in range(bitmap.width):
564                 bitmap[i, y] = 65535 if data[i] else 0
565
566     # Fill initial rows
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)
570
571     err = 0
572
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
578             out[x] = pixel_out
579
580             err = pixel_in - (255 if pixel_out else 0)
581
582             # Distribute error to neighboring pixels
583             for i in range(count):
584                 x1 = x + terms[i]['dx']
585                 dy = terms[i]['dy']
586
587                 rows[dy][x1] = ((terms[i]['dl'] * err) // 256) + rows[dy][x1]
588
589             err = (err * dl) // 256
590
591         write_pixels(dest_bitmap, y, out)
592
593         # Cycle the rows
594         rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0]
595
596         y += 1
597         if y == height:
598             break
599
600         # Fill the next row for future processing
601         fill_row(source_bitmap, swap, rows[2], y + 2, mx)
602
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
607             out[x] = pixel_out
608
609             err = pixel_in - (255 if pixel_out else 0)
610
611             # Distribute error to neighboring pixels (in reverse direction)
612             for i in range(count):
613                 x1 = x - terms[i]['dx']
614                 dy = terms[i]['dy']
615
616                 rows[dy][x1] = ((terms[i]['dl'] * err) // 256) + rows[dy][x1]
617
618             err = (err * dl) // 256
619
620         write_pixels(dest_bitmap, y, out)
621
622         # Cycle the rows again
623         rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0]
624
625         # Fill the next row for future processing
626         fill_row(source_bitmap, swap, rows[2], y + 3, mx)
627
628     # Mark the entire bitmap as dirty (this would be implementation-specific)
629     # In CircuitPython, this might be something like:
630     dest_bitmap.dirty()