PDFFence is local-first PDF change assurance for CI and review workflows. It compares bounded, non-executing inventories of stored PDF surfaces that can matter in a handoff: incremental revisions and terminal-footer evidence, actions, embedded files, forms, annotations, associated-file topology, optional-content topology, signature structure, ByteRange current-file and Contents-boundary evidence, ByteRange top-level-value directness evidence, encryption, and metadata presence.
The report is deliberately privacy-safe. It contains fixed categories, counts, booleans, and generic change messages; it never emits page text, JavaScript, URLs, attachment names or bytes, form names or values, metadata values, certificate material, file paths, or hashes. A declared batch can carry an explicitly public opaque review ID, never its private source path.
PDFFence reads a local PDF without rendering it, executing an action, following a URI, extracting text, opening an attachment, or sending file material over the network. It gives CI a stable answer to questions such as:
- Did an embedded-file stream or associated-file relationship appear?
- Did an action inventory gain or exchange JavaScript, navigation (including document-part and 3D-view navigation), launch, media, or URI action types?
- Did a JavaScript, URI, launch, navigation, form, media, transition, or
RichMedia action's selected stored behavior fields change at a standard
action root or semantic
/Nextmember while its public action inventory remained fixed? - Did selected active content move between document-open or other standard action triggers while the payload set and public inventory remained fixed?
- Did executable action successors exchange their stored positions in a /Next sequence while the payload set and public inventory remained fixed?
- Did action subtypes themselves exchange successor positions while the aggregate action inventory and selected payload evidence remained fixed?
- Did same-subtype successors with different stored non-/Next action members exchange positions while public inventory, selected payload evidence, and subtype-order evidence remained fixed?
- Did a local GoTo action, document-open destination, Link annotation, or outline item retain its explicit, named, or PDF 2.0 structure target when its stored navigation root and public inventory remained fixed?
- Did a PDF 2.0 GoToDp action retain its selected document part, rather than silently rebinding to a different member of the catalog DPart tree?
- Did a GoTo3DView action retain its selected 3D or RichMedia annotation and view, without mistaking target metadata or page state for a semantic rewrite?
- Did a same-root embedded GoTo action retain its catalog-named or FileAttachment-selected child and target-bearing file specification without mistaking descriptive annotation/FileSpec metadata or unrelated mappings for a target rewrite?
- Did a SetOCGState action retain its selected optional-content groups, state operations, and radio-button behavior without mistaking group metadata for a semantic rewrite?
- Did a remote GoTo action retain its effective destination, including a PDF 2.0 remote structure-destination identifier, without opening its target document?
- Did a structurally confirmed incremental revision chain grow?
- Does each PDF's final linked revision footer end its physical source, apart from one optional EOL?
- Did forms, annotations, XFA, RichMedia, a portfolio, or semantic signature structure change?
- Did a semantic signature's well-formed
/ByteRangestop reaching the current stored PDF file after an incremental update? - Does a PDF with semantic signature roots lack any well-formed
/ByteRangethat reaches its current stored file end, even when there is no known-good comparison baseline? - Does a current-file semantic signature ByteRange's one excluded gap exactly
match a direct hexadecimal
/Contentsstring? - Does every top-level value in a semantic signature dictionary with a
/ByteRangeremain a direct PDF object? - Does every semantic signature
/ByteRangeend at the footer of the revision that contains that signature dictionary, including when later revisions exist? - Did catalog-declared optional-content groups or configuration topology change?
- Did the stored PDF bytes change even though the public inventory did not?
- Did a declared handoff batch add or remove a review item, even when its private source path or a rename must not appear in CI output?
Behavior-field evidence and public action inventory are path-based, rather
than inferred from a dictionary key name. The current supported roots are
catalog document-open and additional-action entries, catalog JavaScript
name-tree values, catalog-page tree page and annotation entries, AcroForm
fields and widgets, outline items, and page presentation-step NavNodes;
semantic /Next members follow from those roots. Private action-looking data
can still affect stored-byte or reachability evidence, but cannot alone create
an active-content inventory change. See the policy and threat-model documents
for the exact boundary.
Signature evidence is path-based too. PDFFence recognizes /Type /Sig
dictionaries only as values of catalog-reachable AcroForm signature fields
(including inherited field types) or catalog /Perms DocMDP, UR, and UR3
entries. For those roots alone it reports aggregate counts for present
/ByteRange values, well-formed ordered ranges beginning at byte zero, and
ranges whose final endpoint is the current physical file end. It additionally
counts a narrower positive case only when a bounded raw scan of an
xref-addressable direct signature dictionary finds one direct hexadecimal
/Contents string and a two-pair current-file ByteRange whose excluded gap is
exactly that token. Separately, it counts semantic signature dictionaries with
/ByteRange whose every top-level value is stored directly rather than as an
indirect reference. It also counts xref-addressable indirect signature
dictionaries whose well-formed /ByteRange ends at the unambiguous %%EOF
footer (or its optional EOL) of the revision containing that object. Positions,
object references, revision boundaries, and signature bytes never appear in
output.
Private signature-shaped data can still affect stored-byte or reachability
evidence, but not signature structure or coverage evidence.
The opt-in require_current_file_signature_coverage rule (PFP010) treats a
document with one or more semantic signature roots and no current-file
ByteRange as a review failure. It does not require unsigned PDFs to gain a
signature, and it does not decide whether a later revision is valid, permitted,
or malicious.
The opt-in require_contents_bound_current_signature_coverage rule (PFP011)
is a stricter static review gate: a document with semantic signature roots must
have at least one of those bounded, direct-Contents matches. It is deliberately
conservative; a raw signature object that is unavailable, indirect, malformed,
or beyond the scanner's configured limits does not earn this evidence.
The opt-in require_direct_byte_range_signature_values rule (PFP012) is a
separate PDF object-layout gate. It fails when a semantic signature dictionary
with /ByteRange has an indirect top-level value. It does not require unsigned
PDFs to gain a signature, does not depend on a current-file boundary, and does
not resolve, reveal, or validate the referenced value. Its separate diff event
is limited to a stable semantic ByteRange population so added or removed
signature roots remain signature-structure evidence rather than a redundant
directness change.
The opt-in require_signature_byte_range_own_revision_coverage rule (PFP013)
requires this own-revision evidence for every semantic signature dictionary
with /ByteRange on both sides of a comparison. Unlike PFP010, it permits a
correct older signature after a later incremental update: the range must end
at its signing revision, not necessarily at the current physical file end.
The mapping is deliberately conservative and bounded; an unaddressable,
ambiguous, malformed, over-limit, or non-terminal final revision does not earn
positive evidence. Bytes after the final linked %%EOF must belong
to a valid later revision; arbitrary trailing bytes do not preserve
own-revision credit.
The opt-in require_contents_bound_own_revision_signature_coverage rule
(PFP014) tightens that historical-signature boundary without making it a
current-file requirement. Every semantic /ByteRange must use exactly two
pairs whose sole excluded span is the direct hexadecimal /Contents token in
that signature dictionary, and its final endpoint must be the footer of that
same signature's revision. Thus a correctly bounded older signature can pass
after later incremental updates. Raw-object and revision mapping limits remain
fail-closed. Its separate diff signal is emitted only when the existing
ByteRange-coverage and own-revision aggregates are stable, keeping PFP011 and
PFP013 as the non-duplicative signals for their respective boundary changes.
The opt-in require_terminal_revision_footer rule (PFP015) is an independent
structural review gate. It requires a bounded, unambiguous footer map and a
final linked %%EOF marker that ends each physical source apart from its one
optional EOL. It applies whether a PDF is unsigned, signed, or encrypted, and
does not require a single revision: a normal linked incremental update can
pass. nonterminal and unavailable footer status both fail closed. Its
dedicated diff event exposes only one of those three status labels, never an
offset, footer location, or source bytes.
For a release or handoff containing many PDFs, pdffence batch compares an
explicit JSON manifest against separate before and after roots. It never
discovers filenames or guesses matches: the manifest can pair a rename and
declare an addition or removal. Paths and roots remain private; each result is
identified only by a caller-chosen, deliberately public opaque ID. The batch
policy rule require_same_document_set (PFP016) makes a declared addition or
removal fail CI without changing single-PDF policy behavior. See
the batch manifest contract.
PDFFence is not a renderer, OCR engine, malware sandbox, PDF/A or PDF/UA validator, cryptographic signature validator, certificate or trust-chain validator, or proof that two PDFs look the same. A zero finding does not prove that a PDF is safe or that two rendered documents are identical.
ByteRange evidence is not a cryptographic validation result. The narrower
Contents-boundary check locates delimiters only internally and never emits their
positions or bytes; it does not validate /Contents, calculate a digest,
validate a certificate, evaluate a DocMDP or permissions transform, or
determine a viewer's handling of an incremental update.
Direct-value evidence likewise classifies only top-level stored-object layout;
it does not validate the signature dictionary's semantics, digest, contents,
certificate, or trust result.
Own-revision evidence likewise does not validate a signature or decide whether
an incremental update is valid, permitted, authentic, or safe.
Own-revision Contents-bound evidence likewise classifies only a stored
two-pair layout and delimiters; it does not validate the signature value,
digest, certificate, transform, permission, or trust result.
Terminal-footer evidence likewise does not authenticate a revision, validate
the PDF as a whole, decide whether a linked update is permitted, or establish
that a viewer will handle the source safely.
Optional-content reporting is structural only. It does not determine which layer content a PDF viewer will show, parse page content streams, or evaluate viewer-specific layer behavior.
Encrypted PDFs are identified but deliberately not decrypted or otherwise inventoried. A malformed or oversized PDF fails closed with a generic error.
PDFFence is distributed through GitHub Releases. Install the current tagged wheel directly:
python -m pip install https://github.com/SybilGambleyyu/pdffence/releases/download/v1.26.0/pdffence-1.26.0-py3-none-any.whl
Each release includes a source archive and SHA-256 checksum manifest. A PyPI
distribution is not currently published, so pip install pdffence is not a
supported install command.
Inventory a single PDF:
pdffence profile document.pdf
Compare two revisions:
pdffence diff before.pdf after.pdf --format markdown
Fail CI when a conservative policy finds a change:
pdffence init pdffence.yml
pdffence check before.pdf after.pdf --policy pdffence.yml --format sarif
Review a declared batch without publishing its paths. Manifest IDs are public, so use opaque labels:
pdffence batch-init baseline/ candidate/ review.json
pdffence batch baseline/ candidate/ --manifest review.json --policy pdffence.yml --format sarif
The generated manifest itself contains private relative paths; store it only where those paths are appropriate. It pairs same-relative-path PDFs and leaves renames as explicit entries for a reviewer to pair deliberately.
See the batch manifest contract, threat model, policy format, and validation evidence in docs/.
PDFFence is alpha software. Its supported inventory is intentionally smaller than the PDF format. The boundary is documented so that reports remain useful evidence rather than an overbroad security claim.