Repository: python-pillow/Pillow

Issue:
Opening 12-bit tiff files
Hi, 

I am trying to open 12bit image files with PIL/Pillow. This currently seems to be unsupported. 

For a sample image, the debug mode spits out the following:

`format key: ('II', 1, 1, 1, (12,), ())`

Now I thought I'd cheat and just insert a key into the OPEN_key dict in TiffImagePlugin.py

`(II, 1, 1, 1, (12,), ()): ("I;16", "I;16")`

This now obviously messes up the image, and my tiff file ends seemingly prematurely. 

Am I missing something? If it is possible to read 12bit tiffs easily, please let me know. If it is as of yet unsupported, I am happy to help, although I am afraid I will need some pointers :)

Here are two samples: [Single frame](https://www.dropbox.com/s/m7fnq76ih3my8mo/sample-single.tif), [small stack](https://www.dropbox.com/s/oh82f8jsrpiq0ei/sample.tif) (500mb!).



Discussion:

Resolving patch:
diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py
--- a/PIL/TiffImagePlugin.py
+++ b/PIL/TiffImagePlugin.py
@@ -54,7 +54,9 @@
 import itertools
 import os
 
-
+# Set these to true to force use of libtiff for reading or writing. 
+READ_LIBTIFF = False
+WRITE_LIBTIFF= False
 
 II = b"II" # little-endian (intel-style)
 MM = b"MM" # big-endian (motorola-style)
@@ -149,6 +151,7 @@
     (II, 1, 1, 1, (8,), ()): ("L", "L"),
     (II, 1, 1, 1, (8,8), (2,)): ("LA", "LA"),
     (II, 1, 1, 2, (8,), ()): ("L", "L;R"),
+    (II, 1, 1, 1, (12,), ()): ("I;16", "I;12"),
     (II, 1, 1, 1, (16,), ()): ("I;16", "I;16"),
     (II, 1, 2, 1, (16,), ()): ("I;16S", "I;16S"),
     (II, 1, 1, 1, (32,), ()): ("I", "I;32N"),
@@ -675,8 +678,8 @@ def _decoder(self, rawmode, layer, tile=None):
         return args
 
     def _load_libtiff(self):
-        """ Overload method triggered when we detect a g3/g4 tiff
-            Calls out to lib tiff """
+        """ Overload method triggered when we detect a compressed tiff
+            Calls out to libtiff """
 
         pixel = Image.Image.load(self)
 
@@ -691,7 +694,7 @@ def _load_libtiff(self):
             raise IOError("Not exactly one tile")
 
         d, e, o, a = self.tile[0]
-        d = Image._getdecoder(self.mode, d, a, self.decoderconfig)
+        d = Image._getdecoder(self.mode, 'libtiff', a, self.decoderconfig)
         try:
             d.setimage(self.im, e)
         except ValueError:
@@ -817,11 +820,11 @@ def _setup(self):
             offsets = self.tag[STRIPOFFSETS]
             h = getscalar(ROWSPERSTRIP, ysize)
             w = self.size[0]
-            if self._compression in ["tiff_ccitt", "group3", "group4",
-                                     "tiff_jpeg", "tiff_adobe_deflate",
-                                     "tiff_thunderscan", "tiff_deflate",
-                                     "tiff_sgilog", "tiff_sgilog24",
-                                     "tiff_raw_16"]:
+            if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3", "group4",
+                                                     "tiff_jpeg", "tiff_adobe_deflate",
+                                                     "tiff_thunderscan", "tiff_deflate",
+                                                     "tiff_sgilog", "tiff_sgilog24",
+                                                     "tiff_raw_16"]:
                 ## if Image.DEBUG:
                 ##     print "Activating g4 compression for whole file"
 
@@ -868,7 +871,7 @@ def _setup(self):
                 # we're expecting image byte order. So, if the rawmode
                 # contains I;16, we need to convert from native to image
                 # byte order.
-                if self.mode in ('I;16B', 'I;16'):
+                if self.mode in ('I;16B', 'I;16') and 'I;16' in rawmode:
                     rawmode = 'I;16N'
 
                 # Offset in the tile tuple is 0, we go from 0,0 to
