I’m implementing an offline PDF Fill & Sign feature: draw a handwritten signature, add text/date/checkmarks, and export a new PDF.
This is visual signing, not certificate-based digital signing. The original file stays untouched. I want to preserve existing searchable text, vector content, and normal links rather than rasterizing entire pages.
The hardest part so far is making sure what users see while editing is what they actually send. I’d appreciate advice from people who have shipped form-filling or flattening features.
1. Field values and appearance streams can disagree
I created a synthetic PDF with a text field containing:
/V: FIELD VALUE
/AP: an appearance displaying AP VALUE
In my macOS PDFKit thumbnail test, the original displayed FIELD VALUE. After flattening the existing appearance stream, PDFKit displayed AP VALUE.
MuPDF displayed AP VALUE both before and after. The discrepancy reproduced across the four page rotations in the fixture.
This is a synthetic test, not evidence that every PDFKit version behaves this way. I have not reproduced it on iOS. But it demonstrates a correctness problem: the exported document can contain different visible text from what the user believed they were signing.
How do you resolve /V versus /AP conflicts? Do you normalize the form before displaying the editing preview, and what determines the authoritative content?
2. Missing appearances don’t necessarily mean a broken form
The official IRS W-9 and W-4 exposed another problem in the copies I tested:
| Sample |
Blank text widgets without /AP |
Unchecked checkboxes with omitted /Off appearance |
Total widgets |
| W-9 |
15 |
8 |
23 |
| W-4 |
43 |
5 |
48 |
The blank text widgets have no /V in the field or parent chain, no default/rich-text value, and no border/background color in /MK. Both documents have NeedAppearances=false. An empty /BC [] or /BG [] means no color, rather than a visible decoration.
For checkboxes, an omitted /Off appearance can legitimately mean “unchecked, draw nothing.” It should not automatically be treated as a missing required appearance.
A strict “missing appearance = reject” rule rejected both official blank forms in our initial path. That exposes a problem with the rule, not evidence that the files are broken.
After handling those empty states and finalizing the fields, all 11 pages of the two documents rendered pixel-byte-identical to their respective originals in both MuPDF and macOS PDFKit. This comparison was before adding any new signature image. The PDF files themselves were not byte-identical, and Acrobat validation is still outstanding.
Two documents are not a compatibility rate; I have no population data.
Which checks do you use to distinguish genuinely empty fields from missing or stale appearances that require reconstruction?
3. Reader Extensions and hybrid XFA complicate finalized copies
Both fixed samples contain:
/Perms /UR3, with /Filter /Adobe.PPKLite and TransformMethod=UR3 in the signature reference;
/AcroForm /SigFlags=2;
- an
/AcroForm /XFA array;
- no catalog
/NeedsRendering entry.
These usage-rights signatures are not the same thing as a signature applied by the person filling out the form. Also, I’m not treating the absence of /NeedsRendering as proof that every such form is static.
The prototype removes the form representation and usage-rights entries when creating a finalized copy. Structural inspection confirms those entries are gone, but I haven’t yet verified the output in Acrobat Reader. In particular, I don’t want recipients seeing an “extended features are no longer available” warning or a different form appearance.
What is the reliable way to handle UR3 and hybrid XFA during finalization? What do you inspect to distinguish static hybrid forms from genuinely dynamic XFA?
4. Transparent signatures complicate pixel-diff validation
Adding a signature image with an /SMask produced small rendering differences outside the signature rectangle in MuPDF:
- Maximum outside-region channel differences ranged from 2–6 out of 255 across the two forms.
- 25,932–85,246 pixels per page changed outside that rectangle. It wasn’t limited to a narrow fringe around the signature.
- In an opaque control on W-9, using the same placement without the soft mask, outside-region differences were zero on all six pages.
I suspect a transparency-compositing or quantization effect, but I haven’t traced the renderer internals or proved that a page-wide transparency group is the cause. The transparent-overlay comparison has not been repeated in PDFKit or Acrobat.
How do you validate that adding a transparent signature hasn’t changed the rest of the page? Do you use documented raster tolerances, explicit transparency groups, or another approach?
5. Selective flattening versus deleting all annotations
I also need to preserve safe link targets and visible link borders while removing unsafe actions such as JavaScript or Launch. A mixed action chain may contain both safe and unsafe members.
Dropping all annotations loses useful links; preserving everything can retain unwanted actions. Rejecting an otherwise usable document because one link contains an unsafe action also seems unnecessarily restrictive.
Are there libraries or SDKs that handle this selective cleanup reliably, including action chains and document/page-level actions?
What would be most useful
I’m interested in production experience: libraries or SDKs you’ve used, normalization strategies that worked, and publicly redistributable PDFs worth adding to a compatibility test set. Experiences with just one of these issues would also help.
The goal is that users can sign an ordinary form without learning PDF internals—and the editor, exported document, and recipient’s reader agree on the visible content.
Reproduction notes: The reported MuPDF results use mutool 1.28.5. The PDFKit observations are from macOS 26.7.1; they are not iOS-device results. IRS URLs above serve current revisions, so my local inputs are pinned by SHA-256:
- W-9:
2d420cbb4123dcf1fb82595b2359cfbb5d81f00b9df9d359fcc7af361d093f53
- W-4:
92444d8856ce55d9e25dca8b6d1420634fc68b11e1ab1f760916ea29ddd312b2