With Python`s PIL, how to set DPI before loading an image?

Rockbot picture Rockbot · Feb 5, 2015 · Viewed 15.2k times · Source

I was trying to use PIL to open an (Illustrator) .eps-file, do some changes and save it. I want to set the document to 300 dpi, and color mode to cmyk before opening, creating or interpreting the object.

First I tried the same with PythonMagick and it worked like this:

import PythonMagick
# That's NOT what I want
img72 = PythonMagick.Image()
img_file = 'epstest.eps'
img.read(img_file)
img_dens = img72.density()
print 'W: %d, H: %d' % (img.size().width(), img.size().height())
# W: 403, H: 2475 <-- See here
print 'Density Width: %r' % img_dens.width() # 72
print 'Density Height: %r' % img_dens.height() # 72

# THAT is what I want
img300 = PythonMagick.Image()
img_file = 'epstest.eps'
img300.density('300')      # set density / resolution
img300.read(img_file)      # opens with defined density
img_dens = img300.density()
print 'W: %d, H: %d' % (img.size().width(), img.size().height())
# W: 1679, H: 10312 <-- See here!
print 'Density Width: %r' % img_dens.width() # 300
print 'Density Height: %r' % img_dens.height() # 300

Problem with PythonMagick: Converting Color Modes is not working, so i tried the same with PIL, which I would prefer:

from PIL import Image

img = Image.open('epstest.eps')

I know that it is possible to set dpi while saving.

Stuff that didn't work:

img = Image() # TypeError: 'module' object is not callable
img = Image.new() # TypeError: new() takes at least 2 arguments (0 given)
# .new() would create a different object anyway..
img = Image.open('epstest.eps', dpi = 300)
img = Image.open('epstest.eps', dpi = (300, 300) )
# After opening an Image
img.load(dpi=(300,300))

Regarding the input: My .eps file - if it is interpreted with 72dpi (seems to be PIL default) it ends up with 403x2475 px, with 300dpi it should be 1677x10311 px. Also, the .eps-file does not contain a preview-bitmap, nor any bitmap-data. Only 2 colors (black and white), plain vectors. It would be useful to make a catalogue of a huge amount of color-separated .eps-files.

Regarding the output: will be a png.

Solution:

Many thanks to Paulo - this is his solution with very small changes:

from PIL import Image
from PIL import EpsImagePlugin
import math
filename = 'epstest.eps'
def open_eps(filename, dpi=300.0):
    img = Image.open(filename)
    original = [float(d) for d in img.size]
    # scale = width / original[0] # calculated wrong height
    scale = dpi/72.0            # this fixed it
    if dpi is not 0:
        img.load(scale = math.ceil(scale))
    if scale != 1:
        img.thumbnail([round(scale * d) for d in original], Image.ANTIALIAS)
    return img

img = open_eps(filename, dpi=300.0)
img.save('pil_test.png', dpi=(300.0, 300.0))

Answer

Paulo Scardine picture Paulo Scardine · Feb 5, 2015

AFAIK, while it can contain embedded bitmaps and a preview thumbnail, EPS is a vector-based format. It only makes sense to set DPI if you are generating output in a bitmap format.

You are right - I am trying to generate a bitmap picture from the eps. But opening (parsing?) an .eps-file with a certain resolution determines the actual pixel-size (given a certain document size). PythonMagick does this right but i would like to use PIL if possible. – OP

That is because the EPS driver in PythonMagick converts the EPS to a bitmap representation on input (remember IM, the underlying library, is a 'raster image processor') - while in PIL the EPS driver can also write EPS images.

See "A word about Vector Image formats" in ImageMagick:

Why is this important? Because IM is a 'raster image processor', and while it can read or write images stored in one of the vector formats it does so by converting the image to and from a internal raster image. Consequently if you are trying to convert a image from a vector format, to another vector format, IM will essentially rasterize this image at the currently defined resolution or density which will hopefully (but unlikely) be suitable for the output device you intend to use it on. In other words, any output from IM will never be a true vector format. While it can convert its internal raster format into a vector format file, the result is only a superficial vector image wrapper around an image in raster format. And unless the raster image is defined properly (at the right resolution) for the output device, the result will not be particularly good. Unfortunately new uses to IM do not know anything about this. They see IM as a converter that can convert say PDF to Postscript, producing images with 'blocky' aliasing effects, 'washed out' colors, or blurry images that just do not look good at all, on the intended output device. Which brings use to what I am trying to say... Avoid using ImageMagick for 'Vector Image' to 'Vector Image' conversions EG: converting between formats like: PDF, PS, SVG In other words, use the right tool for the right job. And for this situation, ImageMagick is not the right tool.

See also the note about EPS on PIL:

PIL identifies EPS files containing image data, and can read files that contain embedded raster images (ImageData descriptors). If Ghostscript is available, other EPS files can be read as well. The EPS driver can also write EPS images.

[Update 1]

This information from Pillow docs is missing from the PIL docs:

If Ghostscript is available, you can call the load() method with the following parameter to affect how Ghostscript renders the EPS

scale

Affects the scale of the resultant rasterized image. If the EPS suggests that the image be rendered at 100px x 100px, setting this parameter to 2 will make the Ghostscript render a 200px x 200px image instead. The relative position of the bounding box is maintained:

im = Image.open(...)
im.size #(100,100)
im.load(scale=2)
im.size #(200,200)

[Update 2]

Contrary to my initial guess, PIL also rasterizes the image. When I saved as EPS it just made a wrapper around a bitmap. According to the OP the default resolution seems to be 72 ppi at his environment.

if you know the default resolution is 72 ppi (pixels per inch), calculating the scale for any density you want is a matter of simple proportion - given r as the resolution you want, s is the scale: 1 : s = 72 : r ergo:

im.load(scale=300.0/72.0)

May be it is best if you just specify the desired width instead of the resolution - for example if you want to have it 1677 pixels wide:

def open_eps(filename, width=None):
    original_width = float(Image.open(filename).size[0])
    im = Image.open(filename)
    if width is not None:
        im.load(scale=width/original_width)
    return im

im = open_eps('testfile.eps', 1677)

So the final answer is: although there is no built-in parameter to specify the desired resolution in ppi while loading an EPS file, you can use the scale parameter to load it at any resolution you want. If you care enough, I guess the Pillow maintainers would be glad to receive a PR for this.

[Edit 3]

Paolo, the way is good, but it looks like scale is only accepting plain integers... 4,166666667 (300.0/72.0) is rounded to 4.

Shame on me for not testing.

def open_eps(filename, width=None):
    original = [float(d) for d in Image.open(filename).size]
    scale = width / original[0]
    im = Image.open(filename)
    if width is not None:
        im.load(scale=math.ceil(scale))
    if scale != 1:
        im.thumbnail([int(scale * d) for d in original], Image.ANTIALIAS)
    return im

im = open_eps('testfile.eps', 1677)

Not sure if I should use math.round instead of int but you got the idea.