Merge pull request #5437 from radarhere/stdout

This commit is contained in:
Hugo van Kemenade 2021-05-03 11:57:46 +03:00 committed by GitHub
commit 20b8a83773
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 120 additions and 81 deletions

View File

@ -1,4 +1,5 @@
import re import re
import sys
import zlib import zlib
from io import BytesIO from io import BytesIO
@ -10,6 +11,7 @@ from .helper import (
PillowLeakTestCase, PillowLeakTestCase,
assert_image, assert_image,
assert_image_equal, assert_image_equal,
assert_image_equal_tofile,
hopper, hopper,
is_big_endian, is_big_endian,
is_win32, is_win32,
@ -711,6 +713,32 @@ class TestFilePng:
with pytest.raises(EOFError): with pytest.raises(EOFError):
im.seek(1) 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") @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
@skip_unless_feature("zlib") @skip_unless_feature("zlib")

View File

@ -1,6 +1,8 @@
import os import os
import sys import sys
from io import StringIO from io import BytesIO
import pytest
from PIL import Image, PSDraw from PIL import Image, PSDraw
@ -44,10 +46,21 @@ def test_draw_postscript(tmp_path):
assert os.path.getsize(tempfile) > 0 assert os.path.getsize(tempfile) > 0
def test_stdout(): @pytest.mark.parametrize("buffer", (True, False))
def test_stdout(buffer):
# Temporarily redirect stdout # Temporarily redirect stdout
old_stdout = sys.stdout old_stdout = sys.stdout.buffer
sys.stdout = mystdout = StringIO()
if buffer:
class MyStdOut:
buffer = BytesIO()
mystdout = MyStdOut()
else:
mystdout = BytesIO()
sys.stdout = mystdout
ps = PSDraw.PSDraw() ps = PSDraw.PSDraw()
_create_document(ps) _create_document(ps)
@ -55,4 +68,6 @@ def test_stdout():
# Reset stdout # Reset stdout
sys.stdout = old_stdout sys.stdout = old_stdout
assert mystdout.getvalue() != "" if buffer:
mystdout = mystdout.buffer
assert mystdout.getvalue() != b""

View File

@ -424,7 +424,7 @@ Drawing PostScript
title = "hopper" title = "hopper"
box = (1*72, 2*72, 7*72, 10*72) # in points 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) ps.begin_document(title)
# draw the image (75 dpi) # draw the image (75 dpi)

View File

@ -18,6 +18,7 @@ Example: Draw a gray cross over an image
.. code-block:: python .. code-block:: python
import sys
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
with Image.open("hopper.jpg") as im: with Image.open("hopper.jpg") as im:

View File

@ -354,56 +354,46 @@ def _save(im, fp, filename, eps=1):
# #
# determine PostScript image mode # determine PostScript image mode
if im.mode == "L": if im.mode == "L":
operator = (8, 1, "image") operator = (8, 1, b"image")
elif im.mode == "RGB": elif im.mode == "RGB":
operator = (8, 3, "false 3 colorimage") operator = (8, 3, b"false 3 colorimage")
elif im.mode == "CMYK": elif im.mode == "CMYK":
operator = (8, 4, "false 4 colorimage") operator = (8, 4, b"false 4 colorimage")
else: else:
raise ValueError("image mode is not supported") raise ValueError("image mode is not supported")
base_fp = fp if eps:
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)
# #
# image header # write EPS header
fp.write("gsave\n") fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n")
fp.write("10 dict begin\n") fp.write(b"%%Creator: PIL 0.1 EpsEncode\n")
fp.write(f"/buf {im.size[0] * operator[1]} string def\n") # fp.write("%%CreationDate: %s"...)
fp.write("%d %d scale\n" % im.size) fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size)
fp.write("%d %d 8\n" % im.size) # <= bits fp.write(b"%%Pages: 1\n")
fp.write(f"[{im.size[0]} 0 0 -{im.size[1]} 0 {im.size[1]}]\n") fp.write(b"%%EndComments\n")
fp.write("{ currentfile buf readhexstring pop } bind\n") fp.write(b"%%Page: 1 1\n")
fp.write(operator[2] + "\n") fp.write(b"%%ImageData: %d %d " % im.size)
if hasattr(fp, "flush"): fp.write(b'%d %d 0 1 1 "%s"\n' % operator)
fp.flush()
ImageFile._save(im, base_fp, [("eps", (0, 0) + im.size, 0, None)]) #
# 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()
fp.write("\n%%%%EndBinary\n") ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)])
fp.write("grestore end\n")
if hasattr(fp, "flush"): fp.write(b"\n%%%%EndBinary\n")
fp.flush() fp.write(b"grestore end\n")
finally: if hasattr(fp, "flush"):
if wrapped_fp: fp.flush()
fp.detach()
# #

View File

@ -2135,6 +2135,11 @@ class Image:
elif isinstance(fp, Path): elif isinstance(fp, Path):
filename = str(fp) filename = str(fp)
open_fp = True 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): if not filename and hasattr(fp, "name") and isPath(fp.name):
# only set the name for metadata purposes # only set the name for metadata purposes
filename = fp.name filename = fp.name

