Skip to content

Commit 04272de

Browse files
committed
Add the possibility to save added annotations when reorganizing a pdf (bug 2023086)
1 parent ff1af5a commit 04272de

16 files changed

Lines changed: 444 additions & 164 deletions

src/core/annotation.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,12 +354,12 @@ class AnnotationFactory {
354354

355355
static async saveNewAnnotations(
356356
evaluator,
357+
xref,
357358
task,
358359
annotations,
359360
imagePromises,
360361
changes
361362
) {
362-
const xref = evaluator.xref;
363363
let baseFontRef;
364364
const promises = [];
365365
const { isOffscreenCanvasSupported } = evaluator.options;

src/core/document.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ class Page {
151151
});
152152
}
153153

154+
createAnnotationEvaluator(handler) {
155+
return this.#createPartialEvaluator(handler);
156+
}
157+
154158
#getInheritableProperty(key, getArray = false) {
155159
const value = getInheritableProperty({
156160
dict: this.pageDict,
@@ -386,6 +390,7 @@ class Page {
386390
);
387391
const newData = await AnnotationFactory.saveNewAnnotations(
388392
partialEvaluator,
393+
this.xref,
389394
task,
390395
annotations,
391396
imagePromises,

src/core/editor/pdf_editor.js

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,21 @@
1616
/** @typedef {import("../document.js").PDFDocument} PDFDocument */
1717
/** @typedef {import("../document.js").Page} Page */
1818
/** @typedef {import("../xref.js").XRef} XRef */
19+
/** @typedef {import("../worker.js").WorkerTask} WorkerTask */
20+
// eslint-disable-next-line max-len
21+
/** @typedef {import("../../shared/message_handler.js").MessageHandler} MessageHandler */
1922

2023
import {
2124
deepCompare,
2225
getInheritableProperty,
26+
getNewAnnotationsMap,
2327
stringToAsciiOrUTF16BE,
2428
} from "../core_utils.js";
2529
import { Dict, isName, Name, Ref, RefSet, RefSetCache } from "../primitives.js";
2630
import { getModificationDate, stringToPDFString } from "../../shared/util.js";
2731
import { incrementalUpdate, writeValue } from "../writer.js";
2832
import { NameTree, NumberTree } from "../name_number_tree.js";
33+
import { AnnotationFactory } from "../annotation.js";
2934
import { BaseStream } from "../base_stream.js";
3035
import { StringStream } from "../stream.js";
3136

@@ -75,8 +80,9 @@ class DocumentData {
7580
}
7681

7782
class XRefWrapper {
78-
constructor(entries) {
83+
constructor(entries, getNewRef) {
7984
this.entries = entries;
85+
this._getNewRef = getNewRef;
8086
}
8187

8288
fetch(ref) {
@@ -94,11 +100,17 @@ class XRefWrapper {
94100
fetchAsync(ref) {
95101
return Promise.resolve(this.fetch(ref));
96102
}
103+
104+
getNewTemporaryRef() {
105+
return this._getNewRef();
106+
}
97107
}
98108

99109
class PDFEditor {
100110
hasSingleFile = false;
101111

112+
#newAnnotationsParams = null;
113+
102114
currentDocument = null;
103115

104116
oldPages = [];
@@ -107,7 +119,7 @@ class PDFEditor {
107119

108120
xref = [null];
109121

110-
xrefWrapper = new XRefWrapper(this.xref);
122+
xrefWrapper = new XRefWrapper(this.xref, () => this.newRef);
111123

112124
newRefCount = 1;
113125

@@ -535,13 +547,33 @@ class PDFEditor {
535547
/**
536548
* Extract pages from the given documents.
537549
* @param {Array<PageInfo>} pageInfos
550+
* @param {Object} annotationStorage - The annotation storage containing the
551+
* annotations to be merged into the new document.
552+
* @param {MessageHandler} handler - The message handler to use for processing
553+
* the annotations.
554+
* @param {WorkerTask} task - The worker task to use for reporting progress
555+
* and cancellation.
538556
* @return {Promise<void>}
539557
*/
540-
async extractPages(pageInfos) {
558+
async extractPages(pageInfos, annotationStorage, handler, task) {
541559
const promises = [];
542560
let newIndex = 0;
543561
this.hasSingleFile = pageInfos.length === 1;
544562
const allDocumentData = [];
563+
564+
if (annotationStorage) {
565+
this.#newAnnotationsParams = {
566+
handler,
567+
task,
568+
newAnnotationsByPage: getNewAnnotationsMap(annotationStorage),
569+
imagesPromises: AnnotationFactory.generateImages(
570+
annotationStorage.values(),
571+
this.xrefWrapper,
572+
true
573+
),
574+
};
575+
}
576+
545577
for (const {
546578
document,
547579
includePages,
@@ -1930,16 +1962,44 @@ class PDFEditor {
19301962
await this.#collectDependencies(resources, true, xref)
19311963
);
19321964

1965+
let newAnnots = null;
1966+
19331967
if (annotations) {
19341968
const newAnnotations = await this.#collectDependencies(
19351969
annotations,
19361970
true,
19371971
xref
19381972
);
19391973
this.#fixNamedDestinations(newAnnotations, dedupNamedDestinations);
1940-
pageDict.setIfArray("Annots", newAnnotations);
1974+
if (Array.isArray(newAnnotations) && newAnnotations.length > 0) {
1975+
newAnnots = newAnnotations;
1976+
}
1977+
}
1978+
1979+
const newAnnotations =
1980+
this.#newAnnotationsParams?.newAnnotationsByPage.get(pageIndex);
1981+
if (newAnnotations) {
1982+
const { handler, task, imagesPromises } = this.#newAnnotationsParams;
1983+
const changes = new RefSetCache();
1984+
const newData = await AnnotationFactory.saveNewAnnotations(
1985+
page.createAnnotationEvaluator(handler),
1986+
this.xrefWrapper,
1987+
task,
1988+
newAnnotations,
1989+
imagesPromises,
1990+
changes
1991+
);
1992+
for (const [ref, { data }] of changes.items()) {
1993+
this.xref[ref.num] = data;
1994+
}
1995+
newAnnots ||= [];
1996+
for (const { ref } of newData.annotations) {
1997+
newAnnots.push(ref);
1998+
}
19411999
}
19422000

2001+
pageDict.setIfArray("Annots", newAnnots);
2002+
19432003
if (this.useObjectStreams) {
19442004
const newLastRef = this.newRefCount;
19452005
const pageObjectRefs = [];

src/core/worker.js

Lines changed: 95 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -551,96 +551,111 @@ class WorkerMessageHandler {
551551
return pdfManager.ensureDoc("calculationOrderIds");
552552
});
553553

554-
handler.on("ExtractPages", async function ({ pageInfos }) {
555-
if (!pageInfos) {
556-
warn("extractPages: nothing to extract.");
557-
return null;
558-
}
559-
if (!Array.isArray(pageInfos)) {
560-
pageInfos = [pageInfos];
561-
}
562-
let newDocumentId = 0;
563-
for (const pageInfo of pageInfos) {
564-
if (pageInfo.document === null) {
565-
pageInfo.document = pdfManager.pdfDocument;
566-
} else if (ArrayBuffer.isView(pageInfo.document)) {
567-
const manager = new LocalPdfManager({
568-
source: pageInfo.document,
569-
docId: `${docId}_extractPages_${newDocumentId++}`,
570-
handler,
571-
password: pageInfo.password ?? null,
572-
evaluatorOptions: Object.assign({}, pdfManager.evaluatorOptions),
573-
});
574-
let recoveryMode = false;
575-
let isValid = true;
576-
while (true) {
577-
try {
578-
await manager.requestLoadedStream();
579-
await manager.ensureDoc("checkHeader");
580-
await manager.ensureDoc("parseStartXRef");
581-
await manager.ensureDoc("parse", [recoveryMode]);
582-
break;
583-
} catch (e) {
584-
if (e instanceof XRefParseException) {
585-
if (recoveryMode === false) {
586-
recoveryMode = true;
587-
continue;
554+
handler.on(
555+
"ExtractPages",
556+
async function ({ pageInfos, annotationStorage }) {
557+
if (!pageInfos) {
558+
warn("extractPages: nothing to extract.");
559+
return null;
560+
}
561+
if (!Array.isArray(pageInfos)) {
562+
pageInfos = [pageInfos];
563+
}
564+
let newDocumentId = 0;
565+
for (const pageInfo of pageInfos) {
566+
if (pageInfo.document === null) {
567+
pageInfo.document = pdfManager.pdfDocument;
568+
} else if (ArrayBuffer.isView(pageInfo.document)) {
569+
const manager = new LocalPdfManager({
570+
source: pageInfo.document,
571+
docId: `${docId}_extractPages_${newDocumentId++}`,
572+
handler,
573+
password: pageInfo.password ?? null,
574+
evaluatorOptions: Object.assign({}, pdfManager.evaluatorOptions),
575+
});
576+
let recoveryMode = false;
577+
let isValid = true;
578+
while (true) {
579+
try {
580+
await manager.requestLoadedStream();
581+
await manager.ensureDoc("checkHeader");
582+
await manager.ensureDoc("parseStartXRef");
583+
await manager.ensureDoc("parse", [recoveryMode]);
584+
break;
585+
} catch (e) {
586+
if (e instanceof XRefParseException) {
587+
if (recoveryMode === false) {
588+
recoveryMode = true;
589+
continue;
590+
} else {
591+
isValid = false;
592+
warn("extractPages: XRefParseException.");
593+
}
594+
} else if (e instanceof PasswordException) {
595+
const task = new WorkerTask(
596+
`PasswordException: response ${e.code}`
597+
);
598+
599+
startWorkerTask(task);
600+
601+
try {
602+
const { password } = await handler.sendWithPromise(
603+
"PasswordRequest",
604+
e
605+
);
606+
manager.updatePassword(password);
607+
} catch {
608+
isValid = false;
609+
warn("extractPages: invalid password.");
610+
} finally {
611+
finishWorkerTask(task);
612+
}
588613
} else {
589614
isValid = false;
590-
warn("extractPages: XRefParseException.");
615+
warn("extractPages: invalid document.");
591616
}
592-
} else if (e instanceof PasswordException) {
593-
const task = new WorkerTask(
594-
`PasswordException: response ${e.code}`
595-
);
596-
597-
startWorkerTask(task);
598-
599-
try {
600-
const { password } = await handler.sendWithPromise(
601-
"PasswordRequest",
602-
e
603-
);
604-
manager.updatePassword(password);
605-
} catch {
606-
isValid = false;
607-
warn("extractPages: invalid password.");
608-
} finally {
609-
finishWorkerTask(task);
617+
if (!isValid) {
618+
break;
610619
}
611-
} else {
612-
isValid = false;
613-
warn("extractPages: invalid document.");
614-
}
615-
if (!isValid) {
616-
break;
617620
}
618621
}
619-
}
620-
if (!isValid) {
621-
pageInfo.document = null;
622-
}
623-
const isPureXfa = await manager.ensureDoc("isPureXfa");
624-
if (isPureXfa) {
625-
pageInfo.document = null;
626-
warn("extractPages does not support pure XFA documents.");
622+
if (!isValid) {
623+
pageInfo.document = null;
624+
}
625+
const isPureXfa = await manager.ensureDoc("isPureXfa");
626+
if (isPureXfa) {
627+
pageInfo.document = null;
628+
warn("extractPages does not support pure XFA documents.");
629+
} else {
630+
pageInfo.document = manager.pdfDocument;
631+
}
627632
} else {
628-
pageInfo.document = manager.pdfDocument;
633+
warn("extractPages: invalid document.");
634+
}
635+
}
636+
let task;
637+
try {
638+
const pdfEditor = new PDFEditor();
639+
task = new WorkerTask(`ExtractPages: ${pageInfos.length} page(s)`);
640+
startWorkerTask(task);
641+
const buffer = await pdfEditor.extractPages(
642+
pageInfos,
643+
annotationStorage,
644+
handler,
645+
task
646+
);
647+
return buffer;
648+
} catch (reason) {
649+
// eslint-disable-next-line no-console
650+
console.error(reason);
651+
return null;
652+
} finally {
653+
if (task) {
654+
finishWorkerTask(task);
629655
}
630-
} else {
631-
warn("extractPages: invalid document.");
632656
}
633657
}
634-
try {
635-
const pdfEditor = new PDFEditor();
636-
const buffer = await pdfEditor.extractPages(pageInfos);
637-
return buffer;
638-
} catch (reason) {
639-
// eslint-disable-next-line no-console
640-
console.error(reason);
641-
return null;
642-
}
643-
});
658+
);
644659

645660
handler.on(
646661
"SaveDocument",

src/display/api.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2973,7 +2973,20 @@ class WorkerTransport {
29732973
}
29742974

29752975
extractPages(pageInfos) {
2976-
return this.messageHandler.sendWithPromise("ExtractPages", { pageInfos });
2976+
const params = {
2977+
pageInfos,
2978+
};
2979+
let transfer;
2980+
if (this.annotationStorage.size > 0) {
2981+
const { map, transfer: t } = this.annotationStorage.serializable;
2982+
params.annotationStorage = map;
2983+
transfer = t;
2984+
}
2985+
return this.messageHandler
2986+
.sendWithPromise("ExtractPages", params, transfer)
2987+
.finally(() => {
2988+
this.annotationStorage.resetModified();
2989+
});
29772990
}
29782991

29792992
getPage(pageNumber) {

0 commit comments

Comments
 (0)