Skip to content

Commit 5e7c260

Browse files
committed
fix(annotations): preserve indirect Annots refs and add removeLinks option
When flattening annotations: - Modify indirect Annots arrays in-place to preserve indirection, which prevents signature validation issues when signing later - Add removeLinks option to remove Link annotations (Adobe considers URI actions as 'hidden behavior' that cause validation warnings) - Default removeLinks to true in flattenAll - Reuse existing appearance stream refs when already registered
1 parent ad87805 commit 5e7c260

File tree

3 files changed

+87
-16
lines changed

3 files changed

+87
-16
lines changed

src/annotations/flattener.ts

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,17 @@ export class AnnotationFlattener {
162162
continue;
163163
}
164164

165-
// Skip non-flattenable types
165+
// Handle Link annotations specially
166+
if (subtype === "Link") {
167+
if (options.removeLinks) {
168+
// Remove Link annotations for signing security (they contain "hidden behavior")
169+
if (annotRef) {
170+
refsToRemove.add(`${annotRef.objectNumber} ${annotRef.generation}`);
171+
}
172+
}
173+
}
174+
175+
// Skip other non-flattenable types
166176
if (NON_FLATTENABLE_TYPES.includes(subtype)) {
167177
continue;
168178
}
@@ -212,12 +222,17 @@ export class AnnotationFlattener {
212222
continue;
213223
}
214224

215-
// Normalize appearance stream
225+
// Normalize appearance stream (only if needed)
216226
this.normalizeAppearanceStream(appearance);
217227

218-
// Add appearance as XObject
228+
// Add appearance as XObject - reuse existing ref if already registered
219229
const xObjectName = `FlatAnnot${xObjectIndex++}`;
220-
const appearanceRef = this.registry.register(appearance);
230+
let appearanceRef = this.registry.getRef(appearance);
231+
232+
if (!appearanceRef) {
233+
appearanceRef = this.registry.register(appearance);
234+
}
235+
221236
xObjects.set(xObjectName, appearanceRef);
222237

223238
// Calculate transformation matrix
@@ -267,7 +282,6 @@ export class AnnotationFlattener {
267282
* Generate appearance for annotation types we support.
268283
*/
269284
private generateAppearance(annotation: PDFAnnotation): PdfStream | null {
270-
const type = annotation.type;
271285
const rect = annotation.rect;
272286

273287
// Use instanceof checks for annotation types instead of a switch on `type`
@@ -569,39 +583,78 @@ export class AnnotationFlattener {
569583

570584
/**
571585
* Remove specific annotations from page.
586+
*
587+
* IMPORTANT: If Annots is an indirect reference, we modify the array in-place
588+
* to preserve the indirection. This is critical for signing: if we convert
589+
* an indirect Annots to a direct array, then later signing will convert it
590+
* back to indirect, modifying the page object and potentially breaking
591+
* signature validation.
572592
*/
573593
private removeAnnotations(page: PdfDict, toRemove: Set<string>): void {
574594
if (toRemove.size === 0) {
575595
return;
576596
}
577597

578-
let annots = page.get("Annots");
598+
const annotsEntry = page.get("Annots");
599+
600+
if (!annotsEntry) {
601+
return;
602+
}
603+
604+
// Track if Annots was indirect - we need to preserve this
605+
const wasIndirect = annotsEntry instanceof PdfRef;
606+
let annots: PdfArray | undefined;
579607

580-
if (annots instanceof PdfRef) {
581-
annots = this.registry.resolve(annots) ?? undefined;
608+
if (annotsEntry instanceof PdfRef) {
609+
const resolved = this.registry.resolve(annotsEntry);
610+
annots = resolved instanceof PdfArray ? resolved : undefined;
611+
} else if (annotsEntry instanceof PdfArray) {
612+
annots = annotsEntry;
582613
}
583614

584-
if (!(annots instanceof PdfArray)) {
615+
if (!annots) {
585616
return;
586617
}
587618

588-
const remaining: PdfRef[] = [];
619+
// Find indices to remove (in reverse order for safe removal)
620+
const indicesToRemove: number[] = [];
589621

590622
for (let i = 0; i < annots.length; i++) {
591623
const item = annots.at(i);
592624

593625
if (item instanceof PdfRef) {
594626
const key = `${item.objectNumber} ${item.generation}`;
595627

596-
if (!toRemove.has(key)) {
597-
remaining.push(item);
628+
if (toRemove.has(key)) {
629+
indicesToRemove.push(i);
598630
}
599631
}
600632
}
601633

602-
if (remaining.length === 0) {
603-
page.delete("Annots");
604-
} else if (remaining.length < annots.length) {
634+
if (indicesToRemove.length === 0) {
635+
return;
636+
}
637+
638+
// If Annots was indirect, modify in-place to preserve indirection
639+
if (wasIndirect) {
640+
// Remove in reverse order to maintain valid indices
641+
for (let i = indicesToRemove.length - 1; i >= 0; i--) {
642+
annots.remove(indicesToRemove[i]);
643+
}
644+
} else {
645+
// Annots was direct - build a new array with remaining items
646+
const remaining: PdfRef[] = [];
647+
648+
for (let i = 0; i < annots.length; i++) {
649+
if (!indicesToRemove.includes(i)) {
650+
const item = annots.at(i);
651+
652+
if (item instanceof PdfRef) {
653+
remaining.push(item);
654+
}
655+
}
656+
}
657+
605658
page.set("Annots", PdfArray.of(...remaining));
606659
}
607660
}

src/annotations/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,4 +486,15 @@ export interface RemoveAnnotationsOptions {
486486
export interface FlattenAnnotationsOptions {
487487
/** Annotation types to exclude from flattening */
488488
exclude?: AnnotationSubtype[];
489+
490+
/**
491+
* Remove Link annotations instead of keeping them.
492+
*
493+
* Link annotations contain URI or other actions that Adobe considers
494+
* "hidden behavior" which can cause signature validation warnings.
495+
* Enable this option when preparing documents for signing.
496+
*
497+
* @default false
498+
*/
499+
removeLinks?: boolean;
489500
}

src/api/pdf.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2585,7 +2585,14 @@ export class PDF {
25852585
}
25862586

25872587
// Flatten annotations last (may reference form widgets which are now gone)
2588-
const annotations = this.flattenAnnotations(options?.annotations);
2588+
// By default, remove Link annotations since they contain actions that
2589+
// Adobe considers "hidden behavior" which can cause signature validation warnings.
2590+
const annotationOptions = {
2591+
removeLinks: true,
2592+
...options?.annotations,
2593+
};
2594+
2595+
const annotations = this.flattenAnnotations(annotationOptions);
25892596

25902597
return {
25912598
layers: layerResult.layerCount,

0 commit comments

Comments
 (0)