PSDraw Module, EpsImagePlugin Module¶
- doc
https://pillow.readthedocs.io/en/latest/reference/PSDraw.html, https://pillow.readthedocs.io/en/latest/reference/plugins.html#module-PIL.EpsImagePlugin, https://pillow.readthedocs.io/en/latest/handbook/image-file-formats.html#eps http://effbot.org/imagingbook/psdraw.htm, http://effbot.org/imagingbook/format-eps.htm
Note
All source images in this document are derived from https://www.pexels.com (CC0 License).
EpsImagePlugin basics¶
Saving image as EPS is easy:
>>> from PIL import Image
>>> img = Image.open("data/srcimg01.jpg")
>>> img.save("result/fromsrcimg01.eps")
>>> open("result/fromsrcimg01.eps").readlines()[:3]
['%!PS-Adobe-3.0 EPSF-3.0\n', '%%Creator: PIL 0.1 EpsEncode\n', '%%BoundingBox: 0 0 670 445\n']
Actually, this capability is provided by PIL.EpsImagePlugin.
Now, let us try to open EPS file and load:
>>> img2 = Image.open("result/fromsrcimg01.eps")
>>> img2
<PIL.EpsImagePlugin.EpsImageFile image mode=RGB size=670x445 at 0x2FCAD68>
>>> img2.load()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "c:\Python27\lib\site-packages\PIL\EpsImagePlugin.py", line 336, in load
self.im = Ghostscript(self.tile, self.size, self.fp, scale)
File "c:\Python27\lib\site-packages\PIL\EpsImagePlugin.py", line 137, in Ghostscript
raise WindowsError('Unable to locate Ghostscript on paths')
WindowsError: Unable to locate Ghostscript on paths
If you encounter the same error, you have to install GhostScript. PIL.EpsImagePlugin sometimes invokes ghostscript as needed.
EpsImagePlugin is basically a backroom boy for most of end-users, but using this module directly is sometimes useful for us. For example, if we need to know BoundingBox of PostScript (PS), we can use a knowledge of GhostScript command which EpsImagePlugin has.:
>>> # "xxxxx.ps" has no '%%%%BoundingBox'
... open("xxxxx.ps").readlines()[:5]
['%!PS-Adobe-3.0\n', 'save\n', '/showpage { } def\n', '%%EndComments\n', '%%BeginDocument\n']
>>> #
... import subprocess
>>> from PIL import EpsImagePlugin
>>> gs_binary = EpsImagePlugin.gs_windows_binary
>>> gs_binary = ("gs" if not gs_binary else gs_binary)
>>> gs_binary
'gswin64c'
>>> command = [gs_binary, "-dBATCH", "-dNOPAUSE", "-sDEVICE=bbox", "-q"]
>>> command.append("xxxxx.ps")
>>> gs_output = subprocess.check_output(command, stderr=subprocess.STDOUT)
>>> gs_output.strip().split("\n")
['%%BoundingBox: 0 222 596 620', '%%HiResBoundingBox: 0.000000 222.137993 595.439982 619.541981']
PSDraw basics¶
According to the official documents:
The PSDraw module provides simple print support for Postscript printers. You can print text, graphics and images through this module.
Actually, the PSDraw takes very minimal approach to provide this Postscript support, in other words, you may have to do much work to build the complex PostScript. For example, there is no support to calculate proper position to place texts, etc. If you are not familiar with PostScript, I do not recommend to use this module. If you just want to save image as EPS, see EpsImagePlugin basics.
If you are familiar with PostScript, using the PSDraw should be very easy to you. PSDraw is a builder to PostScript documents:
>>> from PIL import PSDraw
>>> psd = PSDraw.PSDraw() # file=sys.stdout
>>> psd.begin_document()
%!PS-Adobe-3.0
save
/showpage { } def
%%EndComments
%%BeginDocument
/S { show } bind def
/P { moveto show } bind def
/M { moveto } bind def
/X { 0 rmoveto } bind def
/Y { 0 exch rmoveto } bind def
/E { findfont
dup maxlength dict begin
{
1 index /FID ne { def } { pop pop } ifelse
} forall
/Encoding exch def
dup /FontName exch def
currentdict end definefont pop
} bind def
/F { findfont exch scalefont dup setfont
[ exch /setfont cvx ] cvx bind def
} bind def
/Vm { moveto } bind def
/Va { newpath arcn stroke } bind def
/Vl { moveto lineto stroke } bind def
/Vc { newpath 0 360 arc closepath } bind def
/Vr { exch dup 0 rlineto
exch dup neg 0 exch rlineto
exch neg 0 rlineto
0 exch rlineto
100 div setgray fill 0 setgray } bind def
/Tm matrix def
/Ve { Tm currentmatrix pop
translate scale newpath 0 0 .5 0 360 arc closepath
Tm setmatrix
} bind def
/Vf { currentgray exch setgray fill setgray } bind def
%%EndProlog
>>> #
>>> psd.setfont("Courier", 24)
/PSDraw-Courier ISOLatin1Encoding /Courier E
/F0 24 /PSDraw-Courier F
>>> psd.text((0, 0), "Draws text at (0, 0).")
0 0 M (Draws text at \(0, 0\).) S
>>> psd.end_document()
%%EndDocument
restore showpage
%%End
writing PS with PSDraw, and converting it to EPS¶
At first, see below example:
from PIL import Image, PSDraw
# A4: 8.27 x 11.69 inches
# => 595, 834 pixels if dpi=72
psd = PSDraw.PSDraw(open("result/fromsrcimg22.ps", "wb"))
psd.begin_document()
img = Image.open("data/srcimg22.jpg")
img = img.rotate(90).resize((595, 834))
psd.image((0, 0, 595, 834), img, dpi=72)
psd.end_document()
This example seems somewhat nonsence. Because if you just want to save image as PostScript (family), you can simply save it with extention “.eps”. Still you want to do so, maybe you want to build more complex PostScript document, and I assume you need so.
I’ll continue the story.
The PostScript file result/fromsrcimg22.ps
generated by above code is not EPS but PS, so even if
you change extention “.ps” to “.eps”, you can’t load as PIL.Image:
>>> from PIL import Image
>>> im = Image.open("result/fromsrcimg22.eps") # chenged extention, but not EPS
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "c:\Python27\lib\site-packages\PIL\Image.py", line 2321, in open
im = factory(fp, filename)
File "c:\Python27\lib\site-packages\PIL\ImageFile.py", line 97, in __init__
self._open()
File "c:\Python27\lib\site-packages\PIL\EpsImagePlugin.py", line 308, in _open
raise IOError("cannot determine EPS bounding box")
IOError: cannot determine EPS bounding box
Andrew T. Young concluded “the absolute minimum required to get some other program to buy a PS file as EPS is”:
Most of the time, all you need is the %%BoundingBox:
I agree with him, I also think thumbnail is unnecessary.
Unfortunately, the PSDraw itself have no support to decide %%BoundingBox, but with invoking GhostScript, we can detect %%BoundingBox of PostScript document:
from io import BytesIO
from PIL import Image, PSDraw
def get_boundingbox(psdoc):
import os, subprocess, tempfile
from PIL import EpsImagePlugin
t_fd, tfile = tempfile.mkstemp()
os.close(t_fd)
open(tfile, "w").write(psdoc)
gs_binary = EpsImagePlugin.gs_windows_binary
gs_binary = ("gs" if not gs_binary else gs_binary)
command = [gs_binary, "-dBATCH", "-dNOPAUSE", "-sDEVICE=bbox", "-q", tfile]
gs_output = subprocess.check_output(
command, stderr=subprocess.STDOUT)
return gs_output.decode("us-ascii").strip().split("\n")
# A4: 8.27 x 11.69 inches
# => 595, 834 pixels if dpi=72
out = BytesIO()
psd = PSDraw.PSDraw(out)
psd.begin_document()
img = Image.open("data/srcimg22.jpg")
img = img.rotate(90).resize((595, 834))
psd.image((0, 0, 595, 834), img, dpi=72)
psd.end_document()
result = out.getvalue().decode("us-ascii")
# append BB to second line
bb = get_boundingbox(result) # get BB from GS
s1, _, s2 = result.partition("\n")
newpsdoc = s1 + _ + bb[0] + _ + s2
open("result/fromsrcimg22.eps", "w").write(newpsdoc)
Now result/fromsrcimg22.eps
can be loaded as PIL.Image:
>>> from PIL import Image
>>> im = Image.open("result/fromsrcimg22.eps")
>>> im.info
{'BoundingBox': '0 0 596 835', 'PS-Adobe': '3.0'}
>>> im.show()
More complex example (pscal wrapper)¶
This demo script is a tiny wrapper to pscal.
#! /bin/env python
# -*- coding: utf-8 -*-
# epscalwi is a wrapper to pscal (http://www.panix.com/~mbh/projects.html).
#
# This script is a part of
# `Pillow (PIL) examples <https://hhsprings.bitbucket.io/docs/programming/examples/python/PIL/>`_,
# so the main purpose of this script is a simple demonstration of Pillow (PSDraw, and EPSImagePlugin).
# Don't complain its incompleteness :)
#
#
from __future__ import unicode_literals
import sys
import subprocess
from io import BytesIO
from PIL import Image
VERSION = '0.2'
# from ver 0.1 to 0.2: accept no photo image.
def _get_boundingbox(psdoc):
import os, subprocess, tempfile
from PIL import EpsImagePlugin
t_fd, tfile = tempfile.mkstemp()
os.close(t_fd)
open(tfile, "w").write(psdoc)
gs_binary = EpsImagePlugin.gs_windows_binary
gs_binary = ("gs" if not gs_binary else gs_binary)
command = [gs_binary, "-dBATCH", "-dNOPAUSE", "-sDEVICE=bbox", "-q", tfile]
gs_output = subprocess.check_output(
command, stderr=subprocess.STDOUT)
return gs_output.decode("us-ascii").strip().split("\n")
def _call_pscal(args):
arglist = ["-d", args.d, "-L", "English"]
if args.M:
arglist.append("-M")
if args.m:
arglist.append("-m")
if args.n:
arglist.append("-n")
if args.s:
arglist.append("-s")
else:
arglist.append("-S")
if len(args.month_year):
arglist.extend(args.month_year[:2])
if sys.platform == 'win32':
# please install Unix-like environment like MSYS, or cygwin,
# and place pscal and this script in the same directory.
command = ["sh", "pscal"] + list(map(str, arglist))
else:
command = ["pscal"] + list(map(str, arglist))
return subprocess.check_output(command)
def _build_ps(args, outfp=sys.stdout):
from PIL import PSDraw, ImageOps
resample = Image.BICUBIC
#
no_photo = (args.photo_image == ":devnull:")
# A4 portrait: 8.27 x 11.69 inches
# => 595, 834 pixels if dpi=72
dpi_fac = 1 # for understanding dpi, so this has no meaning for me.
# load the output of pscal to PIL.Image.
# NOTE: Because original image from pscal is scalable, so it's beautiful,
# But once it is loaded as raster, it will be dirty...
# Ideally, eps from pscal should be inserted directly, maybe.
# But we can try to do render a little better, with scale option
# of load, and resample option of various transform methods.
pscalimg = Image.open(BytesIO(_call_pscal(args)))
pscalimg.load(scale=12) # for hi-res (warn: large scale needs much memory)
pscalimg = pscalimg.rotate(-90, resample=resample, expand=True)
pscalimg = pscalimg.resize(
(834 * dpi_fac, 1190 // 2 * dpi_fac), resample=resample)
#
psd = PSDraw.PSDraw(outfp)
psd.begin_document()
# outer box (to avoid crop by ghostscript)
height = 1190 if not no_photo else 1190 // 2
psd.line((0, 0), (834, 0))
psd.line((834, 0), (834, height))
psd.line((834, height), (0, height))
psd.line((0, height), (0, 0))
# photo to top, pscal to bottom
mx, my = 20, 1
psd.image(
(mx, my, (834 - mx), (1190 // 2 - my)),
pscalimg, dpi=72 * dpi_fac)
if not no_photo:
photoimg = Image.open(args.photo_image)
photoimg = photoimg.resize(
(834 * dpi_fac, 1190 // 2 * dpi_fac), resample=resample)
psd.image((mx, 1190 // 2 + my, 834 - mx, 1190 - my),
photoimg, dpi=72 * dpi_fac)
psd.end_document()
def main(args):
import os
dn = os.path.dirname(args.out_file)
fn = os.path.basename(args.out_file)
bn, ext = os.path.splitext(fn)
#
out = BytesIO()
_build_ps(args, out)
result = out.getvalue().decode("us-ascii")
#
if ext != ".ps":
# append BB to second line
bb = _get_boundingbox(result) # get BB from GS
s1, _, s2 = result.partition("\n")
result = s1 + _ + bb[0] + _ + s2
#
if ext in (".ps", ".eps"):
open(fn, "w").write(result)
else:
cnv = Image.open(BytesIO(result.encode("us-ascii")))
cnv.save(fn)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
"-d", type=int, default=13, help="moon diameter (default = 13)")
parser.add_argument(
"-M", action="store_true", help="show moon phases (southern hemisphere)")
parser.add_argument(
"-m", action="store_true", help="show moon phases (northern hemisphere)")
parser.add_argument(
"-n", action="store_true", help="show day numbering")
parser.add_argument(
"-S", action="store_false", help="European style (Monday first)", dest="s")
parser.add_argument(
"-s", action="store_true", help="American style (Sunday first)")
parser.add_argument(
"photo_image", help="""\
photo image (if you don't want this, specify :devnull:)""") # args for epscalwi
parser.add_argument(
"out_file", help="output file name") # args for epscalwi
parser.add_argument(
"month_year", type=int, nargs="*", help="'month year' or 'month'")
#
main(parser.parse_args())
Usage:
me@host: ~$ epscalwi.py -h
usage: epscalwi.py [-h] [-d D] [-M] [-m] [-n] [-S] [-s]
photo_image out_file [month_year [month_year ...]]
positional arguments:
photo_image photo image (if you don't want this, specify :devnull:)
out_file output file name
month_year 'month year' or 'month'
optional arguments:
-h, --help show this help message and exit
-d D moon diameter (default = 13)
-M show moon phases (southern hemisphere)
-m show moon phases (northern hemisphere)
-n show day numbering
-S European style (Monday first)
-s American style (Sunday first)
me@host: ~$ epscalwi.py -m data/srcimg22.jpg result/2017_07.ps # => ps
me@host: ~$ epscalwi.py -m data/srcimg22.jpg result/2017_07.eps # => eps
me@host: ~$ epscalwi.py -m data/srcimg22.jpg result/2017_07.jpg # => jpg
me@host: ~$ epscalwi.py -m data/srcimg22.jpg result/2019_03.jpg 6 2019 # of June, 2019
me@host: ~$ epscalwi.py -m :devnull: result/2019_03.jpg 6 2019 # no photo