QOI: QOI_OP_RUN does not update the previously seen pixels, corrupting qoi_test_images/edgecase.qoi
メンテナーはふだん 1 日以内に返信
@manit2004 がすでに取り組んでいます。
2026年11月7日 から。
評価
調査の方向性
QoiDecoder.decode() の QOI_OP_RUN 分岐と、issue で説明されている _add_to_previous_pixels() の呼び出しを読む。提示された 29 バイトの例で失敗を再現し、その後、test_op_index を含む QOI デコーダーのテストを確認する。最初の run ピクセルが後続の QOI_OP_INDEX から利用でき、edgecase.qoi の公式出力が edgecase.png と一致すれば完了。
索引モデルが issue の本文から書いたものです。
説明
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
QoiDecodercode onmainis 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)])
- 主要言語
- Python
- スター
- 14k
- フォーク
- 2.6k
- 平均マージ
- 1日 4時間
- マージ済み PR(30日)
- 182
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
python-pillow/Pillow のほかの issue
-
Converting F images to I turns large values, inf and NaN into -2147483648対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープンConversion
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
python-pillow/Pillow#10372 · コメント 5 件 ·
メンテナーはふだん 1 日以内に返信
-
Sun raster files with RLE encoding and odd-width rows decode shifted対応中かも @PerryLink が 6 日前に担当しました。 オープン🤖-assisted
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
python-pillow/Pillow#10370 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
-
Warn that pre-applying arabic_reshaper + python-bidi corrupts text on a Raqm build対応中かも @Syamjith-NK が 70 日前に担当しました。 オープンDocumentation Font Rendering PNG Third Party Framework
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
python-pillow/Pillow#9907 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
🤖-assisted
難易度 4/5 1〜2日 初心者へのやさしさ 55/100
python-pillow/Pillow#10438 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
`ImageDraw.arc()` segfaults on a tuple subclass whose `__len__` is too large対応中かも @Parsiffall1 が 9 日前に担当しました。 オープン
難易度 4/5 1〜2日 初心者へのやさしさ 58/100
python-pillow/Pillow#10358 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
python-pillow/Pillow の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
-
enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
RunestoneInteractive/rs#1661 ·
メンテナーはふだん 2 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 64/100
streamlink/streamlink#7192 · コメント 4 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1〜3時間 初心者へのやさしさ 75/100
メンテナーはふだん 2 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
aws-samples/sample-per-model-bedrock#795 ·
メンテナーはふだん 1 日以内に返信