Merge pull request #667 from wiredfool/hugovk_pdf

PDF Tests and Fixes merge
This commit is contained in:
wiredfool 2014-05-20 17:18:34 +01:00
commit 97ee7938cb
3 changed files with 202 additions and 94 deletions

View File

@ -30,6 +30,7 @@ from PIL import VERSION, PILLOW_VERSION, _plugins
import warnings import warnings
class _imaging_not_installed: class _imaging_not_installed:
# module placeholder # module placeholder
def __getattr__(self, id): def __getattr__(self, id):
@ -52,7 +53,7 @@ try:
# directly; import Image and use the Image.core variable instead. # directly; import Image and use the Image.core variable instead.
from PIL import _imaging as core from PIL import _imaging as core
if PILLOW_VERSION != getattr(core, 'PILLOW_VERSION', None): if PILLOW_VERSION != getattr(core, 'PILLOW_VERSION', None):
raise ImportError("The _imaging extension was built for another " raise ImportError("The _imaging extension was built for another "
" version of Pillow or PIL") " version of Pillow or PIL")
except ImportError as v: except ImportError as v:
@ -91,10 +92,13 @@ except ImportError:
builtins = __builtin__ builtins = __builtin__
from PIL import ImageMode from PIL import ImageMode
from PIL._binary import i8, o8 from PIL._binary import i8
from PIL._util import isPath, isStringType, deferred_error from PIL._util import isPath
from PIL._util import isStringType
from PIL._util import deferred_error
import os, sys import os
import sys
# type stuff # type stuff
import collections import collections
@ -104,9 +108,10 @@ import numbers
USE_CFFI_ACCESS = hasattr(sys, 'pypy_version_info') USE_CFFI_ACCESS = hasattr(sys, 'pypy_version_info')
try: try:
import cffi import cffi
HAS_CFFI=True HAS_CFFI = True
except: except:
HAS_CFFI=False HAS_CFFI = False
def isImageType(t): def isImageType(t):
""" """
@ -148,16 +153,16 @@ MESH = 4
# resampling filters # resampling filters
NONE = 0 NONE = 0
NEAREST = 0 NEAREST = 0
ANTIALIAS = 1 # 3-lobed lanczos ANTIALIAS = 1 # 3-lobed lanczos
LINEAR = BILINEAR = 2 LINEAR = BILINEAR = 2
CUBIC = BICUBIC = 3 CUBIC = BICUBIC = 3
# dithers # dithers
NONE = 0 NONE = 0
NEAREST = 0 NEAREST = 0
ORDERED = 1 # Not yet implemented ORDERED = 1 # Not yet implemented
RASTERIZE = 2 # Not yet implemented RASTERIZE = 2 # Not yet implemented
FLOYDSTEINBERG = 3 # default FLOYDSTEINBERG = 3 # default
# palettes/quantizers # palettes/quantizers
WEB = 0 WEB = 0
@ -222,7 +227,7 @@ else:
_MODE_CONV = { _MODE_CONV = {
# official modes # official modes
"1": ('|b1', None), # broken "1": ('|b1', None), # broken
"L": ('|u1', None), "L": ('|u1', None),
"I": (_ENDIAN + 'i4', None), "I": (_ENDIAN + 'i4', None),
"F": (_ENDIAN + 'f4', None), "F": (_ENDIAN + 'f4', None),
@ -232,8 +237,8 @@ _MODE_CONV = {
"RGBA": ('|u1', 4), "RGBA": ('|u1', 4),
"CMYK": ('|u1', 4), "CMYK": ('|u1', 4),
"YCbCr": ('|u1', 3), "YCbCr": ('|u1', 3),
"LAB": ('|u1', 3), # UNDONE - unsigned |u1i1i1 "LAB": ('|u1', 3), # UNDONE - unsigned |u1i1i1
# I;16 == I;16L, and I;32 == I;32L # I;16 == I;16L, and I;32 == I;32L
"I;16": ('<u2', None), "I;16": ('<u2', None),
"I;16B": ('>u2', None), "I;16B": ('>u2', None),
"I;16L": ('<u2', None), "I;16L": ('<u2', None),
@ -248,6 +253,7 @@ _MODE_CONV = {
"I;32LS": ('<i4', None), "I;32LS": ('<i4', None),
} }
def _conv_type_shape(im): def _conv_type_shape(im):
shape = im.size[1], im.size[0] shape = im.size[1], im.size[0]
typ, extra = _MODE_CONV[im.mode] typ, extra = _MODE_CONV[im.mode]
@ -368,8 +374,8 @@ def init():
for plugin in _plugins: for plugin in _plugins:
try: try:
if DEBUG: if DEBUG:
print ("Importing %s"%plugin) print ("Importing %s" % plugin)
__import__("PIL.%s"%plugin, globals(), locals(), []) __import__("PIL.%s" % plugin, globals(), locals(), [])
except ImportError: except ImportError:
if DEBUG: if DEBUG:
print("Image: failed to import", end=' ') print("Image: failed to import", end=' ')
@ -379,6 +385,7 @@ def init():
_initialized = 2 _initialized = 2
return 1 return 1
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Codec factories (used by tobytes/frombytes and ImageFile.load) # Codec factories (used by tobytes/frombytes and ImageFile.load)
@ -398,6 +405,7 @@ def _getdecoder(mode, decoder_name, args, extra=()):
except AttributeError: except AttributeError:
raise IOError("decoder %s not available" % decoder_name) raise IOError("decoder %s not available" % decoder_name)
def _getencoder(mode, encoder_name, args, extra=()): def _getencoder(mode, encoder_name, args, extra=()):
# tweak arguments # tweak arguments
@ -421,30 +429,36 @@ def _getencoder(mode, encoder_name, args, extra=()):
def coerce_e(value): def coerce_e(value):
return value if isinstance(value, _E) else _E(value) return value if isinstance(value, _E) else _E(value)
class _E: class _E:
def __init__(self, data): def __init__(self, data):
self.data = data self.data = data
def __add__(self, other): def __add__(self, other):
return _E((self.data, "__add__", coerce_e(other).data)) return _E((self.data, "__add__", coerce_e(other).data))
def __mul__(self, other): def __mul__(self, other):
return _E((self.data, "__mul__", coerce_e(other).data)) return _E((self.data, "__mul__", coerce_e(other).data))
def _getscaleoffset(expr): def _getscaleoffset(expr):
stub = ["stub"] stub = ["stub"]
data = expr(_E(stub)).data data = expr(_E(stub)).data
try: try:
(a, b, c) = data # simplified syntax (a, b, c) = data # simplified syntax
if (a is stub and b == "__mul__" and isinstance(c, numbers.Number)): if (a is stub and b == "__mul__" and isinstance(c, numbers.Number)):
return c, 0.0 return c, 0.0
if (a is stub and b == "__add__" and isinstance(c, numbers.Number)): if (a is stub and b == "__add__" and isinstance(c, numbers.Number)):
return 1.0, c return 1.0, c
except TypeError: pass except TypeError:
pass
try: try:
((a, b, c), d, e) = data # full syntax ((a, b, c), d, e) = data # full syntax
if (a is stub and b == "__mul__" and isinstance(c, numbers.Number) and if (a is stub and b == "__mul__" and isinstance(c, numbers.Number) and
d == "__add__" and isinstance(e, numbers.Number)): d == "__add__" and isinstance(e, numbers.Number)):
return c, e return c, e
except TypeError: pass except TypeError:
pass
raise ValueError("illegal expression") raise ValueError("illegal expression")
@ -495,11 +509,12 @@ class Image:
new.info[k] = v new.info[k] = v
return new return new
_makeself = _new # compatibility _makeself = _new # compatibility
# Context Manager Support # Context Manager Support
def __enter__(self): def __enter__(self):
return self return self
def __exit__(self, *args): def __exit__(self, *args):
self.close() self.close()
@ -518,14 +533,13 @@ class Image:
self.fp.close() self.fp.close()
except Exception as msg: except Exception as msg:
if Image.DEBUG: if Image.DEBUG:
print ("Error closing: %s" %msg) print ("Error closing: %s" % msg)
# Instead of simply setting to None, we're setting up a # Instead of simply setting to None, we're setting up a
# deferred error that will better explain that the core image # deferred error that will better explain that the core image
# object is gone. # object is gone.
self.im = deferred_error(ValueError("Operation on closed image")) self.im = deferred_error(ValueError("Operation on closed image"))
def _copy(self): def _copy(self):
self.load() self.load()
self.im = self.im.copy() self.im = self.im.copy()
@ -533,7 +547,8 @@ class Image:
self.readonly = 0 self.readonly = 0
def _dump(self, file=None, format=None): def _dump(self, file=None, format=None):
import tempfile, os import os
import tempfile
suffix = '' suffix = ''
if format: if format:
suffix = '.'+format suffix = '.'+format
@ -591,7 +606,7 @@ class Image:
e = _getencoder(self.mode, encoder_name, args) e = _getencoder(self.mode, encoder_name, args)
e.setimage(self.im) e.setimage(self.im)
bufsize = max(65536, self.size[0] * 4) # see RawEncode.c bufsize = max(65536, self.size[0] * 4) # see RawEncode.c
data = [] data = []
while True: while True:
@ -663,7 +678,9 @@ class Image:
.. deprecated:: 2.0 .. deprecated:: 2.0
""" """
warnings.warn('fromstring() is deprecated. Please call frombytes() instead.', DeprecationWarning) warnings.warn(
'fromstring() is deprecated. Please call frombytes() instead.',
DeprecationWarning)
return self.frombytes(*args, **kw) return self.frombytes(*args, **kw)
def load(self): def load(self):
@ -779,29 +796,30 @@ class Image:
# Use transparent conversion to promote from transparent # Use transparent conversion to promote from transparent
# color to an alpha channel. # color to an alpha channel.
return self._new(self.im.convert_transparent( return self._new(self.im.convert_transparent(
mode, self.info['transparency'])) mode, self.info['transparency']))
elif self.mode in ('L', 'RGB', 'P') and mode in ('L', 'RGB', 'P'): elif self.mode in ('L', 'RGB', 'P') and mode in ('L', 'RGB', 'P'):
t = self.info['transparency'] t = self.info['transparency']
if isinstance(t, bytes): if isinstance(t, bytes):
# Dragons. This can't be represented by a single color # Dragons. This can't be represented by a single color
warnings.warn('Palette images with Transparency expressed '+ warnings.warn('Palette images with Transparency expressed ' +
' in bytes should be converted to RGBA images') ' in bytes should be converted to RGBA images')
delete_trns = True delete_trns = True
else: else:
# get the new transparency color. # get the new transparency color.
# use existing conversions # use existing conversions
trns_im = Image()._new(core.new(self.mode, (1,1))) trns_im = Image()._new(core.new(self.mode, (1, 1)))
if self.mode == 'P': if self.mode == 'P':
trns_im.putpalette(self.palette) trns_im.putpalette(self.palette)
trns_im.putpixel((0,0), t) trns_im.putpixel((0, 0), t)
if mode in ('L','RGB'): if mode in ('L', 'RGB'):
trns_im = trns_im.convert(mode) trns_im = trns_im.convert(mode)
else: else:
# can't just retrieve the palette number, got to do it # can't just retrieve the palette number, got to do it
# after quantization. # after quantization.
trns_im = trns_im.convert('RGB') trns_im = trns_im.convert('RGB')
trns = trns_im.getpixel((0,0)) trns = trns_im.getpixel((0,0))
elif self.mode == 'P' and mode == 'RGBA': elif self.mode == 'P' and mode == 'RGBA':
delete_trns = True delete_trns = True
@ -840,7 +858,7 @@ class Image:
new_im = self._new(im) new_im = self._new(im)
if delete_trns: if delete_trns:
#crash fail if we leave a bytes transparency in an rgb/l mode. # crash fail if we leave a bytes transparency in an rgb/l mode.
del(new_im.info['transparency']) del(new_im.info['transparency'])
if trns is not None: if trns is not None:
if new_im.mode == 'P': if new_im.mode == 'P':
@ -1020,7 +1038,7 @@ class Image:
return out return out
return self.im.getcolors(maxcolors) return self.im.getcolors(maxcolors)
def getdata(self, band = None): def getdata(self, band=None):
""" """
Returns the contents of this image as a sequence object Returns the contents of this image as a sequence object
containing pixel values. The sequence object is flattened, so containing pixel values. The sequence object is flattened, so
@ -1041,7 +1059,7 @@ class Image:
self.load() self.load()
if band is not None: if band is not None:
return self.im.getband(band) return self.im.getband(band)
return self.im # could be abused return self.im # could be abused
def getextrema(self): def getextrema(self):
""" """
@ -1071,7 +1089,6 @@ class Image:
self.load() self.load()
return self.im.ptr return self.im.ptr
def getpalette(self): def getpalette(self):
""" """
Returns the image palette as a list. Returns the image palette as a list.
@ -1087,8 +1104,7 @@ class Image:
else: else:
return list(self.im.getpalette()) return list(self.im.getpalette())
except ValueError: except ValueError:
return None # no palette return None # no palette
def getpixel(self, xy): def getpixel(self, xy):
""" """
@ -1210,7 +1226,8 @@ class Image:
if isImageType(box) and mask is None: if isImageType(box) and mask is None:
# abbreviated paste(im, mask) syntax # abbreviated paste(im, mask) syntax
mask = box; box = None mask = box
box = None
if box is None: if box is None:
# cover all of self # cover all of self
@ -1316,7 +1333,7 @@ class Image:
# do things the hard way # do things the hard way
im = self.im.convert(mode) im = self.im.convert(mode)
if im.mode not in ("LA", "RGBA"): if im.mode not in ("LA", "RGBA"):
raise ValueError # sanity check raise ValueError # sanity check
self.im = im self.im = im
self.pyaccess = None self.pyaccess = None
self.mode = self.im.mode self.mode = self.im.mode
@ -1394,7 +1411,7 @@ class Image:
self.mode = "P" self.mode = "P"
self.palette = palette self.palette = palette
self.palette.mode = "RGB" self.palette.mode = "RGB"
self.load() # install new palette self.load() # install new palette
def putpixel(self, xy, value): def putpixel(self, xy, value):
""" """
@ -1423,7 +1440,7 @@ class Image:
self.load() self.load()
if self.pyaccess: if self.pyaccess:
return self.pyaccess.putpixel(xy,value) return self.pyaccess.putpixel(xy, value)
return self.im.putpixel(xy, value) return self.im.putpixel(xy, value)
def resize(self, size, resample=NEAREST): def resize(self, size, resample=NEAREST):
@ -1493,6 +1510,7 @@ class Image:
math.cos(angle), math.sin(angle), 0.0, math.cos(angle), math.sin(angle), 0.0,
-math.sin(angle), math.cos(angle), 0.0 -math.sin(angle), math.cos(angle), 0.0
] ]
def transform(x, y, matrix=matrix): def transform(x, y, matrix=matrix):
(a, b, c, d, e, f) = matrix (a, b, c, d, e, f) = matrix
return a*x + b*y + c, d*x + e*y + f return a*x + b*y + c, d*x + e*y + f
@ -1580,13 +1598,13 @@ class Image:
try: try:
format = EXTENSION[ext] format = EXTENSION[ext]
except KeyError: except KeyError:
raise KeyError(ext) # unknown extension raise KeyError(ext) # unknown extension
try: try:
save_handler = SAVE[format.upper()] save_handler = SAVE[format.upper()]
except KeyError: except KeyError:
init() init()
save_handler = SAVE[format.upper()] # unknown format save_handler = SAVE[format.upper()] # unknown format
if isPath(fp): if isPath(fp):
fp = builtins.open(fp, "wb") fp = builtins.open(fp, "wb")
@ -1668,7 +1686,7 @@ class Image:
""" """
return 0 return 0
def thumbnail(self, size, resample=NEAREST): def thumbnail(self, size, resample=ANTIALIAS):
""" """
Make this image into a thumbnail. This method modifies the Make this image into a thumbnail. This method modifies the
image to contain a thumbnail version of itself, no larger than image to contain a thumbnail version of itself, no larger than
@ -1683,26 +1701,27 @@ class Image:
important than quality. important than quality.
Also note that this function modifies the :py:class:`~PIL.Image.Image` Also note that this function modifies the :py:class:`~PIL.Image.Image`
object in place. If you need to use the full resolution image as well, apply object in place. If you need to use the full resolution image as well,
this method to a :py:meth:`~PIL.Image.Image.copy` of the original image. apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original
image.
:param size: Requested size. :param size: Requested size.
:param resample: Optional resampling filter. This can be one :param resample: Optional resampling filter. This can be one
of :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`, of :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`,
:py:attr:`PIL.Image.BICUBIC`, or :py:attr:`PIL.Image.ANTIALIAS` :py:attr:`PIL.Image.BICUBIC`, or :py:attr:`PIL.Image.ANTIALIAS`
(best quality). If omitted, it defaults to (best quality). If omitted, it defaults to
:py:attr:`PIL.Image.NEAREST` (this will be changed to ANTIALIAS in a :py:attr:`PIL.Image.ANTIALIAS`.
future version).
:returns: None :returns: None
""" """
# FIXME: the default resampling filter will be changed
# to ANTIALIAS in future versions
# preserve aspect ratio # preserve aspect ratio
x, y = self.size x, y = self.size
if x > size[0]: y = int(max(y * size[0] / x, 1)); x = int(size[0]) if x > size[0]:
if y > size[1]: x = int(max(x * size[1] / y, 1)); y = int(size[1]) y = int(max(y * size[0] / x, 1))
x = int(size[0])
if y > size[1]:
x = int(max(x * size[1] / y, 1))
y = int(size[1])
size = x, y size = x, y
if size == self.size: if size == self.size:
@ -1717,7 +1736,7 @@ class Image:
except ValueError: except ValueError:
if resample != ANTIALIAS: if resample != ANTIALIAS:
raise raise
im = self.resize(size, NEAREST) # fallback im = self.resize(size, NEAREST) # fallback
self.im = im.im self.im = im.im
self.mode = im.mode self.mode = im.mode
@ -1753,7 +1772,9 @@ class Image:
""" """
if self.mode == 'RGBA': if self.mode == 'RGBA':
return self.convert('RGBa').transform(size, method, data, resample, fill).convert('RGBA') return self.convert('RGBa') \
.transform(size, method, data, resample, fill) \
.convert('RGBA')
if isinstance(method, ImageTransformHandler): if isinstance(method, ImageTransformHandler):
return method.transform(size, self, resample=resample, fill=fill) return method.transform(size, self, resample=resample, fill=fill)
@ -1800,8 +1821,13 @@ class Image:
elif method == QUAD: elif method == QUAD:
# quadrilateral warp. data specifies the four corners # quadrilateral warp. data specifies the four corners
# given as NW, SW, SE, and NE. # given as NW, SW, SE, and NE.
nw = data[0:2]; sw = data[2:4]; se = data[4:6]; ne = data[6:8] nw = data[0:2]
x0, y0 = nw; As = 1.0 / w; At = 1.0 / h sw = data[2:4]
se = data[4:6]
ne = data[6:8]
x0, y0 = nw
As = 1.0 / w
At = 1.0 / h
data = (x0, (ne[0]-x0)*As, (sw[0]-x0)*At, data = (x0, (ne[0]-x0)*As, (sw[0]-x0)*At,
(se[0]-sw[0]-ne[0]+x0)*As*At, (se[0]-sw[0]-ne[0]+x0)*As*At,
y0, (ne[1]-y0)*As, (sw[1]-y0)*At, y0, (ne[1]-y0)*As, (sw[1]-y0)*At,
@ -1835,6 +1861,7 @@ class Image:
im = self.im.transpose(method) im = self.im.transpose(method)
return self._new(im) return self._new(im)
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Lazy operations # Lazy operations
@ -1870,6 +1897,7 @@ class _ImageCrop(Image):
# FIXME: future versions should optimize crop/paste # FIXME: future versions should optimize crop/paste
# sequences! # sequences!
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Abstract handlers. # Abstract handlers.
@ -1877,10 +1905,12 @@ class ImagePointHandler:
# used as a mixin by point transforms (for use with im.point) # used as a mixin by point transforms (for use with im.point)
pass pass
class ImageTransformHandler: class ImageTransformHandler:
# used as a mixin by geometry transforms (for use with im.transform) # used as a mixin by geometry transforms (for use with im.transform)
pass pass
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Factories # Factories
@ -1956,6 +1986,7 @@ def frombytes(mode, size, data, decoder_name="raw", *args):
im.frombytes(data, decoder_name, args) im.frombytes(data, decoder_name, args)
return im return im
def fromstring(*args, **kw): def fromstring(*args, **kw):
"""Deprecated alias to frombytes. """Deprecated alias to frombytes.
@ -2018,9 +2049,9 @@ def frombuffer(mode, size, data, decoder_name="raw", *args):
" frombuffer(mode, size, data, 'raw', mode, 0, 1)", " frombuffer(mode, size, data, 'raw', mode, 0, 1)",
RuntimeWarning, stacklevel=2 RuntimeWarning, stacklevel=2
) )
args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6
if args[0] in _MAPMODES: if args[0] in _MAPMODES:
im = new(mode, (1,1)) im = new(mode, (1, 1))
im = im._new( im = im._new(
core.map_buffer(data, size, decoder_name, None, 0, args) core.map_buffer(data, size, decoder_name, None, 0, args)
) )
@ -2140,8 +2171,8 @@ def open(fp, mode="r"):
fp.seek(0) fp.seek(0)
return factory(fp, filename) return factory(fp, filename)
except (SyntaxError, IndexError, TypeError): except (SyntaxError, IndexError, TypeError):
#import traceback # import traceback
#traceback.print_exc() # traceback.print_exc()
pass pass
if init(): if init():
@ -2153,13 +2184,14 @@ def open(fp, mode="r"):
fp.seek(0) fp.seek(0)
return factory(fp, filename) return factory(fp, filename)
except (SyntaxError, IndexError, TypeError): except (SyntaxError, IndexError, TypeError):
#import traceback # import traceback
#traceback.print_exc() # traceback.print_exc()
pass pass
raise IOError("cannot identify image file %r" raise IOError("cannot identify image file %r"
% (filename if filename else fp)) % (filename if filename else fp))
# #
# Image processing. # Image processing.
@ -2258,6 +2290,7 @@ def merge(mode, bands):
im.putband(bands[i].im, i) im.putband(bands[i].im, i)
return bands[0]._new(im) return bands[0]._new(im)
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Plugin registry # Plugin registry
@ -2316,6 +2349,7 @@ def _show(image, **options):
# override me, as necessary # override me, as necessary
_showxv(image, **options) _showxv(image, **options)
def _showxv(image, title=None, **options): def _showxv(image, title=None, **options):
from PIL import ImageShow from PIL import ImageShow
ImageShow.show(image, title, **options) ImageShow.show(image, title, **options)

