diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index b502c5661..096be6f56 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -1400,8 +1400,7 @@ def _save(im, fp, filename): # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library # based on the data in the strip. - # ICCPROFILE crashes. - blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS, ICCPROFILE] + blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS] atts = {} # bits per sample is a single short in the tiff directory, not a list. atts[BITSPERSAMPLE] = bits[0] @@ -1411,16 +1410,22 @@ def _save(im, fp, filename): legacy_ifd = {} if hasattr(im, 'tag'): legacy_ifd = im.tag.to_v2() - for k, v in itertools.chain(ifd.items(), + for tag, value in itertools.chain(ifd.items(), getattr(im, 'tag_v2', {}).items(), legacy_ifd.items()): - if k not in atts and k not in blocklist: - if isinstance(v, unicode if bytes is str else str): - atts[k] = v.encode('ascii', 'replace') + b"\0" - elif isinstance(v, IFDRational): - atts[k] = float(v) + # Libtiff can only process certain core items without adding + # them to the custom dictionary. It will segfault if it attempts + # to add a custom tag without the dictionary entry + # + # UNDONE -- add code for the custom dictionary + if tag not in TiffTags.LIBTIFF_CORE: continue + if tag not in atts and tag not in blocklist: + if isinstance(value, unicode if bytes is str else str): + atts[tag] = value.encode('ascii', 'replace') + b"\0" + elif isinstance(value, IFDRational): + atts[tag] = float(value) else: - atts[k] = v + atts[tag] = value if DEBUG: print("Converted items: %s" % sorted(atts.items())) diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index 432ec82da..07d594e1a 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -331,3 +331,72 @@ TYPES = {} # 11: "float", # 12: "double", # } + +# +# These tags are handled by default in libtiff, without +# adding to the custom dictionary. From tif_dir.c, searching for +# case TIFFTAG in the _TIFFVSetField function: +# Line: item. +# 148: case TIFFTAG_SUBFILETYPE: +# 151: case TIFFTAG_IMAGEWIDTH: +# 154: case TIFFTAG_IMAGELENGTH: +# 157: case TIFFTAG_BITSPERSAMPLE: +# 181: case TIFFTAG_COMPRESSION: +# 202: case TIFFTAG_PHOTOMETRIC: +# 205: case TIFFTAG_THRESHHOLDING: +# 208: case TIFFTAG_FILLORDER: +# 214: case TIFFTAG_ORIENTATION: +# 221: case TIFFTAG_SAMPLESPERPIXEL: +# 228: case TIFFTAG_ROWSPERSTRIP: +# 238: case TIFFTAG_MINSAMPLEVALUE: +# 241: case TIFFTAG_MAXSAMPLEVALUE: +# 244: case TIFFTAG_SMINSAMPLEVALUE: +# 247: case TIFFTAG_SMAXSAMPLEVALUE: +# 250: case TIFFTAG_XRESOLUTION: +# 256: case TIFFTAG_YRESOLUTION: +# 262: case TIFFTAG_PLANARCONFIG: +# 268: case TIFFTAG_XPOSITION: +# 271: case TIFFTAG_YPOSITION: +# 274: case TIFFTAG_RESOLUTIONUNIT: +# 280: case TIFFTAG_PAGENUMBER: +# 284: case TIFFTAG_HALFTONEHINTS: +# 288: case TIFFTAG_COLORMAP: +# 294: case TIFFTAG_EXTRASAMPLES: +# 298: case TIFFTAG_MATTEING: +# 305: case TIFFTAG_TILEWIDTH: +# 316: case TIFFTAG_TILELENGTH: +# 327: case TIFFTAG_TILEDEPTH: +# 333: case TIFFTAG_DATATYPE: +# 344: case TIFFTAG_SAMPLEFORMAT: +# 361: case TIFFTAG_IMAGEDEPTH: +# 364: case TIFFTAG_SUBIFD: +# 376: case TIFFTAG_YCBCRPOSITIONING: +# 379: case TIFFTAG_YCBCRSUBSAMPLING: +# 383: case TIFFTAG_TRANSFERFUNCTION: +# 389: case TIFFTAG_REFERENCEBLACKWHITE: +# 393: case TIFFTAG_INKNAMES: + +# some of these are not in our TAGS_V2 dict and were included from tiff.h + +LIBTIFF_CORE = set ([255, 256, 257, 258, 259, 262, 263, 266, 274, 277, + 278, 280, 281, 340, 341, 282, 283, 284, 286, 287, + 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 + ]) + +LIBTIFF_CORE.remove(320) # Array of short, crashes +LIBTIFF_CORE.remove(301) # Array of short, crashes +LIBTIFF_CORE.remove(532) # Array of long, crashes + +LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes +LIBTIFF_CORE.remove(322) # We don't have support for tiled images in libtiff +LIBTIFF_CORE.remove(323) # Tiled images +LIBTIFF_CORE.remove(333) # Ink Names either + +# Note to advanced users: There may be combinations of these +# parameters and values that when added properly, will work and +# produce valid tiff images that may work in your application. +# It is safe to add and remove tags from this set from Pillow's point +# of view so long as you test against libtiff. diff --git a/Tests/images/rdf.tif b/Tests/images/rdf.tif new file mode 100644 index 000000000..524a9dbc8 Binary files /dev/null and b/Tests/images/rdf.tif differ diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index d18314271..313fb545f 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -7,7 +7,7 @@ import logging import itertools import os -from PIL import Image, TiffImagePlugin +from PIL import Image, TiffImagePlugin, TiffTags logger = logging.getLogger(__name__) @@ -172,6 +172,59 @@ class TestFileLibTiff(LibTiffTestCase): for field in requested_fields: self.assertTrue(field in reloaded, "%s not in metadata" % field) + def test_additional_metadata(self): + # these should not crash. Seriously dummy data, most of it doesn't make + # any sense, so we're running up against limits where we're asking + # libtiff to do stupid things. + + # Get the list of the ones that we should be able to write + + core_items = dict((tag, info) for tag, info in [(s,TiffTags.lookup(s)) for s + in TiffTags.LIBTIFF_CORE] + if info.type is not None) + + # Exclude ones that have special meaning that we're already testing them + im = Image.open('Tests/images/hopper_g4.tif') + for tag in im.tag_v2.keys(): + try: + del(core_items[tag]) + except: pass + + # Type codes: + # 2: "ascii", + # 3: "short", + # 4: "long", + # 5: "rational", + # 12: "double", + # type: dummy value + values = { 2: 'test', + 3: 1, + 4: 2**20, + 5: TiffImagePlugin.IFDRational(100,1), + 12: 1.05 } + + + new_ifd = TiffImagePlugin.ImageFileDirectory_v2() + for tag, info in core_items.items(): + if info.length == 1: + new_ifd[tag] = values[info.type] + if info.length == 0: + new_ifd[tag] = tuple(values[info.type] for _ in range(3)) + else: + new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) + + # Extra samples really doesn't make sense in this application. + del(new_ifd[338]) + + out = self.tempfile("temp.tif") + TiffImagePlugin.WRITE_LIBTIFF = True + + im.save(out, tiffinfo=new_ifd) + + TiffImagePlugin.WRITE_LIBTIFF = False + + + def test_g3_compression(self): i = Image.open('Tests/images/hopper_g4_500.tif') out = self.tempfile("temp.tif") @@ -395,6 +448,17 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False + def test_crashing_metadata(self): + # issue 1597 + im = Image.open('Tests/images/rdf.tif') + out = self.tempfile('temp.tif') + + TiffImagePlugin.WRITE_LIBTIFF = True + # this shouldn't crash + im.save(out, format='TIFF') + TiffImagePlugin.WRITE_LIBTIFF = False + + if __name__ == '__main__': unittest.main() diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 3f62c30ea..2b728deef 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -498,7 +498,7 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following The :py:attr:`~PIL.Image.Image.tag_v2` attribute contains a dictionary of TIFF metadata. The keys are numerical indexes from -`~PIL.TiffTags.TAGS_V2`. Values are strings or numbers for single +:py:attr:`~PIL.TiffTags.TAGS_V2`. Values are strings or numbers for single items, multiple values are returned in a tuple of values. Rational numbers are returned as a :py:class:`~PIL.TiffImagePlugin.IFDRational` object. @@ -542,13 +542,19 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 3.0.0 -**compression** + .. note:: + + Only some tags are currently supported when writing using + libtiff. The supported list is found in + :py:attr:`~PIL:TiffTags.LIBTIFF_CORE`. + +**compression** A string containing the desired compression method for the - file. (valid only with libtiff installed) Valid compression - methods are: ``None``, ``"tiff_ccitt"``, ``"group3"``, - ``"group4"``, ``"tiff_jpeg"``, ``"tiff_adobe_deflate"``, - ``"tiff_thunderscan"``, ``"tiff_deflate"``, ``"tiff_sgilog"``, - ``"tiff_sgilog24"``, ``"tiff_raw_16"`` + file. (valid only with libtiff installed) Valid compression + methods are: ``None``, ``"tiff_ccitt"``, ``"group3"``, + ``"group4"``, ``"tiff_jpeg"``, ``"tiff_adobe_deflate"``, + ``"tiff_thunderscan"``, ``"tiff_deflate"``, ``"tiff_sgilog"``, + ``"tiff_sgilog24"``, ``"tiff_raw_16"`` These arguments to set the tiff header fields are an alternative to using the general tags available through tiffinfo.