diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 40f3fb44093..2c2ee037227 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -97,6 +97,37 @@ def test_seek_too_far() -> None: im.seek(frame) +def test_seek_frame_with_different_mode() -> None: + # A later frame may use a different mode and pixel size than the first. + # The backing image must not be reused across such a change. + def pcx(bits: int, planes: int, w: int, h: int, body: bytes) -> bytes: + stride = (w * bits + 7) // 8 + stride += stride % 2 + header = bytes([10, 5, 1, bits]) + header += o16(0) + o16(0) + o16(w - 1) + o16(h - 1) + o16(72) + o16(72) + header += bytes(48) + b"\0" + bytes([planes]) + o16(stride) + return header + bytes(128 - len(header)) + body + + w = h = 128 + first_frame = pcx(8, 1, w, h, b"\x41" * (w * h)) + second_frame = pcx(8, 3, w, h, b"\x41" * (3 * w * h)) + b = BytesIO( + o32(DcxImagePlugin.MAGIC) + + o32(16) + + o32(len(first_frame) + 16) + + o32(0) + + first_frame + + second_frame + ) + with Image.open(b) as im: + im.load() + assert im.mode == "L" + im.seek(1) + im.load() + assert im.mode == "RGB" + assert im.getpixel((0, 0)) == (0x41, 0x41, 0x41) + + def test_seek_decompression_bomb() -> None: with open("Tests/images/pil184.pcx", "rb") as fp: first_frame = fp.read() diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index cc384b26ee2..98a1e02acea 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -256,6 +256,22 @@ def test_seek(test_file: str) -> None: assert im.tell() == 0 +def test_seek_frame_with_different_mode() -> None: + # A frame may use a different mode and pixel size than the first, + # so the backing image must not be reused across the change. + out = BytesIO() + Image.new("L", (128, 128)).save( + out, "MPO", save_all=True, append_images=[Image.new("RGB", (128, 128))] + ) + out.seek(0) + with Image.open(out) as im: + im.load() + assert im.mode == "L" + im.seek(1) + im.load() + assert im.mode == "RGB" + + def test_seek_decompression_bomb() -> None: with open("Tests/images/frozenpond.mpo", "rb") as fp: data = fp.read() diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 1047c294438..970b69e2a89 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -112,6 +112,24 @@ def test_seek_tell() -> None: assert layer_number == 2 +def test_seek_layer_with_different_mode() -> None: + # A layer may use a different mode and pixel size than the composite image, + # so the backing image must not be reused across the change. + with open(test_file, "rb") as fp: + data = bytearray(fp.read()) + # Patch the header to report a single grayscale channel for the composite, + # while the RGB/RGBA layer records are left untouched. + data[12:14] = b"\x00\x01" # channels + data[24:26] = b"\x00\x01" # grayscale mode + + with Image.open(io.BytesIO(data)) as im: + im.load() + assert im.mode == "L" + im.seek(im.n_frames) + im.load() + assert im.mode == "RGBA" + + def test_seek_back_to_initial_frame() -> None: # Seeking away from the initial frame and then back should produce the same image with Image.open(test_file) as im: diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index 64762af2622..ff340789fd8 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -77,6 +77,10 @@ def seek(self, frame: int) -> None: self.fp.seek(self._offset[frame]) PcxImageFile._open(self) Image._decompression_bomb_check(self.size) + if self._im is not None and ( + self.im.size != self.size or self.im.mode != self.mode + ): + self._im = None def tell(self) -> int: return self.frame diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index af2e62f7660..946a297074a 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -81,6 +81,10 @@ def seek(self, frame: int) -> None: TiffImagePlugin.TiffImageFile._open(self) Image._decompression_bomb_check(self.size) + if self._im is not None and ( + self.im.size != self.size or self.im.mode != self.mode + ): + self._im = None self.frame = frame diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 41aec322821..6bbc17210d4 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -169,6 +169,10 @@ def seek(self, frame: int) -> None: self.tile = [ ImageFile._Tile("jpeg", (0, 0, *self.size), self.offset, self.tile[0][-1]) ] + if self._im is not None and ( + self.im.size != self.size or self.im.mode != self.mode + ): + self._im = None self.__frame = frame def tell(self) -> int: diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 7d23b1f7110..a23c9c0cf41 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -185,6 +185,10 @@ def seek(self, layer: int) -> None: _, self._mode, _, self.tile = self.layers[layer - 1] self.frame = layer self.fp = self._fp + if self._im is not None and ( + self.im.size != self.size or self.im.mode != self.mode + ): + self._im = None def tell(self) -> int: # return layer number (0=image, 1..max=layers)