diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index e10200c0158..10065d6bd69 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -61,6 +61,10 @@ def test_color_modes() -> None: box_blur(sample.convert("L")) box_blur(sample.convert("LA")) box_blur(sample.convert("LA").convert("La")) + box_blur(sample.convert("I;16")) + box_blur(sample.convert("I;16L")) + box_blur(sample.convert("I;16B")) + box_blur(sample.convert("I;16N")) with pytest.raises(ValueError): box_blur(sample.convert("I")) with pytest.raises(ValueError): @@ -73,6 +77,15 @@ def test_color_modes() -> None: box_blur(sample.convert("YCbCr")) +@pytest.mark.parametrize("mode", ("I;16", "I;16L", "I;16B", "I;16N")) +def test_i16_max_value(mode: str) -> None: + im = Image.new(mode, sample.size, 65535) + assert ( + box_blur(im, 600, 3).get_flattened_data() + == (65535,) * sample.width * sample.height + ) + + @pytest.mark.parametrize("size", ((0, 1), (1, 0))) def test_zero_dimension(size: tuple[int, int]) -> None: assert box_blur(Image.new("L", size)).size == size diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 5f838c092d3..ccf2d5f069b 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -265,6 +265,35 @@ def test_consistency_i16_high_byte(mode: str) -> None: assert im.getpixel((4, 4)) == 1000 +@pytest.mark.parametrize( + "filter_to_apply", + ( + ImageFilter.GaussianBlur, + ImageFilter.GaussianBlur(0), + ImageFilter.GaussianBlur(5), + ImageFilter.GaussianBlur((2, 5)), + ImageFilter.BoxBlur(0), + ImageFilter.BoxBlur(5), + ImageFilter.BoxBlur((2, 5)), + ), +) +@pytest.mark.parametrize("mode", ("I;16", "I;16L", "I;16B", "I;16N")) +def test_blur_i16( + filter_to_apply: ImageFilter.Filter | type[ImageFilter.Filter], mode: str +) -> None: + im = hopper("L") + reference = im.filter(filter_to_apply) + result = im.convert(mode).filter(filter_to_apply) + assert result.mode == mode + assert_image_equal(result.convert("L"), reference) + + +@pytest.mark.parametrize("mode", ("I;16", "I;16L", "I;16B", "I;16N")) +def test_unsharp_mask_i16(mode: str) -> None: + with pytest.raises(ValueError, match="image has wrong mode"): + hopper(mode).filter(ImageFilter.UnsharpMask) + + @pytest.mark.parametrize("size", (-1, math.nan, math.inf, 2**31)) def test_invalid_box_blur_filter(size: int) -> None: for radius in (size, (size, size), (size, 1), (1, size)): diff --git a/docs/releasenotes/13.1.0.rst b/docs/releasenotes/13.1.0.rst new file mode 100644 index 00000000000..140bcbe3115 --- /dev/null +++ b/docs/releasenotes/13.1.0.rst @@ -0,0 +1,56 @@ +13.1.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards incompatible changes +============================== + +TODO +^^^^ + +TODO + +Deprecations +============ + +TODO +^^^^ + +TODO + +API changes +=========== + +TODO +^^^^ + +TODO + +API additions +============= + +TODO +^^^^ + +TODO + +Other changes +============= + +Blurring I;16 images +^^^^^^^^^^^^^^^^^^^^ + +:py:class:`~PIL.ImageFilter.GaussianBlur` and :py:class:`~PIL.ImageFilter.BoxBlur` +now support I;16, I;16L, I;16B and I;16N images. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index e45919afb07..bc0cb895d92 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -15,6 +15,7 @@ expected to be backported to earlier versions. :maxdepth: 2 versioning + 13.1.0 13.0.0 12.3.0 12.2.0 diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 9c4005c3071..222b527c48a 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -2,9 +2,9 @@ typedef UINT8 pixel[4]; -void static inline ImagingLineBoxBlur32( - pixel *lineOut, - pixel *lineIn, +void static inline ImagingLineBoxBlur8( + UINT8 *lineOut, + UINT8 *lineIn, int lastx, int radius, int edgeA, @@ -13,64 +13,33 @@ void static inline ImagingLineBoxBlur32( UINT32 fw ) { int x; - UINT32 acc[4]; - UINT32 bulk[4]; + UINT32 acc; + UINT32 bulk; -#define MOVE_ACC(acc, subtract, add) \ - acc[0] += lineIn[add][0] - lineIn[subtract][0]; \ - acc[1] += lineIn[add][1] - lineIn[subtract][1]; \ - acc[2] += lineIn[add][2] - lineIn[subtract][2]; \ - acc[3] += lineIn[add][3] - lineIn[subtract][3]; +#define MOVE_ACC(acc, subtract, add) acc += lineIn[add] - lineIn[subtract]; -#define ADD_FAR(bulk, acc, left, right) \ - bulk[0] = (acc[0] * ww) + (lineIn[left][0] + lineIn[right][0]) * fw; \ - bulk[1] = (acc[1] * ww) + (lineIn[left][1] + lineIn[right][1]) * fw; \ - bulk[2] = (acc[2] * ww) + (lineIn[left][2] + lineIn[right][2]) * fw; \ - bulk[3] = (acc[3] * ww) + (lineIn[left][3] + lineIn[right][3]) * fw; +#define ADD_FAR(bulk, acc, left, right) \ + bulk = (acc * ww) + (lineIn[left] + lineIn[right]) * fw; -#define SAVE(x, bulk) \ - lineOut[x][0] = (UINT8)((bulk[0] + (1 << 23)) >> 24); \ - lineOut[x][1] = (UINT8)((bulk[1] + (1 << 23)) >> 24); \ - lineOut[x][2] = (UINT8)((bulk[2] + (1 << 23)) >> 24); \ - lineOut[x][3] = (UINT8)((bulk[3] + (1 << 23)) >> 24); +#define SAVE(x, bulk) lineOut[x] = (UINT8)((bulk + (1 << 23)) >> 24) - /* Compute acc for -1 pixel (outside of image): - From "-radius-1" to "-1" get first pixel, - then from "0" to "radius-1". */ - acc[0] = lineIn[0][0] * (radius + 1); - acc[1] = lineIn[0][1] * (radius + 1); - acc[2] = lineIn[0][2] * (radius + 1); - acc[3] = lineIn[0][3] * (radius + 1); - /* As radius can be bigger than xsize, iterate to edgeA -1. */ + acc = lineIn[0] * (radius + 1); for (x = 0; x < edgeA - 1; x++) { - acc[0] += lineIn[x][0]; - acc[1] += lineIn[x][1]; - acc[2] += lineIn[x][2]; - acc[3] += lineIn[x][3]; + acc += lineIn[x]; } - /* Then multiply remainder to last x. */ - acc[0] += lineIn[lastx][0] * (radius - edgeA + 1); - acc[1] += lineIn[lastx][1] * (radius - edgeA + 1); - acc[2] += lineIn[lastx][2] * (radius - edgeA + 1); - acc[3] += lineIn[lastx][3] * (radius - edgeA + 1); + acc += lineIn[lastx] * (radius - edgeA + 1); if (edgeA <= edgeB) { - /* Subtract pixel from left ("0"). - Add pixels from radius. */ for (x = 0; x < edgeA; x++) { MOVE_ACC(acc, 0, x + radius); ADD_FAR(bulk, acc, 0, x + radius + 1); SAVE(x, bulk); } - /* Subtract previous pixel from "-radius". - Add pixels from radius. */ for (x = edgeA; x < edgeB; x++) { MOVE_ACC(acc, x - radius - 1, x + radius); ADD_FAR(bulk, acc, x - radius - 1, x + radius + 1); SAVE(x, bulk); } - /* Subtract previous pixel from "-radius". - Add last pixel. */ for (x = edgeB; x <= lastx; x++) { MOVE_ACC(acc, x - radius - 1, lastx); ADD_FAR(bulk, acc, x - radius - 1, lastx); @@ -99,7 +68,7 @@ void static inline ImagingLineBoxBlur32( #undef SAVE } -void static inline ImagingLineBoxBlur8( +void static inline ImagingLineBoxBlur16( UINT8 *lineOut, UINT8 *lineIn, int lastx, @@ -107,36 +76,143 @@ void static inline ImagingLineBoxBlur8( int edgeA, int edgeB, UINT32 ww, - UINT32 fw + UINT32 fw, + int bigendian ) { int x; - UINT32 acc; - UINT32 bulk; + uint64_t acc; + uint64_t bulk; + UINT16 value; -#define MOVE_ACC(acc, subtract, add) acc += lineIn[add] - lineIn[subtract]; +#define GET(x) \ + ((uint64_t)lineIn[(x) * 2 + (bigendian ? 1 : 0)] + \ + ((uint64_t)lineIn[(x) * 2 + (bigendian ? 0 : 1)] << 8)) + +#define MOVE_ACC(acc, subtract, add) acc += GET(add) - GET(subtract); #define ADD_FAR(bulk, acc, left, right) \ - bulk = (acc * ww) + (lineIn[left] + lineIn[right]) * fw; + bulk = (acc * ww) + (GET(left) + GET(right)) * fw; -#define SAVE(x, bulk) lineOut[x] = (UINT8)((bulk + (1 << 23)) >> 24) +#define SAVE(x, bulk) \ + value = (UINT16)((bulk + (1 << 23)) >> 24); \ + lineOut[x * 2 + (bigendian ? 1 : 0)] = (UINT8)value; \ + lineOut[x * 2 + (bigendian ? 0 : 1)] = (UINT8)(value >> 8); - acc = lineIn[0] * (radius + 1); + acc = GET(0) * (radius + 1); for (x = 0; x < edgeA - 1; x++) { - acc += lineIn[x]; + acc += GET(x); } - acc += lineIn[lastx] * (radius - edgeA + 1); + acc += GET(lastx) * (radius - edgeA + 1); + + if (edgeA <= edgeB) { + for (x = 0; x < edgeA; x++) { + MOVE_ACC(acc, 0, x + radius); + ADD_FAR(bulk, acc, 0, x + radius + 1); + SAVE(x, bulk); + } + for (x = edgeA; x < edgeB; x++) { + MOVE_ACC(acc, x - radius - 1, x + radius); + ADD_FAR(bulk, acc, x - radius - 1, x + radius + 1); + SAVE(x, bulk); + } + for (x = edgeB; x <= lastx; x++) { + MOVE_ACC(acc, x - radius - 1, lastx); + ADD_FAR(bulk, acc, x - radius - 1, lastx); + SAVE(x, bulk); + } + } else { + for (x = 0; x < edgeB; x++) { + MOVE_ACC(acc, 0, x + radius); + ADD_FAR(bulk, acc, 0, x + radius + 1); + SAVE(x, bulk); + } + for (x = edgeB; x < edgeA; x++) { + MOVE_ACC(acc, 0, lastx); + ADD_FAR(bulk, acc, 0, lastx); + SAVE(x, bulk); + } + for (x = edgeA; x <= lastx; x++) { + MOVE_ACC(acc, x - radius - 1, lastx); + ADD_FAR(bulk, acc, x - radius - 1, lastx); + SAVE(x, bulk); + } + } + +#undef GET +#undef MOVE_ACC +#undef ADD_FAR +#undef SAVE +} + +void static inline ImagingLineBoxBlur32( + pixel *lineOut, + pixel *lineIn, + int lastx, + int radius, + int edgeA, + int edgeB, + UINT32 ww, + UINT32 fw +) { + int x; + UINT32 acc[4]; + UINT32 bulk[4]; + +#define MOVE_ACC(acc, subtract, add) \ + acc[0] += lineIn[add][0] - lineIn[subtract][0]; \ + acc[1] += lineIn[add][1] - lineIn[subtract][1]; \ + acc[2] += lineIn[add][2] - lineIn[subtract][2]; \ + acc[3] += lineIn[add][3] - lineIn[subtract][3]; + +#define ADD_FAR(bulk, acc, left, right) \ + bulk[0] = (acc[0] * ww) + (lineIn[left][0] + lineIn[right][0]) * fw; \ + bulk[1] = (acc[1] * ww) + (lineIn[left][1] + lineIn[right][1]) * fw; \ + bulk[2] = (acc[2] * ww) + (lineIn[left][2] + lineIn[right][2]) * fw; \ + bulk[3] = (acc[3] * ww) + (lineIn[left][3] + lineIn[right][3]) * fw; + +#define SAVE(x, bulk) \ + lineOut[x][0] = (UINT8)((bulk[0] + (1 << 23)) >> 24); \ + lineOut[x][1] = (UINT8)((bulk[1] + (1 << 23)) >> 24); \ + lineOut[x][2] = (UINT8)((bulk[2] + (1 << 23)) >> 24); \ + lineOut[x][3] = (UINT8)((bulk[3] + (1 << 23)) >> 24); + + /* Compute acc for -1 pixel (outside of image): + From "-radius-1" to "-1" get first pixel, + then from "0" to "radius-1". */ + acc[0] = lineIn[0][0] * (radius + 1); + acc[1] = lineIn[0][1] * (radius + 1); + acc[2] = lineIn[0][2] * (radius + 1); + acc[3] = lineIn[0][3] * (radius + 1); + /* As radius can be bigger than xsize, iterate to edgeA -1. */ + for (x = 0; x < edgeA - 1; x++) { + acc[0] += lineIn[x][0]; + acc[1] += lineIn[x][1]; + acc[2] += lineIn[x][2]; + acc[3] += lineIn[x][3]; + } + /* Then multiply remainder to last x. */ + acc[0] += lineIn[lastx][0] * (radius - edgeA + 1); + acc[1] += lineIn[lastx][1] * (radius - edgeA + 1); + acc[2] += lineIn[lastx][2] * (radius - edgeA + 1); + acc[3] += lineIn[lastx][3] * (radius - edgeA + 1); if (edgeA <= edgeB) { + /* Subtract pixel from left ("0"). + Add pixels from radius. */ for (x = 0; x < edgeA; x++) { MOVE_ACC(acc, 0, x + radius); ADD_FAR(bulk, acc, 0, x + radius + 1); SAVE(x, bulk); } + /* Subtract previous pixel from "-radius". + Add pixels from radius. */ for (x = edgeA; x < edgeB; x++) { MOVE_ACC(acc, x - radius - 1, x + radius); ADD_FAR(bulk, acc, x - radius - 1, x + radius + 1); SAVE(x, bulk); } + /* Subtract previous pixel from "-radius". + Add last pixel. */ for (x = edgeB; x <= lastx; x++) { MOVE_ACC(acc, x - radius - 1, lastx); ADD_FAR(bulk, acc, x - radius - 1, lastx); @@ -185,7 +261,34 @@ ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) { ImagingSectionEnter(&cookie); - if (imIn->image8) { + if (imIn->type == IMAGING_TYPE_I16) { + int bigendian = 0; + if ( + imIn->mode == IMAGING_MODE_I_16B +#ifdef WORDS_BIGENDIAN + || imIn->mode == IMAGING_MODE_I_16N +#endif + ) { + bigendian = 1; + } + for (y = 0; y < imIn->ysize; y++) { + ImagingLineBoxBlur16( + (imIn == imOut ? (UINT8 *)lineOut : imOut->image8[y]), + imIn->image8[y], + imIn->xsize - 1, + radius, + edgeA, + edgeB, + ww, + fw, + bigendian + ); + if (imIn == imOut) { + // Commit. + memcpy(imOut->image8[y], lineOut, imIn->linesize); + } + } + } else if (imIn->image8) { for (y = 0; y < imIn->ysize; y++) { ImagingLineBoxBlur8( (imIn == imOut ? (UINT8 *)lineOut : imOut->image8[y]), @@ -199,7 +302,7 @@ ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) { ); if (imIn == imOut) { // Commit. - memcpy(imOut->image8[y], lineOut, imIn->xsize); + memcpy(imOut->image8[y], lineOut, imIn->linesize); } } } else { @@ -216,7 +319,7 @@ ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) { ); if (imIn == imOut) { // Commit. - memcpy(imOut->image32[y], lineOut, imIn->xsize * 4); + memcpy(imOut->image32[y], lineOut, imIn->linesize); } } } @@ -254,14 +357,12 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) return ImagingError_Mismatch(); } - if (imIn->type != IMAGING_TYPE_UINT8) { - return ImagingError_ModeError(); - } - - if (imIn->mode != IMAGING_MODE_RGB && imIn->mode != IMAGING_MODE_RGBA && - imIn->mode != IMAGING_MODE_RGBa && imIn->mode != IMAGING_MODE_RGBX && - imIn->mode != IMAGING_MODE_CMYK && imIn->mode != IMAGING_MODE_L && - imIn->mode != IMAGING_MODE_LA && imIn->mode != IMAGING_MODE_La) { + if (!(imIn->type == IMAGING_TYPE_UINT8 && + (imIn->mode == IMAGING_MODE_RGB || imIn->mode == IMAGING_MODE_RGBA || + imIn->mode == IMAGING_MODE_RGBa || imIn->mode == IMAGING_MODE_RGBX || + imIn->mode == IMAGING_MODE_CMYK || imIn->mode == IMAGING_MODE_L || + imIn->mode == IMAGING_MODE_LA || imIn->mode == IMAGING_MODE_La)) && + imIn->type != IMAGING_TYPE_I16) { return ImagingError_ModeError(); } diff --git a/src/libImaging/UnsharpMask.c b/src/libImaging/UnsharpMask.c index e714749effd..b56e01631ca 100644 --- a/src/libImaging/UnsharpMask.c +++ b/src/libImaging/UnsharpMask.c @@ -35,8 +35,12 @@ ImagingUnsharpMask( UINT8 *lineIn8 = NULL; UINT8 *lineOut8 = NULL; + if (imIn->type != IMAGING_TYPE_UINT8) { + return ImagingError_ModeError(); + } + /* First, do a gaussian blur on the image, putting results in imOut - temporarily. All format checks are in gaussian blur. */ + temporarily. All other format checks are in gaussian blur. */ result = ImagingGaussianBlur(imOut, imIn, radius, radius, 3); if (!result) { return NULL;