From 2b7d8be536dc1380c0a069a39134101c632858ff Mon Sep 17 00:00:00 2001 From: Oliver Tonnhofer Date: Tue, 4 Jun 2019 13:30:13 +0200 Subject: [PATCH] tiff: add support for JPEG quality Uses JPEGQUALITY pseudo-tag from libtiff. Also changes the way tags are passed to PyImaging_LibTiffEncoderNew from dict to list to ensure that COMPRESSION tag is added before JPEGQUALITY. This is required as the COMPRESSION tag registers the JPEGQUALITY pseudo-tag. --- Tests/test_file_libtiff.py | 26 ++++++++++++++++++++++ docs/handbook/image-file-formats.rst | 6 ++++++ src/PIL/TiffImagePlugin.py | 18 +++++++++++++++- src/PIL/TiffTags.py | 4 ++++ src/encode.c | 32 ++++++++++++++++------------ 5 files changed, 71 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 070fe44c1..236ce70f6 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -435,18 +435,44 @@ class TestFileLibTiff(LibTiffTestCase): self.assert_image_equal(im, im2) def test_compressions(self): + # Test various tiff compressions and assert similar image content but reduced + # file sizes. im = hopper("RGB") out = self.tempfile("temp.tif") + im.save(out) + size_raw = os.path.getsize(out) for compression in ("packbits", "tiff_lzw"): im.save(out, compression=compression) + size_compressed = os.path.getsize(out) im2 = Image.open(out) self.assert_image_equal(im, im2) im.save(out, compression="jpeg") + size_jpeg = os.path.getsize(out) im2 = Image.open(out) self.assert_image_similar(im, im2, 30) + im.save(out, compression="jpeg", quality=30) + size_jpeg_30 = os.path.getsize(out) + im3 = Image.open(out) + self.assert_image_similar(im2, im3, 30) + + assert size_raw > size_compressed + assert size_compressed > size_jpeg + assert size_jpeg > size_jpeg_30 + + def test_quality(self): + im = hopper("RGB") + out = self.tempfile("temp.tif") + + self.assertRaises(ValueError, im.save, out, compression="tiff_lzw", quality=50) + self.assertRaises(ValueError, im.save, out, compression="jpeg", quality=-1) + self.assertRaises(ValueError, im.save, out, compression="jpeg", quality=101) + self.assertRaises(ValueError, im.save, out, compression="jpeg", quality="good") + im.save(out, compression="jpeg", quality=0) + im.save(out, compression="jpeg", quality=100) + def test_cmyk_save(self): im = hopper("CMYK") out = self.tempfile("temp.tif") diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index cc6102a37..8822711ca 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -738,6 +738,12 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum ``"tiff_thunderscan"``, ``"tiff_deflate"``, ``"tiff_sgilog"``, ``"tiff_sgilog24"``, ``"tiff_raw_16"`` +**quality** + The image quality for JPEG compression, on a scale from 0 (worst) to 100 + (best). The default is 75. + + .. versionadded:: 6.1.0 + These arguments to set the tiff header fields are an alternative to using the general tags available through tiffinfo. diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 7a1ff49e0..d8634ba8a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -116,6 +116,7 @@ PHOTOSHOP_CHUNK = 34377 # photoshop properties ICCPROFILE = 34675 EXIFIFD = 34665 XMP = 700 +JPEGQUALITY = 65537 # pseudo-tag by libtiff # https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java IMAGEJ_META_DATA_BYTE_COUNTS = 50838 @@ -1529,6 +1530,16 @@ def _save(im, fp, filename): ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) if libtiff: + if "quality" in im.encoderinfo: + quality = im.encoderinfo["quality"] + if not isinstance(quality, int) or quality < 0 or quality > 100: + raise ValueError("Invalid quality setting") + if compression != "jpeg": + raise ValueError( + "quality setting only supported for 'jpeg' compression" + ) + ifd[JPEGQUALITY] = quality + if DEBUG: print("Saving using libtiff encoder") print("Items: %s" % sorted(ifd.items())) @@ -1604,7 +1615,12 @@ def _save(im, fp, filename): if im.mode in ("I;16B", "I;16"): rawmode = "I;16N" - a = (rawmode, compression, _fp, filename, atts, types) + # Pass tags as sorted list so that the tags are set in a fixed order. + # This is required by libtiff for some tags. For example, the JPEGQUALITY + # pseudo tag requires that the COMPRESS tag was already set. + tags = list(atts.items()) + tags.sort() + a = (rawmode, compression, _fp, filename, tags, types) e = Image._getencoder(im.mode, "libtiff", a, im.encoderconfig) e.setimage(im.im, (0, 0) + im.size) while True: diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index d0c98aa5a..2971101cd 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -432,6 +432,9 @@ TYPES = {} # 389: case TIFFTAG_REFERENCEBLACKWHITE: # 393: case TIFFTAG_INKNAMES: +# Following pseudo-tags are also handled by default in libtiff: +# TIFFTAG_JPEGQUALITY 65537 + # some of these are not in our TAGS_V2 dict and were included from tiff.h # This list also exists in encode.c @@ -476,6 +479,7 @@ LIBTIFF_CORE = { 333, # as above 269, # this has been in our tests forever, and works + 65537, } LIBTIFF_CORE.remove(320) # Array of short, crashes diff --git a/src/encode.c b/src/encode.c index 7f8cc479a..4db20e5c2 100644 --- a/src/encode.c +++ b/src/encode.c @@ -643,27 +643,29 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) const int core_tags[] = { 256, 257, 258, 259, 262, 263, 266, 269, 274, 277, 278, 280, 281, 340, 341, 282, 283, 284, 286, 287, 296, 297, 321, 338, 32995, 32998, 32996, - 339, 32997, 330, 531, 530 + 339, 32997, 330, 531, 530, 65537 }; - Py_ssize_t d_size; - PyObject *keys, *values; + Py_ssize_t tags_size; + PyObject *item; if (! PyArg_ParseTuple(args, "sssisOO", &mode, &rawmode, &compname, &fp, &filename, &tags, &types)) { return NULL; } - if (!PyDict_Check(tags)) { - PyErr_SetString(PyExc_ValueError, "Invalid tags dictionary"); + if (!PyList_Check(tags)) { + PyErr_SetString(PyExc_ValueError, "Invalid tags list"); return NULL; } else { - d_size = PyDict_Size(tags); - TRACE(("dict size: %d\n", (int)d_size)); - keys = PyDict_Keys(tags); - values = PyDict_Values(tags); - for (pos=0;pos