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