Repository: python-pillow/Pillow

Issue:
Save ImageDescription tag in tif files
I have tif images saved by ImageJ with some usefull information into the ImageDescritption tag. But if I open this file with Pillow and save it, this information are lost (with other tags, see another issue).

Is there a reason for not saving this tag? It seems that only a few tags are saved in the TiffImagePlugin.



Discussion:
If you don't get an answer here you might try stackoverflow: http://stackoverflow.com/questions/tagged/python-imaging-library


Resolving patch:
diff --git a/PIL/IptcImagePlugin.py b/PIL/IptcImagePlugin.py
--- a/PIL/IptcImagePlugin.py
+++ b/PIL/IptcImagePlugin.py
@@ -262,7 +262,7 @@ def getiptcinfo(im):
         # get raw data from the IPTC/NAA tag (PhotoShop tags the data
         # as 4-byte integers, so we cannot use the get method...)
         try:
-            type, data = im.tag.tagdata[TiffImagePlugin.IPTC_NAA_CHUNK]
+            data = im.tag.tagdata[TiffImagePlugin.IPTC_NAA_CHUNK]
         except (AttributeError, KeyError):
             pass
 
diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py
--- a/PIL/TiffImagePlugin.py
+++ b/PIL/TiffImagePlugin.py
@@ -54,6 +54,8 @@
 import itertools
 import os
 
+
+
 II = b"II" # little-endian (intel-style)
 MM = b"MM" # big-endian (motorola-style)
 
@@ -215,11 +217,45 @@ def _accept(prefix):
 # Wrapper for TIFF IFDs.
 
 class ImageFileDirectory(collections.MutableMapping):
-
-    # represents a TIFF tag directory.  to speed things up,
-    # we don't decode tags unless they're asked for.
-
-    def __init__(self, prefix):
+    """ This class represents a TIFF tag directory.  To speed things
+        up, we don't decode tags unless they're asked for.
+
+        Exposes a dictionary interface of the tags in the directory
+        ImageFileDirectory[key] = value
+        value = ImageFileDirectory[key]
+
+        Also contains a dictionary of tag types as read from the tiff
+        image file, 'ImageFileDirectory.tagtype'
+
+
+        Data Structures:
+        'public'
+        * self.tagtype = {} Key: numerical tiff tag number
+                            Value: integer corresponding to the data type from
+                            `TiffTags.TYPES`
+
+        'internal'            
+        * self.tags = {}  Key: numerical tiff tag number
+                          Value: Decoded data, Generally a tuple.
+                            * If set from __setval__ -- always a tuple
+                            * Numeric types -- always a tuple
+                            * String type -- not a tuple, returned as string
+                            * Undefined data -- not a tuple, returned as bytes
+                            * Byte -- not a tuple, returned as byte.
+        * self.tagdata = {} Key: numerical tiff tag number
+                            Value: undecoded byte string from file
+
+
+        Tags will be found in either self.tags or self.tagdata, but
+        not both. The union of the two should contain all the tags
+        from the Tiff image file.  External classes shouldn't
+        reference these unless they're really sure what they're doing.
+        """
+
+    def __init__(self, prefix=II):
+        """
+        :prefix: 'II'|'MM'  tiff endianness
+        """
         self.prefix = prefix[:2]
         if self.prefix == MM:
             self.i16, self.i32 = ib16, ib32
@@ -265,7 +301,8 @@ def __getitem__(self, tag):
         try:
             return self.tags[tag]
         except KeyError:
-            type, data = self.tagdata[tag] # unpack on the fly
+            data = self.tagdata[tag] # unpack on the fly
+            type = self.tagtype[tag]
             size, handler = self.load_dispatch[type]
             self.tags[tag] = data = handler(self, data)
             del self.tagdata[tag]
@@ -294,6 +331,9 @@ def has_key(self, tag):
             return tag in self
 
     def __setitem__(self, tag, value):
+        # tags are tuples for integers
+        # tags are not tuples for byte, string, and undefined data.
+        # see load_*
         if not isinstance(value, tuple):
             value = (value,)
         self.tags[tag] = value
@@ -408,7 +448,7 @@ def load(self, fp):
                 warnings.warn("Possibly corrupt EXIF data.  Expecting to read %d bytes but only got %d. Skipping tag %s" % (size, len(data), tag))
                 continue
 
