Repository: python-pillow/Pillow

Issue:
AttributeError: 'tuple' object has no attribute 'ljust' when trying to save image
### What did you do?

Ran a snippet of code, see below.

### What did you expect to happen?

The script to complete without error.

### What actually happened?

Stack trace, see below.

### What versions of Pillow and Python are you using?

Pillow==3.4.2
(also fails with Pillow==3.5.0.dev0)
Python 3.5.2


Please include code that reproduces the issue and whenever possible, an image that demonstrates the issue. The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as plone, django, or buildout, try to replicate the issue just using Pillow. 

```python
from io import BytesIO
from contextlib import closing
from PIL import Image

image = Image.open('phototest.tif')
print(image)

with closing(BytesIO()) as f:
    image.save(f, image.format or 'JPEG')
    value = f.getvalue()
    

```

Where the image can be found [here](https://github.com/tesseract-ocr/tesseract/blob/master/testing/phototest.tif?raw=true), or download it with this command:

```
curl  "https://github.com/tesseract-ocr/tesseract/blob/master/testing/phototest.tif?raw=true" > phototest.tif
```

The output is:

```
<PIL.TiffImagePlugin.TiffImageFile image mode=1 size=640x480 at 0x7F37D8546E10>
Traceback (most recent call last):
  File "try.py", line 9, in <module>
    image.save(f, image.format or 'JPEG')
  File "/usr/local/lib/python3.5/dist-packages/PIL/Image.py", line 1698, in save
    save_handler(self, fp, filename)
  File "/usr/local/lib/python3.5/dist-packages/PIL/TiffImagePlugin.py", line 1487, in _save
    offset = ifd.save(fp)
  File "/usr/local/lib/python3.5/dist-packages/PIL/TiffImagePlugin.py", line 758, in save
    entries.append((tag, typ, count, data.ljust(4, b"\0"), b""))
AttributeError: 'tuple' object has no attribute 'ljust'
```



Discussion:
Minimal repro is:

```python 
from PIL import Image
im = Image.open('phototest.tif')
im.save('tmp.tif')
```

It's 3.x only, this works in 2.7. It succeeded before the great metadata rewrite of Pillow 3.0.
Error is in a metadata entry: 

```
Tag 34377, Type: 1, Value: ("8BIM\x03\xed\x00\x00\x00\x00\x00\x10\x00\xc8\x00\x00\x00\x01\x00\x01\x00\xc8\x00\x00\x00\x01\x00\x018BIM\x03\xf3\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x008BIM'\x10\x00\x00\x00\x00\x00\n\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02",)
save: PhotoshopInfo (34377) - type: byte (1) - value: <table: 70 bytes>
```
and in 3.5:
```
Tag 34377, Type: 1, Value: ((b"8BIM\x03\xed\x00\x00\x00\x00\x00\x10\x00\xc8\x00\x00\x00\x01\x00\x01\x00\xc8\x00\x00\x00\x01\x00\x018BIM\x03\xf3\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x008BIM'\x10\x00\x00\x00\x00\x00\n\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02",),)
save: PhotoshopInfo (34377) - type: byte (1) - value: ((b"8BIM\x03\xed\x00\x00\x00\x00\x00\x10\x00\xc8\x00\x00\x00\x01\x00\x01\x00\xc8\x00\x00\x00\x01\x00\x018BIM\x03\xf3\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x008BIM'\x10\x00\x00\x00\x00\x00\n\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02",),)
```
Any updates on this ?
Was affected by this issue today (pillow 4.2.1).
This is still broken for me. Pillow 7.0.0

```
>>> im.save('/tmp/WhiteRim.tiff', compression='raw')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.6/dist-packages/PIL/Image.py", line 2102, in save
    save_handler(self, fp, filename)
  File "/usr/local/lib/python3.6/dist-packages/PIL/TiffImagePlugin.py", line 1629, in _save
    offset = ifd.save(fp)
  File "/usr/local/lib/python3.6/dist-packages/PIL/TiffImagePlugin.py", line 865, in save
    result = self.tobytes(offset)
  File "/usr/local/lib/python3.6/dist-packages/PIL/TiffImagePlugin.py", line 828, in tobytes
    entries.append((tag, typ, count, data.ljust(4, b"\0"), b""))
AttributeError: 'tuple' object has no attribute 'ljust'
```
I'm having this same issue with Pillow 7.0.0. But oddly the same code works fine on my Mac, but not on my AWS lambda instance. 
```
Traceback (most recent call last):
  File "/var/task/lambda_function.py", line 121, in lambda_handler
    img.save(local_compressed, extension, quality=IMAGE_COMPRESSION_QUALITY)
  File "/var/task/PIL/Image.py", line 2102, in save
    save_handler(self, fp, filename)
  File "/var/task/PIL/TiffImagePlugin.py", line 1629, in _save
    offset = ifd.save(fp)
  File "/var/task/PIL/TiffImagePlugin.py", line 865, in save
    result = self.tobytes(offset)
  File "/var/task/PIL/TiffImagePlugin.py", line 828, in tobytes
    entries.append((tag, typ, count, data.ljust(4, b"\0"), b""))
AttributeError: 'tuple' object has no attribute 'ljust'

'tuple' object has no attribute 'ljust'
```
@aaronmeierx @TheJKFever What Linux version are you using? Can you reproduce it with the image from the PR [1]? If not, please can you attach your problem image? Which Python version are you using? Does it work for you with Pillow 4.3.0? How about intermediate versions?

[1] https://github.com/python-pillow/Pillow/blob/master/Tests/images/issue_2278.tif

I cannot reproduce with Alpine and Pillow 7.0.0 and that image:

```console
wodby@php.container:/var/www/html $ cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.11.3
PRETTY_NAME="Alpine Linux v3.11"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"
wodby@php.container:/var/www/html $ python3
Python 3.8.1 (default, Dec 30 2019, 15:43:37)
[GCC 9.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from PIL import Image
>>> Image.__version__
'7.0.0'
>>> im = Image.open("issue_2278.tif")
>>> im
<PIL.TiffImagePlugin.TiffImageFile image mode=1 size=640x480 at 0x7F9BA46C6130>
>>> im.save("temp.tif")
```
For the record, the original image appears to have moved to https://github.com/tesseract-ocr/test/blob/master/testing/phototest.tif?raw=true
Closing, as we don't have the images to reproduce the re-opened form of this. If they can be supplied, this can be re-opened.
I'll probably submit a PR later (I think I have another issue I need to look into and if it's related, I'll roll them both together), but for now, for anyone who's stuck on this, I spent a LOT of time tracking it down today. My monkeypatch fix is:

```python
from PIL.TiffImagePlugin import ImageFileDirectory_v2, TiffTags

def _monkey_write_undefined(_, value):
    if isinstance(value, tuple) and len(value) == 1:
        return value[0]
    else:
        return value

ImageFileDirectory_v2._write_dispatch[TiffTags.UNDEFINED] = _monkey_write_undefined
```

This is because the call to `ImageFileDirectory_v2.tobytes(…)` tries to:

```python
values = value if isinstance(value, tuple) else (value,)
data = self._write_dispatch[typ](self, *values)
```

When `typ=7`, this goes to `write_undefined` which returns the value; but the `data.ljust()` call below expectes a `bytes` object.

It doesn't happen with the linked test image, above. I have it happening with several images that I can't share (client images), but here's some data from one of these images in case it helps:

```
❯ nix-shell -p imagemagick --run 'identify broken.tif'
broken.tif TIFF 4800x6000 4800x6000+0+0 8-bit sRGB 82.4212MiB 0.000u 0:00.005
identify: Incompatible type for "RichTIFFIPTC"; tag ignored. `TIFFFetchNormalTag' @ warning/tiff.c/TIFFWarnings/976.
❯ file broken.tif
broken.tif: TIFF image data, big-endian, direntries=22, height=6000, bps=0, compression=none, PhotometricIntepretation=RGB, orientation=upper-left, width=4800
```

Photoshop 2020 reports that this image is from `Adobe Photoshop CC 2018 (Macintosh)`.

FWIW, I think this issue should be reopened, but I understand the difficulty of doing so without a test image/file.


@radarhere @scoates @hugovk 

This issue not only still resists in latest Pillow (using 8.2.0 on Ubuntu 2.04 LTS) *but* I can provide TIFF Testdata and pytest testcases to reproduce this buggy behavior on at least 3 different (Ubuntu-)Machines.

Please someone reopen this asap ... since the patch @scoates provided above finally saved my day.


I think I can probably track down an image for this, now, too. Would a PR help?
Personally, I'm fine with any advance.

This is how I integrated and tested @scoates approach with a broken TIFF:

 ```python
@pytest.fixture
def fixture_suspicous_tif(tmp_path):
    img_path = tmp_path / '00000001.tif'
    path_tif = config('invalid_tiff_01')
    shutil.copy(path_tif, img_path)
    yield img_path


def test_suspicous_tiff_01(fixture_suspicous_tif):
    '''
    Update / sanitize TIFF header data, fails with:
    * Pillow 7.1.0
    * Pillow 8.2.0
    '''

    # prevent to overwrite the script hack just for this test
    ImageFileDirectory_v2._write_dispatch[TiffTags.UNDEFINED] = lambda _, value: value

    with pytest.raises(RuntimeError) as exc:
        sanitize_image_headerdata(fixture_suspicous_tif)
    assert '\'tuple\' object has no attribute \'ljust\'' in str(exc)
```
The key issue here is, the image contains some special header tags unknown to Pillow. 
And there are several tags Pillow doesn't know. But we want Pillow to handle them appropriately. 

In my cases I face some `IPTC` header data. Even ImageMagick complains with :
```
convert-im6.q16: Incompatible type for "RichTIFFIPTC"; tag ignored. `TIFFFetchNormalTag' @ warning/tiff.c/TIFFWarnings/949.
```
even though it's not dying with the totally obfuscating `tuple object has no attribute ljust` (but throws off `IPTC` header and some `EXIF` tags as well. 


Please provide an example image.
[00000001.zip](https://github.com/python-pillow/Pillow/files/6348348/00000001.zip)

@radarhere This archive contains the file exactly as it is, without compression or whatever, utilized it for the testcase above. 
It's rather large, but I didn't manage to resize the image without loosing the suspicious header information.  
The testcase patch above tries to overwrite @scoates `_monkey_write_undefined` which I use actually to patch Pillow within my script which itself replaces

```python
        _write_dispatch[idx] = lambda self, *values: (  # noqa: F821
            b"".join(self._pack(fmt, value) for value in values)
        )
```
from current `TiffImagePlugin.py:654` in a naive fashion.
I've created PR #5425 to resolve this.

Resolving patch:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -565,7 +565,8 @@ def _setitem(self, tag, value, legacy_api):
 
         if self.tagtype[tag] == TiffTags.UNDEFINED:
             values = [
-                value.encode("ascii", "replace") if isinstance(value, str) else value
+                v.encode("ascii", "replace") if isinstance(v, str) else v
+                for v in values
             ]
         elif self.tagtype[tag] == TiffTags.RATIONAL:
             values = [float(v) if isinstance(v, int) else v for v in values]


Test patch:
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
@@ -185,6 +185,21 @@ def test_iptc(tmp_path):
         im.save(out)
 
 
+def test_undefined_zero(tmp_path):
+    # Check that the tag has not been changed since this test was created
+    tag = TiffTags.TAGS_V2[45059]
+    assert tag.type == TiffTags.UNDEFINED
+    assert tag.length == 0
+
+    info = TiffImagePlugin.ImageFileDirectory(b"II*\x00\x08\x00\x00\x00")
+    info[45059] = b"test"
+
+    # Assert that the tag value does not change by setting it to itself
+    original = info[45059]
+    info[45059] = info[45059]
+    assert info[45059] == original
+
+
 def test_empty_metadata():
     f = io.BytesIO(b"II*\x00\x08\x00\x00\x00")
     head = f.read(8)