Creating and Manipulating PDFs with pdfrw

Patrick Maupin created a package he called pdfrw and released it back in 2012. The pdfrw package is a pure-Python library that you can use to read and write PDF files. At the time of writing, pdfrw was at version 0.4. With that version, it supports subsetting, merging, rotating and modifying data in PDFs. The pdfrw package has been used by the rst2pdf package (see chapter 18) since 2010 because pdfrw can “faithfully reproduce vector formats without rasterization”. You can also use pdfrw in conjunction with ReportLab to re-use potions of existing PDFs in new PDFs that you create with ReportLab.

In this article, we will learn how to do the following:

  • Extract certain types of information from a PDF
  • Splitting PDFs
  • Merging / Concatenating PDFs
  • Rotating pages
  • Creating overlays or watermarks
  • Scaling pages
  • Combining the use of pdfrw and ReportLab

Let’s get started!

Note: This article is based on my book, ReportLab: PDF Processing with Python. Code can be found on GitHub.


Installation

As you might expect, you can install pdfrw using pip. Let’s get that done so we can start using pdfrw:

python -m pip install pdfrw

Now that we have pdfrw installed, let’s learn how to extract some information from our PDFs.


Extracting Information from PDF

The pdfrw package does not extract data in quite the same way that PyPDF2 does. If you have using PyPDF2 in the past, then you may recall that PyPDF2 let’s you extract an document information object that you can use to pull out information like author, title, etc. While pdfrw does let you get the Info object, it displays it in a less friendly way. Let’s take a look:

Note: I am using the standard W9 form from the IRS for this example.

# reader.py

from pdfrw import PdfReader

def get_pdf_info(path):
    pdf = PdfReader(path)
    
    print(pdf.keys())
    print(pdf.Info)
    print(pdf.Root.keys())
    print('PDF has {} pages'.format(len(pdf.pages)))
    
if __name__ == '__main__':
    get_pdf_info('w9.pdf')

Here we import pdfrw’s PdfReader class and instantiate it by passing in the path to the PDF file that we want to read. Then we extract the PDF object’s keys, the information object and the Root. We also grab how many pages are in the document. The result of running this code is below:

['/ID', '/Root', '/Info', '/Size']
{'/Author': '(SE:W:CAR:MP)',
 '/CreationDate': "(D:20171109144422-05'00')",
 '/Creator': '(Adobe LiveCycle Designer ES 9.0)',
 '/Keywords': '(Fillable)',
 '/ModDate': "(D:20171109144521-05'00')",
 '/Producer': '(Adobe LiveCycle Designer ES 9.0)',
 '/SPDF': '(1112)',
 '/Subject': '(Request for Taxpayer Identification Number and Certification)',
 '/Title': '(Form W-9 \\(Rev. November 2017\\))'}
['/Pages', '/Perms', '/MarkInfo', '/Extensions', '/AcroForm', '/Metadata', '/Type', '/Names', '/StructTreeRoot']
PDF has 6 pages

If you run this against the reportlab-sample.pdf file that I also included in the source code for this article, you will find that the author name that is returned ends up being ” instead of “Michael Driscoll”. I haven’t figured out exactly why that is, but I am assuming that PyPDF2 does some extra data massaging on the PDF trailer information that pdfrw currently does not do.


Splitting

You can also use pdfrw to split a PDF up. For example, maybe you want to take the cover off of a book for some reason or you just want to extract the chapters of a book into multiple PDFs instead of storing them in one file. This is fairly trivial to do with pdfrw. For this example, we will use my ReportLab book’s sample chapter PDF that you can download on Leanpub.

# splitter.py

from pdfrw import PdfReader, PdfWriter

def split(path, number_of_pages, output):
    pdf_obj = PdfReader(path)
    total_pages = len(pdf_obj.pages)
    
    writer = PdfWriter()
    
    for page in range(number_of_pages):
        if page <= total_pages:
            writer.addpage(pdf_obj.pages[page])
        
    writer.write(output)
    
if __name__ == '__main__':
    split('reportlab-sample.pdf', 10, 'subset.pdf')

Here we create a function called split that takes an input PDF file path, the number of pages that you want to extract and the output path. Then we open up the file using pdfrw’s PdfReader class and grab the total number of pages from the input PDF. Then we create a PdfWriter object and loop over the range of pages that we passed in. In each iteration, we attempt to extract a page from the input PDF and add that page to our writer object. Finally we write the extracted pages to disk.


