Repository: python-pillow/Pillow

Issue:
bug: tobytes() byteorder with multibyte samples, bigendian
Short story and end result: numpy tests are failing on bigendian arches, as reported here: https://bugzilla.redhat.com/show_bug.cgi?id=1019656

Long story: The problem is that the test image 12bit.cropped.tif is a tiff image with data stored in little-endian format and 16bit samples. Now, Image.tobytes() (which is called when doing numpy.array(img)) returns the raw data, without byteswapping. This means that i.e.

```
img = Image.open('12bit.cropped.tif')
print img.tobytes()[0:2]
```

returns `'\xe0\x01'` on both big- and little-endian platforms, which corresponds (correctly) to 480 on little-endian, but on big-endian it corresponds to 57345.

FIxing the issue itself should not be too much trouble, the main issue is where to fix it.
Some possibilities (without very in-depth study of the code):
- The test is wrong. tobytes() returns the raw data, and if that appens to be in little-endian format, then it is up to the user to handle this.
- TiffDecode should byteswap if the endianness of the image data differs from the platform endinanness.

Any other ideas?

Save RGB Tiff using LibTiff raises exception
TiffImagePlugin.py, line 1010:
        `atts = dict([(k,v) for (k,(v,)) in ifd.items() if k not in blocklist])`

fails on BytesPerSample = (8,8,8), since there are too many bytes to unpack. 

This wasn't a problem with bilevel g3/4, but is for deflate. 



Discussion:
(recognizing my code there...) 

I think the decoder should be putting the bytes into native order, i.e. what we get out of the tiff decoder should always be a I;16N rawmode, no matter if it's a II or MM tiff.  I'd bet the libtiff decoder already does this.  

On the other hand, I wouldn't bet against the test being wrong, as I didn't test it against a big endian machine. Time to fire up the old mini I guess. 

I'll dig some on this. (oh, it would be nice if there was documentation of what the in memory bits and assumptions were supposed to be. I'm so sure I've been over this before)

Are there any places to get a bigendian virtual machine to test against?

I've been granted access to a ppc64 cluster by a Fedora contributor. If you post your public ssh key and give a username, I can see whether they can give you access also.

Thanks,  wiredfool works as a username.   

https://gist.github.com/wiredfool/c441e27403d09239a0e9

A little googling led to qemu + a ppc distro + mucking about as well, so there may be a plan b. 

Request posted https://bugzilla.redhat.com/show_bug.cgi?id=1019656#c4

I tried with qemu a while back, little success (if memory serves me right, there was some major component not fully working for ppc back then, and possibly still).

Thanks for the account request. 

My standard estimation on the qemu approach is that it's a day of chasing dependencies. (based on no one giving detailed directions for it working out of the box). 

Dependency-wise it's pretty ok on Fedora, just `# yum install qemu` and then grab a ppc iso (i.e. from http://mirrors.fedoraproject.org/publiclist/Fedora/19/ppc/) and then 

```
qemu-system-ppc64 -cdrom Fedora-19-ppc64-netinst.iso
```

Last time I only saw a yellow screen, but now something actually seems to happen (albeit very slow).

I'm on ubuntu for a host os, and after tracking down an openbios and mucking with the network configuration, I've got a black screen and 100% processor usage. for 15 minutes. It's either glacial or broken. Hopefully your guy will come through. 

wiredfool, please try ssh chinook.fedora.osuosl.org and let me know if you have any problem logging in.

that would be @gustavold , it's asking for a pw. sorry gustavoid.

Oh, sorry. I think it was a file permissions issue. Try again, please.

Still not working.

```
ssh wiredfool@chinook.fedora.osuosl.org 
wiredfool@chinook.fedora.osuosl.org's password: 
Permission denied, please try again.
wiredfool@chinook.fedora.osuosl.org's password: 
Permission denied, please try again.
wiredfool@chinook.fedora.osuosl.org's password: 
Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password).
```

My bad. It is working now. Sorry about that.

\o/

Ok, I've got Pillow built with enough libraries to replicate this on chinook. 

My understanding at this point is:
- to bytes should return the in memory representation. 
- 16 bit integer tiffs don't do any endian shuffling. (with the native decoder, I haven't checked the libtiff one yet) 
- there aren't any conversions from I;16B to I;16L, or vice versa. 
- Loading a MSB version of this image results in swapped bytes on both platforms. `\x01\xe0`
- The mode for an LSB image is I;16, for MSB is I;16B. 
- img.getpixel((0,0)) returns 480 in all 4 combinations of machine/image endianness.
- It appears that the internal image format is image native, which could be either endian. 



Resolving patch:
diff --git a/PIL/Image.py b/PIL/Image.py
--- a/PIL/Image.py
+++ b/PIL/Image.py
@@ -226,16 +226,17 @@ def isImageType(t):
     "CMYK": ('|u1', 4),
     "YCbCr": ('|u1', 3),
     "LAB": ('|u1', 3), # UNDONE - unsigned |u1i1i1
-    "I;16": ('=u2', None),
+	# I;16 == I;16L, and I;32 == I;32L  
+    "I;16": ('<u2', None),
     "I;16B": ('>u2', None),
     "I;16L": ('<u2', None),
