Skip to content
Draft
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
110 changes: 99 additions & 11 deletions Tests/test_image_getdata.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
from __future__ import annotations

import operator

import pytest

from PIL import Image

from .helper import hopper


def test_sanity() -> None:
data = hopper().get_flattened_data()
@pytest.mark.parametrize("method", ["getdata", "get_flattened_data"])
def test_sanity(method: str) -> None:
im = hopper()

data = getattr(im, method)()
assert len(data) == 128 * 128
assert data[0] == (20, 20, 70)
assert data[-1] == data[128 * 128 - 1]

# And the single-band case
band = getattr(im, method)(0)
assert len(band) == 128 * 128
assert band[0] == 20
assert band[-1] == band[128 * 128 - 1]

def test_mode() -> None:

@pytest.mark.parametrize("method", ["getdata", "get_flattened_data"])
def test_mode(method: str) -> None:
def getdata(mode: str) -> tuple[float | tuple[int, ...] | None, int, int]:
im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST)
data = im.get_flattened_data()
data = getattr(im, method)()
return data[0], len(data), len(list(data))

assert getdata("1") == (0, 960, 960)
Expand All @@ -30,11 +42,87 @@ def getdata(mode: str) -> tuple[float | tuple[int, ...] | None, int, int]:
assert getdata("YCbCr") == ((16, 147, 123), 960, 960)


def test_deprecation() -> None:
im = hopper()
with pytest.warns(DeprecationWarning, match="getdata"):
data = im.getdata()
def test_index_out_of_range() -> None:
hopper_data = hopper().getdata()
for index in (128 * 128, -128 * 128 - 1, 1 << 40):
with pytest.raises(IndexError):
_ = hopper_data[index]

assert len(data) == 128 * 128
assert data[0] == (20, 20, 70)
assert list(data)[0] == (20, 20, 70)

def test_iteration_matches_indexing() -> None:
hopper_data = hopper().getdata()
as_list = list(hopper_data)
assert len(as_list) == len(hopper_data)
assert as_list == [hopper_data[i] for i in range(len(hopper_data))]


def test_iteration_is_repeatable() -> None:
hopper_data = hopper().getdata()
assert list(hopper_data) == list(hopper_data)


def test_exhausted_iterator_stays_exhausted() -> None:
hopper_data = hopper().getdata()
it = iter(hopper_data)
assert len(list(it)) == len(hopper_data)
assert list(it) == []
with pytest.raises(StopIteration):
next(it)


def test_length_hint() -> None:
hopper_data = hopper().getdata()
it = iter(hopper_data)
assert operator.length_hint(it) == len(hopper_data)
next(it)
assert operator.length_hint(it) == len(hopper_data) - 1
list(it)
assert operator.length_hint(it) == 0


def test_matches_get_flattened_data() -> None:
assert tuple(hopper().getdata()) == hopper().get_flattened_data()


def test_is_read_only() -> None:
hopper_data = hopper().getdata()
with pytest.raises(TypeError):
hopper_data[0] = (0, 0, 0) # type: ignore[index]


def test_getdata_does_not_expose_the_image_core() -> None:
hopper_data = hopper().getdata()
# "Weird" core bits are not exposed:
for name in ["putpixel", "paste", "ptr", "putdata"]:
assert not hasattr(hopper_data, name)


@pytest.mark.parametrize("size", [(0, 0), (0, 5), (5, 0)])
def test_empty_image(size: tuple[int, int]) -> None:
data = Image.new("RGB", size).getdata()
assert len(data) == 0
assert list(data) == []
with pytest.raises(IndexError):
_ = data[0]


def test_outlives_the_image() -> None:
im = Image.new("RGB", (2, 2), (1, 2, 3))
data = im.getdata()
del im
assert list(data) == [(1, 2, 3)] * 4


def test_is_a_live_view() -> None:
im = Image.new("RGB", (2, 2), (0, 0, 0))
data = im.getdata()
im.putpixel((0, 0), (9, 9, 9))
assert data[0] == (9, 9, 9)


def test_accepted_by_putdata() -> None:
im = hopper()
hopper_data = im.getdata()
out = Image.new("RGB", im.size)
out.putdata(hopper_data)
assert out.tobytes() == im.tobytes()
9 changes: 0 additions & 9 deletions docs/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,6 @@ ExifTags.IFD.Makernote
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
``ExifTags.IFD.MakerNote``.

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
----------------
Expand Down
11 changes: 11 additions & 0 deletions docs/releasenotes/13.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ TODO
API changes
===========

Image getdata()
^^^^^^^^^^^^^^^

:py:meth:`~PIL.Image.Image.getdata` had been marked deprecated since 12.1.0,
to be removed in Pillow 14, as it returned a poorly specified internal Pillow data type.

:py:meth:`~PIL.Image.Image.getdata` now returns an ``ImageLinearAccess`` object,
a read-only, lazy view onto the image, supporting the same operations that had been
documented for the old return value (``len()``, indexing and iteration).


TODO
^^^^

Expand Down
16 changes: 10 additions & 6 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
_plugins,
)
from ._binary import i32le, o32be, o32le
from ._deprecate import deprecate
from ._util import DeferredError, is_path

ElementTree: ModuleType | None
Expand Down Expand Up @@ -1513,7 +1512,7 @@ def getcolors(
return out
return self.im.getcolors(maxcolors)

def getdata(self, band: int | None = None) -> core.ImagingCore:
def getdata(self, band: int | None = None) -> core.ImageLinearAccess:
"""
Returns the contents of this image as a sequence object
containing pixel values. The sequence object is flattened, so
Expand All @@ -1530,12 +1529,11 @@ def getdata(self, band: int | None = None) -> core.ImagingCore:
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
return self.im.getband(band).linear_access()
return self.im.linear_access()

def get_flattened_data(
self, band: int | None = None
Expand Down Expand Up @@ -2089,7 +2087,13 @@ def putalpha(self, alpha: Image | int) -> None:

def putdata(
self,
data: Sequence[float] | Sequence[Sequence[int]] | core.ImagingCore | NumpyArray,
data: (
Sequence[float]
| Sequence[Sequence[int]]
| core.ImagingCore
| core.ImageLinearAccess
| NumpyArray
),
scale: float = 1.0,
offset: float = 0.0,
) -> None:
Expand Down
6 changes: 6 additions & 0 deletions src/PIL/_imaging.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections.abc import Iterator
from typing import Any

class ImagingCore:
Expand All @@ -16,6 +17,11 @@ class PixelAccess:
self, xy: tuple[int, int], color: float | tuple[int, ...]
) -> None: ...

class ImageLinearAccess:
def __len__(self) -> int: ...
def __getitem__(self, index: int) -> float | tuple[int, ...]: ...
def __iter__(self) -> Iterator[float | tuple[int, ...]]: ...

class ImagingDecoder:
def __getattr__(self, name: str) -> Any: ...

Expand Down
Loading
Loading