QOI: QOI_OP_RUN does not update the previously seen pixels, corrupting qoi_test_images/edgecase.qoi

Open Beginner friendly
#10,452 4 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@manit2004 is already working on this.

Since Nov 7, 2026.

  • #10453 by @manit2004 — open
  • #10454 by @lianxiaozhi — closed without merging

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
backend

Research direction

Read the QoiDecoder.decode() QOI_OP_RUN branch and the _add_to_previous_pixels() call described in the issue. Reproduce the failure with the provided 29-byte example, then check the QOI decoder tests, including test_op_index. Done means the initial run pixel is available to a later QOI_OP_INDEX and the official edgecase.qoi output matches edgecase.png.

Written by the indexing model from the issue text.

Description

What did you do?

Opened a QOI image that starts with a run of the initial pixel (0, 0, 0, 255) and later refers to that pixel with QOI_OP_INDEX.

The official test image edgecase.qoi from https://qoiformat.org/qoi_test_images.zip does this. The script below is a minimal 29-byte reproduction.

What did you expect to happen?

The third pixel should be (0, 0, 0, 255), and edgecase.qoi should match edgecase.png.

The specification says "Each pixel that is seen by the encoder and decoder is put into this array". The reference decoder updates the array after every chunk, including QOI_OP_RUN.

What actually happened?

The third pixel is (0, 0, 0, 0):

[(0, 0, 0, 255), (1, 2, 3, 4), (0, 0, 0, 0)]

For edgecase.qoi, 10272 of the 16384 pixels differ from edgecase.png.

The QOI_OP_RUN branch in QoiDecoder.decode() reaches continue before _add_to_previous_pixels() is called, so the pixel is never added to the array. For most pixels this does no harm, because the previous pixel is already in the array from the chunk that produced it. The initial pixel is the exception, because no chunk produced it.

This looks like a regression from #9008, first released in 11.3.0. That PR correctly stopped adding the initial pixel to the array up front, but the up-front add had been hiding this problem. I did not run older releases. On 13.0.0, restoring the removed line makes both the script below and edgecase.qoi decode correctly.

qoi.h's encoder does not add run pixels to its array, so it does not produce this pattern. Encoders that follow the specification literally can, as edgecase.qoi shows.

This change fixes it:

                     value = self._previous_pixel
+                    self._add_to_previous_pixels(value)
                     if bands == 3:

With this change, all eight official test images match their PNGs, and QOI_OP_INDEX as the first chunk still gives (0, 0, 0, 0), as test_op_index expects. I have not run the full test suite.

What are your OS, Python and Pillow versions?
  • OS: Fedora Linux 43
  • Python: 3.14.8
  • Pillow: 13.0.0. The QoiDecoder code on main is identical.
--------------------------------------------------------------------
Pillow 13.0.0
Python 3.14.8 (main, Oct  1 2026, 00:00:00) [GCC 15.3.1 20260722 (Red Hat 15.3.1-1)]
--------------------------------------------------------------------
Python executable is <venv>/bin/python
Environment Python files loaded from <venv>
System Python files loaded from /usr
--------------------------------------------------------------------
Python Pillow modules loaded from <venv>/lib64/python3.14/site-packages/PIL
Binary Pillow modules loaded from <venv>/lib64/python3.14/site-packages/PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 13.0.0
--- TKINTER support ok, loaded 9.0
--- FREETYPE2 support ok, loaded 2.14.3
--- LITTLECMS2 support ok, loaded 2.19
--- WEBP support ok, loaded 1.6.0
--- AVIF support ok, loaded 1.4.2
--- JPEG support ok, compiled for libjpeg-turbo 3.2.0
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.4
--- ZLIB (PNG/ZIP) support ok, loaded 1.3.1.zlib-ng, compiled for zlib-ng 2.3.3
--- LIBTIFF support ok, loaded 4.7.2
--- RAQM (Bidirectional Text) support ok, loaded 0.11.0, fribidi 1.0.16, harfbuzz 14.5.0
*** LIBIMAGEQUANT (Quantization method) support not installed
--- XCB (X protocol) support ok
--------------------------------------------------------------------
from io import BytesIO

from PIL import Image

data = (
    b"qoif"
    + (3).to_bytes(4, "big")  # width
    + (1).to_bytes(4, "big")  # height
    + bytes([4, 0])  # channels, colorspace
    + bytes([0xC0])  # QOI_OP_RUN, length 1: repeats the initial (0, 0, 0, 255)
    + bytes([0xFF, 1, 2, 3, 4])  # QOI_OP_RGBA
    + bytes([53])  # QOI_OP_INDEX: 255 * 11 % 64 == 53, the slot of (0, 0, 0, 255)
    + bytes(7)
    + b"\x01"  # end marker
)

with Image.open(BytesIO(data)) as im:
    print([im.getpixel((x, 0)) for x in range(3)])
Dominant language
Python
Stars
14k
Forks
2.6k
Avg merge
1d 4h
Merged PRs (30d)
182

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from python-pillow/Pillow

All issues in python-pillow/Pillow

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.