Repository: python-pillow/Pillow

Issue:
`Image.fromarrow` minimal example does not work
```python
from PIL import Image
import pyarrow as pa
arr = pa.array([0]*(5*5*4), type=pa.uint8())
im = Image.fromarrow(arr, 'RGBA', (5, 5))
```

- Python 3.13.2 (tags/v3.13.2:4f8bb39, Feb  4 2025, 15:23:48) [MSC v.1942 64 bit (AMD64)] on win32
- Pillow 11.2.1
-  pyarrow 19.0.1

```
Traceback (most recent call last):
  File "C:\Users\...\main.py", line 4, in <module>
    im = Image.fromarrow(arr, 'RGBA', (5, 5))
  File "C:\Users\...\AppData\Local\Programs\Python\Python313\Lib\site-packages\PIL\Image.py", line 3373, in fromarrow
    _im = core.new_arrow(mode, size, schema_capsule, array_capsule)
ValueError: Invalid Arrow array mode or size mismatch
```

Is there any special version required?

Workaround: the following seems to work:

```python
from PIL import Image
import pyarrow as pa

mode = "RGB"
size = (1920, 1080)
img = Image.new(mode, size=size, color="grey")
pa_img_arr = pa.array(img)
pa_img = Image.fromarrow(pa_img_arr, mode, size)
pa_img.save("pa_img.jpg")
```

Discussion:
@aclark4life: @TS53 First Arrow bug report, thank you! We will look into this@wiredfool: From the other thread:
```python
from PIL import Image
import pyarrow as pa
fl_uint8_4_type = pa.field(
    "_", pa.list_(pa.field("_", pa.uint8()).with_nullable(False), 4)
).type
arr = pa.array([[0,0,0,0]]*(5*5), type=fl_uint8_4_type)
im = Image.fromarrow(arr, 'RGBA', (5, 5))
```

Looks like it's not falling back from a 4*pixels in uint8 properly, it's actually requiring the logical array for the pixel.

Solution patch:
diff --git a/.ci/install.sh b/.ci/install.sh
index d065e7ab5..acb84f046 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -66,7 +66,7 @@ if [[ $(uname) != CYGWIN* ]]; then
     pushd depends && ./install_raqm.sh && popd
 
     # libavif
-    pushd depends && CMAKE_POLICY_VERSION_MINIMUM=3.5 ./install_libavif.sh && popd
+    pushd depends && ./install_libavif.sh && popd
 
     # extra test images
     pushd depends && ./install_extra_test_images.sh && popd
diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt
index 2e3610478..86ac2e0b2 100644
--- a/.ci/requirements-mypy.txt
+++ b/.ci/requirements-mypy.txt
@@ -4,6 +4,7 @@ IceSpringPySideStubs-PySide6
 ipython
 numpy
 packaging
+pyarrow-stubs
 pytest
 sphinx
 types-atheris
diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
index a6b52064c..1583435c1 100755
--- a/.github/workflows/wheels-dependencies.sh
+++ b/.github/workflows/wheels-dependencies.sh
@@ -39,7 +39,7 @@ ARCHIVE_SDIR=pillow-depends-main
 # Package versions for fresh source builds
 FREETYPE_VERSION=2.13.3
 HARFBUZZ_VERSION=11.2.1
-LIBPNG_VERSION=1.6.47
+LIBPNG_VERSION=1.6.48
 JPEGTURBO_VERSION=3.1.0
 OPENJPEG_VERSION=2.5.3
 XZ_VERSION=5.8.1
diff --git a/Tests/helper.py b/Tests/helper.py
index 909fff879..ec61cd263 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -161,6 +161,12 @@ def assert_tuple_approx_equal(
             pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets))
 
 
