]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - bitmaptools/__init__.py
disable protected access warning for _bits_per_value. disable public methods warning...
[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  # pylint: disable=protected-access
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     # pylint: disable=too-few-public-methods
536
537     Normal = "bitmaptools.BlendMode.Normal"
538     Screen = "bitmaptools.BlendMode.Screen"
539
540
541 def alphablend(
542     dest: Bitmap,
543     source1: Bitmap,
544     source2: Bitmap,
545     colorspace: Colorspace,
546     factor1: float = 0.5,
547     factor2: Optional[float] = None,
548     blendmode: BlendMode = BlendMode.Normal,
549     skip_source1_index: Optional[int] = None,
550     skip_source2_index: Optional[int] = None,
551 ):
552     """Alpha blend the two source bitmaps into the destination.
553
554     It is permitted for the destination bitmap to be one of the two
555     source bitmaps.
556
557     :param bitmap dest_bitmap: Destination bitmap that will be written into
558     :param bitmap source_bitmap_1: The first source bitmap
559     :param bitmap source_bitmap_2: The second source bitmap
560     :param float factor1: The proportion of bitmap 1 to mix in
561     :param float factor2: The proportion of bitmap 2 to mix in.
562       If specified as `None`, ``1-factor1`` is used.  Usually the proportions should sum to 1.
563     :param displayio.Colorspace colorspace: The colorspace of the bitmaps.
564       They must all have the same colorspace.  Only the following colorspaces are permitted:
565       ``L8``, ``RGB565``, ``RGB565_SWAPPED``, ``BGR565`` and ``BGR565_SWAPPED``.
566     :param bitmaptools.BlendMode blendmode: The blend mode to use. Default is Normal.
567     :param int skip_source1_index: Bitmap palette or luminance index in
568       source_bitmap_1 that will not be blended, set to None to blend all pixels
569     :param int skip_source2_index: Bitmap palette or luminance index in
570       source_bitmap_2 that will not be blended, set to None to blend all pixels
571
572     For the L8 colorspace, the bitmaps must have a bits-per-value of 8.
573     For the RGB colorspaces, they must have a bits-per-value of 16."""
574
575     def clamp(val, minval, maxval):
576         return max(minval, min(maxval, val))
577
578     ifactor1 = int(factor1 * 256)
579     ifactor2 = int(factor2 * 256)
580
581     width, height = dest.width, dest.height
582
583     if colorspace == "L8":
584         for y in range(height):
585             for x in range(width):
586                 sp1 = source1[x, y]
587                 sp2 = source2[x, y]
588                 blend_source1 = skip_source1_index is None or sp1 != skip_source1_index
589                 blend_source2 = skip_source2_index is None or sp2 != skip_source2_index
590
591                 if blend_source1 and blend_source2:
592                     sda = sp1 * ifactor1
593                     sca = sp2 * ifactor2
594
595                     if blendmode == BlendMode.Screen:
596                         blend = sca + sda - (sca * sda // 65536)
597                     elif blendmode == BlendMode.Normal:
598                         blend = sca + sda * (256 - ifactor2) // 256
599
600                     denom = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256
601                     pixel = blend // denom
602                 elif blend_source1:
603                     pixel = sp1 * ifactor1 // 256
604                 elif blend_source2:
605                     pixel = sp2 * ifactor2 // 256
606                 else:
607                     pixel = dest[x, y]
608
609                 dest[x, y] = clamp(pixel, 0, 255)
610
611     else:
612         swap = colorspace in ("RGB565_SWAPPED", "BGR565_SWAPPED")
613         r_mask = 0xF800
614         g_mask = 0x07E0
615         b_mask = 0x001F
616
617         for y in range(height):
618             for x in range(width):
619                 sp1 = source1[x, y]
620                 sp2 = source2[x, y]
621
622                 if swap:
623                     sp1 = ((sp1 & 0xFF) << 8) | ((sp1 >> 8) & 0xFF)
624                     sp2 = ((sp2 & 0xFF) << 8) | ((sp2 >> 8) & 0xFF)
625
626                 blend_source1 = skip_source1_index is None or sp1 != skip_source1_index
627                 blend_source2 = skip_source2_index is None or sp2 != skip_source2_index
628
629                 if blend_source1 and blend_source2:
630                     ifactor_blend = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256
631
632                     red_dca = ((sp1 & r_mask) >> 8) * ifactor1
633                     grn_dca = ((sp1 & g_mask) >> 3) * ifactor1
634                     blu_dca = ((sp1 & b_mask) << 3) * ifactor1
635
636                     red_sca = ((sp2 & r_mask) >> 8) * ifactor2
637                     grn_sca = ((sp2 & g_mask) >> 3) * ifactor2
638                     blu_sca = ((sp2 & b_mask) << 3) * ifactor2
639
640                     if blendmode == BlendMode.Screen:
641                         red_blend = red_sca + red_dca - (red_sca * red_dca // 65536)
642                         grn_blend = grn_sca + grn_dca - (grn_sca * grn_dca // 65536)
643                         blu_blend = blu_sca + blu_dca - (blu_sca * blu_dca // 65536)
644                     elif blendmode == BlendMode.Normal:
645                         red_blend = red_sca + red_dca * (256 - ifactor2) // 256
646                         grn_blend = grn_sca + grn_dca * (256 - ifactor2) // 256
647                         blu_blend = blu_sca + blu_dca * (256 - ifactor2) // 256
648
649                     r = ((red_blend // ifactor_blend) << 8) & r_mask
650                     g = ((grn_blend // ifactor_blend) << 3) & g_mask
651                     b = ((blu_blend // ifactor_blend) >> 3) & b_mask
652
653                     pixel = (r & r_mask) | (g & g_mask) | (b & b_mask)
654
655                     if swap:
656                         pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF)
657
658                 elif blend_source1:
659                     r = ((sp1 & r_mask) * ifactor1 // 256) & r_mask
660                     g = ((sp1 & g_mask) * ifactor1 // 256) & g_mask
661                     b = ((sp1 & b_mask) * ifactor1 // 256) & b_mask
662                     pixel = r | g | b
663                 elif blend_source2:
664                     r = ((sp2 & r_mask) * ifactor2 // 256) & r_mask
665                     g = ((sp2 & g_mask) * ifactor2 // 256) & g_mask
666                     b = ((sp2 & b_mask) * ifactor2 // 256) & b_mask
667                     pixel = r | g | b
668                 else:
669                     pixel = dest[x, y]
670
671                 print(f"pixel hex: {hex(pixel)}")
672                 dest[x, y] = pixel
673
674
675 class DitherAlgorithm:
676     """
677     Options for algorithm to use by dither() function.
678     """
679     # pylint: disable=too-few-public-methods
680
681     Atkinson = "bitmaptools.DitherAlgorithm.Atkinson"
682     FloydStenberg = "bitmaptools.DitherAlgorithm.FloydStenberg"
683
684     atkinson = {
685         "count": 4,
686         "mx": 2,
687         "dl": 256 // 8,
688         "terms": [
689             {"dx": 2, "dy": 0, "dl": 256 // 8},
690             {"dx": -1, "dy": 1, "dl": 256 // 8},
691             {"dx": 0, "dy": 1, "dl": 256 // 8},
692             {"dx": 0, "dy": 2, "dl": 256 // 8},
693         ],
694     }
695
696     floyd_stenberg = {
697         "count": 3,
698         "mx": 1,
699         "dl": 7 * 256 // 16,
700         "terms": [
701             {"dx": -1, "dy": 1, "dl": 3 * 256 // 16},
702             {"dx": 0, "dy": 1, "dl": 5 * 256 // 16},
703             {"dx": 1, "dy": 1, "dl": 1 * 256 // 16},
704         ],
705     }
706
707     algorithm_map = {Atkinson: atkinson, FloydStenberg: floyd_stenberg}
708
709
710 def dither(dest_bitmap, source_bitmap, colorspace, algorithm=DitherAlgorithm.Atkinson):
711     """Convert the input image into a 2-level output image using the given dither algorithm.
712
713     :param bitmap dest_bitmap: Destination bitmap.  It must have
714       a value_count of 2 or 65536.  The stored values are 0 and the maximum pixel value.
715     :param bitmap source_bitmap: Source bitmap that contains the
716       graphical region to be dithered.  It must have a value_count of 65536.
717     :param colorspace: The colorspace of the image.  The supported colorspaces
718       are ``RGB565``, ``BGR565``, ``RGB565_SWAPPED``, and ``BGR565_SWAPPED``
719     :param algorithm: The dither algorithm to use, one of the `DitherAlgorithm` values.
720     """
721     SWAP_BYTES = 1 << 0
722     SWAP_RB = 1 << 1
723     height, width = dest_bitmap.width, dest_bitmap.height
724     swap_bytes = colorspace in (Colorspace.RGB565_SWAPPED, Colorspace.BGR565_SWAPPED)
725     swap_rb = colorspace in (Colorspace.BGR565, Colorspace.BGR565_SWAPPED)
726     algorithm_info = DitherAlgorithm.algorithm_map[algorithm]
727     mx = algorithm_info["mx"]
728     count = algorithm_info["count"]
729     terms = algorithm_info["terms"]
730     dl = algorithm_info["dl"]
731
732     swap = 0
733     if swap_bytes:
734         swap |= SWAP_BYTES
735
736     if swap_rb:
737         swap |= SWAP_RB
738
739     print(f"swap: {swap}")
740
741     # Create row data arrays (3 rows with padding on both sides)
742     rowdata = [[0] * (width + 2 * mx) for _ in range(3)]
743     rows = [rowdata[0][mx:], rowdata[1][mx:], rowdata[2][mx:]]
744
745     # Output array for one row at a time (padded to multiple of 32)
746     out = [False] * (((width + 31) // 32) * 32)
747
748     # Helper function to fill a row with luminance data
749     def fill_row(bitmap, swap, luminance_data, y, mx):
750         if y >= bitmap.height:
751             return
752
753         # Zero out padding area
754         for i in range(mx):
755             luminance_data[-mx + i] = 0
756             luminance_data[bitmap.width + i] = 0
757
758         if bitmap._bits_per_value == 8:  # pylint: disable=protected-access
759             for x in range(bitmap.width):
760                 luminance_data[x] = bitmap[x, y]
761         else:
762             for x in range(bitmap.width):
763                 pixel = bitmap[x, y]
764                 if swap & SWAP_BYTES:
765                     # Swap bytes (equivalent to __builtin_bswap16)
766                     pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF)
767
768                 r = (pixel >> 8) & 0xF8
769                 g = (pixel >> 3) & 0xFC
770                 b = (pixel << 3) & 0xF8
771
772                 if swap & SWAP_BYTES:
773                     r, b = b, r
774
775                 # Calculate luminance using same formula as C version
776                 luminance_data[x] = (r * 78 + g * 154 + b * 29) // 256
777
778     # Helper function to write pixels to destination bitmap
779     def write_pixels(bitmap, y, data):
780         if bitmap._bits_per_value == 1:  # pylint: disable=protected-access
781             for i in range(0, bitmap.width, 32):
782                 # Pack 32 bits into an integer
783                 p = 0
784                 for j in range(min(32, bitmap.width - i)):
785                     p = p << 1
786                     if data[i + j]:
787                         p |= 1
788
789                 # Write packed value
790                 for j in range(min(32, bitmap.width - i)):
791                     bitmap[i + j, y] = (p >> (31 - j)) & 1
792         else:
793             for i in range(bitmap.width):
794                 bitmap[i, y] = 65535 if data[i] else 0
795
796     # Fill initial rows
797     fill_row(source_bitmap, swap, rows[0], 0, mx)
798     fill_row(source_bitmap, swap, rows[1], 1, mx)
799     fill_row(source_bitmap, swap, rows[2], 2, mx)
800
801     err = 0
802
803     for y in range(height):
804         # Going left to right
805         for x in range(width):
806             pixel_in = rows[0][x] + err
807             pixel_out = pixel_in >= 128
808             out[x] = pixel_out
809
810             err = pixel_in - (255 if pixel_out else 0)
811
812             # Distribute error to neighboring pixels
813             for i in range(count):
814                 x1 = x + terms[i]["dx"]
815                 dy = terms[i]["dy"]
816
817                 rows[dy][x1] = ((terms[i]["dl"] * err) // 256) + rows[dy][x1]
818
819             err = (err * dl) // 256
820
821         write_pixels(dest_bitmap, y, out)
822
823         # Cycle the rows
824         rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0]
825
826         y += 1
827         if y == height:
828             break
829
830         # Fill the next row for future processing
831         fill_row(source_bitmap, swap, rows[2], y + 2, mx)
832
833         # Going right to left
834         for x in range(width - 1, -1, -1):
835             pixel_in = rows[0][x] + err
836             pixel_out = pixel_in >= 128
837             out[x] = pixel_out
838
839             err = pixel_in - (255 if pixel_out else 0)
840
841             # Distribute error to neighboring pixels (in reverse direction)
842             for i in range(count):
843                 x1 = x - terms[i]["dx"]
844                 dy = terms[i]["dy"]
845
846                 rows[dy][x1] = ((terms[i]["dl"] * err) // 256) + rows[dy][x1]
847
848             err = (err * dl) // 256
849
850         write_pixels(dest_bitmap, y, out)
851
852         # Cycle the rows again
853         rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0]
854
855         # Fill the next row for future processing
856         fill_row(source_bitmap, swap, rows[2], y + 3, mx)
857
858
859 def boundary_fill(
860     dest_bitmap: Bitmap,
861     x: int,
862     y: int,
863     fill_color_value: int,
864     replaced_color_value: Optional[int] = None,
865 ):
866     """Draws the color value into the destination bitmap enclosed
867     area of pixels of the background_value color. Like "Paint Bucket"
868     fill tool.
869
870     :param bitmap dest_bitmap: Destination bitmap that will be written into
871     :param int x: x-pixel position of the first pixel to check and fill if needed
872     :param int y: y-pixel position of the first pixel to check and fill if needed
873     :param int fill_color_value: Bitmap palette index that will be written into the
874            enclosed area in the destination bitmap
875     :param int replaced_color_value: Bitmap palette index that will filled with the
876            value color in the enclosed area in the destination bitmap"""
877     if fill_color_value == replaced_color_value:
878         return
879     if replaced_color_value == -1:
880         replaced_color_value = dest_bitmap[x, y]
881
882     fill_points = []
883     fill_points.append((x, y))
884
885     seen_points = []
886     minx = x
887     miny = y
888     maxx = x
889     maxy = y
890
891     while len(fill_points) > 0:
892         cur_point = fill_points.pop(0)
893         seen_points.append(cur_point)
894         cur_x = cur_point[0]
895         cur_y = cur_point[1]
896
897         cur_point_color = dest_bitmap[cur_x, cur_y]
898         if replaced_color_value is not None and cur_point_color != replaced_color_value:
899             continue
900         if cur_x < minx:
901             minx = cur_x
902         if cur_y < miny:
903             miny = cur_y
904         if cur_x > maxx:
905             maxx = cur_x
906         if cur_y > maxy:
907             maxy = cur_y
908
909         dest_bitmap[cur_x, cur_y] = fill_color_value
910
911         above_point = (cur_x, cur_y - 1)
912         below_point = (cur_x, cur_y + 1)
913         left_point = (cur_x - 1, cur_y)
914         right_point = (cur_x + 1, cur_y)
915
916         if (
917             above_point[1] >= 0
918             and above_point not in seen_points
919             and above_point not in fill_points
920         ):
921             fill_points.append(above_point)
922         if (
923             below_point[1] < dest_bitmap.height
924             and below_point not in seen_points
925             and below_point not in fill_points
926         ):
927             fill_points.append(below_point)
928         if (
929             left_point[0] >= 0
930             and left_point not in seen_points
931             and left_point not in fill_points
932         ):
933             fill_points.append(left_point)
934         if (
935             right_point[0] < dest_bitmap.width
936             and right_point not in seen_points
937             and right_point not in fill_points
938         ):
939             fill_points.append(right_point)