diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index 18c6f657925..c1b2cc37896 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -25,9 +25,9 @@ def check(im: Image.Image, fill_color: int | tuple[int, ...]) -> None: im.paste(fill_color, (-10, -10, 110, 110)) assert im.getbbox() == (0, 0, 100, 100) - # 8-bit mode - im = Image.new("L", (100, 100), 0) - check(im, 255) + for mode in ("1", "L", "I", "P", "I;16", "I;16L", "I;16B"): + im = Image.new(mode, (100, 100), 0) + check(im, 255) # 32-bit mode im = Image.new("RGB", (100, 100), 0) @@ -44,6 +44,30 @@ def check(im: Image.Image, fill_color: int | tuple[int, ...]) -> None: check(im, (255, 255)) +@pytest.mark.parametrize( + "mode", ("L", "RGB", "RGBA", "I", "F", "I;16", "I;16L", "I;16B") +) +@pytest.mark.parametrize( + "box", + ( + (0, 0, 1, 1), # single pixel, top-left + (99, 0, 100, 1), # single pixel, top-right + (0, 99, 1, 100), # single pixel, bottom-left + (99, 99, 100, 100), # single pixel, bottom-right + (0, 0, 100, 1), # full top row + (0, 99, 100, 100), # full bottom row + (0, 0, 1, 100), # full left column + (99, 0, 100, 100), # full right column + (40, 40, 60, 60), # centred block + ), +) +def test_bbox_edges(mode: str, box: tuple[int, int, int, int]) -> None: + im = Image.new(mode, (100, 100), 0) + bands = Image.getmodebands(mode) + im.paste((255,) * bands, box) + assert im.getbbox() == box + + @pytest.mark.parametrize("mode", ("RGBA", "RGBa", "La", "LA", "PA")) def test_bbox_alpha_only_false(mode: str) -> None: im = Image.new(mode, (100, 100)) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 7a57f6894a0..b0d97a858ca 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -22,69 +22,101 @@ int ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { /* Get the bounding box for any non-zero data in the image.*/ - int x, y; - int has_data; + int xsize = im->xsize, ysize = im->ysize; /* Initialize bounding box to max values */ - bbox[0] = im->xsize; + bbox[0] = xsize; bbox[1] = -1; bbox[2] = bbox[3] = 0; -#define GETBBOX(image, mask) \ - /* first stage: looking for any pixels from top */ \ - for (y = 0; y < im->ysize; y++) { \ - has_data = 0; \ - for (x = 0; x < im->xsize; x++) { \ - if (im->image[y][x] & mask) { \ - has_data = 1; \ - bbox[0] = x; \ - bbox[1] = y; \ - break; \ - } \ - } \ - if (has_data) { \ - break; \ - } \ - } \ - /* Check that we have a box */ \ - if (bbox[1] < 0) { \ - return 0; /* no data */ \ - } \ - /* second stage: looking for any pixels from bottom */ \ - for (y = im->ysize - 1; y >= bbox[1]; y--) { \ - has_data = 0; \ - for (x = 0; x < im->xsize; x++) { \ - if (im->image[y][x] & mask) { \ - has_data = 1; \ - if (x < bbox[0]) { \ - bbox[0] = x; \ - } \ - bbox[3] = y + 1; \ - break; \ - } \ - } \ - if (has_data) { \ - break; \ - } \ - } \ - /* third stage: looking for left and right boundaries */ \ - for (y = bbox[1]; y < bbox[3]; y++) { \ - for (x = 0; x < bbox[0]; x++) { \ - if (im->image[y][x] & mask) { \ - bbox[0] = x; \ - break; \ - } \ - } \ - for (x = im->xsize - 1; x >= bbox[2]; x--) { \ - if (im->image[y][x] & mask) { \ - bbox[2] = x + 1; \ - break; \ - } \ - } \ +#define GETBBOX(image, mask, type) \ + /* first stage: looking for any pixels from top */ \ + for (int y = 0; y < ysize; y++) { \ + const type *restrict row = (const type *)im->image[y]; \ + /* vectorisable OR-reduce of the row */ \ + type acc = 0; \ + for (int x = 0; x < xsize; x++) { \ + acc |= row[x] & mask; \ + } \ + if (!acc) { \ + continue; \ + } \ + for (int x = 0; x < xsize; x++) { \ + if (row[x] & mask) { \ + bbox[0] = x; \ + bbox[1] = y; \ + break; \ + } \ + } \ + break; \ + } \ + /* Check that we have a box */ \ + if (bbox[1] < 0) { \ + return 0; /* no data */ \ + } \ + /* second stage: looking for any pixels from bottom */ \ + for (int y = ysize - 1; y >= bbox[1]; y--) { \ + const type *restrict row = (const type *)im->image[y]; \ + /* vectorisable OR-reduce of the row */ \ + type acc = 0; \ + for (int x = 0; x < xsize; x++) { \ + acc |= row[x] & mask; \ + } \ + if (!acc) { \ + continue; \ + } \ + for (int x = 0; x < xsize; x++) { \ + if (row[x] & mask) { \ + bbox[0] = x < bbox[0] ? x : bbox[0]; \ + bbox[3] = y + 1; \ + break; \ + } \ + } \ + break; \ + } \ + /* third stage: looking for left and right boundaries */ \ + for (int y = bbox[1]; y < bbox[3]; y++) { \ + const type *restrict row = (const type *)im->image[y]; \ + if (bbox[0] > 0) { \ + /* vectorisable OR-reduce of the left margin */ \ + type acc = 0; \ + for (int x = 0; x < bbox[0]; x++) { \ + acc |= row[x] & mask; \ + } \ + if (acc) { \ + for (int x = 0; x < bbox[0]; x++) { \ + if (row[x] & mask) { \ + bbox[0] = x; \ + break; \ + } \ + } \ + } \ + } \ + if (bbox[2] < xsize) { \ + /* vectorisable OR-reduce of the right margin */ \ + type acc = 0; \ + for (int x = bbox[2]; x < xsize; x++) { \ + acc |= row[x] & mask; \ + } \ + if (acc) { \ + for (int x = xsize - 1; x >= bbox[2]; x--) { \ + if (row[x] & mask) { \ + bbox[2] = x + 1; \ + break; \ + } \ + } \ + } \ + } \ } if (im->image8) { - GETBBOX(image8, 0xff); + if (isModeI16(im->mode)) { + // In I;16 modes, image8 is two-byte pixels, so scan as UINT16. + // Since we're looking for zeroes, endianness doesn't matter. + GETBBOX(image8, 0xffff, UINT16); + } else { + GETBBOX(image8, 0xff, UINT8); + } } else { INT32 mask = 0xffffffff; if (im->bands == 3) { @@ -101,7 +133,7 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { mask = 0xff000000; #endif } - GETBBOX(image32, mask); + GETBBOX(image32, mask, INT32); } return 1; /* ok */