Extract a signature from a PDF with Python (digital, scanned, e-signature)
Direct answer: “Extract a signature from a PDF” is three unrelated problems wearing the same sentence. A cryptographic signature (a /Sig form field with an embedded PKCS#7 certificate, the kind that proves a document hasn’t changed since signing) needs pyHanko or endesive to read and verify — pypdf can tell you the field exists but not whether it’s valid. A scanned or photographed signature (ink on paper, then digitized) needs OpenCV to find an irregular, non-rectangular dark region in roughly the signature zone. An e-signature platform signature (DocuSign, Adobe Sign, HelloSign) is usually just a signature-image overlay plus a separate audit-trail PDF — the image extracts like any embedded image, and the legal proof lives in the audit trail, not in pixels on the page. Figure out which of the three you actually have before writing code; the techniques don’t transfer between them.
Why one sentence hides three problems
A signature “on a PDF” can mean three structurally different things, and a PDF viewer renders all three identically — a scribble near a name, visually indistinguishable from one another on screen.
- Cryptographic signature field. The PDF spec defines a
/Sigfield type: a form field whose value is a PKCS#7 (or PAdES/CAdES) signature blob covering a/ByteRangeof the file. This is what Acrobat’s “Certificates” workflow and most compliance-grade e-signing tools produce. The guarantee is real — if the bytes change after signing, validation fails — but it’s a cryptographic check, not a pixel. - Scanned or photographed signature. Someone signed on paper, and the page was scanned or photographed. There’s no field, no certificate, nothing but a blob of ink captured as an image or as part of a flattened scan. Any “verification” here is visual, not cryptographic — a photocopy of a real signature looks identical to a forged one to software.
- E-signature platform output. Tools like DocuSign typically place a rendered signature image at a specific location and attach a separate certificate-of-completion PDF documenting IP address, timestamp, and consent. Some platforms also embed a
/Sigfield for PAdES-compliant signing; many consumer flows don’t. Check which you have — the legal weight is almost never in the pixels.
Treating a scanned signature’s absence like a failed cryptographic check, or treating a cryptographic field’s presence like proof a human actually drew that ink, is the mistake that costs people real debugging time. Each case gets its own section below.
Case 1: Cryptographic signature fields
Start by checking whether a /Sig field exists at all — this is fast and needs no extra dependencies.
pip install pypdf
from pypdf import PdfReader
def find_signature_fields(pdf_path: str) -> list[dict]:
reader = PdfReader(pdf_path)
fields = reader.get_fields() or {}
signatures = []
for name, field in fields.items():
if field.field_type != "/Sig":
continue
value = field.value # a dict-like object when signed, else None
signatures.append({
"field_name": name,
"signed": value is not None,
})
return signatures
print(find_signature_fields("contract.pdf"))
# [{'field_name': 'Signature1', 'signed': True}]
A non-empty result tells you a signature field exists and has a value — it does not tell you the signature is valid. For that you need a library that understands PKCS#7 and PAdES, not just the form-field layer. pyHanko is the actively maintained option and ships a CLI that avoids hand-rolling certificate-chain logic:
pip install "pyhanko[pkcs11]"
pyhanko sign validate --pretty-print contract.pdf
The CLI reports, per signature: whether the byte range it covers matches what’s actually in the file (document integrity), whether the certificate chain is trusted, and whether the signature falls inside the certificate’s validity window. For a programmatic check inside a pipeline rather than a CLI call, endesive is a lighter dependency that covers the common case — reading the embedded certificate and confirming the hash matches:
from endesive.pdf import verify
with open("contract.pdf", "rb") as f:
pdf_bytes = f.read()
results = verify(pdf_bytes)
for cert, pkcs7_data, signer_cert in results:
print(f"Signer: {signer_cert.get_subject()}")
Both libraries solve a harder problem than extraction — they’re verifying a mathematical property of the file, not reading a picture. If your actual goal is “does this contract have a legally binding signature,” this is the only case out of the three that can answer that question with certainty.
Case 2: Scanned or photographed signatures
No field, no certificate — just a blob of ink on an otherwise-flat page. This is a computer-vision problem, structurally close to flattened checkbox detection: find an irregular dark region in the expected area, then decide if its shape and density look like a signature rather than noise, a stamp, or a logo.
pip install pdf2image opencv-python-headless numpy
import cv2
import numpy as np
from pdf2image import convert_from_path
def find_signature_region(pdf_path: str, page_num: int = 1,
search_bottom_fraction: float = 0.35) -> list[dict]:
pages = convert_from_path(pdf_path, dpi=300, first_page=page_num, last_page=page_num)
img = np.array(pages[0].convert("L"))
height, width = img.shape
# Signatures usually sit in the bottom third of a signed page —
# narrowing the search window cuts false positives from body text and logos.
search_top = int(height * (1 - search_bottom_fraction))
region = img[search_top:, :]
_, binary = cv2.threshold(region, 180, 255, cv2.THRESH_BINARY_INV)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
candidates = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
area = w * h
if area < 2000 or area > 150_000:
continue # too small to be a signature, or too large to be just one
# A signature stroke pattern is wider than it is tall and irregular —
# unlike a printed line of text, which is uniform, or a logo, which
# is often closer to square.
aspect = w / h if h else 0
if not (1.5 <= aspect <= 8):
continue
fill = binary[y:y + h, x:x + w]
ink_ratio = float(np.mean(fill > 0)) if fill.size else 0.0
if not (0.05 <= ink_ratio <= 0.45):
continue # too sparse to be ink, or too dense to be a signature stroke
candidates.append({
"x": x, "y": y + search_top, "width": w, "height": h,
"ink_ratio": round(ink_ratio, 3),
})
return candidates
regions = find_signature_region("signed-agreement.pdf")
for r in regions:
print(f"Candidate signature at ({r['x']}, {r['y']}), ink_ratio={r['ink_ratio']}")
Three things decide whether this holds up outside a clean test sample:
- The search window is a heuristic, not a rule. Bottom-third works for most contracts and letters, but a multi-signatory agreement with a signature block table needs a different window — or no window, with filtering done entirely on shape and density.
- Aspect ratio and ink-density ranges need tuning per document population. The
1.5–8and0.05–0.45ranges here are reasonable starting points for a typical cursive signature at 300 DPI, not universal constants. Run this against 30-plus real signed pages and plot the distributions before trusting a cutoff, the same discipline that applies to checkbox fill-ratio thresholds. - Stamps, logos, and dense printed text all pass a naive filter. A company stamp is often within the same size and aspect window as a signature. If false positives are common in your documents, add a second check — ink distributed along a continuous stroke path (high contour-length-to-area ratio) looks different from a stamp’s solid fill or a logo’s sharp edges.
Once you have a candidate region, crop and save it as its own image for a human reviewer, or pass it forward for the vision-LLM fallback below if you need a yes/no answer rather than a bounding box.
Case 3: E-signature platform output
Documents that have already been through DocuSign, Adobe Sign, or similar tools are usually the easiest case in practice, because the signature image is a normal embedded image object, not a scan artifact.
import fitz # PyMuPDF
def extract_embedded_images(pdf_path: str, page_num: int = 0) -> list[dict]:
doc = fitz.open(pdf_path)
page = doc[page_num]
images = []
for img_index, img in enumerate(page.get_images(full=True)):
xref = img[0]
base_image = doc.extract_image(xref)
images.append({
"index": img_index,
"ext": base_image["ext"],
"width": base_image["width"],
"height": base_image["height"],
"bytes": base_image["image"],
})
return images
images = extract_embedded_images("docusign-completed.pdf")
for img in images:
with open(f"image_{img['index']}.{img['ext']}", "wb") as f:
f.write(img["bytes"])
page.get_images(full=True) lists every embedded image object on the page without you having to guess coordinates — a signature rendered by an e-signature platform is usually small, roughly the aspect ratio of a signature, and positioned near a “Signed by” label, so filtering the list by size and position narrows it down quickly. But the image itself proves nothing on its own: the actual evidentiary record for these platforms is the separate certificate-of-completion PDF (timestamps, IP addresses, consent language) that most of them generate alongside the signed document, not the pixels. If a counterparty disputes a signature from one of these tools, that certificate — not the extracted image — is what settles it.
When pixel detection isn’t precise enough: vision-LLM fallback
Skewed photographs, signatures that overlap printed text, or documents where the “bottom third” heuristic doesn’t apply all push the false-positive rate on the OpenCV approach up fast. pdfmux has no built-in signature preset — the same honest gap as its checkbox and financial-statement handling — but its schema-driven BYOK vision-LLM routing handles a yes/no-plus-location question without any threshold tuning:
from pydantic import BaseModel
from pdfmux import process
class SignaturePresence(BaseModel):
has_signature: bool
approximate_location: str # e.g. "bottom right of page 3"
result = process(
"contract-scan.pdf",
quality="standard",
output_format="json",
schema="signature_presence.json", # JSON Schema mirroring SignaturePresence above
llm_provider="gemini",
)
This costs per-page instead of running free on CPU, and it answers “is there a signature here” rather than returning pixel coordinates you can crop — for a bounding box, OpenCV or a dedicated object-detection model is still the right tool. For hardest-case pages — rotated, low-contrast, overlapping text and ink — routing through a vision model is often faster to get working than tuning a contour filter, which is the same trade pdfmux makes for handwritten text more generally.
Choosing an approach
| Situation | Method | Answers | Cost |
|---|---|---|---|
| Need to know if a signature is legally valid | pyHanko / endesive | Cryptographic validity | Free |
| Field exists, just need presence (not validity) | pypdf.get_fields() | Field exists + has a value | Free |
| Scanned/photographed ink signature, clean layout | OpenCV contour + ink-density filter | Bounding box, no validity | Free, needs tuning |
| Skewed, overlapping, or ambiguous signature region | Vision LLM with schema | Presence + rough location, no validity | Per-page fee |
| DocuSign/Adobe Sign output | PyMuPDF image extraction + accompanying certificate PDF | Image + legal proof (in the certificate, not the pixel) | Free |
The question “is this signature real” only has a true answer in Case 1 — everywhere else, you’re locating an image, not verifying a cryptographic fact. Decide which question you’re actually answering before picking a tool.
Production checklist
- Check for a
/Sigform field withpypdf.get_fields()before assuming you have a scan - If validity matters, use
pyHankoorendesive— never infer validity from field presence alone - For scanned signatures, narrow the search region before running contour detection
- Tune aspect-ratio and ink-density thresholds against your own document sample, not the defaults above
- For e-signature platform PDFs, keep the certificate-of-completion file — it carries the legal weight, not the image
- Fall back to a vision-LLM schema for skewed, overlapping, or otherwise ambiguous signature regions
Keep reading
- Detect checkboxes in a PDF with Python — the same three-case structure (field, glyph, pixel) applied to a different form element
- Extract data from fillable PDF forms in Python — reading AcroForm fields beyond signatures
- Extract handwritten text from a PDF in Python — OCR techniques for the ink-on-paper case
- Detect and redact PII in a PDF with Python — signatures are personal data too; what to do once you’ve located one
Last updated: October 2026.
Frequently asked questions
Can pypdf tell me if a PDF signature is cryptographically valid?
No. pypdf's get_fields() will tell you a /Sig field exists and give you its byte range, but it does nothing with the embedded PKCS#7 certificate — it doesn't check the hash, the certificate chain, or whether the document changed after signing. Validity checking is a separate, harder problem that needs pyHanko or a similar signing-aware library.
How do I know if a signature in a PDF is 'real' or just a picture?
Open the PDF's form fields with pypdf or pikepdf. If there's a field with /FT /Sig and a /V dictionary containing /Contents (the signature bytes) and /ByteRange, it's a cryptographic signature tied to the document's actual bytes. If there's no such field — just a JPEG or PNG of someone's signature sitting on the page — it's a picture with no cryptographic guarantee at all, no matter how official the PDF looks.
Does pdfmux extract or verify signatures?
No — pdfmux has no built-in signature preset, digital or image-based. It's a content-extraction tool, not a PDF-forensics tool, so cryptographic validation is out of scope entirely. For locating a signature image on a scanned page, you can route the page to pdfmux's BYOK vision-LLM backend with a custom schema, the same pattern used for checkboxes and financial statements — see the fallback section below.
Why does OpenCV find my company logo instead of the signature?
Logos and signatures both produce irregular, non-rectangular ink blobs, so a size-and-position filter alone isn't enough. Narrow the search to the expected region (bottom third of the last page, or near a 'Signed:' label) and check ink density and aspect ratio together — a signature stroke pattern is denser and more elongated than most logos, but tune the thresholds against your own documents rather than trusting defaults.