Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tp_richcompare handler for Imaging_Type/ImagingCore #7260

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
61 changes: 61 additions & 0 deletions Tests/test_lib_image.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import itertools

import pytest

from PIL import Image
Expand Down Expand Up @@ -32,3 +34,62 @@ def test_setmode() -> None:
im.im.setmode("L")
with pytest.raises(ValueError):
im.im.setmode("RGBABCDE")


@pytest.mark.parametrize("mode", Image.MODES)
def test_equal(mode):
num_img_bytes = len(Image.new(mode, (2, 2)).tobytes())
data = bytes(range(ord("A"), ord("A") + num_img_bytes))
img_a = Image.frombytes(mode, (2, 2), data)
img_b = Image.frombytes(mode, (2, 2), data)
assert img_a.tobytes() == img_b.tobytes()
assert img_a.im == img_b.im


# With mode "1" different bytes can map to the same value,
# so we have to be more specific with the values we use.
@pytest.mark.parametrize(
"bytes_a, bytes_b",
itertools.permutations(
(bytes(x) for x in itertools.product(b"\x00\xff", repeat=4)), 2
),
)
def test_not_equal_mode_1(bytes_a, bytes_b):
# Use rawmode "1;8" so that each full byte is interpreted as a value
# instead of the bits in the bytes being interpreted as values.
img_a = Image.frombytes("1", (2, 2), bytes_a, "raw", "1;8")
img_b = Image.frombytes("1", (2, 2), bytes_b, "raw", "1;8")
assert img_a.tobytes() != img_b.tobytes()
assert img_a.im != img_b.im


@pytest.mark.parametrize("mode", [mode for mode in Image.MODES if mode != "1"])
def test_not_equal(mode):
num_img_bytes = len(Image.new(mode, (2, 2)).tobytes())
data_a = bytes(range(ord("A"), ord("A") + num_img_bytes))
data_b = bytes(range(ord("Z"), ord("Z") - num_img_bytes, -1))
img_a = Image.frombytes(mode, (2, 2), data_a)
img_b = Image.frombytes(mode, (2, 2), data_b)
assert img_a.tobytes() != img_b.tobytes()
assert img_a.im != img_b.im


@pytest.mark.parametrize("mode", ("RGB", "YCbCr", "HSV", "LAB"))
def test_equal_three_channels_four_bytes(mode):
# The "A" and "B" values in LAB images are signed values from -128 to 127,
# but we store them as unsigned values from 0 to 255, so we need to use
# slightly different input bytes for LAB to get the same output.
img_a = Image.new(mode, (1, 1), 0x00B3B231 if mode == "LAB" else 0x00333231)
img_b = Image.new(mode, (1, 1), 0xFFB3B231 if mode == "LAB" else 0xFF333231)
assert img_a.tobytes() == b"123"
assert img_b.tobytes() == b"123"
assert img_a.im == img_b.im


@pytest.mark.parametrize("mode", ("LA", "La", "PA"))
Yay295 marked this conversation as resolved.
Show resolved Hide resolved
def test_equal_two_channels_four_bytes(mode):
img_a = Image.new(mode, (1, 1), 0x32000031)
img_b = Image.new(mode, (1, 1), 0x32FFFF31)
assert img_a.tobytes() == b"12"
assert img_b.tobytes() == b"12"
assert img_a.im == img_b.im
12 changes: 8 additions & 4 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,13 +683,17 @@
if self.__class__ is not other.__class__:
return False
assert isinstance(other, Image)
return (
if self is other:
return True
if not (
self.mode == other.mode
and self.size == other.size
and self.info == other.info
and self.getpalette() == other.getpalette()
and self.tobytes() == other.tobytes()
)
):
return False

Check warning on line 693 in src/PIL/Image.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/Image.py#L693

Added line #L693 was not covered by tests
self.load()
other.load()
return self.im == other.im

def __repr__(self) -> str:
return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % (
Expand Down
176 changes: 150 additions & 26 deletions src/_imaging.c
Original file line number Diff line number Diff line change
Expand Up @@ -3789,39 +3789,163 @@
(ssizessizeobjargproc)NULL, /*sq_ass_slice*/
};

/*
Returns 0 if all of the pixels are the same, otherwise 1.
Skips unused bytes based on the given mode.
*/
static int
_compare_pixels(
const char *mode,
const int ysize,
const int linesize,
const UINT8 **pixels_a,
const UINT8 **pixels_b
) {
// Fortunately, all of the modes that have extra bytes in their pixels
// use four bytes for their pixels.
UINT32 mask = 0xffffffff;
if (!strcmp(mode, "RGB") || !strcmp(mode, "YCbCr") || !strcmp(mode, "HSV") ||
!strcmp(mode, "LAB")) {
// These modes have three channels in four bytes,
// so we have to ignore the last byte.
#ifdef WORDS_BIGENDIAN
mask = 0xffffff00;
#else
mask = 0x00ffffff;
#endif
} else if (!strcmp(mode, "LA") || !strcmp(mode, "La") || !strcmp(mode, "PA")) {
// These modes have two channels in four bytes,
// so we have to ignore the middle two bytes.
mask = 0xff0000ff;
Yay295 marked this conversation as resolved.
Show resolved Hide resolved
}

if (mask == 0xffffffff) {
// If we aren't masking anything we can use memcmp.
for (int y = 0; y < ysize; y++) {
if (memcmp(pixels_a[y], pixels_b[y], linesize)) {
return 1;
}
}
} else {
const int xsize = linesize / 4;
for (int y = 0; y < ysize; y++) {
UINT32 *line_a = (UINT32 *)pixels_a[y];
UINT32 *line_b = (UINT32 *)pixels_b[y];
for (int x = 0; x < xsize; x++, line_a++, line_b++) {
if ((*line_a & mask) != (*line_b & mask)) {
return 1;
}
}
}
}
return 0;
}

