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

Tiff: Add support for JPEG quality #3886

Merged
merged 5 commits into from Jul 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 26 additions & 0 deletions Tests/test_file_libtiff.py
Expand Up @@ -435,18 +435,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)

self.assertGreater(size_raw, size_compressed)
self.assertGreater(size_compressed, size_jpeg)
self.assertGreater(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 @@ -116,6 +116,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 @@ -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()))
Expand Down Expand Up @@ -1607,7 +1618,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
4 changes: 4 additions & 0 deletions src/PIL/TiffTags.py
Expand Up @@ -432,6 +432,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 Down Expand Up @@ -476,6 +479,7 @@ def _populate():
333,
# as above
269, # this has been in our tests forever, and works
65537,
}

LIBTIFF_CORE.remove(320) # Array of short, crashes
Expand Down
33 changes: 18 additions & 15 deletions src/encode.c
Expand Up @@ -643,27 +643,28 @@ 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, "sssnsOO", &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 +689,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