Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
5890e40
WIP - Initial Pillow->Arrow support
wiredfool Aug 24, 2024
d44212d
WIP - Non working struct encoding
wiredfool Aug 24, 2024
bdd4b3a
Export as fixed width pixels, or just pixel values for single channel.
wiredfool Aug 24, 2024
56780ce
Tests, lifetime changes
wiredfool Aug 25, 2024
6ec855e
Lifetime check
wiredfool Aug 25, 2024
e1ef083
fix macoxisim
wiredfool Aug 25, 2024
97eb7c0
WIP -- First light of round trip of image -> arrow -> image
wiredfool Jan 21, 2025
f1349e9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 21, 2025
9d584a1
Lint
wiredfool Jan 25, 2025
2b88b1c
Typing Lint
wiredfool Jan 25, 2025
af64250
Lint
wiredfool Jan 25, 2025
244dded
Typing Lint
wiredfool Jan 25, 2025
dbe0304
Tests for destructors of the PyCapsules
wiredfool Jan 25, 2025
388da5c
Test rejection of incorrect modes
wiredfool Jan 25, 2025
55f5351
Test for size, add offset support
wiredfool Jan 25, 2025
ad492ee
Pull readonly in from the C level
wiredfool Jan 25, 2025
d02417e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 25, 2025
6fad11a
added mutex around refcount, renamed arrow_borrow to refcount
wiredfool Jan 30, 2025
4fc8328
remove unused code
wiredfool Feb 3, 2025
e7bd152
Error handling:
wiredfool Feb 3, 2025
9f94d4f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 3, 2025
13e3301
Fix handling of capsule destruct sequencing
wiredfool Feb 3, 2025
2401757
Add a way to force the use of the old block allocator
wiredfool Feb 3, 2025
be3b0fd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 3, 2025
159ffe3
Fix mutex for Free-tread pythons
wiredfool Feb 3, 2025
91e2759
split pyarrow tests out from plain loopback arrow tests
wiredfool Feb 14, 2025
05ae6e9
install pyarrow for those test platforms where it's available
wiredfool Feb 14, 2025
9ff0465
merge from upstream/main
wiredfool Feb 14, 2025
dd18fac
fix yml
wiredfool Feb 14, 2025
b2210d1
mac/linux: install binary only pyarrow, and don't fail if there's no …
wiredfool Feb 18, 2025
a0927be
merge from main
wiredfool Feb 18, 2025
9021829
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 18, 2025
038f62c
fix macos pyarrow test install
wiredfool Feb 18, 2025
01998fc
Added docs
wiredfool Feb 18, 2025
4ea8ac8
Fix the windows test matrix?
wiredfool Feb 18, 2025
e81b669
Lint
wiredfool Feb 18, 2025
7ac90fa
PyCapsules aren't actually importable in python.
wiredfool Feb 18, 2025
2418a23
Yaml
wiredfool Feb 18, 2025
a129efd
Workflow yaml
wiredfool Feb 18, 2025
afc16e5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 18, 2025
bba1b13
docs tweaks
wiredfool Feb 18, 2025
19eb3e4
environment reference in test-windows.yml
wiredfool Feb 18, 2025
cba8e09
Fix pre-commit.ci lint.
wiredfool Feb 18, 2025
7e59428
Take 4: not environment variables
wiredfool Feb 18, 2025
a8d819c
Mypy error -- can't have a bare tuple
wiredfool Feb 18, 2025
7d498c3
Mypy error -- doesn't like the none return
wiredfool Feb 18, 2025
e4ad2c0
doc indent
wiredfool Feb 18, 2025
cb14672
le-sigh
wiredfool Feb 18, 2025
bafb968
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 18, 2025
cca80e2
make mypy happy
wiredfool Feb 19, 2025
ae187ca
Apply suggestions from code review
wiredfool Mar 29, 2025
42d0682
re-add include item
wiredfool Mar 29, 2025
48bbc64
Test Typing, consistency/style issues
wiredfool Mar 30, 2025
088f80d
Fix mypy
wiredfool Mar 30, 2025
b729f64
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 30, 2025
3e28e05
Types and typos
hugovk Mar 31, 2025
1678641
Apply suggestions from code review
hugovk Apr 1, 2025
e56e01c
Merge branch 'main' into arrow
radarhere Apr 1, 2025
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
107 changes: 107 additions & 0 deletions Tests/test_arrow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from __future__ import annotations

from typing import Any # undone

import pytest

from PIL import Image

from .helper import (
assert_deep_equal,
assert_image_equal,
hopper,
)

pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed")

TEST_IMAGE_SIZE = (10, 10)

Check warning on line 17 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L17

Added line #L17 was not covered by tests


def _test_img_equals_pyarray(img: Image.Image, arr: Any, mask) -> None:
assert img.height * img.width == len(arr)
px = img.load()
assert px is not None
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:
for ix, elt in enumerate(mask):
assert px[x, y][ix] == arr[y * img.width + x].as_py()[elt]

Check warning on line 28 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L20-L28

Added lines #L20 - L28 were not covered by tests
else:
assert_deep_equal(px[x, y], arr[y * img.width + x].as_py())

