Generate flattened PDF with Python

21.3k views Asked by At

When I print a PDF from any of my source PDFs, the file size drops and removes the text boxes presents in form. In short, it flattens the file. This is behavior I want to achieve.

The following code to create a PDF using another PDF as a source (the one I want to flatten), it writes the text boxes form as well.

Can I get a PDF without the text boxes, flatten it? Just like Adobe does when I print a PDF as a PDF.

My other code looks something like this minus some things:

import os
import StringIO
from pyPdf import PdfFileWriter, PdfFileReader
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter

directory = os.path.join(os.getcwd(), "source")  # dir we are interested in
fif = [f for f in os.listdir(directory) if f[-3:] == 'pdf'] # get the PDFs
for i in fif:
    packet = StringIO.StringIO()
    can = canvas.Canvas(packet, pagesize=letter)
    can.rotate(-90)
    can.save()

    packet.seek(0)
    new_pdf = PdfFileReader(packet)
    fname = os.path.join('source', i)
    existing_pdf = PdfFileReader(file(fname, "rb"))
    output = PdfFileWriter()
    nump = existing_pdf.getNumPages()
    page = existing_pdf.getPage(0)
    for l in range(nump):
        output.addPage(existing_pdf.getPage(l))
    page.mergePage(new_pdf.getPage(0))
    outputStream = file("out-"+i, "wb")
    output.write(outputStream)
    outputStream.close()
    print fName + " written as", i

Summing up: I have a pdf, I add a text box to it, covering up info and adding new info, and then I print a pdf from that pdf. The text box becomes not editable or moveable any longer. I wanted to automate that process but everything I tried still allowed that text box to be editable.

5

There are 5 answers

6
naktinis On BEST ANSWER

If installing an OS package is an option, then you could use pdftk with its python wrapper pypdftk like this:

import pypdftk
pypdftk.fill_form('filled.pdf', out_file='flattened.pdf', flatten=True)

You would also need to install the pdftk package, which on Ubuntu could be done like this:

sudo apt-get install pdftk

The pypdftk library can by downloaded from PyPI:

pip install pypdftk

Update: pdftk was briefly removed from Ubuntu in version 18.04, but it seems it is back since 20.04.

4
ViaTech On

Per the Adobe Docs, you can change the Bit Position of the Editable Form Fields to 1 to make the field ReadOnly, see Document management - PDF part1 - 12.7.2 Interactive Form Dictionary and here for details.

I provided an alternative solution but with Django.

Use PyPDF2 to fill the fields, then loop through the annotations to change the bit position. Here an example which works on the 1st page:

from io import BytesIO

from PyPDF2 import PdfFileReader, PdfFileWriter
from PyPDF2.generic import BooleanObject, NameObject, NumberObject

# open the pdf
input_stream = open("YourPDF.pdf", "rb")
reader = PdfFileReader(input_stream, strict=False)
if "/AcroForm" in reader.trailer["/Root"]:
    reader.trailer["/Root"]["/AcroForm"].update(
        {NameObject("/NeedAppearances"): BooleanObject(True)}
    )

writer = PdfFileWriter()
writer.set_need_appearances_writer()
if "/AcroForm" in writer._root_object:
    # Acro form is form field, set needs appearances to fix printing issues
    writer._root_object["/AcroForm"].update(
        {NameObject("/NeedAppearances"): BooleanObject(True)}
    )

data_dict = dict()  # this is a dict of your form values

writer.addPage(reader.getPage(0))
page = writer.getPage(0)
# update form fields
writer.updatePageFormFieldValues(page, data_dict)
for j in range(0, len(page["/Annots"])):
    writer_annot = page["/Annots"][j].getObject()
    for field in data_dict:
        if writer_annot.get("/T") == field:
            # make ReadOnly:
            writer_annot.update({NameObject("/Ff"): NumberObject(1)})

# output_stream is your flattened PDF
output_stream = BytesIO()
writer.write(output_stream)
input_stream.close()

Update

As @MartinThoma pointed out in the comments, PyPDF2 is at end of life and isn't being maintained anymore (he is the maintainer). It is all back to the pypdf package. Nicely enough though, with the updates to pypdf, all I did was swap packages and the code works the same...I was not expecting that!

I have updated my code slightly from when I wrote this initially, but here is the updated version using the updated pypdf instead of PyPDF2:

from io import BytesIO

import pypdf
from pypdf.generic import NameObject, NumberObject, BooleanObject, IndirectObject


def fill_with_pypdf(file, data):
    """
    Used to fill PDF with PyPDF.
    To fill, PDF form must have field name values that match the dictionary keys

    :param file: The PDF being written to
    :param data: The data dictionary being written to the PDF Fields
    :return:
    """
    with open(file, "rb") as input_stream:
        # you don't actually need to wrap the BinaryIO in BytesIO but pycharm complained
        pdf_reader = pypdf.PdfReader(BytesIO(input_stream.read()), strict=False)

        data = {f"{{{{ {k} }}}}": v for k, v in data.items()}
        print(data)

        if "/AcroForm" in pdf_reader.trailer["/Root"]:
            print('here')
            pdf_reader.trailer["/Root"]["/AcroForm"].update(
                {NameObject("/NeedAppearances"): BooleanObject(True)})

        writer = pypdf.PdfWriter()
        # alter NeedAppearances
        try:
            catalog = writer._root_object
            # get the AcroForm tree and add "/NeedAppearances attribute
            if "/AcroForm" not in catalog:
                writer._root_object.update({
                    NameObject("/AcroForm"): IndirectObject(len(writer._objects), 0, writer)})

            need_appearances = NameObject("/NeedAppearances")
            writer._root_object["/AcroForm"][need_appearances] = BooleanObject(True)
        except Exception as e:
            print('set_need_appearances_writer() catch : ', repr(e))

        if "/AcroForm" in writer._root_object:
            # Acro form is form field, set needs appearances to fix printing issues
            writer._root_object["/AcroForm"].update(
                {NameObject("/NeedAppearances"): BooleanObject(True)})

        # loop over all pages
        for page_num in range(len(pdf_reader.pages)):
            writer.add_page(pdf_reader.pages[page_num])
            page = writer.pages[page_num]
            # loop over annotations, but ensure they are there first...
            if page.get('/Annots'):
                # update field values
                writer.update_page_form_field_values(page, data)
                for j in range(0, len(page['/Annots'])):
                    writer_annot = page['/Annots'][j].get_object()
                    # flatten all the fields by setting bit position to 1
                    # use loop below if only specific fields need to be flattened.
                    writer_annot.update({
                        NameObject("/Ff"): NumberObject(1)  # changing bit position to 1 flattens field
                    })
        output_stream = BytesIO()
        writer.write(output_stream)
        print('done')
        return output_stream.getvalue()


