Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions Tests/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
25 changes: 0 additions & 25 deletions Tests/test_file_gif.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
assert_image_similar,
hopper,
is_pypy,
netpbm_available,
)

TYPE_CHECKING = False
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 2 additions & 16 deletions Tests/test_shell_injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
19 changes: 18 additions & 1 deletion docs/releasenotes/13.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this function is unimportant enough that we don't need a deprecation period, then I'm also inclined to think it is unimportant enough that we don't need release notes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been bitten by libraries quietly removing functions (last in oracle/oci-python-sdk#878).

Augmented the changelog entry to include some history (since I had it at hand), and an alternate recipe. What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're concerned about removing it without notifying users, then it sounds like a deprecation period is in order. #9186 deprecated the undocumented Image._show() function.

A deprecation period lasts at least a year, so for this PR, it would target Pillow 14.0.0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not overly concerned about a proper deprecation period for an underscore-private feature that was never really documented.

So to avoid a deprecation period, I should remove the changelog entry?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking further,

# 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)

seems similar to our rationale for adding a deprecation period to Image._showxv

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub Code Search seems to imply there is no code using _save_netpbm that is not in one of Pillow's own files).

For _showxv, a similar search today shows only one actual call in code uploaded 2 years ago while _showxv was removed in 4990404, 5 years ago.


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
============

Expand Down
56 changes: 0 additions & 56 deletions src/PIL/GifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)