diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py
--- a/src/PIL/EpsImagePlugin.py
+++ b/src/PIL/EpsImagePlugin.py
@@ -354,56 +354,46 @@ def _save(im, fp, filename, eps=1):
     #
     # determine PostScript image mode
     if im.mode == "L":
-        operator = (8, 1, "image")
+        operator = (8, 1, b"image")
     elif im.mode == "RGB":
-        operator = (8, 3, "false 3 colorimage")
+        operator = (8, 3, b"false 3 colorimage")
     elif im.mode == "CMYK":
-        operator = (8, 4, "false 4 colorimage")
+        operator = (8, 4, b"false 4 colorimage")
     else:
         raise ValueError("image mode is not supported")
 
-    base_fp = fp
-    wrapped_fp = False
-    if fp != sys.stdout:
-        fp = io.TextIOWrapper(fp, encoding="latin-1")
-        wrapped_fp = True
-
-    try:
-        if eps:
-            #
-            # write EPS header
-            fp.write("%!PS-Adobe-3.0 EPSF-3.0\n")
-            fp.write("%%Creator: PIL 0.1 EpsEncode\n")
-            # fp.write("%%CreationDate: %s"...)
-            fp.write("%%%%BoundingBox: 0 0 %d %d\n" % im.size)
-            fp.write("%%Pages: 1\n")
-            fp.write("%%EndComments\n")
-            fp.write("%%Page: 1 1\n")
-            fp.write("%%ImageData: %d %d " % im.size)
-            fp.write('%d %d 0 1 1 "%s"\n' % operator)
-
+    if eps:
         #
-        # image header
-        fp.write("gsave\n")
-        fp.write("10 dict begin\n")
-        fp.write(f"/buf {im.size[0] * operator[1]} string def\n")
-        fp.write("%d %d scale\n" % im.size)
-        fp.write("%d %d 8\n" % im.size)  # <= bits
-        fp.write(f"[{im.size[0]} 0 0 -{im.size[1]} 0 {im.size[1]}]\n")
-        fp.write("{ currentfile buf readhexstring pop } bind\n")
-        fp.write(operator[2] + "\n")
-        if hasattr(fp, "flush"):
-            fp.flush()
-
-        ImageFile._save(im, base_fp, [("eps", (0, 0) + im.size, 0, None)])
-
-        fp.write("\n%%%%EndBinary\n")
-        fp.write("grestore end\n")
-        if hasattr(fp, "flush"):
-            fp.flush()
-    finally:
-        if wrapped_fp:
-            fp.detach()
+        # write EPS header
+        fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n")
+        fp.write(b"%%Creator: PIL 0.1 EpsEncode\n")
+        # fp.write("%%CreationDate: %s"...)
+        fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size)
+        fp.write(b"%%Pages: 1\n")
+        fp.write(b"%%EndComments\n")
+        fp.write(b"%%Page: 1 1\n")
+        fp.write(b"%%ImageData: %d %d " % im.size)
+        fp.write(b'%d %d 0 1 1 "%s"\n' % operator)
+
+    #
+    # image header
+    fp.write(b"gsave\n")
+    fp.write(b"10 dict begin\n")
+    fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1]))
+    fp.write(b"%d %d scale\n" % im.size)
+    fp.write(b"%d %d 8\n" % im.size)  # <= bits
+    fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1]))
+    fp.write(b"{ currentfile buf readhexstring pop } bind\n")
+    fp.write(operator[2] + b"\n")
+    if hasattr(fp, "flush"):
+        fp.flush()
+
+    ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)])
+
+    fp.write(b"\n%%%%EndBinary\n")
+    fp.write(b"grestore end\n")
+    if hasattr(fp, "flush"):
+        fp.flush()
 
 
 #
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -2135,6 +2135,11 @@ def save(self, fp, format=None, **params):
         elif isinstance(fp, Path):
             filename = str(fp)
             open_fp = True
+        elif fp == sys.stdout:
+            try:
+                fp = sys.stdout.buffer
+            except AttributeError:
+                pass
         if not filename and hasattr(fp, "name") and isPath(fp.name):
             # only set the name for metadata purposes
             filename = fp.name
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -493,7 +493,7 @@ 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
-    if fp == sys.stdout:
+    if fp == sys.stdout or (hasattr(sys.stdout, "buffer") and fp == sys.stdout.buffer):
         fp.flush()
         return
     try:
diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py
--- a/src/PIL/PSDraw.py
+++ b/src/PIL/PSDraw.py
@@ -26,39 +26,36 @@
 class PSDraw:
     """
     Sets up printing to the given file. If ``fp`` is omitted,
-    :py:data:`sys.stdout` is assumed.
+    ``sys.stdout.buffer`` or ``sys.stdout`` is assumed.
     """
 
     def __init__(self, fp=None):
         if not fp:
-            fp = sys.stdout
+            try:
+                fp = sys.stdout.buffer
+            except AttributeError:
+                fp = sys.stdout
         self.fp = fp
 