Check warning on line 30 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L30

Added line #L30 was not covered by tests


# really hard to get a non-nullable list type
fl_uint8_4_type = pyarrow.field(

Check warning on line 34 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L34

Added line #L34 was not covered by tests
"_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4)
).type


@pytest.mark.parametrize(

Check warning on line 39 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L39

Added line #L39 was not covered by tests
"mode, dtype, mask",
(
("L", pyarrow.uint8(), None),
("I", pyarrow.int32(), None),
("F", pyarrow.float32(), None),
("LA", fl_uint8_4_type, [0, 3]),
("RGB", fl_uint8_4_type, [0, 1, 2]),
("RGBA", fl_uint8_4_type, None),
("RGBX", fl_uint8_4_type, None),
("CMYK", fl_uint8_4_type, None),
("YCbCr", fl_uint8_4_type, [0, 1, 2]),
("HSV", fl_uint8_4_type, [0, 1, 2]),
),
)
def test_to_array(mode: str, dtype: Any, mask: Any) -> None:
img = hopper(mode)

Check warning on line 55 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L54-L55

Added lines #L54 - L55 were not covered by tests

# Resize to non-square
img = img.crop((3, 0, 124, 127))
assert img.size == (121, 127)

Check warning on line 59 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L58-L59

Added lines #L58 - L59 were not covered by tests

arr = pyarrow.array(img)
_test_img_equals_pyarray(img, arr, mask)
assert arr.type == dtype

Check warning on line 63 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L61-L63

Added lines #L61 - L63 were not covered by tests

reloaded = Image.fromarrow(arr, mode, img.size)

Check warning on line 65 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L65

Added line #L65 was not covered by tests

assert reloaded

Check warning on line 67 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L67

Added line #L67 was not covered by tests

assert_image_equal(img, reloaded)

Check warning on line 69 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L69

Added line #L69 was not covered by tests


def test_lifetime():

Check warning on line 72 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L72

Added line #L72 was not covered by tests
# valgrind shouldn't error out here.
# arrays should be accessible after the image is deleted.

img = hopper("L")

Check warning on line 76 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L76

Added line #L76 was not covered by tests

arr_1 = pyarrow.array(img)
arr_2 = pyarrow.array(img)

Check warning on line 79 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L78-L79

Added lines #L78 - L79 were not covered by tests

del img

Check warning on line 81 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L81

Added line #L81 was not covered by tests

assert arr_1.sum().as_py() > 0
del arr_1

Check warning on line 84 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L83-L84

Added lines #L83 - L84 were not covered by tests

assert arr_2.sum().as_py() > 0
del arr_2

Check warning on line 87 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L86-L87

Added lines #L86 - L87 were not covered by tests


def test_lifetime2():

Check warning on line 90 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L90

Added line #L90 was not covered by tests
# valgrind shouldn't error out here.
# img should remain after the arrays are collected.

img = hopper("L")

Check warning on line 94 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L94

Added line #L94 was not covered by tests

arr_1 = pyarrow.array(img)
arr_2 = pyarrow.array(img)

Check warning on line 97 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L96-L97

Added lines #L96 - L97 were not covered by tests

assert arr_1.sum().as_py() > 0
del arr_1

Check warning on line 100 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L99-L100

Added lines #L99 - L100 were not covered by tests

assert arr_2.sum().as_py() > 0
del arr_2

Check warning on line 103 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L102-L103

Added lines #L102 - L103 were not covered by tests

img2 = img.copy()
px = img2.load()
assert isinstance(px[0, 0], int)

Check warning on line 107 in Tests/test_arrow.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_arrow.py#L105-L107

