diff --git a/Tests/helper.py b/Tests/helper.py index b93828f58..d77b4b807 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -55,8 +55,8 @@ def convert_to_comparable( if a.mode == "P": new_a = Image.new("L", a.size) new_b = Image.new("L", b.size) - new_a.putdata(a.getdata()) - new_b.putdata(b.getdata()) + new_a.putdata(a.get_flattened_data()) + new_b.putdata(b.get_flattened_data()) elif a.mode == "I;16": new_a = a.convert("I") new_b = b.convert("I") diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index cb267b204..07e62db8c 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -28,9 +28,13 @@ def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image: def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None: - it = iter(im.getdata()) + it = iter(im.get_flattened_data()) for data_row in data: - im_row = [next(it) for _ in range(im.size[0])] + im_row = [] + for _ in range(im.width): + im_v = next(it) + assert isinstance(im_v, (int, float)) + im_row.append(im_v) if any(abs(data_v - im_v) > delta for data_v, im_v in zip(data_row, im_row)): assert im_row == data_row with pytest.raises(StopIteration): diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 727191153..ffc4ce021 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -121,7 +121,6 @@ class TestFileAvif: assert image.size == (128, 128) assert image.format == "AVIF" assert image.get_format_mimetype() == "image/avif" - image.getdata() # generated with: # avifdec hopper.avif hopper_avif_write.png @@ -143,7 +142,6 @@ class TestFileAvif: assert reloaded.mode == "RGB" assert reloaded.size == (128, 128) assert reloaded.format == "AVIF" - reloaded.getdata() # avifdec hopper.avif avif/hopper_avif_write.png assert_image_similar_tofile( diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 90ca2cf12..c2336c058 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -42,7 +42,6 @@ class LibTiffTestCase: # Does the data actually load im.load() - im.getdata() assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im._compression == "group4" diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 5456adf59..f996cce67 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -60,7 +60,6 @@ class TestFileWebp: assert image.size == (128, 128) assert image.format == "WEBP" image.load() - image.getdata() # generated with: # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm @@ -77,7 +76,6 @@ class TestFileWebp: assert image.size == (128, 128) assert image.format == "WEBP" image.load() - image.getdata() if mode == self.rgb_mode: # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index c573390c4..b1aa45f6b 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -29,7 +29,6 @@ def test_read_rgba() -> None: assert image.size == (200, 150) assert image.format == "WEBP" image.load() - image.getdata() image.tobytes() @@ -60,7 +59,6 @@ def test_write_lossless_rgb(tmp_path: Path) -> None: assert image.size == pil_image.size assert image.format == "WEBP" image.load() - image.getdata() assert_image_equal(image, pil_image) @@ -83,7 +81,6 @@ def test_write_rgba(tmp_path: Path) -> None: assert image.size == (10, 10) assert image.format == "WEBP" image.load() - image.getdata() assert_image_similar(image, pil_image, 1.0) @@ -133,7 +130,6 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None: assert image.format == "WEBP" image.load() - image.getdata() with Image.open(file_path) as im: target = im.convert("RGBA") diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 5eaa4f599..b4c0448ac 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -24,6 +24,5 @@ def test_write_lossless_rgb(tmp_path: Path) -> None: assert image.size == (128, 128) assert image.format == "WEBP" image.load() - image.getdata() assert_image_equal(image, hopper(RGB_MODE)) diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index 4fcc37e88..5f4a704f2 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -13,15 +13,15 @@ def test_white() -> None: k = i.getpixel((0, 0)) - L = i.getdata(0) - a = i.getdata(1) - b = i.getdata(2) + L = i.get_flattened_data(0) + a = i.get_flattened_data(1) + b = i.get_flattened_data(2) assert k == (255, 128, 128) - assert list(L) == [255] * 100 - assert list(a) == [128] * 100 - assert list(b) == [128] * 100 + assert L == (255,) * 100 + assert a == (128,) * 100 + assert b == (128,) * 100 def test_green() -> None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 88f55638e..afc6e8e16 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1181,10 +1181,10 @@ class TestImageBytes: assert reloaded.tobytes() == source_bytes @pytest.mark.parametrize("mode", Image.MODES) - def test_getdata_putdata(self, mode: str) -> None: + def test_get_flattened_data_putdata(self, mode: str) -> None: im = hopper(mode) reloaded = Image.new(mode, im.size) - reloaded.putdata(im.getdata()) + reloaded.putdata(im.get_flattened_data()) assert_image_equal(im, reloaded) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index abb22f949..220e128d1 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -78,7 +78,7 @@ def test_fromarray() -> None: }, ) out = Image.fromarray(wrapped) - return out.mode, out.size, list(i.getdata()) == list(out.getdata()) + return out.mode, out.size, i.get_flattened_data() == out.get_flattened_data() # assert test("1") == ("1", (128, 100), True) assert test("L") == ("L", (128, 100), True) diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index b90ce84bc..9df8883a4 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -95,10 +95,10 @@ def test_crop_zero() -> None: cropped = im.crop((10, 10, 20, 20)) assert cropped.size == (10, 10) - assert cropped.getdata()[0] == (0, 0, 0) + assert cropped.getpixel((0, 0)) == (0, 0, 0) im = Image.new("RGB", (0, 0)) cropped = im.crop((10, 10, 20, 20)) assert cropped.size == (10, 10) - assert cropped.getdata()[2] == (0, 0, 0) + assert cropped.getpixel((2, 0)) == (0, 0, 0) diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index c8b213d84..94d6cbaa2 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,23 +1,23 @@ from __future__ import annotations +import pytest + from PIL import Image from .helper import hopper def test_sanity() -> None: - data = hopper().getdata() - - len(data) - list(data) + data = hopper().get_flattened_data() + assert len(data) == 128 * 128 assert data[0] == (20, 20, 70) def test_mode() -> None: def getdata(mode: str) -> tuple[float | tuple[int, ...] | None, int, int]: im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) - data = im.getdata() + data = im.get_flattened_data() return data[0], len(data), len(list(data)) assert getdata("1") == (0, 960, 960) @@ -28,3 +28,13 @@ def test_mode() -> None: assert getdata("RGBA") == ((11, 13, 52, 255), 960, 960) assert getdata("CMYK") == ((244, 242, 203, 0), 960, 960) assert getdata("YCbCr") == ((16, 147, 123), 960, 960) + + +def test_deprecation() -> None: + im = hopper() + with pytest.warns(DeprecationWarning, match="getdata"): + data = im.getdata() + + assert len(data) == 128 * 128 + assert data[0] == (20, 20, 70) + assert list(data)[0] == (20, 20, 70) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index bf8e89b53..226cb4c14 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -2,6 +2,7 @@ from __future__ import annotations import sys from array import array +from typing import cast import pytest @@ -12,21 +13,19 @@ from .helper import assert_image_equal, hopper def test_sanity() -> None: im1 = hopper() + for data in (im1.get_flattened_data(), im1.im): + im2 = Image.new(im1.mode, im1.size, 0) + im2.putdata(data) - data = list(im1.getdata()) + assert_image_equal(im1, im2) - im2 = Image.new(im1.mode, im1.size, 0) - im2.putdata(data) + # readonly + im2 = Image.new(im1.mode, im2.size, 0) + im2.readonly = 1 + im2.putdata(data) - assert_image_equal(im1, im2) - - # readonly - im2 = Image.new(im1.mode, im2.size, 0) - im2.readonly = 1 - im2.putdata(data) - - assert not im2.readonly - assert_image_equal(im1, im2) + assert not im2.readonly + assert_image_equal(im1, im2) def test_long_integers() -> None: @@ -60,22 +59,22 @@ def test_mode_with_L_with_float() -> None: @pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) def test_mode_i(mode: str) -> None: src = hopper("L") - data = list(src.getdata()) + data = src.get_flattened_data() im = Image.new(mode, src.size, 0) im.putdata(data, 2, 256) - target = [2 * elt + 256 for elt in data] - assert list(im.getdata()) == target + target = tuple(2 * elt + 256 for elt in cast(tuple[int, ...], data)) + assert im.get_flattened_data() == target def test_mode_F() -> None: src = hopper("L") - data = list(src.getdata()) + data = src.get_flattened_data() im = Image.new("F", src.size, 0) im.putdata(data, 2.0, 256.0) - target = [2.0 * float(elt) + 256.0 for elt in data] - assert list(im.getdata()) == target + target = tuple(2.0 * float(elt) + 256.0 for elt in cast(tuple[int, ...], data)) + assert im.get_flattened_data() == target def test_array_B() -> None: @@ -86,7 +85,7 @@ def test_array_B() -> None: im = Image.new("L", (150, 100)) im.putdata(arr) - assert len(im.getdata()) == len(arr) + assert len(im.get_flattened_data()) == len(arr) def test_array_F() -> None: @@ -97,7 +96,7 @@ def test_array_F() -> None: arr = array("f", [0.0]) * 15000 im.putdata(arr) - assert len(im.getdata()) == len(arr) + assert len(im.get_flattened_data()) == len(arr) def test_not_flattened() -> None: diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 323d31f51..3e8979a5b 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -160,7 +160,7 @@ class TestImagingCoreResize: r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample) assert r.mode == "RGB" assert r.size == (212, 195) - assert r.getdata()[0] == (0, 0, 0) + assert r.getpixel((0, 0)) == (0, 0, 0) def test_unknown_filter(self) -> None: with pytest.raises(ValueError): diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 5fd7caa7c..a30fb18b8 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -274,13 +274,13 @@ def test_simple_lab() -> None: # not a linear luminance map. so L != 128: assert k == (137, 128, 128) - l_data = i_lab.getdata(0) - a_data = i_lab.getdata(1) - b_data = i_lab.getdata(2) + l_data = i_lab.get_flattened_data(0) + a_data = i_lab.get_flattened_data(1) + b_data = i_lab.get_flattened_data(2) - assert list(l_data) == [137] * 100 - assert list(a_data) == [128] * 100 - assert list(b_data) == [128] * 100 + assert l_data == (137,) * 100 + assert a_data == (128,) * 100 + assert b_data == (128,) * 100 def test_lab_color() -> None: diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index f6acb3aff..113d30755 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -20,21 +20,19 @@ TEST_IMAGE_SIZE = (10, 10) def test_numpy_to_image() -> None: def to_image(dtype: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Image: + data = tuple(range(100)) if bands == 1: if boolean: - data = [0, 255] * 50 - else: - data = list(range(100)) + data = (0, 255) * 50 a = numpy.array(data, dtype=dtype) a.shape = TEST_IMAGE_SIZE i = Image.fromarray(a) - assert list(i.getdata()) == data + assert i.get_flattened_data() == data else: - data = list(range(100)) a = numpy.array([[x] * bands for x in data], dtype=dtype) a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands i = Image.fromarray(a) - assert list(i.getchannel(0).getdata()) == list(range(100)) + assert i.get_flattened_data(0) == tuple(range(100)) return i # Check supported 1-bit integer formats @@ -191,7 +189,7 @@ def test_putdata() -> None: arr = numpy.zeros((15000,), numpy.float32) im.putdata(arr) - assert len(im.getdata()) == len(arr) + assert len(im.get_flattened_data()) == len(arr) def test_resize() -> None: @@ -248,7 +246,7 @@ def test_bool() -> None: a[0][0] = True im2 = Image.fromarray(a) - assert im2.getdata()[0] == 255 + assert im2.getpixel((0, 0)) == 255 def test_no_resource_warning_for_numpy_array() -> None: diff --git a/docs/deprecations.rst b/docs/deprecations.rst index cc5ac283f..b6a7af0a8 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -73,6 +73,16 @@ Image._show ``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15). Use :py:meth:`~PIL.ImageShow.show` instead. +Image getdata() +~~~~~~~~~~~~~~~ + +.. deprecated:: 12.1.0 + +:py:meth:`~PIL.Image.Image.getdata` has been deprecated. +:py:meth:`~PIL.Image.Image.get_flattened_data` can be used instead. This new method is +identical, except that it returns a tuple of pixel values, instead of an internal +Pillow data type. + Removed features ---------------- diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index e68722900..adee49228 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -191,6 +191,7 @@ This helps to get the bounding box coordinates of the input image:: .. automethod:: PIL.Image.Image.getchannel .. automethod:: PIL.Image.Image.getcolors .. automethod:: PIL.Image.Image.getdata +.. automethod:: PIL.Image.Image.get_flattened_data .. automethod:: PIL.Image.Image.getexif .. automethod:: PIL.Image.Image.getextrema .. automethod:: PIL.Image.Image.getpalette diff --git a/docs/releasenotes/12.1.0.rst b/docs/releasenotes/12.1.0.rst index 5aaa52c90..9740b7008 100644 --- a/docs/releasenotes/12.1.0.rst +++ b/docs/releasenotes/12.1.0.rst @@ -1,6 +1,17 @@ 12.1.0 ------ +Deprecations +============ + +Image getdata() +^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.getdata` has been deprecated. +:py:meth:`~PIL.Image.Image.get_flattened_data` can be used instead. This new method is +identical, except that it returns a tuple of pixel values, instead of an internal +Pillow data type. + API changes =========== @@ -13,6 +24,13 @@ To match the behaviour of :py:meth:`~PIL.ImageMorph.LutBuilder.build_lut`, API additions ============= +Image get_flattened_data() +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.get_flattened_data` is identical to the deprecated +:py:meth:`~PIL.Image.Image.getdata`, except that the new method returns a tuple of +pixel values, instead of an internal Pillow data type. + Specify window in ImageGrab on macOS ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index b8755422d..76a0d4ab9 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -753,7 +753,7 @@ def _write_multiple_frames( if delta.mode == "P": # Convert to L without considering palette delta_l = Image.new("L", delta.size) - delta_l.putdata(delta.getdata()) + delta_l.putdata(delta.get_flattened_data()) delta = delta_l mask = ImageMath.lambda_eval( lambda args: args["convert"](args["im"] * 255, "1"), diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b4de099be..57ebea689 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1435,12 +1435,31 @@ class Image: value (e.g. 0 to get the "R" band from an "RGB" image). :returns: A sequence-like object. """ + deprecate("Image.Image.getdata", 14, "get_flattened_data") self.load() if band is not None: return self.im.getband(band) return self.im # could be abused + def get_flattened_data( + self, band: int | None = None + ) -> tuple[tuple[int, ...], ...] | tuple[float, ...]: + """ + Returns the contents of this image as a tuple containing pixel values. + The sequence object is flattened, so that values for line one follow + directly after the values of line zero, and so on. + + :param band: What band to return. The default is to return + all bands. To return a single band, pass in the index + value (e.g. 0 to get the "R" band from an "RGB" image). + :returns: A tuple containing pixel values. + """ + self.load() + if band is not None: + return tuple(self.im.getband(band)) + return tuple(self.im) + def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]: """ Gets the minimum and maximum pixel values for each band in diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 616a9aace..711c62ab2 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -48,6 +48,8 @@ def deprecate( raise RuntimeError(msg) elif when == 13: removed = "Pillow 13 (2026-10-15)" + elif when == 14: + removed = "Pillow 14 (2027-10-15)" else: msg = f"Unknown removal version: {when}. Update {__name__}?" raise ValueError(msg)