-    def _fp_write(self, to_write):
-        if self.fp == sys.stdout:
-            self.fp.write(to_write)
-        else:
-            self.fp.write(bytes(to_write, "UTF-8"))
-
     def begin_document(self, id=None):
         """Set up printing of a document. (Write PostScript DSC header.)"""
         # FIXME: incomplete
-        self._fp_write(
-            "%!PS-Adobe-3.0\n"
-            "save\n"
-            "/showpage { } def\n"
-            "%%EndComments\n"
-            "%%BeginDocument\n"
+        self.fp.write(
+            b"%!PS-Adobe-3.0\n"
+            b"save\n"
+            b"/showpage { } def\n"
+            b"%%EndComments\n"
+            b"%%BeginDocument\n"
         )
-        # self._fp_write(ERROR_PS)  # debugging!
-        self._fp_write(EDROFF_PS)
-        self._fp_write(VDI_PS)
-        self._fp_write("%%EndProlog\n")
+        # self.fp.write(ERROR_PS)  # debugging!
+        self.fp.write(EDROFF_PS)
+        self.fp.write(VDI_PS)
+        self.fp.write(b"%%EndProlog\n")
         self.isofont = {}
 
     def end_document(self):
         """Ends printing. (Write PostScript DSC footer.)"""
-        self._fp_write("%%EndDocument\nrestore showpage\n%%End\n")
+        self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n")
         if hasattr(self.fp, "flush"):
             self.fp.flush()
 
@@ -69,12 +66,13 @@ def setfont(self, font, size):
         :param font: A PostScript font name
         :param size: Size in points.
         """
+        font = bytes(font, "UTF-8")
         if font not in self.isofont:
             # reencode font
-            self._fp_write(f"/PSDraw-{font} ISOLatin1Encoding /{font} E\n")
+            self.fp.write(b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font, font))
             self.isofont[font] = 1
         # rough
-        self._fp_write(f"/F0 {size} /PSDraw-{font} F\n")
+        self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font))
 
     def line(self, xy0, xy1):
         """
@@ -82,7 +80,7 @@ def line(self, xy0, xy1):
         PostScript point coordinates (72 points per inch, (0, 0) is the lower
         left corner of the page).
         """
-        self._fp_write("%d %d %d %d Vl\n" % (*xy0, *xy1))
+        self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1))
 
     def rectangle(self, box):
         """
@@ -97,16 +95,18 @@ def rectangle(self, box):
 
                         %d %d M %d %d 0 Vr\n
         """
-        self._fp_write("%d %d M %d %d 0 Vr\n" % box)
+        self.fp.write(b"%d %d M %d %d 0 Vr\n" % box)
 
     def text(self, xy, text):
         """
         Draws text at the given position. You must use
         :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method.
         """
-        text = "\\(".join(text.split("("))
-        text = "\\)".join(text.split(")"))
-        self._fp_write(f"{xy[0]} {xy[1]} M ({text}) S\n")
+        text = bytes(text, "UTF-8")
+        text = b"\\(".join(text.split(b"("))
+        text = b"\\)".join(text.split(b")"))
+        xy += (text,)
+        self.fp.write(b"%d %d M (%s) S\n" % xy)
 
     def image(self, box, im, dpi=None):
         """Draw a PIL image, centered in the given box."""
@@ -130,14 +130,14 @@ def image(self, box, im, dpi=None):
             y = ymax
         dx = (xmax - x) / 2 + box[0]
         dy = (ymax - y) / 2 + box[1]
-        self._fp_write(f"gsave\n{dx:f} {dy:f} translate\n")
+        self.fp.write(b"gsave\n%f %f translate\n" % (dx, dy))
         if (x, y) != im.size:
             # EpsImagePlugin._save prints the image at (0,0,xsize,ysize)
             sx = x / im.size[0]
             sy = y / im.size[1]
-            self._fp_write(f"{sx:f} {sy:f} scale\n")
+            self.fp.write(b"%f %f scale\n" % (sx, sy))
         EpsImagePlugin._save(im, self.fp, None, 0)
-        self._fp_write("\ngrestore\n")
+        self.fp.write(b"\ngrestore\n")
 
 
 # --------------------------------------------------------------------
@@ -153,7 +153,7 @@ def image(self, box, im, dpi=None):
 #
 
 
-EDROFF_PS = """\
+EDROFF_PS = b"""\
 /S { show } bind def
 /P { moveto show } bind def
 /M { moveto } bind def
@@ -182,7 +182,7 @@ def image(self, box, im, dpi=None):
 # Copyright (c) Fredrik Lundh 1994.
 #
 
-VDI_PS = """\
+VDI_PS = b"""\
 /Vm { moveto } bind def
 /Va { newpath arcn stroke } bind def
 /Vl { moveto lineto stroke } bind def
@@ -207,7 +207,7 @@ def image(self, box, im, dpi=None):
 # 89-11-21 fl: created (pslist 1.10)
 #
 
-ERROR_PS = """\
+ERROR_PS = b"""\
 /landscape false def
 /errorBUF 200 string def
 /errorNL { currentpoint 10 sub exch pop 72 exch moveto } def

diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -1,4 +1,5 @@
 import re
+import sys
 import zlib
 from io import BytesIO
 
@@ -10,6 +11,7 @@
     PillowLeakTestCase,
     assert_image,
     assert_image_equal,
+    assert_image_equal_tofile,
     hopper,
     is_big_endian,
     is_win32,
@@ -711,6 +713,32 @@ def test_seek(self):
             with pytest.raises(EOFError):
                 im.seek(1)
 
+    @pytest.mark.parametrize("buffer", (True, False))
+    def test_save_stdout(self, buffer):
+        old_stdout = sys.stdout.buffer
+
+        if buffer:
+
+            class MyStdOut:
+                buffer = BytesIO()
+
+            mystdout = MyStdOut()
+        else:
+            mystdout = BytesIO()
+
+        sys.stdout = mystdout
+
+        with Image.open(TEST_PNG_FILE) as im:
+            im.save(sys.stdout, "PNG")
+
+        # Reset stdout
+        sys.stdout = old_stdout
+
+        if buffer:
+            mystdout = mystdout.buffer
+        reloaded = Image.open(mystdout)
+        assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
+
 
 @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
 @skip_unless_feature("zlib")
diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py
--- a/Tests/test_psdraw.py
+++ b/Tests/test_psdraw.py
@@ -1,6 +1,8 @@
 import os
 import sys
-from io import StringIO
+from io import BytesIO
+
+import pytest
 
 from PIL import Image, PSDraw
 
@@ -44,10 +46,21 @@ def test_draw_postscript(tmp_path):
     assert os.path.getsize(tempfile) > 0
 
 
-def test_stdout():
+@pytest.mark.parametrize("buffer", (True, False))
+def test_stdout(buffer):
     # Temporarily redirect stdout
-    old_stdout = sys.stdout
-    sys.stdout = mystdout = StringIO()
+    old_stdout = sys.stdout.buffer
+
+    if buffer:
+
+        class MyStdOut:
+            buffer = BytesIO()
+
+        mystdout = MyStdOut()
+    else:
+        mystdout = BytesIO()
+
+    sys.stdout = mystdout
 
     ps = PSDraw.PSDraw()
     _create_document(ps)
@@ -55,4 +68,6 @@ def test_stdout():
     # Reset stdout
     sys.stdout = old_stdout
 
-    assert mystdout.getvalue() != ""
+    if buffer:
+        mystdout = mystdout.buffer
+    assert mystdout.getvalue() != b""

Can't save PNG to sys.stdout
<!--
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? 
Just tried to run simple code (ImageDraw)

### What did you expect to happen?

### What actually happened? 
Exception looks very basic:
```  File "/data/user/0/ru.iiec.pydroid3/files/aarch64-linux-android/lib/python3.8/site-packages/PIL/Image.py", line 2164, in save
    save_handler(self, fp, filename)
  File "/data/user/0/ru.iiec.pydroid3/files/aarch64-linux-android/lib/python3.8/site-packages/PIL/PngImagePlugin.py", line 1225, in _save
    fp.write(_MAGIC)
TypeError: write() argument must be str, not bytes
```

### What are your OS, Python and Pillow versions?

* OS: Android app, "pydroid3" (I don't have a computer now to check it)
* Python: 3.8.3
* Pillow: 8.1.2

<!--
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, ImageDraw

# list of dots, in the following format: [x, y, x, y, x, y,...]
first = (146, 399, 163, 403, 170, 393, 169, 391, 166, 386, 170, 381, 170) #.... long tuple

with Image.open("draw image.jpg") as img:

    draw = ImageDraw.Draw(img)
    for i in range(len(first)//2 - 1):
    	draw.line(first[2*i : 2*i + 4])

    # write to stdout
    img.save(sys.stdout, "PNG")
```
Sorry if I didn't understand the concept... This is my first issue ever...

Hi. You're free to say that Pillow should be able to work with your code, and that you'd like it to be fixed in the next Pillow release - but if you'd like an immediate solution, I'd suggest changing `sys.stdout` to `sys.stdout.buffer`.

```python
import sys
from PIL import Image, ImageDraw

# list of dots, in the following format: [x, y, x, y, x, y,...]
first = (146, 399, 163, 403, 170, 393, 169, 391, 166, 386, 170, 381, 170) #.... long tuple

with Image.open("draw image.jpg") as img:

    draw = ImageDraw.Draw(img)
    for i in range(len(first)//2 - 1):
    	draw.line(first[2*i : 2*i + 4])

    # write to stdout
    img.save(sys.stdout.buffer, "PNG")
```

This makes a difference because while `sys.stdout` requires strings, `sys.stdout.buffer` uses bytes.

```pycon
>>> import sys
>>> sys.stdout.mode
'w'
>>> sys.stdout.buffer.mode
'wb'
```