from pypdf import PdfReader
def signature_report(path):
reader = PdfReader(path)
if reader.root_object.get("/AcroForm") is None:
return {"signed_by": [], "empty_sig_fields": []}
signed, empty = [], []
for name, field in (reader.get_fields() or {}).items():
if field.get("/FT") == "/Sig":
(signed if field.get("/V") is not None else empty).append(name)
return {"signed_by": signed, "empty_sig_fields": empty}
report = signature_report("contract.pdf")
if report["signed_by"]:
raise SystemExit(f"already signed: {report['signed_by']}, do not rewrite this file")
# no form at all: {'signed_by': [], 'empty_sig_fields': []}
# empty signature box: {'signed_by': [], 'empty_sig_fields': ['Signature1']}
# actually signed: {'signed_by': ['Signature1'], 'empty_sig_fields': []}What counts as signed
A signature field can exist long before anyone signs it, and its field type is /Sig either way. What separates a placeholder from a completed signature is the value entry /V, which points at the signature dictionary holding the certificate and the byte range it covers. So an empty signature box lands in empty_sig_fields, and a real signature lands in signed_by. Checking for the presence of a signature field alone will tell you a document is signed when nobody has touched it yet.
If it is already signed
Do not read the file and write it back out with a writer that rebuilds the document. A full rewrite produces different bytes from the ones the signature covers, and validation fails afterwards. Appending your changes as an incremental update leaves the original bytes untouched at the front of the file, so the earlier signature still verifies. In pypdf that is the incremental flag on the writer, which the docs describe as writing the original document first and appending new or modified content, intended for signed documents and forms so signatures stay valid.
from pypdf import PdfWriter
# appends new and modified objects, leaving the original bytes in place
writer = PdfWriter("contract.pdf", incremental=True)
# ... make your changes ...
with open("contract-updated.pdf", "wb") as f:
writer.write(f)One caveat
This finds cryptographic signature fields only. If a tool drew a signature image into the page content and flattened the result, there is no /Sig field left to find and the report comes back empty. Read an empty result as no signature I can invalidate, rather than nobody has signed this.
Back to All Questions