Repository: python-pillow/Pillow

Issue:
Saving images to stdout using some file formats does not write the image correctly
<!--
Thank you for reporting an issue.

Follow these guidelines to ensure your issue is handled properly.

If you have a ...

1.  General question: consider asking the question on Stack Overflow
    with the python-imaging-library tag:

    * https://stackoverflow.com/questions/tagged/python-imaging-library

    Do not ask a question in both places.

    If you think you have found a bug or have an unexplained exception
    then file a bug report here.

2.  Bug report: include a self-contained, copy-pastable example that
    generates the issue if possible. Be concise with code posted.
    Guidelines on how to provide a good bug report:

    * https://stackoverflow.com/help/mcve

    Bug reports which follow these guidelines are easier to diagnose,
    and are often handled much more quickly.

3.  Feature request: do a quick search of existing issues
    to make sure this has not been asked before.

We know asking good questions takes effort, and we appreciate your time.
Thank you.
-->

### What did you do?

I created a image and saved it to stdout using PPM format.

### What did you expect to happen?

The image is written correctly.

### What actually happened?

Only the PPM header is written, and the image data is not.

### What are your OS, Python and Pillow versions?

* OS: Gentoo Linux
* Python: Tested with both 3.8.11 and 3.9.6
* Pillow: 8.3.1

<!--
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.

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
import sys
from PIL import Image

im = Image.frombytes('RGB', (4, 4), b'\0' * 48)
im.save(sys.stdout, 'ppm')
```


Discussion:
Additional note: This bug only happens when saving to stdout. It does not seem to happen when saving to a named file, file object, or other file-like objects like BytesIO.

Interestingly, some other file formats are affected as well: when saving the image as JPEG, no data is written at all. With GIF the image is corrupted (see below). PNG seems to be unaffected, though.

GIF example:
```python
im = Image.frombytes('RGB', (4, 4), b'\xff\0\0' * 16)
im.save(sys.stdout, 'gif')
```
The output has two black pixels (unexpected) in lower left.
`sys.stdout` is effectively a text file, the equivalent of io.StringIO, likely to be in utf-8 or your specific locale. (https://docs.python.org/3/library/sys.html#sys.stdout)  Pillow has never been able to save to a io.StringIO instance on python 3, it has always required a io.BytesIO.

Most image formats have invalid utf-8 sequences in them, so they're not generally going to work with sys.stdout, unless you explicitly write the binary data to the underlying stream as mentioned in the link. 
Well, Pillow should be able to convert `sys.stdout` to `sys.stdout.buffer` according to #5437. Testing with `sys.stdout.buffer` returns the same erroneous results as well.
Missed that change. 

Resolving patch:
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -493,13 +493,6 @@ def _save(im, fp, tile, bufsize=0):
     # But, it would need at least the image size in most cases. RawEncode is
     # a tricky case.
     bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4)  # see RawEncode.c
-    try:
-        stdout = fp == sys.stdout or fp == sys.stdout.buffer
-    except (OSError, AttributeError):
-        stdout = False
-    if stdout:
-        fp.flush()
-        return
     try:
         fh = fp.fileno()
         fp.flush()


Test patch:
diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py
--- a/Tests/test_file_ppm.py
+++ b/Tests/test_file_ppm.py
@@ -1,3 +1,6 @@
+import sys
+from io import BytesIO
+
 import pytest
 
 from PIL import Image
@@ -80,3 +83,30 @@ def test_mimetypes(tmp_path):
         f.write("PyCMYK\n128 128\n255")
     with Image.open(path) as im:
         assert im.get_format_mimetype() == "image/x-portable-anymap"
+
+
+@pytest.mark.parametrize("buffer", (True, False))
+def test_save_stdout(buffer):
+    old_stdout = sys.stdout
+
+    if buffer:
+
+        class MyStdOut:
+            buffer = BytesIO()
+
+        mystdout = MyStdOut()
+    else:
+        mystdout = BytesIO()
+
+    sys.stdout = mystdout
+
+    with Image.open(TEST_FILE) as im:
+        im.save(sys.stdout, "PPM")
+
+    # Reset stdout
+    sys.stdout = old_stdout
+
+    if buffer:
+        mystdout = mystdout.buffer
+    with Image.open(mystdout) as reloaded:
+        assert_image_equal_tofile(reloaded, TEST_FILE)