Added lines #L105 - L107 were not covered by tests
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ optional-dependencies.tests = [
"markdown2",
"olefile",
"packaging",
"pyarrow",
"pyroma",
"pytest",
"pytest-cov",
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def get_version() -> str:
_LIB_IMAGING = (
"Access",
"AlphaComposite",
"Arrow",
"Resample",
"Reduce",
"Bands",
Expand Down
33 changes: 33 additions & 0 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,16 @@ def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]:
new["shape"], new["typestr"] = _conv_type_shape(self)
return new

def __arrow_c_schema__(self) -> object:
self.load()
return self.im.__arrow_c_schema__()

def __arrow_c_array__(
self, requested_schema: object | None = None
) -> Tuple[object, object]:
self.load()
return (self.im.__arrow_c_schema__(), self.im.__arrow_c_array__())

def __getstate__(self) -> list[Any]:
im_data = self.tobytes() # load image first
return [self.info, self.mode, self.size, self.getpalette(), im_data]
Expand Down Expand Up @@ -3250,6 +3260,17 @@ def __array_interface__(self) -> dict[str, Any]:
raise NotImplementedError()


class SupportsArrowArrayInterface(Protocol):
"""
An object that has an ``__arrow_c_array__`` method corresponding to the arrow c data interface.
"""

def __arrow_c_array__(
self, requested_schema: PyCapsule = None
) -> tuple[PyCapsule, PyCapsule]:
raise NotImplementedError()


def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
"""
Creates an image memory from an object exporting the array interface
Expand Down Expand Up @@ -3338,6 +3359,18 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)


def fromarrow(obj: SupportsArrowArrayIngerface, mode, size) -> ImageFile.ImageFile:
if not hasattr(obj, "__arrow_c_array__"):
raise ValueError("arrow_c_array interface not found")

(schema_capsule, array_capsule) = obj.__arrow_c_array__()
_im = core.new_arrow(mode, size, schema_capsule, array_capsule)
if _im:
return Image()._new(_im)

return None


def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile:
"""Creates an image instance from a QImage image"""
from . import ImageQt
Expand Down
76 changes: 76 additions & 0 deletions src/_imaging.c
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,77 @@
return PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE);
}

/* -------------------------------------------------------------------- */
/* Arrow HANDLING */
/* -------------------------------------------------------------------- */

void
ReleaseArrowSchemaPyCapsule(PyObject *capsule) {
struct ArrowSchema *schema =
(struct ArrowSchema *)PyCapsule_GetPointer(capsule, "arrow_schema");
if (schema->release != NULL) {
schema->release(schema);
}
free(schema);
}

PyObject *

Check warning on line 240 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L238-L240

Added lines #L238 - L240 were not covered by tests
ExportArrowSchemaPyCapsule(ImagingObject *self) {
struct ArrowSchema *schema =

Check warning on line 242 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L242

Added line #L242 was not covered by tests
(struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema));
export_imaging_schema(self->image, schema);

Check warning on line 244 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L244

Added line #L244 was not covered by tests
return PyCapsule_New(schema, "arrow_schema", ReleaseArrowSchemaPyCapsule);
}

void
ReleaseArrowArrayPyCapsule(PyObject *capsule) {
struct ArrowArray *array =
(struct ArrowArray *)PyCapsule_GetPointer(capsule, "arrow_array");
if (array->release != NULL) {

Check warning on line 252 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L248-L252

Added lines #L248 - L252 were not covered by tests
array->release(array);
}
free(array);
}

PyObject *

Check warning on line 258 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L256-L258

Added lines #L256 - L258 were not covered by tests
ExportArrowArrayPyCapsule(ImagingObject *self) {
struct ArrowArray *array =

Check warning on line 260 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L260

Added line #L260 was not covered by tests
(struct ArrowArray *)calloc(1, sizeof(struct ArrowArray));
export_imaging_array(self->image, array);

Check warning on line 262 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L262

Added line #L262 was not covered by tests
return PyCapsule_New(array, "arrow_array", ReleaseArrowArrayPyCapsule);
}

static PyObject *
_new_arrow(PyObject *self, PyObject *args) {
char *mode;
int xsize, ysize;
PyObject *schema_capsule, *array_capsule;

Check warning on line 270 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L266-L270

Added lines #L266 - L270 were not covered by tests
PyObject *ret;

if (!PyArg_ParseTuple(
args, "s(ii)OO", &mode, &xsize, &ysize, &schema_capsule, &array_capsule

Check warning on line 274 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L274

Added line #L274 was not covered by tests
)) {
return NULL;
}

struct ArrowSchema *schema =
(struct ArrowSchema *)PyCapsule_GetPointer(schema_capsule, "arrow_schema");

Check warning on line 281 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L281

Added line #L281 was not covered by tests
struct ArrowArray *array =
(struct ArrowArray *)PyCapsule_GetPointer(array_capsule, "arrow_array");

Check warning on line 283 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L283

Added line #L283 was not covered by tests

ret = PyImagingNew(ImagingNewArrow(mode, xsize, ysize, schema, array));
if (schema->release) {
schema->release(schema);

Check warning on line 287 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L286-L287

Added lines #L286 - L287 were not covered by tests
schema->release = NULL;
}
if (!ret && array->release) {

Check warning on line 290 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L289-L290

Added lines #L289 - L290 were not covered by tests
array->release(array);
array->release = NULL;

Check warning on line 292 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L292

Added line #L292 was not covered by tests
}
return ret;
}

Check warning on line 295 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L294-L295

Added lines #L294 - L295 were not covered by tests

/* -------------------------------------------------------------------- */
/* EXCEPTION REROUTING */
/* -------------------------------------------------------------------- */
Expand Down Expand Up @@ -3678,6 +3749,10 @@
/* Misc. */
{"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS},

/* arrow */
{"__arrow_c_schema__", (PyCFunction)ExportArrowSchemaPyCapsule, METH_VARARGS},
{"__arrow_c_array__", (PyCFunction)ExportArrowArrayPyCapsule, METH_VARARGS},

{NULL, NULL} /* sentinel */
};

Expand Down Expand Up @@ -4216,6 +4291,7 @@
{"fill", (PyCFunction)_fill, METH_VARARGS},
{"new", (PyCFunction)_new, METH_VARARGS},
{"new_block", (PyCFunction)_new_block, METH_VARARGS},
{"new_arrow", (PyCFunction)_new_arrow, METH_VARARGS},
{"merge", (PyCFunction)_merge, METH_VARARGS},

/* Functions */
Expand Down
Loading
Loading