Preserve OBJR children without restoring deleted pages

Skip OBJR entries whose targets were not copied with a retained page.
This commit is contained in:
Calixte Denizet 2026-07-13 11:05:51 +02:00
parent 0b613612ed
commit 8070e9dce1
2 changed files with 90 additions and 2 deletions

View File

@ -620,10 +620,15 @@ class PDFEditor {
if (!kidRef) { if (!kidRef) {
continue; continue;
} }
const newKidRef = oldRefMapping.get(kidRef); // Only keep the reference when its target was actually copied. A link
if (!newKidRef) { // annotation targeting a removed page is dropped, so skip its OBJR.
const oldObjRef = kid.getRaw("Obj");
if (oldObjRef instanceof Ref && !oldRefMapping.get(oldObjRef)) {
continue; continue;
} }
const newKidRef =
oldRefMapping.get(kidRef) ||
(await this.#collectDependencies(kidRef, true, xref));
const newKid = this.xref[newKidRef.num]; const newKid = this.xref[newKidRef.num];
// Fix the missing StructParent entry in the referenced object. // Fix the missing StructParent entry in the referenced object.
const objRef = newKid.getRaw("Obj"); const objRef = newKid.getRaw("Obj");

View File

@ -17,6 +17,7 @@ import {
AnnotationEditorType, AnnotationEditorType,
AnnotationMode, AnnotationMode,
AnnotationType, AnnotationType,
bytesToString,
DrawOPS, DrawOPS,
ImageKind, ImageKind,
InvalidPDFException, InvalidPDFException,
@ -6718,6 +6719,88 @@ small scripts as well as for`);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
it("preserves OBJR structure children", async function () {
const objects = [
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R " +
"/StructTreeRoot 4 0 R >>\nendobj\n",
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R /StructParents 0 " +
"/MediaBox [0 0 100 100] /Annots [8 0 R] >>\nendobj\n",
"4 0 obj\n<< /Type /StructTreeRoot /K [5 0 R] " +
"/ParentTree 7 0 R /ParentTreeNextKey 2 >>\nendobj\n",
"5 0 obj\n<< /Type /StructElem /S /Document /P 4 0 R " +
"/K 6 0 R >>\nendobj\n",
"6 0 obj\n<< /Type /StructElem /S /Link /P 5 0 R /Pg 3 0 R " +
"/K 9 0 R >>\nendobj\n",
"7 0 obj\n<< /Nums [0 [6 0 R] 1 6 0 R] >>\nendobj\n",
"8 0 obj\n<< /Type /Annot /Subtype /Link /Rect [0 0 10 10] " +
"/StructParent 1 >>\nendobj\n",
"9 0 obj\n<< /Type /OBJR /Obj 8 0 R /Pg 3 0 R >>\nendobj\n",
];
let loadingTask = getDocument({ data: assemblePdf(objects) });
let pdfDoc = await loadingTask.promise;
let page = await pdfDoc.getPage(1);
let tree = await page.getStructTree();
expect(tree.children[0].children[0].children[0].type).toEqual(
"annotation"
);
const data = await pdfDoc.extractPages([{ document: null }]);
await loadingTask.destroy();
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
page = await pdfDoc.getPage(1);
tree = await page.getStructTree();
expect(tree.children.length).toEqual(1);
expect(tree.children[0].role).toEqual("Document");
expect(tree.children[0].children[0].role).toEqual("Link");
expect(tree.children[0].children[0].children[0].type).toEqual(
"annotation"
);
await loadingTask.destroy();
});
it("doesn't resurrect removed pages referenced through an OBJR", async function () {
const objects = [
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R " +
"/StructTreeRoot 4 0 R >>\nendobj\n",
"2 0 obj\n<< /Type /Pages /Kids [3 0 R 10 0 R] " +
"/Count 2 >>\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R /StructParents 0 " +
"/MediaBox [0 0 100 100] /Annots [8 0 R] >>\nendobj\n",
"4 0 obj\n<< /Type /StructTreeRoot /K [5 0 R] " +
"/ParentTree 7 0 R /ParentTreeNextKey 2 >>\nendobj\n",
"5 0 obj\n<< /Type /StructElem /S /Document /P 4 0 R " +
"/K 6 0 R >>\nendobj\n",
"6 0 obj\n<< /Type /StructElem /S /Link /P 5 0 R /Pg 3 0 R " +
"/K 9 0 R >>\nendobj\n",
"7 0 obj\n<< /Nums [0 [6 0 R] 1 6 0 R] >>\nendobj\n",
"8 0 obj\n<< /Type /Annot /Subtype /Link /Rect [0 0 10 10] " +
"/StructParent 1 /A << /S /GoTo /D [10 0 R /Fit] >> >>\nendobj\n",
"9 0 obj\n<< /Type /OBJR /Obj 8 0 R /Pg 3 0 R >>\nendobj\n",
"10 0 obj\n<< /Type /Page /Parent 2 0 R " +
"/MediaBox [0 0 100 100] >>\nendobj\n",
];
let loadingTask = getDocument({ data: assemblePdf(objects) });
let pdfDoc = await loadingTask.promise;
const data = await pdfDoc.extractPages([
{ document: null, includePages: [0] },
]);
await loadingTask.destroy();
expect(data).toBeInstanceOf(Uint8Array);
const output = bytesToString(data);
const pageObjects = output.match(/\/Type\s*\/Page(?![A-Za-z])/g) || [];
expect(pageObjects.length).toEqual(1);
loadingTask = getDocument({ data });
pdfDoc = await loadingTask.promise;
expect(pdfDoc.numPages).toEqual(1);
await loadingTask.destroy();
});
it("extract pages and merge struct trees", async function () { it("extract pages and merge struct trees", async function () {
let loadingTask = getDocument( let loadingTask = getDocument(
buildGetDocumentParams("two_paragraphs.pdf") buildGetDocumentParams("two_paragraphs.pdf")