diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index cf85ee4fa1c..342bd8654e1 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -91,6 +91,16 @@ def test(mode: str) -> tuple[str, tuple[int, int], bool]: Image.fromarray(wrapped) +def test_fromarray_strides_without_tobytes() -> None: + class Wrapper: + def __init__(self, arr_params: dict[str, Any]) -> None: + self.__array_interface__ = arr_params + + with pytest.raises(ValueError): + wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) + Image.fromarray(wrapped, "L") + + def test_fromarray_palette() -> None: # Arrange i = im.convert("L") diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 4281b182ce7..0d9b4d93d77 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -78,6 +78,8 @@ Constructing images ^^^^^^^^^^^^^^^^^^^ .. autofunction:: new +.. autoclass:: SupportsArrayInterface + :show-inheritance: .. autofunction:: fromarray .. autofunction:: frombytes .. autofunction:: frombuffer diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 245fadd6b3f..7e68ee3cb40 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,7 +41,7 @@ from collections.abc import Callable, MutableMapping from enum import IntEnum from types import ModuleType -from typing import IO, TYPE_CHECKING, Any +from typing import IO, TYPE_CHECKING, Any, Protocol # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -3013,7 +3013,7 @@ def frombytes(mode, size, data, decoder_name="raw", *args) -> Image: return im -def frombuffer(mode, size, data, decoder_name="raw", *args): +def frombuffer(mode, size, data, decoder_name="raw", *args) -> Image: """ Creates an image memory referencing pixel data in a byte buffer. @@ -3069,10 +3069,15 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): return frombytes(mode, size, data, decoder_name, args) -def fromarray( - obj, # type: numpy.typing.ArrayLike - mode: str | None = None, -) -> Image: +class SupportsArrayInterface(Protocol): + """ + An object that has an ``__array_interface__`` dictionary. + """ + + __array_interface__: dict[str, Any] + + +def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: """ Creates an image memory from an object exporting the array interface (using the buffer protocol):: @@ -3151,8 +3156,11 @@ def fromarray( if strides is not None: if hasattr(obj, "tobytes"): obj = obj.tobytes() - else: + elif hasattr(obj, "tostring"): obj = obj.tostring() + else: + msg = "'strides' requires either tobytes() or tostring()" + raise ValueError(msg) return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)