@@ -975,12 +978,16 @@ def _save(im, fp, filename):
     ifd = ImageFileDirectory(prefix)
 
     compression = im.encoderinfo.get('compression',im.info.get('compression','raw'))
-    libtiff = compression in ["tiff_ccitt", "group3", "group4",
-                              "tiff_jpeg", "tiff_adobe_deflate",
-                              "tiff_thunderscan", "tiff_deflate",
-                              "tiff_sgilog", "tiff_sgilog24",
-                              "tiff_raw_16"]
 
+    libtiff = WRITE_LIBTIFF or compression in ["tiff_ccitt", "group3", "group4",
+                                               "tiff_jpeg", "tiff_adobe_deflate",
+                                               "tiff_thunderscan", "tiff_deflate",
+                                               "tiff_sgilog", "tiff_sgilog24",
+                                               "tiff_raw_16"]
+
+    # required for color libtiff images
+    ifd[PLANAR_CONFIGURATION] = getattr(im, '_planar_configuration', 1)
+    
     # -- multi-page -- skip TIFF header on subsequent pages
     if not libtiff and fp.tell() == 0:
         # tiff header (write via IFD to get everything right)
@@ -1083,6 +1090,8 @@ def _save(im, fp, filename):
 
         blocklist =  [STRIPOFFSETS, STRIPBYTECOUNTS, ROWSPERSTRIP, ICCPROFILE] # ICC Profile crashes.
         atts={}
+        # bits per sample is a single short in the tiff directory, not a list. 
+        atts[BITSPERSAMPLE] = bits[0]
         # Merge the ones that we have with (optional) more bits from
         # the original file, e.g x,y resolution so that we can
         # save(load('')) == original file.
@@ -1100,8 +1109,8 @@ def _save(im, fp, filename):
                     continue
                 if type(v) == tuple and len(v) > 2:
                     # List of ints?
-                    # BitsPerSample is one example, I get (8,8,8)
-                    # UNDONE
+                    if type(v[0]) in (int, float):
+                        atts[k] = list(v)
                     continue
                 if type(v) == tuple and len(v) == 2:
                     # one rational tuple
@@ -1121,8 +1130,8 @@ def _save(im, fp, filename):
         if Image.DEBUG:
             print (atts)
 
-        # libtiff always returns the bytes in native order.
-        # we're expecting image byte order. So, if the rawmode
+        # libtiff always expects the bytes in native order.
+        # we're storing image byte order. So, if the rawmode
         # contains I;16, we need to convert from native to image
         # byte order.
         if im.mode in ('I;16B', 'I;16'):
@@ -1130,7 +1139,7 @@ def _save(im, fp, filename):
 
         a = (rawmode, compression, _fp, filename, atts)
         # print (im.mode, compression, a, im.encoderconfig)
-        e = Image._getencoder(im.mode, compression, a, im.encoderconfig)
+        e = Image._getencoder(im.mode, 'libtiff', a, im.encoderconfig)
         e.setimage(im.im, (0,0)+im.size)
         while 1:
             l, s, d = e.encode(16*1024) # undone, change to self.decodermaxblock


Test patch:
diff --git a/Tests/images/12bit.cropped.tif b/Tests/images/12bit.cropped.tif
Binary files a/Tests/images/12bit.cropped.tif and b/Tests/images/12bit.cropped.tif differ
diff --git a/Tests/images/12in16bit.tif b/Tests/images/12in16bit.tif
new file mode 100644
Binary files /dev/null and b/Tests/images/12in16bit.tif differ
diff --git a/Tests/images/12bit.MM.cropped.tif b/Tests/images/16bit.MM.cropped.tif
similarity index 100%
rename from Tests/images/12bit.MM.cropped.tif
rename to Tests/images/16bit.MM.cropped.tif
diff --git a/Tests/images/12bit.MM.deflate.tif b/Tests/images/16bit.MM.deflate.tif
similarity index 100%
rename from Tests/images/12bit.MM.deflate.tif
rename to Tests/images/16bit.MM.deflate.tif
diff --git a/Tests/images/16bit.cropped.tif b/Tests/images/16bit.cropped.tif
new file mode 100644
Binary files /dev/null and b/Tests/images/16bit.cropped.tif differ
diff --git a/Tests/images/12bit.deflate.tif b/Tests/images/16bit.deflate.tif
similarity index 100%
rename from Tests/images/12bit.deflate.tif
rename to Tests/images/16bit.deflate.tif
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,10 +1,10 @@
 from tester import *
 