-    "I;16S": ('=i2', None),
+    "I;16S": ('<i2', None),
     "I;16BS": ('>i2', None),
     "I;16LS": ('<i2', None),
-    "I;32": ('=u4', None),
+    "I;32": ('<u4', None),
     "I;32B": ('>u4', None),
     "I;32L": ('<u4', None),
-    "I;32S": ('=i4', None),
+    "I;32S": ('<i4', None),
     "I;32BS": ('>i4', None),
     "I;32LS": ('<i4', None),
 }
diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py
--- a/PIL/TiffImagePlugin.py
+++ b/PIL/TiffImagePlugin.py
@@ -46,6 +46,7 @@
 from PIL import Image, ImageFile
 from PIL import ImagePalette
 from PIL import _binary
+from PIL._util import isStringType
 
 import warnings
 import array, sys
@@ -804,6 +805,12 @@ def _setup(self):
                     # fillorder==2 modes have a corresponding
                     # fillorder=1 mode
                     self.mode, rawmode = OPEN_INFO[key]
+                # libtiff always returns the bytes in native order.
+                # 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'):
+                    rawmode = 'I;16N'
 
                 # Offset in the tile tuple is 0, we go from 0,0 to
                 # w,h, and we only do this once -- eds
@@ -1005,36 +1012,54 @@ def _save(im, fp, filename):
             _fp = os.dup(fp.fileno())
 
         blocklist =  [STRIPOFFSETS, STRIPBYTECOUNTS, ROWSPERSTRIP, ICCPROFILE] # ICC Profile crashes.
-        atts = dict([(k,v) for (k,(v,)) in ifd.items() if k not in blocklist])
-        try:
-            # pull in more bits from the original file, e.g x,y resolution
-            # so that we can save(load('')) == original file.
-            for k,v in im.ifd.items():
-                if k not in atts and k not in blocklist:
-                    if type(v[0]) == tuple and len(v) > 1:
-                       # A tuple of more than one rational tuples
-                        # flatten to floats, following tiffcp.c->cpTag->TIFF_RATIONAL
-                        atts[k] = [float(elt[0])/float(elt[1]) for elt in v]
-                        continue
-                    if type(v[0]) == tuple and len(v) == 1:
-                       # A tuple of one rational tuples
-                        # flatten to floats, following tiffcp.c->cpTag->TIFF_RATIONAL
-                        atts[k] = float(v[0][0])/float(v[0][1])
-                        continue
-                    if type(v) == tuple and len(v) == 1:
-                        # int or similar
-                        atts[k] = v[0]
-                        continue
-                    if type(v) == str:
-                        atts[k] = v
-                        continue
-
-        except:
-            # if we don't have an ifd here, just punt.
-            pass
+        atts={}
+        # 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.
+        for k,v in itertools.chain(ifd.items(), getattr(im, 'ifd', {}).items()):
+            if k not in atts and k not in blocklist:
+                if type(v[0]) == tuple and len(v) > 1:
+                    # A tuple of more than one rational tuples
+                    # flatten to floats, following tiffcp.c->cpTag->TIFF_RATIONAL
+                    atts[k] = [float(elt[0])/float(elt[1]) for elt in v]
+                    continue
+                if type(v[0]) == tuple and len(v) == 1:
+                    # A tuple of one rational tuples
+                    # flatten to floats, following tiffcp.c->cpTag->TIFF_RATIONAL
+                    atts[k] = float(v[0][0])/float(v[0][1])
+                    continue
+                if type(v) == tuple and len(v) > 2:
+                    # List of ints?
+                    # BitsPerSample is one example, I get (8,8,8)
+                    # UNDONE
+                    continue
+                if type(v) == tuple and len(v) == 2:
+                    # one rational tuple
+                    # flatten to float, following tiffcp.c->cpTag->TIFF_RATIONAL
+                    atts[k] = float(v[0])/float(v[1])
+                    continue
+                if type(v) == tuple and len(v) == 1:
+                    v = v[0]
+                    # drop through
+                if isStringType(v):
+                    atts[k] = bytes(v.encode('ascii', 'replace')) + b"\0"
+                    continue
+                else:
+                    # int or similar
+                    atts[k] = v
+
         if Image.DEBUG:
             print (atts)
+
+        # libtiff always returns the bytes in native order.
+        # we're expecting 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'):
+            rawmode = 'I;16N'
+
         a = (rawmode, compression, _fp, filename, atts)
+        # print (im.mode, compression, a, im.encoderconfig)
         e = Image._getencoder(im.mode, compression, a, im.encoderconfig)
         e.setimage(im.im, (0,0)+im.size)
         while 1:


