Skip to content

"ValueError: tile cannot extend outside image" for JPEG files exported from macOS Photos app #9204

@gcox

Description

@gcox

What did you do?

I'm using Pillow indirectly through another framework that, when encountering a multi-frame image, iterates over the frames in the image, seeks to each one, and creates a copy of each for further processing using img.copy().

I found that an exception is thrown when processing certain jpeg images. Specifically, when a photo has been exported from the macOS Photos app and the output width was limited to something smaller than the original image.

When exporting images as JPEG from the Photos app, it produces files that include two frames:

  • Frame 0: Mode = RGB
  • Frame 1: Mode = L

For the example image, the original size of the image was 3024x4032.

When exporting and keeping the original size, the two frames are:

  • Frame 0: Mode = RGB, Size = 3024x4032
  • Frame 1: Mode = L, Size = 1512x2016

For the example image, I exported it with a custom width of 1024, and the two frames in the resulting image are:

  • Frame 0: Mode = RGB, Size = 1024x1366
  • Frame 1: Mode = L, Size = 1512x2016

It seems that frame 1 being larger than frame 0 is a problem.

What did you expect to happen?

No exception to be thrown when iterating over the individual frames, seeking to them, and calling img.copy()

What actually happened?

An exception is thrown when calling im.copy() after seeking to the second frame *if the second frame's dimensions are larger than the first frame's.

What are your OS, Python and Pillow versions?

  • OS: macOS 15.6.1
  • Python: 3.12.11 - 3.13.7 (all I've tried)
  • Pillow: 11.3+

Reproduction

  • Copy the example image at the bottom into Tests/images as jpeg_tile_error.jpeg
  • Add and run the following test case.
  • An exception is thrown when calling im.copy() on the second frame.
TEST_FILE = "Tests/images/jpeg_tile_error.jpeg"

@skip_unless_feature("jpg")
class TestFileJpegTileError:
    def test_copy(self) -> None:
        # Test img.copy()
        with Image.open(TEST_FILE) as im:
            assert isinstance(im, JpegImagePlugin.JpegImageFile)
            if hasattr(im, "n_frames") and im.n_frames > 1:
                for i in range(im.n_frames):
                    im.seek(i)
                    print(f"Frame {i}, size: {im.size}, mode: {im.mode}, format: {im.format}, get_format_mimetype: {im.get_format_mimetype()}")
                    im_copy = im.copy()
Example Image (jpeg_tile_error.jpeg)

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions