]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/commitdiff
Remove more of PIL
authorMelissa LeBlanc-Williams <melissa@adafruit.com>
Sat, 23 Sep 2023 05:36:30 +0000 (22:36 -0700)
committerMelissa LeBlanc-Williams <melissa@adafruit.com>
Sat, 23 Sep 2023 05:36:30 +0000 (22:36 -0700)
displayio/_bitmap.py
displayio/_display.py
displayio/_displaycore.py
displayio/_epaperdisplay.py
displayio/_fourwire.py
displayio/_group.py
displayio/_i2cdisplay.py
displayio/_palette.py
displayio/_structs.py
displayio/_tilegrid.py

index 03dd75bf483c8f0e17e1910470156114fa591d9e..30009e29df5a579acf2a1962b0ab24f93a3a348c 100644 (file)
@@ -18,17 +18,39 @@ displayio for Blinka
 """
 
 from __future__ import annotations
+import struct
+from array import array
 from typing import Union, Tuple
-from PIL import Image
-from ._structs import RectangleStruct
+from circuitpython_typing import WriteableBuffer
 from ._area import Area
 
 __version__ = "0.0.0+auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
+ALIGN_BITS = 8 * struct.calcsize("I")
+
+
+def stride(width: int, bits_per_pixel: int) -> int:
+    """Return the number of bytes per row of a bitmap with the given width and bits per pixel."""
+    row_width = width * bits_per_pixel
+    return (row_width + (ALIGN_BITS - 1)) // ALIGN_BITS
+
 
 class Bitmap:
-    """Stores values of a certain size in a 2D array"""
+    """Stores values of a certain size in a 2D array
+
+    Bitmaps can be treated as read-only buffers. If the number of bits in a pixel is 8, 16,
+    or 32; and the number of bytes per row is a multiple of 4, then the resulting memoryview
+    will correspond directly with the bitmap's contents. Otherwise, the bitmap data is packed
+    into the memoryview with unspecified padding.
+
+    A Bitmap can be treated as a buffer, allowing its content to be
+    viewed and modified using e.g., with ``ulab.numpy.frombuffer``,
+    but the `displayio.Bitmap.dirty` method must be used to inform
+    displayio when a bitmap was modified through the buffer interface.
+
+    `bitmaptools.arrayblit` can also be useful to move data efficiently
+    into a Bitmap."""
 
     def __init__(self, width: int, height: int, value_count: int):
         """Create a Bitmap object with the given fixed size. Each pixel stores a value that is
@@ -36,12 +58,9 @@ class Bitmap:
         share the underlying Bitmap. value_count is used to minimize the memory used to store
         the Bitmap.
         """
-        self._bmp_width = width
-        self._bmp_height = height
-        self._read_only = False
 
-        if value_count < 0:
-            raise ValueError("value_count must be > 0")
+        if not 1 <= value_count <= 65535:
+            raise ValueError("value_count must be in the range of 1-65535")
 
         bits = 1
         while (value_count - 1) >> bits:
@@ -50,7 +69,28 @@ class Bitmap:
             else:
                 bits += 8
 
-        self._bits_per_value = bits
+        self._from_buffer(width, height, bits, None, False)
+
+    def _from_buffer(
+        self,
+        width: int,
+        height: int,
+        bits_per_value: int,
+        data: WriteableBuffer,
+        read_only: bool,
+    ) -> None:
+        # pylint: disable=too-many-arguments
+        self._bmp_width = width
+        self._bmp_height = height
+        self._stride = stride(width, bits_per_value)
+        self._data_alloc = False
+
+        if data is None or len(data) == 0:
+            data = array("L", [0] * self._stride * height)
+            self._data_alloc = True
+        self._data = data
+        self._read_only = read_only
+        self._bits_per_value = bits_per_value
 
         if (
             self._bits_per_value > 8
@@ -59,8 +99,23 @@ class Bitmap:
         ):
             raise NotImplementedError("Invalid bits per value")
 
-        self._image = Image.new("P", (width, height), 0)
-        self._dirty_area = RectangleStruct(0, 0, width, height)
+        # Division and modulus can be slow because it has to handle any integer. We know
+        # bits_per_value is a power of two. We divide and mod by bits_per_value to compute
+        # the offset into the byte array. So, we can the offset computation to simplify to
+        # a shift for division and mask for mod.
+
+        # Used to divide the index by the number of pixels per word. It's
+        # used in a shift which effectively divides by 2 ** x_shift.
+        self._x_shift = 0
+
+        power_of_two = 1
+        while power_of_two < ALIGN_BITS // bits_per_value:
+            self._x_shift += 1
+            power_of_two = power_of_two << 1
+
+        self._x_mask = (1 << self._x_shift) - 1  # UUsed as a modulus on the x value
+        self._bitmask = (1 << bits_per_value) - 1
+        self._dirty_area = Area(0, 0, width, height)
 
     def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
         """