0
brendan8229 On

I had trouble flattening a form that I had entered content into using pdfrw (How to Populate Fillable PDF's with Python) and found that I had to add an additional step using generate_fdf (pdftk flatten loses fillable field data).

os.system('pdftk '+outtemp+' generate_fdf output '+outfdf)
os.system('pdftk '+outtemp+' fill_form '+outfdf+' output '+outpdf)

I came to this solution because I was able to flatten a file just fine using ghostscript's pdf2ps followed by ps2pdf on my Mac, but the quality had low resolution when I ran it on an Amazon Linux instance. I couldn't figure out why that was the case and so moved to the pdftk solution.

2
Tyler Houssian On

A simple but more of a round about way it to covert the pdf to images than to put those image into a pdf.

You'll need pdf2image and PIL

Like So

from pdf2image import convert_from_path 
from PIL import Image

images = convert_from_path('temp.pdf') 
im1 = images[0]
images.pop(0)

pdf1_filename = "flattened.pdf"

im1.save(pdf1_filename, "PDF" ,resolution=100.0, save_all=True, append_images=images)

Edit:

I created a library to do this called fillpdf

pip install fillpdf

from fillpdf import fillpdfs
fillpdfs.flatten_pdf('input.pdf', 'newflat.pdf')
0
Charalamm On

A solution that goes for Windows as well, converts many pdf pages and flatens the chackbox values as well. For some reason @ViaTech code did not work in my pc (Windows7 python 3.8)

Followed @ViaTech indications and used extensively @hchillon code from this post

from PyPDF2 import PdfFileReader, PdfFileWriter
from PyPDF2.generic import BooleanObject, NameObject, IndirectObject, TextStringObject, NumberObject


def set_need_appearances_writer(writer):

    try:
        catalog = writer._root_object
        # get the AcroForm tree and add "/NeedAppearances attribute
        if "/AcroForm" not in catalog:
            writer._root_object.update({
                NameObject("/AcroForm"): IndirectObject(len(writer._objects), 0, writer)})

        need_appearances = NameObject("/NeedAppearances")
        writer._root_object["/AcroForm"][need_appearances] = BooleanObject(True)
        return writer

    except Exception as e:
        print('set_need_appearances_writer() catch : ', repr(e))
        return writer



class PdfFileFiller(object):

    def __init__(self, infile):

        self.pdf = PdfFileReader(open(infile, "rb"), strict=False)
        if "/AcroForm" in self.pdf.trailer["/Root"]:
            self.pdf.trailer["/Root"]["/AcroForm"].update(
            {NameObject("/NeedAppearances"): BooleanObject(True)})

    # newvals and newchecks have keys have to be filled. '' is not accepted
    def update_form_values(self, outfile, newvals=None, newchecks=None):

        self.pdf2 = MyPdfFileWriter()


        trailer = self.pdf.trailer['/Root'].get('/AcroForm', None)
        if trailer:
            self.pdf2._root_object.update({
                NameObject('/AcroForm'): trailer})

        set_need_appearances_writer(self.pdf2)
        if "/AcroForm" in self.pdf2._root_object:
            self.pdf2._root_object["/AcroForm"].update(
            {NameObject("/NeedAppearances"): BooleanObject(True)})

        for i in range(self.pdf.getNumPages()):
            self.pdf2.addPage(self.pdf.getPage(i))

            self.pdf2.updatePageFormFieldValues(self.pdf2.getPage(i), newvals)
            for j in range(0, len(self.pdf.getPage(i)['/Annots'])):
                writer_annot = self.pdf.getPage(i)['/Annots'][j].getObject()
                for field in newvals:
                    writer_annot.update({NameObject("/Ff"): NumberObject(1)})

            self.pdf2.updatePageFormCheckboxValues(self.pdf2.getPage(i), newchecks)

        with open(outfile, 'wb') as out:
            self.pdf2.write(out)


class MyPdfFileWriter(PdfFileWriter):

    def __init__(self):
        super().__init__()

    def updatePageFormCheckboxValues(self, page, fields):

        for j in range(0, len(page['/Annots'])):
            writer_annot = page['/Annots'][j].getObject()
            for field in fields:
                writer_annot.update({NameObject("/Ff"): NumberObject(1)})




origin = ## Put input pdf path here
destination = ## Put output pdf path here, even if the file does not exist yet

newchecks = {} # A dict with all checkbox values that need to be changed
newvals = {'':''} # A dict with all entry values that need to be changed
# newvals dict has to be equal to {'':''} in case that no changes are needed

c = PdfFileFiller(origin)
c.update_form_values(outfile=destination, newvals=newvals, newchecks=newchecks)
print('PDF has been created\n')