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 support for high bit depth multichannel images #8223

Closed
wants to merge 15 commits into from
Closed
Binary file added Tests/images/uint16_2_4660.tif
Binary file not shown.
Binary file added Tests/images/uint16_3_4660.tif
Binary file not shown.
Binary file added Tests/images/uint16_4_4660.tif
Binary file not shown.
Binary file added Tests/images/uint16_5_4660.tif
Binary file not shown.
34 changes: 34 additions & 0 deletions Tests/test_file_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,40 @@ def test_oom(self, test_file: str) -> None:
with Image.open(test_file):
pass

def test_open_tiff_uint16_multiband(self):
"""Test opening multiband TIFFs and reading all channels."""

def check_pixel(im: Image.Image, expected_pixel, pos: tuple[int, int]):
actual_pixel = im.getpixel((0, 0))
if isinstance(actual_pixel, int):
actual_pixel = (actual_pixel,)
assert actual_pixel == expected_pixel

def check_image(im: Image.Image, width: int, height: int, expected_pixel):
assert im.width == width
assert im.height == height
for x in range(im.width):
for y in range(im.height):
check_pixel(im, expected_pixel, (x, y))

base_value = 4660
for i in range(1, 6):
pixel = tuple([base_value + j for j in range(0, i)])
infile = f"Tests/images/uint16_{i}_{base_value}.tif"
im = Image.open(infile)

im.load()
check_image(im, 10, 10, pixel)

im1 = im.copy()
check_image(im1, 10, 10, pixel)

im2 = im.crop((2, 2, 7, 7))
check_image(im2, 5, 5, pixel)

im3 = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
check_image(im3, 10, 10, pixel)


@pytest.mark.skipif(not is_win32(), reason="Windows only")
class TestFileTiffW32:
Expand Down
6 changes: 4 additions & 2 deletions src/PIL/ImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ def __init__(self, fp=None, filename=None):

self.readonly = 1 # until we know better

self.newconfig = ()

self.decoderconfig = ()
self.decodermaxblock = MAXBLOCK

Expand Down Expand Up @@ -225,7 +227,7 @@ def load(self):
msg = "buffer is not large enough"
raise OSError(msg)
self.im = Image.core.map_buffer(
self.map, self.size, decoder_name, offset, args
self.map, self.size, decoder_name, offset, args, *self.newconfig
)
readonly = 1
# After trashing self.im,
Expand Down Expand Up @@ -314,7 +316,7 @@ def load(self):
def load_prepare(self) -> None:
# create image memory if necessary
if not self.im or self.im.mode != self.mode or self.im.size != self.size:
self.im = Image.core.new(self.mode, self.size)
self.im = Image.core.new(self.mode, self.size, *self.newconfig)
# create palette (optional)
if self.mode == "P":
Image.Image.load(self)
Expand Down
58 changes: 50 additions & 8 deletions src/PIL/TiffImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
from .TiffTags import TYPES

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) # XXX hack202406

