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 #8157

Closed
wants to merge 16 commits into from
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
1,576 changes: 1,576 additions & 0 deletions PIL/TiffImagePlugin.py
Copy link
Member

Choose a reason for hiding this comment

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

Is this an old file added by accident?

The source is now under src/PIL/ not PIL/, and this file has handling for Python 2.6.

Large diffs are not rendered by default.

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.
60 changes: 60 additions & 0 deletions Tests/test_file_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,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 All @@ -916,3 +950,29 @@ def test_fd_leak(self, tmp_path: Path) -> None:
# this should not fail, as load should have closed the file pointer,
# and close should have closed the mmap
os.remove(tmpfile)

# Created with ImageMagick: convert hopper.jpg hopper_jpg.tif
# Contains JPEGTables (347) tag
infile = "Tests/images/hopper_jpg.tif"
im = Image.open(infile)

# Act / Assert
# Should not raise UnicodeDecodeError or anything else
im.save(outfile)

def test_open_tiff_uint16_multiband(self):
"""Test opening multiband TIFFs and reading all channels."""
base_value = 4660
for i in range(2, 6):
infile = f"Tests/images/uint16_{i}_{base_value}.tif"
im = Image.open(infile)
im.load()
pixel = [base_value + j for j in range(0, i)]
actual_pixel = im.getpixel((0, 0))
if type(actual_pixel) is int:
actual_pixel = [actual_pixel]
self.assertEqual(actual_pixel, pixel)


if __name__ == "__main__":
unittest.main()
6 changes: 4 additions & 2 deletions src/PIL/ImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ def __init__(self, fp=None, filename=None):

self.readonly = 1 # until we know better

self.mb_config = ()

self.decoderconfig = ()
self.decodermaxblock = MAXBLOCK

Expand Down Expand Up @@ -227,7 +229,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.mb_config
)
readonly = 1
# After trashing self.im,
Expand Down Expand Up @@ -316,7 +318,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.mb_config)
# create palette (optional)
if self.mode == "P":
Image.Image.load(self)
Expand Down
57 changes: 49 additions & 8 deletions src/PIL/TiffImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,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 +233,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 +248,13 @@
(MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "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 +263,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,)): ("RGB", "RGBX;16L"),
(MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "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 +306,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 +1425,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 +1480,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.mb_config = (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
Loading