From 27d85ba5a102df6519fe0e9bc27d505627429c3d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Aug 2026 21:48:01 +1000 Subject: [PATCH] Deprecate IM image format --- Tests/test_file_im.py | 86 +++++++++++++++++----------- Tests/test_imagefile.py | 3 +- Tests/test_mode_i16.py | 2 +- docs/deprecations.rst | 9 ++- docs/handbook/image-file-formats.rst | 2 + docs/releasenotes/13.0.0.rst | 9 ++- src/PIL/ImImagePlugin.py | 5 ++ src/PIL/_deprecate.py | 2 + 8 files changed, 78 insertions(+), 40 deletions(-) diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index dfd33078847..e88be53b8d1 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -15,24 +15,28 @@ def test_sanity() -> None: - with Image.open(TEST_IM) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == "IM" + with pytest.warns(DeprecationWarning, match="IM image format"): + im = Image.open(TEST_IM) + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "IM" + im.close() def test_name_limit(tmp_path: Path) -> None: out = tmp_path / ("name_limit_test" * 7 + ".im") - with Image.open(TEST_IM) as im: - im.save(out) + with pytest.warns(DeprecationWarning, match="IM image format"): + with Image.open(TEST_IM) as im: + im.save(out) assert filecmp.cmp(out, "Tests/images/hopper_long_name.im") @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: def open_test_image() -> None: - im = Image.open(TEST_IM) + with pytest.warns(DeprecationWarning, match="IM image format"): + im = Image.open(TEST_IM) im.load() with pytest.warns(ResourceWarning): @@ -41,54 +45,63 @@ def open_test_image() -> None: def test_closed_file() -> None: with warnings.catch_warnings(action="error"): - im = Image.open(TEST_IM) + with pytest.warns(DeprecationWarning, match="IM image format"): + im = Image.open(TEST_IM) im.load() im.close() def test_context_manager() -> None: with warnings.catch_warnings(action="error"): - with Image.open(TEST_IM) as im: - im.load() + with pytest.warns(DeprecationWarning, match="IM image format"): + with Image.open(TEST_IM) as im: + im.load() def test_tell() -> None: # Arrange - with Image.open(TEST_IM) as im: - # Act - frame = im.tell() + with pytest.warns(DeprecationWarning, match="IM image format"): + with Image.open(TEST_IM) as im: + # Act + frame = im.tell() # Assert assert frame == 0 def test_n_frames() -> None: - with Image.open(TEST_IM) as im: - assert isinstance(im, ImImagePlugin.ImImageFile) - assert im.n_frames == 1 - assert not im.is_animated + with pytest.warns(DeprecationWarning, match="IM image format"): + im = Image.open(TEST_IM) + assert isinstance(im, ImImagePlugin.ImImageFile) + assert im.n_frames == 1 + assert not im.is_animated + im.close() def test_eoferror() -> None: - with Image.open(TEST_IM) as im: - assert isinstance(im, ImImagePlugin.ImImageFile) - n_frames = im.n_frames + with pytest.warns(DeprecationWarning, match="IM image format"): + im = Image.open(TEST_IM) + assert isinstance(im, ImImagePlugin.ImImageFile) + n_frames = im.n_frames - # Test seeking past the last frame - with pytest.raises(EOFError): - im.seek(n_frames) - assert im.tell() < n_frames + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) + im.close() @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) def test_roundtrip(mode: str, tmp_path: Path) -> None: out = tmp_path / "temp.im" im = hopper(mode) - im.save(out) - assert_image_equal_tofile(im, out) + with pytest.warns(DeprecationWarning, match="IM image format"): + im.save(out) + with pytest.warns(DeprecationWarning, match="IM image format"): + assert_image_equal_tofile(im, out) def test_small_palette(tmp_path: Path) -> None: @@ -97,17 +110,21 @@ def test_small_palette(tmp_path: Path) -> None: im.putpalette(colors) out = tmp_path / "temp.im" - im.save(out) + with pytest.warns(DeprecationWarning, match="IM image format"): + im.save(out) - with Image.open(out) as reloaded: - assert reloaded.getpalette() == colors + [0] * 765 + with pytest.warns(DeprecationWarning, match="IM image format"): + reloaded = Image.open(out) + assert reloaded.getpalette() == colors + [0] * 765 + reloaded.close() def test_save_unsupported_mode(tmp_path: Path) -> None: out = tmp_path / "temp.im" im = hopper("HSV") with pytest.raises(ValueError): - im.save(out) + with pytest.warns(DeprecationWarning, match="IM image format"): + im.save(out) def test_invalid_file() -> None: @@ -118,4 +135,5 @@ def test_invalid_file() -> None: def test_number() -> None: - assert ImImagePlugin.number("1.2") == 1.2 + with pytest.warns(DeprecationWarning, match="IM image format"): + assert ImImagePlugin.number("1.2") == 1.2 diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 2aedbe00528..5e7add81c72 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -52,7 +52,8 @@ def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: assert_image_equal(*roundtrip("BMP")) im1, im2 = roundtrip("GIF") assert_image_similar(im1.convert("P"), im2, 1) - assert_image_equal(*roundtrip("IM")) + with pytest.warns(DeprecationWarning, match="IM image format"): + assert_image_equal(*roundtrip("IM")) assert_image_equal(*roundtrip("MSP")) if features.check("zlib"): # force multiple blocks in PNG driver diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index b78b7984fe2..4a36b05b328 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -44,7 +44,7 @@ def test_basic(tmp_path: Path, mode: str) -> None: im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) verify(im_out) # transform - filename = tmp_path / "temp.im" + filename = tmp_path / "temp.tiff" im_in.save(filename) with Image.open(filename) as im_out: diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 70745104483..41a7524c09c 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -21,7 +21,7 @@ ExifTags.IFD.Makernote ``ExifTags.IFD.MakerNote``. Image getdata() -~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^ .. deprecated:: 12.1.0 @@ -30,6 +30,13 @@ Image getdata() identical, except that it returns a tuple of pixel values, instead of an internal Pillow data type. +IM image format +^^^^^^^^^^^^^^^ + +.. deprecated:: 13.0.0 + +The IM image format has been deprecated. + Removed features ---------------- diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 6b38b7278e8..5e1e0c45a4e 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -473,6 +473,8 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum IM ^^ +.. deprecated:: 13.0.0 + IM is a format used by LabEye and other applications based on the IFUNC image processing library. The library reads and writes most uncompressed interchange versions of this format. diff --git a/docs/releasenotes/13.0.0.rst b/docs/releasenotes/13.0.0.rst index 45667d9ec07..060f381bcbc 100644 --- a/docs/releasenotes/13.0.0.rst +++ b/docs/releasenotes/13.0.0.rst @@ -69,10 +69,13 @@ ImageCms.ImageCmsProfile.product_name and .product_info Deprecations ============ -TODO -^^^^ +IM image format +^^^^^^^^^^^^^^^ -TODO +.. deprecated:: 13.0.0 + +The IM image format has been deprecated. If you are using this format and would like +to continue doing so, please provide a report about what other software uses it. API changes =========== diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index ef54f16e97e..fb5851cef5c 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -31,6 +31,7 @@ from typing import IO, Any from . import Image, ImageFile, ImagePalette +from ._deprecate import deprecate from ._util import DeferredError # -------------------------------------------------------------------- @@ -106,6 +107,7 @@ def number(s: Any) -> float: + deprecate("IM image format", 15) try: return int(s) except ValueError: @@ -206,6 +208,8 @@ def _open(self) -> None: msg = "Not an IM file" raise SyntaxError(msg) + deprecate("IM image format", 15) + # Basic attributes self._size = self.info[SIZE] self._mode = self.info[MODE] @@ -341,6 +345,7 @@ def tell(self) -> int: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + deprecate("IM image format", 15) try: image_type, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index ff18bbea2e5..2921befbaa6 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -48,6 +48,8 @@ def deprecate( raise RuntimeError(msg) elif when == 14: removed = "Pillow 14 (2027-10-15)" + elif when == 15: + removed = "Pillow 15 (2028-10-15)" else: msg = f"Unknown removal version: {when}. Update {__name__}?" raise ValueError(msg)