Merge pull request #1445 from radarhere/pdf

Added PDF multipage saving
This commit is contained in:
wiredfool 2015-10-01 05:33:11 -07:00
commit 92f5133469
3 changed files with 115 additions and 75 deletions

View File

@ -51,17 +51,21 @@ def _endobj(fp):
fp.write("endobj\n") fp.write("endobj\n")
def _save_all(im, fp, filename):
_save(im, fp, filename, save_all=True)
## ##
# (Internal) Image save plugin for the PDF format. # (Internal) Image save plugin for the PDF format.
def _save(im, fp, filename): def _save(im, fp, filename, save_all=False):
resolution = im.encoderinfo.get("resolution", 72.0) resolution = im.encoderinfo.get("resolution", 72.0)
# #
# make sure image data is available # make sure image data is available
im.load() im.load()
xref = [0]*(5+1) # placeholders xref = [0]
class TextWriter(object): class TextWriter(object):
def __init__(self, fp): def __init__(self, fp):
@ -78,11 +82,6 @@ def _save(im, fp, filename):
fp.write("%PDF-1.2\n") fp.write("%PDF-1.2\n")
fp.write("% created by PIL PDF driver " + __version__ + "\n") fp.write("% created by PIL PDF driver " + __version__ + "\n")
#
# Get image characteristics
width, height = im.size
# FIXME: Should replace ASCIIHexDecode with RunLengthDecode (packbits) # FIXME: Should replace ASCIIHexDecode with RunLengthDecode (packbits)
# or LZWDecode (tiff/lzw compression). Note that PDF 1.2 also supports # or LZWDecode (tiff/lzw compression). Note that PDF 1.2 also supports
# Flatedecode (zip compression). # Flatedecode (zip compression).
@ -125,7 +124,7 @@ def _save(im, fp, filename):
# #
# catalogue # catalogue
xref[1] = fp.tell() xref.append(fp.tell())
_obj( _obj(
fp, 1, fp, 1,
Type="/Catalog", Type="/Catalog",
@ -134,89 +133,108 @@ def _save(im, fp, filename):
# #
# pages # pages
numberOfPages = 1
if save_all:
try:
numberOfPages = im.n_frames
except AttributeError:
# Image format does not have n_frames. It is a single frame image
pass
pages = [str(pageNumber*3+4)+" 0 R"
for pageNumber in range(0, numberOfPages)]
xref[2] = fp.tell() xref.append(fp.tell())
_obj( _obj(
fp, 2, fp, 2,
Type="/Pages", Type="/Pages",
Count=1, Count=len(pages),
Kids="[4 0 R]") Kids="["+"\n".join(pages)+"]")
_endobj(fp) _endobj(fp)
# for pageNumber in range(0, numberOfPages):
# image im.seek(pageNumber)
op = io.BytesIO() #
# image
if filter == "/ASCIIHexDecode": op = io.BytesIO()
if bits == 1:
# FIXME: the hex encoder doesn't support packed 1-bit
# images; do things the hard way...
data = im.tobytes("raw", "1")
im = Image.new("L", (len(data), 1), None)
im.putdata(data)
ImageFile._save(im, op, [("hex", (0, 0)+im.size, 0, im.mode)])
elif filter == "/DCTDecode":
Image.SAVE["JPEG"](im, op, filename)
elif filter == "/FlateDecode":
ImageFile._save(im, op, [("zip", (0, 0)+im.size, 0, im.mode)])
elif filter == "/RunLengthDecode":
ImageFile._save(im, op, [("packbits", (0, 0)+im.size, 0, im.mode)])
else:
raise ValueError("unsupported PDF filter (%s)" % filter)
xref[3] = fp.tell() if filter == "/ASCIIHexDecode":
_obj( if bits == 1:
fp, 3, # FIXME: the hex encoder doesn't support packed 1-bit
Type="/XObject", # images; do things the hard way...
Subtype="/Image", data = im.tobytes("raw", "1")
Width=width, # * 72.0 / resolution, im = Image.new("L", (len(data), 1), None)
Height=height, # * 72.0 / resolution, im.putdata(data)
Length=len(op.getvalue()), ImageFile._save(im, op, [("hex", (0, 0)+im.size, 0, im.mode)])
Filter=filter, elif filter == "/DCTDecode":
BitsPerComponent=bits, Image.SAVE["JPEG"](im, op, filename)
DecodeParams=params, elif filter == "/FlateDecode":
ColorSpace=colorspace) ImageFile._save(im, op, [("zip", (0, 0)+im.size, 0, im.mode)])
elif filter == "/RunLengthDecode":
ImageFile._save(im, op, [("packbits", (0, 0)+im.size, 0, im.mode)])
else:
raise ValueError("unsupported PDF filter (%s)" % filter)
fp.write("stream\n") #
fp.fp.write(op.getvalue()) # Get image characteristics
fp.write("\nendstream\n")
_endobj(fp) width, height = im.size
# xref.append(fp.tell())
# page _obj(
fp, pageNumber*3+3,
Type="/XObject",
Subtype="/Image",
Width=width, # * 72.0 / resolution,
Height=height, # * 72.0 / resolution,
Length=len(op.getvalue()),
Filter=filter,
BitsPerComponent=bits,
DecodeParams=params,
ColorSpace=colorspace)
xref[4] = fp.tell() fp.write("stream\n")
_obj(fp, 4) fp.fp.write(op.getvalue())
fp.write( fp.write("\nendstream\n")
"<<\n/Type /Page\n/Parent 2 0 R\n"
"/Resources <<\n/ProcSet [ /PDF %s ]\n"
"/XObject << /image 3 0 R >>\n>>\n"
"/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)
# page contents
op = TextWriter(io.BytesIO()) #
# page
op.write( xref.append(fp.tell())
"q %d 0 0 %d 0 0 cm /image Do Q\n" % ( _obj(fp, pageNumber*3+4)
int(width * 72.0 / resolution), fp.write(
int(height * 72.0 / resolution))) "<<\n/Type /Page\n/Parent 2 0 R\n"
"/Resources <<\n/ProcSet [ /PDF %s ]\n"
"/XObject << /image %d 0 R >>\n>>\n"
"/MediaBox [ 0 0 %d %d ]\n/Contents %d 0 R\n>>\n" % (
procset,
pageNumber*3+3,
int(width * 72.0 / resolution),
int(height * 72.0 / resolution),
pageNumber*3+5))
_endobj(fp)
xref[5] = fp.tell() #
_obj(fp, 5, Length=len(op.fp.getvalue())) # page contents
fp.write("stream\n") op = TextWriter(io.BytesIO())
fp.fp.write(op.fp.getvalue())
fp.write("\nendstream\n")
_endobj(fp) 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.append(fp.tell())
_obj(fp, pageNumber*3+5, Length=len(op.fp.getvalue()))
fp.write("stream\n")
fp.fp.write(op.fp.getvalue())
fp.write("\nendstream\n")
_endobj(fp)
# #
# trailer # trailer
@ -232,6 +250,7 @@ def _save(im, fp, filename):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
Image.register_save("PDF", _save) Image.register_save("PDF", _save)
Image.register_save_all("PDF", _save_all)
Image.register_extension("PDF", ".pdf") Image.register_extension("PDF", ".pdf")

View File

@ -1,17 +1,20 @@
from helper import unittest, PillowTestCase, hopper from helper import unittest, PillowTestCase, hopper
from PIL import Image
import os.path import os.path
class TestFilePdf(PillowTestCase): class TestFilePdf(PillowTestCase):
def helper_save_as_pdf(self, mode): def helper_save_as_pdf(self, mode, save_all=False):
# Arrange # Arrange
im = hopper(mode) im = hopper(mode)
outfile = self.tempfile("temp_" + mode + ".pdf") outfile = self.tempfile("temp_" + mode + ".pdf")
# Act # Act
im.save(outfile) if save_all:
im.save(outfile, save_all=True)
else:
im.save(outfile)
# Assert # Assert
self.assertTrue(os.path.isfile(outfile)) self.assertTrue(os.path.isfile(outfile))
@ -58,6 +61,19 @@ class TestFilePdf(PillowTestCase):
self.assertRaises(ValueError, lambda: im.save(outfile)) self.assertRaises(ValueError, lambda: im.save(outfile))
def test_save_all(self):
# Single frame image
self.helper_save_as_pdf("RGB", save_all=True)
# Multiframe image
im = Image.open("Tests/images/dispose_bgnd.gif")
outfile = self.tempfile('temp.pdf')
im.save(outfile, save_all=True)
self.assertTrue(os.path.isfile(outfile))
self.assertGreater(os.path.getsize(outfile), 0)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -786,6 +786,11 @@ PIL can write PDF (Acrobat) images. Such images are written as binary PDF 1.1
files, using either JPEG or HEX encoding depending on the image mode (and files, using either JPEG or HEX encoding depending on the image mode (and
whether JPEG support is available or not). whether JPEG support is available or not).
When calling :py:meth:`~PIL.Image.Image.save`, if a multiframe image is used,
by default, only the first image will be saved. To save all frames, each frame
to a separate page of the PDF, the ``save_all`` parameter must be present and
set to ``True``.
PIXAR (read only) PIXAR (read only)
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^