@@ -75,12 +130,32 @@ class Bitmap:
         else:
             raise TypeError("Index is not an int, list, or tuple")
 
-        if x > self._image.width or y > self._image.height:
+        if x > self._bmp_width or x < 0 or y > self._bmp_height or y < 0:
             raise ValueError(f"Index {index} is out of range")
         return self._get_pixel(x, y)
 
     def _get_pixel(self, x: int, y: int) -> int:
-        return self._image.getpixel((x, y))
+        if x >= self._bmp_width or x < 0 or y >= self._bmp_height or y < 0:
+            return 0
+        row_start = y * self._stride
+        bytes_per_value = self._bits_per_value // 8
+        if bytes_per_value < 1:
+            word = self._data[row_start + (x >> self._x_shift)]
+            return (
+                word
+                >> (
+                    struct.calcsize("I") * 8
+                    - ((x & self._x_mask) + 1) * self._bits_per_value
+                )
+            ) & self._bitmask
+        row = memoryview(self._data)[row_start : row_start + self._stride]
+        if bytes_per_value == 1:
+            return row[x]
+        if bytes_per_value == 2:
+            return struct.unpack_from("<H", row, x * 2)[0]
+        if bytes_per_value == 4:
+            return struct.unpack_from("<I", row, x * 4)[0]
+        return 0
 
     def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
         """
@@ -96,21 +171,42 @@ class Bitmap:
         elif isinstance(index, int):
             x = index % self._bmp_width
             y = index // self._bmp_width
-        self._image.putpixel((x, y), value)
-        if self._dirty_area.x1 == self._dirty_area.x2:
-            self._dirty_area.x1 = x
-            self._dirty_area.x2 = x + 1
-            self._dirty_area.y1 = y
-            self._dirty_area.y2 = y + 1
+        # update the dirty region
+        self._set_dirty_area(Area(x, y, x + 1, y + 1))
+        self._write_pixel(x, y, value)
+
+    def _write_pixel(self, x: int, y: int, value: int) -> None:
+        if self._read_only:
+            raise RuntimeError("Read-only")
+
+        # Writes the color index value into a pixel position
+        # Must update the dirty area separately
+
+        # Don't write if out of area
+        if x < 0 or x >= self._bmp_width or y < 0 or y >= self._bmp_height:
+            return
+
+        # Update one pixel of data
+        row_start = y * self._stride
+        bytes_per_value = self._bits_per_value // 8
+        if bytes_per_value < 1:
+            bit_position = (
+                struct.calcsize("I") * 8
+                - ((x & self._x_mask) + 1) * self._bits_per_value
+            )
+            index = row_start + (x >> self._x_shift)
+            word = self._data[index]
+            word &= ~(self._bitmask << bit_position)
+            word |= (value & self._bitmask) << bit_position
+            self._data[index] = word
         else:
-            if x < self._dirty_area.x1:
-                self._dirty_area.x1 = x
-            elif x >= self._dirty_area.x2:
-                self._dirty_area.x2 = x + 1
-            if y < self._dirty_area.y1:
-                self._dirty_area.y1 = y
-            elif y >= self._dirty_area.y2:
-                self._dirty_area.y2 = y + 1
+            row = memoryview(self._data)[row_start : row_start + self._stride]
+            if bytes_per_value == 1:
+                row[x] = value
+            elif bytes_per_value == 2:
+                struct.pack_into("<H", row, x * 2, value)
+            elif bytes_per_value == 4:
+                struct.pack_into("<I", row, x * 4, value)
 
     def _finish_refresh(self):
         self._dirty_area.x1 = 0
@@ -118,8 +214,18 @@ class Bitmap:
 
     def fill(self, value: int) -> None:
         """Fills the bitmap with the supplied palette index value."""
-        self._image = Image.new("P", (self._bmp_width, self._bmp_height), value)
-        self._dirty_area = RectangleStruct(0, 0, self._bmp_width, self._bmp_height)
+        if self._read_only:
+            raise RuntimeError("Read-only")
+        self._set_dirty_area(Area(0, 0, self._bmp_width, self._bmp_height))
+
+        # build the packed word
+        word = 0
+        for i in range(32 // self._bits_per_value):
+            word |= (value & self._bitmask) << (32 - ((i + 1) * self._bits_per_value))
+
+        # copy it in
+        for i in range(self._stride * self._bmp_height):
+            self._data[i] = word
 
     def blit(
         self,
@@ -182,7 +288,13 @@ class Bitmap:
             x2 = self._bmp_width
         if y2 == -1:
             y2 = self._bmp_height
-        area = Area(x1, y1, x2, y2)
+        self._set_dirty_area(Area(x1, y1, x2, y2))
+
+    def _set_dirty_area(self, dirty_area: Area) -> None:
+        if self._read_only:
+            raise RuntimeError("Read-only")
+
+        area = dirty_area
         area.canon()
         area.union(self._dirty_area, area)
         bitmap_area = Area(0, 0, self._bmp_width, self._bmp_height)
index ac719817987fc5e586d9922ff1370b1279ece781..e001577f3e38a5b2c1848d296e19b3aa5921716d 100644 (file)
@@ -22,14 +22,12 @@ import struct
 from array import array
 from typing import Optional
 import digitalio
-from PIL import Image
 import microcontroller
-import circuitpython_typing
+from circuitpython_typing import WriteableBuffer, ReadableBuffer
 from ._displaycore import _DisplayCore
 from ._displaybus import _DisplayBus
 from ._colorconverter import ColorConverter
 from ._group import Group
-from ._structs import RectangleStruct
 from ._area import Area
 from ._constants import (
     CHIP_SELECT_TOGGLE_EVERY_BYTE,
@@ -58,7 +56,7 @@ class Display:
     def __init__(
         self,
         display_bus: _DisplayBus,
-        init_sequence: circuitpython_typing.ReadableBuffer,
+        init_sequence: ReadableBuffer,
         *,
         width: int,
         height: int,
@@ -77,16 +75,14 @@ class Display:
         backlight_pin: Optional[microcontroller.Pin] = None,
         brightness_command: Optional[int] = None,
         brightness: float = 1.0,
-        auto_brightness: bool = False,
         single_byte_bounds: bool = False,
         data_as_commands: bool = False,
         auto_refresh: bool = True,
         native_frames_per_second: int = 60,
         backlight_on_high: bool = True,
         SH1107_addressing: bool = False,
-        set_vertical_scroll: int = 0,
     ):
-        # pylint: disable=unused-argument,too-many-locals,invalid-name
+        # pylint: disable=too-many-locals,invalid-name
         """Create a Display object on the given display bus (`displayio.FourWire` or
         `paralleldisplay.ParallelBus`).
 
