Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace sys.stdout with sys.stdout.buffer when saving #5437

Merged
merged 5 commits into from
May 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Tests/test_file_png.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
import sys
import zlib
from io import BytesIO

Expand All @@ -10,6 +11,7 @@
PillowLeakTestCase,
assert_image,
assert_image_equal,
assert_image_equal_tofile,
hopper,
is_big_endian,
is_win32,
Expand Down Expand Up @@ -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")
Expand Down
25 changes: 20 additions & 5 deletions Tests/test_psdraw.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
import sys
from io import StringIO
from io import BytesIO

import pytest

from PIL import Image, PSDraw

Expand Down Expand Up @@ -44,15 +46,28 @@ 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)

# Reset stdout
sys.stdout = old_stdout

assert mystdout.getvalue() != ""
if buffer:
mystdout = mystdout.buffer
assert mystdout.getvalue() != b""
2 changes: 1 addition & 1 deletion docs/handbook/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ Drawing PostScript
title = "hopper"
box = (1*72, 2*72, 7*72, 10*72) # in points

ps = PSDraw.PSDraw() # default is sys.stdout
ps = PSDraw.PSDraw() # default is sys.stdout or sys.stdout.buffer
ps.begin_document(title)

# draw the image (75 dpi)
Expand Down
1 change: 1 addition & 0 deletions docs/reference/ImageDraw.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Example: Draw a gray cross over an image

.. code-block:: python

import sys
from PIL import Image, ImageDraw

with Image.open("hopper.jpg") as im:
Expand Down
78 changes: 34 additions & 44 deletions src/PIL/EpsImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


#
Expand Down
5 changes: 5 additions & 0 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/ImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
64 changes: 32 additions & 32 deletions src/PIL/PSDraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -69,20 +66,21 @@ 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):
"""
Draws a line between the two points. Coordinates are given in
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):
"""
Expand All @@ -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."""
Expand All @@ -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")


# --------------------------------------------------------------------
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down