Repository: python-pillow/Pillow

Issue:
File handler leak with Tiff CCITT Group4 format
The handler to a Tiff CCITT Group4 file is not released properly. The code below gives an example. At the end the program holds waiting for the user to press "enter". At that moment, I use "Process Explorer" to check the file handles. The first is not released while the second is. Bug ? 

Python : 2.7
Pillow : 2.3.0
OS : Windows XP/7 (32 bits)

```
# -*- coding: Utf-8 -*-
import sys
from PIL import Image

with open("mypath\\0000700.tif", "rb") as f:
    imagef = Image.open(f)
    print imagef.size

with open("mypath\\0000700.jpg", "rb") as g:
    imageg = Image.open(g)
    print imageg.size

sys.stdin.readline()
```

Thanks 
Laurent



Discussion:
Pillow doesn't currently support with. It handles the lazy reading and closing of the files as necessary.

How do you explain that the handle is released at the end for the jpg file and not for the tiff one ?

@wiredfool Can you explain why Pillow doesn't support the use of the `with` statement? I'm curious

Well, there are many reasons:

1) there's no __enter__ or __exit__.
2) there's no close either, since it's managed in each backend.

I have a branch where I implemented with to prevent the warning on unclosed files, but I'm being kind of aggressive about closing and disposing of the image data.  It's an approach, I'm not sure if it's the right one.

IC, thanks


Resolving patch:
diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py
--- a/PIL/TiffImagePlugin.py
+++ b/PIL/TiffImagePlugin.py
@@ -693,10 +693,11 @@ def _load_libtiff(self):
         if not len(self.tile) == 1:
             raise IOError("Not exactly one tile")
 
-        d, e, o, a = self.tile[0]
-        d = Image._getdecoder(self.mode, 'libtiff', a, self.decoderconfig)
+        # (self._compression, (extents tuple), 0, (rawmode, self._compression, fp))
+        ignored, extents, ignored_2, args = self.tile[0]
+        decoder = Image._getdecoder(self.mode, 'libtiff', args, self.decoderconfig)
         try:
-            d.setimage(self.im, e)
+            decoder.setimage(self.im, extents)
         except ValueError:
             raise IOError("Couldn't set the image")
 
@@ -712,27 +713,30 @@ def _load_libtiff(self):
             # with here by reordering.
             if Image.DEBUG:
                 print ("have getvalue. just sending in a string from getvalue")
-            n,e = d.decode(self.fp.getvalue())
+            n,err = decoder.decode(self.fp.getvalue())
         elif hasattr(self.fp, "fileno"):
             # we've got a actual file on disk, pass in the fp.
             if Image.DEBUG:
                 print ("have fileno, calling fileno version of the decoder.")
             self.fp.seek(0)
-            n,e = d.decode(b"fpfp") # 4 bytes, otherwise the trace might error out
+            n,err = decoder.decode(b"fpfp") # 4 bytes, otherwise the trace might error out
         else:
             # we have something else.
             if Image.DEBUG:
                 print ("don't have fileno or getvalue. just reading")
             # UNDONE -- so much for that buffer size thing.
-            n, e = d.decode(self.fp.read())
+            n,err = decoder.decode(self.fp.read())
 
 
         self.tile = []
         self.readonly = 0
+        # libtiff closed the fp in a, we need to close self.fp, if possible
+        if hasattr(self.fp, 'close'):
+            self.fp.close()
         self.fp = None # might be shared
 
-        if e < 0:
-            raise IOError(e)
+        if err < 0:
+            raise IOError(err)
 
         self.load_end()
 


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
@@ -1,4 +1,5 @@
 from tester import *
+import os
 
 from PIL import Image, TiffImagePlugin
 
@@ -295,3 +296,13 @@ def xtest_bw_compression_wRGB():
     assert_exception(IOError, lambda: im.save(out, compression='group3'))
     assert_exception(IOError, lambda: im.save(out, compression='group4'))
 
+def test_fp_leak():
+    im = Image.open("Tests/images/lena_g4_500.tif")
+    fn = im.fp.fileno()
+
+    assert_no_exception(lambda: os.fstat(fn))
+    im.load()  # this should close it. 
+    assert_exception(OSError, lambda: os.fstat(fn)) 
+    im = None  # this should force even more closed.
+    assert_exception(OSError, lambda: os.fstat(fn)) 
+    assert_exception(OSError, lambda: os.close(fn))