]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - bitmaptools/__init__.py
fix some pylint issues, and ignore some.
[hackapet/Adafruit_Blinka_Displayio.git] / bitmaptools / __init__.py
1 import math
2 import struct
3 from typing import Optional, Tuple, BinaryIO
4 import circuitpython_typing
5 from displayio import Bitmap, Colorspace
6
7
8 # pylint: disable=invalid-name, too-many-arguments, too-many-locals, too-many-branches, too-many-statements
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(
63     dest_bitmap: Bitmap,
64     xs: circuitpython_typing.ReadableBuffer,
65     ys: circuitpython_typing.ReadableBuffer,
66     value: int,
67     close: bool = True,
68 ):
69     if len(xs) != len(ys):
70         raise ValueError("Length of xs and ys must be equal.")
71
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}")
76         draw_line(
77             dest_bitmap=dest_bitmap,
78             x1=cur_point[0],
79             y1=cur_point[1],
80             x2=next_point[0],
81             y2=next_point[1],
82             value=value,
83         )
84
85     if close:
86         print(f"close: {(xs[0], ys[0])} - {(xs[-1], ys[-1])}")
87         draw_line(
88             dest_bitmap=dest_bitmap,
89             x1=xs[0],
90             y1=ys[0],
91             x2=xs[-1],
92             y2=ys[-1],
93             value=value,
94         )
95
96
97 def blit(
98     dest_bitmap: Bitmap,
99     source_bitmap: Bitmap,
100     x: int,
101     y: int,
102     *,
103     x1: int = 0,
104     y1: int = 0,
105     x2: int | None = None,
106     y2: int | None = None,
107     skip_source_index: int | None = None,
108     skip_dest_index: int | None = None,
109 ):
110     """Inserts the source_bitmap region defined by rectangular boundaries"""
111     # pylint: disable=invalid-name
112     if x2 is None:
113         x2 = source_bitmap.width
114     if y2 is None:
115         y2 = source_bitmap.height
116
117     # Rearrange so that x1 < x2 and y1 < y2
118     if x1 > x2:
119         x1, x2 = x2, x1
120     if y1 > y2:
121         y1, y2 = y2, y1
122
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)
126
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
131
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
138                 ]
139
140                 if (skip_source_index is None) or (
141                     this_pixel_color != skip_source_index
142                 ):
143                     if (skip_dest_index is None) or (
144                         dest_bitmap[y_placement * dest_bitmap.width + x_placement]
145                         != skip_dest_index
146                     ):
147                         dest_bitmap[
148                             y_placement * dest_bitmap.width + x_placement
149                         ] = this_pixel_color
150             elif y_placement > dest_bitmap.height:
151                 break
152
153
154 def rotozoom(
155     dest_bitmap: Bitmap,
156     source_bitmap: Bitmap,
157     *,
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,
169 ):
170     if ox is None:
171         ox = dest_bitmap.width // 2
172     if oy is None:
173         oy = dest_bitmap.height // 2
174
175     if dest_clip0 is None:
176         dest_clip0 = (0, 0)
177     if dest_clip1 is None:
178         dest_clip1 = (dest_bitmap.width, dest_bitmap.height)
179
180     if px is None:
181         px = source_bitmap.width // 2
182     if py is None:
183         py = source_bitmap.height // 2
184
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)
189
190     if angle is None:
191         angle = 0.0
192     if scale is None:
193         scale = 1.0
194
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
199
200     minx = dest_clip1_x
201     miny = dest_clip1_y
202     maxx = dest_clip0_x
203     maxy = dest_clip0_y
204
205     sin_angle = math.sin(angle)
206     cos_angle = math.cos(angle)
207
208     def update_bounds(dx, dy):
209         nonlocal minx, maxx, miny, maxy
210         if dx < minx:
211             minx = int(dx)
212         if dx > maxx:
213             maxx = int(dx)
214         if dy < miny:
215             miny = int(dy)
216         if dy > maxy:
217             maxy = int(dy)
218
219     w = source_bitmap.width
220     h = source_bitmap.height
221
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)
225
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)
229
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)
233
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)
237
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)
243
244     dv_col = cos_angle / scale
245     du_col = sin_angle / scale
246     du_row = dv_col
247     dv_row = -du_col
248
249     startu = px - (ox * dv_col + oy * du_col)
250     startv = py - (ox * dv_row + oy * du_row)
251
252     rowu = startu + miny * du_col
253     rowv = startv + miny * dv_col
254
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
261             ):
262                 c = source_bitmap[int(u), int(v)]
263                 if skip_index is None or c != skip_index:
264                     dest_bitmap[x, y] = c
265             u += du_row
266             v += dv_row
267         rowu += du_col
268         rowv += dv_col
269
270
271 def arrayblit(
272     bitmap: Bitmap,
273     data: circuitpython_typing.ReadableBuffer,
274     x1: int = 0,
275     y1: int = 0,
276     x2: Optional[int] = None,
277     y2: Optional[int] = None,
278     skip_index: Optional[int] = None,
279 ):
280     if x2 is None:
281         x2 = bitmap.width
282     if y2 is None:
283         y2 = bitmap.height
284
285     _value_count = 2**bitmap._bits_per_value  # pylint: disable=protected-access
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:
291                 bitmap[x, y] = value
292
293
294 def readinto(
295     bitmap: Bitmap,
296     file: BinaryIO,
297     bits_per_pixel: int,
298     element_size: int = 1,
299     reverse_pixels_in_element: bool = False,
300     swap_bytes: bool = False,
301     reverse_rows: bool = False,
302 ):
303     width = bitmap.width
304     height = bitmap.height
305     bits_per_value = bitmap._bits_per_value
306     mask = (1 << bits_per_value) - 1
307
308     elements_per_row = (width * bits_per_pixel + element_size * 8 - 1) // (
309         element_size * 8
310     )
311     rowsize = element_size * elements_per_row
312
313     for y in range(height):
314         row_bytes = file.read(rowsize)
315         if len(row_bytes) != rowsize:
316             raise EOFError()
317
318         # Convert the raw bytes into the appropriate type array for processing
319         rowdata = bytearray(row_bytes)
320
321         if swap_bytes:
322             if element_size == 2:
323                 rowdata = bytearray(
324                     b"".join(
325                         struct.pack("<H", struct.unpack(">H", rowdata[i : i + 2])[0])
326                         for i in range(0, len(rowdata), 2)
327                     )
328                 )
329             elif element_size == 4:
330                 rowdata = bytearray(
331                     b"".join(
332                         struct.pack("<I", struct.unpack(">I", rowdata[i : i + 4])[0])
333                         for i in range(0, len(rowdata), 4)
334                     )
335                 )
336
337         y_draw = height - 1 - y if reverse_rows else y
338
339         for x in range(width):
340             value = 0
341             if bits_per_pixel == 1:
342                 byte_offset = x // 8
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:
346                 byte_offset = x // 4
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:
351                 byte_offset = x // 2
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:
356                 value = rowdata[x]
357             elif bits_per_pixel == 16:
358                 value = struct.unpack_from("<H", rowdata, x * 2)[0]
359             elif bits_per_pixel == 24:
360                 offset = x * 3
361                 value = (
362                     (rowdata[offset] << 16)
363                     | (rowdata[offset + 1] << 8)
364                     | rowdata[offset + 2]
365                 )
366             elif bits_per_pixel == 32:
367                 value = struct.unpack_from("<I", rowdata, x * 4)[0]
368
369             bitmap[x, y_draw] = value & mask
370
371
372 class BlendMode:
373     Normal = "bitmaptools.BlendMode.Normal"
374     Screen = "bitmaptools.BlendMode.Screen"
375
376
377 def alphablend(
378     dest: Bitmap,
379     source1: Bitmap,
380     source2: Bitmap,
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,
387 ):
388     """
389     colorspace should be one of: 'L8', 'RGB565', 'RGB565_SWAPPED', 'BGR565_SWAPPED'.
390
391     blendmode must be one of the BlendMode constants
392     """
393
394     def clamp(val, minval, maxval):
395         return max(minval, min(maxval, val))
396
397     ifactor1 = int(factor1 * 256)
398     ifactor2 = int(factor2 * 256)
399
400     width, height = dest.width, dest.height
401
402     if colorspace == "L8":
403         for y in range(height):
404             for x in range(width):
405                 sp1 = source1[x, y]
406                 sp2 = source2[x, y]
407                 blend_source1 = skip_source1_index is None or sp1 != skip_source1_index
408                 blend_source2 = skip_source2_index is None or sp2 != skip_source2_index
409
410                 if blend_source1 and blend_source2:
411                     sda = sp1 * ifactor1
412                     sca = sp2 * ifactor2
413
414                     if blendmode == BlendMode.Screen:
415                         blend = sca + sda - (sca * sda // 65536)
416                     elif blendmode == BlendMode.Normal:
417                         blend = sca + sda * (256 - ifactor2) // 256
418
419                     denom = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256
420                     pixel = blend // denom
421                 elif blend_source1:
422                     pixel = sp1 * ifactor1 // 256
423                 elif blend_source2:
424                     pixel = sp2 * ifactor2 // 256
425                 else:
426                     pixel = dest[x, y]
427
428                 dest[x, y] = clamp(pixel, 0, 255)
429
430     else:
431         swap = colorspace in ("RGB565_SWAPPED", "BGR565_SWAPPED")
432         r_mask = 0xF800
433         g_mask = 0x07E0
434         b_mask = 0x001F
435
436         for y in range(height):
437             for x in range(width):
438                 sp1 = source1[x, y]
439                 sp2 = source2[x, y]
440
441                 if swap:
442                     sp1 = ((sp1 & 0xFF) << 8) | ((sp1 >> 8) & 0xFF)
443                     sp2 = ((sp2 & 0xFF) << 8) | ((sp2 >> 8) & 0xFF)
444
445                 blend_source1 = skip_source1_index is None or sp1 != skip_source1_index
446                 blend_source2 = skip_source2_index is None or sp2 != skip_source2_index
447
448                 if blend_source1 and blend_source2:
449                     ifactor_blend = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256
450
451                     red_dca = ((sp1 & r_mask) >> 8) * ifactor1
452                     grn_dca = ((sp1 & g_mask) >> 3) * ifactor1
453                     blu_dca = ((sp1 & b_mask) << 3) * ifactor1
454
455                     red_sca = ((sp2 & r_mask) >> 8) * ifactor2
456                     grn_sca = ((sp2 & g_mask) >> 3) * ifactor2
457                     blu_sca = ((sp2 & b_mask) << 3) * ifactor2
458
459                     if blendmode == BlendMode.Screen:
460                         red_blend = red_sca + red_dca - (red_sca * red_dca // 65536)
461                         grn_blend = grn_sca + grn_dca - (grn_sca * grn_dca // 65536)
462                         blu_blend = blu_sca + blu_dca - (blu_sca * blu_dca // 65536)
463                     elif blendmode == BlendMode.Normal:
464                         red_blend = red_sca + red_dca * (256 - ifactor2) // 256
465                         grn_blend = grn_sca + grn_dca * (256 - ifactor2) // 256
466                         blu_blend = blu_sca + blu_dca * (256 - ifactor2) // 256
467
468                     r = ((red_blend // ifactor_blend) << 8) & r_mask
469                     g = ((grn_blend // ifactor_blend) << 3) & g_mask
470                     b = ((blu_blend // ifactor_blend) >> 3) & b_mask
471
472                     pixel = (r & r_mask) | (g & g_mask) | (b & b_mask)
473
474                     if swap:
475                         pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF)
476
477                 elif blend_source1:
478                     r = ((sp1 & r_mask) * ifactor1 // 256) & r_mask
479                     g = ((sp1 & g_mask) * ifactor1 // 256) & g_mask
480                     b = ((sp1 & b_mask) * ifactor1 // 256) & b_mask
481                     pixel = r | g | b
482                 elif blend_source2:
483                     r = ((sp2 & r_mask) * ifactor2 // 256) & r_mask
484                     g = ((sp2 & g_mask) * ifactor2 // 256) & g_mask
485                     b = ((sp2 & b_mask) * ifactor2 // 256) & b_mask
486                     pixel = r | g | b
487                 else:
488                     pixel = dest[x, y]
489
490                 print(f"pixel hex: {hex(pixel)}")
491                 dest[x, y] = pixel
492
493
494 class DitherAlgorithm:
495     Atkinson = "bitmaptools.DitherAlgorithm.Atkinson"
496     FloydStenberg = "bitmaptools.DitherAlgorithm.FloydStenberg"
497
498     atkinson = {
499         "count": 4,
500         "mx": 2,
501         "dl": 256 // 8,
502         "terms": [
503             {"dx": 2, "dy": 0, "dl": 256 // 8},
504             {"dx": -1, "dy": 1, "dl": 256 // 8},
505             {"dx": 0, "dy": 1, "dl": 256 // 8},
506             {"dx": 0, "dy": 2, "dl": 256 // 8},
507         ],
508     }
509
510     floyd_stenberg = {
511         "count": 3,
512         "mx": 1,
513         "dl": 7 * 256 // 16,
514         "terms": [
515             {"dx": -1, "dy": 1, "dl": 3 * 256 // 16},
516             {"dx": 0, "dy": 1, "dl": 5 * 256 // 16},
517             {"dx": 1, "dy": 1, "dl": 1 * 256 // 16},
518         ],
519     }
520
521     algorithm_map = {Atkinson: atkinson, FloydStenberg: floyd_stenberg}
522
523
524 def dither(dest_bitmap, source_bitmap, colorspace, algorithm=DitherAlgorithm.Atkinson):
525     SWAP_BYTES = 1 << 0
526     SWAP_RB = 1 << 1
527     height, width = dest_bitmap.width, dest_bitmap.height
528     swap_bytes = colorspace in (Colorspace.RGB565_SWAPPED, Colorspace.BGR565_SWAPPED)
529     swap_rb = colorspace in (Colorspace.BGR565, Colorspace.BGR565_SWAPPED)
530     algorithm_info = DitherAlgorithm.algorithm_map[algorithm]
531     mx = algorithm_info["mx"]
532     count = algorithm_info["count"]
533     terms = algorithm_info["terms"]
534     dl = algorithm_info["dl"]
535
536     swap = 0
537     if swap_bytes:
538         swap |= SWAP_BYTES
539
540     if swap_rb:
541         swap |= SWAP_RB
542
543     print(f"swap: {swap}")
544
545     # Create row data arrays (3 rows with padding on both sides)
546     rowdata = [[0] * (width + 2 * mx) for _ in range(3)]
547     rows = [rowdata[0][mx:], rowdata[1][mx:], rowdata[2][mx:]]
548
549     # Output array for one row at a time (padded to multiple of 32)
550     out = [False] * (((width + 31) // 32) * 32)
551
552     # Helper function to fill a row with luminance data
553     def fill_row(bitmap, swap, luminance_data, y, mx):
554         if y >= bitmap.height:
555             return
556
557         # Zero out padding area
558         for i in range(mx):
559             luminance_data[-mx + i] = 0
560             luminance_data[bitmap.width + i] = 0
561
562         if bitmap._bits_per_value == 8:
563             for x in range(bitmap.width):
564                 luminance_data[x] = bitmap[x, y]
565         else:
566             for x in range(bitmap.width):
567                 pixel = bitmap[x, y]
568                 if swap & SWAP_BYTES:
569                     # Swap bytes (equivalent to __builtin_bswap16)
570                     pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF)
571
572                 r = (pixel >> 8) & 0xF8
573                 g = (pixel >> 3) & 0xFC
574                 b = (pixel << 3) & 0xF8
575
576                 if swap & SWAP_BYTES:
577                     r, b = b, r
578
579                 # Calculate luminance using same formula as C version
580                 luminance_data[x] = (r * 78 + g * 154 + b * 29) // 256
581
582     # Helper function to write pixels to destination bitmap
583     def write_pixels(bitmap, y, data):
584         if bitmap._bits_per_value == 1:
585             for i in range(0, bitmap.width, 32):
586                 # Pack 32 bits into an integer
587                 p = 0
588                 for j in range(min(32, bitmap.width - i)):
589                     p = p << 1
590                     if data[i + j]:
591                         p |= 1
592
593                 # Write packed value
594                 for j in range(min(32, bitmap.width - i)):
595                     bitmap[i + j, y] = (p >> (31 - j)) & 1
596         else:
597             for i in range(bitmap.width):
598                 bitmap[i, y] = 65535 if data[i] else 0
599
600     # Fill initial rows
601     fill_row(source_bitmap, swap, rows[0], 0, mx)
602     fill_row(source_bitmap, swap, rows[1], 1, mx)
603     fill_row(source_bitmap, swap, rows[2], 2, mx)
604
605     err = 0
606
607     for y in range(height):
608         # Going left to right
609         for x in range(width):
610             pixel_in = rows[0][x] + err
611             pixel_out = pixel_in >= 128
612             out[x] = pixel_out
613
614             err = pixel_in - (255 if pixel_out else 0)
615
616             # Distribute error to neighboring pixels
617             for i in range(count):
618                 x1 = x + terms[i]["dx"]
619                 dy = terms[i]["dy"]
620
621                 rows[dy][x1] = ((terms[i]["dl"] * err) // 256) + rows[dy][x1]
622
623             err = (err * dl) // 256
624
625         write_pixels(dest_bitmap, y, out)
626
627         # Cycle the rows
628         rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0]
629
630         y += 1
631         if y == height:
632             break
633
634         # Fill the next row for future processing
635         fill_row(source_bitmap, swap, rows[2], y + 2, mx)
636
637         # Going right to left
638         for x in range(width - 1, -1, -1):
639             pixel_in = rows[0][x] + err
640             pixel_out = pixel_in >= 128
641             out[x] = pixel_out
642
643             err = pixel_in - (255 if pixel_out else 0)
644
645             # Distribute error to neighboring pixels (in reverse direction)
646             for i in range(count):
647                 x1 = x - terms[i]["dx"]
648                 dy = terms[i]["dy"]
649
650                 rows[dy][x1] = ((terms[i]["dl"] * err) // 256) + rows[dy][x1]
651
652             err = (err * dl) // 256
653
654         write_pixels(dest_bitmap, y, out)
655
656         # Cycle the rows again
657         rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0]
658
659         # Fill the next row for future processing
660         fill_row(source_bitmap, swap, rows[2], y + 3, mx)
661
662
663 def boundary_fill(
664     dest_bitmap: Bitmap,
665     x: int,
666     y: int,
667     fill_color_value: int,
668     replaced_color_value: Optional[int] = None,
669 ):
670     if fill_color_value == replaced_color_value:
671         return
672     if replaced_color_value == -1:
673         replaced_color_value = dest_bitmap[x, y]
674
675     fill_points = []
676     fill_points.append((x, y))
677
678     seen_points = []
679     minx = x
680     miny = y
681     maxx = x
682     maxy = y
683
684     while len(fill_points) > 0:
685         cur_point = fill_points.pop(0)
686         seen_points.append(cur_point)
687         cur_x = cur_point[0]
688         cur_y = cur_point[1]
689
690         cur_point_color = dest_bitmap[cur_x, cur_y]
691         if replaced_color_value is not None and cur_point_color != replaced_color_value:
692             continue
693         if cur_x < minx:
694             minx = cur_x
695         if cur_y < miny:
696             miny = cur_y
697         if cur_x > maxx:
698             maxx = cur_x
699         if cur_y > maxy:
700             maxy = cur_y
701
702         dest_bitmap[cur_x, cur_y] = fill_color_value
703
704         above_point = (cur_x, cur_y - 1)
705         below_point = (cur_x, cur_y + 1)
706         left_point = (cur_x - 1, cur_y)
707         right_point = (cur_x + 1, cur_y)
708
709         if (
710             above_point[1] >= 0
711             and above_point not in seen_points
712             and above_point not in fill_points
713         ):
714             fill_points.append(above_point)
715         if (
716             below_point[1] < dest_bitmap.height
717             and below_point not in seen_points
718             and below_point not in fill_points
719         ):
720             fill_points.append(below_point)
721         if (
722             left_point[0] >= 0
723             and left_point not in seen_points
724             and left_point not in fill_points
725         ):
726             fill_points.append(left_point)
727         if (
728             right_point[0] < dest_bitmap.width
729             and right_point not in seen_points
730             and right_point not in fill_points
731         ):
732             fill_points.append(right_point)