Merging / Concatenating

The pdfrw package makes merging multiple PDFs together very easy. Let’s write up a simple example that demonstrates how to do it:

# concatenator.py

from pdfrw import PdfReader, PdfWriter, IndirectPdfDict

def concatenate(paths, output):
    writer = PdfWriter()
    
    for path in paths:
        reader = PdfReader(path)
        writer.addpages(reader.pages)
        
    writer.trailer.Info = IndirectPdfDict(
        Title='Combined PDF Title',
        Author='Michael Driscoll',
        Subject='PDF Combinations',
        Creator='The Concatenator'
    )
        
    writer.write(output)
    
if __name__ == '__main__':
    paths = ['reportlab-sample.pdf', 'w9.pdf']
    concatenate(paths, 'concatenate.pdf')

In this example, we create a function called concatenate that accepts a list of paths to PDFs that we want to concatenate together and the output path. Then iterate over those paths, open the file and add all the pages to the writer object via the writer’s addpages method. Just for fun, we also import IndirectPdfDict, which allows us to add some trailer information to our PDF. In this case, we add the title, author, subject and creator script information to the PDF. Then we write out the concatenated PDF to disk.


Rotating

The pdfrw package also supports rotating the pages of a PDF. So if you happen to have a PDF that was saved in a weird way or an intern that scanned in some documents upside down, then you can use pdfrw (or PyPDF2) to fix the PDFs. Note that in pdfrw you must rotate clockwise in increments that are divisible by 90 degrees.

For this example, I created a function that will extract all the odd pages from the input PDF and rotate them 90 degrees:

# rotator.py

from pdfrw import PdfReader, PdfWriter, IndirectPdfDict

def rotate_odd(path, output):
    reader = PdfReader(path)
    writer = PdfWriter()
    pages = reader.pages
    
    for page in range(len(pages)):
        if page % 2:
            pages[page].Rotate = 90
            writer.addpage(pages[page])
        
    writer.write(output)
    
if __name__ == '__main__':
    rotate_odd('reportlab-sample.pdf', 'rotate_odd.pdf')

Here we just open up the target PDF and create a writer object. Then we grab all the pages and iterate over them. If the page is an odd numbered page, we rotate it and then add that page to our writer object. This code ran pretty fast on my machine and the output is what you would expect.


Overlaying / Watermarking Pages

You can use pdfrw to watermark your PDF with some kind of information. For example, you might want to watermark a PDF with your buyer’s email address or with your logo. You can also use the overlay one PDF on top of another PDF. We will actually use the overlay technique for filling in PDF forms in chapter 17.

Let’s create a simple watermarker script to demonstrate how you might use pdfrw to overlay one PDF on top of another.

# watermarker.py

from pdfrw import PdfReader, PdfWriter, PageMerge

def watermarker(path, watermark, output):
    base_pdf = PdfReader(path)
    watermark_pdf = PdfReader(watermark)
    mark = watermark_pdf.pages[0]
    
    for page in range(len(base_pdf.pages)):
        merger = PageMerge(base_pdf.pages[page])
        merger.add(mark).render()
    
    writer = PdfWriter()
    writer.write(output, base_pdf)
    
if __name__ == '__main__':
    watermarker('reportlab-sample.pdf',
                'watermark.pdf',
                'watermarked-test.pdf')

Here we create a simple watermarker function that takes an input PDF path, the PDF that contains the watermark and the output path of the end result. Then we open up the base PDF and the watermark PDF. We extract the watermark page and then iterate over the pages in the base PDF. In each iteration, we create a PageMerge object using the current base PDF page that we are on. Then we overlay the watermark on top of that page and render it. After the loop finished, we create a PdfWriter object and write the merged PDF to disk.


Scaling

The pdfrw package can also manipulate PDFs in memory. In fact, it will allow you to create Form XObjects. These objects can represent any page or rectangle in a PDF. What this means is that you once you have one of these objects created, you can then scale, rotate and position pages or sub-pages. There is a fun example on the pdfrw Github page called 4up.py that takes pages from a PDF and scales them down to a quarter of their size and positions four pages to a single page.

Here is my version:

# scaler.py

from pdfrw import PdfReader, PdfWriter, PageMerge


