Repository: python-pillow/Pillow

Issue:
Save custom TIFF tags in LZW-compressed TIFFs
If I understand correctly, the recent changes (#369) have not made it possible to write custom TIFF tags. I would like to do the following:

```python
info = TiffImagePlugin.ImageFileDirectory()
CustomTagId = 37000
info[CustomTagId] = 6
info.tagtype[CustomTagId] = 3 # 'short' according to TiffTags.TYPES

img.save(pathname, tiffinfo = info)
```

This corresponds to "Defining Application Tags" in http://www.remotesensing.org/libtiff/addingtags.html.



Discussion:
Sorry I missed this earlier. 

I think this works. The following code writes your custom tag, and will read it back in with the correct value.

```python
def test_custom_metadata():
    img = lena()
    f = tempfile('temp.tiff')

    info = TiffImagePlugin.ImageFileDirectory()
    CustomTagId = 37000

    info[CustomTagId] = 6
    info.tagtype[CustomTagId] = 3 # 'short' according to TiffTags.TYPES

    Image.DEBUG=True
    img.save(f, tiffinfo = info)


    reloaded = Image.open(f)

    assert_equal(6, reloaded.tag.getscalar(CustomTagId))
```

```
Tiffinfo Keys: <bound method ImageFileDirectory.keys of <PIL.TiffImagePlugin.ImageFileDirectory object at 0x7fd62ffb98d0>>
Tag 256, Type: None, Value: (128,)
save: ImageWidth (256) - type: short (3) - value: (128,)
Tag 257, Type: None, Value: (128,)
save: ImageLength (257) - type: short (3) - value: (128,)
Tag 258, Type: None, Value: (8, 8, 8)
save: BitsPerSample (258) - type: short (3) - value: (8, 8, 8)
Tag 259, Type: None, Value: (1,)
save: Compression (259) - type: short (3) - value: (1,)
Tag 262, Type: None, Value: (2,)
save: PhotometricInterpretation (262) - type: short (3) - value: (2,)
Tag 273, Type: None, Value: (0,)
save: StripOffsets (273) - type: long (4) - value: (0,)
Tag 277, Type: None, Value: (3,)
save: SamplesPerPixel (277) - type: short (3) - value: (3,)
Tag 278, Type: None, Value: (128,)
save: RowsPerStrip (278) - type: short (3) - value: (128,)
Tag 279, Type: None, Value: (49152,)
save: StripByteCounts (279) - type: short (3) - value: (49152,)
Tag 284, Type: None, Value: (1,)
save: PlanarConfiguration (284) - type: short (3) - value: (1,)
Tag 37000, Type: 3, Value: (6,)
save: unknown (37000) - type: short (3) - value: (6,)
*** TiffImageFile._open ***
- __first: 8
- ifh:  b'II*\x00\x08\x00\x00\x00'
tag: ImageWidth (256) - type: short (3) - value: (128,)
tag: ImageLength (257) - type: short (3) - value: (128,)
tag: BitsPerSample (258) - type: short (3) - value: (8, 8, 8)
tag: Compression (259) - type: short (3) - value: (1,)
tag: PhotometricInterpretation (262) - type: short (3) - value: (2,)
tag: StripOffsets (273) - type: long (4) - value: (152,)
tag: SamplesPerPixel (277) - type: short (3) - value: (3,)
tag: RowsPerStrip (278) - type: short (3) - value: (128,)
tag: StripByteCounts (279) - type: short (3) - value: (49152,)
tag: PlanarConfiguration (284) - type: short (3) - value: (1,)
tag: unknown (37000) - type: short (3) - value: (6,)
*** Summary ***
- compression: raw
- photometric_interpretation: 2
- planar_configuration: 1
- fill_order: 1
- size: (128, 128)
format key: (b'II', 2, 1, 1, (8, 8, 8), ())
- raw mode: RGB
- pil mode: RGB
tiles:  [('raw', (0, 0, 128, 128), 152, ('RGB', 0, 1))]
ok
```

Unfortunately, I observe that this only works with `TiffImagePlugin.WRITE_LIBTIFF = False`, so I cannot use it for LZW-compressed TIFFs, see #659.

(I have tested the standalone version:

```python
# Tested with WinPython-32bit-3.3.5.0

from PIL import Image, TiffImagePlugin
from nose.tools import assert_equal

def test_custom_metadata():
    img = Image.new('L', (300, 300))
    img.putpixel((150,150), 255)

    info = TiffImagePlugin.ImageFileDirectory()
    CustomTagId = 37000

    info[CustomTagId] = 6
    info.tagtype[CustomTagId] = 3 # 'short' according to TiffTags.TYPES

    Image.DEBUG=True
    TiffImagePlugin.WRITE_LIBTIFF = False # Set to True to see it break.
    img.save('temp.tiff', tiffinfo = info)


    reloaded = Image.open('temp.tiff')

    assert_equal(6, reloaded.tag.getscalar(CustomTagId))


test_custom_metadata()
```

)

I don't remember anything special for custom tags when I did the work on libtiff, so I'm pretty sure that they're not in there yet. 

The custom tag is skipped here - https://github.com/python-pillow/Pillow/blob/f115546b56a54b6458f189/src/PIL/TiffImagePlugin.py#L1512

So this looks like it is part of the 'UNDONE --  add code for the custom dictionary'

Resolving patch:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -54,6 +54,7 @@
 import struct
 import sys
 import warnings
+import distutils.version
 
 from .TiffTags import TYPES
 
@@ -284,6 +285,10 @@ def _limit_rational(val, max_val):
     return n_d[::-1] if inv else n_d
 
 
+def _libtiff_version():
+    return Image.core.libtiff_version.split("\n")[0].split("Version ")[1]
+
+
 ##
 # Wrapper for TIFF IFDs.
 
@@ -1499,12 +1504,13 @@ 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. It will segfault if it attempts
-            # to add a custom tag without the dictionary entry
-            #
-            # UNDONE --  add code for the custom dictionary
+            # them to the custom dictionary. Support has only been been added
+            # for int and float values
             if tag not in TiffTags.LIBTIFF_CORE:
-                continue
+                if (distutils.version.StrictVersion(_libtiff_version()) <
+                    distutils.version.StrictVersion("4.0")) \
+                   or not (isinstance(value, int) or isinstance(value, float)):
+                    continue
             if tag not in atts and tag not in blocklist:
                 if isinstance(value, str if py3 else unicode):
                     atts[tag] = value.encode('ascii', 'replace') + b"\0"
diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py
--- a/src/PIL/TiffTags.py
+++ b/src/PIL/TiffTags.py
@@ -425,6 +425,7 @@ def _populate():
 
 # some of these are not in our TAGS_V2 dict and were included from tiff.h
 
+# This list also exists in encode.c
 LIBTIFF_CORE = {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,


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
@@ -8,6 +8,7 @@
 import logging
 import itertools
 import os
+import distutils.version
 
 from PIL import Image, TiffImagePlugin, TiffTags
 
@@ -231,6 +232,32 @@ def test_additional_metadata(self):
 
         TiffImagePlugin.WRITE_LIBTIFF = False
 
+    def test_custom_metadata(self):
+        custom = {
+            37000: 4,
+            37001: 4.2
+        }
+
+        libtiff_version = TiffImagePlugin._libtiff_version()
+
+        libtiffs = [False]
+        if distutils.version.StrictVersion(libtiff_version) >= \
+           distutils.version.StrictVersion("4.0"):
+            libtiffs.append(True)
+
+        for libtiff in libtiffs:
+            TiffImagePlugin.WRITE_LIBTIFF = libtiff
+
+            im = hopper()
+
+            out = self.tempfile("temp.tif")
+            im.save(out, tiffinfo=custom)
+            TiffImagePlugin.WRITE_LIBTIFF = False
+
+            reloaded = Image.open(out)
+            for tag, value in custom.items():
+                self.assertEqual(reloaded.tag_v2[tag], value)
+
     def test_int_dpi(self):
         # issue #1765
         im = hopper('RGB')
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -135,7 +135,7 @@ def test_write_metadata(self):
         for k, v in original.items():
             if isinstance(v, IFDRational):
                 original[k] = IFDRational(*_limit_rational(v, 2**31))
-            if isinstance(v, tuple) and isinstance(v[0], IFDRational):
+            elif isinstance(v, tuple) and isinstance(v[0], IFDRational):
                 original[k] = tuple(IFDRational(*_limit_rational(elt, 2**31))
                                     for elt in v)
 
diff --git a/Tests/versions.py b/Tests/versions.py
--- a/Tests/versions.py
+++ b/Tests/versions.py
@@ -10,6 +10,7 @@ def version(module, version):
 
 version(Image, "jpeglib")
 version(Image, "zlib")
+version(Image, "libtiff")
 
 try:
     from PIL import ImageFont