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 :)

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