View File

@ -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 # But, it would need at least the image size in most cases. RawEncode is
# a tricky case. # a tricky case.
bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c 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() fp.flush()
return return
try: try:

View File

@ -26,39 +26,36 @@ from . import EpsImagePlugin
class PSDraw: class PSDraw:
""" """
Sets up printing to the given file. If ``fp`` is omitted, 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): def __init__(self, fp=None):
if not fp: if not fp:
fp = sys.stdout try:
fp = sys.stdout.buffer
except AttributeError:
fp = sys.stdout
self.fp = fp 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): def begin_document(self, id=None):
"""Set up printing of a document. (Write PostScript DSC header.)""" """Set up printing of a document. (Write PostScript DSC header.)"""
# FIXME: incomplete # FIXME: incomplete
self._fp_write( self.fp.write(
"%!PS-Adobe-3.0\n" b"%!PS-Adobe-3.0\n"
"save\n" b"save\n"
"/showpage { } def\n" b"/showpage { } def\n"
"%%EndComments\n" b"%%EndComments\n"
"%%BeginDocument\n" b"%%BeginDocument\n"
) )
# self._fp_write(ERROR_PS) # debugging! # self.fp.write(ERROR_PS) # debugging!
self._fp_write(EDROFF_PS) self.fp.write(EDROFF_PS)
self._fp_write(VDI_PS) self.fp.write(VDI_PS)
self._fp_write("%%EndProlog\n") self.fp.write(b"%%EndProlog\n")
self.isofont = {} self.isofont = {}
def end_document(self): def end_document(self):
"""Ends printing. (Write PostScript DSC footer.)""" """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"): if hasattr(self.fp, "flush"):
self.fp.flush() self.fp.flush()
@ -69,12 +66,13 @@ class PSDraw:
:param font: A PostScript font name :param font: A PostScript font name
:param size: Size in points. :param size: Size in points.
""" """
font = bytes(font, "UTF-8")
if font not in self.isofont: if font not in self.isofont:
# reencode font # 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 self.isofont[font] = 1
# rough # 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): def line(self, xy0, xy1):
""" """
@ -82,7 +80,7 @@ class PSDraw:
PostScript point coordinates (72 points per inch, (0, 0) is the lower PostScript point coordinates (72 points per inch, (0, 0) is the lower
left corner of the page). 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): def rectangle(self, box):
""" """
@ -97,16 +95,18 @@ class PSDraw:
%d %d M %d %d 0 Vr\n %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): def text(self, xy, text):
""" """
Draws text at the given position. You must use Draws text at the given position. You must use
:py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method.
""" """
text = "\\(".join(text.split("(")) text = bytes(text, "UTF-8")
text = "\\)".join(text.split(")")) text = b"\\(".join(text.split(b"("))
self._fp_write(f"{xy[0]} {xy[1]} M ({text}) S\n") 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): def image(self, box, im, dpi=None):
"""Draw a PIL image, centered in the given box.""" """Draw a PIL image, centered in the given box."""
@ -130,14 +130,14 @@ class PSDraw:
y = ymax y = ymax
dx = (xmax - x) / 2 + box[0] dx = (xmax - x) / 2 + box[0]
dy = (ymax - y) / 2 + box[1] 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: if (x, y) != im.size:
# EpsImagePlugin._save prints the image at (0,0,xsize,ysize) # EpsImagePlugin._save prints the image at (0,0,xsize,ysize)
sx = x / im.size[0] sx = x / im.size[0]
sy = y / im.size[1] 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) EpsImagePlugin._save(im, self.fp, None, 0)
self._fp_write("\ngrestore\n") self.fp.write(b"\ngrestore\n")
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -153,7 +153,7 @@ class PSDraw:
# #
EDROFF_PS = """\ EDROFF_PS = b"""\
/S { show } bind def /S { show } bind def
/P { moveto show } bind def /P { moveto show } bind def
/M { moveto } bind def /M { moveto } bind def
@ -182,7 +182,7 @@ EDROFF_PS = """\
# Copyright (c) Fredrik Lundh 1994. # Copyright (c) Fredrik Lundh 1994.
# #
VDI_PS = """\ VDI_PS = b"""\
/Vm { moveto } bind def /Vm { moveto } bind def
/Va { newpath arcn stroke } bind def /Va { newpath arcn stroke } bind def
/Vl { moveto lineto stroke } bind def /Vl { moveto lineto stroke } bind def
@ -207,7 +207,7 @@ VDI_PS = """\
# 89-11-21 fl: created (pslist 1.10) # 89-11-21 fl: created (pslist 1.10)
# #
ERROR_PS = """\ ERROR_PS = b"""\
/landscape false def /landscape false def
/errorBUF 200 string def /errorBUF 200 string def
/errorNL { currentpoint 10 sub exch pop 72 exch moveto } def /errorNL { currentpoint 10 sub exch pop 72 exch moveto } def