def get4(srcpages):
    scale = 0.5
    srcpages = PageMerge() + srcpages
    x_increment, y_increment = (scale * i for i in srcpages.xobj_box[2:])
    for i, page in enumerate(srcpages):
        page.scale(scale)
        page.x = x_increment if i & 1 else 0
        page.y = 0 if i & 2 else y_increment
    return srcpages.render()


def scale_pdf(path, output):
    pages = PdfReader(path).pages
    writer = PdfWriter(output)
    scaled_pages = 4

    for i in range(0, len(pages), scaled_pages):
        four_pages = get4(pages[i: i + 4])
        writer.addpage(four_pages)

    writer.write()

if __name__ == '__main__':
    scale_pdf('reportlab-sample.pdf', 'four-page.pdf')

The get4 function comes from the 4up.py script. This function takes a series of pages and uses pdfrw’s PageMerge class to merge those pages together. We basically loop over the passed in pages and scale them down a bit, then we position them on the page and render the page series on one page.

The next function is scale_pdf, which takes the input PDF and the path for the output. Then we extract the pages from the input file and create a writer object. Next we loop over the pages of the input document 4 at a time and pass them to the **get4** function. Then we take the result of that function and add it to our writer object.

Finally we write the document out to disk. Here is a screenshot that kind of shows how it looks:

Now let’s learn how we might combine pdfrw with ReportLab!


Combining pdfrw and ReportLab

One of the neat features of pdfrw is its ability to integrate with the ReportLab toolkit. There are several examples on the pdfrw Github page that show different ways to use the two packages together. The creator of pdfrw thinks that you may be able to simulate some of ReportLab’s pagecatcher functionality which is a part of ReportLab’s paid product. I don’t know if it does or not, but you can definitely do some fun things with pdfrw and ReportLab.

For example, you can use pdfrw to read in pages from a pre-existing PDF and turn them into objects that you can write out in ReportLab. Let’s write a script that will create a subset of a PDF using pdfrw and ReportLab. The following example is based on one from the pdfrw project:

# split_with_rl.py

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl

from reportlab.pdfgen.canvas import Canvas

def split(path, number_of_pages, output):
    pdf_obj = PdfReader(path)

    my_canvas = Canvas(output)

    # create page objects
    pages = pdf_obj.pages[0: number_of_pages]
    pages = [pagexobj(page) for page in pages]

    for page in pages:
        my_canvas.setPageSize((page.BBox[2], page.BBox[3]))
        my_canvas.doForm(makerl(my_canvas, page))
        my_canvas.showPage()

    # write the new PDF to disk
    my_canvas.save()


if __name__ == '__main__':
    split('reportlab-sample.pdf', 10, 'subset-rl.pdf')

Here we import some new functionality. First we import the pagexobj which will create a Form XObject from the view that you give it. The view defaults to an entire page, but you could tell pdfrw to just extract a portion of the page. Next we import the makerl function which will take a ReportLab canvas object and a pdfrw Form XObject and turn it into a form that ReportLab can add to its canvas object.

So let’s examine this code a bit and see how it works. Here we create a reader object and a canvas object. Then we create a list of Form XForm objects starting with the first page to the last page that we specified. Note that we do not check if we asked for too many pages though, so that is something that we could do to enhance this script and make it less likely to fail.

Next we iterate over the pages that we just created and add them to our ReportLab canvas. You will note that we set the page size using the width and height that we extract using pdfrw’s BBox attributes. Then we add the Form XObjects to the canvas. The call to **showPage** tells ReportLab that you finished creating a page and to start a new one. Finally we save the new PDF to disk.

There are some other examples on pdfrw’s site that you should review. For example, there is a neat piece of code that shows how you could take a page from a pre-existing PDF and use it as the background for a new PDF that you create in ReportLab. There is also a really interesting scaling example where you can use pdfrw and ReportLab to scale pages down in much the same way that we did with pdfrw all by itself.


Wrapping Up

The pdfrw package is actually pretty powerful and has features that PyPDF2 does not. Its ability to integrate with ReportLab is one feature that I think is really interesting and could be used to create something original. You can also use pdfrw to do many of the same things that we can do with PyPDF2, such as splitting, merging, rotating and concatenating PDFs together. I actually thought pdfrw was a bit more robust in generating viable PDFs than PyPDF2 but I have not done extensive tests to actually confirm this.

Regardless, I believe that pdfrw is worth adding to your toolkit.


Related Reading