-from PIL import Image
+from PIL import Image, TiffImagePlugin
 
 codecs = dir(Image.core)
 
-if "group4_encoder" not in codecs or "group4_decoder" not in codecs:
+if "libtiff_encoder" not in codecs or "libtiff_decoder" not in codecs:
     skip("tiff support not available")
 
 def _assert_noerr(im):
@@ -141,7 +141,7 @@ def test_g3_compression():
     assert_image_equal(reread, i)
 
 def test_little_endian():
-    im = Image.open('Tests/images/12bit.deflate.tif')
+    im = Image.open('Tests/images/16bit.deflate.tif')
     assert_equal(im.getpixel((0,0)), 480)
     assert_equal(im.mode, 'I;16')
 
@@ -156,7 +156,7 @@ def test_little_endian():
         
 
     out = tempfile("temp.tif")
-    out = "temp.le.tif"
+    #out = "temp.le.tif"
     im.save(out)
     reread = Image.open(out)
 
@@ -166,7 +166,7 @@ def test_little_endian():
     # on big endian, we'll get back mode = 'I;16B' here. 
     
 def test_big_endian():
-    im = Image.open('Tests/images/12bit.MM.deflate.tif')
+    im = Image.open('Tests/images/16bit.MM.deflate.tif')
 
     assert_equal(im.getpixel((0,0)), 480)
     assert_equal(im.mode, 'I;16B')
@@ -201,4 +201,87 @@ def test_g4_string_info():
     reread = Image.open(out)
     assert_equal('temp.tif', reread.tag[269])
 
+def test_12bit_rawmode():
+    """ Are we generating the same interpretation of the image as Imagemagick is? """
+    TiffImagePlugin.READ_LIBTIFF = True
+    #Image.DEBUG = True
+    im = Image.open('Tests/images/12bit.cropped.tif')
+    im.load()
+    TiffImagePlugin.READ_LIBTIFF = False
+    # to make the target --
+    # convert 12bit.cropped.tif -depth 16 tmp.tif
+    # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif
+    # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0,
+    # so we need to unshift so that the integer values are the same. 
+    
+    im2 = Image.open('Tests/images/12in16bit.tif')
+
+    if Image.DEBUG:
+        print (im.getpixel((0,0)))
+        print (im.getpixel((0,1)))
+        print (im.getpixel((0,2)))
+
+        print (im2.getpixel((0,0)))
+        print (im2.getpixel((0,1)))
+        print (im2.getpixel((0,2)))
+  
+    assert_image_equal(im, im2)
+
+def test_blur():
+    # test case from irc, how to do blur on b/w image and save to compressed tif. 
+    from PIL import ImageFilter
+    out = tempfile('temp.tif')
+    im = Image.open('Tests/images/pport_g4.tif')
+    im = im.convert('L')
+
+    im=im.filter(ImageFilter.GaussianBlur(4))
+    im.save(out, compression='tiff_adobe_deflate')
+
+    im2 = Image.open(out)
+    im2.load()
+
+    assert_image_equal(im, im2)
+
+
+def test_compressions():
+    im = lena('RGB')
+    out = tempfile('temp.tif')
+
+    TiffImagePlugin.READ_LIBTIFF = True
+    TiffImagePlugin.WRITE_LIBTIFF = True
+
+    for compression in ('packbits', 'tiff_lzw'):
+        im.save(out, compression=compression)
+        im2 = Image.open(out)
+        assert_image_equal(im, im2)
+
+    im.save(out, compression='jpeg')
+    im2 = Image.open(out)
+    assert_image_similar(im, im2, 30)
+                            
+    TiffImagePlugin.READ_LIBTIFF = False
+    TiffImagePlugin.WRITE_LIBTIFF = False
+
+
+
+
+def test_cmyk_save():
+    im = lena('CMYK')
+    out = tempfile('temp.tif')
+
+    im.save(out, compression='tiff_adobe_deflate')
+    im2 = Image.open(out)
+    assert_image_equal(im, im2)
+
+def xtest_bw_compression_wRGB():
+    """ This test passes, but when running all tests causes a failure due to
+        output on stderr from the error thrown by libtiff. We need to capture that
+        but not now"""
+    
+    im = lena('RGB')
+    out = tempfile('temp.tif')
+
+    assert_exception(IOError, lambda: im.save(out, compression='tiff_ccitt'))
+    assert_exception(IOError, lambda: im.save(out, compression='group3'))
+    assert_exception(IOError, lambda: im.save(out, compression='group4'))
 
diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py
--- a/Tests/test_file_libtiff_small.py
+++ b/Tests/test_file_libtiff_small.py
@@ -6,7 +6,7 @@
 
 codecs = dir(Image.core)
 
-if "group4_encoder" not in codecs or "group4_decoder" not in codecs:
+if "libtiff_encoder" not in codecs or "libtiff_decoder" not in codecs:
     skip("tiff support not available")
 
 """ The small lena image was failing on open in the libtiff
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
@@ -74,7 +74,7 @@ def test_xyres_tiff():
 
 
 def test_little_endian():
-	im = Image.open('Tests/images/12bit.cropped.tif')
+	im = Image.open('Tests/images/16bit.cropped.tif')
 	assert_equal(im.getpixel((0,0)), 480)
 	assert_equal(im.mode, 'I;16')
 
@@ -89,7 +89,7 @@ def test_little_endian():
 		
 
 def test_big_endian():
-	im = Image.open('Tests/images/12bit.MM.cropped.tif')
+	im = Image.open('Tests/images/16bit.MM.cropped.tif')
 	assert_equal(im.getpixel((0,0)), 480)
 	assert_equal(im.mode, 'I;16B')
 
@@ -102,3 +102,29 @@ def test_big_endian():
 	else:
 		assert_equal(b[0], b'\x01')
 		assert_equal(b[1], b'\xe0')
+
+
+def test_12bit_rawmode():
+    """ Are we generating the same interpretation of the image as Imagemagick is? """
+
+    #Image.DEBUG = True
+    im = Image.open('Tests/images/12bit.cropped.tif')
+
+    # to make the target --
+    # convert 12bit.cropped.tif -depth 16 tmp.tif
+    # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif
+    # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0,
+    # so we need to unshift so that the integer values are the same. 
+    
+    im2 = Image.open('Tests/images/12in16bit.tif')
+
+    if Image.DEBUG:
+        print (im.getpixel((0,0)))
+        print (im.getpixel((0,1)))
+        print (im.getpixel((0,2)))
+
+        print (im2.getpixel((0,0)))
+        print (im2.getpixel((0,1)))
+        print (im2.getpixel((0,2)))
+  
+    assert_image_equal(im, im2)
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -38,12 +38,12 @@ def test_8bit():
     im = Image.open('Images/lena.jpg')
     _test_float_conversion(im.convert('L'))
 
-def test_12bit():
-    im = Image.open('Tests/images/12bit.cropped.tif')
+def test_16bit():
+    im = Image.open('Tests/images/16bit.cropped.tif')
     _test_float_conversion(im)
 
-def test_12bit_workaround():
-    im = Image.open('Tests/images/12bit.cropped.tif')
+def test_16bit_workaround():
+    im = Image.open('Tests/images/16bit.cropped.tif')
     _test_float_conversion(im.convert('I'))
     
 
diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py
--- a/Tests/test_numpy.py
+++ b/Tests/test_numpy.py
@@ -75,7 +75,7 @@ def _test_img_equals_nparray(img, np):
 
 
 def test_16bit():
-    img = Image.open('Tests/images/12bit.cropped.tif')
+    img = Image.open('Tests/images/16bit.cropped.tif')
     np_img = numpy.array(img)
     _test_img_equals_nparray(img, np_img)
     assert_equal(np_img.dtype, numpy.dtype('<u2'))