static PyObject *
image_richcompare(const ImagingObject *self, const PyObject *other, const int op) {
if (op != Py_EQ && op != Py_NE) {
Py_RETURN_NOTIMPLEMENTED;

Check warning on line 3847 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L3847

Added line #L3847 was not covered by tests
}

// If the other object is not an ImagingObject.
if (!PyImaging_Check(other)) {
if (op == Py_EQ) {
Py_RETURN_FALSE;

Check warning on line 3853 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L3853

Added line #L3853 was not covered by tests
} else {
Py_RETURN_TRUE;

Check warning on line 3855 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L3855

Added line #L3855 was not covered by tests
}
}

const Imaging img_a = self->image;
const Imaging img_b = ((ImagingObject *)other)->image;

if (strcmp(img_a->mode, img_b->mode) || img_a->xsize != img_b->xsize ||
img_a->ysize != img_b->ysize) {
if (op == Py_EQ) {
Py_RETURN_FALSE;

Check warning on line 3865 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L3865

Added line #L3865 was not covered by tests
} else {
Py_RETURN_TRUE;

Check warning on line 3867 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L3867

Added line #L3867 was not covered by tests
}
}

const ImagingPalette palette_a = img_a->palette;
const ImagingPalette palette_b = img_b->palette;
if (palette_a || palette_b) {
const UINT8 *palette_a_data = palette_a->palette;
const UINT8 *palette_b_data = palette_b->palette;
const UINT8 **palette_a_data_ptr = &palette_a_data;
const UINT8 **palette_b_data_ptr = &palette_b_data;
if (!palette_a || !palette_b || palette_a->size != palette_b->size ||
strcmp(palette_a->mode, palette_b->mode) ||
_compare_pixels(
palette_a->mode,
1,
palette_a->size * 4,
Yay295 marked this conversation as resolved.
Show resolved Hide resolved
palette_a_data_ptr,
palette_b_data_ptr
)) {
if (op == Py_EQ) {
Py_RETURN_FALSE;

Check warning on line 3888 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L3888

Added line #L3888 was not covered by tests
} else {
Py_RETURN_TRUE;

Check warning on line 3890 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L3890

Added line #L3890 was not covered by tests
}
}
}

if (_compare_pixels(
img_a->mode,
img_a->ysize,
img_a->linesize,
(const UINT8 **)img_a->image,
(const UINT8 **)img_b->image
)) {
if (op == Py_EQ) {
Py_RETURN_FALSE;
} else {
Py_RETURN_TRUE;
}
} else {
if (op == Py_EQ) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;

Check warning on line 3911 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L3911

Added line #L3911 was not covered by tests
}
}
}

/* type description */

static PyTypeObject Imaging_Type = {
PyVarObject_HEAD_INIT(NULL, 0) "ImagingCore", /*tp_name*/
sizeof(ImagingObject), /*tp_basicsize*/
0, /*tp_itemsize*/
/* methods */
(destructor)_dealloc, /*tp_dealloc*/
0, /*tp_vectorcall_offset*/
0, /*tp_getattr*/
0, /*tp_setattr*/
0, /*tp_as_async*/
0, /*tp_repr*/
0, /*tp_as_number*/
&image_as_sequence, /*tp_as_sequence*/
0, /*tp_as_mapping*/
0, /*tp_hash*/
0, /*tp_call*/
0, /*tp_str*/
0, /*tp_getattro*/
0, /*tp_setattro*/
0, /*tp_as_buffer*/
Py_TPFLAGS_DEFAULT, /*tp_flags*/
0, /*tp_doc*/
0, /*tp_traverse*/
0, /*tp_clear*/
0, /*tp_richcompare*/
0, /*tp_weaklistoffset*/
0, /*tp_iter*/
0, /*tp_iternext*/
methods, /*tp_methods*/
0, /*tp_members*/
getsetters, /*tp_getset*/
(destructor)_dealloc, /*tp_dealloc*/
0, /*tp_vectorcall_offset*/
0, /*tp_getattr*/
0, /*tp_setattr*/
0, /*tp_as_async*/
0, /*tp_repr*/
0, /*tp_as_number*/
&image_as_sequence, /*tp_as_sequence*/
0, /*tp_as_mapping*/
0, /*tp_hash*/
0, /*tp_call*/
0, /*tp_str*/
0, /*tp_getattro*/
0, /*tp_setattro*/
0, /*tp_as_buffer*/
Py_TPFLAGS_DEFAULT, /*tp_flags*/
0, /*tp_doc*/
0, /*tp_traverse*/
0, /*tp_clear*/
(richcmpfunc)image_richcompare, /*tp_richcompare*/
0, /*tp_weaklistoffset*/
0, /*tp_iter*/
0, /*tp_iternext*/
methods, /*tp_methods*/
0, /*tp_members*/
getsetters, /*tp_getset*/
};

static PyTypeObject ImagingFont_Type = {
Expand Down
Loading