]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - bitmaptools/__init__.py
adding docstrings and copyright.
[hackapet/Adafruit_Blinka_Displayio.git] / bitmaptools / __init__.py
1 # SPDX-FileCopyrightText: 2025 Tim Cocks
2 #
3 # SPDX-License-Identifier: MIT
4 """
5 Collection of bitmap manipulation tools
6 """
7
8 import math
9 import struct
10 from typing import Optional, Tuple, BinaryIO
11 import circuitpython_typing
12 from displayio import Bitmap, Colorspace
13
14
15 # pylint: disable=invalid-name, too-many-arguments, too-many-locals, too-many-branches, too-many-statements
16 def fill_region(dest_bitmap: Bitmap, x1: int, y1: int, x2: int, y2: int, value: int):
17     """Draws the color value into the destination bitmap within the
18     rectangular region bounded by (x1,y1) and (x2,y2), exclusive.
19
20     :param bitmap dest_bitmap: Destination bitmap that will be written into
21     :param int x1: x-pixel position of the first corner of the rectangular fill region
22     :param int y1: y-pixel position of the first corner of the rectangular fill region
23     :param int x2: x-pixel position of the second corner of the rectangular fill region (exclusive)
24     :param int y2: y-pixel position of the second corner of the rectangular fill region (exclusive)
25     :param int value: Bitmap palette index that will be written into the rectangular
26            fill region in the destination bitmap"""
27
28     for y in range(y1, y2):
29         for x in range(x1, x2):
30             dest_bitmap[x, y] = value
31
32
33 def draw_line(dest_bitmap: Bitmap, x1: int, y1: int, x2: int, y2: int, value: int):
34     """Draws a line into a bitmap specified two endpoints (x1,y1) and (x2,y2).
35
36     :param bitmap dest_bitmap: Destination bitmap that will be written into
37     :param int x1: x-pixel position of the line's first endpoint
38     :param int y1: y-pixel position of the line's first endpoint
39     :param int x2: x-pixel position of the line's second endpoint
40     :param int y2: y-pixel position of the line's second endpoint
41     :param int value: Bitmap palette index that will be written into the
42         line in the destination bitmap"""
43     dx = abs(x2 - x1)
44     sx = 1 if x1 < x2 else -1
45     dy = -abs(y2 - y1)
46     sy = 1 if y1 < y2 else -1
47     error = dx + dy
48
49     while True:
50         dest_bitmap[x1, y1] = value
51         if x1 == x2 and y1 == y2:
52             break
53         e2 = 2 * error
54         if e2 >= dy:
55             error += dy
56             x1 += sx
57         if e2 <= dx:
58             error += dx
59             y1 += sy
60
61
62 def draw_circle(dest_bitmap: Bitmap, x: int, y: int, radius: int, value: int):
63     """Draws a circle into a bitmap specified using a center (x0,y0) and radius r.
64
65     :param bitmap dest_bitmap: Destination bitmap that will be written into
66     :param int x: x-pixel position of the circle's center
67     :param int y: y-pixel position of the circle's center
68     :param int radius: circle's radius
69     :param int value: Bitmap palette index that will be written into the
70            circle in the destination bitmap"""
71
72     x = max(0, min(x, dest_bitmap.width - 1))
73     y = max(0, min(y, dest_bitmap.height - 1))
74
75     xb = 0
76     yb = radius
77     d = 3 - 2 * radius
78
79     # Bresenham's circle algorithm
80     while xb <= yb:
81         dest_bitmap[xb + x, yb + y] = value
82         dest_bitmap[-xb + x, -yb + y] = value
83         dest_bitmap[-xb + x, yb + y] = value
84         dest_bitmap[xb + x, -yb + y] = value
85         dest_bitmap[yb + x, xb + y] = value
86         dest_bitmap[-yb + x, xb + y] = value
87         dest_bitmap[-yb + x, -xb + y] = value
88         dest_bitmap[yb + x, -xb + y] = value
89
90         if d <= 0:
91             d = d + (4 * xb) + 6
92         else:
93             d = d + 4 * (xb - yb) + 10
94             yb = yb - 1
95         xb += 1
96
97
98 def draw_polygon(
99     dest_bitmap: Bitmap,
100     xs: circuitpython_typing.ReadableBuffer,
101     ys: circuitpython_typing.ReadableBuffer,
102     value: int,
103     close: bool = True,
104 ):
105     """Draw a polygon connecting points on provided bitmap with provided value
106
107     :param bitmap dest_bitmap: Destination bitmap that will be written into
108     :param ReadableBuffer xs: x-pixel position of the polygon's vertices
109     :param ReadableBuffer ys: y-pixel position of the polygon's vertices
110     :param int value: Bitmap palette index that will be written into the
111            line in the destination bitmap
112     :param bool close: (Optional) Whether to connect first and last point. (True)
113     """
114     if len(xs) != len(ys):
115         raise ValueError("Length of xs and ys must be equal.")
116
117     for i in range(len(xs) - 1):
118         cur_point = (xs[i], ys[i])
119         next_point = (xs[i + 1], ys[i + 1])
120         print(f"cur: {cur_point}, next: {next_point}")
121         draw_line(
122             dest_bitmap=dest_bitmap,
123             x1=cur_point[0],
124             y1=cur_point[1],
125             x2=next_point[0],
126             y2=next_point[1],
127             value=value,
128         )
129
130     if close:
131         print(f"close: {(xs[0], ys[0])} - {(xs[-1], ys[-1])}")
132         draw_line(
133             dest_bitmap=dest_bitmap,
134             x1=xs[0],
135             y1=ys[0],
136             x2=xs[-1],
137             y2=ys[-1],
138             value=value,
139         )
140
141
142 def blit(
143     dest_bitmap: Bitmap,
144     source_bitmap: Bitmap,
145     x: int,
146     y: int,
147     *,
148     x1: int = 0,
149     y1: int = 0,
150     x2: int | None = None,
151     y2: int | None = None,
152     skip_source_index: int | None = None,
153     skip_dest_index: int | None = None,
154 ):
155     """Inserts the source_bitmap region defined by rectangular boundaries
156     (x1,y1) and (x2,y2) into the bitmap at the specified (x,y) location.
157
158     :param bitmap dest_bitmap: Destination bitmap that the area will
159      be copied into.
160     :param bitmap source_bitmap: Source bitmap that contains the graphical
161      region to be copied
162     :param int x: Horizontal pixel location in bitmap where source_bitmap
163                   upper-left corner will be placed
164     :param int y: Vertical pixel location in bitmap where source_bitmap upper-left
165                   corner will be placed
166     :param int x1: Minimum x-value for rectangular bounding box to be
167      copied from the source bitmap
168     :param int y1: Minimum y-value for rectangular bounding box to be
169      copied from the source bitmap
170     :param int x2: Maximum x-value (exclusive) for rectangular
171      bounding box to be copied from the source bitmap.
172      If unspecified or `None`, the source bitmap width is used.
173     :param int y2: Maximum y-value (exclusive) for rectangular
174      bounding box to be copied from the source bitmap.
175      If unspecified or `None`, the source bitmap height is used.
176     :param int skip_source_index: bitmap palette index in the source that
177      will not be copied, set to None to copy all pixels
178     :param int skip_dest_index: bitmap palette index in the
179      destination bitmap that will not get overwritten by the
180      pixels from the source"""
181
182     # pylint: disable=invalid-name
183     if x2 is None:
184         x2 = source_bitmap.width
185     if y2 is None:
186         y2 = source_bitmap.height
187
188     # Rearrange so that x1 < x2 and y1 < y2
189     if x1 > x2:
190         x1, x2 = x2, x1
191     if y1 > y2:
192         y1, y2 = y2, y1
193
194     # Ensure that x2 and y2 are within source bitmap size
195     x2 = min(x2, source_bitmap.width)
196     y2 = min(y2, source_bitmap.height)
197
198     for y_count in range(y2 - y1):
199         for x_count in range(x2 - x1):
200             x_placement = x + x_count
201             y_placement = y + y_count
202
203             if (dest_bitmap.width > x_placement >= 0) and (
204                 dest_bitmap.height > y_placement >= 0
205             ):  # ensure placement is within target bitmap
206                 # get the palette index from the source bitmap
207                 this_pixel_color = source_bitmap[
208                     y1 + (y_count * source_bitmap.width) + x1 + x_count
209                 ]
210
211                 if (skip_source_index is None) or (
212                     this_pixel_color != skip_source_index
213                 ):
214                     if (skip_dest_index is None) or (
215                         dest_bitmap[y_placement * dest_bitmap.width + x_placement]
216                         != skip_dest_index
217                     ):
218                         dest_bitmap[
219                             y_placement * dest_bitmap.width + x_placement
220                         ] = this_pixel_color
221             elif y_placement > dest_bitmap.height:
222                 break
223
224
225 def rotozoom(
226     dest_bitmap: Bitmap,
227     source_bitmap: Bitmap,
228     *,
229     ox: Optional[int] = None,
230     oy: Optional[int] = None,
231     dest_clip0: Optional[Tuple[int, int]] = None,
232     dest_clip1: Optional[Tuple[int, int]] = None,
233     px: Optional[int] = None,
234     py: Optional[int] = None,
235     source_clip0: Optional[Tuple[int, int]] = None,
236     source_clip1: Optional[Tuple[int, int]] = None,
237     angle: Optional[float] = None,
238     scale: Optional[float] = None,
239     skip_index: Optional[int] = None,
240 ):
241     """Inserts the source bitmap region into the destination bitmap with rotation
242     (angle), scale and clipping (both on source and destination bitmaps).
243
244     :param bitmap dest_bitmap: Destination bitmap that will be copied into
245     :param bitmap source_bitmap: Source bitmap that contains the graphical region to be copied
246     :param int ox: Horizontal pixel location in destination bitmap where source bitmap
247            point (px,py) is placed. Defaults to None which causes it to use the horizontal
248            midway point of the destination bitmap.
249     :param int oy: Vertical pixel location in destination bitmap where source bitmap
250            point (px,py) is placed. Defaults to None which causes it to use the vertical
251            midway point of the destination bitmap.
252     :param Tuple[int,int] dest_clip0: First corner of rectangular destination clipping
253            region that constrains region of writing into destination bitmap
254     :param Tuple[int,int] dest_clip1: Second corner of rectangular destination clipping
255            region that constrains region of writing into destination bitmap
256     :param int px: Horizontal pixel location in source bitmap that is placed into the
257            destination bitmap at (ox,oy). Defaults to None which causes it to use the
258            horizontal midway point in the source bitmap.
259     :param int py: Vertical pixel location in source bitmap that is placed into the
260            destination bitmap at (ox,oy). Defaults to None which causes it to use the
261            vertical midway point in the source bitmap.
262     :param Tuple[int,int] source_clip0: First corner of rectangular source clipping
263            region that constrains region of reading from the source bitmap
264     :param Tuple[int,int] source_clip1: Second corner of rectangular source clipping
265            region that constrains region of reading from the source bitmap
266     :param float angle: Angle of rotation, in radians (positive is clockwise direction).
267            Defaults to None which gets treated as 0.0 radians or no rotation.
268     :param float scale: Scaling factor. Defaults to None which gets treated as 1.0 or same
269            as original source size.
270     :param int skip_index: Bitmap palette index in the source that will not be copied,
271            set to None to copy all pixels"""
272     if ox is None:
273         ox = dest_bitmap.width // 2
274     if oy is None:
275         oy = dest_bitmap.height // 2
276
277     if dest_clip0 is None:
278         dest_clip0 = (0, 0)
279     if dest_clip1 is None:
280         dest_clip1 = (dest_bitmap.width, dest_bitmap.height)
281
282     if px is None:
283         px = source_bitmap.width // 2
284     if py is None:
285         py = source_bitmap.height // 2
286
287     if source_clip0 is None:
288         source_clip0 = (0, 0)
289     if source_clip1 is None:
290         source_clip1 = (source_bitmap.width, source_bitmap.height)
291
292     if angle is None:
293         angle = 0.0
294     if scale is None:
295         scale = 1.0
296
297     dest_clip0_x, dest_clip0_y = dest_clip0
298     dest_clip1_x, dest_clip1_y = dest_clip1
299     source_clip0_x, source_clip0_y = source_clip0
300     source_clip1_x, source_clip1_y = source_clip1
301
302     minx = dest_clip1_x
303     miny = dest_clip1_y
304     maxx = dest_clip0_x
305     maxy = dest_clip0_y
306
307     sin_angle = math.sin(angle)
308     cos_angle = math.cos(angle)
309
310     def update_bounds(dx, dy):
311         nonlocal minx, maxx, miny, maxy
312         if dx < minx:
313             minx = int(dx)
314         if dx > maxx:
315             maxx = int(dx)
316         if dy < miny:
317             miny = int(dy)
318         if dy > maxy:
319             maxy = int(dy)
320
321     w = source_bitmap.width
322     h = source_bitmap.height
323
324     dx = -cos_angle * px * scale + sin_angle * py * scale + ox
325     dy = -sin_angle * px * scale - cos_angle * py * scale + oy
326     update_bounds(dx, dy)
327
328     dx = cos_angle * (w - px) * scale + sin_angle * py * scale + ox
329     dy = sin_angle * (w - px) * scale - cos_angle * py * scale + oy
330     update_bounds(dx, dy)
331
332     dx = cos_angle * (w - px) * scale - sin_angle * (h - py) * scale + ox
333     dy = sin_angle * (w - px) * scale + cos_angle * (h - py) * scale + oy
334     update_bounds(dx, dy)
335
336     dx = -cos_angle * px * scale - sin_angle * (h - py) * scale + ox
337     dy = -sin_angle * px * scale + cos_angle * (h - py) * scale + oy
338     update_bounds(dx, dy)
339
340     # Clip to destination area
341     minx = max(minx, dest_clip0_x)
342     maxx = min(maxx, dest_clip1_x - 1)
343     miny = max(miny, dest_clip0_y)
344     maxy = min(maxy, dest_clip1_y - 1)
345
346     dv_col = cos_angle / scale
347     du_col = sin_angle / scale
348     du_row = dv_col
349     dv_row = -du_col
350
351     startu = px - (ox * dv_col + oy * du_col)
352     startv = py - (ox * dv_row + oy * du_row)
353
354     rowu = startu + miny * du_col
355     rowv = startv + miny * dv_col
356
357     for y in range(miny, maxy + 1):
358         u = rowu + minx * du_row
359         v = rowv + minx * dv_row
360         for x in range(minx, maxx + 1):
361             if (source_clip0_x <= u < source_clip1_x) and (
362                 source_clip0_y <= v < source_clip1_y
363             ):
364                 c = source_bitmap[int(u), int(v)]
365                 if skip_index is None or c != skip_index:
366                     dest_bitmap[x, y] = c
367             u += du_row
368             v += dv_row
369         rowu += du_col
370         rowv += dv_col
371
372
373 def arrayblit(
374     bitmap: Bitmap,
375     data: circuitpython_typing.ReadableBuffer,
376     x1: int = 0,
377     y1: int = 0,
378     x2: Optional[int] = None,
379     y2: Optional[int] = None,
380     skip_index: Optional[int] = None,
381 ):
382     """Inserts pixels from ``data`` into the rectangle
383      of width×height pixels with the upper left corner at ``(x,y)``
384
385     The values from ``data`` are taken modulo the number of color values
386     available in the destination bitmap.
387
388     If x1 or y1 are not specified, they are taken as 0.  If x2 or y2
389     are not specified, or are given as None, they are taken as the width
390     and height of the image.
391
392     The coordinates affected by the blit are
393     ``x1 <= x < x2`` and ``y1 <= y < y2``.
394
395     ``data`` must contain at least as many elements as required.  If it
396     contains excess elements, they are ignored.
397
398     The blit takes place by rows, so the first elements of ``data`` go
399     to the first row, the next elements to the next row, and so on.
400
401     :param displayio.Bitmap bitmap: A writable bitmap
402     :param ReadableBuffer data: Buffer containing the source pixel values
403     :param int x1: The left corner of the area to blit into (inclusive)
404     :param int y1: The top corner of the area to blit into (inclusive)
405     :param int x2: The right of the area to blit into (exclusive)
406     :param int y2: The bottom corner of the area to blit into (exclusive)
407     :param int skip_index: Bitmap palette index in the source
408      that will not be copied, set to None to copy all pixels
409
410     """
411     if x2 is None:
412         x2 = bitmap.width
413     if y2 is None:
414         y2 = bitmap.height
415
416     _value_count = 2**bitmap._bits_per_value  # pylint: disable=protected-access
417     for y in range(y1, y2):
418         for x in range(x1, x2):
419             i = y * (x2 - x1) + x
420             value = int(data[i] % _value_count)
421             if skip_index is None or value != skip_index:
422                 bitmap[x, y] = value
423
424
425 def readinto(
426     bitmap: Bitmap,
427     file: BinaryIO,
428     bits_per_pixel: int,
429     element_size: int = 1,
430     reverse_pixels_in_element: bool = False,
431     swap_bytes: bool = False,
432     reverse_rows: bool = False,
433 ):
434     """Reads from a binary file into a bitmap.
435
436     The file must be positioned so that it consists of ``bitmap.height``
437     rows of pixel data, where each row is the smallest multiple
438     of ``element_size`` bytes that can hold ``bitmap.width`` pixels.
439
440     The bytes in an element can be optionally swapped, and the pixels
441     in an element can be reversed.  Also, therow loading direction can
442      be reversed, which may be requires for loading certain bitmap files.
443
444     This function doesn't parse image headers, but is useful to
445     speed up loading of uncompressed image formats such as PCF glyph data.
446
447     :param displayio.Bitmap bitmap: A writable bitmap
448     :param typing.BinaryIO file: A file opened in binary mode
449     :param int bits_per_pixel: Number of bits per pixel.
450      Values 1, 2, 4, 8, 16, 24, and 32 are supported;
451     :param int element_size: Number of bytes per element.
452      Values of 1, 2, and 4 are supported, except that 24
453      ``bits_per_pixel`` requires 1 byte per element.
454     :param bool reverse_pixels_in_element: If set, the first pixel in a
455       word is taken from the Most Significant Bits; otherwise,
456       it is taken from the Least Significant Bits.
457     :param bool swap_bytes_in_element: If the ``element_size`` is not 1,
458      then reverse the byte order of each element read.
459     :param bool reverse_rows: Reverse the direction of the row loading
460      (required for some bitmap images).
461     """
462     width = bitmap.width
463     height = bitmap.height
464     bits_per_value = bitmap._bits_per_value
465     mask = (1 << bits_per_value) - 1
466
467     elements_per_row = (width * bits_per_pixel + element_size * 8 - 1) // (
468         element_size * 8
469     )
470     rowsize = element_size * elements_per_row
471
472     for y in range(height):
473         row_bytes = file.read(rowsize)
474         if len(row_bytes) != rowsize:
475             raise EOFError()
476
477         # Convert the raw bytes into the appropriate type array for processing
478         rowdata = bytearray(row_bytes)
479
480         if swap_bytes:
481             if element_size == 2:
482                 rowdata = bytearray(
483                     b"".join(
484                         struct.pack("<H", struct.unpack(">H", rowdata[i : i + 2])[0])
485                         for i in range(0, len(rowdata), 2)
486                     )
487                 )
488             elif element_size == 4:
489                 rowdata = bytearray(
490                     b"".join(
491                         struct.pack("<I", struct.unpack(">I", rowdata[i : i + 4])[0])
492                         for i in range(0, len(rowdata), 4)
493                     )
494                 )
495
496         y_draw = height - 1 - y if reverse_rows else y
497
498         for x in range(width):
499             value = 0
500             if bits_per_pixel == 1:
501                 byte_offset = x // 8
502                 bit_offset = 7 - (x % 8) if reverse_pixels_in_element else x % 8
503                 value = (rowdata[byte_offset] >> bit_offset) & 0x1
504             elif bits_per_pixel == 2:
505                 byte_offset = x // 4
506                 bit_index = 3 - (x % 4) if reverse_pixels_in_element else x % 4
507                 bit_offset = 2 * bit_index
508                 value = (rowdata[byte_offset] >> bit_offset) & 0x3
509             elif bits_per_pixel == 4:
510                 byte_offset = x // 2
511                 bit_index = 1 - (x % 2) if reverse_pixels_in_element else x % 2
512                 bit_offset = 4 * bit_index
513                 value = (rowdata[byte_offset] >> bit_offset) & 0xF
514             elif bits_per_pixel == 8:
515                 value = rowdata[x]
516             elif bits_per_pixel == 16:
517                 value = struct.unpack_from("<H", rowdata, x * 2)[0]
518             elif bits_per_pixel == 24:
519                 offset = x * 3
520                 value = (
521                     (rowdata[offset] << 16)
522                     | (rowdata[offset + 1] << 8)
523                     | rowdata[offset + 2]
524                 )
525             elif bits_per_pixel == 32:
526                 value = struct.unpack_from("<I", rowdata, x * 4)[0]
527
528             bitmap[x, y_draw] = value & mask
529
530
531 class BlendMode:
532     """
533     Options for modes to use by alphablend() function.
534     """
535
536     Normal = "bitmaptools.BlendMode.Normal"
537     Screen = "bitmaptools.BlendMode.Screen"
538
539
540 def alphablend(
541     dest: Bitmap,
542     source1: Bitmap,
543     source2: Bitmap,
544     colorspace: Colorspace,
545     factor1: float = 0.5,
546     factor2: Optional[float] = None,
547     blendmode: BlendMode = BlendMode.Normal,
548     skip_source1_index: Optional[int] = None,
549     skip_source2_index: Optional[int] = None,
550 ):
551     """Alpha blend the two source bitmaps into the destination.
552
553     It is permitted for the destination bitmap to be one of the two
554     source bitmaps.
555
556     :param bitmap dest_bitmap: Destination bitmap that will be written into
557     :param bitmap source_bitmap_1: The first source bitmap
558     :param bitmap source_bitmap_2: The second source bitmap
559     :param float factor1: The proportion of bitmap 1 to mix in
560     :param float factor2: The proportion of bitmap 2 to mix in.
561       If specified as `None`, ``1-factor1`` is used.  Usually the proportions should sum to 1.
562     :param displayio.Colorspace colorspace: The colorspace of the bitmaps.
563       They must all have the same colorspace.  Only the following colorspaces are permitted:
564       ``L8``, ``RGB565``, ``RGB565_SWAPPED``, ``BGR565`` and ``BGR565_SWAPPED``.
565     :param bitmaptools.BlendMode blendmode: The blend mode to use. Default is Normal.
566     :param int skip_source1_index: Bitmap palette or luminance index in
567       source_bitmap_1 that will not be blended, set to None to blend all pixels
568     :param int skip_source2_index: Bitmap palette or luminance index in
569       source_bitmap_2 that will not be blended, set to None to blend all pixels
570
571     For the L8 colorspace, the bitmaps must have a bits-per-value of 8.
572     For the RGB colorspaces, they must have a bits-per-value of 16."""
573
574     def clamp(val, minval, maxval):
575         return max(minval, min(maxval, val))
576
577     ifactor1 = int(factor1 * 256)
578     ifactor2 = int(factor2 * 256)
579
580     width, height = dest.width, dest.height
581
582     if colorspace == "L8":
583         for y in range(height):
584             for x in range(width):
585                 sp1 = source1[x, y]
586                 sp2 = source2[x, y]
587                 blend_source1 = skip_source1_index is None or sp1 != skip_source1_index
588                 blend_source2 = skip_source2_index is None or sp2 != skip_source2_index
589
590                 if blend_source1 and blend_source2:
591                     sda = sp1 * ifactor1
592                     sca = sp2 * ifactor2
593
594                     if blendmode == BlendMode.Screen:
595                         blend = sca + sda - (sca * sda // 65536)
596                     elif blendmode == BlendMode.Normal:
597                         blend = sca + sda * (256 - ifactor2) // 256
598
599                     denom = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256
600                     pixel = blend // denom
601                 elif blend_source1:
602                     pixel = sp1 * ifactor1 // 256
603                 elif blend_source2:
604                     pixel = sp2 * ifactor2 // 256
605                 else:
606                     pixel = dest[x, y]
607
608                 dest[x, y] = clamp(pixel, 0, 255)
609
610     else:
611         swap = colorspace in ("RGB565_SWAPPED", "BGR565_SWAPPED")
612         r_mask = 0xF800
613         g_mask = 0x07E0
614         b_mask = 0x001F
615
616         for y in range(height):
617             for x in range(width):
618                 sp1 = source1[x, y]
619                 sp2 = source2[x, y]
620
621                 if swap:
622                     sp1 = ((sp1 & 0xFF) << 8) | ((sp1 >> 8) & 0xFF)
623                     sp2 = ((sp2 & 0xFF) << 8) | ((sp2 >> 8) & 0xFF)
624
625                 blend_source1 = skip_source1_index is None or sp1 != skip_source1_index
626                 blend_source2 = skip_source2_index is None or sp2 != skip_source2_index
627
628                 if blend_source1 and blend_source2:
629                     ifactor_blend = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256
630
631                     red_dca = ((sp1 & r_mask) >> 8) * ifactor1
632                     grn_dca = ((sp1 & g_mask) >> 3) * ifactor1
633                     blu_dca = ((sp1 & b_mask) << 3) * ifactor1
634
635                     red_sca = ((sp2 & r_mask) >> 8) * ifactor2
636                     grn_sca = ((sp2 & g_mask) >> 3) * ifactor2
637                     blu_sca = ((sp2 & b_mask) << 3) * ifactor2
638
639                     if blendmode == BlendMode.Screen:
640                         red_blend = red_sca + red_dca - (red_sca * red_dca // 65536)
641                         grn_blend = grn_sca + grn_dca - (grn_sca * grn_dca // 65536)
642                         blu_blend = blu_sca + blu_dca - (blu_sca * blu_dca // 65536)
643                     elif blendmode == BlendMode.Normal:
644                         red_blend = red_sca + red_dca * (256 - ifactor2) // 256
645                         grn_blend = grn_sca + grn_dca * (256 - ifactor2) // 256
646                         blu_blend = blu_sca + blu_dca * (256 - ifactor2) // 256
647
648                     r = ((red_blend // ifactor_blend) << 8) & r_mask
649                     g = ((grn_blend // ifactor_blend) << 3) & g_mask
650                     b = ((blu_blend // ifactor_blend) >> 3) & b_mask
651
652                     pixel = (r & r_mask) | (g & g_mask) | (b & b_mask)
653
654                     if swap:
655                         pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF)
656
657                 elif blend_source1:
658                     r = ((sp1 & r_mask) * ifactor1 // 256) & r_mask
659                     g = ((sp1 & g_mask) * ifactor1 // 256) & g_mask
660                     b = ((sp1 & b_mask) * ifactor1 // 256) & b_mask
661                     pixel = r | g | b
662                 elif blend_source2:
663                     r = ((sp2 & r_mask) * ifactor2 // 256) & r_mask
664                     g = ((sp2 & g_mask) * ifactor2 // 256) & g_mask
665                     b = ((sp2 & b_mask) * ifactor2 // 256) & b_mask
666                     pixel = r | g | b
667                 else:
668                     pixel = dest[x, y]
669
670                 print(f"pixel hex: {hex(pixel)}")
671                 dest[x, y] = pixel
672
673
674 class DitherAlgorithm:
675     """
676     Options for algorithm to use by dither() function.
677     """
678
679     Atkinson = "bitmaptools.DitherAlgorithm.Atkinson"
680     FloydStenberg = "bitmaptools.DitherAlgorithm.FloydStenberg"
681
682     atkinson = {
683         "count": 4,
684         "mx": 2,
685         "dl": 256 // 8,
686         "terms": [
687             {"dx": 2, "dy": 0, "dl": 256 // 8},
688             {"dx": -1, "dy": 1, "dl": 256 // 8},
689             {"dx": 0, "dy": 1, "dl": 256 // 8},
690             {"dx": 0, "dy": 2, "dl": 256 // 8},
691         ],
692     }
693
694     floyd_stenberg = {
695         "count": 3,
696         "mx": 1,
697         "dl": 7 * 256 // 16,
698         "terms": [
699             {"dx": -1, "dy": 1, "dl": 3 * 256 // 16},
700             {"dx": 0, "dy": 1, "dl": 5 * 256 // 16},
701             {"dx": 1, "dy": 1, "dl": 1 * 256 // 16},
702         ],
703     }
704
705     algorithm_map = {Atkinson: atkinson, FloydStenberg: floyd_stenberg}
706
707
708 def dither(dest_bitmap, source_bitmap, colorspace, algorithm=DitherAlgorithm.Atkinson):
709     """Convert the input image into a 2-level output image using the given dither algorithm.
710
711     :param bitmap dest_bitmap: Destination bitmap.  It must have
712       a value_count of 2 or 65536.  The stored values are 0 and the maximum pixel value.
713     :param bitmap source_bitmap: Source bitmap that contains the
714       graphical region to be dithered.  It must have a value_count of 65536.
715     :param colorspace: The colorspace of the image.  The supported colorspaces
716       are ``RGB565``, ``BGR565``, ``RGB565_SWAPPED``, and ``BGR565_SWAPPED``
717     :param algorithm: The dither algorithm to use, one of the `DitherAlgorithm` values.
718     """
719     SWAP_BYTES = 1 << 0
720     SWAP_RB = 1 << 1
721     height, width = dest_bitmap.width, dest_bitmap.height
722     swap_bytes = colorspace in (Colorspace.RGB565_SWAPPED, Colorspace.BGR565_SWAPPED)
723     swap_rb = colorspace in (Colorspace.BGR565, Colorspace.BGR565_SWAPPED)
724     algorithm_info = DitherAlgorithm.algorithm_map[algorithm]
725     mx = algorithm_info["mx"]
726     count = algorithm_info["count"]
727     terms = algorithm_info["terms"]
728     dl = algorithm_info["dl"]
729
730     swap = 0
731     if swap_bytes:
732         swap |= SWAP_BYTES
733
734     if swap_rb:
735         swap |= SWAP_RB
736
737     print(f"swap: {swap}")
738
739     # Create row data arrays (3 rows with padding on both sides)
740     rowdata = [[0] * (width + 2 * mx) for _ in range(3)]
741     rows = [rowdata[0][mx:], rowdata[1][mx:], rowdata[2][mx:]]
742
743     # Output array for one row at a time (padded to multiple of 32)
744     out = [False] * (((width + 31) // 32) * 32)
745
746     # Helper function to fill a row with luminance data
747     def fill_row(bitmap, swap, luminance_data, y, mx):
748         if y >= bitmap.height:
749             return
750
751         # Zero out padding area
752         for i in range(mx):
753             luminance_data[-mx + i] = 0
754             luminance_data[bitmap.width + i] = 0
755
756         if bitmap._bits_per_value == 8:
757             for x in range(bitmap.width):
758                 luminance_data[x] = bitmap[x, y]
759         else:
760             for x in range(bitmap.width):
761                 pixel = bitmap[x, y]
762                 if swap & SWAP_BYTES:
763                     # Swap bytes (equivalent to __builtin_bswap16)
764                     pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF)
765
766                 r = (pixel >> 8) & 0xF8
767                 g = (pixel >> 3) & 0xFC
768                 b = (pixel << 3) & 0xF8
769
770                 if swap & SWAP_BYTES:
771                     r, b = b, r
772
773                 # Calculate luminance using same formula as C version
774                 luminance_data[x] = (r * 78 + g * 154 + b * 29) // 256
775
776     # Helper function to write pixels to destination bitmap
777     def write_pixels(bitmap, y, data):
778         if bitmap._bits_per_value == 1:
779             for i in range(0, bitmap.width, 32):
780                 # Pack 32 bits into an integer
781                 p = 0
782                 for j in range(min(32, bitmap.width - i)):
783                     p = p << 1
784                     if data[i + j]:
785                         p |= 1
786
787                 # Write packed value
788                 for j in range(min(32, bitmap.width - i)):
789                     bitmap[i + j, y] = (p >> (31 - j)) & 1
790         else:
791             for i in range(bitmap.width):
792                 bitmap[i, y] = 65535 if data[i] else 0
793
794     # Fill initial rows
795     fill_row(source_bitmap, swap, rows[0], 0, mx)
796     fill_row(source_bitmap, swap, rows[1], 1, mx)
797     fill_row(source_bitmap, swap, rows[2], 2, mx)
798
799     err = 0
800
801     for y in range(height):
802         # Going left to right
803         for x in range(width):
804             pixel_in = rows[0][x] + err
805             pixel_out = pixel_in >= 128
806             out[x] = pixel_out
807
808             err = pixel_in - (255 if pixel_out else 0)
809
810             # Distribute error to neighboring pixels
811             for i in range(count):
812                 x1 = x + terms[i]["dx"]
813                 dy = terms[i]["dy"]
814
815                 rows[dy][x1] = ((terms[i]["dl"] * err) // 256) + rows[dy][x1]
816
817             err = (err * dl) // 256
818
819         write_pixels(dest_bitmap, y, out)
820
821         # Cycle the rows
822         rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0]
823
824         y += 1
825         if y == height:
826             break
827
828         # Fill the next row for future processing
829         fill_row(source_bitmap, swap, rows[2], y + 2, mx)
830
831         # Going right to left
832         for x in range(width - 1, -1, -1):
833             pixel_in = rows[0][x] + err
834             pixel_out = pixel_in >= 128
835             out[x] = pixel_out
836
837             err = pixel_in - (255 if pixel_out else 0)
838
839             # Distribute error to neighboring pixels (in reverse direction)
840             for i in range(count):
841                 x1 = x - terms[i]["dx"]
842                 dy = terms[i]["dy"]
843
844                 rows[dy][x1] = ((terms[i]["dl"] * err) // 256) + rows[dy][x1]
845
846             err = (err * dl) // 256
847
848         write_pixels(dest_bitmap, y, out)
849
850         # Cycle the rows again
851         rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0]
852
853         # Fill the next row for future processing
854         fill_row(source_bitmap, swap, rows[2], y + 3, mx)
855
856
857 def boundary_fill(
858     dest_bitmap: Bitmap,
859     x: int,
860     y: int,
861     fill_color_value: int,
862     replaced_color_value: Optional[int] = None,
863 ):
864     """Draws the color value into the destination bitmap enclosed
865     area of pixels of the background_value color. Like "Paint Bucket"
866     fill tool.
867
868     :param bitmap dest_bitmap: Destination bitmap that will be written into
869     :param int x: x-pixel position of the first pixel to check and fill if needed
870     :param int y: y-pixel position of the first pixel to check and fill if needed
871     :param int fill_color_value: Bitmap palette index that will be written into the
872            enclosed area in the destination bitmap
873     :param int replaced_color_value: Bitmap palette index that will filled with the
874            value color in the enclosed area in the destination bitmap"""
875     if fill_color_value == replaced_color_value:
876         return
877     if replaced_color_value == -1:
878         replaced_color_value = dest_bitmap[x, y]
879
880     fill_points = []
881     fill_points.append((x, y))
882
883     seen_points = []
884     minx = x
885     miny = y
886     maxx = x
887     maxy = y
888
889     while len(fill_points) > 0:
890         cur_point = fill_points.pop(0)
891         seen_points.append(cur_point)
892         cur_x = cur_point[0]
893         cur_y = cur_point[1]
894
895         cur_point_color = dest_bitmap[cur_x, cur_y]
896         if replaced_color_value is not None and cur_point_color != replaced_color_value:
897             continue
898         if cur_x < minx:
899             minx = cur_x
900         if cur_y < miny:
901             miny = cur_y
902         if cur_x > maxx:
903             maxx = cur_x
904         if cur_y > maxy:
905             maxy = cur_y
906
907         dest_bitmap[cur_x, cur_y] = fill_color_value
908
909         above_point = (cur_x, cur_y - 1)
910         below_point = (cur_x, cur_y + 1)
911         left_point = (cur_x - 1, cur_y)
912         right_point = (cur_x + 1, cur_y)
913
914         if (
915             above_point[1] >= 0
916             and above_point not in seen_points
917             and above_point not in fill_points
918         ):
919             fill_points.append(above_point)
920         if (
921             below_point[1] < dest_bitmap.height
922             and below_point not in seen_points
923             and below_point not in fill_points
924         ):
925             fill_points.append(below_point)
926         if (
927             left_point[0] >= 0
928             and left_point not in seen_points
929             and left_point not in fill_points
930         ):
931             fill_points.append(left_point)
932         if (
933             right_point[0] < dest_bitmap.width
934             and right_point not in seen_points
935             and right_point not in fill_points
936         ):
937             fill_points.append(right_point)