View File

@ -46,9 +46,11 @@ def _obj(fp, obj, **dict):
fp.write("/%s %s\n" % (k, v)) fp.write("/%s %s\n" % (k, v))
fp.write(">>\n") fp.write(">>\n")
def _endobj(fp): def _endobj(fp):
fp.write("endobj\n") fp.write("endobj\n")
## ##
# (Internal) Image save plugin for the PDF format. # (Internal) Image save plugin for the PDF format.
@ -59,13 +61,15 @@ def _save(im, fp, filename):
# make sure image data is available # make sure image data is available
im.load() im.load()
xref = [0]*(5+1) # placeholders xref = [0]*(5+1) # placeholders
class TextWriter: class TextWriter:
def __init__(self, fp): def __init__(self, fp):
self.fp = fp self.fp = fp
def __getattr__(self, name): def __getattr__(self, name):
return getattr(self.fp, name) return getattr(self.fp, name)
def write(self, value): def write(self, value):
self.fp.write(value.encode('latin-1')) self.fp.write(value.encode('latin-1'))
@ -89,13 +93,13 @@ def _save(im, fp, filename):
if im.mode == "1": if im.mode == "1":
filter = "/ASCIIHexDecode" filter = "/ASCIIHexDecode"
colorspace = "/DeviceGray" colorspace = "/DeviceGray"
procset = "/ImageB" # grayscale procset = "/ImageB" # grayscale
bits = 1 bits = 1
elif im.mode == "L": elif im.mode == "L":
filter = "/DCTDecode" filter = "/DCTDecode"
# params = "<< /Predictor 15 /Columns %d >>" % (width-2) # params = "<< /Predictor 15 /Columns %d >>" % (width-2)
colorspace = "/DeviceGray" colorspace = "/DeviceGray"
procset = "/ImageB" # grayscale procset = "/ImageB" # grayscale
elif im.mode == "P": elif im.mode == "P":
filter = "/ASCIIHexDecode" filter = "/ASCIIHexDecode"
colorspace = "[ /Indexed /DeviceRGB 255 <" colorspace = "[ /Indexed /DeviceRGB 255 <"
@ -105,16 +109,16 @@ def _save(im, fp, filename):
g = i8(palette[i*3+1]) g = i8(palette[i*3+1])
b = i8(palette[i*3+2]) b = i8(palette[i*3+2])
colorspace = colorspace + "%02x%02x%02x " % (r, g, b) colorspace = colorspace + "%02x%02x%02x " % (r, g, b)
colorspace = colorspace + b"> ]" colorspace = colorspace + "> ]"
procset = "/ImageI" # indexed color procset = "/ImageI" # indexed color
elif im.mode == "RGB": elif im.mode == "RGB":
filter = "/DCTDecode" filter = "/DCTDecode"
colorspace = "/DeviceRGB" colorspace = "/DeviceRGB"
procset = "/ImageC" # color images procset = "/ImageC" # color images
elif im.mode == "CMYK": elif im.mode == "CMYK":
filter = "/DCTDecode" filter = "/DCTDecode"
colorspace = "/DeviceCMYK" colorspace = "/DeviceCMYK"
procset = "/ImageC" # color images procset = "/ImageC" # color images
else: else:
raise ValueError("cannot save mode %s" % im.mode) raise ValueError("cannot save mode %s" % im.mode)
@ -122,17 +126,21 @@ def _save(im, fp, filename):
# catalogue # catalogue
xref[1] = fp.tell() xref[1] = fp.tell()
_obj(fp, 1, Type = "/Catalog", _obj(
Pages = "2 0 R") fp, 1,
Type="/Catalog",
Pages="2 0 R")
_endobj(fp) _endobj(fp)
# #
# pages # pages
xref[2] = fp.tell() xref[2] = fp.tell()
_obj(fp, 2, Type = "/Pages", _obj(
Count = 1, fp, 2,
Kids = "[4 0 R]") Type="/Pages",
Count=1,
Kids="[4 0 R]")
_endobj(fp) _endobj(fp)
# #
@ -144,29 +152,31 @@ def _save(im, fp, filename):
if bits == 1: if bits == 1:
# FIXME: the hex encoder doesn't support packed 1-bit # FIXME: the hex encoder doesn't support packed 1-bit
# images; do things the hard way... # images; do things the hard way...
data = im.tostring("raw", "1") data = im.tobytes("raw", "1")
im = Image.new("L", (len(data), 1), None) im = Image.new("L", (len(data), 1), None)
im.putdata(data) im.putdata(data)
ImageFile._save(im, op, [("hex", (0,0)+im.size, 0, im.mode)]) ImageFile._save(im, op, [("hex", (0, 0)+im.size, 0, im.mode)])
elif filter == "/DCTDecode": elif filter == "/DCTDecode":
Image.SAVE["JPEG"](im, op, filename) Image.SAVE["JPEG"](im, op, filename)
elif filter == "/FlateDecode": elif filter == "/FlateDecode":
ImageFile._save(im, op, [("zip", (0,0)+im.size, 0, im.mode)]) ImageFile._save(im, op, [("zip", (0, 0)+im.size, 0, im.mode)])
elif filter == "/RunLengthDecode": elif filter == "/RunLengthDecode":
ImageFile._save(im, op, [("packbits", (0,0)+im.size, 0, im.mode)]) ImageFile._save(im, op, [("packbits", (0, 0)+im.size, 0, im.mode)])
else: else:
raise ValueError("unsupported PDF filter (%s)" % filter) raise ValueError("unsupported PDF filter (%s)" % filter)
xref[3] = fp.tell() xref[3] = fp.tell()
_obj(fp, 3, Type = "/XObject", _obj(
Subtype = "/Image", fp, 3,
Width = width, # * 72.0 / resolution, Type="/XObject",
Height = height, # * 72.0 / resolution, Subtype="/Image",
Length = len(op.getvalue()), Width=width, # * 72.0 / resolution,
Filter = filter, Height=height, # * 72.0 / resolution,
BitsPerComponent = bits, Length=len(op.getvalue()),
DecodeParams = params, Filter=filter,
ColorSpace = colorspace) BitsPerComponent=bits,
DecodeParams=params,
ColorSpace=colorspace)
fp.write("stream\n") fp.write("stream\n")
fp.fp.write(op.getvalue()) fp.fp.write(op.getvalue())
@ -179,11 +189,14 @@ def _save(im, fp, filename):
xref[4] = fp.tell() xref[4] = fp.tell()
_obj(fp, 4) _obj(fp, 4)
fp.write("<<\n/Type /Page\n/Parent 2 0 R\n"\ fp.write(
"/Resources <<\n/ProcSet [ /PDF %s ]\n"\ "<<\n/Type /Page\n/Parent 2 0 R\n"
"/XObject << /image 3 0 R >>\n>>\n"\ "/Resources <<\n/ProcSet [ /PDF %s ]\n"
"/MediaBox [ 0 0 %d %d ]\n/Contents 5 0 R\n>>\n" %\ "/XObject << /image 3 0 R >>\n>>\n"
(procset, int(width * 72.0 /resolution) , int(height * 72.0 / resolution))) "/MediaBox [ 0 0 %d %d ]\n/Contents 5 0 R\n>>\n" % (
procset,
int(width * 72.0 / resolution),
int(height * 72.0 / resolution)))
_endobj(fp) _endobj(fp)
# #
@ -191,10 +204,13 @@ def _save(im, fp, filename):
op = TextWriter(io.BytesIO()) op = TextWriter(io.BytesIO())
op.write("q %d 0 0 %d 0 0 cm /image Do Q\n" % (int(width * 72.0 / resolution), int(height * 72.0 / resolution))) op.write(
"q %d 0 0 %d 0 0 cm /image Do Q\n" % (
int(width * 72.0 / resolution),
int(height * 72.0 / resolution)))
xref[5] = fp.tell() xref[5] = fp.tell()
_obj(fp, 5, Length = len(op.fp.getvalue())) _obj(fp, 5, Length=len(op.fp.getvalue()))
fp.write("stream\n") fp.write("stream\n")
fp.fp.write(op.fp.getvalue()) fp.fp.write(op.fp.getvalue())

58
Tests/test_file_pdf.py Normal file
View File

@ -0,0 +1,58 @@
from tester import *
import os.path
def helper_save_as_pdf(mode):
# Arrange
im = lena(mode)
outfile = tempfile("temp_" + mode + ".pdf")
# Act
im.save(outfile)
# Assert
assert_true(os.path.isfile(outfile))
assert_greater(os.path.getsize(outfile), 0)
def test_monochrome():
# Arrange
mode = "1"
# Act / Assert
helper_save_as_pdf(mode)
def test_greyscale():
# Arrange
mode = "L"
# Act / Assert
helper_save_as_pdf(mode)
def test_rgb():
# Arrange
mode = "RGB"
# Act / Assert
helper_save_as_pdf(mode)
def test_p_mode():
# Arrange
mode = "P"
# Act / Assert
helper_save_as_pdf(mode)
def test_cmyk_mode():
# Arrange
mode = "CMYK"
# Act / Assert
helper_save_as_pdf(mode)
# End of file