-            self.tagdata[tag] = typ, data
+            self.tagdata[tag] = data
             self.tagtype[tag] = typ
 
             if Image.DEBUG:
@@ -445,25 +485,42 @@ def save(self, fp):
 
             if tag in self.tagtype:
                 typ = self.tagtype[tag]
-
+                
+            if Image.DEBUG:
+                print ("Tag %s, Type: %s, Value: %s" % (tag, typ, value))
+                   
             if typ == 1:
                 # byte data
-                data = value
+                if isinstance(value, tuple):
+                    data = value = value[-1]
+                else:
+                    data = value
             elif typ == 7:
                 # untyped data
                 data = value = b"".join(value)
-            elif isinstance(value[0], str):
+            elif isStringType(value[0]):
                 # string data
+                if isinstance(value, tuple):
+                    value = value[-1]
                 typ = 2
-                data = value = b"\0".join(value.encode('ascii', 'replace')) + b"\0"
+                # was b'\0'.join(str), which led to \x00a\x00b sorts
+                # of strings which I don't see in in the wild tiffs
+                # and doesn't match the tiff spec: 8-bit byte that
+                # contains a 7-bit ASCII code; the last byte must be
+                # NUL (binary zero). Also, I don't think this was well
+                # excersized before. 
+                data = value = b"" + value.encode('ascii', 'replace') + b"\0"
             else:
                 # integer data
                 if tag == STRIPOFFSETS:
                     stripoffsets = len(directory)
                     typ = 4 # to avoid catch-22
-                elif tag in (X_RESOLUTION, Y_RESOLUTION):
+                elif tag in (X_RESOLUTION, Y_RESOLUTION) or typ==5:
                     # identify rational data fields
                     typ = 5
+                    if isinstance(value[0], tuple):
+                        # long name for flatten
+                        value = tuple(itertools.chain.from_iterable(value))
                 elif not typ:
                     typ = 3
                     for v in value:
@@ -495,6 +552,7 @@ def save(self, fp):
                 count = len(value)
                 if typ == 5:
                     count = count // 2        # adjust for rational data field
+
                 append((tag, typ, count, o32(offset), data))
                 offset = offset + len(data)
                 if offset & 1:
@@ -932,23 +990,34 @@ def _save(im, fp, filename):
     ifd[IMAGEWIDTH] = im.size[0]
     ifd[IMAGELENGTH] = im.size[1]
 
+    # write any arbitrary tags passed in as an ImageFileDirectory
+    info = im.encoderinfo.get("tiffinfo",{})
+    if Image.DEBUG:
+        print ("Tiffinfo Keys: %s"% info.keys)
+    keys = list(info.keys())
+    for key in keys:
+        ifd[key] = info.get(key)
+        try:
+            ifd.tagtype[key] = info.tagtype[key]
+        except:
+            pass # might not be an IFD, Might not have populated type
+
+
     # additions written by Greg Couch, gregc@cgl.ucsf.edu
     # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com
     if hasattr(im, 'tag'):
         # preserve tags from original TIFF image file
-        for key in (RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION):
-            if key in im.tag.tagdata:
-                ifd[key] = im.tag.tagdata.get(key)
-        # preserve some more tags from original TIFF image file
-        # -- 2008-06-06 Florian Hoech
-        ifd.tagtype = im.tag.tagtype
-        for key in (IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP):
+        for key in (RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION,
+                    IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP):
             if key in im.tag:
                 ifd[key] = im.tag[key]
+            ifd.tagtype[key] = im.tag.tagtype.get(key, None)
+
         # preserve ICC profile (should also work when saving other formats
         # which support profiles as TIFF) -- 2008-06-06 Florian Hoech
         if "icc_profile" in im.info:
             ifd[ICCPROFILE] = im.info["icc_profile"]
+            
     if "description" in im.encoderinfo:
         ifd[IMAGEDESCRIPTION] = im.encoderinfo["description"]
     if "resolution" in im.encoderinfo:


Test patch:
diff --git a/Tests/images/lena.tif b/Tests/images/lena.tif
new file mode 100644
Binary files /dev/null and b/Tests/images/lena.tif differ
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -107,6 +107,29 @@ def test_adobe_deflate_tiff():
     assert_equal(im.tile[0][:3], ('tiff_adobe_deflate', (0, 0, 278, 374), 0))
     assert_no_exception(lambda: im.load())
 