Test patch:
diff --git a/Tests/images/12bit.MM.cropped.tif b/Tests/images/12bit.MM.cropped.tif
new file mode 100644
Binary files /dev/null and b/Tests/images/12bit.MM.cropped.tif differ
diff --git a/Tests/images/12bit.MM.deflate.tif b/Tests/images/12bit.MM.deflate.tif
new file mode 100644
Binary files /dev/null and b/Tests/images/12bit.MM.deflate.tif differ
diff --git a/Tests/images/12bit.deflate.tif b/Tests/images/12bit.deflate.tif
new file mode 100644
Binary files /dev/null and b/Tests/images/12bit.deflate.tif differ
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
@@ -93,6 +93,8 @@ def test_g4_write():
     _assert_noerr(reread)
     assert_image_equal(reread, rot)
 
+    assert_equal(reread.info['compression'], orig.info['compression'])
+    
     assert_false(orig.tobytes() == reread.tobytes())
 
 def test_adobe_deflate_tiff():
@@ -105,3 +107,65 @@ def test_adobe_deflate_tiff():
     assert_no_exception(lambda: im.load())
 
 
+def test_little_endian():
+    im = Image.open('Tests/images/12bit.deflate.tif')
+    assert_equal(im.getpixel((0,0)), 480)
+    assert_equal(im.mode, 'I;16')
+
+    b = im.tobytes()
+    # Bytes are in image native order (little endian)
+    if py3:
+        assert_equal(b[0], ord(b'\xe0'))
+        assert_equal(b[1], ord(b'\x01'))
+    else:
+        assert_equal(b[0], b'\xe0')
+        assert_equal(b[1], b'\x01')
+        
+
+    out = tempfile("temp.tif")
+    out = "temp.le.tif"
+    im.save(out)
+    reread = Image.open(out)
+
+    assert_equal(reread.info['compression'], im.info['compression'])
+    assert_equal(reread.getpixel((0,0)), 480)
+    # UNDONE - libtiff defaults to writing in native endian, so
+    # on big endian, we'll get back mode = 'I;16B' here. 
+    
+def test_big_endian():
+    im = Image.open('Tests/images/12bit.MM.deflate.tif')
+
+    assert_equal(im.getpixel((0,0)), 480)
+    assert_equal(im.mode, 'I;16B')
+
+    b = im.tobytes()
+
+    # Bytes are in image native order (big endian)
+    if py3:
+        assert_equal(b[0], ord(b'\x01'))
+        assert_equal(b[1], ord(b'\xe0'))
+    else:
+        assert_equal(b[0], b'\x01')
+        assert_equal(b[1], b'\xe0')
+    
+    out = tempfile("temp.tif")
+    im.save(out)
+    reread = Image.open(out)
+
+    assert_equal(reread.info['compression'], im.info['compression'])
+    assert_equal(reread.getpixel((0,0)), 480)
+
+def test_g4_string_info():
+    """Tests String data in info directory"""
+    file = "Tests/images/lena_g4_500.tif"
+    orig = Image.open(file)
+    
+    out = tempfile("temp.tif")
+
+    orig.tag[269] = 'temp.tif'
+    orig.save(out)
+             
+    reread = Image.open(out)
+    assert_equal('temp.tif', reread.tag[269])
+
+
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
@@ -71,3 +71,34 @@ def test_xyres_tiff():
     im.tag.tags[Y_RESOLUTION] = (72,)
     im._setup()
     assert_equal(im.info['dpi'], (72., 72.))
+
+
+def test_little_endian():
+	im = Image.open('Tests/images/12bit.cropped.tif')
+	assert_equal(im.getpixel((0,0)), 480)
+	assert_equal(im.mode, 'I;16')
+
+	b = im.tobytes()
+	# Bytes are in image native order (little endian)
+	if py3:
+		assert_equal(b[0], ord(b'\xe0'))
+		assert_equal(b[1], ord(b'\x01'))
+	else:
+		assert_equal(b[0], b'\xe0')
+		assert_equal(b[1], b'\x01')
+		
+
+def test_big_endian():
+	im = Image.open('Tests/images/12bit.MM.cropped.tif')
+	assert_equal(im.getpixel((0,0)), 480)
+	assert_equal(im.mode, 'I;16B')
+
+	b = im.tobytes()
+
+	# Bytes are in image native order (big endian)
+	if py3:
+		assert_equal(b[0], ord(b'\x01'))
+		assert_equal(b[1], ord(b'\xe0'))
+	else:
+		assert_equal(b[0], b'\x01')
+		assert_equal(b[1], b'\xe0')
diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py
--- a/Tests/test_numpy.py
+++ b/Tests/test_numpy.py
@@ -78,7 +78,7 @@ def test_16bit():
     img = Image.open('Tests/images/12bit.cropped.tif')
     np_img = numpy.array(img)
     _test_img_equals_nparray(img, np_img)
-    assert_equal(np_img.dtype, numpy.dtype('uint16'))
+    assert_equal(np_img.dtype, numpy.dtype('<u2'))
 
 def test_to_array():
 
@@ -97,9 +97,9 @@ def _to_array(mode, dtype):
              ("RGBX", 'uint8'),
              ("CMYK", 'uint8'),
              ("YCbCr", 'uint8'),
-             ("I;16", 'uint16'),
+             ("I;16", '<u2'),
              ("I;16B", '>u2'),
-             ("I;16L", 'uint16'),
+             ("I;16L", '<u2'),
              ]