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()