@@ -157,12 +153,10 @@ class Display:
         self._native_frames_per_second = native_frames_per_second
         self._native_ms_per_frame = 1000 // native_frames_per_second
 
-        self._auto_brightness = auto_brightness
         self._brightness = brightness
         self._auto_refresh = auto_refresh
 
         self._initialize(init_sequence)
-        self._buffer = Image.new("RGB", (width, height))
         self._current_group = None
         self._last_refresh_call = 0
         self._refresh_thread = None
@@ -295,20 +289,7 @@ class Display:
         if not self._core.start_refresh():
             return False
 
-        # TODO: Likely move this to _refresh_area()
-        # Go through groups and and add each to buffer
-        """
-        if self._core.current_group is not None:
-            buffer = Image.new("RGBA", (self._core.width, self._core.height))
-            # Recursively have everything draw to the image
-            self._core.current_group._fill_area(
-                buffer
-            )  # pylint: disable=protected-access
-            # save image to buffer (or probably refresh buffer so we can compare)
-            self._buffer.paste(buffer)
-        """
         areas_to_refresh = self._get_refresh_areas()
-
         for area in areas_to_refresh:
             self._refresh_area(area)
 
@@ -404,39 +385,22 @@ class Display:
             self._core.end_transaction()
         return True
 
-    def _apply_rotation(self, rectangle):
-        """Adjust the rectangle coordinates based on rotation"""
-        if self._core.rotation == 90:
-            return RectangleStruct(
-                self._core.height - rectangle.y2,
-                rectangle.x1,
-                self._core.height - rectangle.y1,
-                rectangle.x2,
-            )
-        if self._core.rotation == 180:
-            return RectangleStruct(
-                self._core.width - rectangle.x2,
-                self._core.height - rectangle.y2,
-                self._core.width - rectangle.x1,
-                self._core.height - rectangle.y1,
-            )
-        if self._core.rotation == 270:
-            return RectangleStruct(
-                rectangle.y1,
-                self._core.width - rectangle.x2,
-                rectangle.y2,
-                self._core.width - rectangle.x1,
-            )
-        return rectangle
-
-    def fill_row(
-        self, y: int, buffer: circuitpython_typing.WriteableBuffer
-    ) -> circuitpython_typing.WriteableBuffer:
+    def fill_row(self, y: int, buffer: WriteableBuffer) -> WriteableBuffer:
         """Extract the pixels from a single row"""
