Repository: python-pillow/Pillow

Issue:
TIFF fails to save resolution if the original IFD doesn't already have this entry
The image "10ct_32bit_128.tiff"  is from from the Pillow tests directory.  It does not contain a resolution entry.

```
In [2]: Image.open("10ct_32bit_128.tiff").save("/tmp/foo.tif", resolution=123.45)     
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-2-1f6672f8e22d> in <module>()
----> 1 Image.open("10ct_32bit_128.tiff").save("/tmp/foo.tif", resolution=123.45)

/usr/lib/python3.5/site-packages/PIL/Image.py in save(self, fp, format, **params)
   1673 
   1674         try:
-> 1675             save_handler(self, fp, filename)
   1676         finally:
   1677             # do what we can to clean up

/usr/lib/python3.5/site-packages/PIL/TiffImagePlugin.py in _save(im, fp, filename)
   1453 
   1454     else:
-> 1455         offset = ifd.save(fp)
   1456 
   1457         ImageFile._save(im, fp, [

/usr/lib/python3.5/site-packages/PIL/TiffImagePlugin.py in save(self, fp)
    717                 print("Tag %s, Type: %s, Value: %s" % (tag, typ, value))
    718             values = value if isinstance(value, tuple) else (value,)
--> 719             data = self._write_dispatch[typ](self, *values)
    720             if DEBUG:
    721                 tagname = TiffTags.lookup(tag).name

KeyError: None
```

Both Pillow 3.1.1 and master.

Yeah, I know I broke a ton of stuff with my rewrite of the tiff plugin.  Sorry about that :/



Discussion:
I'm seeing a resolution _info_ entry, of a different type:

```
>>> im=Image.open('10ct_32bit_128.tiff')
>>> im.load()
<PixelAccess object at 0x7f084a688870>
>>> im.info
{'resolution': (1, 1), 'compression': 'raw'}
>>> 
```

But there's no resolution entry in the IFD. It appears that it's getting added from default data here:

```
        xres = self.tag_v2.get(X_RESOLUTION,1)
        yres = self.tag_v2.get(Y_RESOLUTION,1)

        if xres and yres:
            resunit = self.tag_v2.get(RESOLUTION_UNIT, 1)
            if resunit == 2:  # dots per inch
                self.info["dpi"] = xres, yres
            elif resunit == 3:  # dots per centimeter. convert to dpi
                self.info["dpi"] = xres * 2.54, yres * 2.54
            else:  # No absolute unit of measurement
                self.info["resolution"] = xres, yres
```

I'm thinking that the default value here should be None instead of 1, at least, and 

Yup, you can check the lack of IFD resolution entry by running the tiffinfo executable.
Actually, the code you quoted should probably check for whether xres and yres are set one after another, rather than at the same time (it wouldn't suprise me that only one of them is set, sometimes).

Yep.

And, I don't know what's missing on that earlier reply. 

There's more work that needs to be done with the Tiff stuff, but time is thin for me right now. 


Resolving patch:
diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py
--- a/PIL/TiffImagePlugin.py
+++ b/PIL/TiffImagePlugin.py
@@ -1359,7 +1359,7 @@ def _save(im, fp, filename):
                     IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP):
             if key in im.tag_v2:
                 ifd[key] = im.tag_v2[key]
-            ifd.tagtype[key] = im.tag_v2.tagtype.get(key, None)
+                ifd.tagtype[key] = im.tag_v2.tagtype[key]
 
         # preserve ICC profile (should also work when saving other formats
         # which support profiles as TIFF) -- 2008-06-06 Florian Hoech


Test patch:
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -1,5 +1,6 @@
 from __future__ import print_function
 import logging
+from io import BytesIO
 import struct
 
 from helper import unittest, PillowTestCase, hopper, py3
@@ -103,6 +104,15 @@ def test_int_resolution(self):
         im._setup()
         self.assertEqual(im.info['dpi'], (71., 71.))
 
+    def test_save_setting_missing_resolution(self):
+        from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION
+        b = BytesIO()
+        Image.open("Tests/images/10ct_32bit_128.tiff").save(
+            b, format="tiff", resolution=123.45)
+        im = Image.open(b)
+        self.assertEqual(float(im.tag_v2[X_RESOLUTION]), 123.45)
+        self.assertEqual(float(im.tag_v2[Y_RESOLUTION]), 123.45)
+
     def test_invalid_file(self):
         invalid_file = "Tests/images/flower.jpg"