Skip to content
Closed
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
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 = ()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have a more descriptive name for this?


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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 @@ getbands(const char *mode) {
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 @@ float16tofloat32(const FLOAT16 in) {
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;
}

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;
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;
}
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 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) {
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 @@ _fill(PyObject *self, PyObject *args) {
return NULL;
}

im = ImagingNewDirty(mode, xsize, ysize);
im = ImagingNewDirty(mode, (ImagingNewParams){xsize, ysize});
if (!im) {
return NULL;
}
Expand All @@ -684,13 +718,14 @@ _fill(PyObject *self, PyObject *args) {
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 @@ _color_lut_3d(ImagingObject *self, PyObject *args) {
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 @@ _gaussian_blur(ImagingObject *self, PyObject *args) {
}

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 @@ _quantize(ImagingObject *self, PyObject *args) {

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 @@ _resize(ImagingObject *self, PyObject *args) {
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 @@ _transpose(ImagingObject *self, PyObject *args) {
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 @@ _unsharp_mask(ImagingObject *self, PyObject *args) {
}

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 @@ _box_blur(ImagingObject *self, PyObject *args) {
}

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 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
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