+def test_write_metadata():
+    """ Test metadata writing through libtiff """
+    img = Image.open('Tests/images/lena_g4.tif')
+    f = tempfile('temp.tiff')
+
+    img.save(f, tiffinfo = img.tag)
+
+    loaded = Image.open(f)
+
+    original = img.tag.named()
+    reloaded = loaded.tag.named()
+
+    # PhotometricInterpretation is set from SAVE_INFO, not the original image. 
+    ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber', 'PhotometricInterpretation']
+
+    for tag, value in reloaded.items():
+        if tag not in ignored:
+            assert_equal(original[tag], value, "%s didn't roundtrip" % tag)
+
+    for tag, value in original.items():
+        if tag not in ignored: 
+            assert_equal(value, reloaded[tag], "%s didn't roundtrip" % tag)
+
 
 def test_g3_compression():
     i = Image.open('Tests/images/lena_g4_500.tif')
@@ -116,7 +139,7 @@ def test_g3_compression():
     reread = Image.open(out)
     assert_equal(reread.info['compression'], 'group3')
     assert_image_equal(reread, i)
-    
+
 def test_little_endian():
     im = Image.open('Tests/images/12bit.deflate.tif')
     assert_equal(im.getpixel((0,0)), 480)
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
new file mode 100644
--- /dev/null
+++ b/Tests/test_file_tiff_metadata.py
@@ -0,0 +1,80 @@
+from tester import *
+from PIL import Image, TiffImagePlugin, TiffTags
+
+tag_ids = dict(zip(TiffTags.TAGS.values(), TiffTags.TAGS.keys()))
+
+def test_rt_metadata():
+    """ Test writing arbitray metadata into the tiff image directory
+        Use case is ImageJ private tags, one numeric, one arbitrary
+        data.  https://github.com/python-imaging/Pillow/issues/291
+        """
+    
+    img = lena()
+
+    textdata = "This is some arbitrary metadata for a text field"
+    info = TiffImagePlugin.ImageFileDirectory()
+
+    info[tag_ids['ImageJMetaDataByteCounts']] = len(textdata)
+    info[tag_ids['ImageJMetaData']] = textdata
+
+    f = tempfile("temp.tif")
+
+    img.save(f, tiffinfo=info)
+    
+    loaded = Image.open(f)
+
+    assert_equal(loaded.tag[50838], (len(textdata),))
+    assert_equal(loaded.tag[50839], textdata)
+    
+def test_read_metadata():
+    img = Image.open('Tests/images/lena_g4.tif')
+    
+    known = {'YResolution': ((1207959552, 16777216),),
+             'PlanarConfiguration': (1,),
+             'BitsPerSample': (1,),
+             'ImageLength': (128,),
+             'Compression': (4,),
+             'FillOrder': (1,),
+             'DocumentName': 'lena.g4.tif',
+             'RowsPerStrip': (128,),
+             'ResolutionUnit': (1,),
+             'PhotometricInterpretation': (0,),
+             'PageNumber': (0, 1),
+             'XResolution': ((1207959552, 16777216),),
+             'ImageWidth': (128,),
+             'Orientation': (1,),
+             'StripByteCounts': (1796,),
+             'SamplesPerPixel': (1,),
+             'StripOffsets': (8,),
+             'Software': 'ImageMagick 6.5.7-8 2012-08-17 Q16 http://www.imagemagick.org'}
+
+    # assert_equal is equivalent, but less helpful in telling what's wrong. 
+    named = img.tag.named()
+    for tag, value in named.items():
+        assert_equal(known[tag], value)
+
+    for tag, value in known.items():
+        assert_equal(value, named[tag])
+
+
+def test_write_metadata():
+    """ Test metadata writing through the python code """
+    img = Image.open('Tests/images/lena.tif')
+
+    f = tempfile('temp.tiff')
+    img.save(f, tiffinfo = img.tag)
+
+    loaded = Image.open(f)
+
+    original = img.tag.named()
+    reloaded = loaded.tag.named()
+
+    ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber', 'StripOffsets']
+    
+    for tag, value in reloaded.items():
+        if tag not in ignored:
+            assert_equal(original[tag], value, "%s didn't roundtrip" % tag)
+
+    for tag, value in original.items():
+        if tag not in ignored: 
+            assert_equal(value, reloaded[tag], "%s didn't roundtrip" % tag)