diff --git a/Tests/helper.py b/Tests/helper.py index 924b9733403..bcf94f595cc 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -292,10 +292,6 @@ def djpeg_available() -> bool: return False -def netpbm_available() -> bool: - return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) - - def magick_command() -> list[str] | None: if sys.platform == "win32": magickhome = os.environ.get("MAGICK_HOME") diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index df11105fb04..ed5d28137e2 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -14,7 +14,6 @@ assert_image_similar, hopper, is_pypy, - netpbm_available, ) TYPE_CHECKING = False @@ -397,30 +396,6 @@ def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: assert_image_equal(im_rgb, reloaded) -@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") -def test_save_netpbm_bmp_mode(tmp_path: Path) -> None: - with Image.open(TEST_GIF) as img: - img_rgb = img.convert("RGB") - - tempfile = str(tmp_path / "temp.gif") - b = BytesIO() - GifImagePlugin._save_netpbm(img_rgb, b, tempfile) - with Image.open(tempfile) as reloaded: - assert_image_equal(img_rgb, reloaded.convert("RGB")) - - -@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") -def test_save_netpbm_l_mode(tmp_path: Path) -> None: - with Image.open(TEST_GIF) as img: - img_l = img.convert("L") - - tempfile = str(tmp_path / "temp.gif") - b = BytesIO() - GifImagePlugin._save_netpbm(img_l, b, tempfile) - with Image.open(tempfile) as reloaded: - assert_image_equal(img_l, reloaded.convert("L")) - - def test_seek() -> None: with Image.open("Tests/images/dispose_none.gif") as img: assert isinstance(img, GifImagePlugin.GifImageFile) diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index a7e95ed83c8..18dda2d4957 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -5,9 +5,9 @@ import pytest -from PIL import GifImagePlugin, Image, JpegImagePlugin +from PIL import Image, JpegImagePlugin -from .helper import djpeg_available, is_win32, netpbm_available +from .helper import djpeg_available, is_win32 TYPE_CHECKING = False if TYPE_CHECKING: @@ -45,17 +45,3 @@ def test_load_djpeg_filename(self, tmp_path: Path) -> None: with Image.open(src_file) as im: assert isinstance(im, JpegImagePlugin.JpegImageFile) im.load_djpeg() - - @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") - def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None: - with Image.open(TEST_GIF) as im: - im_rgb = im.convert("RGB") - self.assert_save_filename_check( - tmp_path, im_rgb, GifImagePlugin._save_netpbm - ) - - @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") - def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None: - with Image.open(TEST_GIF) as im: - im_l = im.convert("L") - self.assert_save_filename_check(tmp_path, im_l, GifImagePlugin._save_netpbm) diff --git a/docs/releasenotes/13.0.0.rst b/docs/releasenotes/13.0.0.rst index 45667d9ec07..42106648cff 100644 --- a/docs/releasenotes/13.0.0.rst +++ b/docs/releasenotes/13.0.0.rst @@ -55,7 +55,7 @@ and so child images could only be retrieved from an :py:class:`PIL.ImageFile.Ima instance. Image._show -~~~~~~~~~~~ +^^^^^^^^^^^ ``Image._show`` has been removed. Use :py:meth:`~PIL.ImageShow.show` instead. @@ -66,6 +66,23 @@ ImageCms.ImageCmsProfile.product_name and .product_info ``.product_info`` attributes have been removed. They were set to ``None`` since Pillow 2.3.0. +GifImagePlugin._save_netpbm() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``GifImagePlugin._save_netpbm()`` has been removed. + +It was never registered as a save handler by default and delegated GIF compression +and RGB quantization to the external NetPBM ``ppmquant`` and ``ppmtogif`` utilities. + +Pillow's own GIF encoder writes LZW-compressed data since Pillow 8.2.0 (2021). +Saving an ``RGB`` image as GIF converts it automatically using an adaptive 256-color +palette since Pillow 2.4.0 (2014). + +To control quantization yourself, convert the image to ``P`` mode first, +e.g. with :py:meth:`~PIL.Image.Image.quantize()`:: + + im.quantize(colors=256).save("out.gif") + Deprecations ============ diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index e686dc80851..b230e952dbb 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -27,8 +27,6 @@ import itertools import math -import os -import subprocess from enum import IntEnum from functools import cached_property from typing import NamedTuple, cast @@ -873,54 +871,6 @@ def _write_local_header( fp.write(o8(8)) # bits -def _save_netpbm(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - # Unused by default. - # To use, uncomment the register_save call at the end of the file. - # - # If you need real GIF compression and/or RGB quantization, you - # can use the external NETPBM/PBMPLUS utilities. See comments - # below for information on how to enable this. - tempfile = im._dump() - - try: - with open(filename, "wb") as f: - if im.mode != "RGB": - subprocess.check_call( - ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL - ) - else: - # Pipe ppmquant output into ppmtogif - # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename) - quant_cmd = ["ppmquant", "256", tempfile] - togif_cmd = ["ppmtogif"] - quant_proc = subprocess.Popen( - quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL - ) - togif_proc = subprocess.Popen( - togif_cmd, - stdin=quant_proc.stdout, - stdout=f, - stderr=subprocess.DEVNULL, - ) - - # Allow ppmquant to receive SIGPIPE if ppmtogif exits - assert quant_proc.stdout is not None - quant_proc.stdout.close() - - retcode = quant_proc.wait() - if retcode: - raise subprocess.CalledProcessError(retcode, quant_cmd) - - retcode = togif_proc.wait() - if retcode: - raise subprocess.CalledProcessError(retcode, togif_cmd) - finally: - try: - os.unlink(tempfile) - except OSError: - pass - - # Force optimization so that we can test performance against # cases where it took lots of memory and time previously. _FORCE_OPTIMIZE = False @@ -1215,9 +1165,3 @@ def write(self, data: Buffer) -> int: Image.register_save_all(GifImageFile.format, _save_all) Image.register_extension(GifImageFile.format, ".gif") Image.register_mime(GifImageFile.format, "image/gif") - -# -# Uncomment the following line if you wish to use NETPBM/PBMPLUS -# instead of the built-in "uncompressed" GIF encoder - -# Image.register_save(GifImageFile.format, _save_netpbm)