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.
This commit is contained in:
Oliver Tonnhofer 2019-06-04 13:30:13 +02:00 committed by Andrew Murray
parent 61add9d6b0
commit 2b7d8be536
5 changed files with 71 additions and 15 deletions

View File

@ -435,18 +435,44 @@ class TestFileLibTiff(LibTiffTestCase):
self.assert_image_equal(im, im2) self.assert_image_equal(im, im2)
def test_compressions(self): def test_compressions(self):
# Test various tiff compressions and assert similar image content but reduced
# file sizes.
im = hopper("RGB") im = hopper("RGB")
out = self.tempfile("temp.tif") out = self.tempfile("temp.tif")
im.save(out)
size_raw = os.path.getsize(out)
for compression in ("packbits", "tiff_lzw"): for compression in ("packbits", "tiff_lzw"):
im.save(out, compression=compression) im.save(out, compression=compression)
size_compressed = os.path.getsize(out)
im2 = Image.open(out) im2 = Image.open(out)
self.assert_image_equal(im, im2) self.assert_image_equal(im, im2)
im.save(out, compression="jpeg") im.save(out, compression="jpeg")
size_jpeg = os.path.getsize(out)
im2 = Image.open(out) im2 = Image.open(out)
self.assert_image_similar(im, im2, 30) 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): def test_cmyk_save(self):
im = hopper("CMYK") im = hopper("CMYK")
out = self.tempfile("temp.tif") out = self.tempfile("temp.tif")

View File

@ -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_thunderscan"``, ``"tiff_deflate"``, ``"tiff_sgilog"``,
``"tiff_sgilog24"``, ``"tiff_raw_16"`` ``"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 These arguments to set the tiff header fields are an alternative to
using the general tags available through tiffinfo. using the general tags available through tiffinfo.

View File

@ -116,6 +116,7 @@ PHOTOSHOP_CHUNK = 34377 # photoshop properties
ICCPROFILE = 34675 ICCPROFILE = 34675
EXIFIFD = 34665 EXIFIFD = 34665
XMP = 700 XMP = 700
JPEGQUALITY = 65537 # pseudo-tag by libtiff
# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java # https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java
IMAGEJ_META_DATA_BYTE_COUNTS = 50838 IMAGEJ_META_DATA_BYTE_COUNTS = 50838
@ -1529,6 +1530,16 @@ def _save(im, fp, filename):
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
if libtiff: 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: if DEBUG:
print("Saving using libtiff encoder") print("Saving using libtiff encoder")
print("Items: %s" % sorted(ifd.items())) print("Items: %s" % sorted(ifd.items()))
@ -1604,7 +1615,12 @@ def _save(im, fp, filename):
if im.mode in ("I;16B", "I;16"): if im.mode in ("I;16B", "I;16"):
rawmode = "I;16N" 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 = Image._getencoder(im.mode, "libtiff", a, im.encoderconfig)
e.setimage(im.im, (0, 0) + im.size) e.setimage(im.im, (0, 0) + im.size)
while True: while True:

View File

@ -432,6 +432,9 @@ TYPES = {}
# 389: case TIFFTAG_REFERENCEBLACKWHITE: # 389: case TIFFTAG_REFERENCEBLACKWHITE:
# 393: case TIFFTAG_INKNAMES: # 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 # some of these are not in our TAGS_V2 dict and were included from tiff.h
# This list also exists in encode.c # This list also exists in encode.c
@ -476,6 +479,7 @@ LIBTIFF_CORE = {
333, 333,
# as above # as above
269, # this has been in our tests forever, and works 269, # this has been in our tests forever, and works
65537,
} }
LIBTIFF_CORE.remove(320) # Array of short, crashes LIBTIFF_CORE.remove(320) # Array of short, crashes

View File

@ -643,27 +643,29 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args)
const int core_tags[] = { const int core_tags[] = {
256, 257, 258, 259, 262, 263, 266, 269, 274, 277, 278, 280, 281, 340, 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, 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; Py_ssize_t tags_size;
PyObject *keys, *values; PyObject *item;
if (! PyArg_ParseTuple(args, "sssisOO", &mode, &rawmode, &compname, &fp, &filename, &tags, &types)) { if (! PyArg_ParseTuple(args, "sssisOO", &mode, &rawmode, &compname, &fp, &filename, &tags, &types)) {
return NULL; return NULL;
} }
if (!PyDict_Check(tags)) { if (!PyList_Check(tags)) {
PyErr_SetString(PyExc_ValueError, "Invalid tags dictionary"); PyErr_SetString(PyExc_ValueError, "Invalid tags list");
return NULL; return NULL;
} else { } else {
d_size = PyDict_Size(tags); tags_size = PyList_Size(tags);
TRACE(("dict size: %d\n", (int)d_size)); TRACE(("tags size: %d\n", (int)tags_size));
keys = PyDict_Keys(tags); for (pos=0;pos<tags_size;pos++){
values = PyDict_Values(tags); item = PyList_GetItem(tags, pos);
for (pos=0;pos<d_size;pos++){ if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) {
TRACE((" key: %d\n", (int)PyInt_AsLong(PyList_GetItem(keys,pos)))); PyErr_SetString(PyExc_ValueError, "Invalid tags list");
return NULL;
}
} }
pos = 0; pos = 0;
} }
@ -688,10 +690,12 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args)
} }
num_core_tags = sizeof(core_tags) / sizeof(int); num_core_tags = sizeof(core_tags) / sizeof(int);
for (pos = 0; pos < d_size; pos++) { for (pos = 0; pos < tags_size; pos++) {
key = PyList_GetItem(keys, pos); item = PyList_GetItem(tags, pos);
// We already checked that tags is a 2-tuple list.
key = PyTuple_GetItem(item, 0);
key_int = (int)PyInt_AsLong(key); key_int = (int)PyInt_AsLong(key);
value = PyList_GetItem(values, pos); value = PyTuple_GetItem(item, 1);
status = 0; status = 0;
is_core_tag = 0; is_core_tag = 0;
is_var_length = 0; is_var_length = 0;