diff --git a/Tests/images/hopper_emboss_I.png b/Tests/images/hopper_emboss_I.png new file mode 100644 index 00000000000..f4dab388fe5 Binary files /dev/null and b/Tests/images/hopper_emboss_I.png differ diff --git a/Tests/images/hopper_emboss_more_I.png b/Tests/images/hopper_emboss_more_I.png new file mode 100644 index 00000000000..c417c915f54 Binary files /dev/null and b/Tests/images/hopper_emboss_more_I.png differ diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index a2ef2280b72..25b72298e92 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -30,15 +30,16 @@ ImageFilter.UnsharpMask(10), ), ) -@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK")) +@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) def test_sanity(filter_to_apply, mode): im = hopper(mode) - out = im.filter(filter_to_apply) - assert out.mode == im.mode - assert out.size == im.size + if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): + out = im.filter(filter_to_apply) + assert out.mode == im.mode + assert out.size == im.size -@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK")) +@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) def test_sanity_error(mode): with pytest.raises(TypeError): im = hopper(mode) @@ -130,10 +131,12 @@ def test_kernel_not_enough_coefficients(): ImageFilter.Kernel((3, 3), (0, 0)) -@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK")) +@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) def test_consistency_3x3(mode): with Image.open("Tests/images/hopper.bmp") as source: - with Image.open("Tests/images/hopper_emboss.bmp") as reference: + reference_name = "hopper_emboss" + reference_name += "_I.png" if mode == "I" else ".bmp" + with Image.open("Tests/images/" + reference_name) as reference: kernel = ImageFilter.Kernel( (3, 3), # fmt: off @@ -146,16 +149,20 @@ def test_consistency_3x3(mode): source = source.split() * 2 reference = reference.split() * 2 - assert_image_equal( - Image.merge(mode, source[: len(mode)]).filter(kernel), - Image.merge(mode, reference[: len(mode)]), - ) + if mode == "I": + source = source[0].convert(mode) + else: + source = Image.merge(mode, source[: len(mode)]) + reference = Image.merge(mode, reference[: len(mode)]) + assert_image_equal(source.filter(kernel), reference) -@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK")) +@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) def test_consistency_5x5(mode): with Image.open("Tests/images/hopper.bmp") as source: - with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: + reference_name = "hopper_emboss_more" + reference_name += "_I.png" if mode == "I" else ".bmp" + with Image.open("Tests/images/" + reference_name) as reference: kernel = ImageFilter.Kernel( (5, 5), # fmt: off @@ -170,10 +177,12 @@ def test_consistency_5x5(mode): source = source.split() * 2 reference = reference.split() * 2 - assert_image_equal( - Image.merge(mode, source[: len(mode)]).filter(kernel), - Image.merge(mode, reference[: len(mode)]), - ) + if mode == "I": + source = source[0].convert(mode) + else: + source = Image.merge(mode, source[: len(mode)]) + reference = Image.merge(mode, reference[: len(mode)]) + assert_image_equal(source.filter(kernel), reference) def test_invalid_box_blur_filter(): diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index fab3b494819..4b8d2bf05c9 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -37,6 +37,17 @@ clip8(float in) { return (UINT8)in; } +static inline INT32 +clip32(float in) { + if (in <= 0.0) { + return 0; + } + if (in >= pow(2, 31) - 1) { + return pow(2, 31) - 1; + } + return (INT32)in; +} + Imaging ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { Imaging imOut; @@ -96,8 +107,8 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { void ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x3(in0, x, kernel, d) \ - (_i2f((UINT8)in0[x - d]) * (kernel)[0] + _i2f((UINT8)in0[x]) * (kernel)[1] + \ - _i2f((UINT8)in0[x + d]) * (kernel)[2]) + (_i2f(in0[x - d]) * (kernel)[0] + _i2f(in0[x]) * (kernel)[1] + \ + _i2f(in0[x + d]) * (kernel)[2]) int x = 0, y = 0; @@ -105,21 +116,40 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 1; y < im->ysize - 1; y++) { - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *out = (UINT8 *)imOut->image[y]; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 1; y < im->ysize - 1; y++) { + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *out = (INT32 *)imOut->image[y]; + + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip32(ss); + } + out[x] = in0[x]; + } + } else { + for (y = 1; y < im->ysize - 1; y++) { + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *out = (UINT8 *)imOut->image[y]; - out[0] = in0[0]; - for (x = 1; x < im->xsize - 1; x++) { - float ss = offset; - ss += KERNEL1x3(in1, x, &kernel[0], 1); - ss += KERNEL1x3(in0, x, &kernel[3], 1); - ss += KERNEL1x3(in_1, x, &kernel[6], 1); - out[x] = clip8(ss); + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip8(ss); + } + out[x] = in0[x]; } - out[x] = in0[x]; } } else { // Add one time for rounding @@ -195,10 +225,10 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { void ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x5(in0, x, kernel, d) \ - (_i2f((UINT8)in0[x - d - d]) * (kernel)[0] + \ - _i2f((UINT8)in0[x - d]) * (kernel)[1] + _i2f((UINT8)in0[x]) * (kernel)[2] + \ - _i2f((UINT8)in0[x + d]) * (kernel)[3] + \ - _i2f((UINT8)in0[x + d + d]) * (kernel)[4]) + (_i2f(in0[x - d - d]) * (kernel)[0] + \ + _i2f(in0[x - d]) * (kernel)[1] + _i2f(in0[x]) * (kernel)[2] + \ + _i2f(in0[x + d]) * (kernel)[3] + \ + _i2f(in0[x + d + d]) * (kernel)[4]) int x = 0, y = 0; @@ -207,27 +237,52 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 2; y < im->ysize - 2; y++) { - UINT8 *in_2 = (UINT8 *)im->image[y - 2]; - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *in2 = (UINT8 *)im->image[y + 2]; - UINT8 *out = (UINT8 *)imOut->image[y]; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 2; y < im->ysize - 2; y++) { + INT32 *in_2 = (INT32 *)im->image[y - 2]; + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *in2 = (INT32 *)im->image[y + 2]; + INT32 *out = (INT32 *)imOut->image[y]; - out[0] = in0[0]; - out[1] = in0[1]; - for (x = 2; x < im->xsize - 2; x++) { - float ss = offset; - ss += KERNEL1x5(in2, x, &kernel[0], 1); - ss += KERNEL1x5(in1, x, &kernel[5], 1); - ss += KERNEL1x5(in0, x, &kernel[10], 1); - ss += KERNEL1x5(in_1, x, &kernel[15], 1); - ss += KERNEL1x5(in_2, x, &kernel[20], 1); - out[x] = clip8(ss); + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip32(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; + } + } else { + for (y = 2; y < im->ysize - 2; y++) { + UINT8 *in_2 = (UINT8 *)im->image[y - 2]; + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *in2 = (UINT8 *)im->image[y + 2]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip8(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; } - out[x + 0] = in0[x + 0]; - out[x + 1] = in0[x + 1]; } } else { // Add one time for rounding @@ -327,7 +382,7 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o Imaging imOut; ImagingSectionCookie cookie; - if (!im || im->type != IMAGING_TYPE_UINT8) { + if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) { return (Imaging)ImagingError_ModeError(); }