+def timeout_unless_slower_valgrind(timeout: float) -> pytest.MarkDecorator:
+    if "PILLOW_VALGRIND_TEST" in os.environ:
+        return pytest.mark.pil_noop_mark()
+    return pytest.mark.timeout(timeout)
+
+
 def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
     reason = f"{feature} not available"
     return pytest.mark.skipif(not features.check(feature), reason=reason)
diff --git a/setup.py b/setup.py
index 5d41e27d9..ab36c6b17 100644
--- a/setup.py
+++ b/setup.py
@@ -224,13 +224,14 @@ def _add_directory(
         path.insert(where, subdir)
 
 
-def _find_include_file(self: pil_build_ext, include: str) -> int:
+def _find_include_file(self: pil_build_ext, include: str) -> str | None:
     for directory in self.compiler.include_dirs:
         _dbg("Checking for include file %s in %s", (include, directory))
-        if os.path.isfile(os.path.join(directory, include)):
+        path = os.path.join(directory, include)
+        if os.path.isfile(path):
             _dbg("Found %s", include)
-            return 1
-    return 0
+            return path
+    return None
 
 
 def _find_library_file(self: pil_build_ext, library: str) -> str | None:
@@ -852,9 +853,13 @@ class pil_build_ext(build_ext):
 
         if feature.want("avif"):
             _dbg("Looking for avif")
-            if _find_include_file(self, "avif/avif.h"):
-                if _find_library_file(self, "avif"):
-                    feature.set("avif", "avif")
+            if avif_h := _find_include_file(self, "avif/avif.h"):
+                with open(avif_h, "rb") as fp:
+                    major_version = int(
+                        fp.read().split(b"#define AVIF_VERSION_MAJOR ")[1].split()[0]
+                    )
+                    if major_version >= 1 and _find_library_file(self, "avif"):
+                        feature.set("avif", "avif")
 
         for f in feature:
             if not feature.get(f) and feature.require(f):
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index 26307817c..f9ade18f9 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -1,5 +1,5 @@
 """
-A Pillow loader for .dds files (S3TC-compressed aka DXTC)
+A Pillow plugin for .dds files (S3TC-compressed aka DXTC)
 Jerome Leclanche <jerome@leclan.ch>
 
 Documentation:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index ded40bc5d..aaa3332ee 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -767,18 +767,20 @@ class Image:
 
         .. warning::
 
-            This method returns the raw image data from the internal
-            storage.  For compressed image data (e.g. PNG, JPEG) use
-            :meth:`~.save`, with a BytesIO parameter for in-memory
-            data.
-
-        :param encoder_name: What encoder to use.  The default is to
-                             use the standard "raw" encoder.
-
-                             A list of C encoders can be seen under
-                             codecs section of the function array in
-                             :file:`_imaging.c`. Python encoders are
-                             registered within the relevant plugins.
+            This method returns raw image data derived from Pillow's internal
+            storage. For compressed image data (e.g. PNG, JPEG) use
+            :meth:`~.save`, with a BytesIO parameter for in-memory data.
+
+        :param encoder_name: What encoder to use.
+
+                             The default is to use the standard "raw" encoder.
+                             To see how this packs pixel data into the returned
+                             bytes, see :file:`libImaging/Pack.c`.
+
+                             A list of C encoders can be seen under codecs
+                             section of the function array in
+                             :file:`_imaging.c`. Python encoders are registered
+                             within the relevant plugins.
         :param args: Extra arguments to the encoder.
         :returns: A :py:class:`bytes` object.
         """
diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py
index f709d026b..d569cb4b8 100644
--- a/src/PIL/WmfImagePlugin.py
+++ b/src/PIL/WmfImagePlugin.py
@@ -81,7 +81,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
 
     def _open(self) -> None:
         # check placable header
-        s = self.fp.read(80)
+        s = self.fp.read(44)
 
         if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"):
             # placeable windows metafile
diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c
index 4fa4ecd1c..6f0a1bfa3 100644
--- a/src/libImaging/Storage.c
+++ b/src/libImaging/Storage.c
@@ -723,6 +723,8 @@ ImagingNewArrow(
     int64_t pixels = (int64_t)xsize * (int64_t)ysize;
 
     // fmt:off   // don't reformat this
+    // stored as a single array, one element per pixel, either single band
+    // or multiband, where each pixel is an I32.
     if (((strcmp(schema->format, "I") == 0  // int32
           && im->pixelsize == 4             // 4xchar* storage
           && im->bands >= 2)                // INT32 into any INT32 Storage mode
@@ -735,6 +737,7 @@ ImagingNewArrow(
             return im;
         }
     }
+    // Stored as [[r,g,b,a],...]
     if (strcmp(schema->format, "+w:4") == 0  // 4 up array
         && im->pixelsize == 4                // storage as 32 bpc
         && schema->n_children > 0            // make sure schema is well formed.
@@ -750,6 +753,17 @@ ImagingNewArrow(
             return im;
         }
     }
+    // Stored as [r,g,b,a,r,g,b,a,...]
+    if (strcmp(schema->format, "C") == 0            // uint8
+        && im->pixelsize == 4                       // storage as 32 bpc
+        && schema->n_children == 0                  // make sure schema is well formed.
+        && strcmp(im->arrow_band_format, "C") == 0  // expected format
+        && 4 * pixels == external_array->length) {  // expected length
+        // single flat array, interleaved storage.
+        if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) {
+            return im;
+        }
+    }
     // fmt: on
     ImagingDelete(im);
     return NULL;
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index d23f0eb5b..6e176e29c 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -118,7 +118,7 @@ V = {
     "LCMS2": "2.17",
     "LIBAVIF": "1.3.0",
     "LIBIMAGEQUANT": "4.3.4",
-    "LIBPNG": "1.6.47",
+    "LIBPNG": "1.6.48",
     "LIBWEBP": "1.5.0",
     "OPENJPEG": "2.5.3",
     "TIFF": "4.7.0",


Test patch:
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
index b484a8cfa..d94de7287 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -15,6 +15,7 @@ from .helper import (
     is_win32,
     mark_if_feature_version,
     skip_unless_feature,
+    timeout_unless_slower_valgrind,
 )
 
 HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript()
@@ -398,7 +399,7 @@ def test_emptyline() -> None:
     assert image.format == "EPS"
 
 
-@pytest.mark.timeout(timeout=5)
+@timeout_unless_slower_valgrind(5)
 @pytest.mark.parametrize(
     "test_file",
     ["Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py
index 81df1ab0b..0fadd01d0 100644
--- a/Tests/test_file_fli.py
+++ b/Tests/test_file_fli.py
@@ -7,7 +7,12 @@ import pytest
 
 from PIL import FliImagePlugin, Image, ImageFile
 
-from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
+from .helper import (
+    assert_image_equal,
+    assert_image_equal_tofile,
+    is_pypy,
+    timeout_unless_slower_valgrind,
+)
 
 # created as an export of a palette image from Gimp2.6
 # save as...-> hopper.fli, default options.
@@ -189,7 +194,7 @@ def test_seek() -> None:
         "Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli",
     ],
 )
-@pytest.mark.timeout(timeout=3)
+@timeout_unless_slower_valgrind(3)
 def test_timeouts(test_file: str) -> None:
     with open(test_file, "rb") as f:
         with Image.open(f) as im:
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index 79f0ec1a8..b9eec591d 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -32,6 +32,7 @@ from .helper import (
     is_win32,
     mark_if_feature_version,
     skip_unless_feature,
+    timeout_unless_slower_valgrind,
 )
 
 ElementTree: ModuleType | None
@@ -1033,7 +1034,7 @@ class TestFileJpeg:
         with pytest.raises(ValueError):
             im.save(f, xmp=b"1" * 65505)
 
-    @pytest.mark.timeout(timeout=1)
+    @timeout_unless_slower_valgrind(1)
     def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None:
         # Even though this decoder never says that it is finished
         # the image should still end when there is no new data
diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py
index bde1e3ab8..a2218673b 100644
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -13,7 +13,12 @@ import pytest
 
 from PIL import Image, PdfParser, features
 
-from .helper import hopper, mark_if_feature_version, skip_unless_feature
+from .helper import (
+    hopper,
+    mark_if_feature_version,
+    skip_unless_feature,
+    timeout_unless_slower_valgrind,
+)
 
 
 def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str:
@@ -339,8 +344,7 @@ def test_pdf_append_to_bytesio() -> None:
     assert len(f.getvalue()) > initial_size
 
 
-@pytest.mark.timeout(1)
-@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
+@timeout_unless_slower_valgrind(1)
 @pytest.mark.parametrize("newline", (b"\r", b"\n"))
 def test_redos(newline: bytes) -> None:
     malicious = b" trailer<<>>" + newline * 3456
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index 502d9df9a..d0d394aa9 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -26,6 +26,7 @@ from .helper import (
     hopper,
     is_pypy,
     is_win32,
+    timeout_unless_slower_valgrind,
 )
 
 ElementTree: ModuleType | None
@@ -988,7 +989,7 @@ class TestFileTiff:
             with pytest.raises(OSError):
                 im.load()
 
-    @pytest.mark.timeout(6)
+    @timeout_unless_slower_valgrind(6)
     @pytest.mark.filterwarnings("ignore:Truncated File Read")
     def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None:
         with Image.open("Tests/images/timeout-6646305047838720") as im:
@@ -1001,7 +1002,7 @@ class TestFileTiff:
             "Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif",
         ],
     )
-    @pytest.mark.timeout(2)
+    @timeout_unless_slower_valgrind(2)
     def test_oom(self, test_file: str) -> None:
         with pytest.raises(UnidentifiedImageError):
             with pytest.warns(UserWarning):
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 7e6118d52..14a067127 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -34,6 +34,7 @@ from .helper import (
     is_win32,
     mark_if_feature_version,
     skip_unless_feature,
+    timeout_unless_slower_valgrind,
 )
 
 ElementTree: ModuleType | None
@@ -572,10 +573,7 @@ class TestImage:
         i = Image.new("RGB", [1, 1])
         assert isinstance(i.size, tuple)
 
-    @pytest.mark.timeout(0.75)
-    @pytest.mark.skipif(
-        "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower"
-    )
+    @timeout_unless_slower_valgrind(0.75)
     @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0)))
     def test_empty_image(self, size: tuple[int, int]) -> None:
         Image.new("RGB", size)
diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py
index 695aecbde..3eb98d379 100644
--- a/Tests/test_imagefontpil.py
+++ b/Tests/test_imagefontpil.py
@@ -7,7 +7,7 @@ import pytest
 
 from PIL import Image, ImageDraw, ImageFont, _util, features
 
-from .helper import assert_image_equal_tofile
+from .helper import assert_image_equal_tofile, timeout_unless_slower_valgrind
 
 fonts = [ImageFont.load_default_imagefont()]
 if not features.check_module("freetype2"):
@@ -72,7 +72,7 @@ def test_decompression_bomb() -> None:
         font.getmask("A" * 1_000_000)
 
 
-@pytest.mark.timeout(4)
+@timeout_unless_slower_valgrind(4)
 def test_oom() -> None:
     glyph = struct.pack(
         ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767
diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py
index ece9f8f26..8dad94fe0 100644
--- a/Tests/test_pyarrow.py
+++ b/Tests/test_pyarrow.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import Any  # undone
+from typing import Any, NamedTuple
 
 import pytest
 
@@ -10,30 +10,73 @@ from .helper import (
     assert_deep_equal,
     assert_image_equal,
     hopper,
+    is_big_endian,
 )
 
-pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed")
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+    import pyarrow
+else:
+    pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed")
 
 TEST_IMAGE_SIZE = (10, 10)
 
 
 def _test_img_equals_pyarray(
-    img: Image.Image, arr: Any, mask: list[int] | None
+    img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
 ) -> None:
-    assert img.height * img.width == len(arr)
+    assert img.height * img.width * elts_per_pixel == len(arr)
     px = img.load()
     assert px is not None
+    if elts_per_pixel > 1 and mask is None:
+        # have to do element-wise comparison when we're comparing
+        # flattened r,g,b,a to a pixel.
+        mask = list(range(elts_per_pixel))
     for x in range(0, img.size[0], int(img.size[0] / 10)):
         for y in range(0, img.size[1], int(img.size[1] / 10)):
             if mask:
+                pixel = px[x, y]
+                assert isinstance(pixel, tuple)
                 for ix, elt in enumerate(mask):
-                    pixel = px[x, y]
-                    assert isinstance(pixel, tuple)
-                    assert pixel[ix] == arr[y * img.width + x].as_py()[elt]
+                    if elts_per_pixel == 1:
+                        assert pixel[ix] == arr[y * img.width + x].as_py()[elt]
+                    else:
+                        assert (
+                            pixel[ix]
+                            == arr[(y * img.width + x) * elts_per_pixel + elt].as_py()
+                        )
             else:
                 assert_deep_equal(px[x, y], arr[y * img.width + x].as_py())
 
 
+def _test_img_equals_int32_pyarray(
+    img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
+) -> None:
+    assert img.height * img.width * elts_per_pixel == len(arr)
+    px = img.load()
+    assert px is not None
+    if mask is None:
+        # have to do element-wise comparison when we're comparing
+        # flattened rgba in an uint32 to a pixel.
+        mask = list(range(elts_per_pixel))
+    for x in range(0, img.size[0], int(img.size[0] / 10)):
+        for y in range(0, img.size[1], int(img.size[1] / 10)):
+            pixel = px[x, y]
+            assert isinstance(pixel, tuple)
+            arr_pixel_int = arr[y * img.width + x].as_py()
+            arr_pixel_tuple = (
+                arr_pixel_int % 256,
+                (arr_pixel_int // 256) % 256,
+                (arr_pixel_int // 256**2) % 256,
+                (arr_pixel_int // 256**3),
+            )
+            if is_big_endian():
+                arr_pixel_tuple = arr_pixel_tuple[::-1]
+
+            for ix, elt in enumerate(mask):
+                assert pixel[ix] == arr_pixel_tuple[elt]
+
+
 # really hard to get a non-nullable list type
 fl_uint8_4_type = pyarrow.field(
     "_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4)
@@ -55,14 +98,14 @@ fl_uint8_4_type = pyarrow.field(
         ("HSV", fl_uint8_4_type, [0, 1, 2]),
     ),
 )
-def test_to_array(mode: str, dtype: Any, mask: list[int] | None) -> None:
+def test_to_array(mode: str, dtype: pyarrow.DataType, mask: list[int] | None) -> None:
     img = hopper(mode)
 
     # Resize to non-square
     img = img.crop((3, 0, 124, 127))
     assert img.size == (121, 127)
 
-    arr = pyarrow.array(img)
+    arr = pyarrow.array(img)  # type: ignore[call-overload]
     _test_img_equals_pyarray(img, arr, mask)
     assert arr.type == dtype
 
@@ -79,8 +122,8 @@ def test_lifetime() -> None:
 
     img = hopper("L")
 
-    arr_1 = pyarrow.array(img)
-    arr_2 = pyarrow.array(img)
+    arr_1 = pyarrow.array(img)  # type: ignore[call-overload]
+    arr_2 = pyarrow.array(img)  # type: ignore[call-overload]
 
     del img
 
@@ -97,8 +140,8 @@ def test_lifetime2() -> None:
 
     img = hopper("L")
 
-    arr_1 = pyarrow.array(img)
-    arr_2 = pyarrow.array(img)
+    arr_1 = pyarrow.array(img)  # type: ignore[call-overload]
+    arr_2 = pyarrow.array(img)  # type: ignore[call-overload]
 
     assert arr_1.sum().as_py() > 0
     del arr_1
@@ -110,3 +153,94 @@ def test_lifetime2() -> None:
     px = img2.load()
     assert px  # make mypy happy
     assert isinstance(px[0, 0], int)
+
+
+class DataShape(NamedTuple):
+    dtype: pyarrow.DataType
+    # Strictly speaking, elt should be a pixel or pixel component, so
+    # list[uint8][4], float, int, uint32, uint8, etc.  But more
+    # correctly, it should be exactly the dtype from the line above.
+    elt: Any
+    elts_per_pixel: int
+
+
+UINT_ARR = DataShape(
+    dtype=fl_uint8_4_type,
+    elt=[1, 2, 3, 4],  # array of 4 uint8 per pixel
+    elts_per_pixel=1,  # only one array per pixel
+)
+
+UINT = DataShape(
+    dtype=pyarrow.uint8(),
+    elt=3,  # one uint8,
+    elts_per_pixel=4,  # but repeated 4x per pixel
+)
+
+UINT32 = DataShape(
+    dtype=pyarrow.uint32(),
+    elt=0xABCDEF45,  # one packed int, doesn't fit in a int32 > 0x80000000
+    elts_per_pixel=1,  # one per pixel
+)
+
+INT32 = DataShape(
+    dtype=pyarrow.uint32(),
+    elt=0x12CDEF45,  # one packed int
+    elts_per_pixel=1,  # one per pixel
+)
+
+
+@pytest.mark.parametrize(
+    "mode, data_tp, mask",
+    (
+        ("L", DataShape(pyarrow.uint8(), 3, 1), None),
+        ("I", DataShape(pyarrow.int32(), 1 << 24, 1), None),
+        ("F", DataShape(pyarrow.float32(), 3.14159, 1), None),
+        ("LA", UINT_ARR, [0, 3]),
+        ("LA", UINT, [0, 3]),
+        ("RGB", UINT_ARR, [0, 1, 2]),
+        ("RGBA", UINT_ARR, None),
+        ("CMYK", UINT_ARR, None),
+        ("YCbCr", UINT_ARR, [0, 1, 2]),
+        ("HSV", UINT_ARR, [0, 1, 2]),
+        ("RGB", UINT, [0, 1, 2]),
+        ("RGBA", UINT, None),
+        ("CMYK", UINT, None),
+        ("YCbCr", UINT, [0, 1, 2]),
+        ("HSV", UINT, [0, 1, 2]),
+    ),
+)
+def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
+    (dtype, elt, elts_per_pixel) = data_tp
+
+    ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
+    arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
+    img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
+
+    _test_img_equals_pyarray(img, arr, mask, elts_per_pixel)
+
+
+@pytest.mark.parametrize(
+    "mode, data_tp, mask",
+    (
+        ("LA", UINT32, [0, 3]),
+        ("RGB", UINT32, [0, 1, 2]),
+        ("RGBA", UINT32, None),
+        ("CMYK", UINT32, None),
+        ("YCbCr", UINT32, [0, 1, 2]),
+        ("HSV", UINT32, [0, 1, 2]),
+        ("LA", INT32, [0, 3]),
+        ("RGB", INT32, [0, 1, 2]),
+        ("RGBA", INT32, None),
+        ("CMYK", INT32, None),
+        ("YCbCr", INT32, [0, 1, 2]),
+        ("HSV", INT32, [0, 1, 2]),
+    ),
+)
+def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
+    (dtype, elt, elts_per_pixel) = data_tp
+
+    ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
+    arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
+    img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
+
+    _test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel)