Skip to content

Commit 3a4c869

Browse files
committed
fix(api): generate /ID for all saves if document lacks one
PDFs without an /ID array cause signature validation issues in Adobe Reader. Now the library ensures all saved PDFs have an /ID by either preserving the existing one or generating a new 16-byte random ID.
1 parent 6e90247 commit 3a4c869

File tree

2 files changed

+74
-3
lines changed

2 files changed

+74
-3
lines changed

src/api/pdf.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,69 @@ describe("PDF", () => {
375375
expect(appendedSection).toContain("trailer");
376376
expect(appendedSection).toContain("/ID");
377377
});
378+
379+
it("generates /ID if document lacks one (new document)", async () => {
380+
// Create a new document which won't have an /ID
381+
const pdf = PDF.create();
382+
383+
pdf.addPage();
384+
385+
// Full save should generate an /ID
386+
const saved = await pdf.save();
387+
const text = new TextDecoder().decode(saved);
388+
389+
// The trailer should include a generated /ID array
390+
expect(text).toContain("/ID");
391+
392+
// Verify the ID format: should be two identical 16-byte hex strings
393+
const idMatch = text.match(/\/ID\s*\[\s*<([a-fA-F0-9]+)>\s*<([a-fA-F0-9]+)>/);
394+
395+
expect(idMatch).not.toBeNull();
396+
397+
const [, id1, id2] = idMatch!;
398+
399+
// Both IDs should be 32 hex chars (16 bytes)
400+
expect(id1.length).toBe(32);
401+
expect(id2.length).toBe(32);
402+
// Both values should be identical for a newly generated ID
403+
expect(id1).toBe(id2);
404+
});
405+
406+
it("generates /ID if document lacks one (loaded PDF)", async () => {
407+
// pdf-overlay-page-1.pdf is a real PDF without an /ID
408+
const bytes = await loadFixture("scenarios", "pdf-overlay-page-1.pdf");
409+
const originalText = new TextDecoder().decode(bytes);
410+
411+
// Verify the original document lacks an /ID
412+
expect(originalText).not.toContain("/ID");
413+
414+
const pdf = await PDF.load(bytes);
415+
416+
// Modify to trigger a save
417+
const catalog = pdf.getCatalog();
418+
419+
catalog?.set("ModDate", PdfString.fromString("D:20240101"));
420+
421+
// Save should generate an /ID
422+
const saved = await pdf.save();
423+
const text = new TextDecoder().decode(saved);
424+
425+
// The trailer should include a generated /ID array
426+
expect(text).toContain("/ID");
427+
428+
// Verify the ID format: should be two identical 16-byte hex strings
429+
const idMatch = text.match(/\/ID\s*\[\s*<([a-fA-F0-9]+)>\s*<([a-fA-F0-9]+)>/);
430+
431+
expect(idMatch).not.toBeNull();
432+
433+
const [, id1, id2] = idMatch!;
434+
435+
// Both IDs should be 32 hex chars (16 bytes)
436+
expect(id1.length).toBe(32);
437+
expect(id2.length).toBe(32);
438+
// Both values should be identical for a newly generated ID
439+
expect(id1).toBe(id2);
440+
});
378441
});
379442

380443
describe("copyPagesFrom", () => {

src/api/pdf.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import type { StandardSecurityHandler } from "#src/security/standard-handler.ts"
3838
import type { SignOptions, SignResult } from "#src/signatures/types";
3939
import type { FindTextOptions, PageText, TextMatch } from "#src/text/types";
4040
import { writeComplete, writeIncremental } from "#src/writer/pdf-writer";
41+
import { randomBytes } from "@noble/ciphers/utils.js";
4142
import { deflate } from "pako";
4243

4344
import { PDFAttachments } from "./pdf-attachments";
@@ -2639,19 +2640,26 @@ export class PDF {
26392640
}
26402641
// Note: action === "remove" means no encrypt dict (decrypted on load, written without encryption)
26412642

2642-
// For incremental saves, preserve the document /ID if not already set
2643-
// This is required for digital signatures to validate properly
2644-
if (useIncremental && !fileId) {
2643+
// Ensure document has an /ID (required for signatures, recommended for all PDFs)
2644+
if (!fileId) {
26452645
const idArray = this.ctx.info.trailer.getArray("ID");
26462646

26472647
if (idArray && idArray.length >= 2) {
2648+
// Preserve existing ID
26482649
const id1 = idArray.at(0);
26492650
const id2 = idArray.at(1);
26502651

26512652
if (id1 instanceof PdfString && id2 instanceof PdfString) {
26522653
fileId = [id1.bytes, id2.bytes];
26532654
}
26542655
}
2656+
2657+
// Generate new ID if document doesn't have one
2658+
if (!fileId) {
2659+
const newId = randomBytes(16);
2660+
2661+
fileId = [newId, newId];
2662+
}
26552663
}
26562664

26572665
// For incremental saves, use the same XRef format as the original document

0 commit comments

Comments
 (0)