# Set these to true to force use of libtiff for reading or writing.
READ_LIBTIFF = False
Expand Down Expand Up @@ -182,6 +183,43 @@
(II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"),
(II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"),
(II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"),
(II, 1, (1, 1), 1, (16, 16), (0,)): ("MB", "MB"),
(
II,
1,
(1, 1, 1),
1,
(16, 16, 16),
(
0,
0,
),
): ("MB", "MB"),
(
II,
1,
(1, 1, 1, 1),
1,
(16, 16, 16, 16),
(
0,
0,
0,
),
): ("MB", "MB"),
(
II,
1,
(1, 1, 1, 1, 1),
1,
(16, 16, 16, 16, 16),
(
0,
0,
0,
0,
),
): ("MB", "MB"),
(MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"),
(II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"),
(II, 1, (2,), 1, (16,), ()): ("I", "I;16S"),
Expand All @@ -196,7 +234,9 @@
(II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"),
(MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"),
(II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
(II, 2, (1, 1, 1), 1, (8, 8, 8), ()): ("RGB", "RGB"),
(MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
(MM, 2, (1, 1, 1), 1, (8, 8, 8), ()): ("RGB", "RGB"),
(II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
(MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
(II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
Expand All @@ -209,11 +249,13 @@
(MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"),
(II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
(MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
(MM, 2, (1, 1, 1, 1), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
(II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
(MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
(II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"),
(MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"),
(II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
(II, 2, (1, 1, 1, 1), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
(MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
(II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"),
(MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"),
Expand All @@ -222,13 +264,16 @@
(II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
(MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
(II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"),
(II, 2, (1, 1, 1), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"),
(MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"),
(II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"),
(MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"),
(II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16L"),
(MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16B"),
(II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"),
(II, 2, (1, 1, 1, 1), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"),
(MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"),
(MM, 2, (1, 1, 1, 1), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"),
(II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"),
(MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"),
(II, 3, (1,), 1, (1,), ()): ("P", "P;1"),
Expand Down Expand Up @@ -262,9 +307,11 @@
# JPEG compressed images handled by LibTiff and auto-converted to RGBX
# Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel
(II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
(II, 6, (1, 1, 1), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
(MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
(II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
(MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
# XXX hack202406 these entries allow all TIFF tests to pass, but more may be needed
}

MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO)
Expand Down Expand Up @@ -1379,14 +1426,6 @@ def _setup(self):
logger.debug("- size: %s", self.size)

sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))
if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1:
# SAMPLEFORMAT is properly per band, so an RGB image will
# be (1,1,1). But, we don't support per band pixel types,
# and anything more than one band is a uint8. So, just
# take the first element. Revisit this if adding support
# for more exotic images.
sample_format = (1,)

bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,))
extra_tuple = self.tag_v2.get(EXTRASAMPLES, ())
if photo in (2, 6, 8): # RGB, YCbCr, LAB
Expand Down Expand Up @@ -1442,6 +1481,9 @@ def _setup(self):

logger.debug("- raw mode: %s", rawmode)
logger.debug("- pil mode: %s", self.mode)
if self.mode == "MB":
assert max(bps_tuple) == min(bps_tuple)
self.newconfig = (max(bps_tuple), samples_per_pixel)

self.info["compression"] = self._compression

Expand Down
70 changes: 56 additions & 14 deletions src/_imaging.c
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@
int bands;

/* FIXME: add primitive to libImaging to avoid extra allocation */
im = ImagingNew(mode, 0, 0);
im = ImagingNew(mode, (ImagingNewParams){0, 0});
if (!im) {
return -1;
}
Expand Down Expand Up @@ -433,6 +433,36 @@
return out[0];
}

static inline PyObject *
getpixel_mb(Imaging im, ImagingAccess access, int x, int y) {
UINT8 pixel[sizeof(INT32) * 6];
assert(im->pixelsize <= sizeof(pixel));
access->get_pixel(im, x, y, &pixel);

PyObject *tuple = PyTuple_New(im->bands);
if (tuple == NULL) {
return NULL;

Check warning on line 444 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L444

Added line #L444 was not covered by tests
}

UINT8 *pos = pixel;
for (int i = 0; i < im->bands; ++i) {
switch (im->depth) {
case CHAR_BIT:
PyTuple_SET_ITEM(tuple, i, PyLong_FromLong(*pos));
break;

Check warning on line 452 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L450-L452

Added lines #L450 - L452 were not covered by tests
case 2 * CHAR_BIT:
PyTuple_SET_ITEM(tuple, i, PyLong_FromLong(*(UINT16 *)pos));
break;
case 4 * CHAR_BIT:
PyTuple_SET_ITEM(tuple, i, PyLong_FromLong(*(INT32 *)pos));
break;

Check warning on line 458 in src/_imaging.c

View check run for this annotation

Codecov / codecov/patch

src/_imaging.c#L456-L458

Added lines #L456 - L458 were not covered by tests
}
pos += im->depth / CHAR_BIT;
}

return tuple;
}

static inline PyObject *
getpixel(Imaging im, ImagingAccess access, int x, int y) {
union {
Expand All @@ -454,6 +484,10 @@
return NULL;
}

if (im->type == IMAGING_TYPE_MB) {
return getpixel_mb(im, access, x, y);
}

access->get_pixel(im, x, y, &pixel);

switch (im->type) {
Expand Down Expand Up @@ -663,7 +697,7 @@
return NULL;
}

im = ImagingNewDirty(mode, xsize, ysize);
im = ImagingNewDirty(mode, (ImagingNewParams){xsize, ysize});
if (!im) {
return NULL;
}
Expand All @@ -684,13 +718,14 @@
static PyObject *
_new(PyObject *self, PyObject *args) {
char *mode;
int xsize, ysize;
int xsize, ysize, depth = -1, bands = -1;

if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) {
if (!PyArg_ParseTuple(args, "s(ii)|ii", &mode, &xsize, &ysize, &depth, &bands)) {
return NULL;
}

return PyImagingNew(ImagingNew(mode, xsize, ysize));
return PyImagingNew(
ImagingNew(mode, (ImagingNewParams){xsize, ysize, depth, bands}));
}

static PyObject *
Expand Down Expand Up @@ -906,7 +941,8 @@
return NULL;
}

imOut = ImagingNewDirty(mode, self->image->xsize, self->image->ysize);
imOut = ImagingNewDirty(
mode, (ImagingNewParams){self->image->xsize, self->image->ysize});
if (!imOut) {
free(prepared_table);
return NULL;
Expand Down Expand Up @@ -1093,7 +1129,7 @@
}

imIn = self->image;
imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize);
imOut = ImagingNewDirty(imIn->mode, (ImagingNewParams){imIn->xsize, imIn->ysize});
if (!imOut) {
return NULL;
}
Expand Down Expand Up @@ -1713,7 +1749,8 @@

if (!self->image->xsize || !self->image->ysize) {
/* no content; return an empty image */
return PyImagingNew(ImagingNew("P", self->image->xsize, self->image->ysize));
return PyImagingNew(ImagingNew(
"P", (ImagingNewParams){self->image->xsize, self->image->ysize}));
}

return PyImagingNew(ImagingQuantize(self->image, colours, method, kmeans));
Expand Down Expand Up @@ -1920,7 +1957,7 @@
a[2] = box[0];
a[5] = box[1];

imOut = ImagingNewDirty(imIn->mode, xsize, ysize);
imOut = ImagingNewDirty(imIn->mode, (ImagingNewParams){xsize, ysize});

imOut = ImagingTransform(
imOut, imIn, IMAGING_TRANSFORM_AFFINE, 0, 0, xsize, ysize, a, filter, 1);
Expand Down Expand Up @@ -2105,13 +2142,17 @@
case 0: /* flip left right */
case 1: /* flip top bottom */
case 3: /* rotate 180 */
imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize);
imOut = ImagingNewDirty(
imIn->mode,
(ImagingNewParams){imIn->xsize, imIn->ysize, imIn->depth, imIn->bands});
break;
case 2: /* rotate 90 */
case 4: /* rotate 270 */
case 5: /* transpose */
case 6: /* transverse */
imOut = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize);
imOut = ImagingNewDirty(
imIn->mode,
(ImagingNewParams){imIn->ysize, imIn->xsize, imIn->depth, imIn->bands});
break;
default:
PyErr_SetString(PyExc_ValueError, "No such transpose operation");
Expand Down Expand Up @@ -2160,7 +2201,7 @@
}

imIn = self->image;
imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize);
imOut = ImagingNewDirty(imIn->mode, (ImagingNewParams){imIn->xsize, imIn->ysize});
if (!imOut) {
return NULL;
}
Expand All @@ -2185,7 +2226,7 @@
}

imIn = self->image;
imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize);
imOut = ImagingNewDirty(imIn->mode, (ImagingNewParams){imIn->xsize, imIn->ysize});
if (!imOut) {
return NULL;
}
Expand Down Expand Up @@ -2781,7 +2822,8 @@
return NULL;
}

im = ImagingNew(self->bitmap->mode, textwidth(self, text), self->ysize);
im = ImagingNew(
self->bitmap->mode, (ImagingNewParams){textwidth(self, text), self->ysize});
if (!im) {
free(text);
return ImagingError_MemoryError();
Expand Down
Loading
Loading