Skip to content

Fix ImImagePlugin.seek() frame stride for non-8-bit modes - #9800

Open
chuenchen309 wants to merge 1 commit into
python-pillow:mainfrom
chuenchen309:fix/im-seek-non-8bit-stride
Open

Fix ImImagePlugin.seek() frame stride for non-8-bit modes#9800
chuenchen309 wants to merge 1 commit into
python-pillow:mainfrom
chuenchen309:fix/im-seek-non-8bit-stride

Conversation

@chuenchen309

Copy link
Copy Markdown
Contributor

Changes proposed in this pull request:

  • Fix ImImagePlugin.seek() computing the per-frame byte stride as 8 * len(self.mode) — the length of the mode name string used as bits-per-pixel. That only equals the real value when len(mode) matches the band count and the bands are 8-bit (L, RGB, RGBA, CMYK, LA, P). For I;16 (16-bit), I / F (32-bit) and YCbCr (3-byte), the stride was wrong, so seek(frame >= 1) on a multi-frame IM image raised OSError: image file is truncated (I;16, YCbCr) or returned pixels from the wrong offset (I, F).
  • Use 8 * bands * itemsize (via ImageMode), which matches Image.new(mode).tobytes() for every IM-supported mode and leaves the six already-correct modes byte-for-byte unchanged.
  • Add a parametrized regression test seeking to frame 1 of a 2-frame IM image in the affected modes.

Reachability, stated honestly: this is a low-frequency bug. Pillow's own _save only writes one frame's data, so Pillow cannot itself produce a valid multi-frame IM file — the trigger is a multi-frame IM/IFUNC file from an external tool in one of these less-common modes. The IM reader does advertise multi-frame support (n_frames/seek are wired), so reading such a file with a wrong stride is a real correctness bug, but it is not a hot path. Happy to close if you'd rather not carry it.

seek() computed the per-frame byte stride as 8 * len(self.mode), using
the length of the mode *name* string as bits-per-pixel. That is only
correct when len(mode) equals the band count and the bands are 8-bit
(L, RGB, RGBA, CMYK, LA, P). For I;16 (16-bit), I / F (32-bit) and
YCbCr (3-byte) the stride was wrong, so seeking to frame >= 1 in a
multi-frame IM image either raised 'image file is truncated' or returned
pixels from the wrong offset.

Use 8 * bands * itemsize, which matches Image.new(mode).tobytes() for
every IM-supported mode and leaves the working modes unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrew Chen <48723787+chuenchen309@users.noreply.github.com>
@radarhere radarhere added the 🤖-assisted AI-assisted label Jul 19, 2026
Comment thread src/PIL/ImImagePlugin.py
else:
bits = 8 * len(self.mode)
mode = ImageMode.getmode(self.mode)
bits = 8 * len(mode.bands) * int(mode.typestr[-1])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's really not clear to a reader why we multiply things with an integer from the last character of a type string :)

@radarhere

Copy link
Copy Markdown
Member

the trigger is a multi-frame IM/IFUNC file from an external tool in one of these less-common modes

Do you have such a file available? Or did you find an IM specification that you were able to refer to for this?

If not, you're saying that this part of our seeking implementation is incorrect... but only this part, and presuming that the rest of it is correct, and creating a new test based on our seeking implementation. It just seems a bit snake-eating-its-own-tail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤖-assisted AI-assisted

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants