2025, Nov 19 07:00

How to Fix pyGLFW set_window_icon Errors: Provide an RGBA HxWx4 Array or a PIL Image (No Flattening)

Fix pyGLFW set_window_icon TypeError with RGBA HxWx4 pixels or a PIL Image. Includes OpenCV/Pillow conversion steps, alpha channel requirements, and BGR to RGB.

Setting a window icon with pyGLFW can be unexpectedly tricky if you pass pixel data in the wrong shape. A common pitfall is flattening the image buffer before handing it to glfw.set_window_icon(), which triggers a TypeError originating inside pyGLFW’s own conversion logic.

Problem in context

The issue appears when an image is read, optionally extended with an alpha channel, then flattened, and finally passed as a tuple to pyGLFW. The following example illustrates the failing approach.

def apply_window_icon(self, icon_path: str):
    frame = cv2.imread(icon_path, cv2.IMREAD_UNCHANGED)
    if frame is None:
        return False
    hh, ww = frame.shape[:2]
    if frame.shape[2] == 3:
        a_layer = numpy.ones((hh, ww, 1), dtype=numpy.uint8) * 255
        frame = numpy.concatenate((frame, a_layer), axis=2)
    frame = frame.astype(numpy.uint8)
    flat_buf = frame.flatten()
    glfw_image = (ww, hh, flat_buf)
    glfw.set_window_icon(self.handle, 1, [glfw_image])

This leads to an error like “TypeError: 'int' object is not subscriptable,” pointing at an internal pixels[i][j][k] access.

What actually goes wrong

The internal implementation of pyGLFW expects the pixel payload to be a structured collection with three indices: height, width, and channel. In other words, a 3D array shaped as H x W x 4. The relevant section in glfw/__init__.py makes this explicit by indexing pixels with three subscripts and iterating over four channels:

else:
    self.width, self.height, pixels = image
    array_type = ctypes.c_ubyte * 4 * self.width * self.height
    self.pixels_array = array_type()
    for i in range(self.height):
        for j in range(self.width):
            for k in range(4):
                self.pixels_array[i][j][k] = pixels[i][j][k]

This means a flattened 1D buffer is incompatible with the expected access pattern. It also implies that the icon must carry an alpha channel, because the innermost loop ranges over four channels (RGBA).

There is another supported path as well. If the object passed to glfw.set_window_icon() looks like a PIL Image, pyGLFW converts it to RGBA and ingests the data directly. The logic checks for .size and .convert and handles it like this:

if hasattr(image, 'size') and hasattr(image, 'convert'):
    # Treat image as PIL/pillow Image object
    self.width, self.height = image.size
    array_type = ctypes.c_ubyte * 4 * (self.width * self.height)
    self.pixels_array = array_type()
    pixels = image.convert('RGBA').getdata()
    for i, pixel in enumerate(pixels):
        self.pixels_array[i] = pixel

Fixing the code

There are two reliable ways forward. The first is to keep using OpenCV, but provide a proper 3D array in RGBA order and avoid flatten(). The second is to pass a PIL Image directly and let pyGLFW handle the conversion.

Below are corrected variants that preserve the original behavior while fixing the data layout and channel order.

import cv2
import numpy
import glfw

# image generated with ImageMagick: 
#    convert -size 32x32 -define png:color-type=2 canvas:white empty.png
# or with Python and PIL
#    python3 -c 'from PIL import Image;Image.new("RGB", (32,32), color="white").save("empty.png")'

ICON_FILE = 'empty.png'

def attach_icon_via_pillow(win_handle, icon_path):
    from PIL import Image
    picture = Image.open(icon_path)
    glfw.set_window_icon(win_handle, 1, [picture])

def attach_icon_via_cv2(win_handle, icon_path):
    img_cv = cv2.imread(icon_path, cv2.IMREAD_UNCHANGED)
    img_cv = cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)  # convert from BGR to RGB

    if img_cv is None:
        return False

    hh, ww = img_cv.shape[:2]

    if img_cv.shape[2] == 3:
        alpha_chan = numpy.ones((hh, ww, 1), dtype=numpy.uint8) * 255
        img_cv = numpy.concatenate((img_cv, alpha_chan), axis=2)

    img_cv = img_cv.astype(numpy.uint8)
    glfw_image = (ww, hh, img_cv)  # 3D array, not flattened
    glfw.set_window_icon(win_handle, 1, [glfw_image])


def demo():
    if not glfw.init():
        return

    win = glfw.create_window(640, 480, "Hello World", None, None)
    if not win:
        glfw.terminate()
        return

    attach_icon_via_cv2(win, ICON_FILE)
    # attach_icon_via_pillow(win, ICON_FILE)

    glfw.make_context_current(win)

    while not glfw.window_should_close(win):
        glfw.swap_buffers(win)
        glfw.poll_events()

    glfw.terminate()

if __name__ == '__main__':
    demo()

Why this matters

pyGLFW’s API supports two distinct icon input forms: a PIL Image or a tuple of width, height, and a 3D RGBA array. Passing a flattened buffer breaks the expected pixels[i][j][k] access, which results in the TypeError you saw. Understanding this contract saves time on debugging and makes the fix straightforward. When documentation is sparse, reading the small but clear sections of library source helps reveal exactly what shape and type the function expects. Including full error traces also shortens the feedback loop, because the failing line often points directly at the mismatch.

Takeaways

Keep the pixel data as a H x W x 4 array when using OpenCV and add an alpha channel if it’s missing. Do not flatten the buffer. If you prefer the simplest path, pass a PIL Image and let pyGLFW convert it to RGBA internally. And when you get stuck or can’t find Python-specific documentation, inspect the library’s source and share the complete error message—the line and context usually reveal the exact shape the function expects.