Fix ImImagePlugin.seek() frame stride for non-8-bit modes - #9800
Fix ImImagePlugin.seek() frame stride for non-8-bit modes#9800chuenchen309 wants to merge 1 commit into
Conversation
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>
| else: | ||
| bits = 8 * len(self.mode) | ||
| mode = ImageMode.getmode(self.mode) | ||
| bits = 8 * len(mode.bands) * int(mode.typestr[-1]) |
There was a problem hiding this comment.
It's really not clear to a reader why we multiply things with an integer from the last character of a type string :)
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. |
|
If you're not aware of any software that interacts with this format, I've created #9849 to suggest deprecating it completely. |
|
Answering both parts directly: no, I don't have such a file, and no, I didn't find a specification. The change came out of reading And your description of the test is fair. It writes the fixture with Pillow and reads it back with Pillow, so it pins the reader against my reading of the reader, not against anything real. One thing that doesn't need a spec, offered for #9849 rather than for this PR. So Pillow's own writer produces files that its own reader reports as animated and then cannot seek into — in every mode, including the ones where the stride is already correct. The multi-frame read path appears never to have had a producer anywhere in Pillow, which reads to me as an argument for your deprecation rather than for patching the stride. For the record, the line I changed can't be spec-derived either: |
|
The logic you used to arrive at this PR is fine. If my PR for deprecating the plugin is accepted, then I would like to not make any changes (that aren't security-related). I don't think it makes sense to modify a deprecated feature, and I don't expect the bugs you're describing to bother anyone because I don't think anyone is using the format. If my PR isn't accepted, then I'm happy to engage with the two problems you've described. |
Changes proposed in this pull request:
ImImagePlugin.seek()computing the per-frame byte stride as8 * len(self.mode)— the length of the mode name string used as bits-per-pixel. That only equals the real value whenlen(mode)matches the band count and the bands are 8-bit (L,RGB,RGBA,CMYK,LA,P). ForI;16(16-bit),I/F(32-bit) andYCbCr(3-byte), the stride was wrong, soseek(frame >= 1)on a multi-frame IM image raisedOSError: image file is truncated(I;16,YCbCr) or returned pixels from the wrong offset (I,F).8 * bands * itemsize(viaImageMode), which matchesImage.new(mode).tobytes()for every IM-supported mode and leaves the six already-correct modes byte-for-byte unchanged.Reachability, stated honestly: this is a low-frequency bug. Pillow's own
_saveonly 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/seekare 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.