-        for x in range(0, self._core.width):
-            _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
-            buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
-            buffer[x * 2 + 1] = _rgb_565 & 0xFF
+        if self._core.colorspace.depth != 16:
+            raise ValueError("Display must have a 16 bit colorspace.")
+
+        area = Area(0, y, self._core.width, y + 1)
+        pixels_per_word = (struct.calcsize("I") * 8) // self._core.colorspace.depth
+        buffer_size = self._core.width // pixels_per_word
+        pixels_per_buffer = area.size()
+        if pixels_per_buffer % pixels_per_word:
+            buffer_size += 1
+
+        buffer = bytearray([0] * (buffer_size * struct.calcsize("I")))
+        mask_length = (pixels_per_buffer // 32) + 1
+        mask = array("L", [0x00000000] * mask_length)
+        self._core.fill_area(area, mask, buffer)
         return buffer
 
     def release(self) -> None:
@@ -460,10 +424,7 @@ class Display:
 
     @property
     def brightness(self) -> float:
-        """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
-        When `auto_brightness` is True, the value of `brightness` will change automatically.
-        If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
-        """
+        """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`."""
         return self._brightness
 
     @brightness.setter
@@ -498,19 +459,6 @@ class Display:
         else:
             raise ValueError("Brightness must be between 0.0 and 1.0")
 
-    @property
-    def auto_brightness(self) -> bool:
-        """True when the display brightness is adjusted automatically, based on an ambient
-        light sensor or other method. Note that some displays may have this set to True by
-        default, but not actually implement automatic brightness adjustment.
-        `auto_brightness` is set to False if `brightness` is set manually.
-        """
-        return self._auto_brightness
-
-    @auto_brightness.setter
-    def auto_brightness(self, value: bool):
-        self._auto_brightness = value
-
     @property
     def width(self) -> int:
         """Display Width"""
index 8db49e01892ba21bb4829879a3fe314fded23e2a..9bbb66518c9966134bd465a72db4412ca7b25e86 100644 (file)
@@ -24,7 +24,7 @@ __repo__ = "https://github.com/adafruit/Adafruit_Blinka_Displayio.git"
 
 import time
 import struct
-import circuitpython_typing
+from circuitpython_typing import WriteableBuffer, ReadableBuffer
 from paralleldisplay import ParallelBus
 from ._fourwire import FourWire
 from ._group import Group
@@ -248,8 +248,8 @@ class _DisplayCore:
     def fill_area(
         self,
         area: Area,
-        mask: circuitpython_typing.WriteableBuffer,
-        buffer: circuitpython_typing.WriteableBuffer,
+        mask: WriteableBuffer,
+        buffer: WriteableBuffer,
     ) -> bool:
         """Call the current group's fill area function"""
         if self.current_group is not None:
@@ -400,7 +400,7 @@ class _DisplayCore:
         self,
         data_type: int,
         chip_select: int,
-        data: circuitpython_typing.ReadableBuffer,
+        data: ReadableBuffer,
     ) -> None:
         """
         Send the data to the current bus
index 686f8bb12d58aae1bdc3356352e0c2d6a7421601..114506c4af915a483befb54d95261ed7f23c6203 100644 (file)
@@ -19,7 +19,7 @@ displayio for Blinka
 
 from typing import Optional
 import microcontroller
-import circuitpython_typing
+from circuitpython_typing import ReadableBuffer
 from ._group import Group
 from ._displaybus import _DisplayBus
 
@@ -42,8 +42,8 @@ class EPaperDisplay:
     def __init__(
         self,
         display_bus: _DisplayBus,
-        start_sequence: circuitpython_typing.ReadableBuffer,
-        stop_sequence: circuitpython_typing.ReadableBuffer,
+        start_sequence: ReadableBuffer,
+        stop_sequence: ReadableBuffer,
         *,
         width: int,
         height: int,
@@ -84,9 +84,9 @@ class EPaperDisplay:
 
         :param display_bus: The bus that the display is connected to
         :type _DisplayBus: displayio.FourWire or displayio.ParallelBus
-        :param ~circuitpython_typing.ReadableBuffer start_sequence: Byte-packed
+        :param ~ReadableBuffer start_sequence: Byte-packed
             initialization sequence.
-        :param ~circuitpython_typing.ReadableBuffer stop_sequence: Byte-packed
+        :param ~ReadableBuffer stop_sequence: Byte-packed
             initialization sequence.
         :param int width: Width in pixels
         :param int height: Height in pixels
index 31b1be5e5a1ea1d578d9ab22640b966742f1145a..142e308cef10b13b01df1b285acb0b59942dcb35 100644 (file)
@@ -22,7 +22,7 @@ from typing import Optional
 import digitalio
 import busio
 import microcontroller
-import circuitpython_typing
+from circuitpython_typing import ReadableBuffer
 from ._constants import (
     CHIP_SELECT_TOGGLE_EVERY_BYTE,
     CHIP_SELECT_UNTOUCHED,
@@ -95,7 +95,7 @@ class FourWire:
     def send(
         self,
         command,
-        data: circuitpython_typing.ReadableBuffer,
+        data: ReadableBuffer,
         *,
         toggle_every_byte: bool = False,
     ) -> None:
@@ -120,7 +120,7 @@ class FourWire:
         self,
         data_type: int,
         chip_select: int,
-        data: circuitpython_typing.ReadableBuffer,
+        data: ReadableBuffer,
     ):
         self._dc.value = data_type == DISPLAY_DATA
         if chip_select == CHIP_SELECT_TOGGLE_EVERY_BYTE:
index ed90f8e105a1f705f9bb7d6190ddb4be4587bedb..833b95c4aec3f65e6e56768f7efe420379f42727 100644 (file)
@@ -19,7 +19,7 @@ displayio for Blinka
 
 from __future__ import annotations
 from typing import Union, Callable
-import circuitpython_typing
+from circuitpython_typing import WriteableBuffer
 from ._structs import TransformStruct
 from ._tilegrid import TileGrid
 from ._colorspace import Colorspace
@@ -149,8 +149,8 @@ class Group:
         self,
         colorspace: Colorspace,
         area: Area,
-        mask: circuitpython_typing.WriteableBuffer,
-        buffer: circuitpython_typing.WriteableBuffer,
+        mask: WriteableBuffer,
+        buffer: WriteableBuffer,
     ) -> bool:
         if self._hidden_group:
             return False
index 67022f5c213048f4704d398d7a72f3429d270cf4..e37eb8a659be5b584b28e71777f7b7131b571426 100644 (file)
@@ -23,7 +23,7 @@ displayio for Blinka
 import time
 import busio
 import digitalio
-import circuitpython_typing
+from circuitpython_typing import ReadableBuffer
 from ._constants import CHIP_SELECT_UNTOUCHED, DISPLAY_COMMAND
 
 __version__ = "0.0.0+auto.0"
@@ -85,7 +85,7 @@ class I2CDisplay:
         while not self._i2c.try_lock():
             pass
 
-    def send(self, command: int, data: circuitpython_typing.ReadableBuffer) -> None:
+    def send(self, command: int, data: ReadableBuffer) -> None:
         """
         Sends the given command value followed by the full set of data. Display state,
         such as vertical scroll, set via ``send`` may or may not be reset once the code is
