Skip to content

Commit

Permalink
tiff: add support for JPEG quality
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
olt committed Jun 4, 2019
1 parent 39b1e55 commit ffb0f27
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 16 deletions.
26 changes: 26 additions & 0 deletions Tests/test_file_libtiff.py
Expand Up @@ -418,18 +418,44 @@ def test_blur(self):
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')
Expand Down
6 changes: 6 additions & 0 deletions docs/handbook/image-file-formats.rst
Expand Up @@ -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.

Expand Down
18 changes: 17 additions & 1 deletion src/PIL/TiffImagePlugin.py
Expand Up @@ -114,6 +114,7 @@
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
Expand Down Expand Up @@ -1496,6 +1497,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()))
Expand Down Expand Up @@ -1560,7 +1571,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:
Expand Down
6 changes: 5 additions & 1 deletion src/PIL/TiffTags.py
Expand Up @@ -430,6 +430,9 @@ def _populate():
# 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
Expand All @@ -438,7 +441,8 @@ def _populate():
296, 297, 321, 320, 338, 32995, 322, 323, 32998,
32996, 339, 32997, 330, 531, 530, 301, 532, 333,
# 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
Expand Down
32 changes: 18 additions & 14 deletions src/encode.c
Expand Up @@ -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<d_size;pos++){
TRACE((" key: %d\n", (int)PyInt_AsLong(PyList_GetItem(keys,pos))));
tags_size = PyList_Size(tags);
TRACE(("tags size: %d\n", (int)tags_size));
for (pos=0;pos<tags_size;pos++){
item = PyList_GetItem(tags, pos);
if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) {
PyErr_SetString(PyExc_ValueError, "Invalid tags list");
return NULL;
}
}
pos = 0;
}
Expand All @@ -688,10 +690,12 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args)
}

num_core_tags = sizeof(core_tags) / sizeof(int);
for (pos = 0; pos < d_size; pos++) {
key = PyList_GetItem(keys, pos);
for (pos = 0; pos < tags_size; 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);
value = PyList_GetItem(values, pos);
value = PyTuple_GetItem(item, 1);
status = 0;
is_core_tag = 0;
is_var_length = 0;
Expand Down

0 comments on commit ffb0f27

Please sign in to comment.