Skip to content

Commit 16aee06

Browse files
authored
Merge pull request #20925 from calixteman/reorganize_save_annotations
Add the possibility to save added annotations when reorganizing a pdf (bug 2023086)
2 parents 504505b + 04272de commit 16aee06

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

@@ -537,13 +549,33 @@ class PDFEditor {
537549
/**
538550
* Extract pages from the given documents.
539551
* @param {Array<PageInfo>} pageInfos
552+
* @param {Object} annotationStorage - The annotation storage containing the
553+
* annotations to be merged into the new document.
554+
* @param {MessageHandler} handler - The message handler to use for processing
555+
* the annotations.
556+
* @param {WorkerTask} task - The worker task to use for reporting progress
557+
* and cancellation.
540558
* @return {Promise<void>}
541559
*/
542-
async extractPages(pageInfos) {
560+
async extractPages(pageInfos, annotationStorage, handler, task) {
543561
const promises = [];
544562
let newIndex = 0;
545563
this.hasSingleFile = pageInfos.length === 1;
546564
const allDocumentData = [];
565+
566+
if (annotationStorage) {
567+
this.#newAnnotationsParams = {
568+
handler,
569+
task,
570+
newAnnotationsByPage: getNewAnnotationsMap(annotationStorage),
571+
imagesPromises: AnnotationFactory.generateImages(
572+
annotationStorage.values(),
573+
this.xrefWrapper,
574+
true
575+
),
576+
};
577+
}
578+
547579
for (const {
548580
document,
549581
includePages,
@@ -1932,16 +1964,44 @@ class PDFEditor {
19321964
await this.#collectDependencies(resources, true, xref)
19331965
);
19341966

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

2003+
pageDict.setIfArray("Annots", newAnnots);
2004+
19452005
if (this.useObjectStreams) {
19462006
const newLastRef = this.newRefCount;
19472007
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)