@@ -99,7 +99,7 @@ class I2CDisplay:
         self,
         data_type: int,
         _chip_select: int,  # Chip select behavior
-        data: circuitpython_typing.ReadableBuffer,
+        data: ReadableBuffer,
     ):
         if data_type == DISPLAY_COMMAND:
             n = len(data)
index af72dcbf4c1b216fa10eeec6c3207b77c8aa5d13..63d98eec3385ceea9e19ed6c556565abfb4f84c4 100644 (file)
@@ -18,7 +18,7 @@ displayio for Blinka
 """
 
 from typing import Optional, Union, Tuple
-import circuitpython_typing
+from circuitpython_typing import ReadableBuffer
 from ._colorconverter import ColorConverter
 from ._colorspace import Colorspace
 from ._structs import InputPixelStruct, OutputPixelStruct, ColorStruct
@@ -63,7 +63,7 @@ class Palette:
     def __setitem__(
         self,
         index: int,
-        value: Union[int, circuitpython_typing.ReadableBuffer, Tuple[int, int, int]],
+        value: Union[int, ReadableBuffer, Tuple[int, int, int]],
     ) -> None:
         """Sets the pixel color at the given index. The index should be
         an integer in the range 0 to color_count-1.
index a46fb48204529403ee4e8f8273fdf3396f86ce8a..3f0c2e1ccb6c7766e1df594c82f72c8af00660dc 100644 (file)
@@ -23,16 +23,6 @@ __version__ = "0.0.0+auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_Displayio.git"
 
 
-@dataclass
-class RectangleStruct:
-    # pylint: disable=invalid-name
-    """Rectangle Struct Dataclass. To eventually be replaced by Area."""
-    x1: int
-    y1: int
-    x2: int
-    y2: int
-
-
 @dataclass
 class TransformStruct:
     # pylint: disable=invalid-name
