Repository: python-pillow/Pillow

Issue:
Failed to write customized tag into Tiff file
I am using python version 2.7

I am trying to write some custom tag(37000 in this case) into new tiff file. It is able to read tag from image but when I am trying to write them it is not successfully written.

When I am use pillow 4.1.0 to do that, tag is not added, and then when I tried old version pillow 2.3, tag is added successfully but cannot save image correctly (only blank)

Could you give me some help, I appreciate that!

Here is the testing code:
```python
from PIL import Image, TiffImagePlugin, TiffTags

def test_custom_metadata():

    img = Image.open('mytifffile.tif')
    img.tag[37000]='my special tag'
    info = TiffImagePlugin.ImageFileDirectory()
    CustomTagId = 37000

    info[CustomTagId] = 'my special tag'
    print(img.tag)
    Image.DEBUG=True
    TiffImagePlugin.WRITE_LIBTIFF = False # Set to True to see it break.
    img.save('temp2.tif',tiffinfo= img.tag)

    img2 = Image.open('temp2.tif')
    print(img2.tag)
test_custom_metadata()
```

outputs are:
`{256: (1703,), 257: (937,), 258: (8,), 259: (5,), 262: (1,), 296: (1,), 273: (8,), 274: (1,), 339: (1,), 277: (1,), 278: (937,), 279: (1849679,), 37000: ('my special tag',), 284: (1,)}
`
`{256: (1703,), 257: (937,), 258: (8,), 259: (5,), 262: (1,), 296: (1,), 273: (8,), 274: (1,), 339: (1,), 277: (1,), 278: (937,), 279: (1849679,), 284: (1,)}`

As you can see, `37000: ('my special tag',)` is missing.

I doing this because, my original tiff file have some customized tag(51023), and it is 16 bit. So what I want to do is to rescale it to 8 bit which is able to do and keep that tag(51023) and value as it is.



Discussion:
Unfortunately, Tiff IFD tags are a bit of a mess when writing. 

The native python tiff writer will write arbitrary tags, but it's limited to raw, uncompressed pixel data. The libtiff writer is limited to only the standard builtin tags, and at that, only a subset. We do not currently implement the api methods required to do arbitrary metadata through libtiff. 


Resolving patch:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -557,26 +557,26 @@ def _setitem(self, tag, value, legacy_api):
             else:
                 self.tagtype[tag] = 7
                 if all(isinstance(v, IFDRational) for v in values):
-                    self.tagtype[tag] = 5
+                    self.tagtype[tag] = TiffTags.RATIONAL
                 elif all(isinstance(v, int) for v in values):
                     if all(v < 2 ** 16 for v in values):
-                        self.tagtype[tag] = 3
+                        self.tagtype[tag] = TiffTags.SHORT
                     else:
-                        self.tagtype[tag] = 4
+                        self.tagtype[tag] = TiffTags.LONG
                 elif all(isinstance(v, float) for v in values):
-                    self.tagtype[tag] = 12
+                    self.tagtype[tag] = TiffTags.DOUBLE
                 else:
                     if py3:
                         if all(isinstance(v, str) for v in values):
-                            self.tagtype[tag] = 2
+                            self.tagtype[tag] = TiffTags.ASCII
                     else:
                         # Never treat data as binary by default on Python 2.
-                        self.tagtype[tag] = 2
+                        self.tagtype[tag] = TiffTags.ASCII
 
-        if self.tagtype[tag] == 7 and py3:
+        if self.tagtype[tag] == TiffTags.UNDEFINED and py3:
             values = [value.encode("ascii", 'replace') if isinstance(
                       value, str) else value]
-        elif self.tagtype[tag] == 5:
+        elif self.tagtype[tag] == TiffTags.RATIONAL:
             values = [float(v) if isinstance(v, int) else v
                       for v in values]
 
@@ -592,7 +592,10 @@ def _setitem(self, tag, value, legacy_api):
         if (info.length == 1) or \
            (info.length is None and len(values) == 1 and not legacy_api):
             # Don't mess with the legacy api, since it's frozen.
-            if legacy_api and self.tagtype[tag] in [5, 10]:  # rationals
+            if legacy_api and self.tagtype[tag] in [
+                TiffTags.RATIONAL,
+                TiffTags.SIGNED_RATIONAL
+            ]:  # rationals
                 values = values,
             try:
                 dest[tag], = values
