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):
@ -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
@ -108,6 +112,7 @@ try:
except: except:
HAS_CFFI = False HAS_CFFI = False
def isImageType(t): def isImageType(t):
""" """
Checks if an object is an image object. Checks if an object is an image object.
@ -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]
@ -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,14 +429,18 @@ 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
@ -438,13 +450,15 @@ def _getscaleoffset(expr):
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")
@ -500,6 +514,7 @@ class Image:
# 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()
@ -525,7 +540,6 @@ class 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
@ -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):
@ -802,6 +819,7 @@ class Image:
# 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
@ -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.
@ -1089,7 +1106,6 @@ class Image:
except ValueError: except ValueError:
return None # no palette return None # no palette
def getpixel(self, xy): def getpixel(self, xy):
""" """
Returns the pixel value at a given position. Returns the pixel value at a given position.
@ -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
@ -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
@ -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:
@ -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.
@ -2160,6 +2191,7 @@ def open(fp, mode="r"):
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.
@ -64,8 +66,10 @@ def _save(im, fp, filename):
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'))
@ -105,7 +109,7 @@ 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"
@ -122,7 +126,9 @@ def _save(im, fp, filename):
# catalogue # catalogue
xref[1] = fp.tell() xref[1] = fp.tell()
_obj(fp, 1, Type = "/Catalog", _obj(
fp, 1,
Type="/Catalog",
Pages="2 0 R") Pages="2 0 R")
_endobj(fp) _endobj(fp)
@ -130,7 +136,9 @@ def _save(im, fp, filename):
# pages # pages
xref[2] = fp.tell() xref[2] = fp.tell()
_obj(fp, 2, Type = "/Pages", _obj(
fp, 2,
Type="/Pages",
Count=1, Count=1,
Kids="[4 0 R]") Kids="[4 0 R]")
_endobj(fp) _endobj(fp)
@ -144,7 +152,7 @@ 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)])
@ -158,7 +166,9 @@ def _save(im, fp, filename):
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(
fp, 3,
Type="/XObject",
Subtype="/Image", Subtype="/Image",
Width=width, # * 72.0 / resolution, Width=width, # * 72.0 / resolution,
Height=height, # * 72.0 / resolution, Height=height, # * 72.0 / resolution,
@ -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,7 +204,10 @@ 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()))

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