index ec5e45f0640c171c3b102bb18d86d9d3ad6387cf..cd2766263c42904870de9e67b023cdb685ca081d 100644 (file)
@@ -19,7 +19,7 @@ displayio for Blinka
 
 import struct
 from typing import Union, Optional, Tuple
-import circuitpython_typing
+from circuitpython_typing import WriteableBuffer
 from ._bitmap import Bitmap
 from ._colorconverter import ColorConverter
 from ._ondiskbitmap import OnDiskBitmap
@@ -215,8 +215,8 @@ class TileGrid:
         self,
         colorspace: Colorspace,
         area: Area,
-        mask: circuitpython_typing.WriteableBuffer,
-        buffer: circuitpython_typing.WriteableBuffer,
+        mask: WriteableBuffer,
+        buffer: WriteableBuffer,
     ) -> bool:
         """Draw onto the image"""
         # pylint: disable=too-many-locals,too-many-branches,too-many-statements
@@ -335,16 +335,18 @@ class TileGrid:
                 else:
                     mask[offset // 32] |= 1 << (offset % 32)
                     if colorspace.depth == 16:
-                        buffer = (
-                            buffer[:offset]
-                            + struct.pack("H", output_pixel.pixel)
-                            + buffer[offset + 2 :]
+                        struct.pack_into(
+                            "H",
+                            buffer,
+                            offset,
+                            output_pixel.pixel,
                         )
                     elif colorspace.depth == 32:
-                        buffer = (
-                            buffer[:offset]
-                            + struct.pack("I", output_pixel.pixel)
-                            + buffer[offset + 4 :]
+                        struct.pack_into(
+                            "I",
+                            buffer,
+                            offset,
+                            output_pixel.pixel,
                         )
                     elif colorspace.depth == 8:
                         buffer[offset] = output_pixel.pixel & 0xFF