@@ -649,13 +652,13 @@ def _register_basic(idx_fmt_name):
             b"".join(self._pack(fmt, value) for value in values))
 
     list(map(_register_basic,
-             [(3, "H", "short"),
-              (4, "L", "long"),
-              (6, "b", "signed byte"),
-              (8, "h", "signed short"),
-              (9, "l", "signed long"),
-              (11, "f", "float"),
-              (12, "d", "double")]))
+             [(TiffTags.SHORT, "H", "short"),
+              (TiffTags.LONG, "L", "long"),
+              (TiffTags.SIGNED_BYTE, "b", "signed byte"),
+              (TiffTags.SIGNED_SHORT, "h", "signed short"),
+              (TiffTags.SIGNED_LONG, "l", "signed long"),
+              (TiffTags.FLOAT, "f", "float"),
+              (TiffTags.DOUBLE, "d", "double")]))
 
     @_register_loader(1, 1)  # Basic type, except for the legacy API.
     def load_byte(self, data, legacy_api=True):
@@ -811,7 +814,10 @@ def save(self, fp):
                     print("- value:", values)
 
             # count is sum of lengths for string and arbitrary data
-            count = len(data) if typ in [2, 7] else len(values)
+            if typ in [TiffTags.ASCII, TiffTags.UNDEFINED]:
+                count = len(data)
+            else:
+                count = len(values)
             # figure out if data fits into the entry
             if len(data) <= 4:
                 entries.append((tag, typ, count, data.ljust(4, b"\0"), b""))
@@ -1513,12 +1519,16 @@ def _save(im, fp, filename):
                                           getattr(im, 'tag_v2', {}).items(),
                                           legacy_ifd.items()):
             # Libtiff can only process certain core items without adding
-            # them to the custom dictionary. Support has only been been added
-            # for int and float values
+            # them to the custom dictionary.
+            # Support for custom items has only been been added
+            # for int, float, unicode, string and byte values
             if tag not in TiffTags.LIBTIFF_CORE:
+                if TiffTags.lookup(tag).type == TiffTags.UNDEFINED:
+                    continue
                 if (distutils.version.StrictVersion(_libtiff_version()) <
                     distutils.version.StrictVersion("4.0")) \
-                   or not (isinstance(value, int) or isinstance(value, float)):
+                   or not (isinstance(value, (int, float, str, bytes)) or
+                           (not py3 and isinstance(value, unicode))):  # noqa: F821
                     continue
             if tag not in atts and tag not in blocklist:
                 if isinstance(value, str if py3 else unicode):  # noqa: F821
@@ -1799,7 +1809,7 @@ def fixOffsets(self, count, isShort=False, isLong=False):
                 # local (not referenced with another offset)
                 self.rewriteLastShortToLong(offset)
                 self.f.seek(-10, os.SEEK_CUR)
-                self.writeShort(4)  # rewrite the type to LONG
+                self.writeShort(TiffTags.LONG)  # rewrite the type to LONG
                 self.f.seek(8, os.SEEK_CUR)
             elif isShort:
                 self.rewriteLastShort(offset)
diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py
--- a/src/PIL/TiffTags.py
+++ b/src/PIL/TiffTags.py
@@ -64,8 +64,12 @@ def lookup(tag):
 SHORT = 3
 LONG = 4
 RATIONAL = 5
+SIGNED_BYTE = 6
 UNDEFINED = 7
+SIGNED_SHORT = 8
+SIGNED_LONG = 9
 SIGNED_RATIONAL = 10
+FLOAT = 11
 DOUBLE = 12
 
 TAGS_V2 = {


Test patch:
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
@@ -235,7 +235,10 @@ def test_additional_metadata(self):
     def test_custom_metadata(self):
         custom = {
             37000: 4,
-            37001: 4.2
+            37001: 4.2,
+            37002: 'custom tag value',
+            37003: u'custom tag value',
+            37004: b'custom tag value'
         }
 
         libtiff_version = TiffImagePlugin._libtiff_version()
@@ -256,6 +259,8 @@ def test_custom_metadata(self):
 
             reloaded = Image.open(out)
             for tag, value in custom.items():
+                if libtiff and isinstance(value, bytes):
+                    value = value.decode()
                 self.assertEqual(reloaded.tag_v2[tag], value)
 
     def test_int_dpi(self):