1 # SPDX-FileCopyrightText: 2025 Tim Cocks
3 # SPDX-License-Identifier: MIT
5 Collection of bitmap manipulation tools
10 from typing import Optional, Tuple, BinaryIO
11 import circuitpython_typing
12 from displayio import Bitmap, Colorspace
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.
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"""
28 for y in range(y1, y2):
29 for x in range(x1, x2):
30 dest_bitmap[x, y] = value
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).
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"""
44 sx = 1 if x1 < x2 else -1
46 sy = 1 if y1 < y2 else -1
50 dest_bitmap[x1, y1] = value
51 if x1 == x2 and y1 == y2:
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.
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"""
72 x = max(0, min(x, dest_bitmap.width - 1))
73 y = max(0, min(y, dest_bitmap.height - 1))
79 # Bresenham's circle algorithm
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
93 d = d + 4 * (xb - yb) + 10
100 xs: circuitpython_typing.ReadableBuffer,
101 ys: circuitpython_typing.ReadableBuffer,
105 """Draw a polygon connecting points on provided bitmap with provided value
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)
114 if len(xs) != len(ys):
115 raise ValueError("Length of xs and ys must be equal.")
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}")
122 dest_bitmap=dest_bitmap,
131 print(f"close: {(xs[0], ys[0])} - {(xs[-1], ys[-1])}")
133 dest_bitmap=dest_bitmap,
144 source_bitmap: Bitmap,
150 x2: Optional[int] = None,
151 y2: Optional[int] = None,
152 skip_source_index: Optional[int] = None,
153 skip_dest_index: Optional[int] = None,
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.
158 :param bitmap dest_bitmap: Destination bitmap that the area will
160 :param bitmap source_bitmap: Source bitmap that contains the graphical
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"""
182 # pylint: disable=invalid-name
184 x2 = source_bitmap.width
186 y2 = source_bitmap.height
188 # Rearrange so that x1 < x2 and y1 < y2
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)
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
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
211 if (skip_source_index is None) or (
212 this_pixel_color != skip_source_index
214 if (skip_dest_index is None) or (
215 dest_bitmap[y_placement * dest_bitmap.width + x_placement]
219 y_placement * dest_bitmap.width + x_placement
221 elif y_placement > dest_bitmap.height:
227 source_bitmap: Bitmap,
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,
241 """Inserts the source bitmap region into the destination bitmap with rotation
242 (angle), scale and clipping (both on source and destination bitmaps).
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"""
273 ox = dest_bitmap.width // 2
275 oy = dest_bitmap.height // 2
277 if dest_clip0 is None:
279 if dest_clip1 is None:
280 dest_clip1 = (dest_bitmap.width, dest_bitmap.height)
283 px = source_bitmap.width // 2
285 py = source_bitmap.height // 2
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)
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
307 sin_angle = math.sin(angle)
308 cos_angle = math.cos(angle)
310 def update_bounds(dx, dy):
311 nonlocal minx, maxx, miny, maxy
321 w = source_bitmap.width
322 h = source_bitmap.height
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)
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)
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)
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)
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)
346 dv_col = cos_angle / scale
347 du_col = sin_angle / scale
351 startu = px - (ox * dv_col + oy * du_col)
352 startv = py - (ox * dv_row + oy * du_row)
354 rowu = startu + miny * du_col
355 rowv = startv + miny * dv_col
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
364 c = source_bitmap[int(u), int(v)]
365 if skip_index is None or c != skip_index:
366 dest_bitmap[x, y] = c
375 data: circuitpython_typing.ReadableBuffer,
378 x2: Optional[int] = None,
379 y2: Optional[int] = None,
380 skip_index: Optional[int] = None,
382 """Inserts pixels from ``data`` into the rectangle
383 of width×height pixels with the upper left corner at ``(x,y)``
385 The values from ``data`` are taken modulo the number of color values
386 available in the destination bitmap.
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.
392 The coordinates affected by the blit are
393 ``x1 <= x < x2`` and ``y1 <= y < y2``.
395 ``data`` must contain at least as many elements as required. If it
396 contains excess elements, they are ignored.
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.
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
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:
429 element_size: int = 1,
430 reverse_pixels_in_element: bool = False,
431 swap_bytes: bool = False,
432 reverse_rows: bool = False,
434 """Reads from a binary file into a bitmap.
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.
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.
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.
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).
463 height = bitmap.height
464 bits_per_value = bitmap._bits_per_value # pylint: disable=protected-access
465 mask = (1 << bits_per_value) - 1
467 elements_per_row = (width * bits_per_pixel + element_size * 8 - 1) // (
470 rowsize = element_size * elements_per_row
472 for y in range(height):
473 row_bytes = file.read(rowsize)
474 if len(row_bytes) != rowsize:
477 # Convert the raw bytes into the appropriate type array for processing
478 rowdata = bytearray(row_bytes)
481 if element_size == 2:
484 struct.pack("<H", struct.unpack(">H", rowdata[i : i + 2])[0])
485 for i in range(0, len(rowdata), 2)
488 elif element_size == 4:
491 struct.pack("<I", struct.unpack(">I", rowdata[i : i + 4])[0])
492 for i in range(0, len(rowdata), 4)
496 y_draw = height - 1 - y if reverse_rows else y
498 for x in range(width):
500 if bits_per_pixel == 1:
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:
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:
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:
516 elif bits_per_pixel == 16:
517 value = struct.unpack_from("<H", rowdata, x * 2)[0]
518 elif bits_per_pixel == 24:
521 (rowdata[offset] << 16)
522 | (rowdata[offset + 1] << 8)
523 | rowdata[offset + 2]
525 elif bits_per_pixel == 32:
526 value = struct.unpack_from("<I", rowdata, x * 4)[0]
528 bitmap[x, y_draw] = value & mask
533 Options for modes to use by alphablend() function.
536 # pylint: disable=too-few-public-methods
538 Normal = "bitmaptools.BlendMode.Normal"
539 Screen = "bitmaptools.BlendMode.Screen"
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,
553 """Alpha blend the two source bitmaps into the destination.
555 It is permitted for the destination bitmap to be one of the two
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
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."""
576 def clamp(val, minval, maxval):
577 return max(minval, min(maxval, val))
579 ifactor1 = int(factor1 * 256)
580 ifactor2 = int(factor2 * 256)
582 width, height = dest.width, dest.height
584 if colorspace == "L8":
585 for y in range(height):
586 for x in range(width):
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
592 if blend_source1 and blend_source2:
596 if blendmode == BlendMode.Screen:
597 blend = sca + sda - (sca * sda // 65536)
598 elif blendmode == BlendMode.Normal:
599 blend = sca + sda * (256 - ifactor2) // 256
601 denom = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256
602 pixel = blend // denom
604 pixel = sp1 * ifactor1 // 256
606 pixel = sp2 * ifactor2 // 256
610 dest[x, y] = clamp(pixel, 0, 255)
613 swap = colorspace in ("RGB565_SWAPPED", "BGR565_SWAPPED")
618 for y in range(height):
619 for x in range(width):
624 sp1 = ((sp1 & 0xFF) << 8) | ((sp1 >> 8) & 0xFF)
625 sp2 = ((sp2 & 0xFF) << 8) | ((sp2 >> 8) & 0xFF)
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
630 if blend_source1 and blend_source2:
631 ifactor_blend = ifactor1 + ifactor2 - ifactor1 * ifactor2 // 256
633 red_dca = ((sp1 & r_mask) >> 8) * ifactor1
634 grn_dca = ((sp1 & g_mask) >> 3) * ifactor1
635 blu_dca = ((sp1 & b_mask) << 3) * ifactor1
637 red_sca = ((sp2 & r_mask) >> 8) * ifactor2
638 grn_sca = ((sp2 & g_mask) >> 3) * ifactor2
639 blu_sca = ((sp2 & b_mask) << 3) * ifactor2
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
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
654 pixel = (r & r_mask) | (g & g_mask) | (b & b_mask)
657 pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF)
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
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
672 print(f"pixel hex: {hex(pixel)}")
676 class DitherAlgorithm:
678 Options for algorithm to use by dither() function.
681 # pylint: disable=too-few-public-methods
683 Atkinson = "bitmaptools.DitherAlgorithm.Atkinson"
684 FloydStenberg = "bitmaptools.DitherAlgorithm.FloydStenberg"
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},
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},
709 algorithm_map = {Atkinson: atkinson, FloydStenberg: floyd_stenberg}
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.
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.
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"]
741 print(f"swap: {swap}")
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:]]
747 # Output array for one row at a time (padded to multiple of 32)
748 out = [False] * (((width + 31) // 32) * 32)
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:
755 # Zero out padding area
757 luminance_data[-mx + i] = 0
758 luminance_data[bitmap.width + i] = 0
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]
764 for x in range(bitmap.width):
766 if swap & SWAP_BYTES:
767 # Swap bytes (equivalent to __builtin_bswap16)
768 pixel = ((pixel & 0xFF) << 8) | ((pixel >> 8) & 0xFF)
770 r = (pixel >> 8) & 0xF8
771 g = (pixel >> 3) & 0xFC
772 b = (pixel << 3) & 0xF8
774 if swap & SWAP_BYTES:
777 # Calculate luminance using same formula as C version
778 luminance_data[x] = (r * 78 + g * 154 + b * 29) // 256
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
786 for j in range(min(32, bitmap.width - i)):
792 for j in range(min(32, bitmap.width - i)):
793 bitmap[i + j, y] = (p >> (31 - j)) & 1
795 for i in range(bitmap.width):
796 bitmap[i, y] = 65535 if data[i] else 0
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)
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
812 err = pixel_in - (255 if pixel_out else 0)
814 # Distribute error to neighboring pixels
815 for i in range(count):
816 x1 = x + terms[i]["dx"]
819 rows[dy][x1] = ((terms[i]["dl"] * err) // 256) + rows[dy][x1]
821 err = (err * dl) // 256
823 write_pixels(dest_bitmap, y, out)
826 rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0]
832 # Fill the next row for future processing
833 fill_row(source_bitmap, swap, rows[2], y + 2, mx)
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
841 err = pixel_in - (255 if pixel_out else 0)
843 # Distribute error to neighboring pixels (in reverse direction)
844 for i in range(count):
845 x1 = x - terms[i]["dx"]
848 rows[dy][x1] = ((terms[i]["dl"] * err) // 256) + rows[dy][x1]
850 err = (err * dl) // 256
852 write_pixels(dest_bitmap, y, out)
854 # Cycle the rows again
855 rows[0], rows[1], rows[2] = rows[1], rows[2], rows[0]
857 # Fill the next row for future processing
858 fill_row(source_bitmap, swap, rows[2], y + 3, mx)
865 fill_color_value: int,
866 replaced_color_value: Optional[int] = None,
868 """Draws the color value into the destination bitmap enclosed
869 area of pixels of the background_value color. Like "Paint Bucket"
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:
881 if replaced_color_value == -1:
882 replaced_color_value = dest_bitmap[x, y]
885 fill_points.append((x, y))
893 while len(fill_points) > 0:
894 cur_point = fill_points.pop(0)
895 seen_points.append(cur_point)
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:
911 dest_bitmap[cur_x, cur_y] = fill_color_value
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)
920 and above_point not in seen_points
921 and above_point not in fill_points
923 fill_points.append(above_point)
925 below_point[1] < dest_bitmap.height
926 and below_point not in seen_points
927 and below_point not in fill_points
929 fill_points.append(below_point)
932 and left_point not in seen_points
933 and left_point not in fill_points
935 fill_points.append(left_point)
937 right_point[0] < dest_bitmap.width
938 and right_point not in seen_points
939 and right_point not in fill_points
941 fill_points.append(right_point)