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.

epscalwi.py
#! /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
_images/2017_07.jpg