From 4e31fb745f4ef79edf461c70837946073c374d8e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Sep 2015 22:51:52 +1000 Subject: [PATCH 1/2] Added PDF multipage saving --- PIL/PdfImagePlugin.py | 163 +++++++++++++++++++++++------------------ Tests/test_file_pdf.py | 22 +++++- 2 files changed, 110 insertions(+), 75 deletions(-) diff --git a/PIL/PdfImagePlugin.py b/PIL/PdfImagePlugin.py index e58e9e666..466fc6723 100644 --- a/PIL/PdfImagePlugin.py +++ b/PIL/PdfImagePlugin.py @@ -51,17 +51,21 @@ def _endobj(fp): 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. -def _save(im, fp, filename): +def _save(im, fp, filename, save_all=False): resolution = im.encoderinfo.get("resolution", 72.0) # # make sure image data is available im.load() - xref = [0]*(5+1) # placeholders + xref = [0] class TextWriter(object): def __init__(self, fp): @@ -78,11 +82,6 @@ def _save(im, fp, filename): fp.write("%PDF-1.2\n") fp.write("% created by PIL PDF driver " + __version__ + "\n") - # - # Get image characteristics - - width, height = im.size - # FIXME: Should replace ASCIIHexDecode with RunLengthDecode (packbits) # or LZWDecode (tiff/lzw compression). Note that PDF 1.2 also supports # Flatedecode (zip compression). @@ -125,7 +124,7 @@ def _save(im, fp, filename): # # catalogue - xref[1] = fp.tell() + xref.append(fp.tell()) _obj( fp, 1, Type="/Catalog", @@ -134,89 +133,108 @@ def _save(im, fp, filename): # # 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( fp, 2, Type="/Pages", - Count=1, - Kids="[4 0 R]") + Count=len(pages), + Kids="["+"\n".join(pages)+"]") _endobj(fp) - # - # image + for pageNumber in range(0, numberOfPages): + im.seek(pageNumber) - op = io.BytesIO() + # + # image - if filter == "/ASCIIHexDecode": - 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) + op = io.BytesIO() - xref[3] = fp.tell() - _obj( - fp, 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) + if filter == "/ASCIIHexDecode": + 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) - fp.write("stream\n") - fp.fp.write(op.getvalue()) - fp.write("\nendstream\n") + # + # Get image characteristics - _endobj(fp) + width, height = im.size - # - # page + xref.append(fp.tell()) + _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() - _obj(fp, 4) - fp.write( - "<<\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) + fp.write("stream\n") + fp.fp.write(op.getvalue()) + fp.write("\nendstream\n") - # - # page contents + _endobj(fp) - op = TextWriter(io.BytesIO()) + # + # page - 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+4) + fp.write( + "<<\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") - fp.fp.write(op.fp.getvalue()) - fp.write("\nendstream\n") + op = TextWriter(io.BytesIO()) - _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 @@ -232,6 +250,7 @@ def _save(im, fp, filename): # -------------------------------------------------------------------- Image.register_save("PDF", _save) +Image.register_save_all("PDF", _save_all) Image.register_extension("PDF", ".pdf") diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 8dad9822c..e3d41ffed 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -1,17 +1,20 @@ from helper import unittest, PillowTestCase, hopper - +from PIL import Image import os.path class TestFilePdf(PillowTestCase): - def helper_save_as_pdf(self, mode): + def helper_save_as_pdf(self, mode, save_all=False): # Arrange im = hopper(mode) outfile = self.tempfile("temp_" + mode + ".pdf") # Act - im.save(outfile) + if save_all: + im.save(outfile, save_all=True) + else: + im.save(outfile) # Assert self.assertTrue(os.path.isfile(outfile)) @@ -58,6 +61,19 @@ class TestFilePdf(PillowTestCase): 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__': unittest.main() From 125ec9c650830d5a2e3bf79100d1f565a0761789 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Oct 2015 19:45:27 +1000 Subject: [PATCH 2/2] Added documentation --- docs/handbook/image-file-formats.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 5d2cab518..f09a7cbbf 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -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 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) ^^^^^^^^^^^^^^^^^