Skip to content

Correct saving more than two MPO frames - #10326

Merged
radarhere merged 3 commits into
python-pillow:mainfrom
umtong:fix-mpo-frame-offsets
Oct 27, 2026
Merged

radarhere merged 3 commits into
python-pillow:mainfrom
umtong:fix-mpo-frame-offsets

Conversation

@umtong

@umtong umtong commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Saving an MPO with four or more frames currently produces incorrect MP Index offsets, so the later frames cannot be read back. The third frame already has an incorrect size in the index, although Pillow can still decode it because its starting offset is valid.

_save_all() stores individual JPEG lengths in offsets, but subtracts the last of those lengths from the absolute file position after writing the next JPEG. This only gives the correct length for the second frame. Capture the file position before each appended JPEG instead.

Changes proposed in this pull request:

  • Calculate each appended frame's size from its own starting file position.
  • Cover two through five frames, with unequal encoded sizes and with or without EXIF. The tests decode every frame, compare its pixels with an independently encoded JPEG, and check the MP Index sizes, header-relative offsets and JPEG boundaries.
  • Add a release note.

For a four-frame example, the JPEG lengths are [771, 637, 653, 661]. Before this change, the MP Index records [771, 637, 1424, 1298] and places the fourth frame at byte 2832 instead of 2061. seek(3) raises ValueError: No data found for frame. With this change, the index agrees with the JPEG positions and all four frames decode. The same checks pass when EXIF shifts the MP header from byte 28 to byte 80.

Validation:

  • New regression cases on the unmodified writer: 6 failed, 2 passed.
  • python3 selftest.py: 59 passed.
  • python3 -m pytest -q Tests/test_file_mpo.py: 43 passed.
  • python3 -m pytest -q Tests/: 4,891 passed, 885 skipped, 3 xfailed. JPEG and zlib were enabled; unavailable optional codecs and tools were skipped.
  • Pre-commit checks passed for all three changed files.

Autonomous contribution by ChatGPT (GPT-6 Pro).

@Muguett-DBY Muguett-DBY left a comment

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.

Capturing the position before each JPEG fixes the length-versus-position mix-up. With the writer from 495a8dbb033aa8dead060ad38fb547d7cc0940e1, all eight added regression bodies pass; six fail with the base writer.

I also checked the index against independently parsed JPEG boundaries, covering RGB/grayscale, EXIF bytes, progressive/optimized JPEG and ICC. Re-saving two existing MPO sequences into one also preserves the frame sizes, offsets and decoded pixels, including backward seeks.

One separate existing corner case showed up with CMYK: the Adobe APP14 puts the MPF TIFF header at byte 26, while the writer assumes byte 28. Both the base and this writer put the TIFF bytes two bytes late, even with just two frames. That appears suitable for a separate fix.

These checks call the exact _save_all functions with the existing Pillow 12.3.0 JPEG encoder and MPO reader on Windows/Python 3.12. I haven't built this branch or run pytest/the full suite. Review and checks assisted by Codex.

@radarhere
radarhere force-pushed the fix-mpo-frame-offsets branch from 7aa2a64 to 287c21d Compare October 27, 2026 11:58
@radarhere radarhere changed the title Fix MPO frame sizes and offsets when saving multiple images Correct saving more than two MPO frames Oct 27, 2026
@radarhere
radarhere enabled auto-merge (squash) October 27, 2026 12:01
@radarhere
radarhere merged commit 94c62b0 into python-pillow:main Oct 27, 2026
66 of 69 checks passed
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